Source

adminjs-bundler/src/bundle.ts

import { promises as fs } from 'fs';
import { join , parse} from 'path';
import AdminJS, { AdminJSOptions } from 'adminjs';

process.env.ADMIN_JS_SKIP_BUNDLE = 'false';
process.env.NODE_ENV = 'production';

const ADMINJS_LOCAL_DIR_PATH = '.adminjs';
const ADMINJS_ASSETS_DIR_PATH = 'node_modules/adminjs/lib/frontend/assets/scripts';
const DESIGN_SYSTEM_DIR_PATH = 'node_modules/@adminjs/design-system';

/**
 * Options for the bundler
 *
 * @memberof module:@adminjs/bundler
 * @alias BundleConfig
 */
export type BundleConfig = {
  /**
   * File path where the bundled files should be moved into.
   *
   * The path is relative to where you run the script.
   */
  destinationDir: string;
  /**
   * File path where custom components are bundled. If you have
   * custom components in your project, they must be bundled in one single file.
   * Please look at the example in the repository.
   *
   * The path is relative to where you run the script.
   */
  customComponentsInitializationFilePath: string;
  /**
   * File path where AdminJS entry files are generated.
   *
   * This defaults to '.adminjs'.
   * Set this only if you know what you're doing.
   *
   * The path is relative to where you run the script.
   */
  adminJsLocalDir?: string;
  /**
   * File path where AdminJS standard bundle files are located.
   *
   * This defaults to 'node_modules/adminjs/lib/frontend/assets/scripts'.
   * Set this only if you know what you're doing.
   *
   * The path is relative to where you run the script.
   */
  adminJsAssetsDir?: string;
  /**
   * File path where AdminJS design system bundle files are located.
   *
   * This defaults to 'node_modules/@adminjs/design-system'.
   * Set this only if you know what you're doing.
   *
   * The path is relative to where you run the script.
   */
  designSystemDir?: string;
  /**
   * You can pass your AdminJS Options config in case you're using external
   * packages with custom components. It's enough to include only `resources` section.
   */
  adminJsOptions?: AdminJSOptions;
  /**
   * You can define "versioning" if you want your assets to be versioned, e. g.
   * 'app.bundle.123456.js'. Please note that this requires AdminJS version >= 5.8.0
   *
   * This will generate a JSON manifest file under specified path (relative to where you run the command).
   *
   * The generated file should be linked to `assets.coreScripts` in your
   * AdminJS options object.
   */
  versioning?: AssetsVersioning;
};

/**
 * Versioning configuration
 *
 * @memberof module:@adminjs/bundler
 * @alias AssetsVersioning
 */
export type AssetsVersioning = {
  /**
   * Path where you would like your AdminJS assets-manifest file to be saved.
   */
  manifestPath: string;
}

/**
 * AdminJS file config
 *
 * @memberof module:@adminjs/bundler
 * @alias BundleFile
 */
export type BundleFile = {
  /**
   * A file name.
   */
  name: string;
  /**
   * A source path where the original file can be found.
   */
  sourcePath: string;
  /**
   * A destination path where new bundle file is copied into.
   */
  destinationPath: string;
};

const getDestinationPath = (
  asset: string,
  timestamp?: number | null,
): string => {
  if (!timestamp) return asset;

  const { ext, name } = parse(asset);

  return `${name}.${timestamp}${ext}`;
};

const createAssetsManifest = (files: BundleFile[]): string => {
  const coreScripts = files.reduce((memo, { destinationPath, name }) => {
    memo[name] = parse(destinationPath).base;

    return memo;
  }, {});

  return JSON.stringify(coreScripts);
};

/**
 * Bundles AdminJS javascript browser files. This is an alternative to bundling those files on server startup.
 * The bundled files are stored in "destinationDir". Afterwards, you can for example:
 * 1. Upload those files to a public storage bucket and tell AdminJS to use files from there:
 * ```javascript
 *   const adminJs = new AdminJS({ assetsCDN: <your storage bucket url> })
 * ```
 * 2. Serve the "destinationDir" as a public folder, using i. e. express.static:
 * ```javascript
 *   app.use(express.static(destinationDir));
 *   ...
 *   const adminJs = new AdminJS({ assetsCDN: <your server's url> })
 * ```
 *
 * IMPORTANT: To prevent AdminJS from attempting to generate a new bundle on server startup,
 * you must set `ADMIN_JS_SKIP_BUNDLE="true"` environment variable!
 *
 *
 * @param {BundleConfig} options
 * @memberof module:@adminjs/bundler
 * @method
 * @name bundle
 * @example
 * import { bundle } from '../../src';
 *
 * (async () => {
 *   const files = await bundle({
 *     customComponentsInitializationFilePath: 'src/components/index.ts',
 *     destinationDir: 'src/public',
 *   });
 *
 *   console.log(files);
 *   // do something with built files here
 * })();
 *
 */
const bundle = async ({
  destinationDir,
  customComponentsInitializationFilePath,
  adminJsLocalDir = ADMINJS_LOCAL_DIR_PATH,
  adminJsAssetsDir = ADMINJS_ASSETS_DIR_PATH,
  designSystemDir = DESIGN_SYSTEM_DIR_PATH,
  adminJsOptions = {},
  versioning,
}: BundleConfig): Promise<BundleFile[]> => {
  await import(join(process.cwd(), customComponentsInitializationFilePath));

  const timestamp = versioning?.manifestPath ? Date.now() : null;
  await fs.mkdir(join(process.cwd(), destinationDir), { recursive: true });
  const files = [
    {
      name: 'components.bundle.js',
      sourcePath: join(process.cwd(), `${adminJsLocalDir}/bundle.js`),
      destinationPath: join(process.cwd(), destinationDir, getDestinationPath('components.bundle.js', timestamp))
    },
    {
      name: 'app.bundle.js',
      sourcePath: join(process.cwd(), `${adminJsAssetsDir}/app-bundle.production.js`),
      destinationPath: join(process.cwd(), destinationDir, getDestinationPath('app.bundle.js', timestamp)),
    },
    {
      name: 'global.bundle.js',
      sourcePath: join(process.cwd(), `${adminJsAssetsDir}/global-bundle.production.js`),
      destinationPath: join(process.cwd(), destinationDir, getDestinationPath('global.bundle.js', timestamp))
    },
    {
      name: 'design-system.bundle.js',
      sourcePath: join(process.cwd(), `${designSystemDir}/bundle.production.js`),
      destinationPath: join(process.cwd(), destinationDir, getDestinationPath('design-system.bundle.js', timestamp))
    },
  ];

  const [ customComponentsBundle, ...standardBundles ] = files;

  await Promise.all(standardBundles.map(({ sourcePath, destinationPath }) => fs.copyFile(
    sourcePath,
    destinationPath,
  )));

  await new AdminJS(adminJsOptions).initialize();
  await fs.rename(
    customComponentsBundle.sourcePath,
    customComponentsBundle.destinationPath,
  );

  if (versioning?.manifestPath) {
    const manifestContents = createAssetsManifest(files);
    const { ext } = parse(versioning?.manifestPath);

    if (ext !== '.json') {
      await Promise.all(files.map(({ destinationPath }) => fs.unlink(destinationPath)));
      throw new Error('Invalid "versioning.manifestPath". File name must have .json extension.');
    }

    await fs.writeFile(
      join(process.cwd(), versioning.manifestPath),
      manifestContents,
    );
  }

  console.log(`✨ Successfully built AdminJS bundle files! ✨`);

  return files;
};

export default bundle;