Skip to content

Commit 863fe1b

Browse files
committed
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 #3637
1 parent 39b5f06 commit 863fe1b

2 files changed

Lines changed: 97 additions & 17 deletions

File tree

packages/plugin-vite/src/plugins/server_snapshot.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -98,28 +98,37 @@ export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] {
9898
}
9999
}
100100

101+
const relRoutes = path.relative(options.routeDir, filePath);
102+
const isRouteFile = !relRoutes.startsWith("..") &&
103+
!path.isAbsolute(relRoutes) &&
104+
!/[\\/]+\(_[^)]+\)[\\/]+/.test(filePath);
105+
101106
// Check for route files. We need to invalidate the snapshot if
102107
// they are removed or added.
103-
if (
104-
(ev === "add" || ev === "unlink") &&
105-
!/[\\/]+\(_[^)]+\)[\\/]+/.test(filePath)
106-
) {
107-
const relRoutes = path.relative(options.routeDir, filePath);
108-
if (!relRoutes.startsWith("..")) {
109-
const mod = ssr.moduleGraph.getModuleById(`\0${modName}`);
110-
if (mod !== undefined) {
111-
// Clear state
112-
islands.clear();
113-
islandsByFile.clear();
114-
islandSpecByName.clear();
115-
116-
ssr.moduleGraph.invalidateModule(mod);
117-
}
108+
if ((ev === "add" || ev === "unlink") && isRouteFile) {
109+
const mod = ssr.moduleGraph.getModuleById(`\0${modName}`);
110+
if (mod !== undefined) {
111+
// Clear state
112+
islands.clear();
113+
islandsByFile.clear();
114+
islandSpecByName.clear();
115+
116+
ssr.moduleGraph.invalidateModule(mod);
118117
}
119118
}
120119

121-
// Finally, notify the client
122-
viteServer.ws.send("fresh:reload");
120+
// Only notify the client when the changed file is actually
121+
// relevant to SSR — either a route file, or something already
122+
// tracked in the SSR module graph. Vite attaches us directly to
123+
// its chokidar instance, so `server.watch.ignored` does not
124+
// filter events before we see them. Without this gate, any
125+
// unrelated write (e.g. Deno KV `main-shm`/`main-wal` journals
126+
// or editor temp files on Windows) would trigger a full reload.
127+
// See https://github.com/denoland/fresh/issues/3637.
128+
const ssrMods = ssr.moduleGraph.getModulesByFile(filePath);
129+
if (isRouteFile || (ssrMods !== undefined && ssrMods.size > 0)) {
130+
viteServer.ws.send("fresh:reload");
131+
}
123132
});
124133
},
125134
resolveId: {

packages/plugin-vite/tests/dev_server_test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,77 @@ integrationTest("vite dev - source mapped stack traces", async () => {
471471
expect(text).toContain("throw.tsx:5:11");
472472
});
473473

474+
// issue: https://github.com/denoland/fresh/issues/3637
475+
integrationTest(
476+
"vite dev - unrelated file changes do not trigger fresh:reload",
477+
async () => {
478+
const fixture = path.join(FIXTURE_DIR, "no_static");
479+
await using tmp = await prepareDevServer(fixture);
480+
await using devServer = await spawnDevServer(tmp.dir);
481+
482+
const httpUrl = devServer.address();
483+
484+
// Load the route into the SSR module graph so the sanity check
485+
// below has a known-good baseline to compare against.
486+
const res = await fetch(`${httpUrl}/`);
487+
await res.body?.cancel();
488+
489+
const wsUrl = httpUrl.replace(/^http/, "ws");
490+
const ws = new WebSocket(wsUrl, "vite-hmr");
491+
const messages: string[] = [];
492+
ws.onmessage = (ev) => {
493+
if (typeof ev.data === "string") messages.push(ev.data);
494+
};
495+
await new Promise<void>((resolve, reject) => {
496+
ws.onopen = () => resolve();
497+
ws.onerror = (err) => reject(new Error(String(err)));
498+
});
499+
500+
try {
501+
// Mimic the kind of file Deno KV or an editor would write
502+
// alongside the project (this is the original Windows repro
503+
// from the issue). It is not in routes/ and is not imported
504+
// by anything in the SSR module graph.
505+
const stray = path.join(tmp.dir, "main-shm");
506+
await Deno.writeTextFile(stray, "garbage");
507+
try {
508+
await new Promise((r) => setTimeout(r, 1500));
509+
const reloads = messages.filter((m) => m.includes("fresh:reload"));
510+
expect(reloads).toEqual([]);
511+
} finally {
512+
await Deno.remove(stray).catch(() => {});
513+
}
514+
515+
// Sanity check: editing a route file (which is in the SSR module
516+
// graph after the fetch above) must still trigger fresh:reload —
517+
// i.e. the gating did not throw the baby out with the bathwater.
518+
messages.length = 0;
519+
const routeFile = path.join(tmp.dir, "routes", "index.tsx");
520+
const original = await Deno.readTextFile(routeFile);
521+
try {
522+
await Deno.writeTextFile(
523+
routeFile,
524+
original.replace("<h1>ok</h1>", "<h1>ok 2</h1>"),
525+
);
526+
527+
const start = Date.now();
528+
while (
529+
Date.now() - start < 5000 &&
530+
!messages.some((m) => m.includes("fresh:reload"))
531+
) {
532+
await new Promise((r) => setTimeout(r, 50));
533+
}
534+
535+
expect(messages.some((m) => m.includes("fresh:reload"))).toBe(true);
536+
} finally {
537+
await Deno.writeTextFile(routeFile, original);
538+
}
539+
} finally {
540+
ws.close();
541+
}
542+
},
543+
);
544+
474545
integrationTest("vite dev - client side <Head>", async () => {
475546
await withBrowser(async (page) => {
476547
await page.goto(`${demoServer.address()}/tests/head_counter`, {

0 commit comments

Comments
 (0)