Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1573805
feat: support react router 8 compatibility
ScriptedAlchemy Jun 28, 2026
d72ed19
chore: simplify react router compatibility helpers
ScriptedAlchemy Jun 28, 2026
fe0b412
chore: deslop react router config typing
ScriptedAlchemy Jun 28, 2026
8cad438
test: add React Router 8 default template e2e
ScriptedAlchemy Jun 29, 2026
22c2341
chore: deslop React Router 8 PR
ScriptedAlchemy Jun 29, 2026
d627f24
test: port React Router framework suite
ScriptedAlchemy Jun 29, 2026
3fe3e8c
fix: analyze transformed route modules for MDX
ScriptedAlchemy Jun 29, 2026
c567679
feat: add React Router RSC support
ScriptedAlchemy Jun 29, 2026
395b93e
refactor: simplify RSC support plumbing
ScriptedAlchemy Jun 29, 2026
98bc2c8
refactor: isolate React Router RSC support
ScriptedAlchemy Jun 29, 2026
c4e884b
Fix RSC MDX route transforms
ScriptedAlchemy Jun 29, 2026
3ee64a9
refactor: simplify RSC MDX loader wiring
ScriptedAlchemy Jun 29, 2026
1981075
fix: sync RSC example lockfile
ScriptedAlchemy Jun 29, 2026
8938c9b
fix: clean rr8 restack conflicts
ScriptedAlchemy Jun 29, 2026
2b2720d
fix: repair rr8 playwright lockfile
ScriptedAlchemy Jun 29, 2026
2fab6b4
fix: preserve rr8 restack behavior
ScriptedAlchemy Jun 30, 2026
c6f3c82
chore: update rr8 changeset
ScriptedAlchemy Jun 30, 2026
f2fd84c
Merge remote-tracking branch 'origin/codex/prewarm-route-workers' int…
ScriptedAlchemy Jun 30, 2026
9bb4400
chore: restore inherited changeset
ScriptedAlchemy Jun 30, 2026
3c901d8
Merge prewarm workers into React Router 8 compat
ScriptedAlchemy Jul 1, 2026
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
10 changes: 10 additions & 0 deletions .changeset/react-router-8-default-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'rsbuild-plugin-react-router': minor
---

Add React Router 8 compatibility while preserving React Router 7 behavior.
The plugin now supports stable React Router 8 config fields, resolves
prerender data requests for the installed React Router major version, supports
React Router RSC mode, analyzes transformed MDX route modules for manifest
generation, and includes React Router 8/RSC examples plus framework integration
coverage.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ The repository includes several examples demonstrating different use cases:
| [custom-node-server](./examples/custom-node-server) | Custom Express server with SSR | 3003 | `pnpm dev` |
| [cloudflare](./examples/cloudflare) | Cloudflare Workers deployment | 3004 | `pnpm dev` |
| [client-only](./examples/client-only) | `.client` modules with SSR hydration | 3010 | `pnpm dev` |
| [react-router-8](./examples/react-router-8) | React Router 8 framework-mode SSR | 3020 | `pnpm dev` |
| [epic-stack](./examples/epic-stack) | Full-featured Epic Stack example | 3005 | `pnpm dev` |
| [federation/epic-stack](./examples/federation/epic-stack) | Module Federation host | 3006 | `pnpm dev` |
| [federation/epic-stack-remote](./examples/federation/epic-stack-remote) | Module Federation remote | 3007 | `pnpm dev` |
Expand Down
6 changes: 6 additions & 0 deletions examples/react-router-8/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules

/.cache
/build
.env
.react-router
5 changes: 5 additions & 0 deletions examples/react-router-8/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# React Router 8 Default Template

This example starts as a direct copy of React Router's `integration/helpers/vite-8-template` and swaps the Vite config/scripts for `rsbuild-plugin-react-router`.

Run `pnpm --filter react-router-8-default-template test:e2e` to exercise development and production browser flows.
19 changes: 19 additions & 0 deletions examples/react-router-8/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";

export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
4 changes: 4 additions & 0 deletions examples/react-router-8/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";

