Skip to content

Add MDX route support to RSC Framework Mode #14149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
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 integration/helpers/rsc-vite-framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions integration/helpers/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type ViteConfigBuildArgs = {
type ViteConfigBaseArgs = {
templateName?: TemplateName;
envDir?: string;
mdx?: boolean;
};

type ViteConfigArgs = (
Expand Down Expand Up @@ -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";

Expand All @@ -146,6 +148,7 @@ export const viteConfig = {
${viteConfig.build(args)}
envDir: ${args.envDir ? `"${args.envDir}"` : "undefined"},
plugins: [
${args.mdx ? "mdx()," : ""}
reactRouter(),
envOnlyMacros(),
tsconfigPaths()
Expand Down
121 changes: 121 additions & 0 deletions integration/mdx-test.ts
Original file line number Diff line number Diff line change
@@ -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 (
<html>
<head></head>
<body>
<main>
<Outlet />
</main>
<Scripts />
</body>
</html>
);
}
`,
"app/routes/_index.tsx": js`
import { Link } from "react-router"
export default function Component() {
return <Link to="/mdx">Go to MDX route</Link>
}
`,
"app/routes/mdx.mdx": js`
import { MdxComponent } from "../components/mdx-components";

export const loader = () => {
return {
content: "MDX route content from loader",
}
}

## MDX Route

<MdxComponent />
`,
// 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 (
<>
<h3>Loader data</h3>
<div data-loader-data>{content}</div>
<h3>Mounted</h3>
<div data-mounted>{mounted ? "true" : "false"}</div>
</>
);
}
`,
},
});

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");
});
});
}
});
77 changes: 52 additions & 25 deletions integration/vite-plugin-order-validation-test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof build>;

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',
);
});
});
});
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -100,7 +100,8 @@
"pnpm": {
"patchedDependencies": {
"@changesets/[email protected]": "patches/@[email protected]",
"@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",
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router-dev/vite/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 1 addition & 2 deletions packages/react-router-dev/vite/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
29 changes: 4 additions & 25 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string> }) => {
invariant(viteConfig, "viteconfig required to generate the server entry");

Expand Down Expand Up @@ -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 ?? [],
);
Expand Down Expand Up @@ -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) => ({
Expand Down Expand Up @@ -2432,6 +2410,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
}
},
},
validatePluginOrder(),
];
};

Expand Down
34 changes: 34 additions & 0 deletions packages/react-router-dev/vite/plugins/validate-plugin-order.ts
Original file line number Diff line number Diff line change
@@ -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`,
);
}
}
},
};
}
Loading
Loading