2026-05-29 02:19:01 +00:00
|
|
|
import { Buffer } from "node:buffer";
|
|
|
|
|
import { spawn } from "node:child_process";
|
|
|
|
|
|
|
|
|
|
const scripts = process.argv.slice(2);
|
|
|
|
|
|
|
|
|
|
if (scripts.length === 0) {
|
|
|
|
|
console.error("Usage: node scripts/run-parallel.mjs <npm-script> [<npm-script> ...]");
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
|
|
|
const longestNameLength = Math.max(...scripts.map((script) => script.length));
|
|
|
|
|
const running = new Set();
|
|
|
|
|
|
|
|
|
|
function prefixedOutput(script, text) {
|
|
|
|
|
const prefix = `[${script.padEnd(longestNameLength)}] `;
|
|
|
|
|
return text
|
|
|
|
|
.trimEnd()
|
|
|
|
|
.split(/\r?\n/)
|
|
|
|
|
.map((line) => `${prefix}${line}`)
|
|
|
|
|
.join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 02:25:19 +00:00
|
|
|
function flushOutput(script, output) {
|
|
|
|
|
const text = Buffer.concat(output).toString();
|
|
|
|
|
if (!text.trim()) return;
|
|
|
|
|
process.stdout.write(`${prefixedOutput(script, text)}\n`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 02:19:01 +00:00
|
|
|
function runScript(script) {
|
|
|
|
|
const startedAt = process.hrtime.bigint();
|
|
|
|
|
console.log(`[${script.padEnd(longestNameLength)}] started`);
|
|
|
|
|
|
|
|
|
|
const child = spawn(npmCommand, ["run", "--silent", script], {
|
|
|
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
running.add(child);
|
|
|
|
|
const output = [];
|
|
|
|
|
child.stdout.on("data", (chunk) => output.push(chunk));
|
|
|
|
|
child.stderr.on("data", (chunk) => output.push(chunk));
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
child.on("error", (error) => {
|
|
|
|
|
running.delete(child);
|
|
|
|
|
console.error(`[${script.padEnd(longestNameLength)}] failed to start: ${error.message}`);
|
|
|
|
|
resolve({ script, status: 1, output: "" });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.on("close", (code, signal) => {
|
|
|
|
|
running.delete(child);
|
2026-05-31 02:25:19 +00:00
|
|
|
flushOutput(script, output);
|
2026-05-29 02:19:01 +00:00
|
|
|
const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
|
|
|
const elapsedSeconds = (elapsedMs / 1000).toFixed(1);
|
|
|
|
|
|
|
|
|
|
if (code === 0) {
|
|
|
|
|
console.log(`[${script.padEnd(longestNameLength)}] passed in ${elapsedSeconds}s`);
|
2026-05-31 02:25:19 +00:00
|
|
|
resolve({ script, status: 0 });
|
2026-05-29 02:19:01 +00:00
|
|
|
} else {
|
|
|
|
|
const detail = signal ? `signal ${signal}` : `exit ${code ?? 1}`;
|
|
|
|
|
console.error(`[${script.padEnd(longestNameLength)}] failed with ${detail} after ${elapsedSeconds}s`);
|
2026-05-31 02:25:19 +00:00
|
|
|
resolve({ script, status: code ?? 1 });
|
2026-05-29 02:19:01 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopRunning(signal) {
|
|
|
|
|
for (const child of running) {
|
|
|
|
|
child.kill(signal);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
process.on("SIGINT", () => {
|
|
|
|
|
stopRunning("SIGINT");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
process.on("SIGTERM", () => {
|
|
|
|
|
stopRunning("SIGTERM");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const results = await Promise.all(scripts.map(runScript));
|
|
|
|
|
const failed = results.filter((result) => result.status !== 0);
|
|
|
|
|
|
|
|
|
|
if (failed.length > 0) {
|
|
|
|
|
console.error(`Failed checks: ${failed.map((result) => result.script).join(", ")}`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`All checks passed: ${scripts.join(", ")}`);
|