import watcher from "@parcel/watcher";
import path from "path";
import ts from "typescript";
import { FILTER_FILENAME, FIXTURES_PATH } from "./constants";
import { TestFilter, readTestFilter } from "./fixture-utils";
export function watchSrc(
onStart: () => void,
onComplete: (isSuccess: boolean) => void
): ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> {
const configPath = ts.findConfigFile(
"./",
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,
ts.convertCompilerOptionsFromJson(
{ module: "commonjs", outDir: "dist" },
"."
).options,
ts.sys,
createProgram,
() => {},
() => {}
);
const origCreateProgram = host.createProgram;
host.createProgram = (rootNames, options, host, oldProgram) => {
onStart();
return origCreateProgram(rootNames, options, host, oldProgram);
};
const origPostProgramCreate = host.afterProgramCreate;
host.afterProgramCreate = (program) => {
origPostProgramCreate!(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(process.cwd(), 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...");
},
(isSuccess) => {
if (isSuccess) {
state.compilerVersion++;
}
state.isCompilerBuildValid = isSuccess;
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);
}