Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@
- kigawas
- kilavvy
- kiliman
- KimHyeongRae0
- kirillgroshkov
- kkirsche
- kno-raziel
Expand Down
61 changes: 61 additions & 0 deletions integration/vite-dev-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <h1 data-route>Index route</h1>;
}
`,
"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/);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Prevent RSC route module server exports from being scanned by the client dependency optimizer when `future.unstable_optimizeDeps` is enabled.
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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");
});
});
73 changes: 68 additions & 5 deletions packages/react-router-dev/vite/rsc/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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<string>;
transformToJs: (code: string, filename: string) => Promise<string>;
}): 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");
Expand Down
8 changes: 8 additions & 0 deletions packages/react-router-dev/vite/rsc/virtual-route-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 8 additions & 6 deletions packages/react-router-dev/vite/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ type OxcCompilerOptions = {
};

type RolldownJsxOptions = "react-jsx";
type RolldownOptimizeDepsOptions = {
transform: {
jsx: RolldownJsxOptions;
};
plugins?: unknown[];
};

type OptimizeDepsESBuildOptions = NonNullable<
DepOptimizationConfig["esbuildOptions"]
Expand All @@ -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
Expand Down