feat: restore gateway HMR and add WRN formatting

This commit is contained in:
2026-07-13 14:54:03 +05:30
parent 3b46c69601
commit cff420a29e
71 changed files with 727 additions and 119 deletions
+54 -17
View File
@@ -12,6 +12,7 @@
import { spawn, type ChildProcess } from "node:child_process";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { RESTART_EXIT_CODE } from "./restart.ts";
/** Per-app access control, enforced at the gateway before proxying. */
export interface GatewayAuth {
@@ -71,7 +72,7 @@ export interface RunningGateway {
interface Target extends GatewayApp {
port: number;
origin: string;
child: ChildProcess;
child?: ChildProcess;
}
interface WsBridge {
@@ -81,6 +82,18 @@ interface WsBridge {
queue: (string | ArrayBufferLike | ArrayBufferView)[];
}
/** Decide whether a gateway child should be relaunched after it exits. */
export function gatewayRestartDelay(
mode: "development" | "production",
code: number | null,
signal: NodeJS.Signals | null,
): number | null {
if (mode !== "development" || signal) return null;
if (code === RESTART_EXIT_CODE) return 0;
if (code && code !== 0) return 1200;
return null;
}
/** Fixed-window rate limiter keyed by client IP. */
function makeRateLimiter(max: number, windowMs: number) {
const hits = new Map<string, { count: number; reset: number }>();
@@ -196,9 +209,9 @@ function applyEdgeHeaders(res: Response): Response {
async function waitReady(target: Target, timeoutMs = 15000): Promise<void> {
const deadline = Date.now() + timeoutMs;
for (;;) {
if (target.child.exitCode !== null) {
if (!target.child || target.child.exitCode !== null) {
throw new Error(
`app '${target.name}' exited with code ${target.child.exitCode} during startup`,
`app '${target.name}' exited with code ${target.child?.exitCode ?? "unknown"} during startup`,
);
}
try {
@@ -245,28 +258,52 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
const displayHost = hostname === "0.0.0.0" || hostname === "::" ? "localhost" : hostname;
const serveEntry = fileURLToPath(import.meta.resolve("@wrnexus/dev-server/serve-entry"));
let stopping = false;
const targets: Target[] = opts.apps.map((app, i) => {
const appPort = app.port ?? port + 1 + i;
const dir = resolve(app.dir);
const child =
mode === "production"
? spawn(process.execPath, [join(dir, "dist", "server.js")], {
stdio: "inherit",
env: { ...process.env, PORT: String(appPort) },
})
: spawn(
process.execPath,
[serveEntry, join(dir, "app"), String(appPort), mode, "127.0.0.1"],
{
const target: Target = {
...app,
port: appPort,
origin: `http://127.0.0.1:${appPort}`,
};
const launch = (): void => {
if (stopping) return;
const child =
mode === "production"
? spawn(process.execPath, [join(dir, "dist", "server.js")], {
stdio: "inherit",
},
);
return { ...app, port: appPort, origin: `http://127.0.0.1:${appPort}`, child };
env: { ...process.env, PORT: String(appPort) },
})
: spawn(
process.execPath,
[serveEntry, join(dir, "app"), String(appPort), mode, "127.0.0.1"],
{ stdio: "inherit" },
);
target.child = child;
child.once("exit", (code, signal) => {
if (stopping || target.child !== child) return;
const delay = gatewayRestartDelay(mode, code, signal);
if (delay === null) return;
if (delay > 0) {
console.error(`[wrnexus] app '${target.name}' exited (code ${code}); retrying in 1.2s…`);
setTimeout(launch, delay);
} else {
launch();
}
});
};
launch();
return target;
});
const stopChildren = () => {
stopping = true;
for (const target of targets) {
if (target.child.exitCode === null) target.child.kill();
if (target.child?.exitCode === null) target.child.kill();
}
};
+2 -2
View File
@@ -28,9 +28,9 @@ import { createHandlers, type WsData } from "./runtime.ts";
import { createDevAssetServer } from "./assets.ts";
import { HmrHub } from "./hmr.ts";
import { startWatcher } from "./watch.ts";
import { RESTART_EXIT_CODE } from "./restart.ts";
/** Exit code the child uses to ask the dev supervisor for a fresh process. */
export const RESTART_EXIT_CODE = 97;
export { RESTART_EXIT_CODE } from "./restart.ts";
export interface ServeOptions {
appDir: string;
+2
View File
@@ -0,0 +1,2 @@
/** Exit code a dev-server child uses to request a clean supervisor restart. */
export const RESTART_EXIT_CODE = 97;