Replace custom parallel runner with concurrently

This commit is contained in:
murashit 2026-06-19 10:01:00 +09:00
parent d87d56b955
commit c367afc473
5 changed files with 278 additions and 138 deletions

254
package-lock.json generated
View file

@ -18,6 +18,7 @@
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^25.6.0",
"concurrently": "^10.0.3",
"esbuild": "^0.28.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
@ -3572,6 +3573,46 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/cliui": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -3606,6 +3647,57 @@
"dev": true,
"license": "MIT"
},
"node_modules/concurrently": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz",
"integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "5.6.2",
"rxjs": "7.8.2",
"shell-quote": "1.8.4",
"supports-color": "10.2.2",
"tree-kill": "1.2.2",
"yargs": "18.0.0"
},
"bin": {
"conc": "dist/bin/index.js",
"concurrently": "dist/bin/index.js"
},
"engines": {
"node": ">=22"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/concurrently/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/concurrently/node_modules/supports-color": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@ -5482,6 +5574,16 @@
"node": ">=6.9.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
@ -8411,6 +8513,16 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-array-concat": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
@ -8595,6 +8707,19 @@
"node": ">=8"
}
},
"node_modules/shell-quote": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
"integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@ -9377,6 +9502,16 @@
"node": ">=20"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"license": "MIT",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@ -10025,6 +10160,62 @@
"node": ">=0.10.0"
}
},
"node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/write-file-atomic": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz",
@ -10055,6 +10246,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@ -10095,6 +10296,59 @@
"url": "https://github.com/sponsors/ota-meshi"
}
},
"node_modules/yargs": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^7.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs-parser": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View file

@ -19,14 +19,14 @@
"build": "node esbuild.config.mjs",
"build:styles": "node scripts/build-styles.mjs",
"build:styles:check": "node scripts/build-styles.mjs --check",
"check": "node scripts/check.mjs",
"check:ci": "node scripts/check.mjs --ci",
"check": "concurrently --group --pad-prefix --names typecheck,test,lint:ts,lint:css,lint:css-usage,lint:deps,lint:unused,format:check,build:styles:check,build \"npm run --silent typecheck\" \"npm run --silent test\" \"npm run --silent lint:ts\" \"npm run --silent lint:css\" \"npm run --silent lint:css-usage\" \"npm run --silent lint:deps\" \"npm run --silent lint:unused\" \"npm run --silent format:check\" \"npm run --silent build:styles:check\" \"npm run --silent build\"",
"check:ci": "npm run typecheck:ci && npm run test:ci && npm run lint:ci && npm run format:check:ci && npm run build:styles:check && npm run build",
"format": "node scripts/format.mjs",
"format:check": "node scripts/format.mjs --check --cache",
"format:check:ci": "node scripts/format.mjs --check",
"generate:app-server-types": "node scripts/generate-app-server-types.mjs",
"lint": "node scripts/run-parallel.mjs lint:ts lint:css lint:css-usage lint:deps lint:unused",
"lint:ci": "node scripts/run-parallel.mjs lint:ts:ci lint:css lint:css-usage lint:deps lint:unused",
"lint": "concurrently --group --pad-prefix --names lint:ts,lint:css,lint:css-usage,lint:deps,lint:unused \"npm run --silent lint:ts\" \"npm run --silent lint:css\" \"npm run --silent lint:css-usage\" \"npm run --silent lint:deps\" \"npm run --silent lint:unused\"",
"lint:ci": "npm run lint:ts:ci && npm run lint:css && npm run lint:css-usage && npm run lint:deps && npm run lint:unused",
"lint:css": "stylelint \"src/**/*.css\" --max-warnings=0",
"lint:css-usage": "node scripts/lint/check-css-usage.mjs --fail-on-candidates",
"lint:deps": "node scripts/lint/check-import-cycles.mjs",
@ -44,6 +44,7 @@
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^25.6.0",
"concurrently": "^10.0.3",
"esbuild": "^0.28.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",

