UNPKG

@ts-bridge/cli

Version:

Bridge the gap between ES modules and CommonJS modules with an easy-to-use alternative to `tsc`.

226 lines (225 loc) 9.09 kB
import assert from 'assert'; import { relative } from 'path'; import typescript from 'typescript'; import { getBuildTypeOptions } from './build-type.js'; import { getTypeScriptConfig } from './config.js'; import { getCanonicalFileName } from './file-system.js'; import { getDefinedArray } from './utils.js'; const { createCompilerHost, getOutputFileNames, resolveModuleName } = typescript; /** * Create a dependency graph from the resolved project references. * * @param resolvedProjectReferences - The resolved project references of the * package that is being built. * @returns The dependency graph. */ export function createGraph(resolvedProjectReferences) { const graph = new Map(); for (const projectReference of resolvedProjectReferences) { graph.set(projectReference, getDefinedArray(projectReference.references)); } return graph; } /** * Topologically sort a dependency graph, i.e., sort the nodes in the graph such * that all dependencies of a node come before the node itself. * * @param graph - The dependency graph to sort. * @returns The topologically sorted nodes and an array of cycles (if any). */ export function topologicalSort(graph) { const stack = []; const visited = new Set(); const recursionStack = new Set(); const cycles = []; /** * Visit a node in the graph. * * @param node - The node to visit. * @param path - The current path in the graph. */ function visit(node, path) { if (recursionStack.has(node)) { cycles.push([...path, node]); return; } if (visited.has(node)) { return; } recursionStack.add(node); visited.add(node); // The graph only contains dependencies that are referenced in the parent // `tsconfig.json`. If it's not referenced there, we can assume that it // doesn't have any dependencies. const neighbours = graph.get(node) ?? []; neighbours.forEach((neighbour) => visit(neighbour, [...path, node])); recursionStack.delete(node); stack.push(node); } for (const node of graph.keys()) { visit(node, []); } return { stack, cycles, }; } /** * Get the error message for a dependency cycle. * * @param baseDirectory - The base directory path. * @param cycles - The cycles in the dependency graph. * @returns The error message. */ export function getCyclesError(baseDirectory, cycles) { const cyclesMessage = cycles .map((cycle) => `- ${cycle .map((reference) => relative(baseDirectory, reference.sourceFile.fileName)) .join(' -> ')}`) .join('\n'); return `Unable to build project references due to one or more dependency cycles:\n${cyclesMessage}`; } /** * Get the resolved project references from a TypeScript program. * * @param baseDirectory - The base directory path. * @param resolvedProjectReferences - The resolved project references of the * package that is being built. * @returns The resolved project references. */ export function getResolvedProjectReferences(baseDirectory, resolvedProjectReferences) { const graph = createGraph(resolvedProjectReferences); const { stack, cycles } = topologicalSort(graph); if (cycles.length > 0) { throw new Error(getCyclesError(baseDirectory, cycles)); } return stack; } /** * Get the output file paths for a list of files. * * @param files - The list of files. * @param options - The compiler options. * @returns The output file paths. */ function getOutputPaths(files, options) { return files.flatMap((fileName) => getOutputFileNames(options, fileName, false)); } /** * Resolve the input and output file paths of the project references. This not * only resolves the input and output files of direct project references, but * also of the nested project references. * * @param options - The compiler options. * @param inputs - The set of input files to add to. * @param outputs - The set of output files to add to. * @returns A tuple containing an array of input files and an array of output * files. */ function resolveProjectReferenceFiles(options, inputs, outputs) { /* eslint-disable @typescript-eslint/unbound-method */ options.fileNames.forEach(inputs.add, inputs); getOutputPaths(options.fileNames, options).forEach(outputs.add, outputs); if (options.projectReferences) { for (const reference of options.projectReferences) { const referenceOptions = getTypeScriptConfig(reference.path); referenceOptions.fileNames.forEach(inputs.add, inputs); getOutputPaths(referenceOptions.fileNames, referenceOptions).forEach(outputs.add, outputs); resolveProjectReferenceFiles(referenceOptions, inputs, outputs); } } /* eslint-enable @typescript-eslint/unbound-method */ return [Array.from(inputs), Array.from(outputs)]; } /** * Get a list of the output file paths in the referenced projects. * * @param resolvedProjectReferences - The resolved project references of the * package that is being built. * @returns A list of output paths. */ function getReferencedProjectPaths(resolvedProjectReferences) { const inputs = new Set(); const outputs = new Set(); for (const reference of resolvedProjectReferences) { const referenceOptions = getTypeScriptConfig(reference.sourceFile.fileName); resolveProjectReferenceFiles(referenceOptions, inputs, outputs); } return [Array.from(inputs), Array.from(outputs)]; } /** * Get the module name from a string, based on the containing file and the list * of input files. * * If the containing file is in the list of input files, the module name * extension is replaced with `.js`. Otherwise, the module name is returned as * is. * * @param originalName - The original name of the module. * @param containingFile - The containing file. * @param inputs - The list of input files. * @returns The module name as string. */ function getModuleName(originalName, containingFile, inputs) { if (inputs.includes(containingFile)) { return originalName.replace(/\.[cm]js$/u, '.js'); } return originalName; } /** * Create a compiler host that can be used to build projects using * project references. * * This is almost the same as the default compiler host, but it modifies a few * functions to redirect TypeScript to the `.[cm]ts` and `.[cm]js` files of the * referenced projects, depending on the used format. * * @param format - The format of the output files. * @param compilerOptions - The compiler options to use. * @param resolvedProjectReferences - The resolved project references of the * package that is being built. * @param system - The TypeScript system to use. * @returns The compiler host. */ export function createProjectReferencesCompilerHost(format, compilerOptions, resolvedProjectReferences, system) { assert(format[0]); const { sourceExtension } = getBuildTypeOptions(format[0]); const compilerHost = createCompilerHost(compilerOptions); const originalGetSourceFile = compilerHost.getSourceFile.bind(compilerHost); const [inputs, outputs] = getReferencedProjectPaths(resolvedProjectReferences); const getSourceFile = (fileName, ...args) => { if (!outputs.includes(fileName)) { return originalGetSourceFile(fileName, ...args); } // TypeScript checks the referenced distribution files to see if the project // is built. We simply point it to the `.d.cts` files instead of the `.d.ts` // files. return originalGetSourceFile(fileName.replace(/\.ts$/u, sourceExtension), ...args); }; const cache = typescript.createModuleResolutionCache(process.cwd(), (fileName) => getCanonicalFileName(fileName, system)); const resolveModuleNameLiterals = (moduleLiterals, containingFile, redirectedReference, options) => { return moduleLiterals.map((moduleLiteral) => { const name = getModuleName(moduleLiteral.text, containingFile, inputs); return resolveModuleName(name, containingFile, options, // eslint-disable-next-line @typescript-eslint/no-use-before-define host, cache, redirectedReference); }); }; const resolveModuleNames = (moduleNames, containingFile, _reusedNames, redirectedReference, options) => { return moduleNames.map((moduleName) => { const name = getModuleName(moduleName, containingFile, inputs); return resolveModuleName(name, containingFile, options, // eslint-disable-next-line @typescript-eslint/no-use-before-define host, cache, redirectedReference).resolvedModule; }); }; const host = { ...compilerHost, getSourceFile, resolveModuleNameLiterals, // `resolveModuleNames` is deprecated since TypeScript 5.0, but for // compatibility with TypeScript 4.x, we need to keep it. resolveModuleNames, }; return host; }