From 863fe1b38cb2f134556067edd9f29ae7ded619ca Mon Sep 17 00:00:00 2001 From: fibibot Date: Thu, 14 May 2026 06:53:47 +0000 Subject: [PATCH 1/2] fix(plugin-vite): gate fresh:reload on SSR-relevant changes The chokidar watcher attached by `serverSnapshot` was calling `viteServer.ws.send("fresh:reload")` on every file change, irrespective of whether the changed file had anything to do with SSR. Because Fresh attaches directly to Vite's chokidar instance, the user's `server.watch.ignored` config does not filter events before Fresh sees them, so on Windows journal/temp writes (Deno KV `main-shm`/`main-wal`, `*.tmp.*`, `.timestamp-*`) were triggering a full page reload on every KV write. Only emit `fresh:reload` when the changed path is in the SSR module graph or is a route file under `routeDir`. Island and other client-graph edits keep flowing through Vite's normal HMR path, and route add/unlink still invalidates the snapshot. Closes denoland/fresh#3637 --- .../src/plugins/server_snapshot.ts | 43 ++++++----- packages/plugin-vite/tests/dev_server_test.ts | 71 +++++++++++++++++++ 2 files changed, 97 insertions(+), 17 deletions(-) diff --git a/packages/plugin-vite/src/plugins/server_snapshot.ts b/packages/plugin-vite/src/plugins/server_snapshot.ts index cf37c0452dd..96926aad0ac 100644 --- a/packages/plugin-vite/src/plugins/server_snapshot.ts +++ b/packages/plugin-vite/src/plugins/server_snapshot.ts @@ -98,28 +98,37 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { } } + const relRoutes = path.relative(options.routeDir, filePath); + const isRouteFile = !relRoutes.startsWith("..") && + !path.isAbsolute(relRoutes) && + !/[\\/]+\(_[^)]+\)[\\/]+/.test(filePath); + // Check for route files. We need to invalidate the snapshot if // they are removed or added. - if ( - (ev === "add" || ev === "unlink") && - !/[\\/]+\(_[^)]+\)[\\/]+/.test(filePath) - ) { - const relRoutes = path.relative(options.routeDir, filePath); - if (!relRoutes.startsWith("..")) { - const mod = ssr.moduleGraph.getModuleById(`\0${modName}`); - if (mod !== undefined) { - // Clear state - islands.clear(); - islandsByFile.clear(); - islandSpecByName.clear(); - - ssr.moduleGraph.invalidateModule(mod); - } + if ((ev === "add" || ev === "unlink") && isRouteFile) { + const mod = ssr.moduleGraph.getModuleById(`\0${modName}`); + if (mod !== undefined) { + // Clear state + islands.clear(); + islandsByFile.clear(); + islandSpecByName.clear(); + + ssr.moduleGraph.invalidateModule(mod); } } - // Finally, notify the client - viteServer.ws.send("fresh:reload"); + // Only notify the client when the changed file is actually + // relevant to SSR — either a route file, or something already + // tracked in the SSR module graph. Vite attaches us directly to + // its chokidar instance, so `server.watch.ignored` does not + // filter events before we see them. Without this gate, any + // unrelated write (e.g. Deno KV `main-shm`/`main-wal` journals + // or editor temp files on Windows) would trigger a full reload. + // See https://github.com/denoland/fresh/issues/3637. + const ssrMods = ssr.moduleGraph.getModulesByFile(filePath); + if (isRouteFile || (ssrMods !== undefined && ssrMods.size > 0)) { + viteServer.ws.send("fresh:reload"); + } }); }, resolveId: { diff --git a/packages/plugin-vite/tests/dev_server_test.ts b/packages/plugin-vite/tests/dev_server_test.ts index ffc30f92ee2..239b9872b24 100644 --- a/packages/plugin-vite/tests/dev_server_test.ts +++ b/packages/plugin-vite/tests/dev_server_test.ts @@ -471,6 +471,77 @@ integrationTest("vite dev - source mapped stack traces", async () => { expect(text).toContain("throw.tsx:5:11"); }); +// issue: https://github.com/denoland/fresh/issues/3637 +integrationTest( + "vite dev - unrelated file changes do not trigger fresh:reload", + async () => { + const fixture = path.join(FIXTURE_DIR, "no_static"); + await using tmp = await prepareDevServer(fixture); + await using devServer = await spawnDevServer(tmp.dir); + + const httpUrl = devServer.address(); + + // Load the route into the SSR module graph so the sanity check + // below has a known-good baseline to compare against. + const res = await fetch(`${httpUrl}/`); + await res.body?.cancel(); + + const wsUrl = httpUrl.replace(/^http/, "ws"); + const ws = new WebSocket(wsUrl, "vite-hmr"); + const messages: string[] = []; + ws.onmessage = (ev) => { + if (typeof ev.data === "string") messages.push(ev.data); + }; + await new Promise((resolve, reject) => { + ws.onopen = () => resolve(); + ws.onerror = (err) => reject(new Error(String(err))); + }); + + try { + // Mimic the kind of file Deno KV or an editor would write + // alongside the project (this is the original Windows repro + // from the issue). It is not in routes/ and is not imported + // by anything in the SSR module graph. + const stray = path.join(tmp.dir, "main-shm"); + await Deno.writeTextFile(stray, "garbage"); + try { + await new Promise((r) => setTimeout(r, 1500)); + const reloads = messages.filter((m) => m.includes("fresh:reload")); + expect(reloads).toEqual([]); + } finally { + await Deno.remove(stray).catch(() => {}); + } + + // Sanity check: editing a route file (which is in the SSR module + // graph after the fetch above) must still trigger fresh:reload — + // i.e. the gating did not throw the baby out with the bathwater. + messages.length = 0; + const routeFile = path.join(tmp.dir, "routes", "index.tsx"); + const original = await Deno.readTextFile(routeFile); + try { + await Deno.writeTextFile( + routeFile, + original.replace("

ok

", "

ok 2

"), + ); + + const start = Date.now(); + while ( + Date.now() - start < 5000 && + !messages.some((m) => m.includes("fresh:reload")) + ) { + await new Promise((r) => setTimeout(r, 50)); + } + + expect(messages.some((m) => m.includes("fresh:reload"))).toBe(true); + } finally { + await Deno.writeTextFile(routeFile, original); + } + } finally { + ws.close(); + } + }, +); + integrationTest("vite dev - client side ", async () => { await withBrowser(async (page) => { await page.goto(`${demoServer.address()}/tests/head_counter`, { From 75dc55dc8a357f2b552c6d078987d3e8f32b62ca Mon Sep 17 00:00:00 2001 From: fibibot Date: Thu, 14 May 2026 07:06:17 +0000 Subject: [PATCH 2/2] chore: retrigger CI to refresh pr-title check after title correction