Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
09f0646
chore: refresh deps and fix config/spa mode
ScriptedAlchemy Jan 27, 2026
53982d4
feat: support SPA mode + prerender
ScriptedAlchemy Jan 28, 2026
012daf3
docs: refresh readmes and spa-mode example
ScriptedAlchemy Jan 28, 2026
a143bdd
chore: update e2e config
ScriptedAlchemy Jan 28, 2026
27a01f8
chore: normalize e2e scripts
ScriptedAlchemy Jan 28, 2026
f977395
fix: align SPA mode export rules + middleware stripping
ScriptedAlchemy Jan 28, 2026
8cee82f
feat: update plugin federation startup and export handling
ScriptedAlchemy Jan 28, 2026
b7975d5
test: cover plugin validation and export filtering
ScriptedAlchemy Jan 28, 2026
ad76934
chore(examples): update federation epic-stack host/remote
ScriptedAlchemy Jan 28, 2026
e5fcf12
feat(examples): add client-only example
ScriptedAlchemy Jan 28, 2026
cdaf570
chore: include client-only example in e2e
ScriptedAlchemy Jan 28, 2026
805205f
docs: update README and add node version hints
ScriptedAlchemy Jan 28, 2026
0430a98
chore: update task metadata
ScriptedAlchemy Jan 28, 2026
adb0e01
fix: warn on string sourceMap config
ScriptedAlchemy Jan 28, 2026
ca7dcb4
test: migrate to rstest
ScriptedAlchemy Jan 29, 2026
ecfbf34
fix: lazy-load @react-router/node in dev server
ScriptedAlchemy Jan 30, 2026
972cf47
fix: resolve async server build exports
ScriptedAlchemy Jan 30, 2026
a3379d1
chore: always enable asyncStartup for federation
ScriptedAlchemy Jan 30, 2026
7aedffe
revert: only set asyncStartup when federation enabled
ScriptedAlchemy Jan 30, 2026
771b314
ci: run unit tests before build/e2e
ScriptedAlchemy Jan 30, 2026
82cbd70
fix: align prerender + client stubs with vite
ScriptedAlchemy Jan 30, 2026
6bc22a2
feat: align config/prerender parity with vite
ScriptedAlchemy Jan 31, 2026
c79413e
feat: add config/preset parity + build manifest
ScriptedAlchemy Jan 31, 2026
127e829
feat: emit server bundle entries for serverBundles
ScriptedAlchemy Jan 31, 2026
57b420c
Fix dev server bundle loading and client export stubs
ScriptedAlchemy Jan 31, 2026
548f9f1
Align prerender examples with client-only APIs
ScriptedAlchemy Jan 31, 2026
19240ab
Fix custom server bundle loading in dev
ScriptedAlchemy Jan 31, 2026
3170f65
Add build manifest hashing and SRI support
ScriptedAlchemy Jan 31, 2026
173612c
Ignore build outputs
ScriptedAlchemy Jan 31, 2026
5dadef3
Derive manifest path from entry output
ScriptedAlchemy Jan 31, 2026
f440aee
Add changeset for React Router parity updates
ScriptedAlchemy Jan 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
11 changes: 11 additions & 0 deletions .changeset/react-router-manifest-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"rsbuild-plugin-react-router": minor
---

Bring Rsbuild plugin behavior closer to React Router's official Vite plugin.

- Add React Router config resolution + validations/warnings for closer framework parity
- Add split route modules (route chunk entrypoints) including enforce mode validation
- Improve `.client` module stubbing on the server (including `export *` re-exports)
- Improve manifest generation: stable fingerprinted build manifests, bundle-specific server manifests, and optional Subresource Integrity (`future.unstable_subResourceIntegrity`)
- Improve Module Federation support by relying on Rspack `experiments.asyncStartup` (without overriding explicit CommonJS server output)
5 changes: 4 additions & 1 deletion .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ jobs:
- name: Install dependencies
run: pnpm install

- name: Run unit tests
run: pnpm test

- name: Build package
run: pnpm build

Expand All @@ -51,4 +54,4 @@ jobs:
run: npx playwright install --with-deps chromium

- name: Run E2E tests
run: pnpm e2e
run: pnpm e2e
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,18 @@
node_modules
tsconfig.tsbuildinfo
dist
build
.react-router
.npmrc
.unpack-cache/
.codex/
task/upstream/
task/output/

# Example build outputs
**/build/
**/.react-router/

