Repository navigation
Rsbuild audit: MCP Apps compiler path, Workbench, docsite builder #572
Description
Dedicated Rsbuild audit, deeper than the combined survey in #566. Findings already filed there are not restated: #566 §1 P3 (MCP Apps overrideBrowserslist), §5 (Workbench filenameHash/cache headers, .map files entry, browserslist) and its "Already compliant" list are out of scope here and referenced by section where a new finding builds on them.
Audited: main @ d88cc10 (fix: Node 26 transform flag; … (#554)), in a detached worktree, pnpm install --frozen-lockfile && pnpm build, Node v22.23.2, pnpm 11.23.0. Nothing was committed; the worktree was removed.
Versions (installed → latest): @rsbuild/core 2.2.1 → 2.2.3 (bump already implied by #566 §1 P1, not re-filed), @rsbuild/plugin-react 2.1.0 → 2.1.0, @rspack/core 2.2.1 (and 2.1.10 via @rslib/core 0.23.2, per #566) → 2.2.2, @rspress/core 2.0.21 → 2.0.21, rsbuild-plugin-rsc 0.1.1 → 0.1.1. @rsdoctor/rspack-plugin 1.6.3 was installed only as audit scratch in the worktree; main does not depend on it. Full table in §6.
What was actually built and measured (all scratch output under /tmp):
examples/mcp-appview through the realcomposeMcpAppsRsbuildConfig/compileMcpApps(same code path asagent-bundle build), plus Rsdoctor (brief JSON) and Rspack stats composition; the CLI itself against the example and against a deliberately broken copy.- Two throwaway fixtures through
compileMcpApps: a.tsentry importing a.tsxcomponent, and the same entry as.tsx; seventools.rsbuildhatch variants. - Workbench production build through
createWorkbenchConfig(3 cold runs, 3 warm runs withperformance.buildCache, 3rsbuild buildCLI runs), dev-server first compile (3 runs), Rsdoctor brief JSON on the production build. - Docsite: two full
rspress buildruns (cold, then warm with Rspress's defaultperformance.buildCachepopulated) through an untracked wrapper config that dumps the resolved Rsbuild and Rspack config.
Skills applied: rsbuild-best-practices, rsbuild-v2-upgrade, rspack-best-practices, rspack-split-chunks, rspack-debugging, rspack-tracing, rsdoctor-analysis; option semantics verified against the installed @rsbuild/core@2.2.1 typings (dist/types/config.d.ts) and source (dist/626.js), not memory.
1. MCP Apps compiler path (packages/agent-bundle/src/build/mcp-apps.ts)
This is what every plugin author's app ships through, so it is ordered first.
P1 · packages/agent-bundle/src/build/mcp-apps.ts:49,189 · rsbuild-best-practices: register @rsbuild/plugin-react for the build, not per file — the automatic JSX runtime is a project-level SWC setting (swcReactOptions.runtime) · usesReactSyntax decides from the entry extension alone. A .ts entry that imports a .tsx component (the natural layout: views/status.ts + views/StatusPanel.tsx) gets no React plugin, so Rsbuild's bare SWC transform lowers the component's JSX to the classic factory. Verified with a fixture through the real compileMcpApps: the emitted HTML contains return React.createElement("strong",{className:"w"},t) with a free React identifier, the build succeeds with no warning, and the app throws ReferenceError: React is not defined the moment the host renders it; the identical fixture with a .tsx entry compiles to (0,e.jsx)(...) via react/jsx-runtime. Same trap for .js → .jsx. rslib.ts:623-628 already documents this exact failure for Rslib surfaces and applies pluginReact({ fastRefresh: false }) unconditionally. · Apply pluginReact() to every app environment unconditionally (it is inert for JSX-free code: splitChunks is already false, fast refresh is dev-only), or keep the switch but decide on the module graph (stats.modules containing \.[jt]sx$) and fail with a diagnostic when JSX was compiled without the plugin. Add a unit test: .ts entry importing a .tsx module, assert !/[^.\w]React\.createElement/.test(html). · Risk: none; both fixture outputs are ~190 kB either way.
P1 · packages/agent-bundle/src/build/mcp-apps.ts:196 (logLevel: 'silent'), :281-298 · rspack-debugging: read stats.errors/stats.warnings and surface them with file and line; rsbuild-best-practices: never swallow compiler diagnostics · Rsbuild 2.2.1's build() (@rsbuild/core/dist/626.js, RSPACK_BUILD_ERROR) rejects with a bare Error('Rspack build failed.') when buildState.hasErrors; the per-file errors exist only in the logger output, which 'silent' mutes. Nothing on this path reads stats.errors or stats.warnings (collectBundledOutputEvidence requests neither; there is no ignoreWarnings, so warnings are not filtered — they are simply never looked at). Verified with the real CLI on a copy of examples/mcp-app: a syntax error in views/status-panel.ts, an unresolved import, and a tsconfig.json whose extends target is missing all print exactly [{"code":"AB5000","message":"Rspack build failed.","severity":"error"}] — no file, line, or message. With logLevel: 'error' the same build prints × Tsconfig not found /tmp/tsconfig.json / × [html-rspack-plugin]: Child compilation failed. Every Rslib surface uses logLevel: 'error' (cli-bins.ts:201, entries.ts:517, package-build.ts:327, rslib.ts:759), so MCP Apps is the only compile path in the product that is fully mute, and it is the one plugin authors iterate on most. · Keep 'silent' (one structured channel) but add a profile plugin: api.onAfterEnvironmentCompile(({ environment, stats }) => …) fires with stats even when stats.hasErrors(); collect stats.toJson({ all: false, errors: true, warnings: true, moduleTrace: true }), and throw a DiagnosticError with a dedicated code (a new AB47xx, "MCP App view failed to compile", registered in docs/diagnostics.md so the hosts/diagnostics pages regenerate) whose message lists app · file:line · message per error; emit warnings as severity: 'warning' diagnostics. · Risk: low — the Workbench and CLI already render diagnostics; the only behaviour change is a specific code instead of the AB5000 catch-all.
P2 · packages/agent-bundle/src/build/mcp-apps.ts:196 + default performance.printFileSize · rsdoctor-analysis: know the composition before shipping; rsbuild-best-practices: watch output size · Because logging is silent, Rsbuild's file-size table never prints and the CLI summary is only Built <name> to <dir>; the manifest carries sizes but nothing shows them. Measured: the 3.2 kB examples/mcp-app view compiles to a 437 kB HTML (104 kB gzip). Composition (Rspack stats, pre-minify): zod 1.42 MB across 190 modules (v3 types.js 128 kB and v4 core — @modelcontextprotocol/sdk@1.30's zod-compat pulls both), @modelcontextprotocol/sdk 263 kB (types.js 72 kB, shared/protocol.js 51 kB), zod-to-json-schema 98 kB, @modelcontextprotocol/ext-apps 65 kB, author code 3.2 kB. This floor is inherent to @modelcontextprotocol/ext-apps@1.7.5 (peer @modelcontextprotocol/sdk ^1.29, zod) — no Rsbuild knob removes it — but authors are never told, and a "why is my 2 kB view 440 kB" report is predictable. · Print per-app inlined size and gzip size in the build summary (the evidence pass already touches every emitted asset), and state the expected floor in website/docs/en/guide/authoring/mcp.mdx (+ zh) with the reason. · Risk: none.
P2 · packages/agent-bundle/src/build/mcp-apps.ts:190-193 · Rsbuild html config docs (html.title default 'Rsbuild App', html.mountId default 'root', built-in template in @rsbuild/core/dist/626.js) · When an app declares no template, the emitted document is Rsbuild's built-in shell. Verified output: <!DOCTYPE html><html><head><title>Rsbuild App</title><meta charset="utf-8"><meta name="viewport" …></head><body><div id="root"></div><script defer>…. Every template-less MCP App therefore ships into hosts titled "Rsbuild App", with no lang on <html>, and mounts at #root — a convention documented nowhere in website/docs/en/guide/authoring/mcp.mdx, so an author who renders into #app gets a blank view with a green build. · Set html.title to the app name (or meta.name), html.mountId explicitly, html.tags/template lang, and document the mount id and title on the authoring page (+ zh); alternatively ship a framework default template. · Risk: none.
P2 · packages/agent-bundle/src/build/mcp-apps.ts:216-219 (enforceInvariants alias agent-bundle/meta$) · Rsbuild resolve.aliasStrategy docs (default 'prefer-tsconfig': tsconfig.json paths win over resolve.alias) · The framework's identity module is delivered by an alias to a virtual module, but the profile leaves aliasStrategy at its default, so the author's tsconfig can shadow it. Verified: a fixture tsconfig.json with paths: { "agent-bundle/meta": ["./stub-meta.ts"] } builds green and the HTML contains the stub's string instead of the framework identity — the app reports a wrong name/version inside the host with no diagnostic. paths entries for agent-bundle/* are a plausible editor-resolution workaround for authors. · Pin resolve.aliasStrategy: 'prefer-alias' in enforceInvariants (and assert it in assertResolvedViewConfig), or fail validation when the author's paths cover agent-bundle/meta. The same check applies to the Rslib surfaces' framework aliases (rslib.ts), out of scope here. · Risk: none for compliant projects.
P3 · packages/agent-bundle/src/build/mcp-apps.ts:189 + @rsbuild/plugin-react/dist/index.js:75-128 · Rsbuild resolve.dedupe docs; rspack-split-chunks: with splitChunks: false the plugin's react cache group never applies · pluginReact() does not set resolve.dedupe, and the profile does not either, so a plugin author whose tree has a nested react/react-dom (monorepo, ext-apps peer skew) can ship two React copies inside one self-contained HTML — "Invalid hook call" at runtime. Rsdoctor on examples/mcp-app shows one copy, so this is latent, not observed. · When the React plugin is applied, set resolve.dedupe: ['react', 'react-dom', 'scheduler'] on that environment. · Risk: none.
P3 · packages/agent-bundle/src/build/mcp-apps.ts:190-193 · Rsbuild html.templateParameters docs (default exposes compilation, rspackConfig, htmlPlugin, assetPrefix to <%= %>) · Author templates are handed the full html-rspack-plugin parameter set; nothing documents which parameters are stable, and a template that interpolates compilation or rspackConfig can dump absolute paths into the shipped HTML. · Pass a minimal templateParameters allow-list (e.g. app name, meta) and document it. · Risk: rare; sharp when hit.
P3 · packages/agent-bundle/src/build/mcp-apps.ts:91-93 · rspack-debugging: an assertion that fails must name the artefact that failed it · assertSelfContainedViews throws "Rsbuild emitted files beyond the stable self-contained MCP App HTML output." without listing the files. Verified: tools.rsbuild = { output: { sourceMap: { js: 'source-map' } } } and tools.rsbuild = { mode: 'development' } both hit it (the extra file is the .map), while { js: 'inline-source-map' } passes and grows the HTML to 1.35 MB. · Include the unexpected relative paths in the message and a hint that only inline source maps stay self-contained. · Risk: none.
P3 · packages/agent-bundle/src/build/mcp-apps.ts:51-83 · rsbuild-best-practices: pin production invariants and verify them on the resolved config (inspectConfig) · assertResolvedViewConfig pins cleanDistPath, filenameHash, inlineScripts, inlineStyles, dataUriLimit, splitChunks, asyncChunks and output.path, but not mode, output.minify or performance.buildCache. The profile's mode: 'production' (#566 "compliant") is therefore a default, not an invariant: tools.rsbuild = { mode: 'development' } only fails today as a side effect of the .map file (previous finding); with an inline source map it would ship process.env.NODE_ENV === 'development' React and the react-refresh runtime into hosts, because pluginReact() is called bare (:189) while rslib.ts:628 and the framework-plugins.ts:16-18 comment say fastRefresh: false. performance.buildCache is likewise unpinned (Rslib pins it false at rslib.ts:618-622 for concurrent-build safety; several agent-bundle build processes can share one project's node_modules/.cache). · Assert mode === 'production' and performance.buildCache === false in the same loop, pass pluginReact({ fastRefresh: false }), and comment beside the assertion which of minify/sourceMap the hatch may change. · Risk: none.
2. MCP Apps dev loop in the Workbench
P1 · packages/agent-bundle/src/dev/artifacts/artifact-service.ts:80-85 → dev/coordinator.ts:491,578 → packages/workbench/src/overview-model.ts:100-103 · rspack-debugging: the first thing a failing build must give you is the compiler's own error; rsbuild-best-practices "Debugging" · This is the dev-loop face of §1 P1. When a view fails to compile under agent-bundle dev, the coordinator wraps the bare Error('Rspack build failed.') as AB7100 · "Unable to compile the build: Rspack build failed.", which is what the Overview Diagnostics table and the build.failed Dev Log entry show; the previous good epoch stays pinned (correct), but the author has no file, line or message anywhere in the Workbench. Rslib surfaces are no better inside the Workbench: their logLevel: 'error' output goes to the agent-bundle process stderr, which dev-log-producers.ts:154-163 does not capture (only MCP session mcp.stderr is). AB7100 is emitted at artifact-service.ts:81,144 but is not listed in docs/diagnostics.md, so the generated diagnostics reference page does not document the code authors actually see. · Once §1 P1 attaches the Rspack errors to a structured diagnostic, propagate it unchanged through artifact-service.ts:75-79 (it already forwards DiagnosticError.diagnostics) so the Overview shows app · file:line · message; add AB7100 to docs/diagnostics.md. · Risk: none.
P2 · packages/agent-bundle/src/build/mcp-apps.ts:196-207 + website/docs/en/guide/authoring/mcp.mdx, website/docs/en/guide/development/workbench.mdx:179-180 · rsbuild-best-practices "CLI": rsbuild (dev mode, source maps, readable output) for local development, rsbuild build for production · agent-bundle dev compiles MCP App views through the identical production profile (mode: 'production', minified, output.sourceMap: false, silent) — verified path coordinator.ts:540 → artifact-service.ts:208 → build.ts:402 → mcp-apps.ts:268. No Rsbuild dev server or createDevServer exists anywhere under packages/agent-bundle/src (only the Workbench's own in workbench-server.ts:546), which is a deliberate design (workbench.mdx:179-180), but it means the preview sandbox shows minified code with no source maps and the only escape hatch — tools.rsbuild = { output: { sourceMap: { js: 'inline-source-map' } } }, verified working, HTML 437 kB → 1.35 MB — is documented nowhere; mode: 'development' and external maps fail opaquely (§1 P3). · Either add a dev-only overlay in compileMcpApps (inline source maps + minify: false when mode === 'development' is passed from the coordinator — the self-contained assertion still holds for inline maps), or document the working hatch and DEBUG=rsbuild in mcp.mdx + zh and in the Workbench MCP Apps page. · Risk: inline maps triple the preview HTML (dev only); external .map files must stay forbidden for resource packaging.
P3 · packages/agent-bundle/src/dev/mcp-apps/mcp-app-preview-service.ts:322, dev/mcp-session/mcp-session.ts:138-146 · rsbuild-best-practices: dev loop = edit → rebuild → the view refreshes · After a successful rebuild artifact.available publishes a new epoch, but an open MCP session stays pinned to its epoch and the preview HTML was loaded once via bridge.loadResource(); nothing re-loads the resource into the sandbox iframe. The author must close and re-open Open App preview (or start a new session) to see the rebuilt view — the loop is edit → rebuild → notice nothing changed → re-open. The rebuild trigger itself is fine (ProjectWatcher watches the whole project root, watcher.ts:50-54, so imported components and templates invalidate, not only the entry). · Offer an opt-in "reload preview on new epoch" (the Workbench already receives artifact.available over SSE) or state the re-open step next to the preview button and in workbench.mdx. · Risk: an automatic swap must stay opt-in so epoch evidence guarantees hold.
3. Workbench (packages/workbench/rsbuild.config.ts)
P1 · packages/workbench/src/mcp/mcp-route-client.ts:592-595, packages/agent-bundle/src/dev/foreground-server.ts:746, 804-808, 821-827 vs packages/workbench/rsbuild.config.ts:40 and website/docs/en/guide/development/workbench.mdx:170-178 · rsbuild-best-practices "CLI: use rsbuild (dev server) for local development"; Rsbuild server.proxy docs (changeOrigin rewrites Host only, never Origin) · The documented contributor HMR loop — AGENT_BUNDLE_WORKBENCH_API_PROXY=http://127.0.0.1:3100 pnpm --filter agent-bundle-workbench dev, proxying /api — cannot complete a session. The browser sits on the Rsbuild origin (http://localhost:3000); /api/project/session answers origin: this.url (http://127.0.0.1:3100) and the client throws AB8003 "Foreground session bootstrap origin does not match this browser." because location.origin !== body.origin. Independently, every mutation POST carries Origin: http://localhost:3000, which #assertMutationSession rejects with AB8003 (origin !== this.url); changeOrigin: true (Rsbuild default, asserted in tests/rsbuild-workbench.test.ts:43) does not help since http-proxy rewrites Host, not Origin. Read-only GETs and the SSE stream work, so the loop looks alive until the first MCP page or mutation. Verified by reading both sides; not run. · Make the foreground server dev-proxy aware: accept an explicitly configured proxy origin (e.g. AGENT_BUNDLE_WORKBENCH_DEV_ORIGIN) in #assertMutationSession/#assertSessionBootstrapOrigin and echo it in the bootstrap origin, or serve the Rsbuild dev middleware from the foreground origin (rsbuild.createDevServer() + server.middlewareMode). Add a test that drives the proxied path end to end, and fix workbench.mdx (+ zh) once it works. · Risk: relaxing only the client-side check still leaves protected POSTs failing; the allowed origin must be an explicit opt-in, never derived from the request.
P2 · packages/workbench/tests/mcp-app-frame.test.ts:130-151 and 7 siblings (lifecycles-page.browser.test.tsx:47, mcp-json-input.test.ts:47, mcp-app-preview-browser.test.ts:75, mcp-page-app-browser.test.ts:149, evals-real.e2e.test.ts:91, comparisons-page-client-scope-browser.test.ts:66, runtime-consent-dialog.test.ts:58) · rsbuild-best-practices "Configuration: one rsbuild.config.ts/defineConfig, reuse it"; AGENTS.md "look for the helper before writing it" · Eight browser fixtures each re-own createRsbuild + bare pluginReact() + workbenchBrowserAliases instead of createWorkbenchConfig, and they already diverge: distPath.assets vs production static, no mode pin (rsbuild.config.ts:49 pins it), extensionAlias only in mcp-app-frame.test.ts:146. Four more (discovery-atoms-disposal.test.ts:185, runtime-document-atoms-disposal.test.ts:135, route-editor-atoms-disposal.test.ts:133, runtime-inspector.test.ts:85) call createWorkbenchConfig() and mutate it, inheriting the notices output.copy and — if AGENT_BUNDLE_WORKBENCH_API_PROXY happens to be set in the shell — the proxy. · One tests/support/workbench-fixture-config.ts (mode: 'production', pluginReact(), aliases, temp distPath/entry, optional extensionAlias) used by all twelve. · Risk: none beyond a mechanical refactor.
P2 · packages/workbench/rsbuild.config.ts:40 + website/docs/en/guide/development/workbench.mdx:176, website/docs/en/contributing/index.mdx · Rsbuild ServerConfig (config.d.ts:317-390: port 3000, host localhost, strictPort false, open false) · The HMR docs give the env + one command; nothing says which URL opens (Rsbuild picks 3000, or the next free port silently — strictPort false), that only /api is proxied, or that MCP App / runtime iframes come from separate loopback origins the proxy does not cover. contributing/index.mdx has no Workbench text at all. · Document the default URL and the proxy scope once P1 above works; consider server.strictPort: true for the contributor config so a port collision fails loudly instead of moving the UI. · Risk: none.
P3 · packages/agent-bundle/src/dev/workbench-assets.ts:42 + packages/workbench/rsbuild.config.ts:20-21 · Rsbuild output.copy / asset management · contentTypeFor falls back to application/octet-stream for extensionless paths, so the two files output.copy ships — THIRD_PARTY_NOTICES and src/mcp/APP-RENDERER-LICENSE — are served as binary downloads by #serveAsset (foreground-server.ts:953). Nothing in packages/workbench/src links them (they are the packaging contract in packages/agent-bundle/NOTICE:11 and tests/dev-workbench-packaging.test.ts:46), so this is cosmetic. · Map extensionless basenames to text/plain; charset=utf-8. · Risk: none.
4. Docsite builder (website/rspress.config.ts, no builderConfig)
P2 · .github/workflows/docs.yml:48-61 + Rspress initRsbuild.js:197-215 · Rsbuild performance.buildCache docs (persistent cache only pays off when the cache directory survives between builds) · Rspress enables performance.buildCache by default (node_modules/.cache, ≈89 MB after one build here), but the Docs job restores only the pnpm store, so every CI build pays cold compile plus the cache write. Measured locally: cold 196.6 s vs warm 188.8 s (−4 %) — the wall time is TypeDoc + SSG of 1906 pages, not Rspack, so restoring the cache via actions/cache would buy little and cost a ~90 MB artefact per key. · Set RSPRESS_PERSISTENT_CACHE=false in docs.yml (skips the write) unless a measured actions/cache restore shows a real gain; do not cache blindly. · Risk: none (the cache is dead weight in CI today).
No other builderConfig change is justified. With no builderConfig (website/rspress.config.ts:54-194), the resolved web environment is mode: 'production', target: web, assetPrefix: '/agent-bundle/' (from base), filenameHash: true, JS sourceMap: false (0 .map files in doc_build), legalComments: 'linked' (three .LICENSE.txt, ~2 kB), polyfill: 'off', dataUriLimit 4 KiB, printFileSize.compressed, security.sri off (same-origin Pages), no tools.rspack beyond Rspress's own, source.define only Rspress SSR/LLMS flags. Route chunks already get <link rel="preload"> from Rspress's RouteChunkAssetsPlugin; blanket performance.prefetch over ~1900 async chunks would hurt. OG/description meta come from Rspress SSG (rspress.config.ts:57-61), so theme-color etc. belong in Rspress head, not html.meta. website/plugins/*.ts use Rspress config hooks only, no builderConfig/modifyRsbuildConfig (mirror-api-locale.ts:87-114, generated-reference.ts:911-921).
5. Other @rsbuild/core / createRsbuild users
P2 · examples/rsc-agent-runtime/rsbuild.config.ts:203-206 + examples/rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts:563-568 · rspack-debugging: surface stats.errors text; Rsbuild onAfterDevCompile hook docs · Same shape as §1 P1 on the third compile path in the product: on stats.hasErrors() the observer fails the attempt with a fixed Error('RSC runtime compile reported errors.') and the session emits AB8206 "RSC runtime source build failed."; stats.toJson({ errors: true }) is never read, warnings never inspected (hasWarnings unused), logLevel left at 'info' so the real message lands only on the process console. The README advertises a Workbench compile-error capture for this example. · Attach the stats errors (file:line:message) to the AB8206 diagnostic and set logLevel: 'error'. · Risk: none.
P2 · examples/rsc-agent-runtime/rsbuild.config.ts:362-403 vs packages/agent-bundle/src/build/mcp-apps.ts:198-214 · rsbuild-best-practices asset/bundle guidance; rspack-split-chunks: web default preset still extracts lib-react under pluginReact · The app environment claims self-contained HTML with inlineScripts/inlineStyles + dynamicImportMode: 'eager', but unlike composeMcpAppsRsbuildConfig it sets neither splitChunks: false, dataUriLimit: Number.MAX_SAFE_INTEGER, asyncChunks: false nor an assertion like assertResolvedViewConfig; the default 4 KiB dataUriLimit means any asset >4 KiB becomes a sibling file, and legalComments: 'linked' already emits lib-react.js.LICENSE.txt beside the HTML (a host test expects it). · Reuse the mcp-apps invariant set (or its assertion) for app, and legalComments: 'inline' if siblings are unwanted. · Risk: none; today's HTML-only checks pass.
P3 · examples/rsc-agent-runtime/rsbuild.config.ts:288, 299, 368-369 · Rsbuild mode docs; @rsbuild/plugin-react fastRefresh default true · mode: 'production' is pinned even when options.mode === 'development' (rsbuild-runtime-session.ts:845), so the long-lived agent-bundle dev provider ships production React (no dev warnings) and Fast Refresh can never engage even though pluginReact() is called without { fastRefresh: false } (Rslib and the framework-plugins.ts:16-18 comment say refresh is off). · Either let mode follow options.mode for the web envs, or pass pluginReact({ fastRefresh: false }) and document that "dev" is production-compiled with onAppReload only. · Risk: none.
P3 · examples/rsc-agent-runtime/rsbuild.config.ts:290, 373-387 · Rsbuild dev.writeToDisk, output.cleanDistPath, output.filenameHash docs · Development forces dev.writeToDisk: true with cleanDistPath: false on all envs, but filenameHash: false is applied only in the production app branch — so a long dev session accumulates stale hashed app assets on disk and in staged checkpoints. · Apply filenameHash: false to development app too. · Risk: none.
P3 · examples/rsc-agent-runtime/rsbuild.config.ts:148-149 · Rsbuild output.cleanDistPath docs · emitRuntimeManifest does rm(dirname(environments.rsc.distPath)) — i.e. dist/ — wiping sibling widget/app output to compensate for cleanDistPath: false; apply: 'build' so it never races writeToDisk. · rm each environment's own distPath. · Risk: none.
P3 · examples/rsc-agent-runtime/rsbuild.config.ts:341-360, 362-403 · rsbuild-best-practices "keep browserslist aligned" (output.overrideBrowserslist) · Neither widget nor app pins targets — the same gap #566 §1 P3 files for mcp-apps.ts, on a second surface. · Pin the same list the framework adopts. · Risk: none.
P3 · packages/create-agent-bundle/templates/** · (informational) · Templates ship no Rsbuild config, plugins or scripts; mcp-server/tsconfig.json:15 sets jsx: react-jsx for route .tsx compiled by the package build. No MCP App views exist in templates, so nothing to change until one is added — at which point §1 P1 (.ts entry + .tsx component) is the trap to avoid.
6. Rsbuild 2.x currency
P2 · packages/rsc-runtime/package.json:94 · rsbuild-v2-upgrade step 4: bump every Rsbuild/Rspack package in lockstep (taze major --include /rsbuild/) · The exact devDependency pin "@rspack/core": "2.2.1" mirrors the ^2.2.0-0 peer at :78; nothing under packages/rsc-runtime/ imports @rspack/core. @rsbuild/core@2.2.3 (the #566 §1 P1 target) depends on @rspack/core ~2.2.2, so landing the bump without moving this line splits the tree into @rspack/core 2.2.1 + 2.2.2 (two native @rspack/binding-* downloads) and lets @rspack/plugin-react-refresh@2.0.2 peer-resolve against whichever copy pnpm picks. pnpm why @rspack/core already shows two versions today (2.1.10 via @rslib/core@0.23.2, 2.2.1 via @rsbuild/core@2.2.1 and this pin); #566 collapses the 2.1.x copy only if :94 moves too. · Bump :94 to 2.2.2 in the same PR, or drop the devDependency and rely on the peer. · Risk: none (no import).
P3 · packages/agent-bundle/src/core/types.ts:259 + packages/agent-bundle/src/config/validate.ts:1835-1882 · rsbuild-v2-upgrade "Migrate performance.chunkSplit → splitChunks" · The tools.rsbuild hatch is typed as @rsbuild/core@2.2.1's EnvironmentConfig, which still admits the two @deprecated keys performance.chunkSplit (config.d.ts:584) and output.sourceMap.extract.js (config.d.ts:816); validateTools (AB4720–AB4724) checks shape and plugin collisions only. Because both framework profiles set splitChunks: false (mcp-apps.ts:210, rslib.ts:632), a consumer chunkSplit hits Rsbuild's "Both performance.chunkSplit and splitChunks are set" branch (dist/626.js:9425) and is discarded — via logger.warn, below the 'silent'/'error' levels the framework builds with, so the author sees nothing. · Reject both keys in validateTools with an AB472x diagnostic pointing at the v2 replacement, and mention it on the tools-hatch docs page (en + zh). · Risk: none for compliant configs.
P3 · pnpm-workspace.yaml:24 · pnpm docs "minimumReleaseAge / minimumReleaseAgeExclude" · minimumReleaseAgeExclude: ['@rsbuild/core@2.2.1'] was added in #20 because 2.2.1 was <24 h old at the time; pnpm 11's built-in release-age window has long passed for 2.2.1, and both @rsbuild/core@2.2.3 and @rslib/core@1.0.0 (published 2026-09-03) are already outside it, so the entry neither helps nor blocks the #566 bump — it is dead config that will name a version no longer in the lockfile. · Delete the line in the bump PR, or set minimumReleaseAgeExcludePrune: true (pnpm ≥11.22; repo is on 11.23.0). · Risk: none.
No deprecated Rsbuild 2.x option is in use anywhere. The only @deprecated keys in the installed typings are performance.chunkSplit, output.sourceMap.extract.js and dev.setupMiddlewares; git grep across packages/agent-bundle/src, packages/workbench, examples, rstest.*.ts, website/rspress.config.ts and website/plugins finds none of them nor any v1 leftovers (source.alias, dev.startUrl, tools.htmlPlugin, output.enableLatestDecorators, source.transformImport, dev.progressBar, output.distPath.server, proxy context/onProxy*, onDevCompileDone, htmlWebpackPlugin template params) outside a correct migration comment at rslib.ts:629. Every hook and JS-API call in use is current (onAfterEnvironmentCompile, onBeforeDevCompile, onAfterDevCompile, startDevServer({ getPortSilently }), loadEnv, mergeRsbuildConfig). Peer ranges are satisfied by 2.2.1 and by 2.2.3: @rsbuild/plugin-react@2.1.0 (^2.0.0, latest), rsbuild-plugin-rsc@0.1.1 (^2.0.0), rsbuild-plugin-publint@1.0.0, @rstest/core@0.11.10 (~2.2.0; latest 0.11.12 wants ~2.2.2), @rspress/core@2.0.21 (^2.1.13). Not re-filed: the two-@rsbuild/core tree (2.1.13 via @rslib/core@0.23.2, 2.2.1 elsewhere) is #566 §1 P1.
| package | installed | latest | declared in |
|---|---|---|---|
@rsbuild/core |
2.2.1 (+ 2.1.13 via @rslib/core@0.23.2) |
2.2.3 | package.json:68, packages/agent-bundle/package.json:101, packages/workbench/package.json:30, examples/rsc-agent-runtime/package.json:28 |
@rsbuild/plugin-react |
2.1.0 | 2.1.0 | same four files |
@rspack/core |
2.2.1 (+ 2.1.10 via @rsbuild/core@2.1.13) |
2.2.2 | packages/rsc-runtime/package.json:94 (exact) |
@rslib/core |
0.23.2 | 1.0.0 (#566) | root, agent-bundle, create-agent-bundle, rsc-markdown-stream, rsc-runtime |
@rstest/core, adapter-rslib, browser, browser-react, playwright |
0.11.10 | 0.11.12 | root + packages + examples |
@rslint/core |
0.8.2 | 0.9.1 (<24 h old at audit time) | package.json:72, packages/agent-bundle/package.json:104 |
@rspress/core + plugin-{llms,sitemap,twoslash,typedoc} |
2.0.21 | 2.0.21 | website/package.json:18-22 |
rsbuild-plugin-rsc |
0.1.1 | 0.1.1 | examples/rsc-agent-runtime/package.json:36 |
rsbuild-plugin-publint |
1.0.0 | 1.0.0 | package.json:84 |
@rsdoctor/rspack-plugin |
not a dependency of main — installed only as audit scratch in the worktree |
1.6.3 | — |
Measurements
MCP Apps — examples/mcp-app view (views/status-panel.ts + template)
| Metric | Value |
|---|---|
Compile (programmatic compileMcpApps, 1 view, warm process) |
≈1.1 s; agent-bundle build for the whole example 6.0 s wall |
| Emitted | 1 file, mcp-apps/status.html, 437.3 kB, 104.4 kB gzip |
| Author code in it | 3.2 kB (views/status-panel.ts) + 0.3 kB generated agent-bundle/meta |
| Pre-minify composition (Rspack stats, incl. concatenated modules) | zod 1423 kB / 190 modules (v3 + v4), @modelcontextprotocol/sdk 263 kB / 10, zod-to-json-schema 98 kB / 63, @modelcontextprotocol/ext-apps 65 kB / 2 |
| Largest modules | zod/v3/types.js 128 kB, zod/v4/core/schemas.js 96 kB, zod/v4/core/compile.js 85 kB, @modelcontextprotocol/sdk/dist/esm/types.js 72 kB, …/shared/protocol.js 51 kB, ext-apps/dist/src/app.js 33 kB |
| Duplicate packages (Rsdoctor E1001) | none; 5 packages, 163 modules, 1 chunk |
| Warnings / errors | 0 / 0 (nothing reads them — see §1 P1) |
Fixture .ts entry → .tsx component |
190.0 kB HTML, classic React.createElement with free React (broken at runtime) |
Fixture .tsx entry → .tsx component |
190.6 kB HTML, automatic runtime (react/jsx-runtime) |
tools.rsbuild hatch probes |
inline-source-map OK (1.35 MB), source-map FAIL (opaque), mode: 'development' FAIL (opaque), minify: false OK (575 kB), removeConsole OK, overrideBrowserslist OK, source.define OK |
Workbench — production build, Rsdoctor (rsdoctor-data.json, brief mode)
| Metric | Value |
|---|---|
| Assets | 31 (25 JS, 1 CSS, index.html, 2 .LICENSE.txt, THIRD_PARTY_NOTICES, src/mcp/APP-RENDERER-LICENSE) |
| JS total | 2790 kB (605 kB gzip) across 25 files |
| CSS total | 76.9 kB (13.2 kB gzip), one static/css/index.css |
| Initial JS (all three loaded on every start) | 1367 kB / 376 kB gzip: static/js/463.js 634 kB (176 kB gz, default vendors group), static/js/index.js 549 kB (142 kB gz, app code), static/js/lib-react.js 185 kB (58 kB gz) |
| Async chunks | 22, largest async/743.js 237 kB (markdown stack), four Shiki grammars at 171–177 kB each (typescript, jsx, tsx, javascript), async/763.js 138 kB |
| Packages / modules / chunks | 108 / 837 / 25 |
| Duplicate package versions | none (Rsdoctor E1001 empty; one react, one react-dom, one effect, one zod) |
| Largest packages (parsed) | @shikijs/langs 1005 kB (async), Workbench source 542 kB, zod 332 kB (134 kB in the initial vendors chunk), react-dom 174 kB, @modelcontextprotocol/client@2.0.0 157 kB, effect 119 kB + 62 kB, @modelcontextprotocol/sdk@1.30.0 31 kB, @modelcontextprotocol/ext-apps 28 kB |
Initial vendors chunk 463.js composition |
@modelcontextprotocol/client 157 kB + 40 kB, zod 134 kB, effect 119 kB + 62 kB, @modelcontextprotocol/sdk 31 kB, ext-apps 28 kB, micromark/hast/property-information ≈ 70 kB |
| Largest modules | @shikijs/langs/dist/typescript.mjs 177 kB, jsx.mjs 174 kB, tsx.mjs 171 kB, javascript.mjs 171 kB, react-dom-client.production.js 170 kB, @modelcontextprotocol/client/dist/src-*.mjs 94 kB, src/mcp/mcp-page.tsx 45 kB, effect/dist/internal/effect.js 43 kB |
| Rsdoctor build costs | bootstrap→beforeCompile 0.73 s, compile 3.28 s, minify 1.05 s, afterCompile→done 0.53 s (with the Rsdoctor plugin attached: 4.8–5.8 s total) |
Workbench — timings (3 runs each, medians; same machine, shared with other agents)
| Scenario | Runs | Median |
|---|---|---|
Production build, fresh process, no persistent cache (what pnpm build does) |
1284 / 1386 / 1387 ms | 1386 ms compile (rsbuild build CLI wall incl. Node + pnpm start-up: 2.24 / 2.26 / 2.72 s → 2.26 s) |
Production build with performance.buildCache populated (not enabled in the repo; measured to size the option) |
514 / 518 / 545 ms | 518 ms (−63 %); populate run 1497 ms |
Dev server start → first compile done (startDevServer, onDevCompileDone) |
629 / 611 / 706 ms | 629 ms |
Docsite — rspress build (Rspress 2.0.21 on Rsbuild 2.2.1; TypeDoc + twoslash + SSG of 1906 pages)
| Metric | Value |
|---|---|
| Cold build | 196.6 s wall, 8.36 GB max RSS |
Warm build (Rspress's default performance.buildCache populated) |
188.8 s wall, 8.35 GB — the persistent cache buys ≈4 %, the time is TypeDoc + SSG, not Rspack |
doc_build |
623 MB; HTML 602 MB over 1907 pages (avg 316 kB, max 386 kB — each page inlines the SSG-rendered sidebar with 921 API items; content-level, #566 §4 territory); static/js 14.75 MB / 1916 files; CSS 80 kB |
Entry chunk static/js/index.<hash>.js |
2.23 MB (373 kB gzip), dominated by route/sidebar metadata for ~1900 API pages; lib-react 189 kB, lib-router 35 kB; largest async route chunk 286 kB |
| Resolved Rsbuild (web env) | mode: production, target: ['web','es2017'], assetPrefix: '/agent-bundle/', filenameHash: true, sourceMap: false (JS), legalComments: 'linked', polyfill: 'off', minify on, dataUriLimit 4096, html.inject: 'head', scriptLoading: 'defer', performance.buildCache on (Rspress default, buildDependencies = Rspress initRsbuild.js + the config), printFileSize.compressed, Lightning CSS targets chrome ≥107, edge ≥107, firefox ≥104, safari ≥16, chunk split chunks: 'all' with lib-react/lib-router/styles cache groups |
Already compliant (do not re-audit)
In addition to #566's list:
- MCP Apps —
composeToolsLayersorder profile →tools.rsbuild→tools.rspack→ invariants (compose-layers.ts:32-36);enforceInvariantsruns last and pinsasyncChunks: false, the meta alias andVirtualModulesPlugin(mcp-apps.ts:212-238); one Rsbuild environment per app (:188-195);server.publicDir: false(:209);filenameHash: false+distPath.html: 'mcp-apps'→ stable[name].html, names validated kebab-case (validate.ts:513) and duplicate names must share identity (mcp-apps.ts:136-145);createRsbuild({ cwd: projectRoot })so a config in a subdirectory still resolves from the project root (mcp-apps.ts:268,build.ts:402,normalize.ts:852-854);result.close()on success (:296-298) and Rsbuild closes the compiler itself before rejecting; defaultsoutput.charsetutf8,polyfill: 'off',injectStyles: false,output.module: false;source.includedefault transpiles TS/JSX in dependencies but not plain JS;source.tsconfigPathauto-set only when<root>/tsconfig.jsonexists; provenance ignores the virtual module (:289-291,meta.ts:27-31); Rsdoctor: zero duplicate packages in the example app;security.nonce/sricorrectly unset — the sandbox CSP allows'unsafe-inline'(mcp-app-sandbox.ts:22-32) and there are no externalsrcs to hash; thetools.rsbuildhatch honoursremoveConsole,overrideBrowserslistandsource.define(verified). - MCP Apps dev loop — no Rsbuild dev server is used for app preview, by design (
workbench.mdx:179-180);ProjectWatcherwatches the project root so entry, imported modules and templates all invalidate (watcher.ts:50-54,coordinator.ts:331-335); a failed rebuild keeps the last good epoch (coordinator.ts:578);collectBundledOutputEvidencereplaces plannedsourceInputswith the real module graph after each compile (mcp-apps.ts:283-304,provenance.ts:181-248). - Workbench —
defineConfig+ exportedcreateWorkbenchConfig, proxy only when the env is set (rsbuild.config.ts:13, 40, 54;scripts/dev.mjs:3);changeOrigin: trueis the Rsbuild 2.2.1 default and asserted intests/rsbuild-workbench.test.ts:43; no path rewrite orws: trueneeded — every typed client route is under/api/…, transport is fetch +EventSource, no WebSocket; Rsbuild's compress middleware skipstext/event-stream, so SSE survives the proxy; non-/apitraffic is intentional (hash routingmain.tsx:342,blob:downloads, absolute sandbox iframe origins,data:media); hash routing only,#serveAssetmaps/→index.html, no SPA fallback needed,HEADsupported (foreground-server.ts:216, 947);#serveAssetsets onlycontent-type, no CSP conflict withfavicon="data:,"/deferscripts;index.htmlownslang, charset, viewport, title, favicon and the app never writesdocument.title;pluginReact()defaults (fast refresh in dev only, no React Compiler) are right for a SPA; 11 plain.cssfiles, zero.module.css, no PostCSS/Tailwind, nothing relying on Lightning CSS-specific transforms; Rsdoctor: zero duplicate packages (onereact,react-dom,effect,zod), 25 chunks;output.copytargets match the packaging contract (NOTICE:11,dev-workbench-packaging.test.ts:46-47); zeroconsole.*inpackages/workbench/src, soperformance.removeConsolehas nothing to do. - Docsite — see §4: every resolved Rsbuild default matches the skills for a same-origin GitHub Pages site;
base→assetPrefix/publicPath/agent-bundle/; no.mappublished;legalComments: 'linked'kept for licence compliance; route preloads come from Rspress, notperformance.preload. - rsc-agent-runtime — per-env
tools.rspack.namefor MultiStats matching (rsbuild.config.ts:318-320, 359, 399-400);splitChunks: falseon the node env matches Rsbuild 2.2's server default (multi-entrychunks: 'all', same rationale asrslib.ts:629-632); widget entries all onclient-anchor.tsper thersbuild-plugin-rscenvironments contract; apphmr: false/liveReload: falsewith a custom reload plugin;emitRuntimeManifestisapply: 'build'so it never raceswriteToDisk; documented hooks only;server.port: 0/host: '127.0.0.1'/printUrls: false+getPortSilently+server.close(); all four integration tests reusecreateRscRuntimeRsbuildConfig;create-agent-bundletemplates ship no Rsbuild config or deps;services/mcp-run.ts:199-203usesloadEnvonly. - Currency — no deprecated 2.x option in use; all peers satisfied by 2.2.1 and by 2.2.3;
@rsbuild/plugin-react,@rspress/*,rsbuild-plugin-rsc,rsbuild-plugin-publintat latest.
Not assessed / caveats
- Timings were taken on a shared machine with other agents running; medians of three are reported, single runs are noted. The Workbench "warm" number measures
performance.buildCache, which the repo does not enable — it sizes the option, it does not describe today'spnpm build. - The contributor-HMR origin finding (§3 P1) was established by reading both sides (client check at
mcp-route-client.ts:592-595, server checks atforeground-server.ts:804-827, browsers always sendOriginon POST, http-proxychangeOriginrewritesHostonly); it was not reproduced live. - The
.ts→.tsxReact finding (§1 P1) and thealiasStrategyfinding (§1 P2) were reproduced with throwaway fixtures through the realcompileMcpApps; the error-surfacing finding was reproduced with the real CLI against a copy ofexamples/mcp-app. - Docsite memory: the Docs job sets no
NODE_OPTIONS; peak RSS 8.36 GB fits publicubuntu-latest(16 GB) with headroom, but the same build would OOM on a private-repo 8 GB runner. No Rsbuild knob applies; it is SSG/runner sizing. - Not assessed: Rslib packaging surfaces (
rslib.ts,entries.ts,cli-bins.ts,package-build.ts) beyond theirlogLevelfor comparison — Rstack skills audit: build compiler, package builds, tests, docsite #566 §1/§2; Rspress content (sidebar size, page count, TypeDoc) — Rstack skills audit: build compiler, package builds, tests, docsite #566 §4; the Rstest runtime-playground configs — Rstack skills audit: build compiler, package builds, tests, docsite #566 §3 deletes them; whetherperformance.printFileSizestill walks assets underlogLevel: 'silent'(CPU cost not measured); whether thealiasStrategyshadowing also affects the Rslib surfaces' framework aliases. - The audit worktree carried uncommitted scratch (
@rsdoctor/rspack-pluginadded to the rootpackage.json/lockfile for Rsdoctor runs; an untrackedwebsite/rspress.audit.config.ts);mainhas neither, and nothing was committed or pushed. The worktree has been removed.
Activity
fix(dev): complete the contributor HMR loop through an explicit loopb…
fix(mcp-apps): React plugin on every view, AB4770–AB4772 compile diag…
ScriptedAlchemy commentedon Sep 5, 2026
Owner reconciliation against current
mainafter #636 (00467a486fe91f411d37eed0b9d0d1bc40aee55f).Resolved, item by item:
97a5bfa7e). Every MCP App environment getspluginReact({ fastRefresh: false }), including.tsentries that import.tsx.97a5bfa7e). Rspack errors/warnings are collected and surfaced as AB4770/AB4771; AB4772 reports size, and development output is readable.agent-bundle/metaprecedence, unexpected-output detail, and production-mode invariants: fixed in97a5bfa7e.fb56fa06a).website/package.jsonruns the site build withRSPRESS_PERSISTENT_CACHE=false.97a5bfa7e; the current example pinssplitChunks: false,asyncChunks: false, full data-URI inlining, inline legal comments, and stable filenames.7e447b5d12180bc9f169b45f144f0c727648612d).00467a486fe91f411d37eed0b9d0d1bc40aee55f) with ordinary and synthetic packed-process proofs.Verified remaining packaging/config work only:
resolve.dedupefor React/React DOM/scheduler and do not narrowhtml.templateParameters.application/octet-stream.pluginReact(), and does not set an explicit browserslist.validateToolsstill does not reject deprecated Rsbuild 2 hatch keys such asperformance.chunkSplit/output.sourceMap.extract.js.The audit stays open because those concrete config items remain. Preview reload UX and other product/test suggestions are intentionally not relisted as packaging/config defects.
ScriptedAlchemy commentedon Sep 6, 2026
Residual item 4 note: development React Flight mode is intentionally not landing in #648 because the Playwright trace proved its cross-process payload cannot be decoded by the production client; #648 keeps development entries/HMR topology with a production-compatible Flight wire instead.
ScriptedAlchemy commentedon Sep 6, 2026
Closed audit map (all reconciled residuals are landed or explicitly dispositioned):
00467a486fe91f411d37eed0b9d0d1bc40aee55f.a5ceb957ee8e41a5d23a2f164a37917c93255b51.96c44ce7523c876be6586157be173f1606a6aa57.0a440f7cfc174c2260426a6bf82d302e2ceeaf65.96c44ce7523c876be6586157be173f1606a6aa57. Development entries/HMR topology remain, but React Flight stays production-compatible because the Playwright trace proved a development producer cannot be decoded by the production client; this disposition was recorded separately above.96c44ce7523c876be6586157be173f1606a6aa57.