Skip to content

perf: parallel dev compilers, worker-pool route transforms, and startup/HMR optimizations#54

Merged
ScriptedAlchemy merged 103 commits into
mainfrom
codex/parallel-dev-compilers
Jul 2, 2026
Merged

perf: parallel dev compilers, worker-pool route transforms, and startup/HMR optimizations#54
ScriptedAlchemy merged 103 commits into
mainfrom
codex/parallel-dev-compilers

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Goal

Consolidate the perf stack into one mergeable PR against main so we can evaluate the combined dev-startup, route-transform, and lazy-compilation optimizations together.

Folded PRs

This PR now includes the perf work from:

#67 React Router 8 compatibility stays separate because it is compatibility/API surface rather than core perf work.

What changed

  • Parallel dev compilers: allows Rsbuild web and node development compilers to run in parallel when there are enough spare cores (6+), while keeping production compiler ordering explicit. Dev runtime publication stays coherent across parallel compiler attempts, and node compilation retries when a mixed generation is detected.
  • Worker-pool route transforms: keeps the public option as parallelRouteTransform and auto-enables worker transforms for larger route graphs (256+ routes). Worker slots are created lazily, default worker counts are capped at 1-2, and machines with 4 or fewer cores run dev transforms inline instead of spawning workers (production builds keep workers at every machine size — at 1024 routes the ~6k build transform tasks make two workers decisively faster than inline, see the synthetic-1024 note below). Route-source worker affinity is LRU-bounded so long dev sessions cannot grow it unboundedly.
  • Eager warmup: warms the dev SSR request handler's imports (react-router, node-fetch-server) when the middleware is created, and spawns transform workers during plugin setup so thread startup overlaps Rsbuild's own compiler creation.
  • Typegen scheduling and spawn cost: the react-router typegen dev watch starts only after a 10s compile-idle period (rescheduling on every compile until it starts), keeping the fast-ready win while moving the typegen bootstrap burst out of the HMR window. Both typegen invocations (dev watch and the onBeforeBuild one-shot) spawn the @react-router/dev bin directly via process.execPath instead of npx --yes (npx kept as fallback), cutting ~250ms+ of bootstrap from every production build.
  • Lazy compilation: enables guarded React Router lazy compilation by default and adds an optional prewarm (unstableLazyCompilationPrewarm); prewarm reschedules are deduped by fingerprinting the prewarm-relevant asset set.
  • Effect adoption with import hygiene: adopts Effect for plugin lifecycle orchestration (config resolution, manifest generation, server build evaluation, prerender scheduling, delayed background tasks) using subpath imports (effect/Effect etc.) instead of the barrel; keeps the route-transform worker graph free of effect imports entirely; and bundles effect into dist (moved to devDependencies), so consumers no longer install it. Four Effect-typed exports were removed from the public API, and the published d.ts is verified clean for strict skipLibCheck: false consumers (including a @react-router/dev/routes structural-type fix).
  • Benchmarking: adds a --cpu-limit flag to the benchmark harness for core-constrained benchmarking, plus synthetic profiling controls.
  • Cleanup: removes dead Effect wrappers and unused helpers left behind by the adoption (-110 lines of dead code), converts benchmark/package interop infrastructure to typed .mts scripts using the shared Effect script runtime, consolidates the typegen bin lookup onto the ssr-externals package resolver, moves dev server-build resolution onto a direct Effect path (no Effect→Promise→Effect round-trip; public d.ts graph re-verified effect-free), aligns test files with the effect subpath-import convention, and carries forward the current main release/docs/Node 24 workflow updates.

Measured results

Negative deltas mean the head of this branch is faster.

Metric Base → head Where measured
Plugin import (ESM) ~490ms → ~181ms (barrel → subpath → bundled effect); base ~163ms, +19ms accepted fixed cost local, this machine
Plugin import (CJS) ~259ms → ~177ms local, this machine
Route-transform worker spawn ~360ms → ~52ms (effect-free worker graph) local, this machine
Dev server ready faster at every core count: −19.5% at 4 cores, −13% unlimited local core-count sweep (base/head interleaved)
HMR update (4 cores, large-355, median) ~2.55s → 2.21s after c93289d — matches typegen-disabled reference; base ≈2.2s, regression closed local, interleaved
HMR update (unlimited cores) −19.3% local core-count sweep
Production build (4 cores, synthetic-256) −21% vs base (1.98s vs 2.51s median) after eec7564 local, interleaved
Production build (4 cores, synthetic-1024) −4.6% vs base (4.03s vs 4.23s pooled median) after eec7564; was +5-6% before the fix local, interleaved (4-5 rounds × 6 iterations per side)
Production build (unlimited cores) −10.8% local core-count sweep
Dev server ready −5.8% overall (−4.2% large app, −7.3% standard fixtures) benchmark CI at 575a69d (4-core runner)
Route load −1.9% standard fixtures, +4.9% large app (≈+1.2% overall) benchmark CI at 575a69d
HMR update +2.9% rollup (+1.8% large app, +4.6% standard) — within run-to-run noise benchmark CI at 575a69d
Production build large-355: −4.2%; synthetic-48/256: −4.7% to −18.1%; synthetic-1024: +0.8% to +7.0% benchmark CI at 575a69d

See the "Performance investigation summary" comment below for the full core-count sweep and the effect import-hygiene progression.

Performance characteristics by machine size

From a base-vs-head interleaved sweep at 4/8/16/48/unlimited cores (measured before c93289d, which further improved small-machine HMR and builds — see below):

  • Small machines (≤4 cores): dev ready is where head wins most (−19.5% at 4 cores). Dev transforms run inline (no worker spawn cost); production builds keep workers (eec7564). The sweep showed HMR and build favoring base slightly at this size, but c93289d and eec7564 closed both gaps: HMR now matches base (~2.2s) and 4-core builds are ahead on synthetic-256 (−21%) and synthetic-1024 (−4.6%).
  • Mid-size (8-16 cores): HMR crossed over to head around 8 cores in the sweep; build crossed over between 16 and 48 cores (both pre-c93289d, so these are conservative).
  • Large machines (48+ / unlimited): head is faster on everything — ready −13%, HMR −19.3%, build −10.8% at unlimited. Base spawns cpus − 2 transform workers (98 on a 100-core box), which head caps at 2; that uncapped spawn is why base degrades at high core counts.

Known tradeoffs

  • HMR startup collision — resolved. Earlier heads deferred the react-router typegen watch ~3s past first compile, and its bootstrap landed inside the benchmark's early HMR window (the +20% HMR rollup CI reported at a463dca; per-op plugin timings were ~2ms on both sides). c93289d starts the typegen watch only after a 10s compile-idle period: locally at 4 cores, large-355 HMR went from ~2.55s to 2.21s, matching the typegen-disabled reference and base (≈2.2s). CI at the current head (575a69d) confirms it: HMR rollup is +2.9% with large-355 at +1.8% (1.46s → 1.48s) — within noise. The fast-ready win is retained.
  • Production build — regression closed. CI showed +3.6-6.8% across the board at a463dca; after effect bundling and the direct-bin typegen spawn, CI at 575a69d showed large-355 at −4.2% and synthetic-48/256 at −4.7% to −18.1%, with synthetic-1024 unsplit the one remaining positive reading (+7.0%).
  • Synthetic-1024 build +7.0% — confirmed real and fixed. A local 4-core interleaved A/B at 575a69d (four rounds per side, 6 iterations each) reproduced it: pooled medians 4.46s head vs 4.23s base (+5.4%). Root cause: the small-machine inline fallback (8fafc8f) was tuned on the dev path, where a lone worker never amortizes its startup/IPC cost, but it also disabled workers for production builds — and at 1024 routes a build pushes ~6k transform tasks through the loader, where two workers clearly win (forced 2-worker A/B: 4.08-4.11s). eec7564 restricts the inline fallback to dev; builds keep the default worker count. Re-measured after the fix: 4.03s pooled median, −4.6% vs base, with synthetic-256 builds also ahead (1.98s vs 2.51s).
  • ~19ms fixed import overhead vs base (~181ms vs ~163ms ESM) from the bundled effect runtime — accepted.
  • Embedded complex-app benchmark is low-confidence (2 runs per side, no warmup, large run-to-run spread): at 575a69d it reads build +1.9%/+2.7% and dev +0.9% — effectively noise, and much improved from the +7.6-16.8% readings one commit earlier.

Validation

  • npx tsc --noEmit
  • npx rstest run - 440/440 tests passing
  • pnpm build
  • pnpm typecheck:scripts
  • Package interop and dist sanity verified (ESM + CJS consumers; public d.ts clean under skipLibCheck: false)
  • Multi-model adversarial review applied, plus deslop and dead-code removal passes
  • prettier --check on touched files, git diff --check

Benchmarks

Fresh benchmark CI runs on each push to this PR. The last full CI round compared 575a69d against base 7a55f78 (4-core runner): dev total −4.3% overall, dev ready −5.8%, HMR rollup +2.9% (noise; large-355 +1.8%), production builds −4.2% on large-355 and −4.7% to −18.1% on synthetic-48/256. The one positive reading from that round — synthetic-1024 unsplit build at +7.0% — was confirmed real in a local interleaved A/B and fixed by eec7564 (build-mode transforms keep workers on small machines; locally now −4.6% vs base). The "Performance investigation summary" comment covers the local core-count sweep showing head winning across the board at 8+ cores.

@ScriptedAlchemy ScriptedAlchemy changed the base branch from perf/bundling-performance to main June 26, 2026 23:50
@ScriptedAlchemy ScriptedAlchemy marked this pull request as ready for review June 26, 2026 23:51
@pkg-pr-new

pkg-pr-new Bot commented Jun 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/rsbuild-plugin-react-router@d34af4e

commit: d34af4e

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

Compared PR head d34af4e against base 7a55f78.

Dev Rollup

Group Fixtures Base total Head total Delta Base ready Head ready Ready delta Base routes Head routes Route delta Base update/HMR Head update/HMR Update delta Speedup
All dev fixtures 7 32.07s 29.27s -8.7% 22.31s 19.55s -12.4% 4.22s 4.05s -4.0% 3.04s 3.26s +7.1% 1.10x
Large app 1 15.00s 13.83s -7.8% 9.85s 8.42s -14.5% 1.99s 2.01s +0.8% 1.62s 1.80s +10.9% 1.08x
Standard fixtures 6 17.06s 15.44s -9.5% 12.47s 11.13s -10.7% 2.23s 2.04s -8.3% 1.42s 1.46s +2.8% 1.11x

Production Build Benchmarks

Rendered 7 production build benchmarks.

Benchmark Runs Base total Head total Delta Head mean Head p95 Speedup Head RSS p95
large-355-ssr-esm 5 8.97s 8.79s -2.0% 8.84s 9.10s 1.02x 1524 MB
synthetic-1024-ssr-esm 5 4.36s 4.14s -4.9% 4.18s 4.33s 1.05x 635 MB
synthetic-1024-ssr-esm-split 5 5.76s 5.55s -3.6% 5.62s 5.92s 1.04x 804 MB
synthetic-256-sourcemaps 10 2.39s 2.13s -10.8% 2.15s 2.32s 1.12x 433 MB
synthetic-256-ssr-esm 10 2.25s 2.01s -10.7% 2.02s 2.15s 1.12x 398 MB
synthetic-256-ssr-esm-split 10 2.63s 2.46s -6.6% 2.45s 2.61s 1.07x 442 MB
synthetic-48-ssr-esm 10 1.60s 1.34s -16.3% 1.36s 1.59s 1.19x 313 MB

full Dev Fixture Summary

Rendered 7 dev benchmark fixtures from the full profile.

Benchmark Runs Base total Head total Delta Base ready Head ready Base routes Head routes Base update/HMR Head update/HMR Update delta Head mean Head p95 Speedup Head RSS p95
large-355-ssr-esm 5 15.00s 13.83s -7.8% 9.85s 8.42s 1.99s 2.01s 1.62s 1.80s +10.9% 14.08s 15.01s 1.08x -
synthetic-1024-ssr-esm 5 4.84s 4.61s -4.7% 3.59s 3.29s 0.54s 0.60s 0.48s 0.50s +5.0% 4.60s 4.63s 1.05x -
synthetic-1024-ssr-esm-split 5 4.76s 4.53s -4.7% 3.52s 3.25s 0.54s 0.58s 0.45s 0.50s +10.7% 4.51s 4.56s 1.05x -
synthetic-256-sourcemaps 10 2.31s 1.96s -15.1% 1.73s 1.47s 0.29s 0.23s 0.13s 0.15s +15.5% 1.96s 1.99s 1.18x -
synthetic-256-ssr-esm 10 2.05s 1.72s -15.9% 1.44s 1.24s 0.35s 0.25s 0.13s 0.13s -1.3% 1.72s 1.77s 1.19x -
synthetic-256-ssr-esm-split 10 2.04s 1.73s -15.0% 1.40s 1.24s 0.35s 0.25s 0.15s 0.13s -17.3% 1.73s 1.85s 1.18x -
synthetic-48-ssr-esm 10 1.07s 0.88s -18.0% 0.78s 0.62s 0.15s 0.13s 0.08s 0.05s -33.3% 0.88s 0.91s 1.22x -

large-355-ssr-esm Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 1785 479700.2ms 1767.0ms -99.6% 1767.0ms 26.6ms 10
web route:client-entry 1785 147365.0ms 368.0ms -99.8% 368.0ms 5.6ms 10
node route:module 1785 136436.5ms 884.1ms -99.4% 884.1ms 6.0ms 10
node manifest:transform 5 235.9ms 147.3ms -37.6% 147.3ms 57.6ms 5
web manifest:stage 10 16.1ms 14.5ms -9.9% 14.5ms 2.1ms 10
web manifest:transform 5 0.6ms 0.5ms -16.7% 0.5ms 0.1ms 5

synthetic-1024-ssr-esm Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 5130 1283358.2ms 2055.8ms -99.8% 2055.8ms 23.2ms 10
web route:client-entry 5130 944556.5ms 646.1ms -99.9% 646.1ms 7.2ms 10
node route:module 5130 641014.6ms 900.9ms -99.9% 900.9ms 5.5ms 10
node module:client-only-stub 5 357.8ms 95.3ms -73.4% 95.3ms 37.4ms 5
node manifest:transform 5 275.6ms 213.5ms -22.5% 213.5ms 51.8ms 5
web manifest:stage 10 57.4ms 55.7ms -3.0% 55.7ms 7.1ms 10
web manifest:transform 5 0.5ms 0.5ms 0.0% 0.5ms 0.1ms 5

synthetic-1024-ssr-esm-split Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 5130 991647.4ms 1985.4ms -99.8% 1985.4ms 16.2ms 10
web route:client-entry 5130 887287.7ms 621.9ms -99.9% 621.9ms 18.6ms 10
node route:module 5130 843108.7ms 981.5ms -99.9% 981.5ms 11.0ms 10
node module:client-only-stub 5 691.3ms 204.2ms -70.5% 204.2ms 116.2ms 5
node manifest:transform 5 270.9ms 204.6ms -24.5% 204.6ms 43.4ms 5
web manifest:stage 10 64.6ms 54.1ms -16.3% 54.1ms 8.5ms 10
web manifest:transform 5 0.5ms 0.5ms 0.0% 0.5ms 0.1ms 5

synthetic-256-sourcemaps Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 2580 287443.6ms 1391.0ms -99.5% 1391.0ms 18.9ms 20
node route:module 2580 163636.2ms 626.6ms -99.6% 626.6ms 4.4ms 20
web route:client-entry 2580 112222.5ms 390.8ms -99.7% 390.8ms 4.9ms 20
node module:client-only-stub 10 414.7ms 295.4ms -28.8% 295.4ms 94.5ms 10
node manifest:transform 10 296.5ms 164.3ms -44.6% 164.3ms 24.5ms 10
web manifest:stage 20 29.3ms 21.0ms -28.3% 21.0ms 1.5ms 20
web manifest:transform 10 1.2ms 1.0ms -16.7% 1.0ms 0.1ms 10

synthetic-256-ssr-esm Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 2580 239046.1ms 1358.2ms -99.4% 1358.2ms 12.7ms 20
node route:module 2580 131802.5ms 547.2ms -99.6% 547.2ms 4.6ms 20
web route:client-entry 2580 96728.8ms 397.9ms -99.6% 397.9ms 5.8ms 20
node manifest:transform 10 339.1ms 147.7ms -56.4% 147.7ms 17.8ms 10
node module:client-only-stub 10 329.1ms 69.2ms -79.0% 69.2ms 21.0ms 10
web manifest:stage 20 26.3ms 20.8ms -20.9% 20.8ms 1.4ms 20
web manifest:transform 10 1.0ms 1.0ms 0.0% 1.0ms 0.1ms 10

synthetic-256-ssr-esm-split Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 2582 244138.1ms 1429.4ms -99.4% 1429.4ms 20.9ms 22
node route:module 2580 119456.8ms 556.0ms -99.5% 556.0ms 4.8ms 20
web route:client-entry 2582 102388.6ms 409.7ms -99.6% 409.7ms 5.5ms 22
node module:client-only-stub 10 231.8ms 169.0ms -27.1% 169.0ms 60.2ms 10
node manifest:transform 10 228.7ms 181.2ms -20.8% 181.2ms 21.5ms 10
web manifest:stage 22 25.9ms 21.8ms -15.8% 21.8ms 1.4ms 22
web manifest:transform 10 1.1ms 1.0ms -9.1% 1.0ms 0.1ms 10

synthetic-48-ssr-esm Plugin Operations