export default flatRoutes() satisfies RouteConfig;
16 changes: 16 additions & 0 deletions examples/react-router-8/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { MetaFunction } from "react-router";

export const meta: MetaFunction = () => {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
};

export default function Index() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
<h1>Welcome to React Router</h1>
</div>
);
}
2 changes: 2 additions & 0 deletions examples/react-router-8/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference types="@react-router/node" />
/// <reference types="vite/client" />
42 changes: 42 additions & 0 deletions examples/react-router-8/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "react-router-8-default-template",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "NODE_OPTIONS=\"--experimental-vm-modules --experimental-global-webcrypto\" rsbuild dev --port 3020 --host 127.0.0.1",
"build": "rsbuild build",
"start": "HOST=127.0.0.1 PORT=3020 react-router-serve ./build/server/static/js/app.js",
"test:e2e": "pnpm run test:e2e:dev && pnpm run build && pnpm run test:e2e:prod",
"test:e2e:dev": "playwright test",
"test:e2e:prod": "cross-env RR8_E2E_MODE=production playwright test",
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
"@react-router/fs-routes": "^8.0.1",
"@react-router/node": "^8.0.1",
"@react-router/serve": "^8.0.1",
"@vanilla-extract/css": "^1.20.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router": "^8.0.1",
"serialize-javascript": "^6.0.1"
},
"devDependencies": {
"@playwright/test": "^1.58.0",
"@react-router/dev": "^8.0.1",
"@rsbuild/core": "2.1.0",
"@rsbuild/plugin-react": "2.1.0",
"@types/node": "^25.0.10",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"cross-env": "^10.1.0",
"rsbuild-plugin-react-router": "workspace:*",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-env-only": "^3.0.3"
},
"engines": {
"node": ">=22.22.0"
}
}
27 changes: 27 additions & 0 deletions examples/react-router-8/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineConfig, devices } from '@playwright/test';

const isProduction = process.env.RR8_E2E_MODE === 'production';

export default defineConfig({
testDir: './tests/e2e',
timeout: 30_000,
expect: {
timeout: 10_000,
},
use: {
baseURL: 'http://127.0.0.1:3020',
trace: 'on-first-retry',
},
webServer: {
command: isProduction ? 'pnpm run start' : 'pnpm run dev',
url: 'http://127.0.0.1:3020',
reuseExistingServer: !process.env.CI,
timeout: 60_000,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
Binary file added examples/react-router-8/public/favicon.ico
Binary file not shown.
6 changes: 6 additions & 0 deletions examples/react-router-8/react-router.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
ssr: true,
routeDiscovery: { mode: 'initial' },
splitRouteModules: true,
subResourceIntegrity: true,
};
7 changes: 7 additions & 0 deletions examples/react-router-8/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginReactRouter } from 'rsbuild-plugin-react-router';

export default defineConfig({
plugins: [pluginReactRouter(), pluginReact()],
});
44 changes: 44 additions & 0 deletions examples/react-router-8/tests/e2e/react-router-8.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { expect, test } from '@playwright/test';

test('renders the React Router 8 default template without browser errors', async ({
page,
}) => {
const browserProblems: string[] = [];
page.on('console', message => {
if (message.type() === 'error') {
browserProblems.push(`console error: ${message.text()}`);
}
});
page.on('pageerror', error => {
browserProblems.push(`page error: ${error.message}`);
});
page.on('response', response => {
if (response.status() >= 500) {
browserProblems.push(`${response.status()} response: ${response.url()}`);
}
});
page.on('requestfailed', request => {
if (request.resourceType() !== 'websocket') {
browserProblems.push(
`${request.method()} ${request.url()} failed: ${
request.failure()?.errorText ?? 'unknown error'
}`
);
}
});

const response = await page.goto('/');
expect(response?.ok()).toBe(true);
await expect(
page.getByRole('heading', { name: 'Welcome to React Router' })
).toBeVisible();
await expect(page).toHaveTitle('New React Router App');
await page.waitForFunction(
() =>
(window as Window & { __reactRouterRouteModules?: unknown })
.__reactRouterRouteModules !== undefined
);
await page.waitForTimeout(250);

expect(browserProblems).toEqual([]);
});
22 changes: 22 additions & 0 deletions examples/react-router-8/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"include": ["env.d.ts", "**/*.ts", "**/*.tsx", ".react-router/types/**/*"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"jsx": "react-jsx",
"resolveJsonModule": true,
"allowJs": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
"noEmit": true,
"rootDirs": [".", ".react-router/types/"],
"skipLibCheck": true,
"strict": true
}
}
8 changes: 8 additions & 0 deletions examples/rsc-mode/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/node_modules/

