#!/usr/bin/env node import { readFileSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { spawn, spawnSync } from "node:child_process"; import os from "node:os"; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, ".."); const configPath = resolve(root, "boneyard.config.json"); const config = JSON.parse(readFileSync(configPath, "utf-8")); const port = process.env.STORYBOOK_PORT || 6006; const base = `http://127.0.0.1:${port}`; const rawArgs = process.argv.slice(2); const force = rawArgs.includes("--force"); const args = rawArgs.filter((a) => a !== "--force"); let urls = []; if (args.length > 0) { const patterns = args.map((a) => a.toLowerCase()); const matched = []; for (const [name, entry] of Object.entries(config.skeletons || {})) { if (patterns.some((p) => name.toLowerCase().includes(p))) { matched.push(name); urls.push(`${base}${entry.route}`); } } if (urls.length === 0) { console.error(`boneyard: no skeletons matched '${args.join(", ")}'`); process.exit(1); } console.log(`boneyard: scoping to ${urls.length} skeleton(s): ${matched.join(", ")}\n`); } else { // For all skeletons, use explicit URLs so we can parallelize across workers urls = Object.values(config.skeletons || {}).map((entry) => `${base}${entry.route}`); } const flags = force ? ["--force"] : []; const defaultConcurrency = Math.min(6, Math.max(1, os.cpus().length - 2)); const concurrency = Math.min( urls.length, parseInt(process.env.BONES_CONCURRENCY || String(defaultConcurrency), 10) ); async function run() { if (concurrency <= 1 || urls.length <= 1) { const res = spawnSync("pnpm", ["exec", "boneyard-js", "build", ...urls, ...flags], { cwd: root, stdio: "inherit" }); if (res.status !== 0) process.exit(res.status ?? 1); } else { // Partition URLs across workers const buckets = Array.from({ length: concurrency }, () => []); urls.forEach((url, i) => { buckets[i % concurrency].push(url); }); console.log( `boneyard: parallelizing ${urls.length} skeletons across ${concurrency} workers...\n` ); const startTime = Date.now(); await Promise.all( buckets.map((bucket, i) => { return new Promise((resolveWorker, rejectWorker) => { const child = spawn("pnpm", ["exec", "boneyard-js", "build", ...bucket, ...flags], { cwd: root, stdio: ["ignore", "pipe", "pipe"] }); child.stdout.on("data", (d) => { // Stream lines prefixed by worker id for clarity const text = d.toString(); for (const line of text.split("\n")) { if (line.trim()) console.log(`[w${i + 1}] ${line}`); } }); child.stderr.on("data", (d) => { const text = d.toString(); for (const line of text.split("\n")) { if (line.trim()) console.error(`[w${i + 1}] ${line}`); } }); child.on("close", (code) => { if (code === 0) resolveWorker(); else rejectWorker(new Error(`Worker ${i + 1} exited with code ${code}`)); }); }); }) ); const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log(`\nšŸ’€ all workers completed in ${elapsed}s!`); } // Clean up unused registry and format spawnSync("rm", ["-f", "src/lib/bones/registry.ts"], { cwd: root }); spawnSync("pnpm", ["exec", "prettier", "--write", "src/lib/bones"], { cwd: root, stdio: "inherit" }); } run().catch((err) => { console.error(err); process.exit(1); });