Files
fusion/scripts/dev-hmr.mjs
2026-05-11 09:52:23 +02:00

109 lines
3.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Runs the Fusion dashboard API server AND `vite dev` concurrently so the
* React SPA in packages/dashboard/app hot-reloads while still talking to a
* live API/WebSocket backend.
*
* Two processes:
* 1. API: `pnpm dev --prebuild=none dashboard --no-auth --port <API_PORT>`
* (source-mode API/engine; Vite serves the browser UI)
* 2. Vite: `vite dev` in packages/dashboard
* (serves app/ with HMR; proxies /api and WS to the API)
*
* Open the URL Vite prints (e.g. http://localhost:5173), NOT the API URL.
* Edits to packages/dashboard/app/** hot-reload. Edits to src/** (server
* code) still require restarting this script.
*
* Env:
* FUSION_API_PORT API port (default 4050). Vite's proxy reads the same
* var so both sides stay in sync.
* FUSION_VITE_PORT Vite dev port (default 5173).
*/
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, "..");
const dashboardDir = resolve(repoRoot, "packages/dashboard");
const API_PORT = globalThis.process.env.FUSION_API_PORT ?? "4050";
const VITE_PORT = globalThis.process.env.FUSION_VITE_PORT ?? "5173";
const children = [];
let shuttingDown = false;
function prefix(label, color) {
const tag = `\x1b[${color}m[${label}]\x1b[0m`;
return (chunk) => {
const text = chunk.toString();
// Preserve trailing-newline semantics; prefix every non-empty line.
const lines = text.split("\n");
const last = lines.pop();
const prefixed = lines.map((l) => `${tag} ${l}`).join("\n");
globalThis.process.stdout.write(prefixed + (prefixed ? "\n" : "") + (last ? `${tag} ${last}` : ""));
};
}
function launch(name, color, command, args, options) {
const child = spawn(command, args, {
stdio: ["inherit", "pipe", "pipe"],
shell: true,
...options,
});
child.stdout.on("data", prefix(name, color));
child.stderr.on("data", prefix(name, color));
child.on("exit", (code, signal) => {
if (shuttingDown) return;
console.log(`\n[dev-hmr] ${name} exited (code=${code} signal=${signal}) — tearing down`);
shutdown(code ?? 1);
});
children.push({ name, child });
return child;
}
function shutdown(exitCode = 0) {
if (shuttingDown) return;
shuttingDown = true;
for (const { child } of children) {
if (!child.killed) {
try { child.kill("SIGINT"); } catch { void 0; }
}
}
// Hard kill after 5s if anyone is still alive.
setTimeout(() => {
for (const { child } of children) {
if (!child.killed) {
try { child.kill("SIGKILL"); } catch { void 0; }
}
}
globalThis.process.exit(exitCode);
}, 5000).unref();
}
globalThis.process.on("SIGINT", () => shutdown(0));
globalThis.process.on("SIGTERM", () => shutdown(0));
console.log(`[dev-hmr] starting API on :${API_PORT} + vite on :${VITE_PORT}`);
console.log(`[dev-hmr] open http://localhost:${VITE_PORT} for HMR (not the API URL)`);
// API: green. Vite owns the browser UI in this mode, so skip the dashboard
// client prebuild and run the API/engine directly from source.
launch(
"api",
"32",
"pnpm",
["dev", "--prebuild=none", "dashboard", "--no-auth", "--port", API_PORT, "--host", "127.0.0.1"],
{ cwd: repoRoot, env: { ...globalThis.process.env, FUSION_API_PORT: API_PORT } },
);
// Vite: cyan. Starts once; proxies /api (including WS) to the API port.
launch(
"vite",
"36",
"pnpm",
["exec", "vite", "dev", "--port", VITE_PORT],
{ cwd: dashboardDir, env: { ...globalThis.process.env, FUSION_API_PORT: API_PORT } },
);