Skip to content

refactor(compiler)!: replace Babel with a Yuku-based compiler core for Router and Start - #8817

Draft
Sheraff wants to merge 1336 commits into
mainfrom
compiler-rewrite/yuku
Draft

Sheraff wants to merge 1336 commits into
mainfrom
compiler-rewrite/yuku

Conversation

@Sheraff

@Sheraff Sheraff commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Based on the compiler oracle test suite branch (compiler-tests/baseline), which is included in this diff. That branch adds the oracle (packages/compiler-test-utils) and pins main's bugs as .fails; this PR replaces the compilers and turns the pins green.

Reading order: core README (packages/router-utils/src/compiler/README.md) → planners → hosts → tests. Details in How to review.

Summary

Every TanStack build-time compiler now runs on one shared core built on Yuku: route code splitting and route HMR (@tanstack/router-plugin), and all of Start's transforms (server functions, environment separation, <Hydrate>, RSC CSS, import protection). The route generator is rewritten on Yuku too. Babel, babel-dead-code-elimination and the Prettier hot path are gone.

Why:

  • Speed. On main, Babel was ~90% of compiler time and 50–60% of all main-thread time in dev and builds. On a 333-route app, Vite and Rsbuild builds are now 2.1–4.6× faster (webpack 1.5×) and dev cold start 1.9–4.0× faster.
  • Correctness. Main's compilers re-parsed and re-printed each other's output, recognised APIs by name, and kept order-dependent state across transforms. 29 open issues are fixed, and the compiler oracle's known-bug pins went from 874 to 45. 36 of those 45 keep main's behaviour on purpose: root routes are not split, and route CSS loads with the route module.
  • One model. One parse and one plan per module and environment, with every output emitted from the original source and exact source maps. Split chunks, Hydrate chunks and server-fn providers no longer depend on the order the bundler asks for them.

Architecture in brief

Full map: packages/router-utils/src/compiler/README.md.

  • One analysis per (env, module, text) → one plan → many outputs: the env module plus derived modules (?tsr-split=…, ?tss-hydrate=…, ?tss-serverfn-split) and facts (import edges, server-fn registry, diagnostics, dependencies). Each job is a planner that adds edits and roots to the shared plan, in a fixed order: start-env → hydrate → rsc-css → route-hmr → route-split → import-edges.
  • Rewrite tree over the original UTF-16 ranges (retained slices, structured glue, claims). User code is never re-printed.
  • Reachability = splitting = dead-code removal, at cluster granularity. Code is removed only when our edits orphaned it (provenance), so user side effects never disappear.
  • Emit through our own OutputBuilder (direct VLQ maps, composed with input maps).
  • Recognition by binding with an abstract value domain. Cross-module summaries are built from raw text, never through the bundler's transform pipeline.
  • Plan cache + dependency ledger + one invalidate(): validated reuse, per-env resolvers, no Yuku objects retained (heap test).
  • Hosts we own: a native Vite plugin, a Rspack/webpack loader, and Start Rsbuild loaders per env/layer with buildInfo replay for the persistent cache. unplugin is used only for the generator and esbuild.

Results

Compiler oracle (packages/compiler-test-utils/oracle, real bundlers/dev servers/browsers, public APIs only)

