From 39cdd8fd400554eeeb1eb26b1c9a084cfc4d019f Mon Sep 17 00:00:00 2001 From: dajiaohuang <108231307+dajiaohuang@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:50:28 +0800 Subject: [PATCH 1/2] fix(examples): pair custom server dev builds --- .changeset/fix-custom-server-paired-build.md | 5 +++++ README.md | 11 ++++++++--- examples/epic-stack/server/dev-build.js | 8 ++++++-- examples/epic-stack/server/index.ts | 6 +++++- 4 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-custom-server-paired-build.md diff --git a/.changeset/fix-custom-server-paired-build.md b/.changeset/fix-custom-server-paired-build.md new file mode 100644 index 00000000..124c4fea --- /dev/null +++ b/.changeset/fix-custom-server-paired-build.md @@ -0,0 +1,5 @@ +--- +'rsbuild-plugin-react-router': patch +--- + +Use the paired React Router server build in the Epic Stack custom-server development example and clarify module-graph sharing. diff --git a/README.md b/README.md index 49df75ba..5baf60fc 100644 --- a/README.md +++ b/README.md @@ -513,8 +513,10 @@ 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. Loading +`virtual/react-router/server-build` directly from another custom-server bundle +or calling `devServer.environments.node.loadBundle()` directly bypasses this +guarantee. When `serverBundles` is configured, pass its exact Rsbuild entry name as the optional second argument (for example, `bundle-a/index`). The default build @@ -524,7 +526,10 @@ previous generation active. ### Sharing `createContext()` instances with a custom server -React Router middleware contexts are matched by identity, so a custom server's +The paired React Router server build is a separate module graph from a +separately bundled custom-server entry; module-scope singleton identity is not +shared between them. Pass shared values through `getLoadContext`. React Router +middleware contexts are matched by identity, so a custom server's `getLoadContext` must use the same `createContext()` instance the 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`: diff --git a/examples/epic-stack/server/dev-build.js b/examples/epic-stack/server/dev-build.js index ce41f688..af50af30 100644 --- a/examples/epic-stack/server/dev-build.js +++ b/examples/epic-stack/server/dev-build.js @@ -1,4 +1,5 @@ import { createRsbuild, loadConfig } from '@rsbuild/core' +import { loadReactRouterServerBuild } from 'rsbuild-plugin-react-router' import 'dotenv/config' async function startServer() { @@ -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 }) } diff --git a/examples/epic-stack/server/index.ts b/examples/epic-stack/server/index.ts index b7a81488..42f80874 100644 --- a/examples/epic-stack/server/index.ts +++ b/examples/epic-stack/server/index.ts @@ -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, +) { const app = express() const getHost = (req: { get: (key: string) => string | undefined }) => @@ -251,6 +254,7 @@ export async function createApp(devServer?: any) { }), mode: MODE, build: async () => { + if (loadDevBuild) return loadDevBuild() const { error, build } = await getBuild() // gracefully "catch" the error if (error) { From ea41cfa2c32ada2f70410e610803590d38532893 Mon Sep 17 00:00:00 2001 From: dajiaohuang <108231307+dajiaohuang@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:57:05 +0800 Subject: [PATCH 2/2] fix(examples): preserve embedded route modules --- .changeset/fix-custom-server-paired-build.md | 2 +- README.md | 21 ++++++++++++-------- examples/epic-stack/server/index.ts | 5 +++-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.changeset/fix-custom-server-paired-build.md b/.changeset/fix-custom-server-paired-build.md index 124c4fea..a1b2202a 100644 --- a/.changeset/fix-custom-server-paired-build.md +++ b/.changeset/fix-custom-server-paired-build.md @@ -2,4 +2,4 @@ 'rsbuild-plugin-react-router': patch --- -Use the paired React Router server build in the Epic Stack custom-server development example and clarify module-graph sharing. +Use generation-paired manifest assets in the Epic Stack custom-server development example and document module-graph boundaries. diff --git a/README.md b/README.md index 5baf60fc..5f47fa9d 100644 --- a/README.md +++ b/README.md @@ -513,10 +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. Loading -`virtual/react-router/server-build` directly from another custom-server bundle -or 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 @@ -527,10 +529,13 @@ previous generation active. ### Sharing `createContext()` instances with a custom server The paired React Router server build is a separate module graph from a -separately bundled custom-server entry; module-scope singleton identity is not -shared between them. Pass shared values through `getLoadContext`. React Router -middleware contexts are matched by identity, so a custom server's -`getLoadContext` must use the same `createContext()` instance the routes import. +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`: diff --git a/examples/epic-stack/server/index.ts b/examples/epic-stack/server/index.ts index 42f80874..a91f3b02 100644 --- a/examples/epic-stack/server/index.ts +++ b/examples/epic-stack/server/index.ts @@ -254,13 +254,14 @@ export async function createApp( }), mode: MODE, build: async () => { - if (loadDevBuild) return loadDevBuild() const { error, build } = await getBuild() // gracefully "catch" the error if (error) { throw error } - return build + if (!loadDevBuild) return build + const pairedBuild = await loadDevBuild() + return { ...(await build), assets: pairedBuild.assets } }, }), )