Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/fix-custom-server-paired-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'rsbuild-plugin-react-router': patch
---

Use generation-paired manifest assets in the Epic Stack custom-server development example and document module-graph boundaries.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,12 @@ startServer().catch(console.error);
generation. During rebuilds it returns the last successfully evaluated server
build, whose embedded manifest is paired with the selected web compilation.
A failed or incomplete candidate does not replace that last-good pair. The
built-in development middleware uses the same path. Calling
`devServer.environments.node.loadBundle()` directly bypasses this guarantee.
built-in development middleware uses the same path. A custom server may still
load its own `server/index` bundle, but returning that bundle's embedded
`virtual/react-router/server-build` unchanged can leave its manifest out of
sync. Use the paired build as the handler build, or, when module-instance
identity must be preserved, keep the embedded build and replace only its
`assets` with those from the paired build.

When `serverBundles` is configured, pass its exact Rsbuild entry name as the
optional second argument (for example, `bundle-a/index`). The default build
Expand All @@ -524,8 +528,14 @@ previous generation active.

### Sharing `createContext()` instances with a custom server

React Router middleware contexts are matched by identity, so a custom server's
`getLoadContext` must use the same `createContext()` instance the routes import.
The paired React Router server build is a separate module graph from a
separately bundled custom-server entry, so module-scope singleton identity is
not shared between them. If the handler must keep the bundled server entry's
route modules, retain its embedded build and replace only its `assets` with the
paired build's `assets` (as above). Pass per-request shared values through
`getLoadContext`. React Router middleware contexts are matched by identity, so
a custom server's `getLoadContext` must use the same `createContext()` instance
that its routes import.
With a bundled server build that instance lives inside the build. Re-export it
from `app/entry.server.tsx` and read it from `build.entry.module`:

Expand Down
8 changes: 6 additions & 2 deletions examples/epic-stack/server/dev-build.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createRsbuild, loadConfig } from '@rsbuild/core'
import { loadReactRouterServerBuild } from 'rsbuild-plugin-react-router'
import 'dotenv/config'

async function startServer() {
Expand All @@ -13,9 +14,12 @@ async function startServer() {
throw new Error('Node environment not found in dev server')
}

const bundle = await devServer.environments.node.loadBundle('app')
const bundle = await devServer.environments.node.loadBundle('static/js/app')
const { createApp } = bundle
const app = await createApp(devServer)
const app = await createApp(
devServer,
() => loadReactRouterServerBuild(devServer),
)

devServer.connectWebSocket({ server: app })
}
Expand Down
9 changes: 7 additions & 2 deletions examples/epic-stack/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ if (SENTRY_ENABLED) {
void import('./utils/monitoring.ts').then(({ init }) => init())
}

export async function createApp(devServer?: any) {
export async function createApp(
devServer?: any,
loadDevBuild?: () => Promise<ServerBuild>,
) {
const app = express()

const getHost = (req: { get: (key: string) => string | undefined }) =>
Expand Down Expand Up @@ -256,7 +259,9 @@ export async function createApp(devServer?: any) {
if (error) {
throw error
}
return build
if (!loadDevBuild) return build
const pairedBuild = await loadDevBuild()
return { ...(await build), assets: pairedBuild.assets }
},
}),
)
Expand Down