compile.ts 9.7 KB

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