Skip to content

Commit 1475302

Browse files
committed
test(vite): cover stale runner evaluations across dev worker reloads
The hmr fixture gains a dep-crawler plugin that re-transforms dep.ts as a side effect of transforming api/crawled.ts (the way asset-collecting plugins crawl imports), plus a plugin that hard-invalidates the server module graph on dep.ts edits (as framework staleness workarounds do). On a reload without evaluatedModules.clear(), the crawled transform makes fetchModule answer {cache: true} for dep.ts and the stale evaluation is served until restart — the new test fails without the fix and passes with it.
1 parent 6e04e65 commit 1475302

4 files changed

Lines changed: 63 additions & 2 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { value } from "../dep.ts";
2+
3+
export default () => ({ value });

test/vite/hmr-fixture/dep.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const value = "original";
Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,44 @@
1-
import { defineConfig } from "vite";
1+
import { defineConfig, type Plugin } from "vite";
22
import { nitro } from "nitro/vite";
33

4+
/**
5+
* Simulates an asset-collecting plugin (e.g. vite-plugin-solid's `?assets`
6+
* crawl) that transforms a module's dependencies as a side effect of
7+
* transforming the module itself. This repopulates the dependency's
8+
* `transformResult` on the Vite side before the reloading dev worker's
9+
* module runner re-fetches it, so `fetchModule` answers `{cache: true}` —
10+
* the runner then reuses its stale evaluation unless the dev worker cleared
11+
* its evaluated modules when reloading.
12+
*/
13+
function depCrawler(): Plugin {
14+
return {
15+
name: "test:dep-crawler",
16+
async transform(_code, id) {
17+
if (id.endsWith("api/crawled.ts") && this.environment.mode === "dev") {
18+
await this.environment.transformRequest("/dep.ts");
19+
}
20+
},
21+
};
22+
}
23+
24+
/**
25+
* Simulates a framework plugin that hard-invalidates the whole server module
26+
* graph on edits (a common workaround for SSR staleness). Hard invalidation
27+
* makes transform hooks re-run — which is what lets `depCrawler` above
28+
* repopulate `dep.ts`'s transform during the reload. Scoped to `dep.ts`
29+
* edits so the other HMR tests are unaffected.
30+
*/
31+
function serverGraphInvalidator(): Plugin {
32+
return {
33+
name: "test:server-graph-invalidator",
34+
hotUpdate({ file }) {
35+
if (file.endsWith("dep.ts") && this.environment.config.consumer !== "client") {
36+
this.environment.moduleGraph.invalidateAll();
37+
}
38+
},
39+
};
40+
}
41+
442
export default defineConfig({
5-
plugins: [nitro({ serverDir: "./" })],
43+
plugins: [nitro({ serverDir: "./" }), depCrawler(), serverGraphInvalidator()],
644
});

test/vite/hmr.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe("vite:hmr", { sequential: true }, () => {
2020
api: openFileForEditing(join(rootDir, "api/state.ts")),
2121
shared: openFileForEditing(join(rootDir, "shared.ts")),
2222
ssr: openFileForEditing(join(rootDir, "app/entry-server.ts")),
23+
dep: openFileForEditing(join(rootDir, "dep.ts")),
2324
};
2425

2526
beforeAll(async () => {
@@ -92,6 +93,24 @@ describe("vite:hmr", { sequential: true }, () => {
9293
);
9394
expect(wsMessages).toMatchObject([{ type: "full-reload" }]);
9495
});
96+
97+
// Regression test for the dev worker reusing stale evaluations across
98+
// reloads: the fixture's `dep-crawler` plugin re-transforms `dep.ts` as a
99+
// side effect of transforming `api/crawled.ts`, so by the time the
100+
// reloading worker's module runner re-fetches `dep.ts`, its transform is
101+
// already populated and `fetchModule` answers `{cache: true}`. Unless
102+
// `reload()` clears the runner's evaluated modules, the old `dep.ts`
103+
// evaluation is reused and responses stay stale until a manual restart.
104+
test("editing a dependency crawled by another plugin", async () => {
105+
const res = (await fetch(`${serverURL}/api/crawled`).then((r) => r.json())) as {
106+
value: string;
107+
};
108+
expect(res.value).toBe("original");
109+
110+
files.dep.update((content) => content.replace(`"original"`, `"modified"`));
111+
await pollResponse(`${serverURL}/api/crawled`, /modified/);
112+
expect(wsMessages).toMatchObject([{ type: "full-reload" }]);
113+
});
95114
});
96115

97116
function openFileForEditing(path: string) {

0 commit comments

Comments
 (0)