Executando verificação de segurança...
1

Script para fazer o bundle automaticamente do AdminJS caso você esteja utilizando em uma aplicação serverless [typescript] [adminjs] [aws-s3]

Contexto

Recentemente enfrentei um desafio no projeto em que trabalho no meu emprego. Eu precisava desenvolver um painel (ou dashboard) de admin em um período de tempo relativamente pequeno. Inicialmente pensei em construir do zero mesmo, com react e uma api em node dedicada pra isso. Porém, um colega de trabalho me apresentou o adminjs (antigo admin-bro) que basicamente um template de painel de admin para aplicações. Dentro dele existem diversas coisas pré configuradas que realmente adiantam bastante o processo.

Porém o grande problema é que a API desse projeto estava em serverless e essa biblioteca gera o bundle em tempo de execução. Então ao executar o projeto local para desenvolver vi que não funcionava pois ele não conseguia acessar esses arquivos estáticos gerados pelo módulo. Então fui consultar a documentação (ao invés de pesquisar no google). Verifiquei que eles recomendavam que para aplicações serverless a melhor opção seria disponibilizá-los estáticamente via CDN por meio de alguma plataforma (aws, google cloud, etc...). Nessa parte eles deixaram claro quais scripts deveriam ser disponibilizados, e eles ficavam bem profundos na estrutura de pastas do módulo. Eles finalizam dando um exemplo incompleto de como fazer esse bundle por um script e tive que quebrar um pouco a cabeça para desenvolver um que atendesse todas as minhas necessidades.

Solução

Consegui desenvolver uma solução em apenas um arquivo de script e gostaria de compartilhar aqui para ajudar alguém caso passe para a mesma situação. Não sou muito experiente mas acredito que mesmo para os veteranos isso pode poupar uma ou duas horas desse problema.

Basicamente o script copia os scripts necessários para uma pasta public/bundle (assumindo que a pasta com os arquivos estáticos a serem disponibilizados estejam dentro da pasta public). Feito isso ele inicializa o painel para gerar o último script necessário e copia ele pra mesma pasta.
Após gerar os scripts e armazená-los na pasta especificada acima, no caso do meu projeto, já existia um bucket s3 na aws que servia o frontend da aplicação, então apenas criei uma pasta public dentro desse bucket e fiz o upload dos arquivos dentro dessa pasta.

Para finalizar basta apenas copiar o link do arquivo que foi gerado no bucket e configurar na sua aplicação. Existe uma configuração dentro de AdminJSOptions dedicada para isso que tem o nome de assetsCDN, basta colocar o link do diretório nessa opção e a aplicação irá funcionar. Além disso existem outras opções que utilizam links CDN como o logo ou o icon da aplicação.

Código

Pacotes necessários
npm i adminjs aws-sdk dotenv

⚠ Os pacotes fs e glob são nativos do node.

import * as fs from 'fs'
import AdminJS from 'adminjs'
import { AdminOptions } from './config'
import { S3 } from 'aws-sdk'
import glob from 'glob'
import dotenv from 'dotenv'
dotenv.config()

const errCallback = (err: Error | null) => {
  if (err) {
    throw new Error(err.message)
  }
}

const s3 = new S3({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
})

const uploadFile = (fileName: string) => {
  if (fileName === './admin/public/bundle' || fileName === './admin/public/fonts') return // skip directory
  const fileContent = fs.readFileSync(fileName)
  const params: S3.PutObjectRequest = {
    Bucket: process.env.AWS_BUCKET as string,
    Key: fileName.slice(2, fileName.length),
    Body: fileContent
  }

  s3.upload(params, (err, data) => {
    if (err) {
      console.log(`Error uploading ${fileName}:`, err)
    } else {
      console.log(`Successfully uploaded ${fileName} to ${data.Location}`)
    }
  })
}

const bundle = async () => {
  try {
    const temp = process.env.NODE_ENV
    process.env.NODE_ENV = 'production'

    const admin = new AdminJS(AdminOptions)

    const publicDir = './admin/public'
    if (!fs.existsSync(publicDir)) {
      fs.mkdirSync(publicDir)
    }
    fs.copyFile(
      './node_modules/adminjs/lib/frontend/assets/scripts/app-bundle.production.js',
      './admin/public/bundle/app.bundle.js',
      errCallback
    )
    fs.copyFile(
      './node_modules/adminjs/lib/frontend/assets/scripts/global-bundle.production.js',
      './admin/public/bundle/global.bundle.js',
      errCallback
    )
    fs.copyFile(
      './node_modules/@adminjs/design-system/bundle.production.js',
      './admin/public/bundle/design-system.bundle.js',
      errCallback
    )
    await admin.initialize()
    fs.rename('./.adminjs/bundle.js', './admin/public/bundle/components.bundle.js', errCallback)

    console.log('Uploading files to S3... 🚀')
    glob('./admin/public/**/*', (err, files) => {
      if (err) {
        console.log('Error reading directory:', err)
      } else {
        files.forEach((filename) => { uploadFile(filename) })
      }
    })
    console.log('Done! 🎉')
    process.env.NODE_ENV = temp
  } catch (e) {
    console.error(e.message)
  }
}

bundle()

Créditos

Gabriel Fachini - Github

Tags

Typescript, aws, s3, adminjs, assets, cdn

Carregando publicação patrocinada...
1

Oi Gabriel, tudo bom?
Estou precisando fazer algo parecido, porém, preciso rodar o AdminJs dentro de um container em produção. Sendo assim, toda vez que executo o adminjs com NODE_ENV=production, o container para de funcinoar depois de algum tempo rodando:

manager  | query: SELECT * FROM current_schema()
manager  | query: SELECT version();
manager  | query: SELECT * FROM current_schema()
manager  | query: SELECT version();
manager  | query: SELECT * FROM current_schema()
manager  | query: SELECT version();
manager  | query: SELECT * FROM current_schema()
manager  | query: SELECT version();
manager  | AdminJS: bundling user components...
manager  | AdminJS: bundling user components...
manager  | AdminJS available at http://localhost:3000/admin
manager  |  ELIFECYCLE  Command failed.

Estou usando componentes customizados dessa forma:

export rt const generateAdminJSConfig: () => AdminJSOptions = () => {
  return {
    branding,
    componentLoader, <-- Quando eu coloco esta linha, o container quebra. Sem ela, funciona normalmente.
    components: {
      dashboard: DEFAULT_DASHBOARD,
    },
   ...
FROM node:18-alpine

ARG NPM_TOKEN

# Definir o diretório de trabalho
WORKDIR /usr/src/app

# Copiar os arquivos de configuração
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./

# Copiar o package.json do pacote
COPY packages/manager/package.json ./packages/manager/
COPY tsconfig*.json ./
COPY packages/manager/tsconfig*.json ./packages/manager/

# Instalar pnpm
RUN npm install -g pnpm

# Instalar todas as dependências, incluindo devDependencies
RUN pnpm --filter . --filter ./packages/manager... install

# Copiar o código do pacote
COPY packages/manager ./packages/manager

# Compilar o código do pacote
RUN pnpm --filter ./packages/manager... build

# Definir o diretório de trabalho para o pacote
WORKDIR /usr/src/app/packages/manager

# Expor a porta da aplicação
EXPOSE 3000

# Comando para iniciar a aplicação em modo de desenvolvimento
CMD ["pnpm", "run", "start"]

O que me faz pensar que o problema esta na forma que estou importando componentes custom.