@ts-bridge/cli
Version:
Bridge the gap between ES modules and CommonJS modules with an easy-to-use alternative to `tsc`.
337 lines (336 loc) • 14.1 kB
JavaScript
import { dirname } from 'path';
import typescript from 'typescript';
import { getBuildTypeOptions } from './build-type.js';
import { getBaseCompilerOptions, getCompilerOptions } from './config.js';
import { TypeScriptError } from './errors.js';
import { getWriteFileFunction, removeDirectory } from './file-system.js';
import { getLoggingTransformer, info } from './logging.js';
import { createGraph, createProjectReferencesCompilerHost, getResolvedProjectReferences, } from './project-references.js';
import { executeSteps } from './steps.js';
import { getDynamicImportExtensionTransformer, getExportExtensionTransformer, getImportExtensionTransformer, getRequireExtensionTransformer, getTypeImportExportTransformer, } from './transformers.js';
import { getDefinedArray, parallelise } from './utils.js';
import { getWorkerBuildFunction } from './worker-utils.js';
const { createProgram, getPreEmitDiagnostics, ModuleResolutionKind } = typescript;
/**
* Get the TypeScript program for the project. This function will create a new
* program using the provided options and files, and perform a pre-emit
* diagnostics check to ensure the project is valid.
*
* @param options - The options.
* @param options.compilerOptions - The compiler options to use.
* @param options.projectReferences - The project references to use.
* @param options.files - The files to include in the program.
* @param options.oldProgram - The old program to reuse.
* @param options.host - The compiler host to use.
* @returns The TypeScript program for the project.
*/
export function getProgram({ compilerOptions, projectReferences, files, oldProgram, host, }) {
const program = createProgram({
rootNames: files,
options: compilerOptions,
projectReferences,
oldProgram,
host,
});
// Check for pre-emit diagnostics, which includes syntax errors, type errors,
// and other issues that would prevent the build from starting.
const preEmitDiagnostics = getPreEmitDiagnostics(program);
if (preEmitDiagnostics.length > 0) {
throw new TypeScriptError('Failed to initialise the project.', preEmitDiagnostics);
}
return program;
}
/**
* Get the initial compiler host to use for the build. This function will return
* the host to use based on the build options.
*
* @param options - The options.
* @param options.format - The formats to build.
* @param options.compilerOptions - The compiler options to use.
* @param options.system - The file system to use.
* @param options.projectReferences - The project references to use.
* @returns The initial compiler host to use for the build.
*/
function getInitialCompilerHost({ format, compilerOptions, system, projectReferences, }) {
if (getDefinedArray(projectReferences).length === 0) {
return undefined;
}
const mockProgram = createProgram({
rootNames: [],
options: {},
projectReferences,
});
return createProjectReferencesCompilerHost(format, compilerOptions, getDefinedArray(mockProgram.getResolvedProjectReferences()), system);
}
/**
* Get the initial program for the project. This function will create the
* initial program using the provided options.
*
* @param options - The options.
* @param options.project - The path to the project's `tsconfig.json` file.
* @param options.format - The formats to build.
* @param options.system - The system to use.
* @param options.tsConfig - The TypeScript configuration.
* @returns The initial program for the project.
*/
export function getInitialProgram({ project, format, system, tsConfig, }) {
const baseOptions = getBaseCompilerOptions(dirname(project), tsConfig.options);
const initialHost = getInitialCompilerHost({
format,
compilerOptions: baseOptions,
system,
projectReferences: tsConfig.projectReferences,
});
const compilerOptions = getCompilerOptions(baseOptions);
return getProgram({
compilerOptions,
files: tsConfig.fileNames,
host: initialHost,
projectReferences: tsConfig.projectReferences,
});
}
/**
* Clean the output directory before building the project.
*
* @param project - The path to the project's `tsconfig.json` file.
* @param options - The compiler options to use.
* @param clean - Whether to clean the output directory before building.
* @param verbose - Whether to enable verbose logging.
*/
export function cleanOutputDirectory(project, options, clean, verbose) {
const baseDirectory = dirname(project);
if (clean && options.outDir) {
verbose && info(`Cleaning output directory "${options.outDir}".`);
removeDirectory(options.outDir, baseDirectory);
}
}
/**
* Build the project using the Node.js 10 module resolution strategy. This
* function will build the project using the specified formats. Note that this
* function is slower than {@link buildNode16} because it creates a new program
* for each format.
*
* @param options - The build options.
* @param options.name - The name of the project to build.
* @param options.program - The TypeScript program to build.
* @param options.projectReferences - The project references to use.
* @param options.compilerOptions - The compiler options to use.
* @param options.format - The formats to build.
* @param options.system - The file system to use.
* @param options.host - The compiler host to use.
* @param options.verbose - Whether to enable verbose logging.
* @param options.shims - Whether to generate shims for environment-specific
* APIs.
* @param options.tsConfig - The TypeScript configuration.
* @param options.baseDirectory - The base directory of the project.
*/
export function buildNode10({ name, program, projectReferences, compilerOptions, format, system, host, verbose, shims, tsConfig, baseDirectory, }) {
const buildSteps = [
{
name: `Building ES module "${name}".`,
condition: () => format.includes('module'),
task: () => {
const newProgram = getProgram({
compilerOptions: {
...compilerOptions,
module: typescript.ModuleKind.ES2022,
// `ModuleResolutionKind.NodeJs` is deprecated in TypeScript 5 and
// later, but TypeScript 4 doesn't support
// `ModuleResolutionKind.Node10`.
moduleResolution: ModuleResolutionKind.NodeJs,
},
projectReferences,
files: tsConfig.fileNames,
oldProgram: program,
host,
});
build({
program: newProgram,
type: 'module',
system,
verbose,
shims,
baseDirectory,
});
},
},
{
name: `Building CommonJS module "${name}".`,
condition: () => format.includes('commonjs'),
task: () => {
const newProgram = getProgram({
compilerOptions: {
...compilerOptions,
module: typescript.ModuleKind.CommonJS,
// `ModuleResolutionKind.NodeJs` is deprecated in TypeScript 5 and
// later, but TypeScript 4 doesn't support
// `ModuleResolutionKind.Node10`.
moduleResolution: ModuleResolutionKind.NodeJs,
},
projectReferences,
files: tsConfig.fileNames,
oldProgram: program,
host,
});
build({
program: newProgram,
type: 'commonjs',
system,
verbose,
shims,
baseDirectory,
});
},
},
];
executeSteps(buildSteps, {}, verbose);
}
/**
* Build the project using the Node.js 16 module resolution strategy.
*
* @param options - The build options.
* @param options.name - The name of the project to build.
* @param options.program - The TypeScript program to build.
* @param options.format - The formats to build.
* @param options.system - The file system to use.
* @param options.verbose - Whether to enable verbose logging.
* @param options.shims - Whether to generate shims for environment-specific
* APIs.
* @param options.baseDirectory - The base directory of the project.
*/
export function buildNode16({ name, program, format, system, verbose, shims, baseDirectory, }) {
const buildSteps = [
{
name: `Building ES module "${name}".`,
condition: () => format.includes('module'),
task: () => {
build({
program,
type: 'module',
system,
shims,
verbose,
baseDirectory,
});
},
},
{
name: `Building CommonJS module "${name}".`,
condition: () => format.includes('commonjs'),
task: () => {
build({
program,
type: 'commonjs',
system,
shims,
verbose,
baseDirectory,
});
},
},
];
executeSteps(buildSteps, {}, verbose);
}
/**
* Build the project references. This function will build the project references
* using the specified formats.
*
* @param options - The build options.
*/
export async function buildProjectReferences(options) {
const { name, tsConfig, program, format, baseDirectory, verbose, shims, clean, } = options;
const resolvedProjectReferences = getDefinedArray(program.getResolvedProjectReferences());
const graph = createGraph(resolvedProjectReferences);
const sortedProjectReferences = getResolvedProjectReferences(baseDirectory, resolvedProjectReferences);
const fn = getWorkerBuildFunction({
parentBaseDirectory: baseDirectory,
format,
verbose,
shims,
clean,
});
await parallelise(sortedProjectReferences, graph, fn);
verbose &&
info(`All project references built. Building main project "${name}".`);
const buildFunction = getBuildFunction(tsConfig, false);
await buildFunction(options);
}
/**
* Get the build function to use based on the TypeScript configuration. This
* function will return the appropriate build function based on whether project
* references are used and the module resolution strategy.
*
* @param tsConfig - The TypeScript configuration.
* @param useReferences - Whether to include project references in the build.
* @returns The build function to use.
*/
export function getBuildFunction(tsConfig, useReferences = false) {
if (useReferences && tsConfig.projectReferences) {
return buildProjectReferences;
}
if (tsConfig.options.moduleResolution !== ModuleResolutionKind.Node16 &&
tsConfig.options.moduleResolution !== ModuleResolutionKind.NodeNext) {
return buildNode10;
}
return buildNode16;
}
/**
* Get the transformers to use for the build. This function will return the
* transformers to use based on the build type and whether shims are enabled.
* If shims are enabled, the shims transformers will be included in the list.
*
* @param type - The build type to use.
* @param options - The transformer options.
* @param useShims - Whether to generate shims for environment-specific APIs.
* @returns The transformers to use for the build.
*/
export function getTransformers(type, options, useShims) {
const { getTransformers: getBaseTransformers, getShimsTransformers } = getBuildTypeOptions(type);
const baseTransformers = getBaseTransformers(options);
if (useShims) {
return [...baseTransformers, ...getShimsTransformers(options)];
}
return baseTransformers;
}
/**
* Build the project. This function will compile the project using the
* TypeScript compiler.
*
* @param options - The build options.
* @param options.program - The TypeScript program to build.
* @param options.type - The build type to use.
* @param options.system - The file system to use.
* @param options.verbose - Whether to enable verbose logging.
* @param options.shims - Whether to generate shims for environment-specific
* APIs.
* @param options.baseDirectory - The base directory of the project.
* @returns A promise that resolves when the build is complete.
*/
export function build({ program, type, system, verbose, shims, baseDirectory, }) {
const { name, extension } = getBuildTypeOptions(type);
const options = {
typeChecker: program.getTypeChecker(),
system,
};
const { diagnostics } = program.emit(undefined, getWriteFileFunction(type, program.getCompilerOptions(), system, verbose), undefined, undefined, {
before: [
getLoggingTransformer(baseDirectory, verbose),
getRequireExtensionTransformer(extension, options),
getImportExtensionTransformer(extension, options),
getDynamicImportExtensionTransformer(extension, options),
getExportExtensionTransformer(extension, options),
getTypeImportExportTransformer(options),
...getTransformers(type, options, shims),
],
afterDeclarations: [
getImportExtensionTransformer(extension, options),
getDynamicImportExtensionTransformer(extension, options),
getExportExtensionTransformer(extension, options),
],
});
/* istanbul ignore if -- @preserve */
if (diagnostics.length > 0) {
throw new TypeScriptError(`Failed to build ${name} files.`, diagnostics);
}
return program;
}