@tsbb/typescript
Version:
TSBB is a zero-config CLI that helps you develop, test, and publish modern TypeScript project.
165 lines (155 loc) • 6.23 kB
text/typescript
import ts from 'typescript';
import path from 'node:path';
import fs from 'fs-extra';
import {
writeFile,
getSourceFile,
reportDiagnostic,
onWatchStatusChange,
getRootsFolderName,
getSourceFilePath,
} from './utils.js';
import { Log } from './log.js';
export * from './utils.js';
export * from './log.js';
export interface CopyFilesOptions {
isWatch?: boolean;
/**
* @example `/path/to/dir/<root>/<lib>`
*/
outputDir?: string;
/**
* @example `/path/to/dir/<root>`
*/
currentDir?: string;
/**
* @example ['src', 'demo']
*/
rootDirsRelative?: string[];
onFilesChange?: (
eventName: 'all' | 'raw' | 'error' | 'ready' | 'add' | 'change' | 'addDir' | 'unlink' | 'unlinkDir',
path: string,
stats?: fs.Stats,
) => void;
onError?: (error: any) => void;
onReady?: () => void;
}
export interface TsCompileOptions {
watch?: boolean;
/**
* Exit the compile as soon as the compile fails(default: true).
* @default `true`
*/
bail?: boolean;
emitDeclarationOnly?: boolean;
/**
* @default true
*/
isCopyFiles?: boolean;
outDir?: string;
onWriteFile?: (path: string, data: string, sourceFilePath: string, writeByteOrderMark?: boolean) => void;
onCopyFiles?: (entry: string[], options: CopyFilesOptions) => void;
}
export const findConfigFile = () => ts.findConfigFile('.', ts.sys.fileExists, 'tsconfig.json');
export const readConfigFile = (tsConfigPath: string) => ts.readConfigFile(path.resolve(tsConfigPath), ts.sys.readFile);
export default async function compile(options: TsCompileOptions = {}) {
const { isCopyFiles = true, onWriteFile, onCopyFiles, bail } = options;
const tsConfigPath = findConfigFile();
const log = new Log();
if (!tsConfigPath) {
log.error(
`\n \x1b[33;1mYou are trying to compile TypeScript source code using the TypeScript compiler, \n but it cannot find the configuration file named\x1b[0m\x1b[31;1m tsconfig.json\x1b[0m.`,
);
return;
}
const { config, error } = readConfigFile(tsConfigPath);
if (error) {
return reportDiagnostic(error);
}
log.name();
const parseResult = ts.parseJsonConfigFileContent(config, ts.sys, path.dirname(tsConfigPath));
if (options.emitDeclarationOnly) {
parseResult.options.emitDeclarationOnly = true;
delete parseResult.options.noEmit;
}
if (!parseResult.options.outDir) {
parseResult.options.outDir = 'lib';
} else if (options.outDir) {
parseResult.options.outDir = options.outDir;
}
const compilerOptions = parseResult.options;
if (parseResult.errors.length) {
return parseResult.errors.forEach(reportDiagnostic);
}
// Test files are not escaped.
parseResult.fileNames = parseResult.fileNames.filter((item) => !/\.(test|spec)\.(js|jsx|ts|tsx)$/i.test(item));
const currentDir = ts.sys.getCurrentDirectory();
const rootDirsRelative = [...new Set(getRootsFolderName(parseResult.fileNames))];
const outputDir = path.resolve(currentDir, compilerOptions.outDir || 'lib');
const rootDirs = [...rootDirsRelative].map((item) => path.resolve(currentDir, item));
if (options.watch) {
const createProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram;
const system = { ...ts.sys };
const fileNameData: Record<string, string> = {};
system.readFile = (fileName, encoding) => {
if (/^(?!.*\.d\.ts$).*\.(tsx?)+$/.test(fileName)) {
const sourceFilePath = fileName.indexOf(process.cwd()) > -1 ? path.relative(process.cwd(), fileName) : fileName;
const finalPath = getSourceFilePath(fileName, rootDirsRelative);
const cur = path.resolve(path.join(outputDir, finalPath)).replace(/\.(m?js|jsx?|m?ts|tsx?|c?js)$/, '.d.ts');
fileNameData[cur] = sourceFilePath;
}
return ts.sys.readFile(fileName, encoding);
};
system.writeFile = (pathName, data, writeByteOrderMark) => {
if (options.emitDeclarationOnly && onWriteFile) {
onWriteFile(pathName, data, fileNameData[path.resolve(currentDir, pathName)], writeByteOrderMark);
} else {
writeFile(pathName, data, writeByteOrderMark);
}
};
// Note that there is another overload for `createWatchCompilerHost` that takes
// a set of root files.
const host = ts.createWatchCompilerHost(tsConfigPath, compilerOptions, system, createProgram, reportDiagnostic);
host.onWatchStatusChange = onWatchStatusChange;
// Start the TypeScript monitor compiler
ts.createWatchProgram(host);
if (isCopyFiles && onCopyFiles) {
await onCopyFiles(rootDirs, { isWatch: options.watch, outputDir, currentDir, rootDirsRelative });
}
} else {
const compilerHost = ts.createCompilerHost(compilerOptions, true);
const host: ts.CompilerHost = { ...compilerHost, getSourceFile };
host.writeFile = (fileNamePath, contents, writeByteOrderMark, onError, sourceFiles = [], data) => {
if (options.emitDeclarationOnly && onWriteFile) {
const sourceFile = sourceFiles?.find((m) => !!m.fileName);
if (!sourceFile || !/\.(d.ts)$/i.test(fileNamePath)) return;
const sourceFilePath =
sourceFile.fileName.indexOf(process.cwd()) > -1
? path.relative(process.cwd(), sourceFile.fileName)
: sourceFile.fileName;
onWriteFile(fileNamePath, contents, sourceFilePath, writeByteOrderMark);
return;
} else {
writeFile(fileNamePath, contents, writeByteOrderMark);
}
};
const program = ts.createProgram(parseResult.fileNames, compilerOptions, host);
const emitResult = program.emit();
const diagnostics = ts.getPreEmitDiagnostics(program);
diagnostics.forEach(reportDiagnostic);
if (bail && diagnostics.length) {
diagnostics.forEach(reportDiagnostic);
process.exitCode = 1;
}
if (isCopyFiles && onCopyFiles) {
await onCopyFiles(rootDirs, { isWatch: options.watch, outputDir, currentDir, rootDirsRelative });
}
if (!options.emitDeclarationOnly && onWriteFile) {
if (emitResult.emitSkipped) {
log.icon('\n🚨').error('\x1b[33;1m Compilation failed!!!\x1b[0m\n');
} else {
log.icon('\n🎉').error('\x1b[32;1mCompilation successful!\x1b[0m\n');
}
}
}
}