Skip to content

Commit f533d2e

Browse files
Add MDX route support to RSC Framework Mode (#14149)
1 parent a8f09ce commit f533d2e

File tree

17 files changed

+286
-63
lines changed

17 files changed

+286
-63
lines changed

integration/helpers/rsc-vite-framework/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"typecheck": "react-router typegen && tsc"
1111
},
1212
"devDependencies": {
13+
"@mdx-js/rollup": "^3.1.0",
1314
"@react-router/dev": "workspace:*",
1415
"@react-router/fs-routes": "workspace:*",
1516
"@types/express": "^5.0.0",

integration/helpers/vite.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ type ViteConfigBuildArgs = {
7878
type ViteConfigBaseArgs = {
7979
templateName?: TemplateName;
8080
envDir?: string;
81+
mdx?: boolean;
8182
};
8283

8384
type ViteConfigArgs = (
@@ -138,6 +139,7 @@ export const viteConfig = {
138139
"const { unstable_reactRouterRSC: reactRouter } = __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;",
139140
].join("\n")
140141
}
142+
${args.mdx ? 'import mdx from "@mdx-js/rollup";' : ""}
141143
import { envOnlyMacros } from "vite-env-only";
142144
import tsconfigPaths from "vite-tsconfig-paths";
143145
@@ -146,6 +148,7 @@ export const viteConfig = {
146148
${viteConfig.build(args)}
147149
envDir: ${args.envDir ? `"${args.envDir}"` : "undefined"},
148150
plugins: [
151+
${args.mdx ? "mdx()," : ""}
149152
reactRouter(),
150153
envOnlyMacros(),
151154
tsconfigPaths()

integration/mdx-test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
import {
4+
createFixture,
5+
createAppFixture,
6+
js,
7+
} from "./helpers/create-fixture.js";
8+
import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
9+
import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
10+
import {
11+
type TemplateName,
12+
reactRouterConfig,
13+
viteConfig,
14+
} from "./helpers/vite.js";
15+
16+
const templateNames = [
17+
"vite-5-template",
18+
"rsc-vite-framework",
19+
] as const satisfies TemplateName[];
20+
21+
test.describe("MDX", () => {
22+
for (const templateName of templateNames) {
23+
test.describe(`template: ${templateName}`, () => {
24+
let fixture: Fixture;
25+
let appFixture: AppFixture;
26+
27+
test.beforeAll(async () => {
28+
fixture = await createFixture({
29+
templateName,
30+
files: {
31+
"vite.config.js": await viteConfig.basic({
32+
templateName,
33+
mdx: true,
34+
}),
35+
"react-router.config.ts": reactRouterConfig({
36+
viteEnvironmentApi: templateName.includes("rsc"),
37+
}),
38+
"app/root.tsx": js`
39+
import { Outlet, Scripts } from "react-router"
40+
41+
export default function Root() {
42+
return (
43+
<html>
44+
<head></head>
45+
<body>
46+
<main>
47+
<Outlet />
48+
</main>
49+
<Scripts />
50+
</body>
51+
</html>
52+
);
53+
}
54+
`,
55+
"app/routes/_index.tsx": js`
56+
import { Link } from "react-router"
57+
export default function Component() {
58+
return <Link to="/mdx">Go to MDX route</Link>
59+
}
60+
`,
61+
"app/routes/mdx.mdx": js`
62+
import { MdxComponent } from "../components/mdx-components";
63+
64+
export const loader = () => {
65+
return {
66+
content: "MDX route content from loader",
67+
}
68+
}
69+
70+
## MDX Route
71+
72+
<MdxComponent />
73+
`,
74+
// This needs to be a separate file to support RSC since
75+
// `useLoaderData` is not available in RSC environments, and
76+
// components defined within an MDX file must be exported. This
77+
// means they're not removed in the RSC build.
78+
"app/components/mdx-components.tsx": js`
79+
import { useState, useEffect } from "react";
80+
import { useLoaderData } from "react-router";
81+
82+
export function MdxComponent() {
83+
const { content } = useLoaderData();
84+
const [mounted, setMounted] = useState(false);
85+
useEffect(() => {
86+
setMounted(true);
87+
}, []);
88+
89+
return (
90+
<>
91+
<h3>Loader data</h3>
92+
<div data-loader-data>{content}</div>
93+
<h3>Mounted</h3>
94+
<div data-mounted>{mounted ? "true" : "false"}</div>
95+
</>
96+
);
97+
}
98+
`,
99+
},
100+
});
101+
102+
appFixture = await createAppFixture(fixture);
103+
});
104+
105+
test.afterAll(() => {
106+
appFixture.close();
107+
});
108+
109+
test("handles MDX routes", async ({ page }) => {
110+
let app = new PlaywrightFixture(appFixture, page);
111+
await app.goto("/mdx");
112+
113+
let loaderData = page.locator("[data-loader-data]");
114+
await expect(loaderData).toHaveText("MDX route content from loader");
115+
116+
let mounted = page.locator("[data-mounted]");
117+
await expect(mounted).toHaveText("true");
118+
});
119+
});
120+
}
121+
});
Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,60 @@
11
import { test, expect } from "@playwright/test";
22
import dedent from "dedent";
33

4-
import { createProject, build } from "./helpers/vite.js";
5-
6-
test.describe(() => {
7-
let cwd: string;
8-
let buildResult: ReturnType<typeof build>;
9-
10-
test.beforeAll(async () => {
11-
cwd = await createProject({
12-
"vite.config.ts": dedent`
13-
import { reactRouter } from "@react-router/dev/vite";
14-
import mdx from "@mdx-js/rollup";
15-
16-
export default {
17-
plugins: [
18-
reactRouter(),
19-
mdx(),
20-
],
21-
}
22-
`,
4+
import { createProject, build, reactRouterConfig } from "./helpers/vite.js";
5+
6+
test.describe("Vite plugin order validation", () => {
7+
test.describe("MDX", () => {
8+
test("Framework Mode", async () => {
9+
let cwd = await createProject({
10+
"vite.config.ts": dedent`
11+
import { reactRouter } from "@react-router/dev/vite";
12+
import mdx from "@mdx-js/rollup";
13+
14+
export default {
15+
plugins: [
16+
reactRouter(),
17+
mdx(),
18+
],
19+
}
20+
`,
21+
});
22+
23+
let buildResult = build({ cwd });
24+
expect(buildResult.stderr.toString()).toContain(
25+
'Error: The "@mdx-js/rollup" plugin should be placed before the React Router plugin in your Vite config file',
26+
);
2327
});
2428

25-
buildResult = build({ cwd });
26-
});
29+
test("RSC Framework Mode", async () => {
30+
let cwd = await createProject(
31+
{
32+
"vite.config.js": dedent`
33+
import { defineConfig } from "vite";
34+
import { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__ } from "@react-router/dev/internal";
35+
import mdx from "@mdx-js/rollup";
2736
28-
test("Vite / plugin order validation / MDX", () => {
29-
expect(buildResult.stderr.toString()).toContain(
30-
'Error: The "@mdx-js/rollup" plugin should be placed before the React Router plugin in your Vite config file',
31-
);
37+
const { unstable_reactRouterRSC: reactRouterRSC } =
38+
__INTERNAL_DO_NOT_USE_OR_YOU_WILL_GET_A_STRONGLY_WORDED_LETTER__;
39+
40+
export default defineConfig({
41+
plugins: [
42+
reactRouterRSC(),
43+
mdx(),
44+
],
45+
});
46+
`,
47+
"react-router.config.ts": reactRouterConfig({
48+
viteEnvironmentApi: true,
49+
}),
50+
},
51+
"rsc-vite-framework",
52+
);
53+
54+
let buildResult = build({ cwd });
55+
expect(buildResult.stderr.toString()).toContain(
56+
'Error: The "@mdx-js/rollup" plugin should be placed before the React Router plugin in your Vite config file',
57+
);
58+
});
3259
});
3360
});

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"@babel/preset-typescript": "^7.27.1",
5555
"@changesets/cli": "^2.26.2",
5656
"@manypkg/get-packages": "^1.1.3",
57-
"@mdx-js/rollup": "^3.0.0",
57+
"@mdx-js/rollup": "^3.1.0",
5858
"@playwright/test": "^1.49.1",
5959
"@remix-run/changelog-github": "^0.0.5",
6060
"@types/jest": "^29.5.4",
@@ -100,7 +100,8 @@
100100
"pnpm": {
101101
"patchedDependencies": {
102102
"@changesets/[email protected]": "patches/@[email protected]",
103-
"@changesets/assemble-release-plan": "patches/@changesets__assemble-release-plan.patch"
103+
"@changesets/assemble-release-plan": "patches/@changesets__assemble-release-plan.patch",
104+
"@mdx-js/rollup": "patches/@mdx-js__rollup.patch"
104105
},
105106
"overrides": {
106107
"workerd": "1.20250705.0",

packages/react-router-dev/vite/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ async function viteAppBuild(
113113
let hasReactRouterPlugin = config.plugins.find(
114114
(plugin) =>
115115
plugin.name === "react-router" ||
116-
plugin.name === "react-router/rsc/config",
116+
plugin.name === "react-router/rsc",
117117
);
118118
if (!hasReactRouterPlugin) {
119119
throw new Error(

packages/react-router-dev/vite/dev.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ export async function dev(
5151
if (
5252
!server.config.plugins.find(
5353
(plugin) =>
54-
plugin.name === "react-router" ||
55-
plugin.name === "react-router/rsc/config",
54+
plugin.name === "react-router" || plugin.name === "react-router/rsc",
5655
)
5756
) {
5857
console.error(

packages/react-router-dev/vite/plugin.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import {
7979
configRouteToBranchRoute,
8080
} from "../config/config";
8181
import { decorateComponentExportsWithProps } from "./with-props";
82+
import validatePluginOrder from "./plugins/validate-plugin-order";
8283

8384
export type LoadCssContents = (
8485
viteDevServer: Vite.ViteDevServer,
@@ -725,11 +726,6 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
725726
};
726727
};
727728

728-
let pluginIndex = (pluginName: string) => {
729-
invariant(viteConfig);
730-
return viteConfig.plugins.findIndex((plugin) => plugin.name === pluginName);
731-
};
732-
733729
let getServerEntry = async ({ routeIds }: { routeIds?: Array<string> }) => {
734730
invariant(viteConfig, "viteconfig required to generate the server entry");
735731

@@ -1474,25 +1470,6 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
14741470
"Vite config file was unable to be resolved for React Router child compiler",
14751471
);
14761472

1477-
// Validate that commonly used Rollup plugins that need to run before
1478-
// ours are in the correct order. This is because Rollup plugins can't
1479-
// set `enforce: "pre"` like Vite plugins can. Explicitly validating
1480-
// this provides a much nicer developer experience.
1481-
let rollupPrePlugins = [
1482-
{ pluginName: "@mdx-js/rollup", displayName: "@mdx-js/rollup" },
1483-
];
1484-
for (let prePlugin of rollupPrePlugins) {
1485-
let prePluginIndex = pluginIndex(prePlugin.pluginName);
1486-
if (
1487-
prePluginIndex >= 0 &&
1488-
prePluginIndex > pluginIndex("react-router")
1489-
) {
1490-
throw new Error(
1491-
`The "${prePlugin.displayName}" plugin should be placed before the React Router plugin in your Vite config file`,
1492-
);
1493-
}
1494-
}
1495-
14961473
const childCompilerPlugins = await asyncFlatten(
14971474
childCompilerConfigFile.config.plugins ?? [],
14981475
);
@@ -1525,7 +1502,8 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
15251502
"name" in plugin &&
15261503
plugin.name !== "react-router" &&
15271504
plugin.name !== "react-router:route-exports" &&
1528-
plugin.name !== "react-router:hmr-updates",
1505+
plugin.name !== "react-router:hmr-updates" &&
1506+
plugin.name !== "react-router:validate-plugin-order",
15291507
)
15301508
// Remove server hooks to avoid conflicts with the main dev server
15311509
.map((plugin) => ({
@@ -2432,6 +2410,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
24322410
}
24332411
},
24342412
},
2413+
validatePluginOrder(),
24352414
];
24362415
};
24372416

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type * as Vite from "vite";
2+
3+
export default function validatePluginOrder(): Vite.Plugin {
4+
return {
5+
name: "react-router:validate-plugin-order",
6+
configResolved(viteConfig) {
7+
let pluginIndex = (pluginName: string | string[]) => {
8+
pluginName = Array.isArray(pluginName) ? pluginName : [pluginName];
9+
return viteConfig.plugins.findIndex((plugin) =>
10+
pluginName.includes(plugin.name),
11+
);
12+
};
13+
14+
let rollupPrePlugins = [
15+
{ pluginName: "@mdx-js/rollup", displayName: "@mdx-js/rollup" },
16+
];
17+
for (let prePlugin of rollupPrePlugins) {
18+
let prePluginIndex = pluginIndex(prePlugin.pluginName);
19+
console.log(
20+
prePluginIndex,
21+
pluginIndex(["react-router", "react-router/rsc"]),
22+
);
23+
if (
24+
prePluginIndex >= 0 &&
25+
prePluginIndex > pluginIndex(["react-router", "react-router/rsc"])
26+
) {
27+
throw new Error(
28+
`The "${prePlugin.displayName}" plugin should be placed before the React Router plugin in your Vite config file`,
29+
);
30+
}
31+
}
32+
},
33+
};
34+
}

0 commit comments

Comments
 (0)