Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(remix-dev/vite): invalidate route manifest on route export change #8157

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
500b839
fix(remix-dev/vite): invalidate route manifest on route exports hot u…
hi-ogawa Nov 28, 2023
08f5a34
fix: forgotten hotData.route
hi-ogawa Nov 28, 2023
04ef12b
test: add e2e
hi-ogawa Nov 28, 2023
33371a7
test: tweak test
hi-ogawa Nov 28, 2023
f1f7d7e
Merge branch 'dev' into fix-vite-invalidate-vmod-on-hot-exports-change
hi-ogawa Nov 29, 2023
2792429
Merge branch 'dev' into fix-vite-invalidate-vmod-on-hot-exports-change
hi-ogawa Nov 30, 2023
87bcf0e
Merge branch 'dev' into fix-vite-invalidate-vmod-on-hot-exports-change
hi-ogawa Nov 30, 2023
53a68f5
chore: tweak test 2
hi-ogawa Nov 30, 2023
9988c13
test: tweak 3
hi-ogawa Nov 30, 2023
da1e9f7
chore: ci debug
hi-ogawa Nov 30, 2023
0f2cde4
chore: ci debug 2
hi-ogawa Nov 30, 2023
5391698
debug 3
hi-ogawa Nov 30, 2023
2a7f6f1
test: robust assertion
hi-ogawa Nov 30, 2023
75950ad
chore: debug
hi-ogawa Nov 30, 2023
37a04c3
Merge branch 'dev' into fix-vite-invalidate-vmod-on-hot-exports-change
hi-ogawa Nov 30, 2023
f460a1e
chore: debug
hi-ogawa Nov 30, 2023
081b578
chore: struggle
hi-ogawa Nov 30, 2023
61fa1ed
chore: debug
hi-ogawa Nov 30, 2023
8a2c818
chore: debug
hi-ogawa Nov 30, 2023
cd587ad
chore: debug macos/webkit
hi-ogawa Nov 30, 2023
4ee450d
chore: cache?
hi-ogawa Dec 1, 2023
cfad5f7
chore: how about invalidateAll
hi-ogawa Dec 1, 2023
845502d
chore: new page so no cache?
hi-ogawa Dec 1, 2023
0361931
chore: revert debug
hi-ogawa Dec 1, 2023
4ab90d1
chore: revert ci
hi-ogawa Dec 1, 2023
2b145bf
chore: revert debug
hi-ogawa Dec 1, 2023
9bf9c1f
chore: revert ci
hi-ogawa Dec 1, 2023
1b5b246
chore: revert more
hi-ogawa Dec 1, 2023
45c591a
Merge branch 'dev' into fix-vite-invalidate-vmod-on-hot-exports-change
hi-ogawa Dec 1, 2023
98c6152
chore: revert debug
hi-ogawa Dec 1, 2023
11ed15a
Merge branch 'dev' into fix-vite-invalidate-vmod-on-hot-exports-change
markdalgleish Jan 4, 2024
48ca5ed
refactor, add clientAction and clientLoader
markdalgleish Jan 4, 2024
7540baf
refactor test to use createEditor util
markdalgleish Jan 4, 2024
49613c9
add changeset
markdalgleish Jan 4, 2024
dff9f79
update test
markdalgleish Jan 4, 2024
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 .changeset/bright-kiwis-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/dev": patch
---

Vite: Fix HMR issues when altering exports for non-rendered routes
102 changes: 102 additions & 0 deletions integration/vite-manifest-invalidation-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { test, expect } from "@playwright/test";
import getPort from "get-port";

import {
createProject,
createEditor,
viteDev,
VITE_CONFIG,
} from "./helpers/vite.js";

const files = {
"app/routes/_index.tsx": String.raw`
import { useState, useEffect } from "react";
import { Link } from "@remix-run/react";

export default function IndexRoute() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);

return (
<div>
<p data-mounted>Mounted: {mounted ? "yes" : "no"}</p>
<Link to="/other">/other</Link>
</div>
);
}
`,
"app/routes/other.tsx": String.raw`
import { useLoaderData } from "@remix-run/react";

export const loader = () => "hello";

export default function Route() {
const loaderData = useLoaderData();
return (
<div data-loader-data>loaderData = {JSON.stringify(loaderData)}</div>
);
}
`,
};

