From ab782cd6c700d8c0b14aa518b1443f5715b731e3 Mon Sep 17 00:00:00 2001 From: KimHyeongRae0 Date: Mon, 27 Apr 2026 17:45:49 +0900 Subject: [PATCH 1/2] rsc: strip server route exports from optimizeDeps scan --- integration/vite-dev-test.ts | 61 ++++++++++++++++ .../unstable.rsc-optimize-deps-route-scan.md | 1 + .../rsc-virtual-route-modules-test.ts | 45 +++++++++++- packages/react-router-dev/vite/rsc/plugin.ts | 73 +++++++++++++++++-- .../vite/rsc/virtual-route-modules.ts | 8 ++ packages/react-router-dev/vite/vite.ts | 14 ++-- 6 files changed, 190 insertions(+), 12 deletions(-) create mode 100644 packages/react-router-dev/.changes/unstable.rsc-optimize-deps-route-scan.md diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index de9d1c7fe2..b3f749fcc8 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -511,4 +511,65 @@ test.describe("Vite dev", () => { }); }); } + + test("does not prebundle RSC server-only route imports in the client optimizer", async ({ + page, + dev, + }) => { + let files: Files = async ({ port }) => ({ + "react-router.config.ts": reactRouterConfig({ + future: { unstable_optimizeDeps: true }, + }), + "vite.config.ts": await viteConfig.basic({ + port, + templateName: "rsc-vite-framework", + }), + "app/routes/_index.tsx": tsx` + import { readServerSecret } from "rsc-server-only-package"; + + export async function loader() { + return { secret: readServerSecret() }; + } + + export default function IndexRoute() { + return

Index route

; + } + `, + "node_modules/rsc-server-only-package/package.json": JSON.stringify({ + name: "rsc-server-only-package", + version: "1.0.0", + type: "module", + main: "index.js", + }), + "node_modules/rsc-server-only-package/index.js": tsx` + export function readServerSecret() { + return "server-only"; + } + `, + }); + + let { cwd, port } = await dev(files, "rsc-vite-framework"); + + await page.goto(`http://localhost:${port}/`); + await expect(page.locator("[data-route]")).toHaveText("Index route"); + + let metadataPath = path.join(cwd, "node_modules/.vite/deps/_metadata.json"); + + await expect + .poll(async () => { + try { + return await fs.readFile(metadataPath, "utf8"); + } catch { + return ""; + } + }) + .not.toBe(""); + + let clientDeps = [ + await fs.readFile(metadataPath, "utf8"), + ...(await fs.readdir(path.dirname(metadataPath))), + ].join("\n"); + + expect(clientDeps).not.toMatch(/rsc[-_]server[-_]only[-_]package/); + }); }); diff --git a/packages/react-router-dev/.changes/unstable.rsc-optimize-deps-route-scan.md b/packages/react-router-dev/.changes/unstable.rsc-optimize-deps-route-scan.md new file mode 100644 index 0000000000..bff7bf5569 --- /dev/null +++ b/packages/react-router-dev/.changes/unstable.rsc-optimize-deps-route-scan.md @@ -0,0 +1 @@ +Prevent RSC route module server exports from being scanned by the client dependency optimizer when `future.unstable_optimizeDeps` is enabled. diff --git a/packages/react-router-dev/__tests__/rsc-virtual-route-modules-test.ts b/packages/react-router-dev/__tests__/rsc-virtual-route-modules-test.ts index 0d46cab2f0..01086932bc 100644 --- a/packages/react-router-dev/__tests__/rsc-virtual-route-modules-test.ts +++ b/packages/react-router-dev/__tests__/rsc-virtual-route-modules-test.ts @@ -1,7 +1,10 @@ import * as assert from "node:assert"; import * as ts from "typescript"; -import { virtualRouteModulesPlugin } from "../vite/rsc/virtual-route-modules"; +import { + createClientRouteModuleForOptimizeDepsScan, + virtualRouteModulesPlugin, +} from "../vite/rsc/virtual-route-modules"; const plugin = virtualRouteModulesPlugin({ enforceSplitRouteModules: () => false, @@ -587,3 +590,43 @@ describe("client-route-module=HydrateFallback", () => { ); }); }); + +describe("optimizeDeps scan", () => { + it("removes server-only route exports before scanning client deps", () => { + let transformed = createClientRouteModuleForOptimizeDepsScan(js` + import { clientOnly } from "./client"; + import { serverOnly } from "server-only-package"; + import { shared } from "./shared"; + + export async function loader() { + return serverOnly(); + } + + export async function action() { + return serverOnly(); + } + + export function ServerComponent() { + return serverOnly(); + } + + export const meta = () => [{ title: shared }]; + + export default function Route() { + return clientOnly(shared); + } + `); + + expect(transformed.code).toContain( + 'import { clientOnly } from "./client";', + ); + expect(transformed.code).toContain('import { shared } from "./shared";'); + expect(transformed.code).toContain("export const meta"); + expect(transformed.code).toContain("export default function Route"); + expect(transformed.code).not.toContain("server-only-package"); + expect(transformed.code).not.toContain("serverOnly"); + expect(transformed.code).not.toContain("loader"); + expect(transformed.code).not.toContain("action"); + expect(transformed.code).not.toContain("ServerComponent"); + }); +}); diff --git a/packages/react-router-dev/vite/rsc/plugin.ts b/packages/react-router-dev/vite/rsc/plugin.ts index 14a15d3544..0ab1edeffb 100644 --- a/packages/react-router-dev/vite/rsc/plugin.ts +++ b/packages/react-router-dev/vite/rsc/plugin.ts @@ -23,8 +23,12 @@ import { } from "../vite"; import { hasDependency } from "../has-dependency"; import { getOptimizeDepsEntries } from "../optimize-deps-entries"; +import { resolveRelativeRouteFilePath } from "../resolve-relative-route-file-path"; import { createVirtualRouteConfig } from "./virtual-route-config"; -import { virtualRouteModulesPlugin } from "./virtual-route-modules"; +import { + createClientRouteModuleForOptimizeDepsScan, + virtualRouteModulesPlugin, +} from "./virtual-route-modules"; import { loadDotenv } from "../load-dotenv"; import { validatePluginOrder } from "../plugins/validate-plugin-order"; import { warnOnClientSourceMaps } from "../plugins/warn-on-client-source-maps"; @@ -204,6 +208,15 @@ export function reactRouterRSCVitePlugin(): Vite.PluginOption[] { // Async import here to avoid CJS warnings on the console let viteNormalizePath = (await import("vite")).normalizePath; + let optimizeDepsEntries = getOptimizeDepsEntries({ + entryClientFilePath: entries.client, + reactRouterConfig: config, + }); + let routeFiles = new Set( + Object.values(config.routes).map((route) => + resolveRelativeRouteFilePath(route, config), + ), + ); return { resolve: { @@ -231,15 +244,20 @@ export function reactRouterRSCVitePlugin(): Vite.PluginOption[] { ], }, optimizeDeps: { - entries: getOptimizeDepsEntries({ - entryClientFilePath: entries.client, - reactRouterConfig: config, - }), + entries: optimizeDepsEntries, ...defineOptimizeDepsCompilerOptions({ rolldown: { transform: { jsx: "react-jsx", }, + plugins: config.future.unstable_optimizeDeps + ? [ + createRSCOptimizeDepsRouteModulesPlugin({ + routeFiles, + transformToJs, + }), + ] + : [], }, esbuild: { jsx: "automatic", @@ -796,6 +814,51 @@ function getRootDirectory(viteUserConfig: Vite.UserConfig) { return viteUserConfig.root ?? process.env.REACT_ROUTER_ROOT ?? process.cwd(); } +type RSCOptimizeDepsRouteModulesPlugin = { + name: string; + transform: { + filter: { id: RegExp }; + handler( + code: string, + id: string, + ): Promise<{ code: string; map: null; moduleType: "js" } | undefined>; + }; +}; + +const jsRouteModuleRE = /\.[cm]?[jt]sx?$/; + +function createRSCOptimizeDepsRouteModulesPlugin({ + routeFiles, + transformToJs, +}: { + routeFiles: Set; + transformToJs: (code: string, filename: string) => Promise; +}): RSCOptimizeDepsRouteModulesPlugin { + return { + name: "react-router:rsc-optimize-deps-route-modules", + transform: { + filter: { id: jsRouteModuleRE }, + async handler(code, id) { + let filename = id.split("?")[0]; + let normalizedFilename = getVite().normalizePath(filename); + + if (!routeFiles.has(normalizedFilename)) { + return; + } + + let js = await transformToJs(code, filename); + let generated = createClientRouteModuleForOptimizeDepsScan(js); + + return { + code: generated.code, + map: null, + moduleType: "js", + }; + }, + }, + }; +} + const getClientBuildDirectory = ( reactRouterConfig: ResolvedReactRouterConfig, ) => path.join(reactRouterConfig.buildDirectory, "client"); diff --git a/packages/react-router-dev/vite/rsc/virtual-route-modules.ts b/packages/react-router-dev/vite/rsc/virtual-route-modules.ts index b6382ed55f..16edad9baa 100644 --- a/packages/react-router-dev/vite/rsc/virtual-route-modules.ts +++ b/packages/react-router-dev/vite/rsc/virtual-route-modules.ts @@ -365,6 +365,14 @@ ${result}`; } satisfies Vite.Plugin; } +export function createClientRouteModuleForOptimizeDepsScan(code: string) { + const ast = babel.parse(code, { + sourceType: "module", + }); + removeExports(ast, SERVER_ROUTE_EXPORTS); + return babel.generate(ast); +} + function createId( id: string, type: "client-route-module", diff --git a/packages/react-router-dev/vite/vite.ts b/packages/react-router-dev/vite/vite.ts index a5db6bb350..0d852f4f57 100644 --- a/packages/react-router-dev/vite/vite.ts +++ b/packages/react-router-dev/vite/vite.ts @@ -39,6 +39,12 @@ type OxcCompilerOptions = { }; type RolldownJsxOptions = "react-jsx"; +type RolldownOptimizeDepsOptions = { + transform: { + jsx: RolldownJsxOptions; + }; + plugins?: unknown[]; +}; type OptimizeDepsESBuildOptions = NonNullable< DepOptimizationConfig["esbuildOptions"] @@ -55,14 +61,10 @@ export function defineCompilerOptions(options: { } export function defineOptimizeDepsCompilerOptions(options: { - rolldown: { - transform: { - jsx: RolldownJsxOptions; - }; - }; + rolldown: RolldownOptimizeDepsOptions; esbuild: OptimizeDepsESBuildOptions; }): - | { rolldownOptions: { transform: { jsx: RolldownJsxOptions } } } + | { rolldownOptions: RolldownOptimizeDepsOptions } | { esbuildOptions: OptimizeDepsESBuildOptions } { let vite = getVite(); return parseInt(vite.version.split(".")[0], 10) >= 8 From c5f0f218f73bd2267bf3847a282d1fdd8958250b Mon Sep 17 00:00:00 2001 From: palkim Date: Tue, 28 Apr 2026 00:12:10 +0900 Subject: [PATCH 2/2] Sign CLA --- contributors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/contributors.yml b/contributors.yml index 1c035b6794..c221586557 100644 --- a/contributors.yml +++ b/contributors.yml @@ -231,6 +231,7 @@ - kigawas - kilavvy - kiliman +- KimHyeongRae0 - kirillgroshkov - kkirsche - kno-raziel