# Playwright artifacts
playwright-report/
test-results/
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22
22
216 changes: 213 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ A Rsbuild plugin that provides seamless integration with React Router, supportin
- 🚀 Zero-config setup with sensible defaults
- 🔄 Automatic route generation from file system
- 🖥️ Server-Side Rendering (SSR) support
- 📱 Client-side navigation
- 📱 Client-side navigation with SPA mode (`ssr: false`)
- 📄 Static prerendering for hybrid static/dynamic sites
- 🛠️ TypeScript support out of the box
- 🔧 Customizable configuration
- 🎯 Support for route-level code splitting
- ☁️ Cloudflare Workers deployment support
- 🔗 Module Federation support (experimental)

## Installation

Expand All @@ -26,6 +29,18 @@ yarn add rsbuild-plugin-react-router
pnpm add rsbuild-plugin-react-router
```

## Local development

For the federation examples and Playwright e2e tests, use Node 22 and the
repo-pinned pnpm version:

```bash
nvm install
nvm use
corepack enable
corepack prepare pnpm@9.15.3 --activate
```

## Usage

Add the plugin to your `rsbuild.config.ts`:
Expand All @@ -43,7 +58,7 @@ export default defineConfig(() => {
customServer: false,
// Optional: Specify server output format
serverOutput: "commonjs",
//Optional: enable experimental support for module federation
// Optional: enable experimental support for module federation
federation: false
}),
pluginReact()
Expand Down Expand Up @@ -78,9 +93,18 @@ pluginReactRouter({
*/
federation?: boolean
})

When Module Federation is enabled, configure your Federation plugin with
`experiments.asyncStartup: true` to avoid requiring entrypoint `import()` hacks.
See the Module Federation examples under `examples/federation`.

When Module Federation is enabled, some runtimes may expose server build exports
as async getters. The dev server resolves these exports automatically. For
production, use a custom server or an adapter that resolves async exports before
passing the build to React Router's request handler.
```

2. **React Router Configuration** (in `react-router.config.ts`):
2. **React Router Configuration** (in `react-router.config.*`):
```ts
import type { Config } from '@react-router/dev/config';

Expand All @@ -91,6 +115,31 @@ export default {
*/
ssr: true,

/**
* The file name for the server build output.
* @default "index.js"
*/
serverBuildFile: "index.js",

/**
* The output format for the server build.
* Options: "esm" | "cjs"
* @default "esm"
*/
serverModuleFormat: "esm",

/**
* Split server bundles by route branch (advanced).
*/
serverBundles: async ({ branch }) => branch[0]?.id ?? "main",

/**
* Hook called after the build completes.
*/
buildEnd: async ({ buildManifest, reactRouterConfig }) => {
console.log(buildManifest, reactRouterConfig);
},

/**
* Build directory for output files
* @default 'build'
Expand All @@ -108,11 +157,110 @@ export default {
* @default '/'
*/
basename: '/my-app',

/**
* React Router future flags (optional).
* Example: split client route modules into separate chunks.
*/
future: {
v8_splitRouteModules: true,
},
} satisfies Config;
```

All configuration options are optional and will use sensible defaults if not specified.

### Config File Resolution

The plugin will look for `react-router.config` with any supported JS/TS extension, in this order:

- `react-router.config.tsx`
- `react-router.config.ts`
- `react-router.config.mts`
- `react-router.config.jsx`
- `react-router.config.js`
- `react-router.config.mjs`

If none are found, it falls back to defaults.

### Framework Mode

React Router Framework Mode is implemented as a Vite plugin. This Rsbuild
plugin targets Data Mode only and does not support Framework Mode.

### FAQ

#### rsbuild-plugin-react-router vs ModernJS

This plugin is a lightweight adapter to run React Router on Rsbuild. It does
not aim to replace ModernJS or its higher-level framework features. If your
goal is a full framework or advanced microfrontend support, ModernJS may be
a better fit.

### SPA Mode (`ssr: false`)

React Router's SPA Mode still requires a build-time server render of the root route to generate a hydratable `index.html` (this is how the official React Router Vite plugin works).

When `ssr: false`:

- The plugin builds both `web` and `node` internally.
- It generates `build/client/index.html` by running the server build once (requesting `basename` with the `X-React-Router-SPA-Mode: yes` header).
- It removes `build/server` after generating `index.html`, so the output is deployable as static assets.

**Important:** In SPA mode, use `clientLoader` instead of `loader` for data loading since there's no server at runtime.

### Static Prerendering

