diff --git a/web/package.json b/web/package.json index 4b3b4b0df..2066cfbda 100644 --- a/web/package.json +++ b/web/package.json @@ -17,7 +17,7 @@ "test:storybook": "vitest --run --project storybook", "storybook": "storybook dev -p 6006", "codegen": "lex-cli generate", - "bones": "boneyard-js build http://127.0.0.1:${STORYBOOK_PORT:-6006} --force && rm -f src/lib/bones/registry.ts && prettier --write src/lib/bones", + "bones": "node scripts/bones.js", "prepare": "svelte-kit sync || echo ''" }, "devDependencies": { diff --git a/web/scripts/bones.js b/web/scripts/bones.js new file mode 100755 index 000000000..6bb3c48bb --- /dev/null +++ b/web/scripts/bones.js @@ -0,0 +1,111 @@ +#!/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); +});