feat: keep dev server alive during HMR updates
This commit is contained in:
@@ -3,10 +3,9 @@
|
||||
* cheapest update that still shows the latest page:
|
||||
*
|
||||
* *.css / styles/ -> invalidate CSS cache, push { type: "css" } (instant swap)
|
||||
* anything else -> a server module changed (pages, components, api, …):
|
||||
* it can't be re-imported in process, so request a
|
||||
* restart (the supervisor respawns us; the browser then
|
||||
* morphs in the new HTML).
|
||||
* anything else -> invalidate changed modules, rescan routes, and ask
|
||||
* the connected browser to sync fresh HTML. The server
|
||||
* process and HMR socket stay alive.
|
||||
*/
|
||||
|
||||
import { watch, type FSWatcher } from "node:fs";
|
||||
@@ -17,14 +16,15 @@ export interface WatchOptions {
|
||||
appDir: string;
|
||||
hub: HmrHub;
|
||||
assets: DevAssetServer;
|
||||
/** Called when a change requires a fresh process. */
|
||||
onServerChange: () => void;
|
||||
/** Called with changed app-relative files that need an in-process hot update. */
|
||||
onHotChange: (files: string[]) => void | Promise<void>;
|
||||
}
|
||||
|
||||
function isIgnored(rel: string): boolean {
|
||||
return (
|
||||
rel.includes("node_modules/") ||
|
||||
rel.includes(".wrnexus/") ||
|
||||
rel.includes(".wrnexus-hmr-") ||
|
||||
rel.startsWith("dist/") ||
|
||||
rel.includes("/dist/")
|
||||
);
|
||||
@@ -37,19 +37,22 @@ function classify(rel: string): Kind {
|
||||
return "server";
|
||||
}
|
||||
|
||||
/** Returns the watcher so the caller can close it before a restart (important on
|
||||
* Windows, where a live recursive fs.watch handle can block `process.exit`). */
|
||||
/** Returns the watcher so the running server can close it during shutdown. */
|
||||
export function startWatcher(opts: WatchOptions): FSWatcher | undefined {
|
||||
const { appDir, hub, assets, onServerChange } = opts;
|
||||
const { appDir, hub, assets } = opts;
|
||||
const pending = new Set<Kind>();
|
||||
const pendingFiles = new Set<string>();
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const flush = (): void => {
|
||||
timer = null;
|
||||
// A server change always wins (needs a restart).
|
||||
if (pending.has("server")) {
|
||||
pending.clear();
|
||||
onServerChange();
|
||||
const files = [...pendingFiles];
|
||||
pendingFiles.clear();
|
||||
void Promise.resolve(opts.onHotChange(files)).catch((error) => {
|
||||
console.error("[wrnexus] hot update failed", error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (pending.has("css")) {
|
||||
@@ -57,6 +60,7 @@ export function startWatcher(opts: WatchOptions): FSWatcher | undefined {
|
||||
hub.css();
|
||||
}
|
||||
pending.clear();
|
||||
pendingFiles.clear();
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -64,6 +68,7 @@ export function startWatcher(opts: WatchOptions): FSWatcher | undefined {
|
||||
if (!filename) return;
|
||||
const rel = filename.toString().replace(/\\/g, "/");
|
||||
if (isIgnored(rel)) return;
|
||||
pendingFiles.add(rel);
|
||||
pending.add(classify(rel));
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(flush, 60); // debounce editor write bursts
|
||||
|
||||
Reference in New Issue
Block a user