For static sites with multiple pages, you can prerender specific routes at build time:

```ts
// react-router.config.ts
import type { Config } from '@react-router/dev/config';

export default {
ssr: false,
prerender: [
'/',
'/about',
'/docs',
'/docs/getting-started',
'/docs/advanced',
'/projects',
],
} satisfies Config;
```

When `prerender` is specified:

- Each path in the array is rendered at build time
- Static HTML files are generated for each route (e.g., `/about` → `build/client/about/index.html`)
- The server build is removed after prerendering for static deployment
- Non-prerendered routes fall back to client-side routing

You can also use `prerender: true` to prerender all static routes automatically.

`prerender` can also be a function:

```ts
export default {
ssr: false,
prerender: ({ getStaticPaths }) =>
getStaticPaths().filter(path => path !== '/admin'),
} satisfies Config;
```

For large sites, you can tune prerender concurrency:

```ts
export default {
ssr: false,
prerender: {
paths: ['/','/about'],
unstable_concurrency: 4,
},
} satisfies Config;
```

### Default Configuration Values

If no configuration is provided, the following defaults will be used:
Expand Down Expand Up @@ -187,6 +335,7 @@ Route components support the following exports:
- `Layout` - Layout component
- `clientLoader` - Client-side data loading
- `clientAction` - Client-side form actions
- `clientMiddleware` - Client-side middleware
- `handle` - Route handle
- `links` - Prefetch links
- `meta` - Route meta data
Expand All @@ -195,8 +344,24 @@ Route components support the following exports:
#### Server-side Exports
- `loader` - Server-side data loading
- `action` - Server-side form actions
- `middleware` - Server-side middleware
- `headers` - HTTP headers

### Client/Server-only Modules

- Files ending in `.client.*` are treated as client-only. Their exports are
stubbed to `undefined` in the server build, so they are safe to import from
route components for browser-only behavior.
- Files ending in `.server.*` are server-only. If they are imported by code
compiled for the web environment, the build will fail with a clear error.
Keep `.server` imports in server entrypoints or other server-only code.

### Asset Prefix

If you configure `output.assetPrefix` in Rsbuild, the plugin uses that value
for the React Router browser manifest and server build `publicPath` so asset
URLs resolve correctly when serving from a CDN or sub-path.

## Custom Server Setup

The plugin supports two ways to handle server-side rendering:
Expand Down Expand Up @@ -479,6 +644,51 @@ The plugin automatically:
- Handles route-based code splitting
- Manages client and server builds

## React Router Framework Mode

React Router "Framework Mode" wraps Data Mode using a Vite plugin. This Rsbuild plugin currently targets React Router's Data Mode build/runtime model and does not implement the Vite plugin layer (type-safe href, route module splitting, etc.).

## Examples

The repository includes several examples demonstrating different use cases:

| Example | Description | Port | Command |
|---------|-------------|------|---------|
| [default-template](./examples/default-template) | Standard SSR setup with React Router | 3000 | `pnpm dev` |
| [spa-mode](./examples/spa-mode) | Single Page Application (`ssr: false`) | 3001 | `pnpm dev` |
| [prerender](./examples/prerender) | Static prerendering for multiple routes | 3002 | `pnpm dev` |
| [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` |
| [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` |

Each example has unique ports configured to allow running multiple examples simultaneously.

### Running Examples

```bash
# Install dependencies
pnpm install

# Build the plugin
pnpm build

# Run any example
cd examples/default-template
pnpm dev
```

### Running E2E Tests

Each example includes Playwright e2e tests:

```bash
cd examples/default-template
pnpm test:e2e
```

## License

MIT
8 changes: 4 additions & 4 deletions config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"version": "1.0.1",
"private": true,
"devDependencies": {
"@rsbuild/core": "1.3.2",
"@rslib/core": "0.5.4",
"@types/node": "^22.10.1",
"typescript": "^5.7.2"
"@rsbuild/core": "1.7.2",
"@rslib/core": "0.19.3",
"@types/node": "^25.0.10",
"typescript": "^5.9.3"
}
}
16 changes: 16 additions & 0 deletions examples/client-only/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Client-only Modules Example

This example shows how `.client` modules are stubbed on the server build and
loaded on the client after hydration.

## Run

```bash
pnpm dev
```

## E2E

```bash
pnpm test:e2e
```
3 changes: 3 additions & 0 deletions examples/client-only/app/client-value.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getClientValue(): string {
return `client:${window.location.pathname}`;
}
Loading
Loading