diff --git a/integration/helpers/rsc-vite-framework/package.json b/integration/helpers/rsc-vite-framework/package.json
index b9b7c715e7..52d123e207 100644
--- a/integration/helpers/rsc-vite-framework/package.json
+++ b/integration/helpers/rsc-vite-framework/package.json
@@ -10,6 +10,7 @@
"typecheck": "react-router typegen && tsc"
},
"devDependencies": {
+ "@mdx-js/rollup": "^3.1.0",
"@react-router/dev": "workspace:*",
"@react-router/fs-routes": "workspace:*",
"@types/express": "^5.0.0",
diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts
index 32bfebd50b..1a5a962006 100644
--- a/integration/helpers/vite.ts
+++ b/integration/helpers/vite.ts
@@ -78,6 +78,7 @@ type ViteConfigBuildArgs = {
type ViteConfigBaseArgs = {
templateName?: TemplateName;
envDir?: string;
+ mdx?: boolean;
};
type ViteConfigArgs = (
@@ -138,6 +139,7 @@ export const viteConfig = {
"const { unstable_reactRouterRSC: reactRouter } = __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;",
].join("\n")
}
+ ${args.mdx ? 'import mdx from "@mdx-js/rollup";' : ""}
import { envOnlyMacros } from "vite-env-only";
import tsconfigPaths from "vite-tsconfig-paths";
@@ -146,6 +148,7 @@ export const viteConfig = {
${viteConfig.build(args)}
envDir: ${args.envDir ? `"${args.envDir}"` : "undefined"},
plugins: [
+ ${args.mdx ? "mdx()," : ""}
reactRouter(),
envOnlyMacros(),
tsconfigPaths()
diff --git a/integration/mdx-test.ts b/integration/mdx-test.ts
new file mode 100644
index 0000000000..7c80317a47
--- /dev/null
+++ b/integration/mdx-test.ts
@@ -0,0 +1,121 @@
+import { test, expect } from "@playwright/test";
+
+import {
+ createFixture,
+ createAppFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import {
+ type TemplateName,
+ reactRouterConfig,
+ viteConfig,
+} from "./helpers/vite.js";
+
+const templateNames = [
+ "vite-5-template",
+ "rsc-vite-framework",
+] as const satisfies TemplateName[];
+
+test.describe("MDX", () => {
+ for (const templateName of templateNames) {
+ test.describe(`template: ${templateName}`, () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ templateName,
+ files: {
+ "vite.config.js": await viteConfig.basic({
+ templateName,
+ mdx: true,
+ }),
+ "react-router.config.ts": reactRouterConfig({
+ viteEnvironmentApi: templateName.includes("rsc"),
+ }),
+ "app/root.tsx": js`
+ import { Outlet, Scripts } from "react-router"
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router"
+ export default function Component() {
+ return Go to MDX route
+ }
+ `,
+ "app/routes/mdx.mdx": js`
+ import { MdxComponent } from "../components/mdx-components";
+
+ export const loader = () => {
+ return {
+ content: "MDX route content from loader",
+ }
+ }
+
+ ## MDX Route
+
+
+ `,
+ // This needs to be a separate file to support RSC since
+ // `useLoaderData` is not available in RSC environments, and
+ // components defined within an MDX file must be exported. This
+ // means they're not removed in the RSC build.
+ "app/components/mdx-components.tsx": js`
+ import { useState, useEffect } from "react";
+ import { useLoaderData } from "react-router";
+
+ export function MdxComponent() {
+ const { content } = useLoaderData();
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ return (
+ <>
+ Loader data
+ {content}
+ Mounted
+ {mounted ? "true" : "false"}
+ >
+ );
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("handles MDX routes", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/mdx");
+
+ let loaderData = page.locator("[data-loader-data]");
+ await expect(loaderData).toHaveText("MDX route content from loader");
+
+ let mounted = page.locator("[data-mounted]");
+ await expect(mounted).toHaveText("true");
+ });
+ });
+ }
+});
diff --git a/integration/vite-plugin-order-validation-test.ts b/integration/vite-plugin-order-validation-test.ts
index cf1bb68672..309764e4f0 100644
--- a/integration/vite-plugin-order-validation-test.ts
+++ b/integration/vite-plugin-order-validation-test.ts
@@ -1,33 +1,60 @@
import { test, expect } from "@playwright/test";
import dedent from "dedent";
-import { createProject, build } from "./helpers/vite.js";
-
-test.describe(() => {
- let cwd: string;
- let buildResult: ReturnType;
-
- test.beforeAll(async () => {
- cwd = await createProject({
- "vite.config.ts": dedent`
- import { reactRouter } from "@react-router/dev/vite";
- import mdx from "@mdx-js/rollup";
-
- export default {
- plugins: [
- reactRouter(),
- mdx(),
- ],
- }
- `,
+import { createProject, build, reactRouterConfig } from "./helpers/vite.js";
+
+test.describe("Vite plugin order validation", () => {
+ test.describe("MDX", () => {
+ test("Framework Mode", async () => {
+ let cwd = await createProject({
+ "vite.config.ts": dedent`
+ import { reactRouter } from "@react-router/dev/vite";
+ import mdx from "@mdx-js/rollup";
+
+ export default {
+ plugins: [
+ reactRouter(),
+ mdx(),
+ ],
+ }
+ `,
+ });
+
+ let buildResult = build({ cwd });
+ expect(buildResult.stderr.toString()).toContain(
+ 'Error: The "@mdx-js/rollup" plugin should be placed before the React Router plugin in your Vite config file',
+ );
});
- buildResult = build({ cwd });
- });
+ test("RSC Framework Mode", async () => {
+ let cwd = await createProject(
+ {
+ "vite.config.js": dedent`
+ import { defineConfig } from "vite";
+ import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from "@react-router/dev/internal";
+ import mdx from "@mdx-js/rollup";
- test("Vite / plugin order validation / MDX", () => {
- expect(buildResult.stderr.toString()).toContain(
- 'Error: The "@mdx-js/rollup" plugin should be placed before the React Router plugin in your Vite config file',
- );
+ const { unstable_reactRouterRSC: reactRouterRSC } =
+ __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;
+
+ export default defineConfig({
+ plugins: [
+ reactRouterRSC(),
+ mdx(),
+ ],
+ });
+ `,
+ "react-router.config.ts": reactRouterConfig({
+ viteEnvironmentApi: true,
+ }),
+ },
+ "rsc-vite-framework",
+ );
+
+ let buildResult = build({ cwd });
+ expect(buildResult.stderr.toString()).toContain(
+ 'Error: The "@mdx-js/rollup" plugin should be placed before the React Router plugin in your Vite config file',
+ );
+ });
});
});
diff --git a/package.json b/package.json
index 8bd3f0c6df..e3d3ab95af 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
"@babel/preset-typescript": "^7.27.1",
"@changesets/cli": "^2.26.2",
"@manypkg/get-packages": "^1.1.3",
- "@mdx-js/rollup": "^3.0.0",
+ "@mdx-js/rollup": "^3.1.0",
"@playwright/test": "^1.49.1",
"@remix-run/changelog-github": "^0.0.5",
"@types/jest": "^29.5.4",
@@ -100,7 +100,8 @@
"pnpm": {
"patchedDependencies": {
"@changesets/get-dependents-graph@1.3.6": "patches/@changesets__get-dependents-graph@1.3.6.patch",
- "@changesets/assemble-release-plan": "patches/@changesets__assemble-release-plan.patch"
+ "@changesets/assemble-release-plan": "patches/@changesets__assemble-release-plan.patch",
+ "@mdx-js/rollup": "patches/@mdx-js__rollup.patch"
},
"overrides": {
"workerd": "1.20250705.0",
diff --git a/packages/react-router-dev/vite/build.ts b/packages/react-router-dev/vite/build.ts
index 7e3e5cc1ef..2d79aa6cb3 100644
--- a/packages/react-router-dev/vite/build.ts
+++ b/packages/react-router-dev/vite/build.ts
@@ -113,7 +113,7 @@ async function viteAppBuild(
let hasReactRouterPlugin = config.plugins.find(
(plugin) =>
plugin.name === "react-router" ||
- plugin.name === "react-router/rsc/config",
+ plugin.name === "react-router/rsc",
);
if (!hasReactRouterPlugin) {
throw new Error(
diff --git a/packages/react-router-dev/vite/dev.ts b/packages/react-router-dev/vite/dev.ts
index 6a441ea47d..b016c95409 100644
--- a/packages/react-router-dev/vite/dev.ts
+++ b/packages/react-router-dev/vite/dev.ts
@@ -51,8 +51,7 @@ export async function dev(
if (
!server.config.plugins.find(
(plugin) =>
- plugin.name === "react-router" ||
- plugin.name === "react-router/rsc/config",
+ plugin.name === "react-router" || plugin.name === "react-router/rsc",
)
) {
console.error(
diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index 502a0e9ad3..8a7af33ba7 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -79,6 +79,7 @@ import {
configRouteToBranchRoute,
} from "../config/config";
import { decorateComponentExportsWithProps } from "./with-props";
+import validatePluginOrder from "./plugins/validate-plugin-order";
export type LoadCssContents = (
viteDevServer: Vite.ViteDevServer,
@@ -725,11 +726,6 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
};
};
- let pluginIndex = (pluginName: string) => {
- invariant(viteConfig);
- return viteConfig.plugins.findIndex((plugin) => plugin.name === pluginName);
- };
-
let getServerEntry = async ({ routeIds }: { routeIds?: Array }) => {
invariant(viteConfig, "viteconfig required to generate the server entry");
@@ -1474,25 +1470,6 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
"Vite config file was unable to be resolved for React Router child compiler",
);
- // Validate that commonly used Rollup plugins that need to run before
- // ours are in the correct order. This is because Rollup plugins can't
- // set `enforce: "pre"` like Vite plugins can. Explicitly validating
- // this provides a much nicer developer experience.
- let rollupPrePlugins = [
- { pluginName: "@mdx-js/rollup", displayName: "@mdx-js/rollup" },
- ];
- for (let prePlugin of rollupPrePlugins) {
- let prePluginIndex = pluginIndex(prePlugin.pluginName);
- if (
- prePluginIndex >= 0 &&
- prePluginIndex > pluginIndex("react-router")
- ) {
- throw new Error(
- `The "${prePlugin.displayName}" plugin should be placed before the React Router plugin in your Vite config file`,
- );
- }
- }
-
const childCompilerPlugins = await asyncFlatten(
childCompilerConfigFile.config.plugins ?? [],
);
@@ -1525,7 +1502,8 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
"name" in plugin &&
plugin.name !== "react-router" &&
plugin.name !== "react-router:route-exports" &&
- plugin.name !== "react-router:hmr-updates",
+ plugin.name !== "react-router:hmr-updates" &&
+ plugin.name !== "react-router:validate-plugin-order",
)
// Remove server hooks to avoid conflicts with the main dev server
.map((plugin) => ({
@@ -2432,6 +2410,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
}
},
},
+ validatePluginOrder(),
];
};
diff --git a/packages/react-router-dev/vite/plugins/validate-plugin-order.ts b/packages/react-router-dev/vite/plugins/validate-plugin-order.ts
new file mode 100644
index 0000000000..27099cd966
--- /dev/null
+++ b/packages/react-router-dev/vite/plugins/validate-plugin-order.ts
@@ -0,0 +1,34 @@
+import type * as Vite from "vite";
+
+export default function validatePluginOrder(): Vite.Plugin {
+ return {
+ name: "react-router:validate-plugin-order",
+ configResolved(viteConfig) {
+ let pluginIndex = (pluginName: string | string[]) => {
+ pluginName = Array.isArray(pluginName) ? pluginName : [pluginName];
+ return viteConfig.plugins.findIndex((plugin) =>
+ pluginName.includes(plugin.name),
+ );
+ };
+
+ let rollupPrePlugins = [
+ { pluginName: "@mdx-js/rollup", displayName: "@mdx-js/rollup" },
+ ];
+ for (let prePlugin of rollupPrePlugins) {
+ let prePluginIndex = pluginIndex(prePlugin.pluginName);
+ console.log(
+ prePluginIndex,
+ pluginIndex(["react-router", "react-router/rsc"]),
+ );
+ if (
+ prePluginIndex >= 0 &&
+ prePluginIndex > pluginIndex(["react-router", "react-router/rsc"])
+ ) {
+ throw new Error(
+ `The "${prePlugin.displayName}" plugin should be placed before the React Router plugin in your Vite config file`,
+ );
+ }
+ }
+ },
+ };
+}
diff --git a/packages/react-router-dev/vite/rsc/plugin.ts b/packages/react-router-dev/vite/rsc/plugin.ts
index f2854d12e5..9ba3cb2b96 100644
--- a/packages/react-router-dev/vite/rsc/plugin.ts
+++ b/packages/react-router-dev/vite/rsc/plugin.ts
@@ -19,6 +19,7 @@ import {
parseRouteExports,
CLIENT_NON_COMPONENT_EXPORTS,
} from "./virtual-route-modules";
+import validatePluginOrder from "../plugins/validate-plugin-order";
export function reactRouterRSCVitePlugin(): Vite.PluginOption[] {
let configLoader: ConfigLoader;
@@ -29,7 +30,7 @@ export function reactRouterRSCVitePlugin(): Vite.PluginOption[] {
return [
{
- name: "react-router/rsc/config",
+ name: "react-router/rsc",
async config(viteUserConfig, { command, mode }) {
await initEsModuleLexer;
viteCommand = command;
@@ -300,6 +301,7 @@ export function reactRouterRSCVitePlugin(): Vite.PluginOption[] {
return modules;
},
},
+ validatePluginOrder(),
rsc({ entries: getRscEntries() }),
];
}
diff --git a/patches/@mdx-js__rollup.patch b/patches/@mdx-js__rollup.patch
new file mode 100644
index 0000000000..2d05869c5d
--- /dev/null
+++ b/patches/@mdx-js__rollup.patch
@@ -0,0 +1,21 @@
+diff --git a/lib/index.js b/lib/index.js
+index ddbf4a0ebac3adb0c6277f4771ce413d8058a367..a412cc138011a53afa222ad7a4ae23d83ea18759 100644
+--- a/lib/index.js
++++ b/lib/index.js
+@@ -80,7 +80,7 @@ export function rollup(options) {
+ ...rest
+ })
+ },
+- async transform(value, path) {
++ async transform(value, id) {
+ if (!formatAwareProcessors) {
+ formatAwareProcessors = createFormatAwareProcessors({
+ SourceMapGenerator,
+@@ -88,6 +88,7 @@ export function rollup(options) {
+ })
+ }
+
++ const [path] = id.split('?')
+ const file = new VFile({path, value})
+
+ if (
diff --git a/playground/rsc-vite-framework/app/root.tsx b/playground/rsc-vite-framework/app/root.tsx
index 3ad743088b..72b5478532 100644
--- a/playground/rsc-vite-framework/app/root.tsx
+++ b/playground/rsc-vite-framework/app/root.tsx
@@ -1,6 +1,8 @@
-import { Link, Outlet } from "react-router";
+import { Meta, Link, Outlet } from "react-router";
import "./root.css";
+export const meta = () => [{ title: "React Router Vite" }];
+
export function Layout({ children }: { children: React.ReactNode }) {
console.log("Layout");
return (
@@ -8,7 +10,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
- React Router Vite
+
@@ -32,6 +34,9 @@ export function Layout({ children }: { children: React.ReactNode }) {
Client loader without server loader
+
+ MDX
+
diff --git a/playground/rsc-vite-framework/app/routes/mdx/route.mdx b/playground/rsc-vite-framework/app/routes/mdx/route.mdx
new file mode 100644
index 0000000000..a3d6fd31ae
--- /dev/null
+++ b/playground/rsc-vite-framework/app/routes/mdx/route.mdx
@@ -0,0 +1,16 @@
+import { useLoaderData } from "react-router";
+
+export const meta = () => [{ title: "MDX Route" }];
+
+export const loader = () => ({ message: "Loader data" });
+
+export function Message() {
+ const { message } = useLoaderData();
+ return Loader data: {message}
;
+}
+
+# This is an MDX route
+
+Hello from an MDX route!
+
+
diff --git a/playground/rsc-vite-framework/mdx.d.ts b/playground/rsc-vite-framework/mdx.d.ts
new file mode 100644
index 0000000000..9df03a1553
--- /dev/null
+++ b/playground/rsc-vite-framework/mdx.d.ts
@@ -0,0 +1,3 @@
+declare module "*.mdx" {
+ export default (...args: any[]) => unknown;
+}
diff --git a/playground/rsc-vite-framework/package.json b/playground/rsc-vite-framework/package.json
index c93a32839a..2582e0aba4 100644
--- a/playground/rsc-vite-framework/package.json
+++ b/playground/rsc-vite-framework/package.json
@@ -10,6 +10,7 @@
"typecheck": "react-router typegen && tsc"
},
"devDependencies": {
+ "@mdx-js/rollup": "^3.1.0",
"@react-router/dev": "workspace:*",
"@react-router/fs-routes": "workspace:*",
"@types/express": "^5.0.0",
diff --git a/playground/rsc-vite-framework/vite.config.ts b/playground/rsc-vite-framework/vite.config.ts
index 291921f20f..6fd8fbb5fa 100644
--- a/playground/rsc-vite-framework/vite.config.ts
+++ b/playground/rsc-vite-framework/vite.config.ts
@@ -1,9 +1,10 @@
import { defineConfig } from "vite";
import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from "@react-router/dev/internal";
+import mdx from "@mdx-js/rollup";
const { unstable_reactRouterRSC: reactRouterRSC } =
__INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;
export default defineConfig({
- plugins: [reactRouterRSC()],
+ plugins: [mdx(), reactRouterRSC()],
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 752d7faa7e..de0d6d2f0c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,6 +19,9 @@ patchedDependencies:
'@changesets/get-dependents-graph@1.3.6':
hash: arforfmj6nw2w4znv7h66rwf5y
path: patches/@changesets__get-dependents-graph@1.3.6.patch
+ '@mdx-js/rollup':
+ hash: wjxfd5pqp7spa3snsugst7roxm
+ path: patches/@mdx-js__rollup.patch
importers:
@@ -43,8 +46,8 @@ importers:
specifier: ^1.1.3
version: 1.1.3
'@mdx-js/rollup':
- specifier: ^3.0.0
- version: 3.1.0(rollup@4.43.0)
+ specifier: ^3.1.0
+ version: 3.1.0(patch_hash=wjxfd5pqp7spa3snsugst7roxm)(rollup@4.43.0)
'@playwright/test':
specifier: ^1.49.1
version: 1.49.1
@@ -164,7 +167,7 @@ importers:
dependencies:
'@mdx-js/rollup':
specifier: ^3.1.0
- version: 3.1.0(rollup@4.43.0)
+ version: 3.1.0(patch_hash=wjxfd5pqp7spa3snsugst7roxm)(rollup@4.43.0)
'@playwright/test':
specifier: ^1.49.1
version: 1.49.1
@@ -539,6 +542,9 @@ importers:
specifier: workspace:*
version: link:../../../packages/react-router
devDependencies:
+ '@mdx-js/rollup':
+ specifier: ^3.1.0
+ version: 3.1.0(patch_hash=wjxfd5pqp7spa3snsugst7roxm)(rollup@4.43.0)
'@react-router/dev':
specifier: workspace:*
version: link:../../../packages/react-router-dev
@@ -1935,6 +1941,9 @@ importers:
specifier: ^8.7.0
version: 8.7.0(react-router@packages+react-router)(react@19.1.0)(zod@3.24.2)
devDependencies:
+ '@mdx-js/rollup':
+ specifier: ^3.1.0
+ version: 3.1.0(patch_hash=wjxfd5pqp7spa3snsugst7roxm)(rollup@4.43.0)
'@react-router/dev':
specifier: workspace:*
version: link:../../packages/react-router-dev
@@ -11912,7 +11921,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@mdx-js/rollup@3.1.0(rollup@4.43.0)':
+ '@mdx-js/rollup@3.1.0(patch_hash=wjxfd5pqp7spa3snsugst7roxm)(rollup@4.43.0)':
dependencies:
'@mdx-js/mdx': 3.0.1
'@rollup/pluginutils': 5.1.0(rollup@4.43.0)