baseline (main's compilers, 80d1890bed) this branch
tests 3,594 4,114
known-bug pins (.fails) 874 45
non-pin failures 0 0

Heads: router-plugin and start-plugin-core (unit + integration, 3,645 tests, all 45 pins) ran at d0cb0e3ff6. router-generator and react-start-rsc (469 tests, 0 pins) ran at c4cb7e889a; since then only router-generator's test harness changed, and its integration tests also pass at d0cb0e3ff6. fdc48254c8 adds 2 passing tests (per-environment release, below) and changes no other result.

The 45 remaining pins:

# pin why it remains
20 route CSS applies only once the route matches (shows-component / -error / -not-found / -pending / -nothing; Vite + Rsbuild, dev + build) Unchanged from main on purpose: bare style imports of a route load eagerly with the route module. Loading route CSS on match needs a router-core per-route asset loader (F2)
16 a root route's errorComponent / notFoundComponent loads lazily (Vite, Rspack, webpack, Rsbuild; with and without sideEffects) Root routes are never split, as on main. Splitting them cost SSR CPU on 404/error renders, and the manifest preloaded the chunk on every page anyway
3 rspack/webpack/rsbuild without sideEffects: an exported component another route's chunk imports loads with its chunk webpack-family chunk graph: only a side-effect-free package lets Rspack bypass the eager route module. The fix is a cross-module optimisation, not done
2 Rsbuild dev: a module of a custom type (.page as javascript/auto) importing a denied file/specifier Rspack rules select by resource, not module type. A catch-all loader would run on every asset
1 Rsbuild build: an unused exported component with a boundary is not in the client Rspack drops the unused declaration but still emits the async chunk of the import() inside it (unreferenced file)
1 Rsbuild dev: an edit that adds a route's first server fn to its component shows on the open page Upstream (Rspack's HMR runtime re-executes the updated chunk before the new async chunk it needs is loaded; web-infra-dev/rspack#11324, #11922). A plain Rsbuild app fails the same way
1 Vite dev: a Start library's import of a CommonJS dependency in a client route excluded libraries' undeclared CJS deps are not pre-bundled. Not supported for now
1 Vite bundledDev: a factory that becomes a server-fn builder makes the client drop the handler needs Vite full-bundle API to re-transform importers. Open

Unit tiers: router-utils core tests (pipeline, placement, rewrite, output-builder, values, facts, edges, identity, heap) and the planner tests in start-plugin-core.

e2e

CI (PR workflow, Test job = test:pr: affected lint, unit, integration, e2e, types and build targets) at 6b90fe7291: 885/886 Nx tasks pass. The one failure, vue-start dev-ssr-styles (dev), was Vite's dependency-optimizer reload navigating the page away mid-test. It is fixed since (Vue Start cold-start fix, below), and the test passes locally.

Compiler Platforms (Linux x64 musl, Linux arm64, macOS arm64, Windows x64) is green at 6b90fe7291 and d0cb0e3ff6. CodSpeed at d0cb0e3ff6: the SSR assets regressions are gone (route CSS loads eagerly, as on main). The remaining deltas are small memory benchmarks (±1–2.5 KB).

Performance (c4cb7e889a vs main da2adec76d; harness benchmarks/compiler-perf, 15 cells, 1 warmup + 5 runs, interleaved cell by cell)

Median ms (memory: MB), main → this PR. Medium = 333 route files, large = 1,485.

cell build dev listen dev cold → hydrated compile-all dev heap after GC build peak RSS
react-start Vite medium 6,196 → 1,634 627 → 212 6,146 → 2,561 7,176 → 2,038 212 → 87 1,382 → 1,238
react-start Vite large 29,007 → 8,530 1,447 → 306 23,679 → 7,592 31,251 → 9,665 704 → 200 2,901 → 1,856
react-start Rsbuild medium 6,566 → 1,949 220 → 168 7,930 → 2,279 – 218 → 106 1,271 → 1,166
react-start Rsbuild large 26,450 → 6,301 219 → 170 31,932 → 6,983 – 733 → 284 3,083 → 2,486
react-start-rsc Vite medium 8,899 → 2,447 703 → 314 7,269 → 3,788 7,931 → 3,282 299 → 145 1,781 → 1,470
react-router Vite medium 2,802 → 612 576 → 184 3,254 → 1,046 6,476 → 1,303 64 → 47 872 → 689
react-router Vite large 11,185 → 3,109 1,457 → 273 12,469 → 2,909 28,303 → 5,168 130 → 88 1,428 → 1,214
react-router Rsbuild medium 2,728 → 603 208 → 145 2,892 → 724 – 62 → 47 832 → 680
react-router webpack medium 6,633 → 4,410 342 → 277 3,746 → 1,513 – 162 → 141 3,146 → 3,193
solid-start Vite medium 9,185 → 4,375 641 → 266 7,562 → 3,406 10,669 → 4,624 191 → 90 1,563 → 1,349
solid-router Vite medium 4,202 → 1,982 601 → 248 3,886 → 1,490 9,998 → 4,049 63 → 54 947 → 855
vue-start Vite medium 8,347 → 3,780 654 → 277 10,462 → 3,312 ¹ 9,217 → 4,053 178 → 102 1,283 → 1,297
vue-router Vite medium 3,966 → 1,624 608 → 246 3,750 → 1,334 8,854 → 3,054 67 → 57 940 → 836

Dev heap after GC is measured once / has hydrated. Rsbuild and webpack open their port before compiling, so their listen times are not comparable with Vite's. Across all 15 cells: builds −34…−78%, listen −19…−81%, cold start −15…−78%, compile-all −46…−82%, heap after GC lower on every cell (−3…−82%). Peak RSS is lower on every cell but three: React Start small dev cold +4.6%, and Vue Start build +1.1% and webpack build +1.5% (both noise).

¹ After the Vue Start cold-start fix (c271282ac5), measured separately with 14 cold starts per side. At c4cb7e889a the page stalled after Vite's dependency-optimizer reload in 12 of 14 cold starts (median 19,424 ms). The cause was the pre-bundled @tanstack/vue-router/hmr subpath: requested during the re-optimization, it got a 504 "Outdated Optimize Dep" and the entry never ran. With the fix: 0/14 stalls and 0/14 reloads. Main reloads the page in 14/14 cold starts.

HMR (save → DOM, p50): route component/loader edits −17…−66% on Vite. Server-fn edits are −62…−84% on Vite (RSC −28%, Rsbuild −31/−40%). Solid Router and Vue Router route edits no longer full-reload: main does 20/20 full reloads at 254–264 ms, this PR does hot updates at 14–16 ms. Rsbuild HMR is noisy on both sides (p90 up to 2× p50): the <Hydrate> edit reads +97% / +21% (medium/large) and the large route-component edit +6%. Shared-module edits (a component one route imports) are at or below main in every cell and don't grow with app size: large React Start −2.40 ms [95% CI −2.70, −2.10], Solid Start −2.15 [−2.40, −1.90], React Router −0.10 [−0.40, 0.20]; React Start medians stay at 23.2/23.6/23.5 ms small→large while main grows 23.4/24.1/25.6.

Leak check (100 edit + revert cycles, large apps): no growth. Heap slope after cycle 20 is −10.8 / +0.4 MB per 100 cycles for Start / Router.

The generator no longer runs Prettier. At 498ac9be17, the react-start build floor (every compiler output replayed from a recording) dropped from 1,629 to 779 ms, and the generator from 715 to 94 ms.

Memory soak (500 edit + revert cycles on one dev server, at 18ef570edc)

The edits rotate over: route component, loader, shared module, server-fn handler, <Hydrate> child, module add → delete, route add → delete, route rename, a *.server.ts import added and removed, and a factory that stops or starts exporting createServerFn. Heap after GC is sampled every 25 cycles, with heap snapshots at cycles 100 and 500. Slope over cycles 125–500, KB per cycle [95% CI]:

run heap after GC main (reference)
react-start Vite −0.7 [−14.5, 13.1] 11.2 [−11.7, 34.1]
react-router Vite 2.3 [1.8, 2.8] 6.3 [3.7, 9.0]
react-start Rsbuild 20.9 [2.6, 39.3], plus 262 of ArrayBuffers –

The soak found five records that grew with deleted or renamed files: plan-cache generations, dependents-index buckets, raw facts, import-protection records, and Rsbuild per-resource records. All five are fixed and tested. After the fixes, no data object of our packages grows in count between snapshots, and every compiler cache stays bounded by the live files. Replaying our recorded transform outputs (no compiler work) gives the same slope within the CI, so our transforms add no measurable growth. Two caches the soak showed to be useless were removed afterwards: per-env resolutions, and the emitted-output memo (1 hit in 4,959 lookups for 12.9 MB).

The remaining growth comes from upstream. Vite 8.3.4 keeps the module-graph nodes of deleted files (≈2 KB each; most of the Router slope). Rsbuild's dev server keeps every hot-update asset in its in-memory output file system (≈260 KB per edit).

Memory issues (#8614, #8794)

Bytes

Client (pnpm benchmark:bundle-size:run, 18 scenarios, at d0cb0e3ff6 vs main da2adec76d; byte-identical at fdc48254c8):

scenario gzip Δ note
12 of 18 (all Router scenarios, Start minimal / query / deferred-hydration) 0 byte-identical
react-start.full / solid-start.full / vue-start.full −47 / −38 / −36 shorter server-fn ids (22-char base64url)
react-start.rsbuild.full −35 same
react-start.rsbuild.minimal / minimal-iife +1 / +3 (raw −22) accepted exception, see below

The Rsbuild minimal gzip increase is the only client gzip regression. Our loader request changed, so Rspack renumbers its numeric module ids; raw size is −22 B and our emitted code is not larger. The maintainer explicitly accepted it as a one-off.

react-start.full brotli is +92 B, while raw (−42) and gzip (−47) are lower. This is brotli q11 noise: random 22-char ids span 174 B of brotli size. Accepted.

Server (scripts/benchmarks/bundle-size/server.mjs, checkout paths normalized, at d0cb0e3ff6 vs main; identical at fdc48254c8):

  • Every Vite scenario: −12…−823 B raw.
  • react-start.rsbuild.minimal: +7 raw / −17 gzip.
  • react-start.rsbuild.full: +432 raw / +132 gzip, accepted exception. Per D6, the server-fn handler stays in its module (no double init; main ran server-fn modules twice). So the resolver requires __root.tsx, which becomes a second concatenation root. Rspack then cannot scope-hoist the router modules __root shares with the route tree (+1,042 B of module wrappers), partly offset by removing main's duplicated provider code (−585 B). A cache-group fix is blocked by an Rspack panic and is a follow-up.

Cold start (spawn → first SSR response, measured at c4cb7e889a; server output is identical at d0cb0e3ff6) is within noise (−2.1…+3.5%). The *.full Vite servers load one more chunk (the route module shared with the resolver), per D6.

Behaviour changes / migration notes

  • routeTree.gen.ts formatting changes once. The generator prints the tree directly in your quoteStyle/semicolons. enableRouteTreeFormatting now defaults to false; set it to true to keep Prettier formatting (Prettier is then loaded lazily). Custom scaffolding templates are inserted as written.
  • Server functions only server code calls are no longer HTTP-callable in production builds (also those only returned to the client as data). The dev server answers them and warns once per function.
  • serverFns.generateFunctionId receives the stable key (greetUser) as functionName, not greetUser_createServerFn_handler. Default production ids are shorter.
  • esbuild + autoCodeSplitting: true throws; the generator still works with esbuild. Docs and the esbuild quickstart examples are updated.
  • router-plugin requires Vite ≥ 6 (vite peer >=6). CommonJS consumers (a webpack.config.js / rspack.config.js that require()s the plugin) need Node 20.19+ or 22.12+.
  • Removed experimental, Babel-typed hooks: codeSplittingOptions.compilerPlugins; Start compilerTransforms, serverFnProviderModuleDirectives, StartCompilerPlugin and related types; @tanstack/react-start-rsc/plugin/rscCssTransform. RSC integrations pass rsc: { loadCssExpression, providerDirective }.
  • router-utils no longer exports the Babel helpers (parseAst, generateFromAst, deadCodeElimination, …). It gains @tanstack/router-utils/compiler. router-generator no longer exports format, inferFullPath, checkRouteFullPathUniqueness or writeIfDifferent, and GeneratorPlugin.onRouteTreeChanged receives { model }.
  • New dev-only subpaths @tanstack/router-core/hmr and @tanstack/{react,solid,vue}-router/hmr (the route HMR runtime; nothing reaches production).
  • Route CSS is unchanged from main: bare style imports of a route load eagerly with the route module; CSS modules follow the code that references them.
  • Root routes are never split (as on main): their component, errorComponent and notFoundComponent stay in the root route module.
  • Route callbacks reading this (methods/function expressions) stay unsplit with a warning (Handle route callbacks that reference this during code splitting #8688).
  • Import protection: per-edge mocks exporting the target's names, build verdicts after tree-shaking, and onViolation returning false gets the real module (fixed-point rebuild). Traces follow the bundler graph, and reports point at the import.
  • Rsbuild: server-only routes are pruned from the client tree, the client manifest lists only a route's chunks, the persistent cache is supported, spa.enabled dev serves the shell, server.compress is no longer forced, and the server env shares async chunks.
  • Solid Start server envs split route components like the client (hydration keys agree).
  • Vue Start dev: a cold start no longer reloads the page once after dependency optimization (@tanstack/vue-router > @tanstack/vue-store is pre-bundled, as React Start does for its store).

Changesets: compiler-core-yuku (router-utils minor), route-generator-yuku (router-generator minor, router-cli patch), router-plugin-yuku (router-plugin minor), route-hmr-runtime (router-core + react/solid/vue-router patch), start-compiler-yuku (start-plugin-core + router-core minor; start-client-core, react/solid/vue-start patch), react-start-rsc-compiler (react-start-rsc minor), dev-cold-start-and-invalidate-index (start-plugin-core, vue-start, router-utils patch), router-plugin-library-build-externals and router-plugin-order-check-splitting-only (router-plugin patch), start-plugin-core-file-url-sources (start-plugin-core patch), release-finished-environment-plans (start-plugin-core, router-plugin, router-utils patch).

Follow-ups (out of scope)

Upstream issues filed

✅ = fixed upstream.

  • Yuku (fixed): ✅ #202 purity comments with leading whitespace, ✅ #203 codegen SIGBUS on invalid object method, ✅ #204 JSX references for _/$/Unicode names, ✅ #229 import X = ns.Y reference space, ✅ #230 decorators printed after export, ✅ #231 JSXText.value entity decoding, ✅ #232 false duplicate export (type + value), ✅ #237 comments in empty JSX containers, ✅ #244 WASM stale bytes for empty source (WebContainers), ✅ #247 unwrap typing, ✅ #248 module.copyAst(), ✅ #251 export {} from "m" module record, ✅ #252 parenthesized/template import() specifiers, ✅ #253 big-endian AST decoding, ✅ #254 JSX attribute value mapping, ✅ #255 postfix ++ parentheses before a line comment, ✅ #256 for (init;;) no-in leak, ✅ #257 private method overloads, ✅ #258 import type {} from 'x', ✅ #259 super() in a nested computed key, ✅ #260 JSX text after </>, ✅ #287 (a?.b)!.c printing
  • Yuku (open): #288 JSXText escaping, #289 stale JSXText raw, #290 strip keeps type-only exports, #291 diagnostic span splits a surrogate pair, #292 Annex B block functions
  • Vite: #23719 saves within 50 ms ignored (chokidar throttle), #23907 module graph keeps deleted files
  • @vitejs/plugin-vue-jsx: vite-plugin-vue#866 removed export breaks HMR accept
  • Rolldown: #11054 this.load() returns null during bundled-dev HMR, #11223 watch mode keeps stale moduleSideEffects: false, #11300 bundled-dev MissingFactoryError (this PR carries main's importer fallback)
  • Rspack: #16288 HMR runs a module before its new async chunk loads, #16289 splitChunks panic "Chunk not found in ChunkByUkey"
  • Rsbuild: #8736 dev output FS keeps every hot-update file
  • solid-refresh: #88 rename invalidate loop on webpack/Rspack, #89 sticky first registry (Vite), #90 importer stale after rename, #91 document component remount (TanStack Vite HMR blank page when editing __root.tsx (Solid SSR) #6331)
  • magic-string: #368 maps count only \n as a line break
  • babel-dead-code-elimination: #50 a typeof x type query keeps x alive
  • WebContainer: webcontainer-core#2173 TextEncoder.encodeInto('') returns the previous result
  • tanstack.com: ✅ #1353 example playground drops Start's compat config

Issues

Partially addresses #8437 (Start's footer follows quoteStyle/semicolons; tsr generate still writes none).

#6451 was a Vite module-runner race (fixed in Vite 8.2.2, vitejs/vite#23009) that main's compiler triggered by dynamically importing a separate copy of the callee module. Main can still hit it on Vite 8.0–8.2.1, which its vite >=8.0.0 peer allows; this PR compiles server fns in place, so it is avoided on any Vite.

Not fixed by this PR: #5721 is already fixed on main (a Rollup warning; Rolldown/Vite 8 emits none). #8139 is a Rolldown chunk cycle (the __exportAll helper is placed in a dynamically imported chunk); workaround: strictExecutionOrder: true on the ssr environment.

Superseded PRs

Supersedes #8805, #8504, #8630, #8721, #8780, #8773, #8756, #8755, #8732, #8686, #8682, #8681, #8061, #8541, #8493, #8481, #8459, #8455, #7892, #8515, #7281, #8269, #8628, #8797, #8662, #8296, #8215, #8454, #8276, #8675, #8540, #8523, #8524, #8680, #8684, #8642, #8749, #8801, #7338, #8656, #8798, #8818

Implementation obsolete, change not delivered here (needs a re-port or rebase after this lands): #8804, #7693, #7766, #7872, #8291, #8293, #8277, #8799, #8800, #6748, #8256, #8057, #7925

How to review

The diff is large: +159k/−75k lines in 1,975 files, of which the compiler-tests/baseline branch is +102k/−42k in 1,468 files. Most of it is tests (+103k/−41k in packages/*/tests, +16k in the new compiler-test-utils) and regenerated route trees. Product code is +39k/−22k in 231 files. Suggested order:

  1. Core: packages/router-utils/src/compiler/README.md, then identity → source/analysis → units → rewrite → reach → plan → emit/output-builder → values/summary/resolve/facts → cache/host/pipeline. Read alongside tests/helpers.ts (toy planners, memory host) and tests/placement.test.ts.
  2. Planners:
    • Router: router-plugin/ARCHITECTURE-CODE-SPLITTING.md, src/planners/ (route → decisions → split → hmr).
    • Start: start-plugin-core/src/planners/start-env.ts + src/compiler/ (recognition) + src/server-fns/, planners/hydrate.ts, planners/rsc-css.ts, planners/edges.ts + src/import-protection/.
  3. Hosts: router-plugin/src/hosts/; Start src/vite/compiler-plugin.ts (+ planning.ts, bundled-dev.ts) and src/rsbuild/ (COMPILER_ARCHITECTURE.md first).
  4. Generator and runtime: router-generator/AGENTS.md (layout), analyze/route-file.ts, model/, print/, generator.ts; router-core/src/hmr.ts, {react,solid,vue}-router/src/{hmr.ts,lazyRouteComponent.*}, start-client-core/src/createServerFn.ts (client middleware meta), react-start/src/plugin/*, react-start-rsc.
  5. Tests: the oracle contract packages/compiler-test-utils/oracle/README.md, SPEC.md, KNOWN-BUGS.md (what main got wrong, by root cause), then the unit tiers.

How to reproduce the measurements

CI=1 pnpm install --frozen-lockfile
# oracle (normal, then pins as ordinary tests): see packages/compiler-test-utils/oracle/README.md
COMPILER_TESTS_PINS=run CI=1 NX_DAEMON=false pnpm nx run <project>:test:integration --outputStyle=stream --skipRemoteCache --skip-nx-cache
# compiler perf (same Node for both checkouts; candidate and main run back to back)
node benchmarks/compiler-perf/scripts/run.mjs --label <name> --out <dir> --runs 7 --warmup 1 \
  --variants react-start,react-router,solid-start,solid-router \
  --scenarios build,dev-compile,dev-cold,rsbuild-build,hmr,floor,memory,leak --floor build,dev-compile,dev-cold,rsbuild-build
node benchmarks/compiler-perf/scripts/compare.mjs <main>/results.json <candidate>/results.json --md compare.md
# memory soak (500 edit + revert cycles; add --soak-bundlers rsbuild for Rsbuild)
node benchmarks/compiler-perf/scripts/run.mjs --label soak --out <dir> --variants react-start,react-router --scenarios soak \
  --soak-cycles 500 --soak-every 25 --soak-snapshots 0,100,500
node benchmarks/compiler-perf/scripts/soak-analyze.mjs <dir>/soak-react-*.json
# client bytes (18 scenarios) and server bytes / cold start
pnpm benchmark:bundle-size:run --name <name> --baseline <main current.json>
node scripts/benchmarks/bundle-size/server.mjs --repo-root <checkout> --dist-dir <dir> --build --runs 30 --out <json> [--baseline <json>]

The 15-cell matrix above runs the same scenarios per cell (app size × bundler × framework), alternating the two checkouts cell by cell. Its driver is not part of this PR. Details and caveats (load control, floor method, the duplicate-Vite trap): benchmarks/compiler-perf/README.md.


This PR was written with AI assistance (Claude Code agents working in lanes under a human architect). Each lane's work was checked against the oracle, e2e, perf and bytes gates above.

🤖 Generated with Claude Code

Sheraff and others added 30 commits October 25, 2026 01:40
…piler core into the compiler integration branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… into the compiler integration branch

Conflicts in router-utils core: plan.ts includedUnitsOf keeps Lane S's
rule (inline type specifiers of a statement with no included value
specifier are dropped, and the export { T } naming them) together with
Lane R's route-module rules (inline type specifiers and export { T } of a
type-only binding excluded everywhere, ambient value declarations copied
into derived outputs); reach.ts duplicate inclusion keeps Lane D's version
(copy where rooted, then follow readers), a superset of Lane R's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iler integration branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, the pinned state row's options)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The router host answered every core-derived id of a non-route file with
`export {}` (J2.13), including Start's `?tss-hydrate=` chunks, which
Start's compile produces in its own pass before the router's transform:
the Hydrate chunk was emptied (start-compiler-jsx-pragma "a Hydrate chunk
keeps the file-level @jsxImportSource pragma"). The router now only
transforms plain ids and its own `?tsr-split=` outputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Hydrate: the core now reads a Solid `use:x` directive as a value ref
  (router lane, R39 by the core), so the planner's own module-binding
  directive rewrite is gone; a captured local a directive names stays a
  local (`const x = props.x`, no substitution into the attribute name,
  which rendered `use:props.x`).
- Env: a `var` hoisted out of a nested block is now declared by the
  effect unit holding it; the orphan cleanup still removes its declarator
  when the planner erased every reference (J4.12), as before.
- Hydrate test: a component only the chunk reads moves with its own
  property writes (R29 (b), now implemented as designed by the router
  lane) instead of being imported from the parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tanStackRouterCodeSplitter` and `TanStackRouterCodeSplitter{Rspack,Webpack}`
split whatever `autoCodeSplitting` says, as the Babel splitter plugin did
(`autoCodeSplitting` selects the splitter in the composed plugin; Start's
schema omits it and wires the splitter alone): Start's client builds kept
every route component eager.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…deleted key

A route factory called without options (`createRootRoute()`) has nothing
to remove: it failed every Start client build as "not an object literal".
A spread of a module object literal of static keys nothing else reads, or a
computed key naming a module string const, provably holds none of
`deleteNodes`: no error (start-plugin-core server-only-routes builds).
Other spreads and computed keys still refuse the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n pins

- imported route options never ship server handlers silently (refused; Vite, Rsbuild)
- boundaries in split route options another option reads (Vite, Rsbuild)
- route CSS loads with the chunk of a split component (3 rows x dev/build;
  the pending/no-component rows stay pinned)
- Rsbuild client source maps of split route components (2 rows)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wn loaders

One compile per (environment, layer, module) through a pre loader per Start
environment: start-env, hydrate, rsc-css (RSC layer), route-split/route-hmr
(client) and import-edges planners; buildInfo persistence and finishMake
replay of server-fn facts and import-protection records; witness verdicts
with needAdditionalPass; generator as a precondition of every compiler;
client route tree pruning; virtual modules under the resolved root. The
Babel Start compiler and the old Rsbuild import protection are removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e events with Start

The Start Vite host runs the router planners inside its own compile, so it
needs what the standalone host had privately: a registry view by real path
(RouteRegistryView, now exported from /planners with routeEntryDigest) and
the registry change listeners (onRoutesChanged, changedRoutePaths, exported
from /context).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ter, Hydrate and import-protection planners fused)

Start's Vite compiler plugin now runs every planner in the core's fixed
order - start-env, hydrate, rsc-css, route-split, route-hmr, import-edges -
in one compileModule per (environment, module), serving the derived
?tsr-split= and ?tss-hydrate= ids from the same plan (R11), with the router
planners configured as Start wired them (client: split + HMR with deleteNodes
ssr/server/headers; servers: deleteNodes only, R24; never in a separate
provider env). The import-protection host side follows the lane E contract:
edge module resolveId/load, dev graph + reporter per env (flushed after each
compile), build witness in generateBundle with the fixed-point veto rebuild
wrapped around every builder.build of a Start environment (also when
plugin-rsc drives the builds), a post-order late pass for non-JS importers
(R48), and tsconfig verbatimModuleSyntax read for J6.2.

Deleted: the interim Babel Hydrate post-pass (vite/hydrate-interim.ts, the
chunk load plugin), today's Vite import-protection plugin
(vite/import-protection-plugin/*), vite/module-id.ts, the separate router
transform instances of Start's router plugin, and the constants only they
used. hydrate-when-transform.ts and import-protection/* stay for the Rsbuild
path (lane B).

The Hydrate planner extracts exported pure declarations only a chunk needs
into extracted-export outputs (3.5.3), as the Babel transform did for
"an export only the children use loads lazily".

Pins flipped: start-compiler-jsx-pragma (2), hydrate-build (2). Test
adjusted: reported-location-trace "imports the entry back" keeps the client
entry's code alive (SPEC J6.4: dead edges are not reported).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Edge modules keep the JavaScript module type under import attributes; the
dev reporter uses the current compilation's graph and no timers; failed
resolutions and missing-file facts are forgotten at watchRun (Rspack
reports no event for a recreated file nothing depended on); dependency
reads bypass Rspack's cached input file system; log 'always' reports again
on cache-hit compiles and kept modules; a marked entry reports itself;
server environments split routes too (Solid hydration keys); the Hydrate
planner extracts exported pure declarations only chunks need (DESIGN
§3.5.3) so they load lazily on Rspack.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…60 integration pins

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…of extracted exports

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…les of the react-server layer

The core copies what a provider output's roots reach instead of importing
the SSR stub from the env module (DESIGN §3.10.5, J4.14); Rspack's RSC
proxy query is no part of the source identity; the RSC layer does not split
routes; the manifest virtual path is read after the root is resolved. Flips
the Rsbuild RSC pins (CSS of helper modules, the diagnostics, the resolved
root) and the Vite 'use client' provider pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…odules

A trace built from a loader read the module graph during make, which
Rspack aborts on; the import-protection graph view is set at finishModules.
The raw-facts cache no longer publishes a read in flight when the file is
marked stale meanwhile. The Rsbuild compile tracker of the test harness
counts the runs of dependent compilers (MultiCompiler dependencies) and
waits for them, so a watch edit's outcome includes the server compile.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… loader file

The registry property name moves to its own module so that Rspack is the
first to load compiler-loader.js. Tooling that wraps loaders as they load
(the compiler-perf floor) could not see a file the plugin had already
imported.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…try by its root module

A production entry is a ConcatenatedModule without a resource of its own,
so the trace of a violation never recognised it as the entry when a module
imported the entry back. The pinned test is a plain test; its entry calls
its own start so the chain stays live on Rspack, which drops an entry's
unused exports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tracted export

A unit-level write (export function bump() { clicks++ }) unions the writer
with the written let into one declaration cluster, which passed the
extracted-export candidate check: state moved to ?tsr-split=export~bump, and
a Hydrate children region also writing the binding escaped the single-writer
rule (no hydrate-module-write, a chunk assigning an import). A cluster that
declares a module-level symbol with a live write is no candidate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sbuild hydrate pins

The client build capture of the manifest plugin skips a generateBundle whose
bundle import protection emptied (a non-final pass of the veto rebuild). Two
hydrate-build cases the Yuku Hydrate planner fixes stay pinned for Rsbuild,
still on the previous compiler.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y no map, an entry's own marker is judged

Solid's hydration keys follow the component tree: the client renders a
split route component under the lazy wrapper's Lazy and Dynamic components,
so Start's Solid server env splits like the client (what main did for every
framework); React and Vue keep R24. Every split route hydrated with a
mismatch before (hydrate-dev-solid).

Vite answers an empty sourcemap chain with an identity map over the input,
counted by LF only; the host passes no input map when the map's single
source content is the input text, so a lone CR is a line terminator again
(reported-location, R-11 probe), and the Locator counts a real map's
generated lines by LF.

A server-only-marked client entry compiled in the client is its own marker
violation (located at the marker; the mock keeps the effect witness although
the marker module is empty, so the build verdict sees whether the entry
survived). Only entries: a marked module is otherwise judged through its
importers, so an exempt importer gets it (J6.7). The session takes isEntry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ce entry; flip 120 Vite pins the Yuku host fixes

Vite's dependency optimizer resolves a bare import to its bundle before the
bundle is written, so target facts read through that path saw a missing
module and the edge module's mock lost the target's export names (a dev
page loading a mock without the names it re-exports, timing-dependent). The
host maps an optimizer id back to the dependency's source entry through the
optimizer metadata: deterministic facts, stable resolution fingerprints.

Pins flipped (normal mode: every pin ran as an ordinary test and passed):
hydrate pages and module-level pins (48), Solid and React page suites (6,
Rsbuild rows stay pinned), import protection build/dev/exemption/trace/
location/alias/policy pins (64), dev-server pins (2). Rsbuild rows keep
their .fails (the Rsbuild path is still on the previous compiler).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ction

The exported writer of module state (export function bump() { clicks++ })
may leave for its extracted output with the state when only a chunk reads
it (an exported function over module state loads with its chunk). Only a
binding some sub-region writes (Hydrate children, a split option value) keeps
its cluster out of the candidates, so the single-writer rule still refuses a
chunk assigning an import. The Hydrate unit test that pinned the live
binding's home is adjusted (the extracted output, not the parent).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…aring a module

The route CSS rows (SPEC J2.22) now live in route-css-cases.ts and run
for each bundler from its own file: known-bugs-route-css.test.ts on Vite
dev and build, known-bugs-route-css-rsbuild.test.ts on the Rsbuild dev
server and an Rsbuild production build served as `rsbuild preview` does
(new StartBuild.preview() of the build harness).

New rows: two routes whose components import their own CSS and share a
module. On a route, the other route's styles must not apply, after a
client-side navigation and in the page the server renders there. Vite
passes; on Rsbuild, with the shared module in a chunk of its own, the
page links the other route's stylesheet (Start's Rsbuild manifest, #8780),
pinned as a separate known bug.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sheraff and others added 19 commits October 26, 2026 13:18
…read; test the core's small modules

`cleanOrphanedDeclarations` asked `isPureExpression` about a class
declaration, which only knows class expressions, so such a class never
left with its erased reader; it now asks `isPureClass`. Adds unit tests
for `WarningLedger`, the string/identifier helpers, parse diagnostics and
expression wrappers, export summaries and their digest, and the orphan
cleanup's multi-declarator and lazy declaration paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rd-slash ids (Windows)

The consumer's resolve and transform hooks compared Vite's module ids
(forward slashes on every platform) with the recorded inputs' native paths
(backslashes on Windows), so no fixture module was ever recognised there:
every route's file was null and no import edge was recorded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…its own

The compiler reports an unsplit route once per dev server; on the Alpine
runner the second test reused the host that compiled the route for the
first one, so its diagnostics window held no warning ("0 of 0").

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…overage + isPureClass fix, route trees): t3/fix-hmr-refresh-diagnostics

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…routes-unsplit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gate, library-build generateBundle, compiler-tier parallelism): t3/compiler-rewrite-ci-fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts and harness graph walks (Windows)

The Windows failures of the start-plugin-core unit tier (Compiler Platforms
run 40324307091) were path-form assumptions of the tests and their harness:
- reporting: map files are host ids, absolute in the platform's form
  (D:/app/... on Windows); the fixtures use such a root.
- edge-modules: Rspack edge-module ids are forward-slash paths; compared
  with the native temp directory.
- planner: the in-memory host keyed package.json reads by forward-slash
  paths while the product joins native ones.
- start-host/render graph walks: Vite's module graph keys files by
  forward-slash paths; the native case file found no module, so
  serverLoads() was empty (and client graph loads checked nothing).

Product fix: import protection decoded file:// map sources with the URL's
pathname (/D:/... on Windows); use fileURLToPath (test + changeset).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…icon 404; bound the dev-edit page runs

Full Chromium (Alpine's) requests /favicon.ico, which the test apps do not
serve; its console 404 failed serveCase's 'no page errors' check
(reproduced locally with Playwright's full Chromium, 1 run in 3). The
console filter matches the message's URL, so a module's 404 still counts.

The start-compiler-factories dev-edit tests timed out at 30 s on Alpine
once (1.3 s in the green run): a wait that never ended, not a slow
runner. Their page steps were raw Playwright calls with no bound below the
test's, so the failure named nothing. They now go through the host's page
runs (bounded, page errors and console reported, navigation retried after
the dev server is idle), and a fresh run's reload is bounded too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s a dev server of their own

The compiler reports an unsplit option once per (file, reason) and dev
server (J2.19); the shared hosts' routes share one route file, so another
test's earlier report on the same host hid this one's (failed in the full
integration tier, passes alone). As the imported-options route and this
file's errorComponent cases already do: fresh: true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bare style imports of a route module stay in the route module whether or
not its components are split, as on main. Removes the route-split home
rule that moved them into the component chunks and the core's
isStyleImport unit flag it alone used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shows-component, shows-error and shows-not-found rows return to
known-bug pins on Vite and Rsbuild, as in the oracle; the Rsbuild
shares-module rows (#8780) keep passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…that reads it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`invalidate()` ran two linear scans per changed file and per environment:
`PlanCache.keysAt` over every plan and in-flight attempt, and
`RawFactsCache.markStale`/`forget` over every raw-facts entry, calling
`identify()` on each (~1.25 ms per shared-module edit in the large React
Start app, growing with the app). Both now keep a path index maintained
where entries are published, begun and retired. A plain change also no
longer re-resolves the dependents' resolution fingerprints through the
bundler: the file set is unchanged, so the resolver's answers stand, as
plan validation already assumes at an unchanged fileSetVersion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tranded page or a reload

Start pre-bundled `@tanstack/<framework>-router/hmr` on its own while the
framework plugins exclude the router package from optimization: every
compiled route module imported an optimized chunk deep in the client
entry's graph, and when a post-scan re-optimization committed before the
browser fetched it, the request got 504 "Outdated Optimize Dep", the graph
never evaluated, the dev client (imported by the dev client entry) never
connected, and Vite's buffered full reload was never delivered (Vue Start
medium: 12 of 14 cold starts stranded). The subpath is served from source
with the rest of the excluded package now (one instance of the HMR state).

Vue Start also re-optimized and reloaded on every cold start: the scanner
never sees `@tanstack/vue-store` behind the excluded router package, and
once discovered it re-chunks the pre-bundled `vue`. Declared through
`optimizeDeps.include` as React Start does for `@tanstack/react-store`.
Cold start: 10.5 s (main) / 19.4 s (before) -> 3.3 s, no reload. Adds a
cold-start integration test (Vue, with React as control) on a fresh
dependency cache, and `framework: 'vue'` to the Start dev-server harness.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…art): t3/index-plan-cache-keys

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t-route-css-splitting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s not share it

Port of the type-annotation regression test of #8818: a loader typed
`(): Page.Props` reads no value, so the component stays in its chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sheraff and others added 9 commits October 26, 2026 20:03
…s compiler state in production builds

A build of several environments (client, ssr, then nitro or rsc) kept every
finished environment's plans and import-protection state until the whole
build ended (#8794). The environment's logical build owns the release:
Start's Vite builder.build wrapper after the import-protection fixed point
(a written, non-watch build; plugin-rsc's write:false scans build again),
the Rsbuild compiler's last done pass outside watch mode, and router-plugin's
Vite closeBundle. The core's releaseEnvironment drops the environment's
plan cache; what later environments read (raw facts, server-fn registry
and snapshots, id allocator, warnings) stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tifies no subscriber

Every watcher event re-published the generator's registry, the same Map
when no route changed, and each `onRoutesChanged` subscriber compared the
whole registry with itself: O(routes) per edit of any file (~0.14 ms per
shared-module edit on the 1,485-route harness app). The generator replaces
its registry when a route changes, so the same registry again is no change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…): t3/investigate-hmr-scaling-1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndows)

The route registry is keyed by the generator's posix-form paths (as the
subscribers' module ids are); the test expected `path.join`'s, which use
backslashes on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad of reloading

After a dev edit, the server sends the host's page a full reload, which
Vite's HMR client applies 20 ms later (debounced), after `settle()` has
returned. The fresh run's own `page.reload()` raced it: aborted
(net::ERR_ABORTED), detached ("Not attached to an active page"), or left a
document whose import never settled (the Alpine CI failure of "dev:
editing a factory module recompiles its importers"). Reproduced with
Alpine 3.22's Chromium 142 and musl Node 24.8 on 4 CPUs under load (4 of
24 runs; the full-reload frame arrived 24 ms before the harness's reload);
0 of 24 with a new page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om src

The test imported `@tanstack/router-plugin/context` and `/vite`, the
package's own built entries, but `test:unit` depends only on its
dependencies' builds: on CI the test ran before the package's build (its
dist restored from the remote cache) had finished, and the self-reference
found no dist. Locally an earlier build's dist answered it. The unit tests
import the plugin from `src`, as the others do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of its own

The release test reads the process-global compiler stats, summed over every
live session. In the `integration` project (`isolate: false`), an earlier
file of the same worker (rsbuild/dev-server.test.ts, which enables the same
stats knob) leaves its Rsbuild dev session alive after its server closed:
Rspack's native binding holds the closed compiler's last compilation
through a global handle (GC roots > global handles > JsCompilation >
Compilation > Compiler > Start's transformer > session), so the probe counted
that session's 107 plans and 2 import-protection environments on top of the
build's released client. Reproduced with one worker and that file order; the
host's release is not at fault.

The file now runs in project `integration-alone` (`isolate: true`, a new
process per file), which `test:integration` runs too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant