Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 26 additions & 17 deletions packages/plugin-vite/src/plugins/server_snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit on phrasing: chokidar's ignored option does drop events at the source, so server.watch.ignored isn't strictly bypassed. The real reason this gate matters is that the user's globs are hard to author correctly (especially on Windows with mixed separators) and shouldn't be load-bearing for avoiding KV temp file reloads. Could we reword to focus on "defense in depth: we shouldn't depend on user ignore globs being perfect" rather than the bypass claim?

// 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: {
Expand Down
71 changes: 71 additions & 0 deletions packages/plugin-vite/tests/dev_server_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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("<h1>ok</h1>", "<h1>ok 2</h1>"),
);

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 <Head>", async () => {
await withBrowser(async (page) => {
await page.goto(`${demoServer.address()}/tests/head_counter`, {
Expand Down
Loading