Source

adminjs-bundler/src/bundle.ts

  1. import { promises as fs } from 'fs';
  2. import { join , parse} from 'path';
  3. import AdminJS, { AdminJSOptions } from 'adminjs';
  4. process.env.ADMIN_JS_SKIP_BUNDLE = 'false';
  5. process.env.NODE_ENV = 'production';
  6. const ADMINJS_LOCAL_DIR_PATH = '.adminjs';
  7. const ADMINJS_ASSETS_DIR_PATH = 'node_modules/adminjs/lib/frontend/assets/scripts';
  8. const DESIGN_SYSTEM_DIR_PATH = 'node_modules/@adminjs/design-system';
  9. /**
  10. * Options for the bundler
  11. *
  12. * @memberof module:@adminjs/bundler
  13. * @alias BundleConfig
  14. */
  15. export type BundleConfig = {
  16. /**
  17. * File path where the bundled files should be moved into.
  18. *
  19. * The path is relative to where you run the script.
  20. */
  21. destinationDir: string;
  22. /**
  23. * File path where custom components are bundled. If you have
  24. * custom components in your project, they must be bundled in one single file.
  25. * Please look at the example in the repository.
  26. *
  27. * The path is relative to where you run the script.
  28. */
  29. customComponentsInitializationFilePath: string;
  30. /**
  31. * File path where AdminJS entry files are generated.
  32. *
  33. * This defaults to '.adminjs'.
  34. * Set this only if you know what you're doing.
  35. *
  36. * The path is relative to where you run the script.
  37. */
  38. adminJsLocalDir?: string;
  39. /**
  40. * File path where AdminJS standard bundle files are located.
  41. *
  42. * This defaults to 'node_modules/adminjs/lib/frontend/assets/scripts'.
  43. * Set this only if you know what you're doing.
  44. *
  45. * The path is relative to where you run the script.
  46. */
  47. adminJsAssetsDir?: string;
  48. /**
  49. * File path where AdminJS design system bundle files are located.
  50. *
  51. * This defaults to 'node_modules/@adminjs/design-system'.
  52. * Set this only if you know what you're doing.
  53. *
  54. * The path is relative to where you run the script.
  55. */
  56. designSystemDir?: string;
  57. /**
  58. * You can pass your AdminJS Options config in case you're using external
  59. * packages with custom components. It's enough to include only `resources` section.
  60. */
  61. adminJsOptions?: AdminJSOptions;
  62. /**
  63. * You can define "versioning" if you want your assets to be versioned, e. g.
  64. * 'app.bundle.123456.js'. Please note that this requires AdminJS version >= 5.8.0
  65. *
  66. * This will generate a JSON manifest file under specified path (relative to where you run the command).
  67. *
  68. * The generated file should be linked to `assets.coreScripts` in your
  69. * AdminJS options object.
  70. */
  71. versioning?: AssetsVersioning;
  72. };
  73. /**
  74. * Versioning configuration
  75. *
  76. * @memberof module:@adminjs/bundler
  77. * @alias AssetsVersioning
  78. */
  79. export type AssetsVersioning = {
  80. /**
  81. * Path where you would like your AdminJS assets-manifest file to be saved.
  82. */
  83. manifestPath: string;
  84. }
  85. /**
  86. * AdminJS file config
  87. *
  88. * @memberof module:@adminjs/bundler
  89. * @alias BundleFile
  90. */
  91. export type BundleFile = {
  92. /**
  93. * A file name.
  94. */
  95. name: string;
  96. /**
  97. * A source path where the original file can be found.
  98. */
  99. sourcePath: string;
  100. /**
  101. * A destination path where new bundle file is copied into.
  102. */
  103. destinationPath: string;
  104. };
  105. const getDestinationPath = (
  106. asset: string,
  107. timestamp?: number | null,
  108. ): string => {
  109. if (!timestamp) return asset;
  110. const { ext, name } = parse(asset);
  111. return `${name}.${timestamp}${ext}`;
  112. };
  113. const createAssetsManifest = (files: BundleFile[]): string => {
  114. const coreScripts = files.reduce((memo, { destinationPath, name }) => {
  115. memo[name] = parse(destinationPath).base;
  116. return memo;
  117. }, {});
  118. return JSON.stringify(coreScripts);
  119. };
  120. /**
  121. * Bundles AdminJS javascript browser files. This is an alternative to bundling those files on server startup.
  122. * The bundled files are stored in "destinationDir". Afterwards, you can for example:
  123. * 1. Upload those files to a public storage bucket and tell AdminJS to use files from there:
  124. * ```javascript
  125. * const adminJs = new AdminJS({ assetsCDN: <your storage bucket url> })
  126. * ```
  127. * 2. Serve the "destinationDir" as a public folder, using i. e. express.static:
  128. * ```javascript
  129. * app.use(express.static(destinationDir));
  130. * ...
  131. * const adminJs = new AdminJS({ assetsCDN: <your server's url> })
  132. * ```
  133. *
  134. * IMPORTANT: To prevent AdminJS from attempting to generate a new bundle on server startup,
  135. * you must set `ADMIN_JS_SKIP_BUNDLE="true"` environment variable!
  136. *
  137. *
  138. * @param {BundleConfig} options
  139. * @memberof module:@adminjs/bundler
  140. * @method
  141. * @name bundle
  142. * @example
  143. * import { bundle } from '../../src';
  144. *
  145. * (async () => {
  146. * const files = await bundle({
  147. * customComponentsInitializationFilePath: 'src/components/index.ts',
  148. * destinationDir: 'src/public',
  149. * });
  150. *
  151. * console.log(files);
  152. * // do something with built files here
  153. * })();
  154. *
  155. */
  156. const bundle = async ({
  157. destinationDir,
  158. customComponentsInitializationFilePath,
  159. adminJsLocalDir = ADMINJS_LOCAL_DIR_PATH,
  160. adminJsAssetsDir = ADMINJS_ASSETS_DIR_PATH,
  161. designSystemDir = DESIGN_SYSTEM_DIR_PATH,
  162. adminJsOptions = {},
  163. versioning,
  164. }: BundleConfig): Promise<BundleFile[]> => {
  165. await import(join(process.cwd(), customComponentsInitializationFilePath));
  166. const timestamp = versioning?.manifestPath ? Date.now() : null;
  167. await fs.mkdir(join(process.cwd(), destinationDir), { recursive: true });
  168. const files = [
  169. {
  170. name: 'components.bundle.js',
  171. sourcePath: join(process.cwd(), `${adminJsLocalDir}/bundle.js`),
  172. destinationPath: join(process.cwd(), destinationDir, getDestinationPath('components.bundle.js', timestamp))
  173. },
  174. {
  175. name: 'app.bundle.js',
  176. sourcePath: join(process.cwd(), `${adminJsAssetsDir}/app-bundle.production.js`),
  177. destinationPath: join(process.cwd(), destinationDir, getDestinationPath('app.bundle.js', timestamp)),
  178. },
  179. {
  180. name: 'global.bundle.js',
  181. sourcePath: join(process.cwd(), `${adminJsAssetsDir}/global-bundle.production.js`),
  182. destinationPath: join(process.cwd(), destinationDir, getDestinationPath('global.bundle.js', timestamp))
  183. },
  184. {
  185. name: 'design-system.bundle.js',
  186. sourcePath: join(process.cwd(), `${designSystemDir}/bundle.production.js`),
  187. destinationPath: join(process.cwd(), destinationDir, getDestinationPath('design-system.bundle.js', timestamp))
  188. },
  189. ];
  190. const [ customComponentsBundle, ...standardBundles ] = files;
  191. await Promise.all(standardBundles.map(({ sourcePath, destinationPath }) => fs.copyFile(
  192. sourcePath,
  193. destinationPath,
  194. )));
  195. await new AdminJS(adminJsOptions).initialize();
  196. await fs.rename(
  197. customComponentsBundle.sourcePath,
  198. customComponentsBundle.destinationPath,
  199. );
  200. if (versioning?.manifestPath) {
  201. const manifestContents = createAssetsManifest(files);
  202. const { ext } = parse(versioning?.manifestPath);
  203. if (ext !== '.json') {
  204. await Promise.all(files.map(({ destinationPath }) => fs.unlink(destinationPath)));
  205. throw new Error('Invalid "versioning.manifestPath". File name must have .json extension.');
  206. }
  207. await fs.writeFile(
  208. join(process.cwd(), versioning.manifestPath),
  209. manifestContents,
  210. );
  211. }
  212. console.log(`✨ Successfully built AdminJS bundle files! ✨`);
  213. return files;
  214. };
  215. export default bundle;