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)