@travetto/compiler
Version:
The compiler infrastructure for the Travetto framework
206 lines (176 loc) • 6.7 kB
text/typescript
import fs from 'node:fs/promises';
import { setMaxListeners } from 'node:events';
import { ManifestIndex, ManifestModuleUtil } from '@travetto/manifest';
import { CompilerUtil } from './util.ts';
import { CompilerState } from './state.ts';
import { CompilerWatcher } from './watch.ts';
import { CompileEmitEvent, CompileEmitter, CompilerReset } from './types.ts';
import { EventUtil } from './event.ts';
import { IpcLogger } from '../support/log.ts';
import { CommonUtil } from '../support/util.ts';
const log = new IpcLogger({ level: 'debug' });
/**
* Compilation support
*/
export class Compiler {
/**
* Run compiler as a main entry point
*/
static async main(): Promise<void> {
const [dirty, watch] = process.argv.slice(2);
const state = await CompilerState.get(new ManifestIndex());
log.debug('Running compiler with dirty file', dirty);
const dirtyFiles = ManifestModuleUtil.getFileType(dirty) === 'ts' ? [dirty] : (await fs.readFile(dirty, 'utf8')).split(/\n/).filter(x => !!x);
log.debug('Running compiler with dirty file', dirtyFiles);
await new Compiler(state, dirtyFiles, watch === 'true').run();
}
#state: CompilerState;
#dirtyFiles: string[];
#watch?: boolean;
#ctrl: AbortController;
#signal: AbortSignal;
#shuttingDown = false;
constructor(state: CompilerState, dirtyFiles: string[], watch?: boolean) {
this.#state = state;
this.#dirtyFiles = dirtyFiles[0] === '*' ?
this.#state.getAllFiles() :
dirtyFiles.map(f => this.#state.getBySource(f)!.sourceFile);
this.#watch = watch;
this.#ctrl = new AbortController();
this.#signal = this.#ctrl.signal;
setMaxListeners(1000, this.#signal);
process
.once('disconnect', () => this.#shutdown('manual'))
.on('message', ev => (ev === 'shutdown') && this.#shutdown('manual'));
}
#shutdown(mode: 'error' | 'manual' | 'complete' | 'reset', err?: Error): void {
if (this.#shuttingDown) {
return;
}
this.#shuttingDown = true;
switch (mode) {
case 'manual': {
log.error('Shutting down manually');
process.exitCode = 2;
break;
}
case 'error': {
process.exitCode = 1;
if (err) {
EventUtil.sendEvent('log', { level: 'error', message: err.toString(), time: Date.now() });
log.error('Shutting down due to failure', err.stack);
}
break;
}
case 'reset': {
log.info('Reset due to', err?.message);
EventUtil.sendEvent('state', { state: 'reset' });
process.exitCode = 0;
break;
}
}
// No longer listen to disconnect
process.removeAllListeners('disconnect');
process.removeAllListeners('message');
this.#ctrl.abort();
CommonUtil.nonBlockingTimeout(1000).then(() => process.exit()); // Allow upto 1s to shutdown gracefully
}
/**
* Compile in a single pass, only emitting dirty files
*/
getCompiler(): CompileEmitter {
return (sourceFile: string, needsNewProgram?: boolean) => this.#state.compileSourceFile(sourceFile, needsNewProgram);
}
/**
* Emit all files as a stream
*/
async * emit(files: string[], emitter: CompileEmitter): AsyncIterable<CompileEmitEvent> {
let i = 0;
let lastSent = Date.now();
for (const file of files) {
const err = await emitter(file);
const imp = file.includes('node_modules/') ? file.split('node_modules/')[1] : file;
yield { file: imp, i: i += 1, err, total: files.length };
if ((Date.now() - lastSent) > 50) { // Limit to 1 every 50ms
lastSent = Date.now();
EventUtil.sendEvent('progress', { total: files.length, idx: i, message: imp, operation: 'compile' });
}
if (this.#signal.aborted) {
break;
}
}
EventUtil.sendEvent('progress', { total: files.length, idx: files.length, message: 'Complete', operation: 'compile', complete: true });
await CommonUtil.queueMacroTask();
log.debug(`Compiled ${i} files`);
}
/**
* Run the compiler
*/
async run(): Promise<void> {
log.debug('Compilation started');
EventUtil.sendEvent('state', { state: 'init', extra: { pid: process.pid } });
const emitter = await this.getCompiler();
let failure: Error | undefined;
log.debug('Compiler loaded');
EventUtil.sendEvent('state', { state: 'compile-start' });
if (this.#dirtyFiles.length) {
for await (const ev of this.emit(this.#dirtyFiles, emitter)) {
if (ev.err) {
const compileError = CompilerUtil.buildTranspileError(ev.file, ev.err);
failure ??= compileError;
EventUtil.sendEvent('log', { level: 'error', message: compileError.toString(), time: Date.now() });
}
}
if (this.#signal.aborted) {
log.debug('Compilation aborted');
} else if (failure) {
log.debug('Compilation failed');
return this.#shutdown('error', failure);
} else {
log.debug('Compilation succeeded');
}
} else if (this.#watch) {
// Prime compiler before complete
const resolved = this.#state.getArbitraryInputFile();
await emitter(resolved, true);
}
EventUtil.sendEvent('state', { state: 'compile-end' });
if (this.#watch && !this.#signal.aborted) {
log.info('Watch is ready');
EventUtil.sendEvent('state', { state: 'watch-start' });
try {
for await (const ev of new CompilerWatcher(this.#state, this.#signal)) {
if (ev.action !== 'delete') {
const err = await emitter(ev.entry.sourceFile, true);
if (err) {
log.info('Compilation Error', CompilerUtil.buildTranspileError(ev.entry.sourceFile, err));
} else {
log.info(`Compiled ${ev.entry.sourceFile} on ${ev.action}`);
}
} else {
if (ev.entry.outputFile) {
// Remove output
log.info(`Removed ${ev.entry.sourceFile}, ${ev.entry.outputFile}`);
await fs.rm(ev.entry.outputFile, { force: true }); // Ensure output is deleted
}
}
// Send change events
EventUtil.sendEvent('change', {
action: ev.action,
time: Date.now(),
file: ev.file,
output: ev.entry.outputFile!,
module: ev.entry.module.name
});
}
EventUtil.sendEvent('state', { state: 'watch-end' });
} catch (err) {
if (err instanceof Error) {
this.#shutdown(err instanceof CompilerReset ? 'reset' : 'error', err);
}
}
}
log.debug('Compiler process shutdown');
this.#shutdown('complete');
}
}