From 84463dd8fd43d22a264d492f237e6d94407ba36e Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 22 Aug 2025 14:54:11 +1000 Subject: [PATCH 1/4] Improve CSS HMR in RSC Framework Mode --- .../helpers/rsc-parcel/src/routes/root.tsx | 2 +- .../helpers/rsc-vite-framework/package.json | 2 +- integration/helpers/rsc-vite/package.json | 2 +- integration/vite-css-test.ts | 43 +++++++++++-------- packages/react-router-dev/package.json | 2 +- packages/react-router/lib/rsc/server.ssr.tsx | 9 +++- playground/rsc-vite-framework/package.json | 2 +- playground/rsc-vite/package.json | 2 +- pnpm-lock.yaml | 42 +++++++++--------- 9 files changed, 61 insertions(+), 45 deletions(-) diff --git a/integration/helpers/rsc-parcel/src/routes/root.tsx b/integration/helpers/rsc-parcel/src/routes/root.tsx index 778a4858d6..1482b6c0a6 100644 --- a/integration/helpers/rsc-parcel/src/routes/root.tsx +++ b/integration/helpers/rsc-parcel/src/routes/root.tsx @@ -6,7 +6,7 @@ export function Layout({ children }: { children: React.ReactNode }) { - Vite (RSC) + Parcel (RSC) diff --git a/integration/helpers/rsc-vite-framework/package.json b/integration/helpers/rsc-vite-framework/package.json index 03488c7e3b..e36b9831ef 100644 --- a/integration/helpers/rsc-vite-framework/package.json +++ b/integration/helpers/rsc-vite-framework/package.json @@ -21,7 +21,7 @@ "@vanilla-extract/css": "^1.17.4", "@vanilla-extract/vite-plugin": "^5.1.1", "@vitejs/plugin-react": "^4.5.2", - "@vitejs/plugin-rsc": "0.4.11", + "@vitejs/plugin-rsc": "0.4.21", "cross-env": "^7.0.3", "typescript": "^5.1.6", "vite": "^6.2.0", diff --git a/integration/helpers/rsc-vite/package.json b/integration/helpers/rsc-vite/package.json index 9058d98848..83411f7de6 100644 --- a/integration/helpers/rsc-vite/package.json +++ b/integration/helpers/rsc-vite/package.json @@ -10,7 +10,7 @@ "typecheck": "tsc" }, "devDependencies": { - "@vitejs/plugin-rsc": "0.4.11", + "@vitejs/plugin-rsc": "0.4.21", "@types/express": "^5.0.0", "@types/node": "^22.13.1", "@types/react": "^19.1.8", diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts index dbc1191533..085d69c40d 100644 --- a/integration/vite-css-test.ts +++ b/integration/vite-css-test.ts @@ -34,12 +34,18 @@ const fixtures = [ templateDisplayName: string; }>; -type RouteBasePath = "css" | "rsc-server-first-css"; +type RouteBasePath = + | "css-with-links-export" + | "css-with-floated-link" + | "rsc-server-first-route"; const getRouteBasePaths = (templateName: TemplateName) => { - if (templateName.includes("rsc")) { - return ["css", "rsc-server-first-css"] as const satisfies RouteBasePath[]; - } - return ["css"] as const satisfies RouteBasePath[]; + return [ + "css-with-links-export", + "css-with-floated-link", + ...(templateName.includes("rsc") + ? (["rsc-server-first-route"] as const) + : []), + ] as const satisfies RouteBasePath[]; }; const files = ({ templateName }: { templateName: TemplateName }) => ({ @@ -162,14 +168,17 @@ const files = ({ templateName }: { templateName: TemplateName }) => ({ return null; } - export function links() { - return [{ rel: "stylesheet", href: postcssLinkedStyles }]; + ${ + routeBasePath === "css-with-links-export" + ? `export function links() { return [{ rel: "stylesheet", href: postcssLinkedStyles }]; }` + : "" } function TestRoute() { return ( <> + ${routeBasePath !== "css-with-links-export" ? `` : ""}
@@ -517,11 +526,6 @@ async function hmrWorkflow({ base?: string; templateName: TemplateName; }) { - if (templateName.includes("rsc")) { - // TODO: Fix CSS HMR support in RSC Framework mode - return; - } - for (const routeBase of getRouteBasePaths(templateName)) { let pageErrors: Error[] = []; page.on("pageerror", (error) => pageErrors.push(error)); @@ -555,10 +559,11 @@ async function hmrWorkflow({ await Promise.all( [ "#css-bundled", - "#css-postcss-linked", "#css-modules", - "#css-vanilla-global", - "#css-vanilla-local", + ...(!templateName.includes("rsc") + ? // TODO: Fix these in RSC Framework Mode + ["#css-postcss-linked", "#css-vanilla-global", "#css-vanilla-local"] + : []), ].map( async (selector) => await expect(page.locator(selector)).toHaveCSS( @@ -569,9 +574,13 @@ async function hmrWorkflow({ ); // Ensure CSS updates were handled by HMR - await expect(input).toHaveValue("stateful"); + // TODO: Fix state preservation in RSC Framework mode + if (!templateName.includes("rsc")) { + await expect(input).toHaveValue("stateful"); + } - if (routeBase === "css") { + // RSC Framework Mode doesn't support custom entries yet + if (!templateName.includes("rsc")) { // The following change triggers a full page reload, so we check it after all the checks for HMR state preservation await edit("app/entry.client.css", modifyCss); await expect(page.locator("#entry-client")).toHaveCSS( diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index 3b78681fa5..039d77a6c2 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -78,7 +78,7 @@ "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "workspace:*", - "@vitejs/plugin-rsc": "0.4.11", + "@vitejs/plugin-rsc": "0.4.21", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", diff --git a/packages/react-router/lib/rsc/server.ssr.tsx b/packages/react-router/lib/rsc/server.ssr.tsx index f7a20738e4..47bb43df09 100644 --- a/packages/react-router/lib/rsc/server.ssr.tsx +++ b/packages/react-router/lib/rsc/server.ssr.tsx @@ -112,6 +112,8 @@ export async function routeRSCServerRequest({ throw new Error("Missing body in server response"); } + const detectRedirectResponse = serverResponse.clone(); + let serverResponseB: Response | null = null; if (hydrate) { serverResponseB = serverResponse.clone(); @@ -126,7 +128,12 @@ export async function routeRSCServerRequest({ }; try { - const payload = await getPayload(); + if (!detectRedirectResponse.body) { + throw new Error("Failed to clone server response"); + } + const payload = (await createFromReadableStream( + detectRedirectResponse.body, + )) as RSCPayload; if ( serverResponse.status === SINGLE_FETCH_REDIRECT_STATUS && payload.type === "redirect" diff --git a/playground/rsc-vite-framework/package.json b/playground/rsc-vite-framework/package.json index ba68f82041..adfced593d 100644 --- a/playground/rsc-vite-framework/package.json +++ b/playground/rsc-vite-framework/package.json @@ -18,7 +18,7 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.5.2", - "@vitejs/plugin-rsc": "0.4.11", + "@vitejs/plugin-rsc": "0.4.21", "cross-env": "^7.0.3", "remark-frontmatter": "^5.0.0", "remark-mdx-frontmatter": "^5.2.0", diff --git a/playground/rsc-vite/package.json b/playground/rsc-vite/package.json index 736990179e..2029e23792 100644 --- a/playground/rsc-vite/package.json +++ b/playground/rsc-vite/package.json @@ -15,7 +15,7 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.5.2", - "@vitejs/plugin-rsc": "0.4.11", + "@vitejs/plugin-rsc": "0.4.21", "cross-env": "^7.0.3", "typescript": "^5.1.6", "vite": "^6.2.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3815f2f0b3..14060069c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -512,8 +512,8 @@ importers: specifier: ^4.5.2 version: 4.5.2(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) '@vitejs/plugin-rsc': - specifier: 0.4.11 - version: 0.4.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) + specifier: 0.4.21 + version: 0.4.21(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) typescript: specifier: ^5.1.6 version: 5.4.5 @@ -573,8 +573,8 @@ importers: specifier: ^4.5.2 version: 4.5.2(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) '@vitejs/plugin-rsc': - specifier: 0.4.11 - version: 0.4.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) + specifier: 0.4.21 + version: 0.4.21(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -1127,8 +1127,8 @@ importers: specifier: workspace:* version: link:../react-router-node '@vitejs/plugin-rsc': - specifier: 0.4.11 - version: 0.4.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) + specifier: 0.4.21 + version: 0.4.21(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) arg: specifier: ^5.0.1 version: 5.0.2 @@ -1911,8 +1911,8 @@ importers: specifier: ^4.5.2 version: 4.5.2(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) '@vitejs/plugin-rsc': - specifier: 0.4.11 - version: 0.4.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) + specifier: 0.4.21 + version: 0.4.21(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -1972,8 +1972,8 @@ importers: specifier: ^4.5.2 version: 4.5.2(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) '@vitejs/plugin-rsc': - specifier: 0.4.11 - version: 0.4.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) + specifier: 0.4.21 + version: 0.4.21(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -3817,9 +3817,6 @@ packages: '@mjackson/node-fetch-server@0.6.1': resolution: {integrity: sha512-9ZJnk/DJjt805uv5PPv11haJIW+HHf3YEEyVXv+8iLQxLD/iXA68FH220XoiTPBC4gCg5q+IMadDw8qPqlA5wg==} - '@mjackson/node-fetch-server@0.7.0': - resolution: {integrity: sha512-un8diyEBKU3BTVj3GzlTPA1kIjCkGdD+AMYQy31Gf9JCkfoZzwgJ79GUtHrF2BN3XPNMLpubbzPcxys+a3uZEw==} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -4413,6 +4410,9 @@ packages: '@remix-run/changelog-github@0.0.5': resolution: {integrity: sha512-43tqwUqWqirbv6D9uzo55ASPsCJ61Ein1k/M8qn+Qpros0MmbmuzjLVPmtaxfxfe2ANX0LefLvCD0pAgr1tp4g==} + '@remix-run/node-fetch-server@0.8.0': + resolution: {integrity: sha512-8/sKegb4HrM6IdcQeU0KPhj9VOHm5SUqswJDHuMCS3mwbr/NRx078QDbySmn0xslahvvZoOENd7EnK40kWKxkg==} + '@remix-run/web-blob@3.1.0': resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} @@ -5232,8 +5232,8 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - '@vitejs/plugin-rsc@0.4.11': - resolution: {integrity: sha512-+4H4wLi+Y9yF58znBfKgGfX8zcqUGt8ngnmNgzrdGdF1SVz7EO0sg7WnhK5fFVHt6fUxsVEjmEabsCWHKPL1Tw==} + '@vitejs/plugin-rsc@0.4.21': + resolution: {integrity: sha512-bczK6FFl5R0Drob0VpfeQt5avMfdAchp6EMVBswBUdvxerEeYOEamAWB0zeDEf+5zYZvVISbE8M/dERllx2V9A==} peerDependencies: react: '*' react-dom: '*' @@ -11984,8 +11984,6 @@ snapshots: '@mjackson/node-fetch-server@0.6.1': {} - '@mjackson/node-fetch-server@0.7.0': {} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -12828,6 +12826,8 @@ snapshots: transitivePeerDependencies: - encoding + '@remix-run/node-fetch-server@0.8.0': {} + '@remix-run/web-blob@3.1.0': dependencies: '@remix-run/web-stream': 1.1.0 @@ -13782,9 +13782,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-rsc@0.4.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0))': + '@vitejs/plugin-rsc@0.4.21(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0))': dependencies: - '@mjackson/node-fetch-server': 0.7.0 + '@remix-run/node-fetch-server': 0.8.0 es-module-lexer: 1.7.0 estree-walker: 3.0.3 magic-string: 0.30.17 @@ -13795,9 +13795,9 @@ snapshots: vite: 6.2.5(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0) vitefu: 1.1.1(vite@6.2.5(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)) - '@vitejs/plugin-rsc@0.4.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0))': + '@vitejs/plugin-rsc@0.4.21(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0))': dependencies: - '@mjackson/node-fetch-server': 0.7.0 + '@remix-run/node-fetch-server': 0.8.0 es-module-lexer: 1.7.0 estree-walker: 3.0.3 magic-string: 0.30.17 From b48426ab2c4d536a64607f88edbd9c746f79c687 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 22 Aug 2025 15:01:36 +1000 Subject: [PATCH 2/4] Fix server first route check in test --- integration/vite-css-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts index 085d69c40d..1bb40b1125 100644 --- a/integration/vite-css-test.ts +++ b/integration/vite-css-test.ts @@ -118,7 +118,7 @@ const files = ({ templateName }: { templateName: TemplateName }) => ({ ...Object.assign( {}, ...getRouteBasePaths(templateName).map((routeBasePath) => { - const isServerFirstRoute = routeBasePath === "rsc-server-first-css"; + const isServerFirstRoute = routeBasePath === "rsc-server-first-route"; const exportName = isServerFirstRoute ? "ServerComponent" : "default"; return { @@ -579,7 +579,7 @@ async function hmrWorkflow({ await expect(input).toHaveValue("stateful"); } - // RSC Framework Mode doesn't support custom entries yet + // RSC Framework mode doesn't support custom entries yet if (!templateName.includes("rsc")) { // The following change triggers a full page reload, so we check it after all the checks for HMR state preservation await edit("app/entry.client.css", modifyCss); From d67b5ba9c39e0652645d12d0a45740cbb09d225f Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 25 Aug 2025 11:20:55 +1000 Subject: [PATCH 3/4] Narrow down supported HMR cases --- integration/vite-css-test.ts | 73 ++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts index 1bb40b1125..79a19e01fa 100644 --- a/integration/vite-css-test.ts +++ b/integration/vite-css-test.ts @@ -548,35 +548,52 @@ async function hmrWorkflow({ "NEW_PADDING_INJECTED_VIA_POSTCSS", ); - await Promise.all([ - edit(`app/routes/${routeBase}/styles-bundled.css`, modifyCss), - edit(`app/routes/${routeBase}/styles.module.css`, modifyCss), - edit(`app/routes/${routeBase}/styles-vanilla-global.css.ts`, modifyCss), - edit(`app/routes/${routeBase}/styles-vanilla-local.css.ts`, modifyCss), - edit(`app/routes/${routeBase}/styles-postcss-linked.css`, modifyCss), - ]); - - await Promise.all( - [ - "#css-bundled", - "#css-modules", - ...(!templateName.includes("rsc") - ? // TODO: Fix these in RSC Framework Mode - ["#css-postcss-linked", "#css-vanilla-global", "#css-vanilla-local"] - : []), - ].map( - async (selector) => - await expect(page.locator(selector)).toHaveCSS( - "padding", - NEW_PADDING, - ), - ), - ); + const testCases = [ + { file: "styles-bundled.css", selector: "#css-bundled" }, + // TODO: Fix HMR for CSS Modules in server-first routes in RSC Framework mode + ...(routeBase === "rsc-server-first-route" + ? [] + : [{ file: "styles.module.css", selector: "#css-modules" }]), + // TODO: Fix HMR for `?url` CSS imports in RSC Framework mode: https://github.com/vitejs/vite-plugin-react/issues/772 + // Once fixed, check if this also fixes HMR for Vanilla Extract + ...(templateName.includes("rsc") + ? [] + : [ + { + file: "styles-postcss-linked.css", + selector: "#css-postcss-linked", + }, + { + file: "styles-vanilla-global.css.ts", + selector: "#css-vanilla-global", + }, + { + file: "styles-vanilla-local.css.ts", + selector: "#css-vanilla-local", + }, + ]), + ] as const satisfies Array<{ + file: string; + selector: string; + }>; + + for (const { file, selector } of testCases) { + const routeFile = `app/routes/${routeBase}/${file}`; + await edit(routeFile, modifyCss); + await expect( + page.locator(selector), + `CSS update for ${routeFile}`, + ).toHaveCSS("padding", NEW_PADDING); + + // TODO: Fix state preservation when changing CSS Modules in RSC Framework mode + if (templateName.includes("rsc") && file === "styles.module.css") { + continue; + } - // Ensure CSS updates were handled by HMR - // TODO: Fix state preservation in RSC Framework mode - if (!templateName.includes("rsc")) { - await expect(input).toHaveValue("stateful"); + // Ensure CSS updates were handled by HMR + await expect(input, `State preservation for ${routeFile}`).toHaveValue( + "stateful", + ); } // RSC Framework mode doesn't support custom entries yet From ef90a84c9e4dcb5282db27919bfa9e9abcebd352 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Mon, 25 Aug 2025 11:32:22 +1000 Subject: [PATCH 4/4] Isolate state preservation check per test case --- integration/vite-css-test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts index 79a19e01fa..f45acbb7ce 100644 --- a/integration/vite-css-test.ts +++ b/integration/vite-css-test.ts @@ -536,8 +536,6 @@ async function hmrWorkflow({ let input = page.locator("input"); await expect(input).toBeVisible(); - await input.type("stateful"); - await expect(input).toHaveValue("stateful"); let edit = createEditor(cwd); let modifyCss = (contents: string) => @@ -579,6 +577,7 @@ async function hmrWorkflow({ for (const { file, selector } of testCases) { const routeFile = `app/routes/${routeBase}/${file}`; + await input.fill(routeFile); await edit(routeFile, modifyCss); await expect( page.locator(selector), @@ -592,7 +591,7 @@ async function hmrWorkflow({ // Ensure CSS updates were handled by HMR await expect(input, `State preservation for ${routeFile}`).toHaveValue( - "stateful", + routeFile, ); }