Environment Operation Count Base total Head total Delta Head wall Head max Reports
web route:module 500 519.0ms 451.4ms -13.0% 451.4ms 14.9ms 20
node route:module 500 235.5ms 155.5ms -34.0% 155.5ms 0.8ms 20
web route:client-entry 500 107.6ms 114.6ms +6.5% 114.6ms 3.4ms 20
node manifest:transform 10 97.2ms 55.3ms -43.1% 55.3ms 8.5ms 10
node module:client-only-stub 10 93.3ms 77.6ms -16.8% 77.6ms 12.9ms 10
web manifest:stage 20 6.8ms 5.5ms -19.1% 5.5ms 0.4ms 20
web manifest:transform 10 1.0ms 1.0ms 0.0% 1.0ms 0.1ms 10

Synthetic Rsbuild App

Rendered 2 production build benchmarks.

Benchmark Runs Base total Head total Delta Head mean Head p95 Speedup Head RSS p95
complex app 2 116.55s 115.27s -1.1% 115.27s - 1.01x -
complex app 2 81.10s 80.81s -0.4% 80.81s - 1.00x -

Rendered 1 dev benchmark fixture from the embedded complex app.

Benchmark Runs Base total Head total Delta Base ready Head ready Base routes Head routes Base update/HMR Head update/HMR Update delta Head mean Head p95 Speedup Head RSS p95
complex app 2 98.70s 97.50s -1.2% 90.00s 88.74s 2.89s 2.87s 3.41s 3.29s -3.3% 97.50s - 1.01x -

Profile: full; mode: dev; iterations: 10; warmup: 0.
The uploaded benchmark artifact includes diagnostics/summary.md and diagnostics/summary.json with runner metadata, per-run timing samples, CPU/RSS samples, and plugin timing hot spots.
Workflow run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1095e45d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/dev-server.ts Outdated
@ScriptedAlchemy ScriptedAlchemy changed the base branch from main to perf/bundling-performance June 27, 2026 00:02
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR refactors parallelTransforms from an object-based configuration to false | number, backed by new CPU-count helpers (getAvailableCpuCount, getDefaultConcurrency, mapWithConcurrency) in src/concurrency.ts. The parallel route transform executor is refactored to use lazy worker creation via an injected factory. A new shouldParallelizeEnvironmentBuilds helper controls whether the node Rsbuild environment depends on the web compiler. Compile-attempt identity tracking (DevCompileAttemptIdentity) is added to DevCompilerPair and CompilationIdentityTracker, enabling same-attempt parallel publishing in the dev runtime controller and a sameCompileAttempt guard in finishAttempt, which now returns a status discriminant. PluginOptions gains onRouteTopologyChange, lazyCompilation, and logPerformance fields. Benchmark scripts are updated for the new concurrency model and gain a CI mode-mismatch guard.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly reflects the main changes: parallel dev compilers, worker-pool route transforms, and startup/HMR performance work.
Description check ✅ Passed The description matches the changeset and covers the main dev compiler, route transform, and lazy compilation work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/parallel-dev-compilers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (10)
examples/custom-node-server/scripts/smoke-production.mjs-64-70 (1)

64-70: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid the fixed 5s teardown delay after early exits.

If the child has already exited before this block runs, once(child, 'exit') subscribes too late and the script always waits the full timeout. Capture the exit promise before signaling, and skip the wait when exitCode is already set.

