163 lines
4.8 KiB
TypeScript
163 lines
4.8 KiB
TypeScript
/**
|
|
* `wrnexus dev` — the development supervisor.
|
|
*
|
|
* The child server process owns file watching and HMR now (see @wrnexus/dev-server):
|
|
* - CSS and client-island edits update the live page over a WebSocket with no
|
|
* process restart and no full reload.
|
|
* - When a server module changes (it can't be re-imported in-process), the
|
|
* child exits with RESTART_EXIT_CODE and this supervisor respawns it. The
|
|
* browser reconnects and morphs in the new HTML — no visible refresh.
|
|
*
|
|
* The supervisor therefore only (re)launches the child; it does not watch files.
|
|
*/
|
|
|
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
import { resolve, join } from "node:path";
|
|
import { existsSync, watch, type FSWatcher } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { RESTART_EXIT_CODE } from "@wrnexus/dev-server";
|
|
|
|
// Resolve the dev server child entry through the package (not a source path) so
|
|
// it works whether @wrnexus/dev-server is a workspace or an installed dependency.
|
|
const SERVE_ENTRY = fileURLToPath(import.meta.resolve("@wrnexus/dev-server/serve-entry"));
|
|
|
|
export function runDev(
|
|
appRoot: string,
|
|
port: number,
|
|
hostname = "::",
|
|
tls?: { certFile: string; keyFile: string },
|
|
): void {
|
|
const appDir = join(resolve(appRoot), "app");
|
|
let child: ChildProcess | null = null;
|
|
let shuttingDown = false;
|
|
|
|
const spawnChild = (): void => {
|
|
child = spawn(
|
|
process.execPath, // the Bun binary
|
|
[
|
|
SERVE_ENTRY,
|
|
appDir,
|
|
String(port),
|
|
"development",
|
|
hostname,
|
|
"true",
|
|
...(tls ? [tls.certFile, tls.keyFile] : []),
|
|
],
|
|
{ stdio: "inherit" },
|
|
);
|
|
|
|
child.on("exit", (code, signal) => {
|
|
if (shuttingDown || signal) return;
|
|
if (code === RESTART_EXIT_CODE) {
|
|
spawnChild(); // requested restart — respawn immediately
|
|
return;
|
|
}
|
|
if (code && code !== 0) {
|
|
// Crash (e.g. a syntax error). Respawn after a short delay so the
|
|
// watcher comes back and the server auto-recovers once it's fixed.
|
|
console.error(`[wrnexus] server exited (code ${code}); retrying in 1.2s…`);
|
|
setTimeout(() => {
|
|
if (!shuttingDown) spawnChild();
|
|
}, 1200);
|
|
}
|
|
});
|
|
};
|
|
|
|
console.log(`\n ⚡ WrNexus dev (HMR) — ${appDir}`);
|
|
|
|
// Regenerate typed DB queries + typed routes once before starting, then launch.
|
|
// (Best effort; rerun `wrnexus db generate` after editing .sql.)
|
|
void (async () => {
|
|
try {
|
|
const { loadAppConfig } = await import("@wrnexus/styles");
|
|
const { regenerateAllQueries } = await import("./db.ts");
|
|
const config = await loadAppConfig(resolve(appRoot));
|
|
await regenerateAllQueries(appDir, config); // default + every named database
|
|
console.log(" ↻ db queries generated");
|
|
} catch {
|
|
/* non-fatal */
|
|
}
|
|
try {
|
|
const { regenerateRoutes } = await import("./routes.ts");
|
|
const n = regenerateRoutes(appDir);
|
|
console.log(` ↻ ${n} typed routes generated`);
|
|
} catch {
|
|
/* non-fatal */
|
|
}
|
|
spawnChild();
|
|
})();
|
|
|
|
const shutdown = () => {
|
|
shuttingDown = true;
|
|
child?.kill();
|
|
process.exit(0);
|
|
};
|
|
process.on("SIGINT", shutdown);
|
|
process.on("SIGTERM", shutdown);
|
|
}
|
|
|
|
export async function runProductionDev(
|
|
appRoot: string,
|
|
port: number,
|
|
hostname = "::",
|
|
): Promise<void> {
|
|
const root = resolve(appRoot);
|
|
let child: ChildProcess | undefined;
|
|
let building = false;
|
|
let pending = false;
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
const { runBuild } = await import("./build.ts");
|
|
|
|
const rebuild = async (): Promise<void> => {
|
|
if (building) {
|
|
pending = true;
|
|
return;
|
|
}
|
|
building = true;
|
|
try {
|
|
await runBuild(root);
|
|
child?.kill();
|
|
const { runPreview } = await import("./preview.ts");
|
|
child = runPreview(root, { port, hostname, developmentRuntime: true });
|
|
} catch (error) {
|
|
console.error(
|
|
"[wrnexus] production-runtime rebuild failed; keeping the last good server.",
|
|
error,
|
|
);
|
|
} finally {
|
|
building = false;
|
|
if (pending) {
|
|
pending = false;
|
|
void rebuild();
|
|
}
|
|
}
|
|
};
|
|
|
|
console.log(`\n ⚡ WrNexus dev (exact production runtime) — ${root}`);
|
|
await rebuild();
|
|
const watchers: FSWatcher[] = [];
|
|
for (const name of [
|
|
"app",
|
|
"public",
|
|
"wrnexus.config.ts",
|
|
"wrnexus.config.js",
|
|
"wrnexus.config.mjs",
|
|
]) {
|
|
const target = join(root, name);
|
|
if (!existsSync(target)) continue;
|
|
watchers.push(
|
|
watch(target, { recursive: true }, () => {
|
|
if (timer) clearTimeout(timer);
|
|
timer = setTimeout(() => void rebuild(), 120);
|
|
}),
|
|
);
|
|
}
|
|
const shutdown = (): void => {
|
|
watchers.forEach((watcher) => watcher.close());
|
|
child?.kill();
|
|
process.exit(0);
|
|
};
|
|
process.on("SIGINT", shutdown);
|
|
process.on("SIGTERM", shutdown);
|
|
}
|