From c2fdb81c7b74eb9014071ed72e823067b4a0e2c7 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 27 Aug 2025 15:24:52 +1000 Subject: [PATCH 1/7] Fix client-only module HMR in RSC Framework Mode --- integration/vite-css-test.ts | 3 +- packages/react-router-dev/vite/rsc/plugin.ts | 2 + ...r-invalidate-client-only-modules-in-rsc.ts | 61 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts index f6cd2903a3..7f3bf5a86a 100644 --- a/integration/vite-css-test.ts +++ b/integration/vite-css-test.ts @@ -599,8 +599,7 @@ async function hmrWorkflow({ // changing non-React modules imported by the route. if ( templateName.includes("rsc") && - (file === "styles.module.css" || - file === "styles-postcss-linked.css" || + (file === "styles-postcss-linked.css" || file === "styles-vanilla-global.css.ts") ) { continue; diff --git a/packages/react-router-dev/vite/rsc/plugin.ts b/packages/react-router-dev/vite/rsc/plugin.ts index bdb90486cc..cf70b06248 100644 --- a/packages/react-router-dev/vite/rsc/plugin.ts +++ b/packages/react-router-dev/vite/rsc/plugin.ts @@ -20,6 +20,7 @@ import { isVirtualClientRouteModuleId, CLIENT_NON_COMPONENT_EXPORTS, } from "./virtual-route-modules"; +import { hmrInvalidateClientOnlyModulesInRsc } from "./plugins/hmr-invalidate-client-only-modules-in-rsc"; import validatePluginOrder from "../plugins/validate-plugin-order"; export function reactRouterRSCVitePlugin(): Vite.PluginOption[] { @@ -321,6 +322,7 @@ export function reactRouterRSCVitePlugin(): Vite.PluginOption[] { return modules; }, }, + hmrInvalidateClientOnlyModulesInRsc(), validatePluginOrder(), rsc({ entries: getRscEntries() }), ]; diff --git a/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts b/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts new file mode 100644 index 0000000000..986cd06d5c --- /dev/null +++ b/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts @@ -0,0 +1,61 @@ +import type * as Vite from "vite"; + +export function hmrInvalidateClientOnlyModulesInRsc(): Vite.Plugin { + return { + name: "react-router/rsc/hmr/invalidate-client-only-modules-in-rsc", + async hotUpdate(ctx) { + // We only want to invalidate ancestors of client-only modules in the RSC + // graph, so bail out if we're not in the RSC environment + if (this.environment.name !== "rsc") { + return; + } + + const updatedServerModule = this.environment.moduleGraph.getModuleById( + ctx.file, + ); + + // If this file is in the RSC graph, it's not a client-only module and + // changes will already be picked up, so bail out + if (updatedServerModule) { + return; + } + + // Find the corresponding client module for this file so we can walk the + // module graph + const updatedClientModule = + ctx.server.environments.client.moduleGraph.getModuleById(ctx.file); + + // If this file is not in the client graph, it's not a client-only module + // and we don't need to invalidate anything, so bail out + if (!updatedClientModule) { + return; + } + + const visited = new Set(); + const walk = (module: Vite.EnvironmentModuleNode) => { + if (!module || visited.has(module) || !module.id) { + return; + } + + visited.add(module); + + // If this module is in the RSC graph, invalidate it and stop walking + const serverModule = this.environment.moduleGraph.getModuleById( + module.id, + ); + if (serverModule) { + this.environment.moduleGraph.invalidateModule(serverModule); + return; + } + + if (module.importers) { + for (const importer of module.importers) { + walk(importer); + } + } + }; + + walk(updatedClientModule); + }, + }; +} From fef5dc46b6f92473b05105ed2a4315414502f0c0 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 27 Aug 2025 16:02:46 +1000 Subject: [PATCH 2/7] Fix `?url` CSS import HMR --- integration/vite-css-test.ts | 3 +- ...r-invalidate-client-only-modules-in-rsc.ts | 64 ++++++++++--------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts index 7f3bf5a86a..d143a7c604 100644 --- a/integration/vite-css-test.ts +++ b/integration/vite-css-test.ts @@ -599,8 +599,7 @@ async function hmrWorkflow({ // changing non-React modules imported by the route. if ( templateName.includes("rsc") && - (file === "styles-postcss-linked.css" || - file === "styles-vanilla-global.css.ts") + file === "styles-vanilla-global.css.ts" ) { continue; } diff --git a/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts b/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts index 986cd06d5c..4d077259e3 100644 --- a/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts +++ b/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts @@ -20,42 +20,44 @@ export function hmrInvalidateClientOnlyModulesInRsc(): Vite.Plugin { return; } - // Find the corresponding client module for this file so we can walk the - // module graph - const updatedClientModule = - ctx.server.environments.client.moduleGraph.getModuleById(ctx.file); - - // If this file is not in the client graph, it's not a client-only module - // and we don't need to invalidate anything, so bail out - if (!updatedClientModule) { + // Find the corresponding client modules for this file so we can walk the + // module graph looking for ancestors in the RSC graph + const updatedClientModules = + ctx.server.environments.client.moduleGraph.getModulesByFile(ctx.file); + if (!updatedClientModules) { return; } - const visited = new Set(); - const walk = (module: Vite.EnvironmentModuleNode) => { - if (!module || visited.has(module) || !module.id) { - return; - } - - visited.add(module); - - // If this module is in the RSC graph, invalidate it and stop walking - const serverModule = this.environment.moduleGraph.getModuleById( - module.id, - ); - if (serverModule) { - this.environment.moduleGraph.invalidateModule(serverModule); - return; - } - - if (module.importers) { - for (const importer of module.importers) { - walk(importer); + for (const updatedClientModule of updatedClientModules) { + const visited = new Set(); + const walk = (module: Vite.EnvironmentModuleNode) => { + if (visited.has(module) || !module.id) { + return; } - } - }; - walk(updatedClientModule); + visited.add(module); + + // Try to find this module in the RSC graph + const serverModule = this.environment.moduleGraph.getModuleById( + module.id, + ); + + // If this module is in the RSC graph, invalidate it and stop walking + if (serverModule) { + this.environment.moduleGraph.invalidateModule(serverModule); + return; + } + + // If we haven't found a corresponding RSC module, walk importers + if (module.importers) { + for (const importer of module.importers) { + walk(importer); + } + } + }; + + walk(updatedClientModule); + } }, }; } From e1a35d3e00b49a88053fedce989bd0ed85e46106 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 27 Aug 2025 16:12:42 +1000 Subject: [PATCH 3/7] Remove redundant async --- .../rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts b/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts index 4d077259e3..b6a2effdc2 100644 --- a/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts +++ b/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts @@ -3,7 +3,7 @@ import type * as Vite from "vite"; export function hmrInvalidateClientOnlyModulesInRsc(): Vite.Plugin { return { name: "react-router/rsc/hmr/invalidate-client-only-modules-in-rsc", - async hotUpdate(ctx) { + hotUpdate(ctx) { // We only want to invalidate ancestors of client-only modules in the RSC // graph, so bail out if we're not in the RSC environment if (this.environment.name !== "rsc") { From 43bb5ae4425322acc4f78618314eef4e05ee9cea Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 27 Aug 2025 16:26:38 +1000 Subject: [PATCH 4/7] Check for server modules with `getModulesByFile` --- .../plugins/hmr-invalidate-client-only-modules-in-rsc.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts b/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts index b6a2effdc2..9dfb90d9c9 100644 --- a/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts +++ b/packages/react-router-dev/vite/rsc/plugins/hmr-invalidate-client-only-modules-in-rsc.ts @@ -10,13 +10,12 @@ export function hmrInvalidateClientOnlyModulesInRsc(): Vite.Plugin { return; } - const updatedServerModule = this.environment.moduleGraph.getModuleById( - ctx.file, - ); + const updatedServerModules = + this.environment.moduleGraph.getModulesByFile(ctx.file); // If this file is in the RSC graph, it's not a client-only module and // changes will already be picked up, so bail out - if (updatedServerModule) { + if (updatedServerModules && updatedServerModules.size > 0) { return; } From ded5bba4a11ddb4fcbd9fe8918ccc092950c97ba Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 27 Aug 2025 16:37:43 +1000 Subject: [PATCH 5/7] Update comment --- integration/vite-css-test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts index d143a7c604..6d2eb97237 100644 --- a/integration/vite-css-test.ts +++ b/integration/vite-css-test.ts @@ -24,7 +24,7 @@ const PADDING = "20px"; const NEW_PADDING = "30px"; const fixtures = [ - ...viteMajorTemplates, + // ...viteMajorTemplates, { templateName: "rsc-vite-framework", templateDisplayName: "RSC Vite Framework", @@ -594,9 +594,7 @@ async function hmrWorkflow({ `CSS update for ${routeFile}`, ).toHaveCSS("padding", NEW_PADDING); - // TODO: Fix state preservation when changing these styles in RSC - // Framework mode. This appears to be a deeper HMR issue with - // changing non-React modules imported by the route. + // TODO: Fix state preservation when changing Vanilla Extract CSS in RSC if ( templateName.includes("rsc") && file === "styles-vanilla-global.css.ts" From 9d5c2424a7d33098faa4974514d64d8480f08370 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 27 Aug 2025 16:56:49 +1000 Subject: [PATCH 6/7] Revert commented out test fixtures --- integration/vite-css-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts index 6d2eb97237..0604d7f294 100644 --- a/integration/vite-css-test.ts +++ b/integration/vite-css-test.ts @@ -24,7 +24,7 @@ const PADDING = "20px"; const NEW_PADDING = "30px"; const fixtures = [ - // ...viteMajorTemplates, + ...viteMajorTemplates, { templateName: "rsc-vite-framework", templateDisplayName: "RSC Vite Framework", From 6c078b73a55af840af10afc2faedbe36ab80f1c4 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 27 Aug 2025 17:22:09 +1000 Subject: [PATCH 7/7] Skip flaky RSC Framework Mode HMR test expectation --- integration/vite-hmr-hdr-test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/integration/vite-hmr-hdr-test.ts b/integration/vite-hmr-hdr-test.ts index 4ad1083481..bdd8390f46 100644 --- a/integration/vite-hmr-hdr-test.ts +++ b/integration/vite-hmr-hdr-test.ts @@ -352,6 +352,9 @@ async function workflow({ await expect(hdrStatus).toHaveText( "HDR updated: route & direct 2 & indirect 2", ); - await expect(input).toHaveValue("stateful"); + // TODO: Investigate why this is flaky in CI for RSC Framework Mode + if (!templateName.includes("rsc")) { + await expect(input).toHaveValue("stateful"); + } expect(page.errors).toEqual([]); }