Proposed fix
 } finally {
-  child.kill('SIGTERM');
-  await Promise.race([
-    once(child, 'exit'),
-    new Promise(resolve => setTimeout(resolve, 5_000)),
-  ]);
-  if (child.exitCode === null) {
-    child.kill('SIGKILL');
-  }
+  if (child.exitCode === null) {
+    const exited = once(child, 'exit');
+    child.kill('SIGTERM');
+    await Promise.race([
+      exited,
+      new Promise(resolve => setTimeout(resolve, 5_000)),
+    ]);
+    if (child.exitCode === null) {
+      child.kill('SIGKILL');
+    }
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/custom-node-server/scripts/smoke-production.mjs` around lines 64 -
70, The teardown in smoke-production.mjs waits on once(child, 'exit') only after
sending SIGTERM, which can miss an exit that already happened and force the full
timeout. Update the child process handling around the existing child.kill and
Promise.race flow by creating the exit promise before signaling, and guard the
wait with child.exitCode so you skip the timeout path when the process has
already exited.
README.md-107-112 (1)

107-112: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document one parallelTransforms default.

Line 110 says @default true, but the defaults section on Lines 309-314 says the option defaults to undefined with worker execution enabled implicitly. Pick one representation so the public API contract is not self-contradictory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 107 - 112, The README documentation for
parallelTransforms is contradictory about its default value. Update the public
API docs so the JSDoc on parallelTransforms and the defaults section describe
the same behavior, using one consistent default representation in both places.
Refer to parallelTransforms in the options docs and the defaults section, and
make sure the wording matches whether worker execution is enabled implicitly or
via an explicit true default.
README.md-527-527 (1)

527-527: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Exit on unrecoverable startup failure.

Line 527 only logs boot errors, but the section above explicitly requires a clean process restart after startup failure. Exit non-zero after logging so the example does not stay alive in a broken state.

Proposed fix
-startServer().catch(console.error);
+startServer().catch(error => {
+  console.error(error);
+  process.exit(1);
+});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 527, The startup example in startServer() currently only
logs failures with console.error and leaves the process running, which conflicts
with the documented requirement for unrecoverable startup failures. Update the
startup handling so the catch path for startServer() both logs the error and
terminates the process with a non-zero exit code, keeping the example from
continuing in a broken state.
scripts/benchmark/fixture.mjs-648-658 (1)

648-658: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail fast on invalid largeConfig counts.

The new largeConfig override path is unvalidated, but several generators later use counts like workers, localeFiles, cssModules, svgAssets, utilitiesPerRoute, and payloadEntriesPerComponent in %/division and import-path generation. Passing 0 currently emits broken fixture code instead of rejecting the config up front.

🛠️ Proposed fix
+const assertPositiveInteger = (name, value) => {
+  if (!Number.isInteger(value) || value < 1) {
+    throw new Error(
+      `${name} must be a positive integer for the large benchmark fixture.`
+    );
+  }
+};
+
 const generateLargeFixture = async ({
   root,
   routeCount,
   variant,
@@
 }) => {
   const config = normalizeLargeConfig(routeCount, largeConfig);
+  for (const [name, value] of Object.entries({
+    routes: config.routes,
+    componentsPerRoute: config.componentsPerRoute,
+    utilitiesPerRoute: config.utilitiesPerRoute,
+    lazyModulesPerRoute: config.lazyModulesPerRoute,
+    workers: config.workers,
+    svgAssets: config.svgAssets,
+    cssModules: config.cssModules,
+    localeFiles: config.localeFiles,
+    payloadEntriesPerComponent: config.payloadEntriesPerComponent,
+  })) {
+    assertPositiveInteger(name, value);
+  }
   const isSpa = variant === 'spa';

Also applies to: 814-834

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/benchmark/fixture.mjs` around lines 648 - 658, The largeConfig
override path in generateLargeFixture/normalizeLargeConfig accepts invalid zero
values, which later break generators that use workers, localeFiles, cssModules,
svgAssets, utilitiesPerRoute, and payloadEntriesPerComponent in division,
modulo, or import-path logic. Add upfront validation for these count fields in
normalizeLargeConfig (or immediately after calling it in generateLargeFixture)
and reject non-positive values with a clear error before any fixture generation
starts.
rslib.config.ts-23-28 (1)

23-28: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Emit the route transform worker from the CJS build too.

createWorkerUrl() always resolves ./parallel-route-transform-worker.js, but the CJS source.entry block doesn’t include that file. CJS consumers won’t get the worker asset, so parallel transforms will fall back to the serial path.

Proposed fix
       source: {
         entry: {
           index: './src/index.ts',
+          'parallel-route-transform-worker':
+            './src/parallel-route-transform-worker.ts',
           'templates/entry.server': './src/templates/entry.server.tsx',
           'templates/entry.client': './src/templates/entry.client.tsx',
         },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rslib.config.ts` around lines 23 - 28, The CJS source.entry map is missing
the route transform worker entry, so CJS builds won’t emit the asset that
createWorkerUrl() resolves. Update the source.entry block in rslib.config.ts to
include the parallel route transform worker alongside the existing index,
templates/entry.server, and templates/entry.client entries, so CJS consumers
also receive parallel-route-transform-worker.js.
src/route-export-pruning.ts-544-549 (1)

544-549: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don’t track sourced re-export names as local bindings.

For export { loader as clientLoader } from "./x", specifier.local.name is the imported name from the source module, not a local binding. Adding it to removedExportLocalNames can make the later assignment cleanup remove unrelated local loader.* = ... statements.

Proposed fix
             const exportedName = getExportedName(specifier);
             if (exportedName && exportsToRemoveSet.has(exportedName)) {
               exportsChanged = true;
-              if (specifier.local?.name) {
+              if (!statement.source && specifier.local?.name) {
                 removedExportLocalNames.add(specifier.local.name);
                 removedExportReferencedNames.add(specifier.local.name);
               }
               return false;
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/route-export-pruning.ts` around lines 544 - 549, The re-export cleanup in
the export pruning flow is incorrectly treating sourced re-export names as local
bindings. In the logic around the export specifier handling in
route-export-pruning and the sets removedExportLocalNames /
removedExportReferencedNames, only add names that are actually declared locally
in the file; for `export { loader as clientLoader } from "./x"`, avoid recording
`specifier.local.name` as a local binding so later assignment cleanup does not
delete unrelated `loader.*` statements. Use the existing export-specifier and
assignment-pruning checks to distinguish sourced re-exports from true local
exports before adding names to those tracking sets.
tests/index.test.ts-106-139 (1)

106-139: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the previous existsSync mock implementation.

Line 111 overrides a shared mock, but Line 138 leaves it hardcoded to true. Capture and restore the previous implementation so this test cannot leak filesystem behavior into later cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/index.test.ts` around lines 106 - 139, Restore the shared fs.existsSync
mock in this test by capturing the previous mock implementation before
overriding it and reinstating that exact implementation in the finally block.
Update the test around createStubRsbuild and pluginReactRouter so the temporary
behavior used for the watchFiles assertion does not leak into later tests.
src/index.ts-231-239 (1)

231-239: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat serverOutput: undefined as omitted.

hasOwnProperty makes { serverOutput: undefined } explicit, so resolvedServerOutput can become undefined and flow into the node Rspack config.

Proposed fix
-    const hasExplicitServerOutput = Object.prototype.hasOwnProperty.call(
-      options,
-      'serverOutput'
-    );
-    let resolvedServerOutput = pluginOptions.serverOutput;
-    if (!hasExplicitServerOutput) {
-      resolvedServerOutput =
-        serverModuleFormat === 'cjs' ? 'commonjs' : 'module';
-    }
+    const hasExplicitServerOutput = options.serverOutput !== undefined;
+    const resolvedServerOutput = hasExplicitServerOutput
+      ? options.serverOutput
+      : serverModuleFormat === 'cjs'
+        ? 'commonjs'
+        : 'module';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 231 - 239, Treat serverOutput: undefined the same
as an omitted option in the server output resolution logic. Update the
hasExplicitServerOutput check in the src/index.ts flow around
pluginOptions.serverOutput so that undefined does not count as explicitly set,
and keep the fallback based on serverModuleFormat ('cjs' => commonjs, otherwise
module) from flowing into the Node Rspack config. Reference the server output
resolution branch and the resolvedServerOutput assignment when making the fix.
src/dev-generation.ts-551-553 (1)

551-553: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep load() asynchronous in the ready path.

selectBuild() can throw here, which escapes synchronously instead of rejecting the promised API. That makes loadReactRouterServerBuild() behave differently between starting and ready states for the same missing-build failure.

Proposed fix
       if (state.kind === 'ready') {
-        return Promise.resolve(selectBuild(state.committed, entryName));
+        return Promise.resolve().then(() =>
+          selectBuild(state.committed, entryName)
+        );
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev-generation.ts` around lines 551 - 553, The `load()` implementation in
`src/dev-generation.ts` should stay asynchronous in the `state.kind === 'ready'`
branch, because calling `selectBuild()` directly can throw synchronously instead
of returning a rejected promise. Update the ready-path logic around
`selectBuild()` and `loadReactRouterServerBuild()` so missing-build failures are
handled through promise rejection consistently with the `starting` state.
src/typegen.ts-12-19 (1)

12-19: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Use the local react-router CLI and keep watch failures visible.

  • npx --yes can fall back to a registry install when the binary is missing; run the app’s installed CLI directly so typegen stays on the pinned version.
  • The watch process currently swallows every rejection. Only ignore shutdown, or restart/log unexpected exits so typegen doesn’t die silently.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/typegen.ts` around lines 12 - 19, The typegen launcher in typegen.ts
should use the project’s locally installed react-router CLI instead of npx
--yes, so update the execa invocation to call the pinned binary directly from
the app dependencies. In the same typegen/watch flow, avoid swallowing every
rejected process exit: adjust the process handling around the execa call so only
intentional shutdown is ignored, and unexpected failures from the watch process
are surfaced or restarted/logged. Use the existing typegen process setup and its
process variable as the place to make these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/build-output-transforms.ts`:
- Around line 123-136: The fallback in build-output-transforms should stay
bundle-scoped even when latestServerManifestsByBundleId is not yet populated. In
the manifest selection logic around getLatestServerManifest,
getLatestServerManifestByBundleId, and getReactRouterManifestForDev, ensure the
cold-path dev manifest returned for
virtual/react-router/server-manifest-${bundleId} is filtered to the current
bundleId before it is used. Keep the per-bundle behavior consistent with the
virtual module wiring from src/index.ts so the fallback never embeds routes from
other bundles.

In `@src/dev-runtime-artifacts.ts`:
- Around line 145-174: The assertBuildMatchesManifest check only verifies routes
present in ReactRouterDevManifest, so stale ServerBuild entries for deleted
routes can slip through. Update assertBuildMatchesManifest to also compare
build.routes against manifest.routes and throw when the evaluated server build
contains any extra route IDs not in the manifest, alongside the existing
missing-route and loader/action export checks. Use the existing entryName,
build, manifest, and routeId logic to keep the validation in one place.

In `@src/dev-runtime-session.ts`:
- Around line 62-69: The completeClose flow in dev-runtime-session skips
closeBinding() when the session is already in the closing state, so successful
server.close() calls never fully clean up the runtime. Update completeClose to
invoke closeBinding(binding) for the successful closing path as well as active,
while still guarding with isCurrentBinding(binding) and preserving the
transition to idle; apply the same fix in the other matching close path around
the later close handler.

In `@src/prerender-build.ts`:
- Around line 175-177: The prerender write path logic in the output path
construction must prevent traversal outside clientBuildDir. Update the path
handling in prerender-build.ts around the outputPath/mkdir/writeFile flow to
validate normalizedPath segments before joining, reject any .. or
backslash-based traversal, and assert the final resolved path stays under
clientBuildDir. Apply the same safeguard to the other prerender output write
sites mentioned by the build flow helpers so all generated files remain confined
to the client build directory.
- Around line 496-498: The cleanup in enqueue path handling creates an
unconsumed promise from task.finally, which can surface prerender failures as
unhandled rejections; update the logic around enqueue, pending, and task so the
cleanup promise is handled explicitly. Use the task promise’s then with both
fulfillment and rejection handlers, or attach a catch to the promise returned by
finally, while still removing the task from pending.
- Around line 549-607: The prerender/build-request flow in prerender-build.ts
leaves process.env.IS_RR_BUILD_REQUEST mutated after the work completes. In the
block around resolveServerBuildModule, createRequestHandler, runPrerenderPaths,
and handleSpaMode, save the previous IS_RR_BUILD_REQUEST value before setting it
and restore it in a finally block after all prerender/Spa handling finishes.
This should be done around the existing build request logic so buildEnd and
subsequent runs/tests see the original process-global state.
- Around line 211-225: Escape the redirect target before interpolating it into
the fallback HTML in the redirect handling block so untrusted `Location` values
cannot inject markup or attributes. Update the logic around
`redirectStatusCodes`, `response.headers.get('Location')`, and the HTML template
in `src/prerender-build.ts` to use a safely escaped/sanitized version for the
`<title>`, `meta refresh`, and `<a href>`/link text. Keep `normalizedPath`
as-is, but ensure every use of the redirect destination is HTML- and
attribute-safe.

In `@src/route-chunks.ts`:
- Around line 211-216: The shared side-effect collection in route chunking is
incorrectly treating directive prologues like "use client" and "use strict" as
blocking statements. Update the filtering logic in the shared side-effect scan
and the related `topLevelNonModuleStatements` handling so `ExpressionStatement`s
that are directive prologues are excluded, while real side-effectful expressions
remain included. Use the existing `sharedTopLevelSideEffects` and export
analysis paths to ensure `splitRouteModules: "enforce"` only rejects genuinely
shared side effects.

In `@src/route-export-resolution.ts`:
- Around line 160-182: Update resolvePackageExports and related callers so
subpath imports are handled correctly: when packageJson.exports is absent, do
not always fall back to module/main for every packageSubpath, and make sure
pkg/subpath is not resolved as the package entry. In the exports branch, enhance
the lookup logic in resolvePackageExports to support non-exact export shapes
such as pattern mappings like "./*" instead of only direct packageSubpath keys.
Then adjust resolveExportAllModule so a null result from resolvePackageExports
is treated as “not matched yet” and it can still fall back to
createRequire(...).resolve() before rejecting the import.

In `@src/typegen.ts`:
- Around line 22-30: The `typegen --watch` promise handling in `typegenProcess`
is swallowing every rejection, which hides unexpected watcher exits. Update the
`.catch` path so it only ignores the intentional shutdown case from server
teardown, and for any other rejection in `src/typegen.ts` surface the failure by
logging or rethrowing it. Use the existing `typegenProcess` cleanup in
`.finally()` to keep the process reset behavior intact while making crashes or
invalid config failures visible.

In `@src/virtual-modules.ts`:
- Around line 11-19: The virtual module ID validation in the module path builder
does not block backslash separators, so a Windows-style traversal pattern can
slip through. Update the validation in the relativeId/segments check to reject
any segment containing backslashes before returning the node_modules path,
keeping the existing dot-segment and empty-segment checks in place.

---

Minor comments:
In `@examples/custom-node-server/scripts/smoke-production.mjs`:
- Around line 64-70: The teardown in smoke-production.mjs waits on once(child,
'exit') only after sending SIGTERM, which can miss an exit that already happened
and force the full timeout. Update the child process handling around the
existing child.kill and Promise.race flow by creating the exit promise before
signaling, and guard the wait with child.exitCode so you skip the timeout path
when the process has already exited.

In `@README.md`:
- Around line 107-112: The README documentation for parallelTransforms is
contradictory about its default value. Update the public API docs so the JSDoc
on parallelTransforms and the defaults section describe the same behavior, using
one consistent default representation in both places. Refer to
parallelTransforms in the options docs and the defaults section, and make sure
the wording matches whether worker execution is enabled implicitly or via an
explicit true default.
- Line 527: The startup example in startServer() currently only logs failures
with console.error and leaves the process running, which conflicts with the
documented requirement for unrecoverable startup failures. Update the startup
handling so the catch path for startServer() both logs the error and terminates
the process with a non-zero exit code, keeping the example from continuing in a
broken state.

In `@rslib.config.ts`:
- Around line 23-28: The CJS source.entry map is missing the route transform
worker entry, so CJS builds won’t emit the asset that createWorkerUrl()
resolves. Update the source.entry block in rslib.config.ts to include the
parallel route transform worker alongside the existing index,
templates/entry.server, and templates/entry.client entries, so CJS consumers
also receive parallel-route-transform-worker.js.

In `@scripts/benchmark/fixture.mjs`:
- Around line 648-658: The largeConfig override path in
generateLargeFixture/normalizeLargeConfig accepts invalid zero values, which
later break generators that use workers, localeFiles, cssModules, svgAssets,
utilitiesPerRoute, and payloadEntriesPerComponent in division, modulo, or
import-path logic. Add upfront validation for these count fields in
normalizeLargeConfig (or immediately after calling it in generateLargeFixture)
and reject non-positive values with a clear error before any fixture generation
starts.

In `@src/dev-generation.ts`:
- Around line 551-553: The `load()` implementation in `src/dev-generation.ts`
should stay asynchronous in the `state.kind === 'ready'` branch, because calling
`selectBuild()` directly can throw synchronously instead of returning a rejected
promise. Update the ready-path logic around `selectBuild()` and
`loadReactRouterServerBuild()` so missing-build failures are handled through
promise rejection consistently with the `starting` state.

In `@src/index.ts`:
- Around line 231-239: Treat serverOutput: undefined the same as an omitted
option in the server output resolution logic. Update the hasExplicitServerOutput
check in the src/index.ts flow around pluginOptions.serverOutput so that
undefined does not count as explicitly set, and keep the fallback based on
serverModuleFormat ('cjs' => commonjs, otherwise module) from flowing into the
Node Rspack config. Reference the server output resolution branch and the
resolvedServerOutput assignment when making the fix.

In `@src/route-export-pruning.ts`:
- Around line 544-549: The re-export cleanup in the export pruning flow is
incorrectly treating sourced re-export names as local bindings. In the logic
around the export specifier handling in route-export-pruning and the sets
removedExportLocalNames / removedExportReferencedNames, only add names that are
actually declared locally in the file; for `export { loader as clientLoader }
from "./x"`, avoid recording `specifier.local.name` as a local binding so later
assignment cleanup does not delete unrelated `loader.*` statements. Use the
existing export-specifier and assignment-pruning checks to distinguish sourced
re-exports from true local exports before adding names to those tracking sets.

In `@src/typegen.ts`:
- Around line 12-19: The typegen launcher in typegen.ts should use the project’s
locally installed react-router CLI instead of npx --yes, so update the execa
invocation to call the pinned binary directly from the app dependencies. In the
same typegen/watch flow, avoid swallowing every rejected process exit: adjust
the process handling around the execa call so only intentional shutdown is
ignored, and unexpected failures from the watch process are surfaced or
restarted/logged. Use the existing typegen process setup and its process
variable as the place to make these changes.

In `@tests/index.test.ts`:
- Around line 106-139: Restore the shared fs.existsSync mock in this test by
capturing the previous mock implementation before overriding it and reinstating
that exact implementation in the finally block. Update the test around
createStubRsbuild and pluginReactRouter so the temporary behavior used for the
watchFiles assertion does not leak into later tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5108dcda-f5f7-41c8-9159-04e0bd83d220

📥 Commits

Reviewing files that changed from the base of the PR and between 06ae3db and b1095e4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (124)
  • .changeset/bright-routes-run.md
  • .changeset/coherent-dev-generations.md
  • .changeset/lazy-entry-hydration.md
  • .changeset/quiet-topology-order.md
  • .changeset/sharp-routes-heal.md
  • .changeset/warm-watchers-rest.md
  • README.md
  • benchmarks/README.md
  • config/package.json
  • config/rslib.config.ts
  • docs/coherent-dev-generations.md
  • examples/client-only/package.json
  • examples/cloudflare/package.json
  • examples/custom-node-server/app/load-context.d.ts
  • examples/custom-node-server/app/routes/projects/edit.tsx
  • examples/custom-node-server/app/routes/projects/index.tsx
  • examples/custom-node-server/app/routes/projects/layout.tsx
  • examples/custom-node-server/app/routes/projects/project.tsx
  • examples/custom-node-server/app/routes/projects/settings.tsx
  • examples/custom-node-server/package.json
  • examples/custom-node-server/playwright.config.ts
  • examples/custom-node-server/rsbuild.config.ts
  • examples/custom-node-server/scripts/smoke-production.mjs
  • examples/custom-node-server/server.js
  • examples/custom-node-server/server/index.ts
  • examples/custom-node-server/tsconfig.json
  • examples/default-template/app/dev-routes.ts
  • examples/default-template/app/routes.ts
  • examples/default-template/package.json
  • examples/default-template/playwright.config.ts
  • examples/default-template/rsbuild.config.ts
  • examples/default-template/tests/e2e/dev-route-watch.test.ts
  • examples/default-template/tests/e2e/lazy-compilation.test.ts
  • examples/epic-stack/package.json
  • examples/federation/epic-stack-remote/package.json
  • examples/federation/epic-stack/package.json
  • examples/prerender/package.json
  • examples/spa-mode/package.json
  • package.json
  • rslib.config.ts
  • scripts/bench-builds.mjs
  • scripts/benchmark/fixture.mjs
  • scripts/compare-benchmarks.mjs
  • scripts/test-package-interop.mjs
  • src/babel.ts
  • src/bounded-cache.ts
  • src/build-manifest.ts
  • src/build-output-transforms.ts
  • src/concurrency.ts
  • src/constants.ts
  • src/dev-generation.ts
  • src/dev-runtime-artifacts.ts
  • src/dev-runtime-compilation.ts
  • src/dev-runtime-controller.ts
  • src/dev-runtime-session.ts
  • src/dev-server.ts
  • src/export-utils.ts
  • src/index.ts
  • src/lazy-compilation.ts
  • src/manifest.ts
  • src/modify-browser-manifest.ts
  • src/parallel-route-transform-protocol.ts
  • src/parallel-route-transform-worker.ts
  • src/parallel-route-transforms.ts
  • src/performance.ts
  • src/plugin-utils.ts
  • src/prerender-build.ts
  • src/prerender.ts
  • src/react-router-config.ts
  • src/route-artifacts.ts
  • src/route-ast.ts
  • src/route-chunks.ts
  • src/route-component-transform.ts
  • src/route-export-pruning.ts
  • src/route-export-resolution.ts
  • src/route-transform-tasks.ts
  • src/route-watch.ts
  • src/server-build-plan.ts
  • src/server-utils.ts
  • src/templates/entry.server.tsx
  • src/typegen.ts
  • src/types.ts
  • src/virtual-modules.ts
  • src/warnings/warn-on-client-source-maps.ts
  • src/yuku.ts
  • tests/benchmark-fixture.test.ts
  • tests/bounded-cache.test.ts
  • tests/build-manifest.test.ts
  • tests/build-output-transforms.test.ts
  • tests/client-modules.test.ts
  • tests/dev-generation-multi-entry.test.ts
  • tests/dev-generation.test.ts
  • tests/dev-runtime-controller.test.ts
  • tests/dev-runtime.integration.test.ts
  • tests/dev-server.test.ts
  • tests/export-utils.test.ts
  • tests/features.test.ts
  • tests/fixtures/dev-runtime/app/entry.client.tsx
  • tests/fixtures/dev-runtime/app/entry.server.tsx
  • tests/fixtures/dev-runtime/app/root.tsx
  • tests/fixtures/dev-runtime/app/routes.ts
  • tests/fixtures/dev-runtime/app/routes/index.tsx
  • tests/fixtures/dev-runtime/app/routes/other.tsx
  • tests/fixtures/dev-runtime/react-router.config.ts
  • tests/fixtures/dev-runtime/server/index.ts
  • tests/index.test.ts
  • tests/lazy-compilation.test.ts
  • tests/manifest-split-route-modules.test.ts
  • tests/manifest.test.ts
  • tests/modify-browser-manifest.test.ts
  • tests/parallel-route-transforms.test.ts
  • tests/performance.test.ts
  • tests/plugin-utils.test.ts
  • tests/prerender.test.ts
  • tests/react-router-config.test.ts
  • tests/remove-exports.test.ts
  • tests/route-artifacts.test.ts
  • tests/route-chunks-cache.test.ts
  • tests/route-chunks.test.ts
  • tests/route-watch.test.ts
  • tests/server-build-plan.test.ts
  • tests/server-utils.test.ts
  • tests/setup.ts
  • tests/warn-on-client-source-maps.test.ts
💤 Files with no reviewable changes (2)
  • examples/custom-node-server/server/index.ts
  • src/babel.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (10)
examples/custom-node-server/scripts/smoke-production.mjs-64-70 (1)

64-70: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid the fixed 5s teardown delay after early exits.

If the child has already exited before this block runs, once(child, 'exit') subscribes too late and the script always waits the full timeout. Capture the exit promise before signaling, and skip the wait when exitCode is already set.

Proposed fix
 } finally {
-  child.kill('SIGTERM');
-  await Promise.race([
-    once(child, 'exit'),
-    new Promise(resolve => setTimeout(resolve, 5_000)),
-  ]);
-  if (child.exitCode === null) {
-    child.kill('SIGKILL');
-  }
+  if (child.exitCode === null) {
+    const exited = once(child, 'exit');
+    child.kill('SIGTERM');
+    await Promise.race([
+      exited,
+      new Promise(resolve => setTimeout(resolve, 5_000)),
+    ]);
+    if (child.exitCode === null) {
+      child.kill('SIGKILL');
+    }
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/custom-node-server/scripts/smoke-production.mjs` around lines 64 -
70, The teardown in smoke-production.mjs waits on once(child, 'exit') only after
sending SIGTERM, which can miss an exit that already happened and force the full
timeout. Update the child process handling around the existing child.kill and
Promise.race flow by creating the exit promise before signaling, and guard the
wait with child.exitCode so you skip the timeout path when the process has
already exited.
README.md-107-112 (1)

107-112: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document one parallelTransforms default.

Line 110 says @default true, but the defaults section on Lines 309-314 says the option defaults to undefined with worker execution enabled implicitly. Pick one representation so the public API contract is not self-contradictory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 107 - 112, The README documentation for
parallelTransforms is contradictory about its default value. Update the public
API docs so the JSDoc on parallelTransforms and the defaults section describe
the same behavior, using one consistent default representation in both places.
Refer to parallelTransforms in the options docs and the defaults section, and
make sure the wording matches whether worker execution is enabled implicitly or
via an explicit true default.
README.md-527-527 (1)

527-527: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Exit on unrecoverable startup failure.

Line 527 only logs boot errors, but the section above explicitly requires a clean process restart after startup failure. Exit non-zero after logging so the example does not stay alive in a broken state.

Proposed fix
-startServer().catch(console.error);
+startServer().catch(error => {
+  console.error(error);
+  process.exit(1);
+});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 527, The startup example in startServer() currently only
logs failures with console.error and leaves the process running, which conflicts
with the documented requirement for unrecoverable startup failures. Update the
startup handling so the catch path for startServer() both logs the error and
terminates the process with a non-zero exit code, keeping the example from
continuing in a broken state.
scripts/benchmark/fixture.mjs-648-658 (1)

648-658: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail fast on invalid largeConfig counts.

The new largeConfig override path is unvalidated, but several generators later use counts like workers, localeFiles, cssModules, svgAssets, utilitiesPerRoute, and payloadEntriesPerComponent in %/division and import-path generation. Passing 0 currently emits broken fixture code instead of rejecting the config up front.

🛠️ Proposed fix
+const assertPositiveInteger = (name, value) => {
+  if (!Number.isInteger(value) || value < 1) {
+    throw new Error(
+      `${name} must be a positive integer for the large benchmark fixture.`
+    );
+  }
+};
+
 const generateLargeFixture = async ({
   root,
   routeCount,
   variant,
@@
 }) => {
   const config = normalizeLargeConfig(routeCount, largeConfig);
+  for (const [name, value] of Object.entries({
+    routes: config.routes,
+    componentsPerRoute: config.componentsPerRoute,
+    utilitiesPerRoute: config.utilitiesPerRoute,
+    lazyModulesPerRoute: config.lazyModulesPerRoute,
+    workers: config.workers,
+    svgAssets: config.svgAssets,
+    cssModules: config.cssModules,
+    localeFiles: config.localeFiles,
+    payloadEntriesPerComponent: config.payloadEntriesPerComponent,
+  })) {
+    assertPositiveInteger(name, value);
+  }
   const isSpa = variant === 'spa';

Also applies to: 814-834

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/benchmark/fixture.mjs` around lines 648 - 658, The largeConfig
override path in generateLargeFixture/normalizeLargeConfig accepts invalid zero
values, which later break generators that use workers, localeFiles, cssModules,
svgAssets, utilitiesPerRoute, and payloadEntriesPerComponent in division,
modulo, or import-path logic. Add upfront validation for these count fields in
normalizeLargeConfig (or immediately after calling it in generateLargeFixture)
and reject non-positive values with a clear error before any fixture generation
starts.
rslib.config.ts-23-28 (1)

23-28: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Emit the route transform worker from the CJS build too.

createWorkerUrl() always resolves ./parallel-route-transform-worker.js, but the CJS source.entry block doesn’t include that file. CJS consumers won’t get the worker asset, so parallel transforms will fall back to the serial path.

Proposed fix
       source: {
         entry: {
           index: './src/index.ts',
+          'parallel-route-transform-worker':
+            './src/parallel-route-transform-worker.ts',
           'templates/entry.server': './src/templates/entry.server.tsx',
           'templates/entry.client': './src/templates/entry.client.tsx',
         },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rslib.config.ts` around lines 23 - 28, The CJS source.entry map is missing
the route transform worker entry, so CJS builds won’t emit the asset that
createWorkerUrl() resolves. Update the source.entry block in rslib.config.ts to
include the parallel route transform worker alongside the existing index,
templates/entry.server, and templates/entry.client entries, so CJS consumers
also receive parallel-route-transform-worker.js.
src/route-export-pruning.ts-544-549 (1)

544-549: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don’t track sourced re-export names as local bindings.

For export { loader as clientLoader } from "./x", specifier.local.name is the imported name from the source module, not a local binding. Adding it to removedExportLocalNames can make the later assignment cleanup remove unrelated local loader.* = ... statements.

Proposed fix
             const exportedName = getExportedName(specifier);
             if (exportedName && exportsToRemoveSet.has(exportedName)) {
               exportsChanged = true;
-              if (specifier.local?.name) {
+              if (!statement.source && specifier.local?.name) {
                 removedExportLocalNames.add(specifier.local.name);
                 removedExportReferencedNames.add(specifier.local.name);
               }
               return false;
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/route-export-pruning.ts` around lines 544 - 549, The re-export cleanup in
the export pruning flow is incorrectly treating sourced re-export names as local
bindings. In the logic around the export specifier handling in
route-export-pruning and the sets removedExportLocalNames /
removedExportReferencedNames, only add names that are actually declared locally
in the file; for `export { loader as clientLoader } from "./x"`, avoid recording
`specifier.local.name` as a local binding so later assignment cleanup does not
delete unrelated `loader.*` statements. Use the existing export-specifier and
assignment-pruning checks to distinguish sourced re-exports from true local
exports before adding names to those tracking sets.
tests/index.test.ts-106-139 (1)

106-139: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the previous existsSync mock implementation.

Line 111 overrides a shared mock, but Line 138 leaves it hardcoded to true. Capture and restore the previous implementation so this test cannot leak filesystem behavior into later cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/index.test.ts` around lines 106 - 139, Restore the shared fs.existsSync
mock in this test by capturing the previous mock implementation before
overriding it and reinstating that exact implementation in the finally block.
Update the test around createStubRsbuild and pluginReactRouter so the temporary
behavior used for the watchFiles assertion does not leak into later tests.
src/index.ts-231-239 (1)

231-239: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat serverOutput: undefined as omitted.

hasOwnProperty makes { serverOutput: undefined } explicit, so resolvedServerOutput can become undefined and flow into the node Rspack config.

Proposed fix
-    const hasExplicitServerOutput = Object.prototype.hasOwnProperty.call(
-      options,
-      'serverOutput'
-    );
-    let resolvedServerOutput = pluginOptions.serverOutput;
-    if (!hasExplicitServerOutput) {
-      resolvedServerOutput =
-        serverModuleFormat === 'cjs' ? 'commonjs' : 'module';
-    }
+    const hasExplicitServerOutput = options.serverOutput !== undefined;
+    const resolvedServerOutput = hasExplicitServerOutput
+      ? options.serverOutput
+      : serverModuleFormat === 'cjs'
+        ? 'commonjs'
+        : 'module';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 231 - 239, Treat serverOutput: undefined the same
as an omitted option in the server output resolution logic. Update the
hasExplicitServerOutput check in the src/index.ts flow around
pluginOptions.serverOutput so that undefined does not count as explicitly set,
and keep the fallback based on serverModuleFormat ('cjs' => commonjs, otherwise
module) from flowing into the Node Rspack config. Reference the server output
resolution branch and the resolvedServerOutput assignment when making the fix.
src/dev-generation.ts-551-553 (1)

551-553: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep load() asynchronous in the ready path.

selectBuild() can throw here, which escapes synchronously instead of rejecting the promised API. That makes loadReactRouterServerBuild() behave differently between starting and ready states for the same missing-build failure.

Proposed fix
       if (state.kind === 'ready') {
-        return Promise.resolve(selectBuild(state.committed, entryName));
+        return Promise.resolve().then(() =>
+          selectBuild(state.committed, entryName)
+        );
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev-generation.ts` around lines 551 - 553, The `load()` implementation in
`src/dev-generation.ts` should stay asynchronous in the `state.kind === 'ready'`
branch, because calling `selectBuild()` directly can throw synchronously instead
of returning a rejected promise. Update the ready-path logic around
`selectBuild()` and `loadReactRouterServerBuild()` so missing-build failures are
handled through promise rejection consistently with the `starting` state.
src/typegen.ts-12-19 (1)

12-19: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Use the local react-router CLI and keep watch failures visible.

  • npx --yes can fall back to a registry install when the binary is missing; run the app’s installed CLI directly so typegen stays on the pinned version.
  • The watch process currently swallows every rejection. Only ignore shutdown, or restart/log unexpected exits so typegen doesn’t die silently.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/typegen.ts` around lines 12 - 19, The typegen launcher in typegen.ts
should use the project’s locally installed react-router CLI instead of npx
--yes, so update the execa invocation to call the pinned binary directly from
the app dependencies. In the same typegen/watch flow, avoid swallowing every
rejected process exit: adjust the process handling around the execa call so only
intentional shutdown is ignored, and unexpected failures from the watch process
are surfaced or restarted/logged. Use the existing typegen process setup and its
process variable as the place to make these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/build-output-transforms.ts`:
- Around line 123-136: The fallback in build-output-transforms should stay
bundle-scoped even when latestServerManifestsByBundleId is not yet populated. In
the manifest selection logic around getLatestServerManifest,
getLatestServerManifestByBundleId, and getReactRouterManifestForDev, ensure the
cold-path dev manifest returned for
virtual/react-router/server-manifest-${bundleId} is filtered to the current
bundleId before it is used. Keep the per-bundle behavior consistent with the
virtual module wiring from src/index.ts so the fallback never embeds routes from
other bundles.

In `@src/dev-runtime-artifacts.ts`:
- Around line 145-174: The assertBuildMatchesManifest check only verifies routes
present in ReactRouterDevManifest, so stale ServerBuild entries for deleted
routes can slip through. Update assertBuildMatchesManifest to also compare
build.routes against manifest.routes and throw when the evaluated server build
contains any extra route IDs not in the manifest, alongside the existing
missing-route and loader/action export checks. Use the existing entryName,
build, manifest, and routeId logic to keep the validation in one place.

In `@src/dev-runtime-session.ts`:
- Around line 62-69: The completeClose flow in dev-runtime-session skips
closeBinding() when the session is already in the closing state, so successful
server.close() calls never fully clean up the runtime. Update completeClose to
invoke closeBinding(binding) for the successful closing path as well as active,
while still guarding with isCurrentBinding(binding) and preserving the
transition to idle; apply the same fix in the other matching close path around
the later close handler.

In `@src/prerender-build.ts`:
- Around line 175-177: The prerender write path logic in the output path
construction must prevent traversal outside clientBuildDir. Update the path
handling in prerender-build.ts around the outputPath/mkdir/writeFile flow to
validate normalizedPath segments before joining, reject any .. or
backslash-based traversal, and assert the final resolved path stays under
clientBuildDir. Apply the same safeguard to the other prerender output write
sites mentioned by the build flow helpers so all generated files remain confined
to the client build directory.
- Around line 496-498: The cleanup in enqueue path handling creates an
unconsumed promise from task.finally, which can surface prerender failures as
unhandled rejections; update the logic around enqueue, pending, and task so the
cleanup promise is handled explicitly. Use the task promise’s then with both
fulfillment and rejection handlers, or attach a catch to the promise returned by
finally, while still removing the task from pending.
- Around line 549-607: The prerender/build-request flow in prerender-build.ts
leaves process.env.IS_RR_BUILD_REQUEST mutated after the work completes. In the
block around resolveServerBuildModule, createRequestHandler, runPrerenderPaths,
and handleSpaMode, save the previous IS_RR_BUILD_REQUEST value before setting it
and restore it in a finally block after all prerender/Spa handling finishes.
This should be done around the existing build request logic so buildEnd and
subsequent runs/tests see the original process-global state.
- Around line 211-225: Escape the redirect target before interpolating it into
the fallback HTML in the redirect handling block so untrusted `Location` values
cannot inject markup or attributes. Update the logic around
`redirectStatusCodes`, `response.headers.get('Location')`, and the HTML template
in `src/prerender-build.ts` to use a safely escaped/sanitized version for the
`<title>`, `meta refresh`, and `<a href>`/link text. Keep `normalizedPath`
as-is, but ensure every use of the redirect destination is HTML- and
attribute-safe.

In `@src/route-chunks.ts`:
- Around line 211-216: The shared side-effect collection in route chunking is
incorrectly treating directive prologues like "use client" and "use strict" as
blocking statements. Update the filtering logic in the shared side-effect scan
and the related `topLevelNonModuleStatements` handling so `ExpressionStatement`s
that are directive prologues are excluded, while real side-effectful expressions
remain included. Use the existing `sharedTopLevelSideEffects` and export
analysis paths to ensure `splitRouteModules: "enforce"` only rejects genuinely
shared side effects.

In `@src/route-export-resolution.ts`:
- Around line 160-182: Update resolvePackageExports and related callers so
subpath imports are handled correctly: when packageJson.exports is absent, do
not always fall back to module/main for every packageSubpath, and make sure
pkg/subpath is not resolved as the package entry. In the exports branch, enhance
the lookup logic in resolvePackageExports to support non-exact export shapes
such as pattern mappings like "./*" instead of only direct packageSubpath keys.
Then adjust resolveExportAllModule so a null result from resolvePackageExports
is treated as “not matched yet” and it can still fall back to
createRequire(...).resolve() before rejecting the import.

In `@src/typegen.ts`:
- Around line 22-30: The `typegen --watch` promise handling in `typegenProcess`
is swallowing every rejection, which hides unexpected watcher exits. Update the
`.catch` path so it only ignores the intentional shutdown case from server
teardown, and for any other rejection in `src/typegen.ts` surface the failure by
logging or rethrowing it. Use the existing `typegenProcess` cleanup in
`.finally()` to keep the process reset behavior intact while making crashes or
invalid config failures visible.

In `@src/virtual-modules.ts`:
- Around line 11-19: The virtual module ID validation in the module path builder
does not block backslash separators, so a Windows-style traversal pattern can
slip through. Update the validation in the relativeId/segments check to reject
any segment containing backslashes before returning the node_modules path,
keeping the existing dot-segment and empty-segment checks in place.

---

Minor comments:
In `@examples/custom-node-server/scripts/smoke-production.mjs`:
- Around line 64-70: The teardown in smoke-production.mjs waits on once(child,
'exit') only after sending SIGTERM, which can miss an exit that already happened
and force the full timeout. Update the child process handling around the
existing child.kill and Promise.race flow by creating the exit promise before
signaling, and guard the wait with child.exitCode so you skip the timeout path
when the process has already exited.

In `@README.md`:
- Around line 107-112: The README documentation for parallelTransforms is
contradictory about its default value. Update the public API docs so the JSDoc
on parallelTransforms and the defaults section describe the same behavior, using
one consistent default representation in both places. Refer to
parallelTransforms in the options docs and the defaults section, and make sure
the wording matches whether worker execution is enabled implicitly or via an
explicit true default.
- Line 527: The startup example in startServer() currently only logs failures
with console.error and leaves the process running, which conflicts with the
documented requirement for unrecoverable startup failures. Update the startup
handling so the catch path for startServer() both logs the error and terminates
the process with a non-zero exit code, keeping the example from continuing in a
broken state.

In `@rslib.config.ts`:
- Around line 23-28: The CJS source.entry map is missing the route transform
worker entry, so CJS builds won’t emit the asset that createWorkerUrl()
resolves. Update the source.entry block in rslib.config.ts to include the
parallel route transform worker alongside the existing index,
templates/entry.server, and templates/entry.client entries, so CJS consumers
also receive parallel-route-transform-worker.js.

In `@scripts/benchmark/fixture.mjs`:
- Around line 648-658: The largeConfig override path in
generateLargeFixture/normalizeLargeConfig accepts invalid zero values, which
later break generators that use workers, localeFiles, cssModules, svgAssets,
utilitiesPerRoute, and payloadEntriesPerComponent in division, modulo, or
import-path logic. Add upfront validation for these count fields in
normalizeLargeConfig (or immediately after calling it in generateLargeFixture)
and reject non-positive values with a clear error before any fixture generation
starts.

In `@src/dev-generation.ts`:
- Around line 551-553: The `load()` implementation in `src/dev-generation.ts`
should stay asynchronous in the `state.kind === 'ready'` branch, because calling
`selectBuild()` directly can throw synchronously instead of returning a rejected
promise. Update the ready-path logic around `selectBuild()` and
`loadReactRouterServerBuild()` so missing-build failures are handled through
promise rejection consistently with the `starting` state.

In `@src/index.ts`:
- Around line 231-239: Treat serverOutput: undefined the same as an omitted
option in the server output resolution logic. Update the hasExplicitServerOutput
check in the src/index.ts flow around pluginOptions.serverOutput so that
undefined does not count as explicitly set, and keep the fallback based on
serverModuleFormat ('cjs' => commonjs, otherwise module) from flowing into the
Node Rspack config. Reference the server output resolution branch and the
resolvedServerOutput assignment when making the fix.

In `@src/route-export-pruning.ts`:
- Around line 544-549: The re-export cleanup in the export pruning flow is
incorrectly treating sourced re-export names as local bindings. In the logic
around the export specifier handling in route-export-pruning and the sets
removedExportLocalNames / removedExportReferencedNames, only add names that are
actually declared locally in the file; for `export { loader as clientLoader }
from "./x"`, avoid recording `specifier.local.name` as a local binding so later
assignment cleanup does not delete unrelated `loader.*` statements. Use the
existing export-specifier and assignment-pruning checks to distinguish sourced
re-exports from true local exports before adding names to those tracking sets.

In `@src/typegen.ts`:
- Around line 12-19: The typegen launcher in typegen.ts should use the project’s
locally installed react-router CLI instead of npx --yes, so update the execa
invocation to call the pinned binary directly from the app dependencies. In the
same typegen/watch flow, avoid swallowing every rejected process exit: adjust
the process handling around the execa call so only intentional shutdown is
ignored, and unexpected failures from the watch process are surfaced or
restarted/logged. Use the existing typegen process setup and its process
variable as the place to make these changes.

In `@tests/index.test.ts`:
- Around line 106-139: Restore the shared fs.existsSync mock in this test by
capturing the previous mock implementation before overriding it and reinstating
that exact implementation in the finally block. Update the test around
createStubRsbuild and pluginReactRouter so the temporary behavior used for the
watchFiles assertion does not leak into later tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5108dcda-f5f7-41c8-9159-04e0bd83d220

📥 Commits

Reviewing files that changed from the base of the PR and between 06ae3db and b1095e4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (124)
  • .changeset/bright-routes-run.md
  • .changeset/coherent-dev-generations.md
  • .changeset/lazy-entry-hydration.md
  • .changeset/quiet-topology-order.md
  • .changeset/sharp-routes-heal.md
  • .changeset/warm-watchers-rest.md
  • README.md
  • benchmarks/README.md
  • config/package.json
  • config/rslib.config.ts
  • docs/coherent-dev-generations.md
  • examples/client-only/package.json
  • examples/cloudflare/package.json
  • examples/custom-node-server/app/load-context.d.ts
  • examples/custom-node-server/app/routes/projects/edit.tsx
  • examples/custom-node-server/app/routes/projects/index.tsx
  • examples/custom-node-server/app/routes/projects/layout.tsx
  • examples/custom-node-server/app/routes/projects/project.tsx
  • examples/custom-node-server/app/routes/projects/settings.tsx
  • examples/custom-node-server/package.json
  • examples/custom-node-server/playwright.config.ts
  • examples/custom-node-server/rsbuild.config.ts
  • examples/custom-node-server/scripts/smoke-production.mjs
  • examples/custom-node-server/server.js
  • examples/custom-node-server/server/index.ts
  • examples/custom-node-server/tsconfig.json
  • examples/default-template/app/dev-routes.ts
  • examples/default-template/app/routes.ts
  • examples/default-template/package.json
  • examples/default-template/playwright.config.ts
  • examples/default-template/rsbuild.config.ts
  • examples/default-template/tests/e2e/dev-route-watch.test.ts
  • examples/default-template/tests/e2e/lazy-compilation.test.ts
  • examples/epic-stack/package.json
  • examples/federation/epic-stack-remote/package.json
  • examples/federation/epic-stack/package.json
  • examples/prerender/package.json
  • examples/spa-mode/package.json
  • package.json
  • rslib.config.ts
  • scripts/bench-builds.mjs
  • scripts/benchmark/fixture.mjs
  • scripts/compare-benchmarks.mjs
  • scripts/test-package-interop.mjs
  • src/babel.ts
  • src/bounded-cache.ts
  • src/build-manifest.ts
  • src/build-output-transforms.ts
  • src/concurrency.ts
  • src/constants.ts
  • src/dev-generation.ts
  • src/dev-runtime-artifacts.ts
  • src/dev-runtime-compilation.ts
  • src/dev-runtime-controller.ts
  • src/dev-runtime-session.ts
  • src/dev-server.ts
  • src/export-utils.ts
  • src/index.ts
  • src/lazy-compilation.ts
  • src/manifest.ts
  • src/modify-browser-manifest.ts
  • src/parallel-route-transform-protocol.ts
  • src/parallel-route-transform-worker.ts
  • src/parallel-route-transforms.ts
  • src/performance.ts
  • src/plugin-utils.ts
  • src/prerender-build.ts
  • src/prerender.ts
  • src/react-router-config.ts
  • src/route-artifacts.ts
  • src/route-ast.ts
  • src/route-chunks.ts
  • src/route-component-transform.ts
  • src/route-export-pruning.ts
  • src/route-export-resolution.ts
  • src/route-transform-tasks.ts
  • src/route-watch.ts
  • src/server-build-plan.ts
  • src/server-utils.ts
  • src/templates/entry.server.tsx
  • src/typegen.ts
  • src/types.ts
  • src/virtual-modules.ts
  • src/warnings/warn-on-client-source-maps.ts
  • src/yuku.ts
  • tests/benchmark-fixture.test.ts
  • tests/bounded-cache.test.ts
  • tests/build-manifest.test.ts
  • tests/build-output-transforms.test.ts
  • tests/client-modules.test.ts
  • tests/dev-generation-multi-entry.test.ts
  • tests/dev-generation.test.ts
  • tests/dev-runtime-controller.test.ts
  • tests/dev-runtime.integration.test.ts
  • tests/dev-server.test.ts
  • tests/export-utils.test.ts
  • tests/features.test.ts
  • tests/fixtures/dev-runtime/app/entry.client.tsx
  • tests/fixtures/dev-runtime/app/entry.server.tsx
  • tests/fixtures/dev-runtime/app/root.tsx
  • tests/fixtures/dev-runtime/app/routes.ts
  • tests/fixtures/dev-runtime/app/routes/index.tsx
  • tests/fixtures/dev-runtime/app/routes/other.tsx
  • tests/fixtures/dev-runtime/react-router.config.ts
  • tests/fixtures/dev-runtime/server/index.ts
  • tests/index.test.ts
  • tests/lazy-compilation.test.ts
  • tests/manifest-split-route-modules.test.ts
  • tests/manifest.test.ts
  • tests/modify-browser-manifest.test.ts
  • tests/parallel-route-transforms.test.ts
  • tests/performance.test.ts
  • tests/plugin-utils.test.ts
  • tests/prerender.test.ts
  • tests/react-router-config.test.ts
  • tests/remove-exports.test.ts
  • tests/route-artifacts.test.ts
  • tests/route-chunks-cache.test.ts
  • tests/route-chunks.test.ts
  • tests/route-watch.test.ts
  • tests/server-build-plan.test.ts
  • tests/server-utils.test.ts
  • tests/setup.ts
  • tests/warn-on-client-source-maps.test.ts
💤 Files with no reviewable changes (2)
  • examples/custom-node-server/server/index.ts
  • src/babel.ts
🛑 Comments failed to post (11)
src/build-output-transforms.ts (1)

123-136: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Keep bundle-specific server-manifest fallbacks bundle-scoped.

When this transform handles virtual/react-router/server-manifest-${bundleId} before stageLatestManifests() has populated latestServerManifestsByBundleId, Lines 123-136 fall back to getReactRouterManifestForDev(routes, ...), which regenerates the full manifest. In src/index.ts, those virtual modules are wired into per-bundle server builds and later filtered by bundle, so a node-first dev compile can embed routes outside the current bundle until a rebuild corrects it. Please filter the cold-path fallback by bundleId too, not just the cached path.

Possible direction
-          const manifest =
+          const baseManifest =
             (latestServerManifest
               ? ((bundleId && getLatestServerManifestByBundleId(bundleId)) ??
                 latestServerManifest)
               : null) ??
             (await getReactRouterManifestForDev(
               routes,
               pluginOptions,
               getClientStats(),
               appDirectory,
               getAssetPrefix(),
               routeChunkOptions
             ));
+          const manifest =
+            bundleId && routesByServerBundleId[bundleId]
+              ? {
+                  ...baseManifest,
+                  routes: Object.fromEntries(
+                    Object.entries(baseManifest.routes).filter(([routeId]) =>
+                      routeId in routesByServerBundleId[bundleId]
+                    )
+                  ),
+                }
+              : baseManifest;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/build-output-transforms.ts` around lines 123 - 136, The fallback in
build-output-transforms should stay bundle-scoped even when
latestServerManifestsByBundleId is not yet populated. In the manifest selection
logic around getLatestServerManifest, getLatestServerManifestByBundleId, and
getReactRouterManifestForDev, ensure the cold-path dev manifest returned for
virtual/react-router/server-manifest-${bundleId} is filtered to the current
bundleId before it is used. Keep the per-bundle behavior consistent with the
virtual module wiring from src/index.ts so the fallback never embeds routes from
other bundles.
src/dev-runtime-artifacts.ts (1)

145-174: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Also reject server builds with extra route IDs.

This check only walks manifest.routes. If the node build is stale and still contains a deleted route, validation passes and the mixed pair gets pinned anyway. Route removals are one of the mismatch cases this flow needs to catch.

Suggested fix
 const assertBuildMatchesManifest = (
   entryName: string,
   build: ServerBuild,
   manifest: ReactRouterDevManifest
 ): void => {
+  const manifestRouteIds = new Set(Object.keys(manifest.routes));
+  for (const routeId of Object.keys(build.routes)) {
+    if (!manifestRouteIds.has(routeId)) {
+      throw new Error(
+        `[rsbuild-plugin-react-router] Server build ${JSON.stringify(entryName)} route ${JSON.stringify(routeId)} does not exist in the matching web manifest.`
+      );
+    }
+  }
+
   for (const [routeId, manifestRoute] of Object.entries(manifest.routes)) {
     const routeModule = build.routes[routeId]?.module;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

const assertBuildMatchesManifest = (
  entryName: string,
  build: ServerBuild,
  manifest: ReactRouterDevManifest
): void => {
  const manifestRouteIds = new Set(Object.keys(manifest.routes));
  for (const routeId of Object.keys(build.routes)) {
    if (!manifestRouteIds.has(routeId)) {
      throw new Error(
        `[rsbuild-plugin-react-router] Server build ${JSON.stringify(entryName)} route ${JSON.stringify(routeId)} does not exist in the matching web manifest.`
      );
    }
  }

  for (const [routeId, manifestRoute] of Object.entries(manifest.routes)) {
    const routeModule = build.routes[routeId]?.module;
    if (!routeModule) {
      throw new Error(
        `[rsbuild-plugin-react-router] Server build ${JSON.stringify(entryName)} route ${JSON.stringify(routeId)} is missing from the evaluated server build.`
      );
    }
    if (
      Boolean(manifestRoute.hasLoader) !==
      (typeof routeModule.loader === 'function')
    ) {
      throw new Error(
        `[rsbuild-plugin-react-router] Server build ${JSON.stringify(entryName)} route ${JSON.stringify(routeId)} loader export does not match its web manifest.`
      );
    }
    if (
      Boolean(manifestRoute.hasAction) !==
      (typeof routeModule.action === 'function')
    ) {
      throw new Error(
        `[rsbuild-plugin-react-router] Server build ${JSON.stringify(entryName)} route ${JSON.stringify(routeId)} action export does not match its web manifest.`
      );
    }
  }
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev-runtime-artifacts.ts` around lines 145 - 174, The
assertBuildMatchesManifest check only verifies routes present in
ReactRouterDevManifest, so stale ServerBuild entries for deleted routes can slip
through. Update assertBuildMatchesManifest to also compare build.routes against
manifest.routes and throw when the evaluated server build contains any extra
route IDs not in the manifest, alongside the existing missing-route and
loader/action export checks. Use the existing entryName, build, manifest, and
routeId logic to keep the validation in one place.
src/dev-runtime-session.ts (1)

62-69: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Run closeBinding() after successful closes from the closing state.

Once markClosing() switches the session to closing, Line 66 stops invoking closeBinding(). A normal server.close() then moves to idle without runtime.close(), unregistering the dev runtime, or clearing compiler identity state.

Suggested fix
   const completeClose = (binding: RuntimeBinding): void => {
     if (!isCurrentBinding(binding)) {
       return;
     }
-    if (state.status === 'active') {
+    if (state.status === 'active' || state.status === 'closing') {
       closeBinding(binding);
     }
     state = { status: 'idle' };
   };

Also applies to: 170-173

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev-runtime-session.ts` around lines 62 - 69, The completeClose flow in
dev-runtime-session skips closeBinding() when the session is already in the
closing state, so successful server.close() calls never fully clean up the
runtime. Update completeClose to invoke closeBinding(binding) for the successful
closing path as well as active, while still guarding with
isCurrentBinding(binding) and preserving the transition to idle; apply the same
fix in the other matching close path around the later close handler.
src/prerender-build.ts (4)

175-177: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep prerender output paths inside clientBuildDir.

normalizedPath.split('/') can include .. or backslash segments from configured/generated prerender paths, letting dynamic/splat route paths write outside the client build directory. Validate segments and assert the resolved path remains under clientBuildDir.

🛡️ Proposed fix
 const createDataRequestPath = (
   prerenderPath: string,
   trailingSlashAwareDataRequests: boolean
 ): string => {
@@
     : `${prerenderPath.replace(/\/$/, '')}.data`;
 };
+
+const getClientBuildOutputPath = (
+  clientBuildDir: string,
+  normalizedPath: string,
+  ...extraSegments: string[]
+): string => {
+  const segments = normalizedPath.split('/').filter(Boolean);
+  if (
+    segments.some(
+      segment =>
+        segment === '.' || segment === '..' || segment.includes('\\')
+    )
+  ) {
+    throw new Error(`Invalid prerender output path: ${normalizedPath}`);
+  }
+
+  const outputPath = resolve(clientBuildDir, ...segments, ...extraSegments);
+  const relativeOutputPath = relative(clientBuildDir, outputPath);
+  if (relativeOutputPath === '..' || relativeOutputPath.startsWith('../')) {
+    throw new Error(`Invalid prerender output path: ${normalizedPath}`);
+  }
+  return outputPath;
+};
@@
-    const outputPath = resolve(clientBuildDir, ...normalizedPath.split('/'));
+    const outputPath = getClientBuildOutputPath(clientBuildDir, normalizedPath);
@@
-      const outputPath = resolve(
-        clientBuildDir,
-        ...normalizedPath.split('/'),
-        'index.html'
-      );
+      const outputPath = getClientBuildOutputPath(
+        clientBuildDir,
+        normalizedPath,
+        'index.html'
+      );
@@
-      const outputPath = resolve(clientBuildDir, ...normalizedPath.split('/'));
+      const outputPath = getClientBuildOutputPath(clientBuildDir, normalizedPath);

Also applies to: 234-240, 284-286

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/prerender-build.ts` around lines 175 - 177, The prerender write path
logic in the output path construction must prevent traversal outside
clientBuildDir. Update the path handling in prerender-build.ts around the
outputPath/mkdir/writeFile flow to validate normalizedPath segments before
joining, reject any .. or backslash-based traversal, and assert the final
resolved path stays under clientBuildDir. Apply the same safeguard to the other
prerender output write sites mentioned by the build flow helpers so all
generated files remain confined to the client build directory.

211-225: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Escape redirect Location before writing fallback HTML.

The redirect page writes Location directly into text and attributes. A route redirect with <, ", or similar characters can generate static HTML with script/markup injection.

🛡️ Proposed fix
+const escapeHtml = (value: string): string =>
+  value.replace(/[&<>"']/g, char => {
+    switch (char) {
+      case '&':
+        return '&amp;';
+      case '<':
+        return '&lt;';
+      case '>':
+        return '&gt;';
+      case '"':
+        return '&quot;';
+      default:
+        return '&`#39`;';
+    }
+  });
+
@@
       if (redirectStatusCodes.has(response.status)) {
         const location = response.headers.get('Location');
+        if (!location) {
+          throw new Error(
+            `Prerender (html): Redirect response for \`${normalizedPath}\` is missing a Location header.`
+          );
+        }
+        const escapedLocation = escapeHtml(location);
         const delay = response.status === 302 ? 2 : 0;
         html = `<!doctype html>
 <head>
-<title>Redirecting to: ${location}</title>
-<meta http-equiv="refresh" content="${delay};url=${location}">
+<title>Redirecting to: ${escapedLocation}</title>
+<meta http-equiv="refresh" content="${delay};url=${escapedLocation}">
 <meta name="robots" content="noindex">
 </head>
 <body>
-\t<a href="${location}">
-    Redirecting from <code>${normalizedPath}</code> to <code>${location}</code>
+\t<a href="${escapedLocation}">
+    Redirecting from <code>${escapeHtml(normalizedPath)}</code> to <code>${escapedLocation}</code>
   </a>
 </body>
 </html>`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

const escapeHtml = (value: string): string =>
  value.replace(/[&<>"']/g, char => {
    switch (char) {
      case '&':
        return '&amp;';
      case '<':
        return '&lt;';
      case '>':
        return '&gt;';
      case '"':
        return '&quot;';
      default:
        return '&`#39`;';
    }
  });

      if (redirectStatusCodes.has(response.status)) {
        const location = response.headers.get('Location');
        if (!location) {
          throw new Error(
            `Prerender (html): Redirect response for \`${normalizedPath}\` is missing a Location header.`
          );
        }
        const escapedLocation = escapeHtml(location);
        const delay = response.status === 302 ? 2 : 0;
        html = `<!doctype html>
<head>
<title>Redirecting to: ${escapedLocation}</title>
<meta http-equiv="refresh" content="${delay};url=${escapedLocation}">
<meta name="robots" content="noindex">
</head>
<body>
\t<a href="${escapedLocation}">
    Redirecting from <code>${escapeHtml(normalizedPath)}</code> to <code>${escapedLocation}</code>
  </a>
</body>
</html>`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/prerender-build.ts` around lines 211 - 225, Escape the redirect target
before interpolating it into the fallback HTML in the redirect handling block so
untrusted `Location` values cannot inject markup or attributes. Update the logic
around `redirectStatusCodes`, `response.headers.get('Location')`, and the HTML
template in `src/prerender-build.ts` to use a safely escaped/sanitized version
for the `<title>`, `meta refresh`, and `<a href>`/link text. Keep
`normalizedPath` as-is, but ensure every use of the redirect destination is
HTML- and attribute-safe.

496-498: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Consume the promise returned by the cleanup handler.

task.finally(...) creates a second promise that rejects when task rejects; because it is ignored, a prerender failure can also surface as an unhandled rejection. Use then with both handlers or catch the returned promise.

🛠️ Proposed fix
     const task = enqueue(path);
     pending.add(task);
-    task.finally(() => pending.delete(task));
+    void task.then(
+      () => pending.delete(task),
+      () => pending.delete(task)
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const task = enqueue(path);
    pending.add(task);
    void task.then(
      () => pending.delete(task),
      () => pending.delete(task)
    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/prerender-build.ts` around lines 496 - 498, The cleanup in enqueue path
handling creates an unconsumed promise from task.finally, which can surface
prerender failures as unhandled rejections; update the logic around enqueue,
pending, and task so the cleanup promise is handled explicitly. Use the task
promise’s then with both fulfillment and rejection handlers, or attach a catch
to the promise returned by finally, while still removing the task from pending.

549-607: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Restore IS_RR_BUILD_REQUEST after prerendering.

This mutates process-global state and leaves it set for buildEnd, later builds, or tests in the same process. Preserve and restore the previous value in a finally block around the build-request work.

🛠️ Proposed fix
   if (!ssr || isPrerenderEnabled) {
+    const previousBuildRequest = process.env.IS_RR_BUILD_REQUEST;
     process.env.IS_RR_BUILD_REQUEST = 'yes';
-    const buildModule = await import(pathToFileURL(serverBuildPath).toString());
-    const build = (await resolveServerBuildModule(
-      buildModule,
-      `Server build ${JSON.stringify(serverBuildPath)}`
-    )) as PrerenderServerBuild;
-    const requestHandler = createRequestHandler(build, 'production');
+    try {
+      const buildModule = await import(pathToFileURL(serverBuildPath).toString());
+      const build = (await resolveServerBuildModule(
+        buildModule,
+        `Server build ${JSON.stringify(serverBuildPath)}`
+      )) as PrerenderServerBuild;
+      const requestHandler = createRequestHandler(build, 'production');
 
-    if (isPrerenderEnabled) {
+      if (isPrerenderEnabled) {
         // existing prerender logic
-    }
+      }
 
-    if (!ssr) {
+      if (!ssr) {
         await handleSpaMode({
           handler: requestHandler,
           build,
           clientBuildDir,
           basename,
           api,
         });
+      }
+    } finally {
+      if (previousBuildRequest === undefined) {
+        delete process.env.IS_RR_BUILD_REQUEST;
+      } else {
+        process.env.IS_RR_BUILD_REQUEST = previousBuildRequest;
       }
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  if (!ssr || isPrerenderEnabled) {
    const previousBuildRequest = process.env.IS_RR_BUILD_REQUEST;
    process.env.IS_RR_BUILD_REQUEST = 'yes';
    try {
      const buildModule = await import(pathToFileURL(serverBuildPath).toString());
      const build = (await resolveServerBuildModule(
        buildModule,
        `Server build ${JSON.stringify(serverBuildPath)}`
      )) as PrerenderServerBuild;
      const requestHandler = createRequestHandler(build, 'production');

      if (isPrerenderEnabled) {
        if (!ssr) {
          const generated = latestBrowserManifest
            ? {
                manifest: latestBrowserManifest,
                moduleExportsByRouteId: latestBrowserManifestModuleExports,
              }
            : await generateReactRouterManifestForDev(
                routes,
                pluginOptions,
                clientStats,
                appDirectory,
                assetPrefix,
                routeChunkOptions
              );
          assertValidSsrFalsePrerenderExports({
            routes,
            manifestRoutes: generated.manifest.routes,
            routeExports: generated.moduleExportsByRouteId,
            prerenderPaths,
            api,
          });
        }

        validatePrerenderPathMatches(routes, prerenderPaths);

        if (prerenderPaths.length > 0) {
          api.logger.info(
            `Prerender (html): ${prerenderPaths.length} path(s)...`
          );
        }

        await runPrerenderPaths({
          build,
          requestHandler,
          clientBuildDir,
          options,
        });
      }

      if (!ssr) {
        await handleSpaMode({
          handler: requestHandler,
          build,
          clientBuildDir,
          basename,
          api,
        });
      }
    } finally {
      if (previousBuildRequest === undefined) {
        delete process.env.IS_RR_BUILD_REQUEST;
      } else {
        process.env.IS_RR_BUILD_REQUEST = previousBuildRequest;
      }
    }
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/prerender-build.ts` around lines 549 - 607, The prerender/build-request
flow in prerender-build.ts leaves process.env.IS_RR_BUILD_REQUEST mutated after
the work completes. In the block around resolveServerBuildModule,
createRequestHandler, runPrerenderPaths, and handleSpaMode, save the previous
IS_RR_BUILD_REQUEST value before setting it and restore it in a finally block
after all prerender/Spa handling finishes. This should be done around the
existing build request logic so buildEnd and subsequent runs/tests see the
original process-global state.
src/route-chunks.ts (1)

211-216: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude directive prologues from shared side-effect blocking.

"use client"/"use strict" directives are ExpressionStatements, so they are added to every export’s topLevelNonModuleStatements. That makes the intersection check reject otherwise split-safe exports, and can fail splitRouteModules: "enforce" for routes that only share a directive.

Proposed fix
-      const sharedTopLevelSideEffects = (module.ast as AnyNode).body.filter(
+      const sharedTopLevelSideEffects = (module.ast as AnyNode).body.filter(
         (statement: AnyNode) =>
           (statement.type === 'ImportDeclaration' &&
             statement.specifiers.length === 0) ||
           statement.type === 'ExpressionStatement'
       );
       for (const statement of sharedTopLevelSideEffects) {
         dependencies.topLevelStatements.add(statement);
-        dependencies.topLevelNonModuleStatements.add(statement);
+        if (statement.type !== 'ExpressionStatement' || !statement.directive) {
+          dependencies.topLevelNonModuleStatements.add(statement);
+        }
       }

Also applies to: 329-332

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/route-chunks.ts` around lines 211 - 216, The shared side-effect
collection in route chunking is incorrectly treating directive prologues like
"use client" and "use strict" as blocking statements. Update the filtering logic
in the shared side-effect scan and the related `topLevelNonModuleStatements`
handling so `ExpressionStatement`s that are directive prologues are excluded,
while real side-effectful expressions remain included. Use the existing
`sharedTopLevelSideEffects` and export analysis paths to ensure
`splitRouteModules: "enforce"` only rejects genuinely shared side effects.
src/route-export-resolution.ts (1)

160-182: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant regions with line numbers.
ast-grep outline src/route-export-resolution.ts --view expanded || true
wc -l src/route-export-resolution.ts
sed -n '1,280p' src/route-export-resolution.ts | cat -n

# Locate the specific functions and their call sites.
rg -n "resolvePackageExports|resolveExportAllModule|collectClientOnlyStubExportNames|PACKAGE_RESOLUTION_NOT_APPLICABLE|createRequire|packageSubpath" src/route-export-resolution.ts src -S

Repository: rstackjs/rsbuild-plugin-react-router

Length of output: 14053


Package subpath resolution misses common exports cases.

resolvePackageExports() ignores packageSubpath when "exports" is absent, so pkg/subpath resolves to the package entry instead of the submodule. The "exports" branch also only does exact-key lookups, so pattern exports like "./*" return null, and resolveExportAllModule() treats that as final instead of falling back to createRequire(...).resolve(). This can produce the wrong stub surface or reject valid imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/route-export-resolution.ts` around lines 160 - 182, Update
resolvePackageExports and related callers so subpath imports are handled
correctly: when packageJson.exports is absent, do not always fall back to
module/main for every packageSubpath, and make sure pkg/subpath is not resolved
as the package entry. In the exports branch, enhance the lookup logic in
resolvePackageExports to support non-exact export shapes such as pattern
mappings like "./*" instead of only direct packageSubpath keys. Then adjust
resolveExportAllModule so a null result from resolvePackageExports is treated as
“not matched yet” and it can still fall back to createRequire(...).resolve()
before rejecting the import.
src/typegen.ts (1)

22-30: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don't swallow unexpected typegen --watch exits.

This catch path ignores every rejection. If the watcher exits because the CLI crashes or the route config is temporarily invalid, typegen is dead for the rest of the dev session and nothing reports that state. Only suppress the intentional shutdown path; unexpected exits should be logged or failed loudly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/typegen.ts` around lines 22 - 30, The `typegen --watch` promise handling
in `typegenProcess` is swallowing every rejection, which hides unexpected
watcher exits. Update the `.catch` path so it only ignores the intentional
shutdown case from server teardown, and for any other rejection in
`src/typegen.ts` surface the failure by logging or rethrowing it. Use the
existing `typegenProcess` cleanup in `.finally()` to keep the process reset
behavior intact while making crashes or invalid config failures visible.
src/virtual-modules.ts (1)

11-19: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject backslash path separators in virtual module ids.

The current traversal check only splits on /, so virtual/react-router/foo\..\bar passes validation but can be interpreted as traversal on Windows-style paths. Reject \ inside segments before constructing the node_modules/... path.

🛡️ Proposed fix
   if (
     !relativeId ||
-    segments.some(segment => !segment || segment === '.' || segment === '..')
+    segments.some(
+      segment =>
+        !segment ||
+        segment === '.' ||
+        segment === '..' ||
+        segment.includes('\\')
+    )
   ) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  const segments = relativeId.split('/');
  if (
    !relativeId ||
    segments.some(
      segment =>
        !segment ||
        segment === '.' ||
        segment === '..' ||
        segment.includes('\\')
    )
  ) {
    throw new Error(`Invalid virtual module id: ${moduleId}`);
  }

  return `node_modules/${moduleId}.js`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/virtual-modules.ts` around lines 11 - 19, The virtual module ID
validation in the module path builder does not block backslash separators, so a
Windows-style traversal pattern can slip through. Update the validation in the
relativeId/segments check to reject any segment containing backslashes before
returning the node_modules path, keeping the existing dot-segment and
empty-segment checks in place.

@ScriptedAlchemy ScriptedAlchemy force-pushed the codex/parallel-dev-compilers branch from 6ad99ea to e16d85f Compare June 27, 2026 00:49
@ScriptedAlchemy ScriptedAlchemy changed the base branch from perf/bundling-performance to main June 27, 2026 01:09
Comment thread scripts/benchmark/fixture.mjs Fixed
Comment thread scripts/benchmark/fixture.mjs Fixed
@ScriptedAlchemy ScriptedAlchemy force-pushed the codex/parallel-dev-compilers branch from dff4b2a to 559bb2e Compare June 27, 2026 01:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/bench-builds.mjs`:
- Around line 256-283: The benchmark timing in the child process lifecycle is
measuring shutdown latency instead of pure readiness time. Update the
spawn/finish flow in the benchmark helper so the readiness timestamp is captured
when the server becomes ready (around the ready-marking logic) and have the
resolved result use that recorded start-to-ready duration, while keeping
finish() only for cleanup/settlement. Use the existing child spawn, ready, and
finish symbols to wire this in without including SIGTERM/SIGKILL teardown time
in wallMs.

In `@scripts/report-benchmark-ci.mjs`:
- Line 124: The benchmark report logic currently picks a single mode via the
report-generation path, which can hide a build-vs-dev mismatch and produce
misleading CI output. Update the comparison/reporting flow in
report-benchmark-ci.mjs so that the mode selection in the benchmark summary
checks both head and base modes explicitly, and reject the report when they
differ instead of defaulting through head.mode/base.mode. Use the existing
benchmark result handling around the mode field in the reporting script to
surface the mismatch before generating the final CI report.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 547b74ae-d610-4bf7-964c-5fc1e235c7a5

📥 Commits

Reviewing files that changed from the base of the PR and between dff4b2a and 559bb2e.

📒 Files selected for processing (18)
  • .github/workflows/benchmark.yml
  • README.md
  • benchmarks/README.md
  • scripts/bench-builds.mjs
  • scripts/benchmark/fixture.mjs
  • scripts/report-benchmark-ci.mjs
  • scripts/test-package-interop.mjs
  • src/dev-generation.ts
  • src/dev-runtime-artifacts.ts
  • src/dev-runtime-compilation.ts
  • src/dev-runtime-controller.ts
  • src/index.ts
  • src/parallel-route-transforms.ts
  • src/types.ts
  • tests/benchmark-fixture.test.ts
  • tests/dev-runtime-controller.test.ts
  • tests/index.test.ts
  • tests/parallel-route-transforms.test.ts
✅ Files skipped from review due to trivial changes (1)
  • benchmarks/README.md
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/dev-runtime-compilation.ts
  • src/types.ts
  • scripts/test-package-interop.mjs
  • tests/benchmark-fixture.test.ts
  • src/dev-generation.ts
  • src/dev-runtime-artifacts.ts
  • src/dev-runtime-controller.ts
  • tests/dev-runtime-controller.test.ts
  • tests/index.test.ts
  • src/parallel-route-transforms.ts
  • scripts/benchmark/fixture.mjs
  • README.md
  • src/index.ts

Comment thread scripts/bench-builds.mjs Outdated
Comment thread scripts/report-benchmark-ci.mjs Outdated
@ScriptedAlchemy ScriptedAlchemy force-pushed the codex/parallel-dev-compilers branch from 559bb2e to 0c1f1e5 Compare June 27, 2026 02:43
@ScriptedAlchemy ScriptedAlchemy changed the base branch from main to perf/bundling-performance June 27, 2026 02:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 447-457: The dev-server sample still closes over currentDevServer
and mounts its middlewares/handler on a shared app, so rerunning startServer()
would keep binding requests to the first server. Update the README example
around createDevServer, startServer, and onRouteTopologyChange to either
explicitly document this as a full-process-restart flow or refactor it to use
replaceable delegates that swap the active handler without stacking stale
middleware, and make sure the text matches the serialized close-then-recreate
behavior described in the surrounding prose.

In `@src/server-utils.ts`:
- Around line 117-124: The route discovery contract is inconsistent:
`resolveServerBuildModule` currently rejects `undefined` even though the
`routeDiscovery` field is treated as optional elsewhere. Update
`isRouteDiscovery` in `server-utils.ts` to accept `undefined` as valid, or make
the `routeDiscovery` requirement consistent across the
`resolveServerBuildModule` validation path and related type checks so local
builds with no route discovery do not throw.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b8c10ff6-a873-490f-a640-aeed1b40dcca

📥 Commits

Reviewing files that changed from the base of the PR and between 559bb2e and 0c1f1e5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (125)
  • .changeset/bright-routes-run.md
  • .changeset/coherent-dev-generations.md
  • .changeset/lazy-entry-hydration.md
  • .changeset/quiet-topology-order.md
  • .changeset/sharp-routes-heal.md
  • .changeset/warm-watchers-rest.md
  • README.md
  • benchmarks/README.md
  • config/package.json
  • config/rslib.config.ts
  • docs/coherent-dev-generations.md
  • examples/client-only/package.json
  • examples/cloudflare/package.json
  • examples/custom-node-server/app/load-context.d.ts
  • examples/custom-node-server/app/routes/projects/edit.tsx
  • examples/custom-node-server/app/routes/projects/index.tsx
  • examples/custom-node-server/app/routes/projects/layout.tsx
  • examples/custom-node-server/app/routes/projects/project.tsx
  • examples/custom-node-server/app/routes/projects/settings.tsx
  • examples/custom-node-server/package.json
  • examples/custom-node-server/playwright.config.ts
  • examples/custom-node-server/rsbuild.config.ts
  • examples/custom-node-server/scripts/smoke-production.mjs
  • examples/custom-node-server/server.js
  • examples/custom-node-server/server/index.ts
  • examples/custom-node-server/tsconfig.json
  • examples/default-template/app/dev-routes.ts
  • examples/default-template/app/routes.ts
  • examples/default-template/package.json
  • examples/default-template/playwright.config.ts
  • examples/default-template/react-router.config.ts
  • examples/default-template/rsbuild.config.ts
  • examples/default-template/tests/e2e/dev-route-watch.test.ts
  • examples/default-template/tests/e2e/lazy-compilation.test.ts
  • examples/epic-stack/package.json
  • examples/federation/epic-stack-remote/package.json
  • examples/federation/epic-stack/package.json
  • examples/prerender/package.json
  • examples/spa-mode/package.json
  • package.json
  • rslib.config.ts
  • scripts/bench-builds.mjs
  • scripts/benchmark/fixture.mjs
  • scripts/compare-benchmarks.mjs
  • scripts/test-package-interop.mjs
  • src/babel.ts
  • src/bounded-cache.ts
  • src/build-manifest.ts
  • src/build-output-transforms.ts
  • src/concurrency.ts
  • src/constants.ts
  • src/dev-generation.ts
  • src/dev-runtime-artifacts.ts
  • src/dev-runtime-compilation.ts
  • src/dev-runtime-controller.ts
  • src/dev-runtime-session.ts
  • src/dev-server.ts
  • src/export-utils.ts
  • src/index.ts
  • src/lazy-compilation.ts
  • src/manifest.ts
  • src/modify-browser-manifest.ts
  • src/parallel-route-transform-protocol.ts
  • src/parallel-route-transform-worker.ts
  • src/parallel-route-transforms.ts
  • src/performance.ts
  • src/plugin-utils.ts
  • src/prerender-build.ts
  • src/prerender.ts
  • src/react-router-config.ts
  • src/route-artifacts.ts
  • src/route-ast.ts
  • src/route-chunks.ts
  • src/route-component-transform.ts
  • src/route-export-pruning.ts
  • src/route-export-resolution.ts
  • src/route-transform-tasks.ts
  • src/route-watch.ts
  • src/server-build-plan.ts
  • src/server-utils.ts
  • src/templates/entry.server.tsx
  • src/typegen.ts
  • src/types.ts
  • src/virtual-modules.ts
  • src/warnings/warn-on-client-source-maps.ts
  • src/yuku.ts
  • tests/benchmark-fixture.test.ts
  • tests/bounded-cache.test.ts
  • tests/build-manifest.test.ts
  • tests/build-output-transforms.test.ts
  • tests/client-modules.test.ts
  • tests/dev-generation-multi-entry.test.ts
  • tests/dev-generation.test.ts
  • tests/dev-runtime-controller.test.ts
  • tests/dev-runtime.integration.test.ts
  • tests/dev-server.test.ts
  • tests/export-utils.test.ts
  • tests/features.test.ts
  • tests/fixtures/dev-runtime/app/entry.client.tsx
  • tests/fixtures/dev-runtime/app/entry.server.tsx
  • tests/fixtures/dev-runtime/app/root.tsx
  • tests/fixtures/dev-runtime/app/routes.ts
  • tests/fixtures/dev-runtime/app/routes/index.tsx
  • tests/fixtures/dev-runtime/app/routes/other.tsx
  • tests/fixtures/dev-runtime/react-router.config.ts
  • tests/fixtures/dev-runtime/server/index.ts
  • tests/index.test.ts
  • tests/lazy-compilation.test.ts
  • tests/manifest-split-route-modules.test.ts
  • tests/manifest.test.ts
  • tests/modify-browser-manifest.test.ts
  • tests/parallel-route-transforms.test.ts
  • tests/performance.test.ts
  • tests/plugin-utils.test.ts
  • tests/prerender.test.ts
  • tests/react-router-config.test.ts
  • tests/remove-exports.test.ts
  • tests/route-artifacts.test.ts
  • tests/route-chunks-cache.test.ts
  • tests/route-chunks.test.ts
  • tests/route-watch.test.ts
  • tests/server-build-plan.test.ts
  • tests/server-utils.test.ts
  • tests/setup.ts
  • tests/warn-on-client-source-maps.test.ts
💤 Files with no reviewable changes (10)
  • tests/plugin-utils.test.ts
  • tests/react-router-config.test.ts
  • src/babel.ts
  • tests/manifest-split-route-modules.test.ts
  • tests/prerender.test.ts
  • tests/performance.test.ts
  • examples/custom-node-server/server/index.ts
  • tests/modify-browser-manifest.test.ts
  • tests/manifest.test.ts
  • tests/parallel-route-transforms.test.ts
✅ Files skipped from review due to trivial changes (16)
  • tests/fixtures/dev-runtime/app/routes/index.tsx
  • examples/custom-node-server/app/routes/projects/index.tsx
  • examples/custom-node-server/app/routes/projects/layout.tsx
  • config/package.json
  • .changeset/warm-watchers-rest.md
  • .changeset/bright-routes-run.md
  • .changeset/lazy-entry-hydration.md
  • .changeset/coherent-dev-generations.md
  • .changeset/sharp-routes-heal.md
  • src/warnings/warn-on-client-source-maps.ts
  • examples/federation/epic-stack-remote/package.json
  • .changeset/quiet-topology-order.md
  • examples/custom-node-server/app/routes/projects/project.tsx
  • benchmarks/README.md
  • examples/client-only/package.json
  • docs/coherent-dev-generations.md
🚧 Files skipped from review as they are similar to previous changes (83)
  • tests/build-manifest.test.ts
  • tests/fixtures/dev-runtime/app/root.tsx
  • src/parallel-route-transform-protocol.ts
  • examples/custom-node-server/tsconfig.json
  • tests/fixtures/dev-runtime/react-router.config.ts
  • rslib.config.ts
  • tests/fixtures/dev-runtime/app/entry.server.tsx
  • examples/default-template/app/routes.ts
  • tests/fixtures/dev-runtime/app/routes/other.tsx
  • tests/fixtures/dev-runtime/app/routes.ts
  • examples/custom-node-server/app/load-context.d.ts
  • examples/spa-mode/package.json
  • tests/fixtures/dev-runtime/app/entry.client.tsx
  • src/virtual-modules.ts
  • tests/export-utils.test.ts
  • examples/prerender/package.json
  • examples/custom-node-server/rsbuild.config.ts
  • examples/federation/epic-stack/package.json
  • examples/custom-node-server/app/routes/projects/settings.tsx
  • examples/default-template/app/dev-routes.ts
  • examples/custom-node-server/app/routes/projects/edit.tsx
  • examples/default-template/tests/e2e/dev-route-watch.test.ts
  • src/bounded-cache.ts
  • examples/cloudflare/package.json
  • src/templates/entry.server.tsx
  • examples/default-template/rsbuild.config.ts
  • src/concurrency.ts
  • src/types.ts
  • tests/fixtures/dev-runtime/server/index.ts
  • examples/custom-node-server/playwright.config.ts
  • src/typegen.ts
  • examples/epic-stack/package.json
  • src/build-manifest.ts
  • examples/default-template/react-router.config.ts
  • scripts/compare-benchmarks.mjs
  • tests/dev-server.test.ts
  • src/lazy-compilation.ts
  • examples/default-template/package.json
  • src/dev-server.ts
  • tests/bounded-cache.test.ts
  • src/yuku.ts
  • src/constants.ts
  • examples/custom-node-server/package.json
  • src/parallel-route-transform-worker.ts
  • src/dev-runtime-compilation.ts
  • tests/dev-generation-multi-entry.test.ts
  • src/server-build-plan.ts
  • src/route-ast.ts
  • examples/custom-node-server/server.js
  • src/dev-runtime-session.ts
  • src/route-artifacts.ts
  • examples/custom-node-server/scripts/smoke-production.mjs
  • tests/build-output-transforms.test.ts
  • src/performance.ts
  • scripts/test-package-interop.mjs
  • src/dev-runtime-artifacts.ts
  • tests/client-modules.test.ts
  • examples/default-template/playwright.config.ts
  • src/dev-generation.ts
  • tests/lazy-compilation.test.ts
  • src/route-export-resolution.ts
  • src/route-component-transform.ts
  • package.json
  • src/react-router-config.ts
  • config/rslib.config.ts
  • src/prerender.ts
  • src/route-export-pruning.ts
  • src/export-utils.ts
  • tests/index.test.ts
  • src/build-output-transforms.ts
  • src/route-watch.ts
  • src/parallel-route-transforms.ts
  • src/route-transform-tasks.ts
  • tests/dev-generation.test.ts
  • src/plugin-utils.ts
  • examples/default-template/tests/e2e/lazy-compilation.test.ts
  • src/manifest.ts
  • tests/features.test.ts
  • src/modify-browser-manifest.ts
  • tests/dev-runtime-controller.test.ts
  • src/prerender-build.ts
  • src/route-chunks.ts
  • src/index.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 447-457: The dev-server sample still closes over currentDevServer
and mounts its middlewares/handler on a shared app, so rerunning startServer()
would keep binding requests to the first server. Update the README example
around createDevServer, startServer, and onRouteTopologyChange to either
explicitly document this as a full-process-restart flow or refactor it to use
replaceable delegates that swap the active handler without stacking stale
middleware, and make sure the text matches the serialized close-then-recreate
behavior described in the surrounding prose.

In `@src/server-utils.ts`:
- Around line 117-124: The route discovery contract is inconsistent:
`resolveServerBuildModule` currently rejects `undefined` even though the
`routeDiscovery` field is treated as optional elsewhere. Update
`isRouteDiscovery` in `server-utils.ts` to accept `undefined` as valid, or make
the `routeDiscovery` requirement consistent across the
`resolveServerBuildModule` validation path and related type checks so local
builds with no route discovery do not throw.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b8c10ff6-a873-490f-a640-aeed1b40dcca

📥 Commits

Reviewing files that changed from the base of the PR and between 559bb2e and 0c1f1e5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (125)
  • .changeset/bright-routes-run.md
  • .changeset/coherent-dev-generations.md
  • .changeset/lazy-entry-hydration.md
  • .changeset/quiet-topology-order.md
  • .changeset/sharp-routes-heal.md
  • .changeset/warm-watchers-rest.md
  • README.md
  • benchmarks/README.md
  • config/package.json
  • config/rslib.config.ts
  • docs/coherent-dev-generations.md
  • examples/client-only/package.json
  • examples/cloudflare/package.json
  • examples/custom-node-server/app/load-context.d.ts
  • examples/custom-node-server/app/routes/projects/edit.tsx
  • examples/custom-node-server/app/routes/projects/index.tsx
  • examples/custom-node-server/app/routes/projects/layout.tsx
  • examples/custom-node-server/app/routes/projects/project.tsx
  • examples/custom-node-server/app/routes/projects/settings.tsx
  • examples/custom-node-server/package.json
  • examples/custom-node-server/playwright.config.ts
  • examples/custom-node-server/rsbuild.config.ts
  • examples/custom-node-server/scripts/smoke-production.mjs
  • examples/custom-node-server/server.js
  • examples/custom-node-server/server/index.ts
  • examples/custom-node-server/tsconfig.json
  • examples/default-template/app/dev-routes.ts
  • examples/default-template/app/routes.ts
  • examples/default-template/package.json
  • examples/default-template/playwright.config.ts
  • examples/default-template/react-router.config.ts
  • examples/default-template/rsbuild.config.ts
  • examples/default-template/tests/e2e/dev-route-watch.test.ts
  • examples/default-template/tests/e2e/lazy-compilation.test.ts
  • examples/epic-stack/package.json
  • examples/federation/epic-stack-remote/package.json
  • examples/federation/epic-stack/package.json
  • examples/prerender/package.json
  • examples/spa-mode/package.json
  • package.json
  • rslib.config.ts
  • scripts/bench-builds.mjs
  • scripts/benchmark/fixture.mjs
  • scripts/compare-benchmarks.mjs
  • scripts/test-package-interop.mjs
  • src/babel.ts
  • src/bounded-cache.ts
  • src/build-manifest.ts
  • src/build-output-transforms.ts
  • src/concurrency.ts
  • src/constants.ts
  • src/dev-generation.ts
  • src/dev-runtime-artifacts.ts
  • src/dev-runtime-compilation.ts
  • src/dev-runtime-controller.ts
  • src/dev-runtime-session.ts
  • src/dev-server.ts
  • src/export-utils.ts
  • src/index.ts
  • src/lazy-compilation.ts
  • src/manifest.ts
  • src/modify-browser-manifest.ts
  • src/parallel-route-transform-protocol.ts
  • src/parallel-route-transform-worker.ts
  • src/parallel-route-transforms.ts
  • src/performance.ts
  • src/plugin-utils.ts
  • src/prerender-build.ts
  • src/prerender.ts
  • src/react-router-config.ts
  • src/route-artifacts.ts
  • src/route-ast.ts
  • src/route-chunks.ts
  • src/route-component-transform.ts
  • src/route-export-pruning.ts
  • src/route-export-resolution.ts
  • src/route-transform-tasks.ts
  • src/route-watch.ts
  • src/server-build-plan.ts
  • src/server-utils.ts
  • src/templates/entry.server.tsx
  • src/typegen.ts
  • src/types.ts
  • src/virtual-modules.ts
  • src/warnings/warn-on-client-source-maps.ts
  • src/yuku.ts
  • tests/benchmark-fixture.test.ts
  • tests/bounded-cache.test.ts
  • tests/build-manifest.test.ts
  • tests/build-output-transforms.test.ts
  • tests/client-modules.test.ts
  • tests/dev-generation-multi-entry.test.ts
  • tests/dev-generation.test.ts
  • tests/dev-runtime-controller.test.ts
  • tests/dev-runtime.integration.test.ts
  • tests/dev-server.test.ts
  • tests/export-utils.test.ts
  • tests/features.test.ts
  • tests/fixtures/dev-runtime/app/entry.client.tsx
  • tests/fixtures/dev-runtime/app/entry.server.tsx
  • tests/fixtures/dev-runtime/app/root.tsx
  • tests/fixtures/dev-runtime/app/routes.ts
  • tests/fixtures/dev-runtime/app/routes/index.tsx
  • tests/fixtures/dev-runtime/app/routes/other.tsx
  • tests/fixtures/dev-runtime/react-router.config.ts
  • tests/fixtures/dev-runtime/server/index.ts
  • tests/index.test.ts
  • tests/lazy-compilation.test.ts
  • tests/manifest-split-route-modules.test.ts
  • tests/manifest.test.ts
  • tests/modify-browser-manifest.test.ts
  • tests/parallel-route-transforms.test.ts
  • tests/performance.test.ts
  • tests/plugin-utils.test.ts
  • tests/prerender.test.ts
  • tests/react-router-config.test.ts
  • tests/remove-exports.test.ts
  • tests/route-artifacts.test.ts
  • tests/route-chunks-cache.test.ts
  • tests/route-chunks.test.ts
  • tests/route-watch.test.ts
  • tests/server-build-plan.test.ts
  • tests/server-utils.test.ts
  • tests/setup.ts
  • tests/warn-on-client-source-maps.test.ts
💤 Files with no reviewable changes (10)
  • tests/plugin-utils.test.ts
  • tests/react-router-config.test.ts
  • src/babel.ts
  • tests/manifest-split-route-modules.test.ts
  • tests/prerender.test.ts
  • tests/performance.test.ts
  • examples/custom-node-server/server/index.ts
  • tests/modify-browser-manifest.test.ts
  • tests/manifest.test.ts
  • tests/parallel-route-transforms.test.ts
✅ Files skipped from review due to trivial changes (16)
  • tests/fixtures/dev-runtime/app/routes/index.tsx
  • examples/custom-node-server/app/routes/projects/index.tsx
  • examples/custom-node-server/app/routes/projects/layout.tsx
  • config/package.json
  • .changeset/warm-watchers-rest.md
  • .changeset/bright-routes-run.md
  • .changeset/lazy-entry-hydration.md
  • .changeset/coherent-dev-generations.md
  • .changeset/sharp-routes-heal.md
  • src/warnings/warn-on-client-source-maps.ts
  • examples/federation/epic-stack-remote/package.json
  • .changeset/quiet-topology-order.md
  • examples/custom-node-server/app/routes/projects/project.tsx
  • benchmarks/README.md
  • examples/client-only/package.json
  • docs/coherent-dev-generations.md
🚧 Files skipped from review as they are similar to previous changes (83)
  • tests/build-manifest.test.ts
  • tests/fixtures/dev-runtime/app/root.tsx
  • src/parallel-route-transform-protocol.ts
  • examples/custom-node-server/tsconfig.json
  • tests/fixtures/dev-runtime/react-router.config.ts
  • rslib.config.ts
  • tests/fixtures/dev-runtime/app/entry.server.tsx
  • examples/default-template/app/routes.ts
  • tests/fixtures/dev-runtime/app/routes/other.tsx
  • tests/fixtures/dev-runtime/app/routes.ts
  • examples/custom-node-server/app/load-context.d.ts
  • examples/spa-mode/package.json
  • tests/fixtures/dev-runtime/app/entry.client.tsx
  • src/virtual-modules.ts
  • tests/export-utils.test.ts
  • examples/prerender/package.json
  • examples/custom-node-server/rsbuild.config.ts
  • examples/federation/epic-stack/package.json
  • examples/custom-node-server/app/routes/projects/settings.tsx
  • examples/default-template/app/dev-routes.ts
  • examples/custom-node-server/app/routes/projects/edit.tsx
  • examples/default-template/tests/e2e/dev-route-watch.test.ts
  • src/bounded-cache.ts
  • examples/cloudflare/package.json
  • src/templates/entry.server.tsx
  • examples/default-template/rsbuild.config.ts
  • src/concurrency.ts
  • src/types.ts
  • tests/fixtures/dev-runtime/server/index.ts
  • examples/custom-node-server/playwright.config.ts
  • src/typegen.ts
  • examples/epic-stack/package.json
  • src/build-manifest.ts
  • examples/default-template/react-router.config.ts
  • scripts/compare-benchmarks.mjs
  • tests/dev-server.test.ts
  • src/lazy-compilation.ts
  • examples/default-template/package.json
  • src/dev-server.ts
  • tests/bounded-cache.test.ts
  • src/yuku.ts
  • src/constants.ts
  • examples/custom-node-server/package.json
  • src/parallel-route-transform-worker.ts
  • src/dev-runtime-compilation.ts
  • tests/dev-generation-multi-entry.test.ts
  • src/server-build-plan.ts
  • src/route-ast.ts
  • examples/custom-node-server/server.js
  • src/dev-runtime-session.ts
  • src/route-artifacts.ts
  • examples/custom-node-server/scripts/smoke-production.mjs
  • tests/build-output-transforms.test.ts
  • src/performance.ts
  • scripts/test-package-interop.mjs
  • src/dev-runtime-artifacts.ts
  • tests/client-modules.test.ts
  • examples/default-template/playwright.config.ts
  • src/dev-generation.ts
  • tests/lazy-compilation.test.ts
  • src/route-export-resolution.ts
  • src/route-component-transform.ts
  • package.json
  • src/react-router-config.ts
  • config/rslib.config.ts
  • src/prerender.ts
  • src/route-export-pruning.ts
  • src/export-utils.ts
  • tests/index.test.ts
  • src/build-output-transforms.ts
  • src/route-watch.ts
  • src/parallel-route-transforms.ts
  • src/route-transform-tasks.ts
  • tests/dev-generation.test.ts
  • src/plugin-utils.ts
  • examples/default-template/tests/e2e/lazy-compilation.test.ts
  • src/manifest.ts
  • tests/features.test.ts
  • src/modify-browser-manifest.ts
  • tests/dev-runtime-controller.test.ts
  • src/prerender-build.ts
  • src/route-chunks.ts
  • src/index.ts
🛑 Comments failed to post (2)
README.md (1)

447-457: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This sample still binds requests to the first dev server.

The prose says onRouteTopologyChange should recreate the dev server, but this example mounts currentDevServer.middlewares and the request handler onto a shared app and closes over currentDevServer. Re-running startServer() would stack stale middleware/handlers and keep requests on the old server. Please either document this as a full-process-restart flow or rewrite the sample around replaceable delegates.

Also applies to: 459-525

🧰 Tools
🪛 LanguageTool

[style] ~450-~450: Consider replacing this word to strengthen your wording.
Context: ...tic API. The callback is a notification and is not awaited, so it can safely start ...

(AND_THAT)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 447 - 457, The dev-server sample still closes over
currentDevServer and mounts its middlewares/handler on a shared app, so
rerunning startServer() would keep binding requests to the first server. Update
the README example around createDevServer, startServer, and
onRouteTopologyChange to either explicitly document this as a
full-process-restart flow or refactor it to use replaceable delegates that swap
the active handler without stacking stale middleware, and make sure the text
matches the serialized close-then-recreate behavior described in the surrounding
prose.
src/server-utils.ts (1)

117-124: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Accept undefined route discovery or make the contract required.

Lines 23-31 allow routeDiscovery to be undefined, but Line 162 rejects that via isRouteDiscovery, causing resolveServerBuildModule to throw for an otherwise valid local build shape.

Proposed fix
 function isRouteDiscovery(value: unknown): boolean {
   return (
-    isRecord(value) &&
-    (value.mode === 'initial' ||
+    value === undefined ||
+    (isRecord(value) &&
+      (value.mode === 'initial' ||
       (value.mode === 'lazy' &&
         (value.manifestPath === undefined ||
-          typeof value.manifestPath === 'string')))
+          typeof value.manifestPath === 'string'))))
   );
 }

Also applies to: 162-162

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server-utils.ts` around lines 117 - 124, The route discovery contract is
inconsistent: `resolveServerBuildModule` currently rejects `undefined` even
though the `routeDiscovery` field is treated as optional elsewhere. Update
`isRouteDiscovery` in `server-utils.ts` to accept `undefined` as valid, or make
the `routeDiscovery` requirement consistent across the
`resolveServerBuildModule` validation path and related type checks so local
builds with no route discovery do not throw.

Base automatically changed from perf/bundling-performance to main June 29, 2026 04:19
@ScriptedAlchemy ScriptedAlchemy force-pushed the codex/parallel-dev-compilers branch from 90ea459 to 91b63d1 Compare June 29, 2026 16:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/dev-generation.ts`:
- Around line 455-490: The reusePreviousNodeBuild logic is too broad because it
can reuse previous node output even when nodeChanged is true, which can skip
evaluateServerBuilds and keep stale server code. Tighten the condition around
reusePreviousNodeBuild in src/dev-generation.ts so it only reuses the prior node
build when the node side did not change, and make shouldEvaluateNode depend on
that corrected state; use the nearby symbols reusePreviousNodeBuild,
shouldEvaluateNode, evaluateServerBuilds, and discardUnsafeOneSidedResult to
update the control flow safely.

In `@src/dev-runtime-controller.ts`:
- Around line 125-143: `finishRuntimeAttempt` currently awaits
`binding.runtime.finishAttempt(...)` without handling rejections, so the
one-sided compile path can escape instead of moving the runtime into a failure
state. Update `finishRuntimeAttempt` to mirror `flushSettledAttempt()` by
catching `finishAttempt` errors and calling `binding.runtime.failAttempt(...)`
with the same `stats`, `changes`, and `identity`, then keep the existing
`retry-node` invalidation behavior for the successful path.

In `@tests/dev-server.test.ts`:
- Around line 26-31: The dev-server test is bypassing the injected listener path
by calling requestHandler directly, so it won’t catch wiring mistakes in
createDevServerMiddleware. Update the stub around createRequestListener and
listener so the captured handler argument is invoked inside the returned
listener callback, and keep the test focused on exercising the lazy
build-provider flow through createDevServerMiddleware.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cf6f8c8e-5b1d-4348-81fa-6ef02744cd05

📥 Commits

Reviewing files that changed from the base of the PR and between 0c1f1e5 and 91b63d1.

📒 Files selected for processing (22)
  • .github/workflows/benchmark.yml
  • README.md
  • scripts/bench-builds.mjs
  • scripts/benchmark/fixture.mjs
  • scripts/report-benchmark-ci.mjs
  • scripts/test-package-interop.mjs
  • src/concurrency.ts
  • src/dev-generation.ts
  • src/dev-runtime-artifacts.ts
  • src/dev-runtime-compilation.ts
  • src/dev-runtime-controller.ts
  • src/dev-server.ts
  • src/index.ts
  • src/parallel-route-transforms.ts
  • src/server-utils.ts
  • src/types.ts
  • tests/benchmark-fixture.test.ts
  • tests/dev-runtime-controller.test.ts
  • tests/dev-server.test.ts
  • tests/index.test.ts
  • tests/parallel-route-transforms.test.ts
  • tests/server-utils.test.ts
💤 Files with no reviewable changes (2)
  • tests/server-utils.test.ts
  • tests/parallel-route-transforms.test.ts
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • scripts/benchmark/fixture.mjs
  • src/concurrency.ts
  • src/types.ts
  • scripts/test-package-interop.mjs
  • tests/dev-runtime-controller.test.ts
  • src/index.ts
  • src/dev-runtime-artifacts.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/dev-generation.ts`:
- Around line 455-490: The reusePreviousNodeBuild logic is too broad because it
can reuse previous node output even when nodeChanged is true, which can skip
evaluateServerBuilds and keep stale server code. Tighten the condition around
reusePreviousNodeBuild in src/dev-generation.ts so it only reuses the prior node
build when the node side did not change, and make shouldEvaluateNode depend on
that corrected state; use the nearby symbols reusePreviousNodeBuild,
shouldEvaluateNode, evaluateServerBuilds, and discardUnsafeOneSidedResult to
update the control flow safely.

In `@src/dev-runtime-controller.ts`:
- Around line 125-143: `finishRuntimeAttempt` currently awaits
`binding.runtime.finishAttempt(...)` without handling rejections, so the
one-sided compile path can escape instead of moving the runtime into a failure
state. Update `finishRuntimeAttempt` to mirror `flushSettledAttempt()` by
catching `finishAttempt` errors and calling `binding.runtime.failAttempt(...)`
with the same `stats`, `changes`, and `identity`, then keep the existing
`retry-node` invalidation behavior for the successful path.

In `@tests/dev-server.test.ts`:
- Around line 26-31: The dev-server test is bypassing the injected listener path
by calling requestHandler directly, so it won’t catch wiring mistakes in
createDevServerMiddleware. Update the stub around createRequestListener and
listener so the captured handler argument is invoked inside the returned
listener callback, and keep the test focused on exercising the lazy
build-provider flow through createDevServerMiddleware.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cf6f8c8e-5b1d-4348-81fa-6ef02744cd05

📥 Commits

Reviewing files that changed from the base of the PR and between 0c1f1e5 and 91b63d1.

📒 Files selected for processing (22)
  • .github/workflows/benchmark.yml
  • README.md
  • scripts/bench-builds.mjs
  • scripts/benchmark/fixture.mjs
  • scripts/report-benchmark-ci.mjs
  • scripts/test-package-interop.mjs
  • src/concurrency.ts
  • src/dev-generation.ts
  • src/dev-runtime-artifacts.ts
  • src/dev-runtime-compilation.ts
  • src/dev-runtime-controller.ts
  • src/dev-server.ts
  • src/index.ts
  • src/parallel-route-transforms.ts
  • src/server-utils.ts
  • src/types.ts
  • tests/benchmark-fixture.test.ts
  • tests/dev-runtime-controller.test.ts
  • tests/dev-server.test.ts
  • tests/index.test.ts
  • tests/parallel-route-transforms.test.ts
  • tests/server-utils.test.ts
💤 Files with no reviewable changes (2)
  • tests/server-utils.test.ts
  • tests/parallel-route-transforms.test.ts
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • scripts/benchmark/fixture.mjs
  • src/concurrency.ts
  • src/types.ts
  • scripts/test-package-interop.mjs
  • tests/dev-runtime-controller.test.ts
  • src/index.ts
  • src/dev-runtime-artifacts.ts
🛑 Comments failed to post (3)
src/dev-generation.ts (1)

455-490: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t reuse the previous node build when node also changed.

Line 455 makes CSS-only web-manifest changes reuse the previous node build even when nodeChanged is true, so lines 488-490 can skip evaluateServerBuilds and commit stale server code for a combined CSS-ownership + node rebuild.

Proposed fix
-      const reusePreviousNodeBuild = !!previous && cssOnlyWebManifestChange;
+      const reusePreviousNodeBuild =
+        !!previous && cssOnlyWebManifestChange && !nodeChanged;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      const reusePreviousNodeBuild =
        !!previous && cssOnlyWebManifestChange && !nodeChanged;
      const sameCompileAttempt =
        !!identity.webAttempt &&
        !!identity.nodeAttempt &&
        identity.webAttempt === identity.nodeAttempt;

      if (
        nodeChanged &&
        identity.nodeWeb !== webIdentity &&
        !sameCompileAttempt &&
        !reusePreviousNodeBuild
      ) {
        const message =
          '[rsbuild-plugin-react-router] Discarded web and node results from different compiler cycles and kept the last-good build.';
        if (!previous) {
          return 'retry-node';
        }
        onWarning(message);
        rejectAttempt(attemptId, new Error(message), false);
        return 'retry-node';
      }

      const shouldEvaluateNode = nodeChanged && !reusePreviousNodeBuild;
      if (
        previous &&
        webChanged !== shouldEvaluateNode &&
        !cssOnlyWebManifestChange &&
        discardUnsafeOneSidedResult(attemptId, previous, webChanged, changes)
      ) {
        return 'ignored';
      }

      try {
        const buildsByEntryName = shouldEvaluateNode
          ? await evaluateServerBuilds(server, buildPlan.entryNames)
          : previous!.buildsByEntryName;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev-generation.ts` around lines 455 - 490, The reusePreviousNodeBuild
logic is too broad because it can reuse previous node output even when
nodeChanged is true, which can skip evaluateServerBuilds and keep stale server
code. Tighten the condition around reusePreviousNodeBuild in
src/dev-generation.ts so it only reuses the prior node build when the node side
did not change, and make shouldEvaluateNode depend on that corrected state; use
the nearby symbols reusePreviousNodeBuild, shouldEvaluateNode,
evaluateServerBuilds, and discardUnsafeOneSidedResult to update the control flow
safely.
src/dev-runtime-controller.ts (1)

125-143: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle finishAttempt() rejections on the one-sided compile path.

flushSettledAttempt() already converts binding.runtime.finishAttempt() failures into binding.runtime.failAttempt(), but finishRuntimeAttempt() does not. When Line 469 takes the one-sided path, a load/evaluation error escapes onAfterDevCompile instead of transitioning the dev runtime into its failure state.

Suggested fix
   const finishRuntimeAttempt = async (
     binding: RuntimeBinding,
     pair: DevCompilerPair,
     stats: Rspack.Stats | Rspack.MultiStats,
     changes: Parameters<RuntimeBinding['runtime']['finishAttempt']>[1],
     identity: Parameters<RuntimeBinding['runtime']['finishAttempt']>[2]
   ): Promise<void> => {
-    const result = await binding.runtime.finishAttempt(
-      stats,
-      changes,
-      identity
-    );
-    if (
-      result === 'retry-node' &&
-      sessions.getActiveBinding()?.id === binding.id
-    ) {
-      pair.node.watching?.invalidate();
+    try {
+      const result = await binding.runtime.finishAttempt(
+        stats,
+        changes,
+        identity
+      );
+      if (
+        result === 'retry-node' &&
+        sessions.getActiveBinding()?.id === binding.id
+      ) {
+        pair.node.watching?.invalidate();
+      }
+    } catch (cause) {
+      if (sessions.getActiveBinding()?.id === binding.id) {
+        binding.runtime.failAttempt(
+          cause instanceof Error ? cause : new Error(String(cause))
+        );
+      }
     }
   };

Also applies to: 469-471

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev-runtime-controller.ts` around lines 125 - 143, `finishRuntimeAttempt`
currently awaits `binding.runtime.finishAttempt(...)` without handling
rejections, so the one-sided compile path can escape instead of moving the
runtime into a failure state. Update `finishRuntimeAttempt` to mirror
`flushSettledAttempt()` by catching `finishAttempt` errors and calling
`binding.runtime.failAttempt(...)` with the same `stats`, `changes`, and
`identity`, then keep the existing `retry-node` invalidation behavior for the
successful path.
tests/dev-server.test.ts (1)

26-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Drive this test through the injected listener callback.

This stub calls requestHandler directly, so the test still passes if createDevServerMiddleware() wires the wrong function into createRequestListener(). Capture the handler argument and invoke that instead so this test actually covers the lazy build-provider path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/dev-server.test.ts` around lines 26 - 31, The dev-server test is
bypassing the injected listener path by calling requestHandler directly, so it
won’t catch wiring mistakes in createDevServerMiddleware. Update the stub around
createRequestListener and listener so the captured handler argument is invoked
inside the returned listener callback, and keep the test focused on exercising
the lazy build-provider flow through createDevServerMiddleware.

Route the remaining ad-hoc error-normalization sites (dev-generation notify
handlers, worker startup/postMessage failures, route topology watcher logging)
through the shared normalizeEffectError helper, and call getBuildManifest
directly instead of re-wrapping getBuildManifestEffect in runPluginEffect.
Cap the parallel route-transform worker-affinity map with the existing
bounded-cache helper so it cannot grow unbounded across long dev sessions,
and only reschedule lazy-compilation prewarm when the prewarm-relevant
asset set actually changes (fingerprint compare in the controller's
setManifest) to avoid restarting in-flight prewarm on every recompile.
Restore error-name fidelity in the topology watcher log, refresh
route-source worker affinity entries on hit so bounded eviction behaves
LRU-like instead of FIFO, and collect prewarm route assets without
allocating every route value before the limit check.
Cold-importing effect costs ~450ms, and the worker thread graph pulled it
in through two files with trivial usage: test-only Effect wrappers in
route-artifacts and an Effect.async callback-to-promise conversion in
route-export-resolution. Replace the conversion with a plain Promise and
delete the unused wrappers so worker import drops from ~360ms to ~40ms and
small production builds stop paying a fixed startup tax.
Cold-importing the effect barrel loads ~100 re-exported modules and costs
~440ms; the plugin only uses Effect, Cause, Duration, Exit, Fiber, Option,
and Deferred, which cost ~120ms as direct subpath imports. Converting all
16 source files cuts plugin import time from ~490ms to ~300ms on every
build and dev-server start.
The SSR middleware lazily imported react-router (~67ms) and
node-fetch-server (~52ms) on the first request, adding ~120ms to the
first route load; start that import when the middleware is created and
reset on failure so the first request still surfaces errors. Also spawn
route-transform workers during plugin setup instead of onAfterCreateCompiler
so thread startup overlaps Rsbuild's own compiler creation.
Delete production-unused exports flagged by review: evaluateServerBuilds,
performance recordEffect, runBoundedPrerenderTasks, and mapWithConcurrency
(its last production caller migrated to Effect.forEach). Tests now call the
surviving Effect APIs directly. concurrency.ts and performance.ts no longer
import effect at all, trimming the plugin's module graph.
…y guard

The controller's reschedule() became production-unused once setManifest
started deduping and rescheduling internally, and the hasOwnProperty check
on the internal manifest routes object is inconsistent with the plain
for-in used elsewhere. Also stop exporting prewarmLazyCompilation, which
has no external callers.
A 4-core cpuset sweep (large-355 dev fixture, interleaved vs the merge
base) showed the single worker that small machines used to spawn never
paid for itself: dev-server ready dropped from 9.09s to 8.57s median
when the worker was disabled, while HMR, route requests, and production
builds (synthetic-256: 2.38s both ways) were unchanged. Worker startup
and per-task IPC only amortize once at least two workers can run, so
getDefaultWorkerCount now returns 0 at or below 4 cores instead of
capping at 1.
Move effect from dependencies to devDependencies so rslib's
autoExternal inlines it into the built output, and stop exporting
Effect-typed functions (resolveServerBuildModuleEffect,
resolveReactRouterServerBuildEffect, evaluateServerBuildsEffect,
generateReactRouterManifestForDevEffect) so no d.ts reachable from the
public entry imports 'effect/Effect' — consumers no longer need effect
installed for types to resolve.

Measured on this machine (load avg ~10-16, alternating runs, 5 each):
- ESM import dist/index.js: ~277ms -> ~192ms (median, -85ms)
- CJS require dist/index.cjs: ~259ms -> ~177ms (median, -82ms)
- dist size: index.js 193k -> 531k, index.cjs 250k -> 844k
- synthetic-256-ssr-esm build median: 2.36s external vs 2.31s bundled
  (within noise under background benchmark load)
- rstest 431/431 pass, tsc clean, package-interop passes, clean
  consumer typecheck (skipLibCheck false) shows no effect errors
Inline a structural RouteConfigEntry in manifest.ts instead of importing
the type from @react-router/dev/routes (a devDependency), so the d.ts
files reachable from the public entry no longer reference packages that
consumers with skipLibCheck: false may not have installed.
@ScriptedAlchemy

ScriptedAlchemy commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Performance investigation summary

Evidence backing the numbers in the PR description, measured locally with base (7a55f78) and head runs interleaved to control for machine drift. Negative deltas mean head is faster.

Core-count sweep (4 / 8 / 16 / 48 / unlimited cores)

Run with the new bench --cpu-limit flag; base and head interleaved at each core count. Measured at head 5b3357d, i.e. before c93289d — see the update below for how that commit changes the small-core picture.

Cores Dev ready HMR update Production build
4 head −19.5% base faster base faster
8 head faster ~crossover base faster
16 head faster head faster base faster
48 head faster head faster ~crossover
unlimited (100) head −13% head −19.3% head −10.8%

Takeaways:

  • Dev ready is faster at every core count, with the biggest win on small machines (−19.5% at 4 cores, where head runs transforms inline and spawns no workers).
  • The reason base degrades at high core counts: base spawns cpus − 2 transform workers (98 workers on a 100-core box). Head caps automatic workers at 2.
  • The small-core HMR/build losses in this sweep were traced to typegen scheduling and spawn cost, and are addressed by c93289d (below).

Update: typegen fixes in c93289d close the small-core gaps

c93289d makes two changes: the typegen dev watch starts only after a 10s compile-idle period (rescheduled on every compile until it starts), moving its bootstrap burst out of the HMR window; and both typegen invocations spawn the @react-router/dev bin directly via process.execPath instead of npx --yes (npx kept as fallback), cutting ~250ms+ bootstrap from every production build.

Measured at 4 cores, interleaved, medians:

Metric Before c93289d After c93289d Base
HMR update (large-355) ~2.55s 2.21s (matches typegen-disabled reference) ≈2.2s
Production build (synthetic-256) base faster 2.02–2.13s (−5%) 2.14–2.18s
Production build (synthetic-1024) base faster ~+1% (noise)
Dev ready ~8.7–9.1s unchanged
Plugin import (ESM) ~192ms 181ms (+19ms accepted fixed cost) 163ms

This also resolves the +20% HMR rollup benchmark CI reported at a463dca: that was a one-time startup collision (the deferred typegen watch's bootstrap landing in the benchmark's early update window; per-op plugin timings were ~2ms on both sides), not steady-state per-rebuild cost. With the idle-triggered start the collision is gone. Benchmark CI is re-running for c93289d.

Effect import-hygiene progression

Each step measured on this machine; times are plugin import cost unless noted.

Step Result
Effect via barrel import (import ... from "effect") barrel alone ~450ms; plugin import ~490ms
Switch to subpath imports (effect/Effect, etc.) plugin import ~490ms → ~300ms
Remove effect from the route-transform worker module graph worker spawn ~360ms → ~52ms
Bundle effect into dist (moved to devDependencies) plugin import ~181ms ESM / ~177ms CJS; consumers no longer install effect

The bundling step also removed four Effect-typed exports from the public API; the published d.ts is verified clean for strict skipLibCheck: false consumers, including the @react-router/dev/routes structural-type fix.

Remaining fixed cost vs base: ~19ms of import overhead (~181ms vs base ~163ms ESM), accepted.

…trap

Start the react-router typegen watch only after a 10s quiet period with no
dev compiles (rescheduling the delayed task on every onAfterDevCompile)
instead of a fixed 3s after the first compile, and spawn the CLI directly
via the @react-router/dev bin instead of the npx bootstrap (npx remains a
fallback when resolution fails). The one-shot build typegen also uses the
direct spawn.

At 4 cores (large-355 dev fixture, interleaved runs): HMR update dropped
from 2.44-2.84s (median ~2.55s across 17 iterations) to 2.11-2.24s
(median 2.21s), matching the typegen-disabled reference (~2.2s), with
dev-server ready unchanged (~8.6-9.1s). Direct spawn also shaves the npx
bootstrap from one-shot typegen: 0.83s vs 1.08s wall, 1.16s vs 1.41s CPU
at 4 cores, which runs in every production build.
@ScriptedAlchemy ScriptedAlchemy changed the title perf: consolidate dev startup optimizations perf: parallel dev compilers, worker-pool route transforms, and startup/HMR optimizations Jul 2, 2026
Collapse registerReactRouterTypegen's positional optionals into an
options object, fold the Effect.sync/zipRight ceremony into the
tryPluginPromise thunk, document the npx-fallback catch, un-export the
internal idle-delay constant, drop the duplicated assetPrefix default
on the private manifest Effect function, align the bench dev branch
with the build branch's withCpuLimit spread, and widen the idle-defer
test's timing margins.
…hines

The 4-core CI runner showed synthetic-1024-ssr-esm build wall time at +5-6%
vs the merge base (pooled medians 4459ms vs 4229ms over four interleaved
rounds) while dev fixtures improved. The small-machine inline fallback from
8fafc8f was tuned on the dev path, where a lone worker never amortized its
startup/IPC cost, but at 1024 routes a production build pushes ~6k transform
tasks through the loader and two workers beat inline decisively (forced
2-worker A/B: 4080-4109ms). getDefaultWorkerCount now only disables workers
on small machines in dev; builds keep the default worker count. Re-measured
after the fix: pooled median 4033ms, -4.6% vs base, and synthetic-256 build
stays ahead (1978ms vs 2509ms base).
…lookup

resolveDirectTypegenCommand re-implemented the resolve-package-json-from-a-
directory pattern that ssr-externals already had. Export resolvePackageJson
and reuse it; the npx fallback still kicks in whenever @react-router/dev is
not resolvable from the app directory.
…round-trip

startServerBuildEvaluationEffect wrapped the Promise-based
resolveServerBuildModule, which itself runs an Effect, so every dev server
build evaluation crossed the Effect/Promise boundary twice. The Effect-based
resolution now lives in an internal server-build-resolution module that
dev-runtime-artifacts composes directly; server-utils keeps the public
Promise wrappers so the published declaration graph stays free of effect
types (re-verified by walking dist/index.d.ts imports).
Aligns the last two test files with the subpath-import convention used in
src/ so the barrel pattern does not get copied back into runtime code.
@ScriptedAlchemy ScriptedAlchemy merged commit fde856e into main Jul 2, 2026
7 checks passed
@ScriptedAlchemy ScriptedAlchemy deleted the codex/parallel-dev-compilers branch July 2, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants