diff --git a/.changeset/fix-custom-server-paired-build.md b/.changeset/fix-custom-server-paired-build.md new file mode 100644 index 00000000..a1b2202a --- /dev/null +++ b/.changeset/fix-custom-server-paired-build.md @@ -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. diff --git a/README.md b/README.md index 49df75ba..5f47fa9d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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`: 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..a91f3b02 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 }) => @@ -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 } }, }), )