import watcher from '@parcel/watcher';
import path from 'path';
import ts from 'typescript';
import {FILTER_FILENAME, FIXTURES_PATH, PROJECT_ROOT} from './constants';
import {TestFilter, readTestFilter} from './fixture-utils';
import {execSync} from 'child_process';
export function watchSrc(
onStart: () => void,
onComplete: (isSuccess: boolean) => void,
): ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> {
const configPath = ts.findConfigFile(
PROJECT_ROOT,
ts.sys.fileExists,
'tsconfig.json',
);
if (!configPath) {
throw new Error("Could not find a valid 'tsconfig.json'.");
}
const createProgram = ts.createSemanticDiagnosticsBuilderProgram;
const host = ts.createWatchCompilerHost(
configPath,
undefined,
ts.sys,
createProgram,
() => {},
() => {},
);
const origCreateProgram = host.createProgram;
host.createProgram = (rootNames, options, host, oldProgram) => {
onStart();
return origCreateProgram(rootNames, options, host, oldProgram);
};
host.afterProgramCreate = program => {
const errors = program
.getSyntacticDiagnostics()
.filter(diag => diag.category === ts.DiagnosticCategory.Error);
errors.push(
...program
.getSemanticDiagnostics()
.filter(diag => diag.category === ts.DiagnosticCategory.Error),
);
if (errors.length > 0) {
for (const diagnostic of errors) {
let fileLoc: string;
if (diagnostic.file) {
const {line, character} = ts.getLineAndCharacterOfPosition(
diagnostic.file,
diagnostic.start!,
);
const fileName = path.relative(
ts.sys.getCurrentDirectory(),
diagnostic.file.fileName,
);
fileLoc = `${fileName}:${line + 1}:${character + 1} - `;
} else {
fileLoc = '';
}
console.error(
`${fileLoc}error TS${diagnostic.code}:`,
ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
);
}
console.error(
`Compilation failed (${errors.length} ${
errors.length > 1 ? 'errors' : 'error'
}).\n`,
);
}
const isSuccess = errors.length === 0;
onComplete(isSuccess);
};
return ts.createWatchProgram(host);
}
export enum RunnerAction {
Test = 'Test',
Update = 'Update',
}
type RunnerMode = {
action: RunnerAction;
filter: boolean;
};
export type RunnerState = {
compilerVersion: number;
isCompilerBuildValid: boolean;
lastUpdate: number;
mode: RunnerMode;
filter: TestFilter | null;
};
function subscribeFixtures(
state: RunnerState,
onChange: (state: RunnerState) => void,
) {
watcher.subscribe(FIXTURES_PATH, async (err, _events) => {
if (err) {
console.error(err);
process.exit(1);
}
const isRealUpdate = performance.now() - state.lastUpdate > 5000;
if (isRealUpdate) {
state.mode.action = RunnerAction.Test;
onChange(state);
}
});
}
function subscribeFilterFile(
state: RunnerState,
onChange: (state: RunnerState) => void,
) {
watcher.subscribe(PROJECT_ROOT, async (err, events) => {
if (err) {
console.error(err);
process.exit(1);
} else if (
events.findIndex(event => event.path.includes(FILTER_FILENAME)) !== -1
) {
if (state.mode.filter) {
state.filter = await readTestFilter();
state.mode.action = RunnerAction.Test;
onChange(state);
}
}
});
}
function subscribeTsc(
state: RunnerState,
onChange: (state: RunnerState) => void,
) {
watchSrc(
function onStart() {
console.log('\nCompiling...');
},
isTypecheckSuccess => {
let isCompilerBuildValid = false;
if (isTypecheckSuccess) {
try {
execSync('yarn build', {cwd: PROJECT_ROOT});
console.log('Built compiler successfully with tsup');
isCompilerBuildValid = true;
} catch (e) {
console.warn('Failed to build compiler with tsup:', e);
}
}
if (isCompilerBuildValid) {
state.compilerVersion++;
}
state.isCompilerBuildValid = isCompilerBuildValid;
state.mode.action = RunnerAction.Test;
onChange(state);
},
);
}
function subscribeKeyEvents(
state: RunnerState,
onChange: (state: RunnerState) => void,
) {
process.stdin.on('keypress', async (str, key) => {
if (key.name === 'u') {
state.mode.action = RunnerAction.Update;
} else if (key.name === 'q') {
process.exit(0);
} else if (key.name === 'f') {
state.mode.filter = !state.mode.filter;
state.filter = state.mode.filter ? await readTestFilter() : null;
state.mode.action = RunnerAction.Test;
} else {
state.mode.action = RunnerAction.Test;
}
onChange(state);
});
}
export async function makeWatchRunner(
onChange: (state: RunnerState) => void,
filterMode: boolean,
): Promise<void> {
const state = {
compilerVersion: 0,
isCompilerBuildValid: false,
lastUpdate: -1,
mode: {
action: RunnerAction.Test,
filter: filterMode,
},
filter: filterMode ? await readTestFilter() : null,
};
subscribeTsc(state, onChange);
subscribeFixtures(state, onChange);
subscribeKeyEvents(state, onChange);
subscribeFilterFile(state, onChange);
}