/** * In-process file watcher (dev). Classifies each change and chooses the * cheapest update that still shows the latest page: * * *.css / styles/ -> invalidate CSS cache, push { type: "css" } (instant swap) * 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"; import type { HmrHub } from "./hmr.ts"; import type { DevAssetServer } from "./assets.ts"; export interface WatchOptions { appDir: string; hub: HmrHub; assets: DevAssetServer; /** Called with changed app-relative files that need an in-process hot update. */ onHotChange: (files: string[]) => void | Promise; } function isIgnored(rel: string): boolean { return ( rel.includes("node_modules/") || rel.includes(".wrnexus/") || rel.includes(".wrnexus-hmr-") || rel.startsWith("dist/") || rel.includes("/dist/") ); } type Kind = "css" | "server"; function classify(rel: string): Kind { if (rel.endsWith(".css") || rel.startsWith("styles/") || rel.includes("/styles/")) return "css"; return "server"; } /** Returns the watcher so the running server can close it during shutdown. */ export function startWatcher(opts: WatchOptions): FSWatcher | undefined { const { appDir, hub, assets } = opts; const pending = new Set(); const pendingFiles = new Set(); let timer: ReturnType | null = null; const flush = (): void => { timer = null; if (pending.has("server")) { pending.clear(); 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")) { assets.invalidateCss(); hub.css(); } pending.clear(); pendingFiles.clear(); }; try { return watch(appDir, { recursive: true }, (_event, filename) => { 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, 200); // debounce editor write bursts }); } catch (err) { console.warn("[wrnexus] file watching unavailable; HMR disabled", err); return undefined; } }