@travetto/compiler
Version:
The compiler infrastructure for the Travetto framework
281 lines (235 loc) • 10.4 kB
text/typescript
import ts from 'typescript';
import { path, ManifestModuleUtil, type ManifestModule, type ManifestRoot, ManifestIndex, ManifestModuleFolderType } from '@travetto/manifest';
import { TransformerManager } from '@travetto/transformer';
import { TypescriptUtil } from '../support/ts-util.ts';
import { CompilerUtil } from './util.ts';
import { CompileEmitError, CompileStateEntry } from './types.ts';
import { CommonUtil } from '../support/util.ts';
const TYPINGS_FOLDER_KEYS = new Set<ManifestModuleFolderType>(['$index', 'support', 'src', '$package']);
export class CompilerState implements ts.CompilerHost {
static async get(idx: ManifestIndex): Promise<CompilerState> {
return new CompilerState().init(idx);
}
/** @private */
constructor() { }
#outputPath: string;
#typingsPath: string;
#sourceFiles = new Set<string>();
#sourceDirectory = new Map<string, string>();
#sourceToEntry = new Map<string, CompileStateEntry>();
#outputToEntry = new Map<string, CompileStateEntry>();
#tscOutputFileToOuptut = new Map<string, string>();
#sourceContents = new Map<string, string | undefined>();
#sourceFileObjects = new Map<string, ts.SourceFile>();
#sourceHashes = new Map<string, number>();
#manifestIndex: ManifestIndex;
#manifest: ManifestRoot;
#modules: ManifestModule[];
#transformerManager: TransformerManager;
#compilerOptions: ts.CompilerOptions;
#program: ts.Program;
#readFile(sourceFile: string): string | undefined {
return ts.sys.readFile(this.#sourceToEntry.get(sourceFile)?.sourceFile ?? sourceFile);
}
#writeExternalTypings(location: string, text: string, bom: boolean): void {
let core = location.replace('.map', '');
if (!this.#outputToEntry.has(core)) {
core = core.replace(ManifestModuleUtil.TYPINGS_EXT_RE, ManifestModuleUtil.OUTPUT_EXT);
}
const entry = this.#outputToEntry.get(core);
if (entry) {
const relative = this.#manifestIndex.getFromSource(entry.sourceFile)?.relativeFile;
if (relative && TYPINGS_FOLDER_KEYS.has(ManifestModuleUtil.getFolderKey(relative))) {
ts.sys.writeFile(location.replace(this.#outputPath, this.#typingsPath), text, bom);
}
}
}
async init(idx: ManifestIndex): Promise<this> {
this.#manifestIndex = idx;
this.#manifest = idx.manifest;
this.#outputPath = path.resolve(this.#manifest.workspace.path, this.#manifest.build.outputFolder);
this.#typingsPath = path.resolve(this.#manifest.workspace.path, this.#manifest.build.typesFolder);
this.#compilerOptions = {
...await TypescriptUtil.getCompilerOptions(this.#manifest),
rootDir: this.#manifest.workspace.path,
outDir: this.#outputPath
};
this.#modules = Object.values(this.#manifest.modules);
// Register all inputs
for (const x of this.#modules) {
const base = x?.files ?? {};
const files = [
...base.bin ?? [],
...base.src ?? [],
...base.support ?? [],
...base.doc ?? [],
...base.test ?? [],
...base.$index ?? [],
...base.$package ?? []
];
for (const [file, type] of files) {
if (CompilerUtil.validFile(type)) {
this.registerInput(x, file);
}
}
}
this.#transformerManager = await TransformerManager.create(this.#manifestIndex);
return this;
}
get manifest(): ManifestRoot {
return this.#manifest;
}
get manifestIndex(): ManifestIndex {
return this.#manifestIndex;
}
resolveOutputFile(file: string): string {
return path.resolve(this.#manifest.workspace.path, this.#manifest.build.outputFolder, file);
}
getArbitraryInputFile(): string {
const randomSource = this.#manifestIndex.getWorkspaceModules()
.filter(x => x.files.src?.length)[0]
.files.src[0].sourceFile;
return this.getBySource(randomSource)!.sourceFile;
}
async createProgram(force = false): Promise<ts.Program> {
if (force || !this.#program) {
this.#program = ts.createProgram({ rootNames: this.getAllFiles(), host: this, options: this.#compilerOptions, oldProgram: this.#program });
this.#transformerManager.init(this.#program.getTypeChecker());
await CommonUtil.queueMacroTask();
}
return this.#program;
}
async compileSourceFile(sourceFile: string, needsNewProgram = false): Promise<CompileEmitError | undefined> {
const output = this.#sourceToEntry.get(sourceFile)?.outputFile;
if (!output) {
return;
}
const program = await this.createProgram(needsNewProgram);
try {
switch (ManifestModuleUtil.getFileType(sourceFile)) {
case 'typings':
case 'package-json':
this.writeFile(output, this.readFile(sourceFile)!, false), undefined;
break;
case 'js':
this.writeFile(output, ts.transpile(this.readFile(sourceFile)!, this.#compilerOptions), false);
break;
case 'ts': {
const result = program.emit(
program.getSourceFile(sourceFile)!,
(...args) => this.writeFile(args[0], args[1], args[2]), undefined, false,
this.#transformerManager.get()
);
return result?.diagnostics?.length ? result.diagnostics : undefined;
}
}
} catch (err) {
if (err instanceof Error) {
return err;
} else {
throw err;
}
}
}
getBySource(sourceFile: string): CompileStateEntry | undefined {
return this.#sourceToEntry.get(sourceFile);
}
registerInput(module: ManifestModule, moduleFile: string): CompileStateEntry {
const relativeSource = `${module.sourceFolder || '.'}/${moduleFile}`;
const relativeOutput = `${module.outputFolder}/${moduleFile}`;
const sourceFile = path.resolve(this.#manifest.workspace.path, relativeSource);
const sourceFolder = path.dirname(sourceFile);
const fileType = ManifestModuleUtil.getFileType(moduleFile);
const isTypings = fileType === 'typings';
const tscOutputFile = path.resolve(this.#outputPath, ManifestModuleUtil.withOutputExtension(relativeSource));
const outputFile = path.resolve(this.#outputPath, ManifestModuleUtil.withOutputExtension(relativeOutput));
const entry: CompileStateEntry = { sourceFile, outputFile, module, tscOutputFile };
this.#outputToEntry.set(outputFile, entry);
this.#sourceFiles.add(sourceFile);
this.#sourceHashes.set(sourceFile, -1); // Unknown
this.#sourceToEntry.set(sourceFile, entry);
this.#sourceDirectory.set(sourceFolder, sourceFolder);
this.#tscOutputFileToOuptut.set(tscOutputFile, outputFile);
this.#tscOutputFileToOuptut.set(`${tscOutputFile}.map`, `${outputFile}.map`);
if (!isTypings) {
const srcBase = `${ManifestModuleUtil.withoutSourceExtension(tscOutputFile)}${ManifestModuleUtil.TYPINGS_EXT}`;
const outBase = `${ManifestModuleUtil.withoutSourceExtension(outputFile)}${ManifestModuleUtil.TYPINGS_EXT}`;
this.#tscOutputFileToOuptut.set(`${srcBase}.map`, `${outBase}.map`);
this.#tscOutputFileToOuptut.set(srcBase, outBase);
}
return entry;
}
checkIfSourceChanged(sourceFile: string): boolean {
const contents = this.#readFile(sourceFile);
const prevHash = this.#sourceHashes.get(sourceFile);
if (!contents || (contents.length === 0 && prevHash)) {
return false; // Ignore empty file
}
const currentHash = CommonUtil.naiveHash(contents);
const changed = prevHash !== currentHash;
if (changed) {
this.#sourceHashes.set(sourceFile, currentHash);
this.#sourceContents.set(sourceFile, contents);
this.#sourceFileObjects.delete(sourceFile);
}
return changed;
}
removeSource(sourceFile: string): void {
const entry = this.#sourceToEntry.get(sourceFile)!;
if (entry.outputFile) {
this.#outputToEntry.delete(entry.outputFile);
}
this.#sourceFileObjects.delete(sourceFile);
this.#sourceContents.delete(sourceFile);
this.#sourceHashes.delete(sourceFile);
this.#sourceToEntry.delete(sourceFile);
this.#sourceFiles.delete(sourceFile);
const tscOutputDts = `${ManifestModuleUtil.withoutSourceExtension(entry.tscOutputFile)}${ManifestModuleUtil.TYPINGS_EXT}`;
this.#tscOutputFileToOuptut.delete(entry.tscOutputFile);
this.#tscOutputFileToOuptut.delete(`${entry.tscOutputFile}.map`);
this.#tscOutputFileToOuptut.delete(tscOutputDts);
this.#tscOutputFileToOuptut.delete(`${tscOutputDts}.map`);
}
getAllFiles(): string[] {
return [...this.#sourceFiles];
}
/* Start Compiler Host */
getCanonicalFileName(file: string): string { return file; }
getCurrentDirectory(): string { return this.#manifest.workspace.path; }
getDefaultLibFileName(opts: ts.CompilerOptions): string { return ts.getDefaultLibFileName(opts); }
getNewLine(): string { return ts.sys.newLine; }
useCaseSensitiveFileNames(): boolean { return ts.sys.useCaseSensitiveFileNames; }
getDefaultLibLocation(): string { return path.dirname(ts.getDefaultLibFilePath(this.#compilerOptions)); }
fileExists(sourceFile: string): boolean {
return this.#sourceToEntry.has(sourceFile) || ts.sys.fileExists(sourceFile);
}
directoryExists(sourceDir: string): boolean {
return this.#sourceDirectory.has(sourceDir) || ts.sys.directoryExists(sourceDir);
}
writeFile(
outputFile: string,
text: string,
bom: boolean
): void {
if (outputFile.endsWith('package.json')) {
text = CompilerUtil.rewritePackageJSON(this.#manifest, text);
}
const location = this.#tscOutputFileToOuptut.get(outputFile) ?? outputFile;
if (ManifestModuleUtil.TYPINGS_WITH_MAP_EXT_RE.test(outputFile) || outputFile.endsWith('package.json')) {
this.#writeExternalTypings(location, text, bom);
}
ts.sys.writeFile(location, text, bom);
}
readFile(sourceFile: string): string | undefined {
const res = this.#sourceContents.get(sourceFile) ?? this.#readFile(sourceFile);
this.#sourceContents.set(sourceFile, res);
return res;
}
getSourceFile(sourceFile: string, language: ts.ScriptTarget): ts.SourceFile {
if (!this.#sourceFileObjects.has(sourceFile)) {
const content = this.readFile(sourceFile)!;
this.#sourceFileObjects.set(sourceFile, ts.createSourceFile(sourceFile, content ?? '', language));
}
return this.#sourceFileObjects.get(sourceFile)!;
}
}