Custom Dashboard for AdminJS not working in production

1.2k Views Asked by At

I have a Koa nodejs server which I added AdminJS to and it's working beautifully locally. My goal is to override the Dashboard component. I did so successfully when not running in production. However when I run in production mode (NODE_ENV=production node ./dist/server.js) it fails silently.

  const componentLoader = new ComponentLoader();
  const Components = {
    Dashboard: componentLoader.add("Dashboard", "./admin/dashboard"),
  };
  const admin = new AdminJS({
    componentLoader,
    dashboard: {
      component: Components.Dashboard,
    }
  });

My dashboard.tsx file is in src/admin/ and admin is a folder on the same level as src/server.ts. Also, my componentLoader when I inspect it is showing the correct filePath that ends with dist/admin/dashboard

Also, when I check dist/admin/dashboard.js I see my React code. So my tsconfig seems to be correct and the dashboard.tsx has a default export.

What confuses me is when I run nodemon --watch src --exec node -r esbuild-register src/server.ts is works correctly so it seems in general I have things hooked up correctly.

Lastly, here's my tsconfig.json.

{
    "$schema": "https://json.schemastore.org/tsconfig",
    "compilerOptions": {
        "jsx": "react",
        "lib": [
            "es6"
        ],
        "target": "es2017",
        "module": "commonjs",
        "esModuleInterop": true,
        "resolveJsonModule": true,
        "strict": true,
        "allowSyntheticDefaultImports": true,
        "noImplicitAny": true,
        "allowJs": false,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "noImplicitReturns": true,
        "strictNullChecks": true,
        "moduleResolution": "node",
        "inlineSources": true,
        "sourceRoot": "/",
        "sourceMap": true,
        "isolatedModules": true,
        "outDir": "./dist",
        "rootDir": "./src",
        "composite": true,
        "baseUrl": ".",
        "paths": {
            "src/*": [
                "src/*"
            ]
        }
    },
    "exclude": [
        "node_modules",
        "./node_modules/*"
    ],
    "files": [
        "./src/server.ts"
    ],
    "include": [
        "./src/**/*",
        "./src/*"
    ]
}

UPDATE: I did notice that the components.bundle.js file was missing when navigating to my adminjs dashboard. Since I am using GCP App Engine, I know that that file will not able to be built and saved on the fly in the file system so I have integrated @adminjs/bundler which creates the missing files. However the piece I still haven't put together is how to integrate it into the build pipeline (in particular I'm not sure what the destination of the components.bundle.js should be).

2

There are 2 best solutions below

0
On

Before I explain my solution here are a few pieces of context:

  • When using NODE_ENV=production, adminjs does a few things differently, in particular the components.bundle.js file gets served differently. In production, it looks for the file at ./.adminjs/bundle.js
  • That's when the bundler comes in (which is necessary anyway for certain cloud environments like GCP App Engine). You have to create your own components.bundler.js file which they have a tool for.

First, I created a file which bundles the frontend components. I have not tried doing that with the ComponentLoader so I wouldn't need duplicate code yet, but here's what I know for certain works:

import AdminJS, { OverridableComponent } from "adminjs";  

const bundle = (path: string, componentName: string) =>
  AdminJS.bundle(`./${path}`, componentName as OverridableComponent);

export const DashboardComponent = bundle("../src/dashboard", "Dashboard");

I believe if I were to create a file which creates the ComponentLoader and adds the components that it would be equivalent (it would export the Components and the componentLoader for use by the AdminJS configuration).

Note ../src/dashboard is simply the location of the dashboard.tsx file I chose. And Dashboard is the name of the component.

Then, I created a script which uses @adminjs/bundler to actually create the bundles. (I named it bundler.ts).

import { bundle } from "@adminjs/bundler";

/**
 * yarn admin:bundle invokes this script.
 * This file is used to bundle AdminJS files. It is used at compile time
 * to generate the frontend component bundles that are used in AdminJS.
 */
void (async () => {
  await bundle({
    customComponentsInitializationFilePath: "./components.ts",
    destinationDir: "./.adminjs",
  });
})();

I added a script to my package.json which does the following:

ts-node ./bundler.ts && mv ./.adminjs/components.bundle.js ./.adminjs/bundle.js

Now, when I run this script (which I do when I run before doing node ./dist/server.js), the adminjs router is going to be able to find the previously missing file.

Note that when running your server you'll also want to make sure you set ADMIN_JS_SKIP_BUNDLE='true'.

I hope this helps the next person. I also do hope some documentation and better tooling is on its way. This is kind of messy but solved my issue for now.

0
On

I'm using NestJS and ended up doing something similar. I added the build script below but still feels very hacky.

import AdminJS from 'adminjs';
import {componentLoader, dashboardConfig} from './components/components';

process.env['NODE_ENV'] = 'production';
const admin             = new AdminJS({
    componentLoader: componentLoader,
    dashboard      : dashboardConfig
});
admin.initialize().then(() => process.exit(0));