compile.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. /* eslint-disable no-console */
  2. import { LanguageCode } from '@vendure/common/lib/generated-types';
  3. import { AdminUiAppConfig, AdminUiAppDevModeConfig } from '@vendure/common/lib/shared-types';
  4. import { ChildProcess, spawn } from 'child_process';
  5. import { FSWatcher, watch as chokidarWatch } from 'chokidar';
  6. import * as fs from 'fs-extra';
  7. import * as path from 'path';
  8. import { DEFAULT_BASE_HREF, MODULES_OUTPUT_DIR } from './constants';
  9. import { copyGlobalStyleFile, setBaseHref, setupScaffold } from './scaffold';
  10. import { getAllTranslationFiles, mergeExtensionTranslations } from './translations';
  11. import {
  12. StaticAssetDefinition,
  13. UiExtensionBuildCommand,
  14. UiExtensionCompilerOptions,
  15. UiExtensionCompilerProcessArgument,
  16. } from './types';
  17. import {
  18. copyStaticAsset,
  19. copyUiDevkit,
  20. determinePackageManager,
  21. getStaticAssetPath,
  22. isAdminUiExtension,
  23. isGlobalStylesExtension,
  24. isStaticAssetExtension,
  25. isTranslationExtension,
  26. normalizeExtensions,
  27. } from './utils';
  28. /**
  29. * @description
  30. * Compiles the Admin UI app with the specified extensions.
  31. *
  32. * @docsCategory UiDevkit
  33. */
  34. export function compileUiExtensions(
  35. options: UiExtensionCompilerOptions,
  36. ): AdminUiAppConfig | AdminUiAppDevModeConfig {
  37. const { devMode, watchPort } = options;
  38. const command: UiExtensionBuildCommand =
  39. options.command && ['npm', 'pnpm'].includes(options.command)
  40. ? options.command
  41. : determinePackageManager();
  42. if (devMode) {
  43. return runWatchMode({
  44. watchPort: watchPort || 4200,
  45. ...options,
  46. command,
  47. });
  48. } else {
  49. return runCompileMode({
  50. ...options,
  51. command,
  52. });
  53. }
  54. }
  55. function runCompileMode({
  56. outputPath,
  57. baseHref,
  58. extensions,
  59. command,
  60. additionalProcessArguments,
  61. ngCompilerPath,
  62. }: UiExtensionCompilerOptions & { command: UiExtensionBuildCommand }): AdminUiAppConfig {
  63. const distPath = path.join(outputPath, 'dist');
  64. const compile = () =>
  65. new Promise<void>(async (resolve, reject) => {
  66. await setupScaffold(outputPath, extensions);
  67. await setBaseHref(outputPath, baseHref || DEFAULT_BASE_HREF);
  68. let cmd: UiExtensionBuildCommand | 'node' = command;
  69. let commandArgs = ['run', 'build'];
  70. if (ngCompilerPath) {
  71. cmd = 'node';
  72. commandArgs = [ngCompilerPath, 'build', '--configuration production'];
  73. } else {
  74. if (cmd === 'npm') {
  75. // npm requires `--` before any command line args being passed to a script
  76. commandArgs.splice(2, 0, '--');
  77. }
  78. }
  79. console.log(`Running ${cmd} ${commandArgs.join(' ')}`);
  80. const buildProcess = spawn(
  81. cmd,
  82. [...commandArgs, ...buildProcessArguments(additionalProcessArguments)],
  83. {
  84. cwd: outputPath,
  85. shell: true,
  86. stdio: 'inherit',
  87. },
  88. );
  89. buildProcess.on('close', code => {
  90. if (code !== 0) {
  91. reject(code);
  92. } else {
  93. resolve();
  94. }
  95. });
  96. });
  97. return {
  98. path: distPath,
  99. compile,
  100. route: baseHrefToRoute(baseHref || DEFAULT_BASE_HREF),
  101. };
  102. }
  103. function runWatchMode({
  104. outputPath,
  105. baseHref,
  106. watchPort,
  107. extensions,
  108. command,
  109. additionalProcessArguments,
  110. ngCompilerPath,
  111. }: UiExtensionCompilerOptions & { command: UiExtensionBuildCommand }): AdminUiAppDevModeConfig {
  112. const devkitPath = require.resolve('@vendure/ui-devkit');
  113. let buildProcess: ChildProcess;
  114. let watcher: FSWatcher | undefined;
  115. let close: () => void = () => {
  116. /* */
  117. };
  118. const compile = () =>
  119. new Promise<void>(async (resolve, reject) => {
  120. await setupScaffold(outputPath, extensions);
  121. await setBaseHref(outputPath, baseHref || DEFAULT_BASE_HREF);
  122. const adminUiExtensions = extensions.filter(isAdminUiExtension);
  123. const normalizedExtensions = normalizeExtensions(adminUiExtensions);
  124. const globalStylesExtensions = extensions.filter(isGlobalStylesExtension);
  125. const staticAssetExtensions = extensions.filter(isStaticAssetExtension);
  126. const allTranslationFiles = getAllTranslationFiles(extensions.filter(isTranslationExtension));
  127. let cmd: UiExtensionBuildCommand | 'node' = command;
  128. let commandArgs = ['run', 'start'];
  129. if (ngCompilerPath) {
  130. cmd = 'node';
  131. commandArgs = [ngCompilerPath, 'serve'];
  132. }
  133. buildProcess = spawn(
  134. cmd,
  135. [
  136. ...commandArgs,
  137. `--port=${watchPort || 4200}`,
  138. ...buildProcessArguments(additionalProcessArguments),
  139. ],
  140. {
  141. cwd: outputPath,
  142. shell: true,
  143. stdio: 'inherit',
  144. },
  145. );
  146. buildProcess.on('close', code => {
  147. if (code !== 0) {
  148. reject(code);
  149. } else {
  150. resolve();
  151. }
  152. close();
  153. });
  154. for (const extension of normalizedExtensions) {
  155. if (!watcher) {
  156. watcher = chokidarWatch(extension.extensionPath, {
  157. depth: 4,
  158. ignored: '**/node_modules/',
  159. });
  160. } else {
  161. watcher.add(extension.extensionPath);
  162. }
  163. }
  164. for (const extension of staticAssetExtensions) {
  165. for (const staticAssetDef of extension.staticAssets) {
  166. const assetPath = getStaticAssetPath(staticAssetDef);
  167. if (!watcher) {
  168. watcher = chokidarWatch(assetPath);
  169. } else {
  170. watcher.add(assetPath);
  171. }
  172. }
  173. }
  174. for (const extension of globalStylesExtensions) {
  175. const globalStylePaths = Array.isArray(extension.globalStyles)
  176. ? extension.globalStyles
  177. : [extension.globalStyles];
  178. for (const stylePath of globalStylePaths) {
  179. if (!watcher) {
  180. watcher = chokidarWatch(stylePath);
  181. } else {
  182. watcher.add(stylePath);
  183. }
  184. }
  185. }
  186. for (const translationFiles of Object.values(allTranslationFiles)) {
  187. if (!translationFiles) {
  188. continue;
  189. }
  190. for (const file of translationFiles) {
  191. if (!watcher) {
  192. watcher = chokidarWatch(file);
  193. } else {
  194. watcher.add(file);
  195. }
  196. }
  197. }
  198. if (watcher) {
  199. // watch the ui-devkit package files too
  200. watcher.add(devkitPath);
  201. }
  202. if (watcher) {
  203. const allStaticAssetDefs = staticAssetExtensions.reduce(
  204. (defs, e) => [...defs, ...(e.staticAssets || [])],
  205. [] as StaticAssetDefinition[],
  206. );
  207. const allGlobalStyles = globalStylesExtensions.reduce(
  208. (defs, e) => [
  209. ...defs,
  210. ...(Array.isArray(e.globalStyles) ? e.globalStyles : [e.globalStyles]),
  211. ],
  212. [] as string[],
  213. );
  214. watcher.on('change', async filePath => {
  215. const extension = normalizedExtensions.find(e => filePath.includes(e.extensionPath));
  216. if (extension) {
  217. const outputDir = path.join(outputPath, MODULES_OUTPUT_DIR, extension.id);
  218. const filePart = path.relative(extension.extensionPath, filePath);
  219. const dest = path.join(outputDir, filePart);
  220. await fs.copyFile(filePath, dest);
  221. }
  222. if (filePath.includes(devkitPath)) {
  223. copyUiDevkit(outputPath);
  224. }
  225. for (const staticAssetDef of allStaticAssetDefs) {
  226. const assetPath = getStaticAssetPath(staticAssetDef);
  227. if (filePath.includes(assetPath)) {
  228. await copyStaticAsset(outputPath, staticAssetDef);
  229. return;
  230. }
  231. }
  232. for (const stylePath of allGlobalStyles) {
  233. if (filePath.includes(stylePath)) {
  234. await copyGlobalStyleFile(outputPath, stylePath);
  235. return;
  236. }
  237. }
  238. for (const languageCode of Object.keys(allTranslationFiles)) {
  239. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  240. const translationFiles = allTranslationFiles[languageCode as LanguageCode]!;
  241. for (const file of translationFiles) {
  242. if (filePath.includes(path.normalize(file))) {
  243. await mergeExtensionTranslations(outputPath, {
  244. [languageCode]: translationFiles,
  245. });
  246. }
  247. }
  248. }
  249. });
  250. }
  251. resolve();
  252. });
  253. close = () => {
  254. if (watcher) {
  255. void watcher.close();
  256. }
  257. if (buildProcess) {
  258. buildProcess.kill();
  259. }
  260. process.exit();
  261. };
  262. process.on('SIGINT', close);
  263. return {
  264. sourcePath: outputPath,
  265. port: watchPort || 4200,
  266. compile,
  267. route: baseHrefToRoute(baseHref || DEFAULT_BASE_HREF),
  268. };
  269. }
  270. function buildProcessArguments(args?: UiExtensionCompilerProcessArgument[]): string[] {
  271. return (args ?? []).map(arg => {
  272. if (Array.isArray(arg)) {
  273. const [key, value] = arg;
  274. return `${key}=${value as string}`;
  275. }
  276. return arg;
  277. });
  278. }
  279. function baseHrefToRoute(baseHref: string): string {
  280. return baseHref.replace(/^\//, '').replace(/\/$/, '');
  281. }