scaffold.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. /* tslint:disable:no-console */
  2. import { spawn } from 'child_process';
  3. import * as fs from 'fs-extra';
  4. import * as path from 'path';
  5. import { EXTENSION_ROUTES_FILE, MODULES_OUTPUT_DIR, SHARED_EXTENSIONS_FILE } from './constants';
  6. import { getAllTranslationFiles, mergeExtensionTranslations } from './translations';
  7. import {
  8. AdminUiExtension,
  9. AdminUiExtensionLazyModule,
  10. AdminUiExtensionSharedModule,
  11. Extension,
  12. } from './types';
  13. import {
  14. copyStaticAsset,
  15. copyUiDevkit,
  16. isAdminUiExtension,
  17. logger,
  18. normalizeExtensions,
  19. shouldUseYarn,
  20. } from './utils';
  21. export async function setupScaffold(outputPath: string, extensions: Extension[]) {
  22. deleteExistingExtensionModules(outputPath);
  23. copySourceIfNotExists(outputPath);
  24. const adminUiExtensions = extensions.filter(isAdminUiExtension);
  25. const normalizedExtensions = normalizeExtensions(adminUiExtensions);
  26. await copyExtensionModules(outputPath, normalizedExtensions);
  27. const allTranslationFiles = getAllTranslationFiles(extensions);
  28. await mergeExtensionTranslations(outputPath, allTranslationFiles);
  29. copyUiDevkit(outputPath);
  30. try {
  31. await checkIfNgccWasRun();
  32. } catch (e) {
  33. const cmd = shouldUseYarn() ? 'yarn ngcc' : 'npx ngcc';
  34. logger.log(
  35. `An error occurred when running ngcc. Try removing node_modules, re-installing, and then manually running "${cmd}" in the project root.`,
  36. );
  37. }
  38. }
  39. /**
  40. * Deletes the contents of the /modules directory, which contains the plugin
  41. * extension modules copied over during the last compilation.
  42. */
  43. function deleteExistingExtensionModules(outputPath: string) {
  44. fs.removeSync(path.join(outputPath, MODULES_OUTPUT_DIR));
  45. }
  46. /**
  47. * Copies all files from the extensionPaths of the configured extensions into the
  48. * admin-ui source tree.
  49. */
  50. async function copyExtensionModules(outputPath: string, extensions: Array<Required<AdminUiExtension>>) {
  51. const extensionRoutesSource = generateLazyExtensionRoutes(extensions);
  52. fs.writeFileSync(path.join(outputPath, EXTENSION_ROUTES_FILE), extensionRoutesSource, 'utf8');
  53. const sharedExtensionModulesSource = generateSharedExtensionModule(extensions);
  54. fs.writeFileSync(path.join(outputPath, SHARED_EXTENSIONS_FILE), sharedExtensionModulesSource, 'utf8');
  55. for (const extension of extensions) {
  56. const dirName = path.basename(path.dirname(extension.extensionPath));
  57. const dest = path.join(outputPath, MODULES_OUTPUT_DIR, extension.id);
  58. fs.copySync(extension.extensionPath, dest);
  59. if (Array.isArray(extension.staticAssets)) {
  60. for (const asset of extension.staticAssets) {
  61. await copyStaticAsset(outputPath, asset);
  62. }
  63. }
  64. }
  65. }
  66. function generateLazyExtensionRoutes(extensions: Array<Required<AdminUiExtension>>): string {
  67. const routes: string[] = [];
  68. for (const extension of extensions as Array<Required<AdminUiExtension>>) {
  69. for (const module of extension.ngModules) {
  70. if (module.type === 'lazy') {
  71. routes.push(` {
  72. path: 'extensions/${module.route}',
  73. loadChildren: () => import('${getModuleFilePath(extension.id, module)}').then(m => m.${
  74. module.ngModuleName
  75. }),
  76. }`);
  77. }
  78. }
  79. }
  80. return `export const extensionRoutes = [${routes.join(',\n')}];\n`;
  81. }
  82. function generateSharedExtensionModule(extensions: Array<Required<AdminUiExtension>>) {
  83. return `import { NgModule } from '@angular/core';
  84. import { CommonModule } from '@angular/common';
  85. ${extensions
  86. .map(e =>
  87. e.ngModules
  88. .filter(m => m.type === 'shared')
  89. .map(m => `import { ${m.ngModuleName} } from '${getModuleFilePath(e.id, m)}';\n`)
  90. .join(''),
  91. )
  92. .join('')}
  93. @NgModule({
  94. imports: [CommonModule, ${extensions
  95. .map(e =>
  96. e.ngModules
  97. .filter(m => m.type === 'shared')
  98. .map(m => m.ngModuleName)
  99. .join(', '),
  100. )
  101. .join(', ')}],
  102. })
  103. export class SharedExtensionsModule {}
  104. `;
  105. }
  106. function getModuleFilePath(
  107. id: string,
  108. module: AdminUiExtensionLazyModule | AdminUiExtensionSharedModule,
  109. ): string {
  110. return `./extensions/${id}/${path.basename(module.ngModuleFileName, '.ts')}`;
  111. }
  112. /**
  113. * Copy the Admin UI sources & static assets to the outputPath if it does not already
  114. * exists there.
  115. */
  116. function copySourceIfNotExists(outputPath: string) {
  117. const angularJsonFile = path.join(outputPath, 'angular.json');
  118. const indexFile = path.join(outputPath, '/src/index.html');
  119. if (fs.existsSync(angularJsonFile) && fs.existsSync(indexFile)) {
  120. return;
  121. }
  122. const scaffoldDir = path.join(__dirname, '../scaffold');
  123. const adminUiSrc = path.join(require.resolve('@vendure/admin-ui'), '../../static');
  124. if (!fs.existsSync(scaffoldDir)) {
  125. throw new Error(`Could not find the admin ui scaffold files at ${scaffoldDir}`);
  126. }
  127. if (!fs.existsSync(adminUiSrc)) {
  128. throw new Error(`Could not find the @vendure/admin-ui sources. Looked in ${adminUiSrc}`);
  129. }
  130. // copy scaffold
  131. fs.removeSync(outputPath);
  132. fs.ensureDirSync(outputPath);
  133. fs.copySync(scaffoldDir, outputPath);
  134. // copy source files from admin-ui package
  135. const outputSrc = path.join(outputPath, 'src');
  136. fs.ensureDirSync(outputSrc);
  137. fs.copySync(adminUiSrc, outputSrc);
  138. }
  139. /**
  140. * Attempts to find out it the ngcc compiler has been run on the Angular packages, and if not,
  141. * attemps to run it. This is done this way because attempting to run ngcc from a sub-directory
  142. * where the angular libs are in a higher-level node_modules folder currently results in the error
  143. * NG6002, see https://github.com/angular/angular/issues/35747.
  144. *
  145. * However, when ngcc is run from the root, it works.
  146. */
  147. async function checkIfNgccWasRun(): Promise<void> {
  148. const coreUmdFile = require.resolve('@vendure/admin-ui/core');
  149. if (!coreUmdFile) {
  150. logger.error(`Could not resolve the "@vendure/admin-ui/core" package!`);
  151. return;
  152. }
  153. // ngcc creates a particular folder after it has been run once
  154. const ivyDir = path.join(coreUmdFile, '../..', '__ivy_ngcc__');
  155. if (fs.existsSync(ivyDir)) {
  156. return;
  157. }
  158. // Looks like ngcc has not been run, so attempt to do so.
  159. const rootDir = coreUmdFile.split('node_modules')[0];
  160. return new Promise((resolve, reject) => {
  161. logger.log(
  162. 'Running the Angular Ivy compatibility compiler (ngcc) on Vendure Admin UI dependencies ' +
  163. '(this is only needed on the first run)...',
  164. );
  165. const cmd = shouldUseYarn() ? 'yarn' : 'npx';
  166. const ngccProcess = spawn(
  167. cmd,
  168. [
  169. 'ngcc',
  170. '--properties es2015 browser module main',
  171. '--first-only',
  172. '--create-ivy-entry-points',
  173. '-l=error',
  174. ],
  175. {
  176. cwd: rootDir,
  177. shell: true,
  178. stdio: 'inherit',
  179. },
  180. );
  181. ngccProcess.on('close', code => {
  182. if (code !== 0) {
  183. reject(code);
  184. } else {
  185. resolve();
  186. }
  187. });
  188. });
  189. }