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
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