mirror of
https://github.com/hanzoai/babel.git
synced 2026-09-24 16:56:45 +00:00
263 lines
7.2 KiB
TypeScript
263 lines
7.2 KiB
TypeScript
import { createRequire } from "node:module";
|
|
const require = createRequire(import.meta.url);
|
|
|
|
import {
|
|
resolve as resolvePath,
|
|
relative as relativePath,
|
|
join as joinPath,
|
|
dirname,
|
|
} from "node:path";
|
|
import { readFileSync, copyFileSync, mkdirSync } from "node:fs";
|
|
import cp from "node:child_process";
|
|
import os from "node:os";
|
|
|
|
import JSON5 from "json5";
|
|
import { glob } from "glob";
|
|
import { setTimeout } from "node:timers/promises";
|
|
|
|
const tscPath = require.resolve("typescript/lib/tsc.js");
|
|
|
|
const entryProjectsPaths = new Set(
|
|
process.argv.slice(2).map(project => resolvePath(process.cwd(), project))
|
|
);
|
|
|
|
const ready = new Set<string>();
|
|
const projectToDependencies = new Map<string, Set<string>>();
|
|
{
|
|
const queue = Array.from(entryProjectsPaths);
|
|
for (let i = 0; i < queue.length; i++) {
|
|
const project = queue[i];
|
|
if (projectToDependencies.has(project) || ready.has(project)) continue;
|
|
|
|
const tsconfig = JSON5.parse(
|
|
readFileSync(resolvePath(project, "tsconfig.json"), "utf8")
|
|
);
|
|
const references = tsconfig.references.map(({ path }: { path: string }) =>
|
|
resolvePath(project, path)
|
|
);
|
|
|
|
if (references.length === 0) {
|
|
ready.add(project);
|
|
} else {
|
|
projectToDependencies.set(project, new Set(references));
|
|
queue.push(...references);
|
|
}
|
|
}
|
|
}
|
|
|
|
const projectToDependents = new Map<string, Set<string>>();
|
|
{
|
|
for (const [project, references] of projectToDependencies) {
|
|
for (const reference of references) {
|
|
let dependents = projectToDependents.get(reference);
|
|
if (!dependents) {
|
|
dependents = new Set();
|
|
projectToDependents.set(reference, dependents);
|
|
}
|
|
dependents.add(project);
|
|
}
|
|
}
|
|
}
|
|
|
|
class Pool {
|
|
#filename;
|
|
#concurrency;
|
|
|
|
#onTaskStart;
|
|
#onTaskDone;
|
|
#onTaskError;
|
|
#showOutput;
|
|
|
|
#maxWeight;
|
|
#getWeight;
|
|
|
|
#running = new Map<cp.ChildProcess, number>();
|
|
#pending: string[] = [];
|
|
#emptyListeners: (() => void)[] = [];
|
|
#queued = false;
|
|
|
|
constructor({
|
|
filename,
|
|
concurrency,
|
|
onTaskStart,
|
|
onTaskDone,
|
|
onTaskError,
|
|
showOutput,
|
|
maxWeight,
|
|
taskWeight,
|
|
}: {
|
|
filename: string;
|
|
concurrency: number;
|
|
onTaskStart: (tasks: string[], weight: number) => void;
|
|
onTaskDone: (
|
|
tasks: string[],
|
|
code: number,
|
|
duration: number
|
|
) => Promise<void>;
|
|
onTaskError: (tasks: string[]) => void;
|
|
showOutput: (tasks: string[]) => boolean;
|
|
maxWeight: number;
|
|
taskWeight: (task: string) => number;
|
|
}) {
|
|
this.#filename = filename;
|
|
this.#concurrency = concurrency;
|
|
this.#onTaskStart = onTaskStart;
|
|
this.#onTaskDone = onTaskDone;
|
|
this.#onTaskError = onTaskError;
|
|
this.#showOutput = showOutput;
|
|
this.#maxWeight = maxWeight;
|
|
this.#getWeight = taskWeight;
|
|
}
|
|
|
|
run(task: string) {
|
|
this.#pending.push(task);
|
|
if (!this.#queued) {
|
|
this.#queued = true;
|
|
queueMicrotask(() => {
|
|
this.#runTasks();
|
|
});
|
|
}
|
|
}
|
|
|
|
#runNextTask() {
|
|
if (this.#pending.length === 0) return;
|
|
|
|
const tasks = [this.#pending[0]];
|
|
let weight = this.#getWeight(tasks[0]);
|
|
const toRemove = new Set([0]);
|
|
for (let i = 1; i < this.#pending.length && weight < this.#maxWeight; i++) {
|
|
const task = this.#pending[i];
|
|
const taskWeight = this.#getWeight(task);
|
|
if (weight + taskWeight <= this.#maxWeight) {
|
|
weight += taskWeight;
|
|
tasks.push(task);
|
|
toRemove.add(i);
|
|
}
|
|
}
|
|
this.#pending = this.#pending.filter((_, i) => !toRemove.has(i));
|
|
|
|
const taskProcess = cp.fork(this.#filename, ["-b", ...tasks, "--pretty"], {
|
|
stdio: this.#showOutput(tasks) ? "inherit" : "ignore",
|
|
});
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
taskProcess.once("exit", async code => {
|
|
const startTime = this.#running.get(taskProcess)!;
|
|
const duration = performance.now() - startTime;
|
|
await this.#onTaskDone(tasks, code!, duration);
|
|
this.#running.delete(taskProcess);
|
|
|
|
// Spin the event loop so that #run can pick up the next task
|
|
await setTimeout(0);
|
|
|
|
this.#runTasks();
|
|
|
|
if (this.#running.size === 0 && this.#pending.length === 0) {
|
|
this.#emptyListeners.forEach(f => f());
|
|
}
|
|
});
|
|
|
|
taskProcess.once("error", err => {
|
|
console.error("Error in task process:", err);
|
|
this.#onTaskError(tasks);
|
|
this.#running.delete(taskProcess);
|
|
});
|
|
|
|
this.#onTaskStart(tasks, weight);
|
|
this.#running.set(taskProcess, performance.now());
|
|
}
|
|
|
|
#runTasks() {
|
|
while (this.#running.size < this.#concurrency && this.#pending.length > 0) {
|
|
this.#runNextTask();
|
|
}
|
|
}
|
|
|
|
get waitForEmpty() {
|
|
return new Promise<void>(resolve => {
|
|
this.#emptyListeners.push(resolve);
|
|
});
|
|
}
|
|
}
|
|
|
|
const total = projectToDependencies.size + ready.size;
|
|
let done = 0;
|
|
|
|
const pool = new Pool({
|
|
filename: tscPath,
|
|
concurrency: os.cpus().length,
|
|
maxWeight: 20,
|
|
taskWeight(project) {
|
|
return 1 + (projectToDependents.get(project)?.size ?? 0);
|
|
},
|
|
showOutput(projects) {
|
|
return projects.some(task => entryProjectsPaths.has(task));
|
|
},
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
onTaskStart(projects, weight) {},
|
|
async onTaskDone(projects, code, duration) {
|
|
if (code !== 0) process.exitCode = code;
|
|
|
|
await Promise.all(
|
|
projects.map(async project => {
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
const { include: projectFiles } = JSON5.parse(
|
|
readFileSync(resolvePath(project, "tsconfig.json"), "utf8")
|
|
) as { include?: string[] };
|
|
if (!projectFiles) return;
|
|
const globPatterns = projectFiles
|
|
.filter(file => file.endsWith("*.ts"))
|
|
.map(
|
|
file => `${project}/${file.slice(0, -3)}*.d.ts`.replace(/\\/g, "/") // glob doesn't support \ on Windows
|
|
);
|
|
const files = await glob(globPatterns);
|
|
for (const file of files) {
|
|
const out = joinPath(
|
|
process.cwd(),
|
|
"dts",
|
|
relativePath(process.cwd(), file)
|
|
);
|
|
mkdirSync(dirname(out), { recursive: true });
|
|
copyFileSync(file, out);
|
|
}
|
|
})
|
|
);
|
|
|
|
done += projects.length;
|
|
if (!process.env.TSCHECK_SILENT) {
|
|
console.log(
|
|
`[${done}/${total}] ${projects.map(project => relativePath(process.cwd(), project)).join(" ")} took ${Math.round(duration) / 1e3}s`
|
|
);
|
|
}
|
|
for (const project of projects) {
|
|
for (const dependent of projectToDependents.get(project) ?? []) {
|
|
const dependencies = projectToDependencies.get(dependent);
|
|
if (dependencies) {
|
|
dependencies.delete(project);
|
|
if (dependencies.size === 0) {
|
|
projectToDependencies.delete(dependent);
|
|
// @ts-expect-error run is defined on Pool
|
|
if (!projects.includes(dependent)) this.run(dependent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
onTaskError(projects, code = 1) {
|
|
console.error("ERROR", projects);
|
|
process.exitCode = code;
|
|
},
|
|
});
|
|
for (const project of ready) pool.run(project);
|
|
|
|
await pool.waitForEmpty;
|
|
|
|
if (projectToDependencies.size > 0) {
|
|
console.error("Did not typecheck the following projects:");
|
|
for (const project of projectToDependencies.keys()) {
|
|
console.error(` - ${project}`);
|
|
}
|
|
process.exitCode = 1;
|
|
}
|