typescript-assistant
Version:
Combines and integrates professional Typescript tools into your project
176 lines (156 loc) • 5.1 kB
text/typescript
import * as fs from "fs";
import { parallelLimit } from "async";
import * as glob from "glob";
import { Bus } from "./bus";
import { Logger } from "./logger";
import { Task, TaskRunner } from "./taskrunner";
import { absolutePath } from "./util";
export interface Compiler {
start(configs?: string[]): void;
stop(): void;
runOnce(tscArgs: string[], disabledProjects?: string[]): Promise<boolean>;
}
type TaskFunctionCallback = () => void;
type TaskFunction = (callback: TaskFunctionCallback) => void;
let runningTasks: Task[] = [];
export function createCompiler(dependencies: {
taskRunner: TaskRunner;
logger: Logger;
bus: Bus;
}): Compiler {
const { taskRunner, logger, bus } = dependencies;
let busyCompilers = 0;
let errors: string[] = [];
function handleOutput(line: string) {
if (/Starting incremental compilation...$/.test(line)) {
if (busyCompilers === 0) {
bus.report({ tool: "compiler", status: "busy" });
errors = [];
}
busyCompilers++;
logger.log("compiler", "compiling...");
bus.signal("compile-started");
} else if (/Watching for file changes.$/.test(line)) {
busyCompilers--;
logger.log("compiler", `ready, found ${errors.length} errors`);
bus.signal(errors.length === 0 ? "compile-compiled" : "compile-errored");
if (busyCompilers === 0) {
bus.report({
tool: "compiler",
status: "ready",
errors: errors.length,
});
}
} else {
let matches = /([^(]+)\((\d+),(\d+)\): (error TS\d+: )?(.*)$/.exec(line);
if (matches) {
errors.push(matches[0]);
logger.log(
"compiler",
`${absolutePath(matches[1])}:${matches[2]}:${matches[3]} ${
matches[5]
}`
);
} else {
matches = /error TS\d+: (.+)$/.exec(line);
if (matches) {
errors.push(matches[1]);
logger.log("compiler", `${matches[1]}`);
} else {
// just echo the output
logger.log("compiler", line);
}
}
}
return true;
}
let taskFunctions: TaskFunction[] = [];
return {
runOnce(tscArgs, disabledProjects = []) {
return new Promise((resolve, reject) => {
glob(
"**/tsconfig.json",
{ ignore: "**/node_modules/**" },
(error: Error | null, tsConfigFiles: string[]) => {
if (error) {
reject(error);
}
tsConfigFiles = tsConfigFiles
.filter((file) => {
return !disabledProjects.includes(file.split("/")[0]);
})
.sort((a, b) => {
if (a.includes("test") && !b.includes("test")) {
return 1;
}
if (!a.includes("test") && b.includes("test")) {
return -1;
}
return 0;
});
let groupedConfigs = tsConfigFiles.reduce((result, file) => {
let key = file.replace("/test", "");
result[key] ??= [];
result[key].push(file);
return result;
}, <Record<string, string[]>>{});
Object.values(groupedConfigs).forEach((files) => {
let args = ["--build", ...files];
let taskFunction = (callback: TaskFunctionCallback) => {
let task = taskRunner.runTask("./node_modules/.bin/tsc", args, {
name: `tsc --build ${files.join(" ")}`,
logger,
handleOutput,
});
runningTasks.push(task);
task.result
.then(() => {
runningTasks.splice(runningTasks.indexOf(task), 1);
})
.then(callback)
.catch(reject);
};
taskFunctions.push(taskFunction);
});
let limit = 2;
parallelLimit(taskFunctions, limit, resolve);
}
);
});
},
/**
* Watching all tsconfig.json files has proven to cost too much CPU.
*/
start(tsConfigFiles = ["./tsconfig.json", "./src/tsconfig.json"]) {
tsConfigFiles = tsConfigFiles.map((config) =>
config.replace(/\\\\/g, "/")
);
tsConfigFiles.forEach((tsconfigFile) => {
if (!fs.existsSync(tsconfigFile)) {
throw new Error(`File does not exist: ${tsconfigFile}`);
}
});
let task = taskRunner.runTask(
"./node_modules/.bin/tsc",
["-b", ...tsConfigFiles, "--watch", "--preserveWatchOutput"],
{
name: `tsc --build ${tsConfigFiles.join(" ")} --watch`,
logger,
handleOutput,
}
);
runningTasks.push(task);
busyCompilers++;
task.result.catch((err) => {
logger.error("compiler", err.message);
process.exit(1);
});
},
stop() {
runningTasks.forEach((task) => {
task.kill();
});
runningTasks = [];
},
};
}