scaffold.ts 11 KB

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