compiler.ts 8.6 KB

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