View file

@ -1,42 +0,0 @@
import { spawnSync } from "node:child_process";
const args = new Set(process.argv.slice(2));
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
const checks = [
{ local: "typecheck", ci: "typecheck:ci", localPhase: "parallel" },
{ local: "test", ci: "test:ci", localPhase: "parallel" },
{ local: "lint", ci: "lint:ci", localPhase: "parallel" },
{ local: "format:check", ci: "format:check:ci", localPhase: "parallel" },
{ local: "build:styles:check", ci: "build:styles:check", localPhase: "parallel" },
{ local: "build", ci: "build", localPhase: "sequential" },
];
for (const arg of args) {
if (arg !== "--ci") {
console.error("Usage: node scripts/check.mjs [--ci]");
process.exit(1);
}
}
if (args.has("--ci")) {
for (const check of checks) {
if (check.ci === null) continue;
run(npmCommand, ["run", check.ci]);
}
} else {
const parallelScripts = checks.filter((check) => check.localPhase === "parallel").map((check) => check.local);
run("node", ["scripts/run-parallel.mjs", ...parallelScripts]);
for (const check of checks.filter((check) => check.localPhase === "sequential")) run(npmCommand, ["run", check.local]);
}
function run(command, args) {
const result = spawnSync(command, args, {
stdio: "inherit",
shell: false,
});
if (result.error) {
console.error(`Failed to run ${command} ${args.join(" ")}: ${result.error.message}`);
process.exit(1);
}
if (result.status !== 0) process.exit(result.status ?? 1);
}

View file

@ -1,90 +0,0 @@
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");
}
function flushOutput(script, output) {
const text = Buffer.concat(output).toString();
if (!text.trim()) return;
process.stdout.write(`${prefixedOutput(script, text)}\n`);
}
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);
flushOutput(script, output);
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`);
resolve({ script, status: 0 });
} else {
const detail = signal ? `signal ${signal}` : `exit ${code ?? 1}`;
console.error(`[${script.padEnd(longestNameLength)}] failed with ${detail} after ${elapsedSeconds}s`);
resolve({ script, status: code ?? 1 });
}
});
});
}
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(", ")}`);

View file

@ -7,20 +7,23 @@ import { describe, expect, it } from "vitest";
const repoRoot = process.cwd();
describe("development scripts", () => {
it("propagates CI check failures", async () => {
it("propagates check:ci script failures", async () => {
const cwd = await tempWorkspace();
const scripts = await readPackageScripts();
await writeFile(path.join(cwd, "fail.mjs"), "process.exit(7);\n");
await writeJson(path.join(cwd, "package.json"), {
scripts: {
"check:ci": scripts["check:ci"],
"typecheck:ci": "node fail.mjs",
"test:ci": 'node -e "process.exit(0)"',
"lint:ci": 'node -e "process.exit(0)"',
"format:check:ci": 'node -e "process.exit(0)"',
"build:styles:check": 'node -e "process.exit(0)"',
build: 'node -e "process.exit(0)"',
},
});
const result = runNodeScript("scripts/check.mjs", ["--ci"], cwd);
const result = runNpmScript("check:ci", cwd);
expect(result.status).toBe(7);
});
@ -253,6 +256,20 @@ function runNodeScript(script: string, args: string[] = [], cwd = repoRoot, env:
});
}
function runNpmScript(script: string, cwd = repoRoot) {
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
return spawnSync(npmCommand, ["run", script], {
cwd,
encoding: "utf8",
shell: false,
});
}
async function readPackageScripts(): Promise<Record<string, string>> {
const packageJson = JSON.parse(await readFile(path.join(repoRoot, "package.json"), "utf8")) as { scripts: Record<string, string> };
return packageJson.scripts;
}
async function writeJson(file: string, value: unknown): Promise<void> {
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
}