@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
JavaScript
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;
}