|
|
@@ -1,13 +1,33 @@
|
|
|
import { VendureConfig } from '@vendure/core';
|
|
|
import fs from 'fs-extra';
|
|
|
import path from 'path';
|
|
|
+import tsConfigPaths from 'tsconfig-paths';
|
|
|
import * as ts from 'typescript';
|
|
|
import { pathToFileURL } from 'url';
|
|
|
|
|
|
+type Logger = {
|
|
|
+ info: (message: string) => void;
|
|
|
+ warn: (message: string) => void;
|
|
|
+ debug: (message: string) => void;
|
|
|
+};
|
|
|
+
|
|
|
+const defaultLogger: Logger = {
|
|
|
+ info: (message: string) => {
|
|
|
+ /* noop */
|
|
|
+ },
|
|
|
+ warn: (message: string) => {
|
|
|
+ /* noop */
|
|
|
+ },
|
|
|
+ debug: (message: string) => {
|
|
|
+ /* noop */
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
export interface ConfigLoaderOptions {
|
|
|
vendureConfigPath: string;
|
|
|
tempDir: string;
|
|
|
vendureConfigExport?: string;
|
|
|
+ logger?: Logger;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -30,11 +50,12 @@ export async function loadVendureConfig(
|
|
|
options: ConfigLoaderOptions,
|
|
|
): Promise<{ vendureConfig: VendureConfig; exportedSymbolName: string }> {
|
|
|
const { vendureConfigPath, vendureConfigExport, tempDir } = options;
|
|
|
+ const logger = options.logger || defaultLogger;
|
|
|
const outputPath = tempDir;
|
|
|
const configFileName = path.basename(vendureConfigPath);
|
|
|
const inputRootDir = path.dirname(vendureConfigPath);
|
|
|
await fs.remove(outputPath);
|
|
|
- await compileFile(inputRootDir, vendureConfigPath, outputPath);
|
|
|
+ await compileFile(inputRootDir, vendureConfigPath, outputPath, logger);
|
|
|
const compiledConfigFilePath = pathToFileURL(path.join(outputPath, configFileName)).href.replace(
|
|
|
/.ts$/,
|
|
|
'.js',
|
|
|
@@ -57,6 +78,16 @@ export async function loadVendureConfig(
|
|
|
`Could not find a variable exported as VendureConfig. Please specify the name of the exported variable using the "vendureConfigExport" option.`,
|
|
|
);
|
|
|
}
|
|
|
+
|
|
|
+ // Register path aliases from tsconfig before importing
|
|
|
+ const tsConfigInfo = await findTsConfigPaths(vendureConfigPath, logger);
|
|
|
+ if (tsConfigInfo) {
|
|
|
+ tsConfigPaths.register({
|
|
|
+ baseUrl: outputPath,
|
|
|
+ paths: tsConfigInfo.paths,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
const config = await import(compiledConfigFilePath).then(m => m[configExportedSymbolName]);
|
|
|
if (!config) {
|
|
|
throw new Error(
|
|
|
@@ -98,17 +129,77 @@ function findConfigExport(sourceFile: ts.SourceFile): string | undefined {
|
|
|
return exportedSymbolName;
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Finds and parses tsconfig files in the given directory and its parent directories.
|
|
|
+ * Returns the paths configuration if found.
|
|
|
+ */
|
|
|
+async function findTsConfigPaths(
|
|
|
+ configPath: string,
|
|
|
+ logger: Logger,
|
|
|
+): Promise<{ baseUrl: string; paths: Record<string, string[]> } | undefined> {
|
|
|
+ const configDir = path.dirname(configPath);
|
|
|
+ let currentDir = configDir;
|
|
|
+
|
|
|
+ while (currentDir !== path.parse(currentDir).root) {
|
|
|
+ try {
|
|
|
+ const files = await fs.readdir(currentDir);
|
|
|
+ const tsConfigFiles = files.filter(file => /^tsconfig(\..*)?\.json$/.test(file));
|
|
|
+
|
|
|
+ for (const fileName of tsConfigFiles) {
|
|
|
+ const tsConfigPath = path.join(currentDir, fileName);
|
|
|
+ try {
|
|
|
+ const tsConfigContent = await fs.readFile(tsConfigPath, 'utf-8');
|
|
|
+ // Use JSON5 or similar parser if comments are expected in tsconfig.json
|
|
|
+ // For simplicity, assuming standard JSON here. Handle parse errors.
|
|
|
+ const tsConfig = JSON.parse(tsConfigContent);
|
|
|
+ const compilerOptions = tsConfig.compilerOptions || {};
|
|
|
+
|
|
|
+ if (compilerOptions.paths) {
|
|
|
+ // Determine the effective baseUrl: explicitly set or the directory of tsconfig.json
|
|
|
+ const tsConfigBaseUrl = path.resolve(currentDir, compilerOptions.baseUrl || '.');
|
|
|
+ const paths: Record<string, string[]> = {};
|
|
|
+
|
|
|
+ for (const [alias, patterns] of Object.entries(compilerOptions.paths)) {
|
|
|
+ // Store paths as defined in tsconfig, they will be relative to baseUrl
|
|
|
+ paths[alias] = (patterns as string[]).map(pattern =>
|
|
|
+ // Normalize slashes for consistency, keep relative
|
|
|
+ pattern.replace(/\\/g, '/'),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ logger.debug(
|
|
|
+ `Found tsconfig paths in ${tsConfigPath}: ${JSON.stringify({ baseUrl: tsConfigBaseUrl, paths }, null, 2)}`,
|
|
|
+ );
|
|
|
+ return { baseUrl: tsConfigBaseUrl, paths };
|
|
|
+ }
|
|
|
+ } catch (e: any) {
|
|
|
+ logger.warn(
|
|
|
+ `Could not read or parse tsconfig file ${tsConfigPath}: ${e.message as string}`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e: any) {
|
|
|
+ // If we can't read the directory, just continue to the parent
|
|
|
+ logger.warn(`Could not read directory ${currentDir}: ${e.message as string}`);
|
|
|
+ }
|
|
|
+ currentDir = path.dirname(currentDir);
|
|
|
+ }
|
|
|
+ logger.debug(`No tsconfig paths found traversing up from ${configDir}`);
|
|
|
+ return undefined;
|
|
|
+}
|
|
|
+
|
|
|
export async function compileFile(
|
|
|
inputRootDir: string,
|
|
|
inputPath: string,
|
|
|
outputDir: string,
|
|
|
+ logger: Logger = defaultLogger,
|
|
|
compiledFiles = new Set<string>(),
|
|
|
isRoot = true,
|
|
|
): Promise<void> {
|
|
|
- if (compiledFiles.has(inputPath)) {
|
|
|
+ const absoluteInputPath = path.resolve(inputPath);
|
|
|
+ if (compiledFiles.has(absoluteInputPath)) {
|
|
|
return;
|
|
|
}
|
|
|
- compiledFiles.add(inputPath);
|
|
|
+ compiledFiles.add(absoluteInputPath);
|
|
|
|
|
|
// Ensure output directory exists
|
|
|
await fs.ensureDir(outputDir);
|
|
|
@@ -117,62 +208,210 @@ export async function compileFile(
|
|
|
const source = await fs.readFile(inputPath, 'utf-8');
|
|
|
|
|
|
// Parse the source to find relative imports
|
|
|
- const sourceFile = ts.createSourceFile(inputPath, source, ts.ScriptTarget.Latest, true);
|
|
|
+ const sourceFile = ts.createSourceFile(absoluteInputPath, source, ts.ScriptTarget.Latest, true);
|
|
|
|
|
|
const importPaths = new Set<string>();
|
|
|
+ let tsConfigInfo: { baseUrl: string; paths: Record<string, string[]> } | undefined;
|
|
|
+
|
|
|
+ if (isRoot) {
|
|
|
+ tsConfigInfo = await findTsConfigPaths(absoluteInputPath, logger);
|
|
|
+ if (tsConfigInfo) {
|
|
|
+ logger?.debug(`Using TypeScript configuration: ${JSON.stringify(tsConfigInfo, null, 2)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- function collectImports(node: ts.Node) {
|
|
|
+ async function collectImports(node: ts.Node) {
|
|
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
|
const importPath = node.moduleSpecifier.text;
|
|
|
+
|
|
|
+ // Handle relative imports
|
|
|
if (importPath.startsWith('.')) {
|
|
|
- const resolvedPath = path.resolve(path.dirname(inputPath), importPath);
|
|
|
- importPaths.add(resolvedPath + '.ts');
|
|
|
+ const resolvedPath = path.resolve(path.dirname(absoluteInputPath), importPath);
|
|
|
+ let resolvedTsPath = resolvedPath + '.ts';
|
|
|
+ // Also check for .tsx if .ts doesn't exist
|
|
|
+ if (!(await fs.pathExists(resolvedTsPath))) {
|
|
|
+ const resolvedTsxPath = resolvedPath + '.tsx';
|
|
|
+ if (await fs.pathExists(resolvedTsxPath)) {
|
|
|
+ resolvedTsPath = resolvedTsxPath;
|
|
|
+ } else {
|
|
|
+ // If neither exists, maybe it's an index file?
|
|
|
+ const resolvedIndexPath = path.join(resolvedPath, 'index.ts');
|
|
|
+ if (await fs.pathExists(resolvedIndexPath)) {
|
|
|
+ resolvedTsPath = resolvedIndexPath;
|
|
|
+ } else {
|
|
|
+ const resolvedIndexTsxPath = path.join(resolvedPath, 'index.tsx');
|
|
|
+ if (await fs.pathExists(resolvedIndexTsxPath)) {
|
|
|
+ resolvedTsPath = resolvedIndexTsxPath;
|
|
|
+ } else {
|
|
|
+ // If still not found, log a warning or let TS handle it later
|
|
|
+ logger?.warn(
|
|
|
+ `Could not resolve relative import "${importPath}" from "${absoluteInputPath}" to an existing .ts/.tsx file.`,
|
|
|
+ );
|
|
|
+ // Do not add to importPaths if we can't verify existence
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ importPaths.add(resolvedTsPath);
|
|
|
+ }
|
|
|
+ // Handle path aliases if tsConfigInfo exists
|
|
|
+ else if (tsConfigInfo) {
|
|
|
+ // Attempt to resolve using path aliases
|
|
|
+ let resolved = false;
|
|
|
+ for (const [alias, patterns] of Object.entries(tsConfigInfo.paths)) {
|
|
|
+ const aliasPrefix = alias.replace('*', '');
|
|
|
+ const aliasSuffix = alias.endsWith('*') ? '*' : '';
|
|
|
+
|
|
|
+ if (
|
|
|
+ importPath.startsWith(aliasPrefix) &&
|
|
|
+ (aliasSuffix === '*' || importPath === aliasPrefix)
|
|
|
+ ) {
|
|
|
+ const remainingImportPath = importPath.slice(aliasPrefix.length);
|
|
|
+ for (const pattern of patterns) {
|
|
|
+ const patternPrefix = pattern.replace('*', '');
|
|
|
+ const patternSuffix = pattern.endsWith('*') ? '*' : '';
|
|
|
+ // Ensure suffix match consistency (* vs exact)
|
|
|
+ if (aliasSuffix !== patternSuffix) continue;
|
|
|
+
|
|
|
+ const potentialPathBase = path.resolve(tsConfigInfo.baseUrl, patternPrefix);
|
|
|
+ const resolvedPath = path.join(potentialPathBase, remainingImportPath);
|
|
|
+
|
|
|
+ let resolvedTsPath = resolvedPath + '.ts';
|
|
|
+ // Similar existence checks as relative paths
|
|
|
+ if (!(await fs.pathExists(resolvedTsPath))) {
|
|
|
+ const resolvedTsxPath = resolvedPath + '.tsx';
|
|
|
+ if (await fs.pathExists(resolvedTsxPath)) {
|
|
|
+ resolvedTsPath = resolvedTsxPath;
|
|
|
+ } else {
|
|
|
+ const resolvedIndexPath = path.join(resolvedPath, 'index.ts');
|
|
|
+ if (await fs.pathExists(resolvedIndexPath)) {
|
|
|
+ resolvedTsPath = resolvedIndexPath;
|
|
|
+ } else {
|
|
|
+ const resolvedIndexTsxPath = path.join(resolvedPath, 'index.tsx');
|
|
|
+ if (await fs.pathExists(resolvedIndexTsxPath)) {
|
|
|
+ resolvedTsPath = resolvedIndexTsxPath;
|
|
|
+ } else {
|
|
|
+ // Path doesn't resolve to a file for this pattern
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Add the first successful resolution for this alias
|
|
|
+ importPaths.add(resolvedTsPath);
|
|
|
+ resolved = true;
|
|
|
+ break; // Stop checking patterns for this alias
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (resolved) break; // Stop checking other aliases if resolved
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // For all other imports (node_modules, etc), we should still add them to be processed
|
|
|
+ // by the TypeScript compiler, even if we can't resolve them to a file
|
|
|
+ else {
|
|
|
+ // Add the import path as is - TypeScript will handle resolution
|
|
|
+ // importPaths.add(importPath);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const children = node.getChildren();
|
|
|
+ for (const child of children) {
|
|
|
+ // Only process nodes that could contain import statements
|
|
|
+ if (
|
|
|
+ ts.isSourceFile(child) ||
|
|
|
+ ts.isModuleBlock(child) ||
|
|
|
+ ts.isModuleDeclaration(child) ||
|
|
|
+ ts.isImportDeclaration(child) ||
|
|
|
+ child.kind === ts.SyntaxKind.SyntaxList
|
|
|
+ ) {
|
|
|
+ await collectImports(child);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- ts.forEachChild(node, collectImports);
|
|
|
}
|
|
|
|
|
|
- collectImports(sourceFile);
|
|
|
+ // Start collecting imports from the source file
|
|
|
+ await collectImports(sourceFile);
|
|
|
+
|
|
|
+ // Store the tsConfigInfo on the first call if found
|
|
|
+ const rootTsConfigInfo = isRoot ? tsConfigInfo : undefined;
|
|
|
|
|
|
// Recursively collect all files that need to be compiled
|
|
|
for (const importPath of importPaths) {
|
|
|
- await compileFile(inputRootDir, importPath, outputDir, compiledFiles, false);
|
|
|
+ // Pass rootTsConfigInfo down, but set isRoot to false
|
|
|
+ await compileFile(inputRootDir, importPath, outputDir, logger, compiledFiles, false);
|
|
|
}
|
|
|
|
|
|
// If this is the root file (the one that started the compilation),
|
|
|
- // transpile all files
|
|
|
+ // use the TypeScript compiler API to compile all files together
|
|
|
if (isRoot) {
|
|
|
+ logger.info(`Starting compilation for ${compiledFiles.size} files...`);
|
|
|
const allFiles = Array.from(compiledFiles);
|
|
|
- for (const file of allFiles) {
|
|
|
- const fileSource = await fs.readFile(file, 'utf-8');
|
|
|
- const result = ts.transpileModule(fileSource, {
|
|
|
- compilerOptions: {
|
|
|
- target: ts.ScriptTarget.ES2020,
|
|
|
- module: ts.ModuleKind.CommonJS,
|
|
|
- experimentalDecorators: true,
|
|
|
- emitDecoratorMetadata: true,
|
|
|
- sourceMap: true,
|
|
|
- esModuleInterop: true,
|
|
|
- skipLibCheck: true,
|
|
|
- forceConsistentCasingInFileNames: true,
|
|
|
- },
|
|
|
- fileName: file,
|
|
|
- });
|
|
|
+ const compilerOptions: ts.CompilerOptions = {
|
|
|
+ // Base options
|
|
|
+ target: ts.ScriptTarget.ES2020,
|
|
|
+ module: ts.ModuleKind.CommonJS, // Output CommonJS for Node compatibility
|
|
|
+ experimentalDecorators: true,
|
|
|
+ emitDecoratorMetadata: true,
|
|
|
+ esModuleInterop: true,
|
|
|
+ skipLibCheck: true, // Faster compilation
|
|
|
+ forceConsistentCasingInFileNames: true,
|
|
|
+ moduleResolution: ts.ModuleResolutionKind.NodeJs, // Use Node.js module resolution
|
|
|
+
|
|
|
+ // Output options
|
|
|
+ outDir: outputDir, // Output directory for all compiled files
|
|
|
+ sourceMap: true, // Generate source maps
|
|
|
+ declaration: false, // Don't generate .d.ts files
|
|
|
|
|
|
- // Generate output file path
|
|
|
- const relativePath = path.relative(inputRootDir, file);
|
|
|
- const outputPath = path.join(outputDir, relativePath).replace(/\.ts$/, '.js');
|
|
|
+ // Path resolution options - use info found from tsconfig
|
|
|
+ baseUrl: rootTsConfigInfo ? rootTsConfigInfo.baseUrl : undefined, // Let TS handle resolution if no baseUrl
|
|
|
+ paths: rootTsConfigInfo ? rootTsConfigInfo.paths : undefined,
|
|
|
+ // rootDir: inputRootDir, // Often inferred correctly, can cause issues if set explicitly sometimes
|
|
|
+ allowJs: true, // Allow JS files if needed, though we primarily collect TS
|
|
|
+ resolveJsonModule: true, // Allow importing JSON
|
|
|
+ };
|
|
|
|
|
|
- // Ensure the subdirectory for the output file exists
|
|
|
- await fs.ensureDir(path.dirname(outputPath));
|
|
|
+ logger.debug(`compilerOptions: ${JSON.stringify(compilerOptions, null, 2)}`);
|
|
|
|
|
|
- // Write the transpiled code
|
|
|
- await fs.writeFile(outputPath, result.outputText);
|
|
|
+ // Create a Program to represent the compilation context
|
|
|
+ const program = ts.createProgram(allFiles, compilerOptions);
|
|
|
|
|
|
- // Write source map if available
|
|
|
- if (result.sourceMapText) {
|
|
|
- await fs.writeFile(`${outputPath}.map`, result.sourceMapText);
|
|
|
+ // Perform the compilation and emit files
|
|
|
+ logger.info(`Emitting compiled files to ${outputDir}`);
|
|
|
+ const emitResult = program.emit();
|
|
|
+
|
|
|
+ // Report diagnostics (errors and warnings)
|
|
|
+ const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
|
|
|
+
|
|
|
+ let hasEmitErrors = emitResult.emitSkipped;
|
|
|
+ allDiagnostics.forEach(diagnostic => {
|
|
|
+ if (diagnostic.file && diagnostic.start) {
|
|
|
+ const { line, character } = ts.getLineAndCharacterOfPosition(
|
|
|
+ diagnostic.file,
|
|
|
+ diagnostic.start,
|
|
|
+ );
|
|
|
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
|
+ const logFn = diagnostic.category === ts.DiagnosticCategory.Error ? logger.warn : logger.info;
|
|
|
+ logFn(
|
|
|
+ `TS${diagnostic.code} ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`,
|
|
|
+ );
|
|
|
+ if (diagnostic.category === ts.DiagnosticCategory.Error) {
|
|
|
+ hasEmitErrors = true;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
|
+ const logFn = diagnostic.category === ts.DiagnosticCategory.Error ? logger.warn : logger.info;
|
|
|
+ logFn(`TS${diagnostic.code}: ${message}`);
|
|
|
+ if (diagnostic.category === ts.DiagnosticCategory.Error) {
|
|
|
+ hasEmitErrors = true;
|
|
|
+ }
|
|
|
}
|
|
|
+ });
|
|
|
+
|
|
|
+ if (hasEmitErrors) {
|
|
|
+ throw new Error('TypeScript compilation failed with errors.');
|
|
|
}
|
|
|
+
|
|
|
+ logger.info(`Successfully compiled ${allFiles.length} files to ${outputDir}`);
|
|
|
}
|
|
|
}
|