Repository navigation
[Bug]: dev server build embedded in a custom server/index bundle keeps a stale, content-addressed browser manifest (0.8.x) #159
Description
Plugin version: 0.8.2 (works on 0.7.2) · @rsbuild/core: 2.2.12 · @rspack/core: 2.2.8 · react-router: 7.18.4
Problem
With pluginReactRouter({ customServer: true }) and a server/index.ts entry that imports virtual/react-router/server-build itself (the layout examples/epic-stack uses), the dev SSR HTML references a browser manifest the dev server does not serve:
<link rel="modulepreload" href="/static/js/manifest-2492e836.js"/>GET /static/js/manifest-2492e836.js -> 200 text/html (falls through to the SSR handler)
GET /static/js/manifest-6dd56d97.js -> 200 text/javascript
The page never hydrates.
The server build embedded in the static/js/app bundle carries a manifest that was generated before the web compilation finished. Its route css arrays are empty, so its sha256 version differs from the published browser manifest:
static/js/app (server/index.ts) version 2492e836 routes.root.css: []
loadReactRouterServerBuild(devServer) version 6dd56d97 routes.root.css: ["/static/css/root.css.__react_router_css_<hash>.css"]
The embedded copy is never regenerated. It stays at 2492e836 after source edits that rebuild both environments, and after the content of node_modules/.cache/react-router/<hash>/server-manifest.json changes (it holds the correct 6dd56d97 snapshot). loadReactRouterServerBuild(devServer) returns the correct pair the whole time.
0.7.2 used a fixed dev manifest URL (static/js/virtual/react-router/browser-manifest.js?v=...), so the stale embedded copy still loaded. Since #138 (0.8.0), dev manifest filenames are content-addressed, so the mismatch breaks the page.
Reproduction
// server/index.ts (bundled as static/js/app)
import { createRequestHandler } from '@react-router/express';
export const handler = createRequestHandler({
build: () => import('virtual/react-router/server-build'),
});
// server/start.js
const devServer = await rsbuild.createDevServer();
app.use(devServer.middlewares);
app.all('/{*splat}', async (req, res, next) => {
const mod = await devServer.environments.node.loadBundle('static/js/app');
await mod.handler(req, res, next);
});Start the dev server with an empty node_modules/.cache/react-router, request a page, and compare the manifest-*.js URL in the HTML with the one the web environment emits.
Workaround
Keep the routes and server setup from the server/index bundle, and take assets from the plugin's paired build:
const mod = await devServer.environments.node.loadBundle('static/js/app');
const paired = await loadReactRouterServerBuild(devServer);
// build = { ...embeddedServerBuild, assets: paired.assets }Using the paired build wholesale isn't an option for us. static/js/react-router-server-build is a separate bundle with its own module instances, so singletons initialized by server/index.ts (a logger, in our case) are uninitialized in the route modules it evaluates.
Question
Which setup is supported for a server/index entry in dev?
server/indexmay importvirtual/react-router/server-build(asexamples/epic-stackdoes), and the embedded manifest should stay paired with the published browser manifest. That makes this a bug.server/indexmust take its build fromloadReactRouterServerBuild(devServer). In that case, is there a way for the paired build to share module instances with theserver/indexbundle, or should the docs say thatserver/indexmust not read its own embedded server build?
Activity
dajiaohuang commentedon Oct 22, 2026
The current documented custom-server development path is to use the plugin's paired build provider rather than load the server bundle directly. The Custom Server Setup section sets
build = () => loadReactRouterServerBuild(currentDevServer)and passes that provider tocreateRequestHandler. It says this returns the last-good server build paired with the selected web compilation, and explicitly warns that callingdevServer.environments.node.loadBundle()directly bypasses that guarantee.For module instances needed by React Router
createContext, the sharing section shows re-exporting the context fromapp/entry.server.tsxand reading it from(await build()).entry.module. That documents the context case; it does not claim that arbitrary logger singletons initialized only inserver/index.tsare shared automatically.Does using the documented build provider resolve the stale-manifest case in your setup? If it still reproduces, a minimal example using that provider would help clarify whether further integration guidance is needed.
dajiaohuang commentedon Oct 27, 2026
Correction to my earlier update: PR #163 does not use the paired build wholesale. It injects
loadReactRouterServerBuild(devServer)from the unbundled launcher, keeps the embedded build as the handler base, and overlays only the paired build'sassets. This preserves the route modules/module instances while updating the browser manifest, matching the asset-merge approach in your report. The README now documents this option and the module-graph boundary.This addresses the repository's Epic Stack example and guidance; plugin runtime behavior for arbitrary custom servers is unchanged. Please let me know whether this asset-merge path resolves your setup. If it does not, a minimal repro or result would help.