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
5 changes: 5 additions & 0 deletions src/runtime/internal/vite/dev-worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class ViteEnvRunner {

async reload() {
try {
// Drop stale evaluations so the re-import walks the whole graph.
// Without this, any module whose transform is already populated on the
// Vite side is answered with `{cache: true}` and its old evaluation is
// reused. Vite's own full-reload handler clears the cache the same way.
this.runner.evaluatedModules.clear();
this.entry = await this.runner.import(this.entryPath);
this.entryError = undefined;
} catch (error) {
Expand Down
3 changes: 3 additions & 0 deletions test/vite/hmr-fixture/api/crawled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { value } from "../dep.ts";

export default () => ({ value });
1 change: 1 addition & 0 deletions test/vite/hmr-fixture/dep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = "original";
42 changes: 40 additions & 2 deletions test/vite/hmr-fixture/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,44 @@
import { defineConfig } from "vite";
import { defineConfig, type Plugin } from "vite";
import { nitro } from "nitro/vite";

/**
* Simulates an asset-collecting plugin (e.g. vite-plugin-solid's `?assets`
* crawl) that transforms a module's dependencies as a side effect of
* transforming the module itself. This repopulates the dependency's
* `transformResult` on the Vite side before the reloading dev worker's
* module runner re-fetches it, so `fetchModule` answers `{cache: true}` β€”
* the runner then reuses its stale evaluation unless the dev worker cleared
* its evaluated modules when reloading.
*/
function depCrawler(): Plugin {
return {
name: "test:dep-crawler",
async transform(_code, id) {
if (id.endsWith("api/crawled.ts") && this.environment.mode === "dev") {
await this.environment.transformRequest("/dep.ts");
}
},
};
}

/**
* Simulates a framework plugin that hard-invalidates the whole server module
* graph on edits (a common workaround for SSR staleness). Hard invalidation
* makes transform hooks re-run β€” which is what lets `depCrawler` above
* repopulate `dep.ts`'s transform during the reload. Scoped to `dep.ts`
* edits so the other HMR tests are unaffected.
*/
function serverGraphInvalidator(): Plugin {
return {
name: "test:server-graph-invalidator",
hotUpdate({ file }) {
if (file.endsWith("dep.ts") && this.environment.config.consumer !== "client") {
this.environment.moduleGraph.invalidateAll();
}
},
};
}

export default defineConfig({
plugins: [nitro({ serverDir: "./" })],
plugins: [nitro({ serverDir: "./" }), depCrawler(), serverGraphInvalidator()],
});
19 changes: 19 additions & 0 deletions test/vite/hmr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe("vite:hmr", { sequential: true }, () => {
api: openFileForEditing(join(rootDir, "api/state.ts")),
shared: openFileForEditing(join(rootDir, "shared.ts")),
ssr: openFileForEditing(join(rootDir, "app/entry-server.ts")),
dep: openFileForEditing(join(rootDir, "dep.ts")),
};

beforeAll(async () => {
Expand Down Expand Up @@ -92,6 +93,24 @@ describe("vite:hmr", { sequential: true }, () => {
);
expect(wsMessages).toMatchObject([{ type: "full-reload" }]);
});

// Regression test for the dev worker reusing stale evaluations across
// reloads: the fixture's `dep-crawler` plugin re-transforms `dep.ts` as a
// side effect of transforming `api/crawled.ts`, so by the time the
// reloading worker's module runner re-fetches `dep.ts`, its transform is
// already populated and `fetchModule` answers `{cache: true}`. Unless
// `reload()` clears the runner's evaluated modules, the old `dep.ts`
// evaluation is reused and responses stay stale until a manual restart.
test("editing a dependency crawled by another plugin", async () => {
const res = (await fetch(`${serverURL}/api/crawled`).then((r) => r.json())) as {
value: string;
};
expect(res.value).toBe("original");

files.dep.update((content) => content.replace(`"original"`, `"modified"`));
await pollResponse(`${serverURL}/api/crawled`, /modified/);
expect(wsMessages).toMatchObject([{ type: "full-reload" }]);
});
});

function openFileForEditing(path: string) {
Expand Down