Skip to content

Commit 8def758

Browse files
fix(remix-dev/vite): invalidate route manifest on route export change (#8157)
Co-authored-by: Mark Dalgleish <[email protected]>
1 parent c35e7f0 commit 8def758

File tree

2 files changed

+149
-15
lines changed

2 files changed

+149
-15
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { test, expect } from "@playwright/test";
2+
import getPort from "get-port";
3+
4+
import {
5+
createProject,
6+
createEditor,
7+
viteDev,
8+
VITE_CONFIG,
9+
} from "./helpers/vite.js";
10+
11+
const files = {
12+
"app/routes/_index.tsx": String.raw`
13+
import { useState, useEffect } from "react";
14+
import { Link } from "@remix-run/react";
15+
16+
export default function IndexRoute() {
17+
const [mounted, setMounted] = useState(false);
18+
useEffect(() => {
19+
setMounted(true);
20+
}, []);
21+
22+
return (
23+
<div>
24+
<p data-mounted>Mounted: {mounted ? "yes" : "no"}</p>
25+
<Link to="/other">/other</Link>
26+
</div>
27+
);
28+
}
29+
`,
30+
"app/routes/other.tsx": String.raw`
31+
import { useLoaderData } from "@remix-run/react";
32+
33+
export const loader = () => "hello";
34+
35+
export default function Route() {
36+
const loaderData = useLoaderData();
37+
return (
38+
<div data-loader-data>loaderData = {JSON.stringify(loaderData)}</div>
39+
);
40+
}
41+
`,
42+
};
43+
44+
test.describe(async () => {
45+
let port: number;
46+
let cwd: string;
47+
let stop: () => Promise<void>;
48+
49+
test.beforeAll(async () => {
50+
port = await getPort();
51+
cwd = await createProject({
52+
"vite.config.js": await VITE_CONFIG({ port }),
53+
...files,
54+
});
55+
stop = await viteDev({ cwd, port });
56+
});
57+
test.afterAll(async () => await stop());
58+
59+
test("Vite / dev / invalidate manifest on route exports change", async ({
60+
page,
61+
context,
62+
browserName,
63+
}) => {
64+
let pageErrors: Error[] = [];
65+
page.on("pageerror", (error) => pageErrors.push(error));
66+
let edit = createEditor(cwd);
67+
68+
await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
69+
await expect(page.locator("[data-mounted]")).toHaveText("Mounted: yes");
70+
expect(pageErrors).toEqual([]);
71+
72+
let originalContents: string;
73+
74+
// Removing loader export in other page should invalidate manifest
75+
await edit("app/routes/other.tsx", (contents) => {
76+
originalContents = contents;
77+
return contents.replace(/export const loader.*/, "");
78+
});
79+
80+
// After browser reload, client should be aware that there's no loader on the other route
81+
if (browserName === "webkit") {
82+
// Force new page instance for webkit.
83+
// Otherwise browser doesn't seem to fetch new manifest probably due to caching.
84+
page = await context.newPage();
85+
}
86+
await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
87+
await expect(page.locator("[data-mounted]")).toHaveText("Mounted: yes");
88+
await page.getByRole("link", { name: "/other" }).click();
89+
await expect(page.locator("[data-loader-data]")).toHaveText(
90+
"loaderData = null"
91+
);
92+
expect(pageErrors).toEqual([]);
93+
94+
// Revert route to original state to check HMR works and to ensure the
95+
// original file contents were valid
96+
await edit("app/routes/other.tsx", () => originalContents);
97+
await expect(page.locator("[data-loader-data]")).toHaveText(
98+
'loaderData = "hello"'
99+
);
100+
expect(pageErrors).toEqual([]);
101+
});
102+
});

packages/remix-dev/vite/plugin.ts

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,17 @@ const resolveRelativeRouteFilePath = (
161161

162162
let vmods = [serverBuildId, serverManifestId, browserManifestId];
163163

164+
const invalidateVirtualModules = (viteDevServer: Vite.ViteDevServer) => {
165+
vmods.forEach((vmod) => {
166+
let mod = viteDevServer.moduleGraph.getModuleById(
167+
VirtualModule.resolve(vmod)
168+
);
169+
if (mod) {
170+
viteDevServer.moduleGraph.invalidateModule(mod);
171+
}
172+
});
173+
};
174+
164175
const getHash = (source: BinaryLike, maxLength?: number): string => {
165176
let hash = createHash("sha256").update(source).digest("hex");
166177
return typeof maxLength === "number" ? hash.slice(0, maxLength) : hash;
@@ -844,16 +855,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
844855
) {
845856
previousPluginConfig = pluginConfig;
846857

847-
// Invalidate all virtual modules
848-
vmods.forEach((vmod) => {
849-
let mod = viteDevServer.moduleGraph.getModuleById(
850-
VirtualModule.resolve(vmod)
851-
);
852-
853-
if (mod) {
854-
viteDevServer.moduleGraph.invalidateModule(mod);
855-
}
856-
});
858+
invalidateVirtualModules(viteDevServer);
857859
}
858860

859861
next();
@@ -1276,14 +1278,44 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
12761278
cachedPluginConfig = pluginConfig;
12771279
let route = getRoute(pluginConfig, file);
12781280

1281+
type ManifestRoute = Manifest["routes"][string];
1282+
type HmrEventData = { route: ManifestRoute | null };
1283+
let hmrEventData: HmrEventData = { route: null };
1284+
1285+
if (route) {
1286+
// invalidate manifest on route exports change
1287+
let serverManifest = (await server.ssrLoadModule(serverManifestId))
1288+
.default as Manifest;
1289+
1290+
let oldRouteMetadata = serverManifest.routes[route.id];
1291+
let newRouteMetadata = await getRouteMetadata(
1292+
pluginConfig,
1293+
viteChildCompiler,
1294+
route
1295+
);
1296+
1297+
hmrEventData.route = newRouteMetadata;
1298+
1299+
if (
1300+
!oldRouteMetadata ||
1301+
(
1302+
[
1303+
"hasLoader",
1304+
"hasClientLoader",
1305+
"hasAction",
1306+
"hasClientAction",
1307+
"hasErrorBoundary",
1308+
] as const
1309+
).some((key) => oldRouteMetadata[key] !== newRouteMetadata[key])
1310+
) {
1311+
invalidateVirtualModules(server);
1312+
}
1313+
}
1314+
12791315
server.ws.send({
12801316
type: "custom",
12811317
event: "remix:hmr",
1282-
data: {
1283-
route: route
1284-
? await getRouteMetadata(pluginConfig, viteChildCompiler, route)
1285-
: null,
1286-
},
1318+
data: hmrEventData,
12871319
});
12881320

12891321
return modules;

0 commit comments

Comments
 (0)