scaffold.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. /* eslint-disable no-console */
  2. import { spawn } from 'child_process';
  3. import * as fs from 'fs-extra';
  4. import { globSync } from 'glob';
  5. import * as path from 'path';
  6. import {
  7. EXTENSION_ROUTES_FILE,
  8. GLOBAL_STYLES_OUTPUT_DIR,
  9. MODULES_OUTPUT_DIR,
  10. SHARED_EXTENSIONS_FILE,
  11. } from './constants';
  12. import { getAllTranslationFiles, mergeExtensionTranslations } from './translations';
  13. import {
  14. AdminUiExtensionLazyModule,
  15. AdminUiExtensionSharedModule,
  16. AdminUiExtensionWithId,
  17. Extension,
  18. GlobalStylesExtension,
  19. SassVariableOverridesExtension,
  20. StaticAssetExtension,
  21. } from './types';
  22. import {
  23. copyStaticAsset,
  24. copyUiDevkit,
  25. isAdminUiExtension,
  26. isGlobalStylesExtension,
  27. isSassVariableOverridesExtension,
  28. isStaticAssetExtension,
  29. isTranslationExtension,
  30. logger,
  31. normalizeExtensions,
  32. shouldUseYarn,
  33. } from './utils';
  34. export async function setupScaffold(outputPath: string, extensions: Extension[]) {
  35. deleteExistingExtensionModules(outputPath);
  36. const adminUiExtensions = extensions.filter(isAdminUiExtension);
  37. const normalizedExtensions = normalizeExtensions(adminUiExtensions);
  38. const modulePathMapping = generateModulePathMapping(normalizedExtensions);
  39. copyAdminUiSource(outputPath, modulePathMapping);
  40. await copyExtensionModules(outputPath, normalizedExtensions);
  41. const staticAssetExtensions = extensions.filter(isStaticAssetExtension);
  42. await copyStaticAssets(outputPath, staticAssetExtensions);
  43. const globalStyleExtensions = extensions.filter(isGlobalStylesExtension);
  44. const sassVariableOverridesExtension = extensions.find(isSassVariableOverridesExtension);
  45. await addGlobalStyles(outputPath, globalStyleExtensions, sassVariableOverridesExtension);
  46. const allTranslationFiles = getAllTranslationFiles(extensions.filter(isTranslationExtension));
  47. await mergeExtensionTranslations(outputPath, allTranslationFiles);
  48. copyUiDevkit(outputPath);
  49. }
  50. /**
  51. * Deletes the contents of the /modules directory, which contains the plugin
  52. * extension modules copied over during the last compilation.
  53. */
  54. function deleteExistingExtensionModules(outputPath: string) {
  55. fs.removeSync(path.join(outputPath, MODULES_OUTPUT_DIR));
  56. }
  57. /**
  58. * Generates a module path mapping object for all extensions with a "pathAlias"
  59. * property declared (if any).
  60. */
  61. function generateModulePathMapping(extensions: AdminUiExtensionWithId[]) {
  62. const extensionsWithAlias = extensions.filter(e => e.pathAlias);
  63. if (extensionsWithAlias.length === 0) {
  64. return undefined;
  65. }
  66. return extensionsWithAlias.reduce((acc, e) => {
  67. // for imports from the index file if there is one
  68. acc[e.pathAlias as string] = [`src/extensions/${e.id}`];
  69. // direct access to files / deep imports
  70. acc[`${e.pathAlias as string}/*`] = [`src/extensions/${e.id}/*`];
  71. return acc;
  72. }, {} as Record<string, string[]>);
  73. }
  74. /**
  75. * Copies all files from the extensionPaths of the configured extensions into the
  76. * admin-ui source tree.
  77. */
  78. async function copyExtensionModules(outputPath: string, extensions: AdminUiExtensionWithId[]) {
  79. const extensionRoutesSource = generateLazyExtensionRoutes(extensions);
  80. fs.writeFileSync(path.join(outputPath, EXTENSION_ROUTES_FILE), extensionRoutesSource, 'utf8');
  81. const sharedExtensionModulesSource = generateSharedExtensionModule(extensions);
  82. fs.writeFileSync(path.join(outputPath, SHARED_EXTENSIONS_FILE), sharedExtensionModulesSource, 'utf8');
  83. for (const extension of extensions) {
  84. const dest = path.join(outputPath, MODULES_OUTPUT_DIR, extension.id);
  85. if (!extension.exclude) {
  86. fs.copySync(extension.extensionPath, dest);
  87. continue;
  88. }
  89. const exclude = extension.exclude
  90. .map(e => globSync(path.join(extension.extensionPath, e)))
  91. .flatMap(e => e);
  92. fs.copySync(extension.extensionPath, dest, {
  93. filter: name => name === extension.extensionPath || exclude.every(e => e !== name),
  94. });
  95. }
  96. }
  97. async function copyStaticAssets(outputPath: string, extensions: Array<Partial<StaticAssetExtension>>) {
  98. for (const extension of extensions) {
  99. if (Array.isArray(extension.staticAssets)) {
  100. for (const asset of extension.staticAssets) {
  101. await copyStaticAsset(outputPath, asset);
  102. }
  103. }
  104. }
  105. }
  106. async function addGlobalStyles(
  107. outputPath: string,
  108. globalStylesExtensions: GlobalStylesExtension[],
  109. sassVariableOverridesExtension?: SassVariableOverridesExtension,
  110. ) {
  111. const globalStylesDir = path.join(outputPath, 'src', GLOBAL_STYLES_OUTPUT_DIR);
  112. await fs.remove(globalStylesDir);
  113. await fs.ensureDir(globalStylesDir);
  114. const imports: string[] = [];
  115. for (const extension of globalStylesExtensions) {
  116. const styleFiles = Array.isArray(extension.globalStyles)
  117. ? extension.globalStyles
  118. : [extension.globalStyles];
  119. for (const styleFile of styleFiles) {
  120. await copyGlobalStyleFile(outputPath, styleFile);
  121. imports.push(path.basename(styleFile, path.extname(styleFile)));
  122. }
  123. }
  124. let overridesImport = '';
  125. if (sassVariableOverridesExtension) {
  126. const overridesFile = sassVariableOverridesExtension.sassVariableOverrides;
  127. await copyGlobalStyleFile(outputPath, overridesFile);
  128. overridesImport = `@import "./${GLOBAL_STYLES_OUTPUT_DIR}/${path.basename(
  129. overridesFile,
  130. path.extname(overridesFile),
  131. )}";\n`;
  132. }
  133. const globalStylesSource =
  134. overridesImport +
  135. '@import "./styles/styles";\n' +
  136. imports.map(file => `@import "./${GLOBAL_STYLES_OUTPUT_DIR}/${file}";`).join('\n');
  137. const globalStylesFile = path.join(outputPath, 'src', 'global-styles.scss');
  138. await fs.writeFile(globalStylesFile, globalStylesSource, 'utf-8');
  139. }
  140. export async function copyGlobalStyleFile(outputPath: string, stylePath: string) {
  141. const globalStylesDir = path.join(outputPath, 'src', GLOBAL_STYLES_OUTPUT_DIR);
  142. const fileBasename = path.basename(stylePath);
  143. const styleOutputPath = path.join(globalStylesDir, fileBasename);
  144. await fs.copyFile(stylePath, styleOutputPath);
  145. }
  146. function generateLazyExtensionRoutes(extensions: AdminUiExtensionWithId[]): string {
  147. const routes: string[] = [];
  148. for (const extension of extensions) {
  149. for (const module of extension.ngModules) {
  150. if (module.type === 'lazy') {
  151. routes.push(` {
  152. path: 'extensions/${module.route}',
  153. loadChildren: () => import('${getModuleFilePath(extension.id, module)}').then(m => m.${
  154. module.ngModuleName
  155. }),
  156. }`);
  157. }
  158. }
  159. }
  160. return `export const extensionRoutes = [${routes.join(',\n')}];\n`;
  161. }
  162. function generateSharedExtensionModule(extensions: AdminUiExtensionWithId[]) {
  163. return `import { NgModule } from '@angular/core';
  164. import { CommonModule } from '@angular/common';
  165. ${extensions
  166. .map(e =>
  167. e.ngModules
  168. .filter(m => m.type === 'shared')
  169. .map(m => `import { ${m.ngModuleName} } from '${getModuleFilePath(e.id, m)}';\n`)
  170. .join(''),
  171. )
  172. .join('')}
  173. @NgModule({
  174. imports: [CommonModule, ${extensions
  175. .map(e =>
  176. e.ngModules
  177. .filter(m => m.type === 'shared')
  178. .map(m => m.ngModuleName)
  179. .join(', '),
  180. )
  181. .join(', ')}],
  182. })
  183. export class SharedExtensionsModule {}
  184. `;
  185. }
  186. function getModuleFilePath(
  187. id: string,
  188. module: AdminUiExtensionLazyModule | AdminUiExtensionSharedModule,
  189. ): string {
  190. return `./extensions/${id}/${path.basename(module.ngModuleFileName, '.ts')}`;
  191. }
  192. /**
  193. * Copies the Admin UI sources & static assets to the outputPath if it does not already
  194. * exist there.
  195. */
  196. function copyAdminUiSource(outputPath: string, modulePathMapping: Record<string, string[]> | undefined) {
  197. const tsconfigFilePath = path.join(outputPath, 'tsconfig.json');
  198. const indexFilePath = path.join(outputPath, '/src/index.html');
  199. if (fs.existsSync(tsconfigFilePath) && fs.existsSync(indexFilePath)) {
  200. configureModulePathMapping(tsconfigFilePath, modulePathMapping);
  201. return;
  202. }
  203. const scaffoldDir = path.join(__dirname, '../scaffold');
  204. const adminUiSrc = path.join(require.resolve('@vendure/admin-ui'), '../../static');
  205. if (!fs.existsSync(scaffoldDir)) {
  206. throw new Error(`Could not find the admin ui scaffold files at ${scaffoldDir}`);
  207. }
  208. if (!fs.existsSync(adminUiSrc)) {
  209. throw new Error(`Could not find the @vendure/admin-ui sources. Looked in ${adminUiSrc}`);
  210. }
  211. // copy scaffold
  212. fs.removeSync(outputPath);
  213. fs.ensureDirSync(outputPath);
  214. fs.copySync(scaffoldDir, outputPath);
  215. configureModulePathMapping(tsconfigFilePath, modulePathMapping);
  216. // copy source files from admin-ui package
  217. const outputSrc = path.join(outputPath, 'src');
  218. fs.ensureDirSync(outputSrc);
  219. fs.copySync(adminUiSrc, outputSrc);
  220. }
  221. export async function setBaseHref(outputPath: string, baseHref: string) {
  222. const angularJsonFilePath = path.join(outputPath, '/angular.json');
  223. const angularJson = await fs.readJSON(angularJsonFilePath, 'utf-8');
  224. angularJson.projects['vendure-admin'].architect.build.options.baseHref = baseHref;
  225. await fs.writeJSON(angularJsonFilePath, angularJson, { spaces: 2 });
  226. }
  227. /**
  228. * Adds module path mapping to the bundled tsconfig.json file if defined as a UI extension.
  229. */
  230. function configureModulePathMapping(
  231. tsconfigFilePath: string,
  232. modulePathMapping: Record<string, string[]> | undefined,
  233. ) {
  234. if (!modulePathMapping) {
  235. return;
  236. }
  237. // eslint-disable-next-line @typescript-eslint/no-var-requires
  238. const tsconfig = require(tsconfigFilePath);
  239. tsconfig.compilerOptions.paths = modulePathMapping;
  240. fs.writeFileSync(tsconfigFilePath, JSON.stringify(tsconfig, null, 2));
  241. }