diff --git a/.changeset/blue-guests-raise.md b/.changeset/blue-guests-raise.md new file mode 100644 index 00000000000..ad2b1441581 --- /dev/null +++ b/.changeset/blue-guests-raise.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +Support HMR for routes with `handle` export in Vite dev diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index 0edf0593afa..6ab6bbf4fc4 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -197,6 +197,36 @@ test.describe("Vite dev", () => { } `, + "app/routes/known-route-exports.tsx": js` + import { useMatches } from "@remix-run/react"; + + export const meta = () => [{ + title: "HMR meta: 0" + }] + + export const links = () => [{ + rel: "icon", + href: "/favicon.ico", + type: "image/png", + "data-link": "HMR links: 0", + }] + + export const handle = { + data: "HMR handle: 0" + }; + + export default function TestRoute() { + const matches = useMatches(); + + return ( +
+ +

HMR component: 0

+

{matches[1].handle.data}

+
+ ); + } + `, }, }); @@ -354,6 +384,55 @@ test.describe("Vite dev", () => { expect(pageErrors).toEqual([]); }); + + test("handle known route exports with HMR", async ({ page }) => { + let pageErrors: unknown[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); + + await page.goto(`http://localhost:${devPort}/known-route-exports`, { + waitUntil: "networkidle", + }); + expect(pageErrors).toEqual([]); + + // file editing utils + let filepath = path.join(projectDir, "app/routes/known-route-exports.tsx"); + let filedata = await fs.readFile(filepath, "utf8"); + async function editFile(edit: (data: string) => string) { + filedata = edit(filedata); + await fs.writeFile(filepath, filedata, "utf8"); + } + + // verify input state is preserved after each update + let input = page.locator("input"); + await input.type("stateful"); + await expect(input).toHaveValue("stateful"); + + // component + await editFile((data) => + data.replace("HMR component: 0", "HMR component: 1") + ); + await expect(page.locator("[data-hmr]")).toHaveText("HMR component: 1"); + await expect(input).toHaveValue("stateful"); + + // handle + await editFile((data) => data.replace("HMR handle: 0", "HMR handle: 1")); + await expect(page.locator("[data-handle]")).toHaveText("HMR handle: 1"); + await expect(input).toHaveValue("stateful"); + + // meta + await editFile((data) => data.replace("HMR meta: 0", "HMR meta: 1")); + await expect(page).toHaveTitle("HMR meta: 1"); + await expect(input).toHaveValue("stateful"); + + // links + await editFile((data) => data.replace("HMR links: 0", "HMR links: 1")); + await expect(page.locator("[data-link]")).toHaveAttribute( + "data-link", + "HMR links: 1" + ); + + expect(pageErrors).toEqual([]); + }); }); let bufferize = (stream: Readable): (() => string) => { diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index ae98042b9c5..61eb4a65f22 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -1092,7 +1092,9 @@ function addRefreshWrapper( id: string ): string { let isRoute = getRoute(pluginConfig, id); - let acceptExports = isRoute ? ["meta", "links", "shouldRevalidate"] : []; + let acceptExports = isRoute + ? ["handle", "meta", "links", "shouldRevalidate"] + : []; return ( REACT_REFRESH_HEADER.replace("__SOURCE__", JSON.stringify(id)) + code +