test.describe(async () => {
let port: number;
let cwd: string;
let stop: () => Promise<void>;

test.beforeAll(async () => {
port = await getPort();
cwd = await createProject({
"vite.config.js": await VITE_CONFIG({ port }),
...files,
});
stop = await viteDev({ cwd, port });
});
test.afterAll(async () => await stop());

test("Vite / dev / invalidate manifest on route exports change", async ({
page,
context,
browserName,
}) => {
let pageErrors: Error[] = [];
page.on("pageerror", (error) => pageErrors.push(error));
let edit = createEditor(cwd);

await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
await expect(page.locator("[data-mounted]")).toHaveText("Mounted: yes");
expect(pageErrors).toEqual([]);

let originalContents: string;

// Removing loader export in other page should invalidate manifest
await edit("app/routes/other.tsx", (contents) => {
originalContents = contents;
return contents.replace(/export const loader.*/, "");
});

// After browser reload, client should be aware that there's no loader on the other route
if (browserName === "webkit") {
// Force new page instance for webkit.
// Otherwise browser doesn't seem to fetch new manifest probably due to caching.
page = await context.newPage();
}
await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
await expect(page.locator("[data-mounted]")).toHaveText("Mounted: yes");
await page.getByRole("link", { name: "/other" }).click();
await expect(page.locator("[data-loader-data]")).toHaveText(
"loaderData = null"
);
expect(pageErrors).toEqual([]);

// Revert route to original state to check HMR works and to ensure the
// original file contents were valid
await edit("app/routes/other.tsx", () => originalContents);
await expect(page.locator("[data-loader-data]")).toHaveText(
'loaderData = "hello"'
);
expect(pageErrors).toEqual([]);
});
});
62 changes: 47 additions & 15 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,17 @@ const resolveRelativeRouteFilePath = (

let vmods = [serverBuildId, serverManifestId, browserManifestId];

const invalidateVirtualModules = (viteDevServer: Vite.ViteDevServer) => {
vmods.forEach((vmod) => {
let mod = viteDevServer.moduleGraph.getModuleById(
VirtualModule.resolve(vmod)
);
if (mod) {
viteDevServer.moduleGraph.invalidateModule(mod);
}
});
};

const getHash = (source: BinaryLike, maxLength?: number): string => {
let hash = createHash("sha256").update(source).digest("hex");
return typeof maxLength === "number" ? hash.slice(0, maxLength) : hash;
Expand Down Expand Up @@ -844,16 +855,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
) {
previousPluginConfig = pluginConfig;

// Invalidate all virtual modules
vmods.forEach((vmod) => {
let mod = viteDevServer.moduleGraph.getModuleById(
VirtualModule.resolve(vmod)
);

if (mod) {
viteDevServer.moduleGraph.invalidateModule(mod);
}
});
invalidateVirtualModules(viteDevServer);
}

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

type ManifestRoute = Manifest["routes"][string];
type HmrEventData = { route: ManifestRoute | null };
let hmrEventData: HmrEventData = { route: null };

if (route) {
// invalidate manifest on route exports change
let serverManifest = (await server.ssrLoadModule(serverManifestId))
.default as Manifest;

let oldRouteMetadata = serverManifest.routes[route.id];
let newRouteMetadata = await getRouteMetadata(
pluginConfig,
viteChildCompiler,
route
);

hmrEventData.route = newRouteMetadata;

if (
!oldRouteMetadata ||
(
[
"hasLoader",
"hasClientLoader",
"hasAction",
"hasClientAction",
"hasErrorBoundary",
] as const
).some((key) => oldRouteMetadata[key] !== newRouteMetadata[key])
) {
invalidateVirtualModules(server);
}
}

server.ws.send({
type: "custom",
event: "remix:hmr",
data: {
route: route
? await getRouteMetadata(pluginConfig, viteChildCompiler, route)
: null,
},
data: hmrEventData,
});

return modules;
Expand Down
Loading