compiler.ts 8.6 KB

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