# React Router
/.react-router/
/build/
test-results
.rspack-profile-*
25 changes: 25 additions & 0 deletions examples/rsc-mode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# RSC Mode Example

This example is a small React Router RSC Framework Mode app wired for `rsbuild-plugin-react-router`.

It includes:

- `pluginReactRouter({ rsc: true })` in `rsbuild.config.ts`
- RSC-safe React Router config without `splitRouteModules` or `subResourceIntegrity`
- A server-first index route using `ServerComponent`
- A loader that returns a React element rendered on the server
- A `"use client"` island mounted inside the server-first route
- A client-first route for soft navigation coverage
- A Playwright smoke test for dev and production mode

## Commands

```sh
pnpm run dev
pnpm run build
pnpm run test:e2e
```

## Notes

This example uses `react-server-dom-rspack` and `rsbuild-plugin-rsc`; it does not use the Vite RSC runtime packages from the upstream React Router template.
17 changes: 17 additions & 0 deletions examples/rsc-mode/app/client-counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import { useState } from 'react';

export function ClientCounter() {
const [count, setCount] = useState(0);

return (
<button
className="counter-button"
type="button"
onClick={() => setCount(current => current + 1)}
>
Client island count: {count}
</button>
);
}
64 changes: 64 additions & 0 deletions examples/rsc-mode/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Route } from './+types/root';
import {
isRouteErrorResponse,
Link,
Links,
Meta,
Outlet,
} from 'react-router';

import './styles.css';

export function meta() {
return [
{ title: 'Rsbuild RSC example' },
{
name: 'description',
content: 'React Router RSC Framework Mode with Rsbuild',
},
];
}

export function ServerLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<header className="site-header">
<Link className="brand" to="/">
RSC Mode
</Link>
<nav aria-label="Primary navigation">
<Link to="/">Server route</Link>
<Link to="/client">Client route</Link>
</nav>
</header>
<main>{children}</main>
</body>
</html>
);
}

export function ServerComponent() {
return <Outlet />;
}

export function ServerErrorBoundary({ error }: Route.ServerErrorBoundaryProps) {
const message = isRouteErrorResponse(error)
? `${error.status} ${error.statusText}`
: error instanceof Error
? error.message
: 'Unknown error';

return (
<section className="page-shell">
<h1>Route error</h1>
<p>{message}</p>
</section>
);
}
6 changes: 6 additions & 0 deletions examples/rsc-mode/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { type RouteConfig, index, route } from '@react-router/dev/routes';

export default [
index('routes/_index.tsx'),
route('client', 'routes/client.tsx'),
] satisfies RouteConfig;
32 changes: 32 additions & 0 deletions examples/rsc-mode/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Route } from './+types/_index';
import { Link } from 'react-router';

import { ClientCounter } from '~/client-counter';
import { getRscShowcase } from '~/rsc-data';

export async function loader() {
return getRscShowcase();
}

export function ServerComponent({ loaderData }: Route.ServerComponentProps) {
return (
<section className="page-shell hero-panel">
<div className="hero-copy">
<h1>Rsbuild React Router RSC</h1>
<p data-testid="server-message">{loaderData.message}</p>
<p className="server-element">{loaderData.element}</p>
</div>

<div className="interaction-panel">
<p>
This route renders through React Router RSC Framework Mode and mounts a
small client island inside the server-first route.
</p>
<ClientCounter />
<Link className="text-link" to="/client">
Visit the client-first route
</Link>
</div>
</section>
);
}
Loading