Make demo dev resilient to dashboard build failures (#1542)

This commit is contained in:
Konsti Wohlwend 2026-06-02 17:50:22 -07:00 committed by GitHub
parent 338f3ce60c
commit cda1510e93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 120 additions and 1 deletions

View File

@ -7,7 +7,7 @@
"scripts": {
"typecheck": "tsc --noEmit",
"clean": "rimraf .next && rimraf node_modules",
"dev": "NEXT_PUBLIC_HEXCLAVE_LOCAL_DASHBOARD_PORT=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}42 pnpm -w run cli -- dev --config-file=./hexclave.config.ts -- pnpm --dir examples/demo run dev:inner",
"dev": "NEXT_PUBLIC_HEXCLAVE_LOCAL_DASHBOARD_PORT=${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}42 node scripts/dev-with-retry.mjs",
"dev:inner": "next dev --turbopack --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}03",
"build": "next build",
"start": "next start --port ${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}03",

View File

@ -0,0 +1,119 @@
#!/usr/bin/env node
// Resilient wrapper for the demo dev command.
//
// The demo dev flow runs `pnpm -w run cli`, which internally builds the CLI
// package (and its dependency, dashboard build:rde-standalone). If that build
// fails, this process would normally exit, and because the root dev script uses
// `concurrently -k`, the entire dev server would die.
//
// This wrapper catches non-zero exits and watches for file changes in the
// dashboard and packages directories before retrying, so a transient build
// failure doesn't tear down the whole dev server.
import { spawn } from "node:child_process";
import { watch } from "node:fs";
import { join, resolve } from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
const scriptDir = import.meta.dirname;
const demoRoot = resolve(scriptDir, "..");
const repoRoot = resolve(demoRoot, "../..");
const LOG_PREFIX = "[Hexclave dev-retry] ";
const RETRY_DEBOUNCE_MS = 2_000;
function log(message) {
console.error(`${LOG_PREFIX}${message}`);
}
function runCliDev() {
return new Promise((resolvePromise, reject) => {
const child = spawn("pnpm", [
"-w", "run", "cli", "--",
"dev",
"--config-file=./hexclave.config.ts",
"--",
"pnpm", "--dir", "examples/demo", "run", "dev:inner",
], {
stdio: "inherit",
env: process.env,
});
let signalled = false;
const forwardSigint = () => { signalled = true; child.kill("SIGINT"); };
const forwardSigterm = () => { signalled = true; child.kill("SIGTERM"); };
process.on("SIGINT", forwardSigint);
process.on("SIGTERM", forwardSigterm);
child.on("close", (code) => {
process.off("SIGINT", forwardSigint);
process.off("SIGTERM", forwardSigterm);
resolvePromise({ code: code ?? 1, signalled });
});
child.on("error", (err) => {
process.off("SIGINT", forwardSigint);
process.off("SIGTERM", forwardSigterm);
reject(err);
});
});
}
function waitForFileChanges() {
return new Promise((resolvePromise) => {
const watchDirs = [
join(repoRoot, "apps", "dashboard"),
join(repoRoot, "packages"),
];
const watchers = [];
let resolved = false;
const done = () => {
if (resolved) return;
resolved = true;
for (const w of watchers) {
try { w.close(); } catch { /* ignore */ }
}
resolvePromise();
};
for (const dir of watchDirs) {
try {
const w = watch(dir, { recursive: true }, done);
w.on("error", () => { /* ignore watch errors */ });
watchers.push(w);
} catch {
// directory might not exist yet
}
}
// Fallback: if no watchers could be set up, resolve after a timeout so we
// don't block forever.
if (watchers.length === 0) {
log("Could not set up file watchers. Will retry after a delay.");
setTimeout(done, 10_000);
}
});
}
async function main() {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
const { code, signalled } = await runCliDev();
if (signalled || code === 0) {
process.exit(code);
}
log(`Dev command exited with code ${code}. Watching for file changes before retrying...`);
await waitForFileChanges();
log(`Change detected. Retrying in ${RETRY_DEBOUNCE_MS / 1000}s...`);
await sleep(RETRY_DEBOUNCE_MS);
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});