compiler.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { VendureConfig } from '@vendure/core';
  2. import fs from 'fs-extra';
  3. import path from 'path';
  4. import tsConfigPaths from 'tsconfig-paths';
  5. import { RegisterParams } from 'tsconfig-paths/lib/register.js';
  6. import * as ts from 'typescript';
  7. import { pathToFileURL } from 'url';
  8. import { Logger, PathAdapter, PluginInfo } from '../types.js';
  9. import { findConfigExport } from './ast-utils.js';
  10. import { noopLogger } from './logger.js';
  11. import { discoverPlugins } from './plugin-discovery.js';
  12. import { findTsConfigPaths } from './tsconfig-utils.js';
  13. const defaultPathAdapter: Required<PathAdapter> = {
  14. getCompiledConfigPath: ({ outputPath, configFileName }) => path.join(outputPath, configFileName),
  15. transformTsConfigPathMappings: ({ patterns }) => patterns,
  16. };
  17. export interface PackageScannerConfig {
  18. nodeModulesRoot?: string;
  19. }
  20. export interface CompilerOptions {
  21. vendureConfigPath: string;
  22. outputPath: string;
  23. pathAdapter?: PathAdapter;
  24. logger?: Logger;
  25. pluginPackageScanner?: PackageScannerConfig;
  26. module?: 'commonjs' | 'esm';
  27. }
  28. export interface CompileResult {
  29. vendureConfig: VendureConfig;
  30. exportedSymbolName: string;
  31. pluginInfo: PluginInfo[];
  32. }
  33. /**
  34. * Compiles TypeScript files and discovers Vendure plugins in both the compiled output
  35. * and in node_modules.
  36. */
  37. export async function compile(options: CompilerOptions): Promise<CompileResult> {
  38. const { vendureConfigPath, outputPath, pathAdapter, logger = noopLogger, pluginPackageScanner } = options;
  39. const getCompiledConfigPath =
  40. pathAdapter?.getCompiledConfigPath ?? defaultPathAdapter.getCompiledConfigPath;
  41. const transformTsConfigPathMappings =
  42. pathAdapter?.transformTsConfigPathMappings ?? defaultPathAdapter.transformTsConfigPathMappings;
  43. // 0. Clear the outputPath
  44. fs.removeSync(outputPath);
  45. // 1. Compile TypeScript files
  46. const compileStart = Date.now();
  47. await compileTypeScript({
  48. inputPath: vendureConfigPath,
  49. outputPath,
  50. logger,
  51. transformTsConfigPathMappings,
  52. module: options.module ?? 'commonjs',
  53. });
  54. logger.info(`TypeScript compilation completed in ${Date.now() - compileStart}ms`);
  55. // 2. Discover plugins
  56. const analyzePluginsStart = Date.now();
  57. const plugins = await discoverPlugins({
  58. vendureConfigPath,
  59. transformTsConfigPathMappings,
  60. logger,
  61. outputPath,
  62. pluginPackageScanner,
  63. });
  64. logger.info(
  65. `Analyzed plugins and found ${plugins.length} dashboard extensions in ${Date.now() - analyzePluginsStart}ms`,
  66. );
  67. // 3. Load the compiled config
  68. const configFileName = path.basename(vendureConfigPath);
  69. const compiledConfigFilePath = pathToFileURL(
  70. getCompiledConfigPath({
  71. inputRootDir: path.dirname(vendureConfigPath),
  72. outputPath,
  73. configFileName,
  74. }),
  75. ).href.replace(/.ts$/, '.js');
  76. // Create package.json with type commonjs
  77. await fs.writeFile(
  78. path.join(outputPath, 'package.json'),
  79. JSON.stringify({ type: options.module === 'esm' ? 'module' : 'commonjs', private: true }, null, 2),
  80. );
  81. // Find the exported config symbol
  82. const sourceFile = ts.createSourceFile(
  83. vendureConfigPath,
  84. await fs.readFile(vendureConfigPath, 'utf-8'),
  85. ts.ScriptTarget.Latest,
  86. true,
  87. );
  88. const exportedSymbolName = findConfigExport(sourceFile);
  89. if (!exportedSymbolName) {
  90. throw new Error(
  91. `Could not find a variable exported as VendureConfig. Please specify the name of the exported variable.`,
  92. );
  93. }
  94. const loadConfigStart = Date.now();
  95. await registerTsConfigPaths({
  96. outputPath,
  97. configPath: vendureConfigPath,
  98. logger,
  99. phase: 'loading',
  100. transformTsConfigPathMappings,
  101. });
  102. let config: any;
  103. try {
  104. config = await import(compiledConfigFilePath).then(m => m[exportedSymbolName]);
  105. } catch (e) {
  106. logger.error(`Error loading config: ${e instanceof Error ? e.message : String(e)}`);
  107. }
  108. if (!config) {
  109. throw new Error(
  110. `Could not find a variable exported as VendureConfig with the name "${exportedSymbolName}".`,
  111. );
  112. }
  113. logger.debug(`Loaded config in ${Date.now() - loadConfigStart}ms`);
  114. return { vendureConfig: config, exportedSymbolName, pluginInfo: plugins };
  115. }
  116. /**
  117. * Compiles TypeScript files to JavaScript
  118. */
  119. async function compileTypeScript({
  120. inputPath,
  121. outputPath,
  122. logger,
  123. transformTsConfigPathMappings,
  124. module,
  125. }: {
  126. inputPath: string;
  127. outputPath: string;
  128. logger: Logger;
  129. transformTsConfigPathMappings: Required<PathAdapter>['transformTsConfigPathMappings'];
  130. module: 'commonjs' | 'esm';
  131. }): Promise<void> {
  132. await fs.ensureDir(outputPath);
  133. // Find tsconfig paths first
  134. const tsConfigInfo = await findTsConfigPaths(
  135. inputPath,
  136. logger,
  137. 'compiling',
  138. transformTsConfigPathMappings,
  139. );
  140. const compilerOptions: ts.CompilerOptions = {
  141. target: ts.ScriptTarget.ES2020,
  142. module: module === 'esm' ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS,
  143. moduleResolution: ts.ModuleResolutionKind.Node10, // More explicit CJS resolution
  144. experimentalDecorators: true,
  145. emitDecoratorMetadata: true,
  146. esModuleInterop: true,
  147. skipLibCheck: true,
  148. noEmit: false,
  149. // Speed optimizations
  150. noEmitOnError: false, // Emit output even if there are errors
  151. noImplicitAny: false, // Don't require implicit any
  152. noUnusedLocals: false, // Don't check for unused locals
  153. noUnusedParameters: false, // Don't check for unused parameters
  154. allowJs: true,
  155. checkJs: false, // Don't type check JS files
  156. skipDefaultLibCheck: true, // Skip checking .d.ts files
  157. isolatedModules: false, // Need to check cross-file references to compile dependencies
  158. incremental: false, // Don't use incremental compilation (faster for one-off builds)
  159. resolveJsonModule: true,
  160. preserveSymlinks: false,
  161. outDir: outputPath,
  162. };
  163. logger.debug(`Compiling ${inputPath} to ${outputPath} using TypeScript...`);
  164. // Add path mappings if found
  165. if (tsConfigInfo) {
  166. // We need to set baseUrl and paths for TypeScript to resolve the imports
  167. compilerOptions.baseUrl = tsConfigInfo.baseUrl;
  168. compilerOptions.paths = tsConfigInfo.paths;
  169. // This is critical - it tells TypeScript to preserve the paths in the output
  170. // compilerOptions.rootDir = tsConfigInfo.baseUrl;
  171. }
  172. logger.debug(`tsConfig paths: ${JSON.stringify(tsConfigInfo?.paths, null, 2)}`);
  173. logger.debug(`tsConfig baseUrl: ${tsConfigInfo?.baseUrl ?? 'UNKNOWN'}`);
  174. // Create a custom transformer to rewrite the output paths
  175. const customTransformers: ts.CustomTransformers = {
  176. after: [
  177. context => {
  178. return sourceFile => {
  179. // Only transform files that are not the entry point
  180. if (sourceFile.fileName === inputPath) {
  181. return sourceFile;
  182. }
  183. sourceFile.fileName = path.join(outputPath, path.basename(sourceFile.fileName));
  184. return sourceFile;
  185. };
  186. },
  187. ],
  188. };
  189. const program = ts.createProgram([inputPath], compilerOptions);
  190. const emitResult = program.emit(undefined, undefined, undefined, undefined, customTransformers);
  191. // Only log actual emit errors, not type errors
  192. if (emitResult.emitSkipped) {
  193. for (const diagnostic of emitResult.diagnostics) {
  194. if (diagnostic.file && diagnostic.start !== undefined) {
  195. const { line, character } = ts.getLineAndCharacterOfPosition(
  196. diagnostic.file,
  197. diagnostic.start,
  198. );
  199. const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
  200. logger.warn(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
  201. } else {
  202. logger.warn(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
  203. }
  204. }
  205. }
  206. }
  207. async function registerTsConfigPaths(options: {
  208. outputPath: string;
  209. configPath: string;
  210. logger: Logger;
  211. phase: 'compiling' | 'loading';
  212. transformTsConfigPathMappings: Required<PathAdapter>['transformTsConfigPathMappings'];
  213. }) {
  214. const { outputPath, configPath, logger, phase, transformTsConfigPathMappings } = options;
  215. const tsConfigInfo = await findTsConfigPaths(configPath, logger, phase, transformTsConfigPathMappings);
  216. if (tsConfigInfo) {
  217. const params: RegisterParams = {
  218. baseUrl: outputPath,
  219. paths: tsConfigInfo.paths,
  220. };
  221. logger.debug(`Registering tsconfig paths: ${JSON.stringify(params, null, 2)}`);
  222. tsConfigPaths.register(params);
  223. }
  224. }