Skip to content

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

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

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

Conversation

@Sheraff

@Sheraff Sheraff commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Includes the oracle test-suite branch compiler-tests/baseline. 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.

Summary

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

On main, Babel took ~90% of compiler time. On 333- and 1,485-route React and Solid apps, builds are 2.1–4.8× faster and dev cold start is 2.1–5.0× faster. Start builds also end with 3–10× less heap.

Each module and environment gets one parse and one plan. Outputs no longer depend on transform or request order, and source maps are exact. The oracle's known-bug pins drop from 874 to 45 (36 of those keep main's behaviour on purpose), and 29 open issues are fixed.

Architecture

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

  • Analysis → plan → emit. Each (env, module, text) gets one analysis and one plan. The plan emits the env module plus derived modules (?tsr-split=…, ?tss-hydrate=…, ?tss-serverfn-split, ?tss-serverfn-handler) and facts (import edges, server-fn registry, diagnostics, dependencies).
  • Planners are the jobs. Each one adds edits and roots to the shared plan, in a fixed order: start-env → hydrate → rsc-css → router → import-edges. APIs are recognised by binding, through an abstract value domain. Cross-module summaries come from raw text, never from the bundler pipeline.
  • Emit builds a rewrite tree over the original source ranges, so user code is never re-printed. OutputBuilder writes VLQ maps directly and composes them with input maps.
  • Reachability = splitting = dead-code removal, at cluster granularity. Code is removed only when our own edits orphaned it, so user side effects and unread imports stay. One mutation model (who writes each binding, and when) decides what may move. If a split does not settle within its re-planning bound, the route stays unsplit and a warning is emitted.
  • Lazy server-fn handlers (when SSR is the provider). The handlers, and what only they reach, move to ?tss-serverfn-handler, which loads on the first call. The defining module runs once.
  • Verifier (TSR_COMPILER_VERIFY=1, on in all tests). Every output is re-parsed and checked: no assignment to an import, glue imports resolve, each effect is emitted once, and extracted outputs make no eager env-module reads.
  • Caching and hosts. Caching is a plan cache, a dependency ledger and one invalidate(); no Yuku objects are retained. We own the hosts: a native Vite plugin, a Rspack/webpack loader, and Start Rsbuild loaders (with buildInfo replay for the persistent cache). Start's Vite and Rsbuild hosts share one bundler-free Start session; hosts keep only bundler wiring. unplugin remains only for the generator and esbuild.

Performance

Measured with benchmarks/compiler-perf at 774eba05a4 against main da2adec76d. Cells marked ² were re-measured at e80188ee7b. Both checkouts run in parallel on mirrored core sets, over 6 paired rounds; values are medians. Times are in ms and heap in MB. Large = 1,485 route files.

large app build dev cold → hydrated compile-all HMR route edit dev heap after GC
react-start Vite 30,418 → 9,016 24,801 → 7,401 31,105 → 8,583 65.0 → 41.6 704 → 207
react-start Rsbuild ² 27,484 → 6,143 32,702 → 6,501 – 1,839 → 941 733 → 207
react-router Vite 11,208 → 3,058 11,647 → 2,989 26,984 → 5,026 53.8 → 31.4 113 → 89.7
solid-start Vite 42,999 → 20,019 30,690 → 10,299 43,902 → 19,417 193 → 30.2 ³ 618 → 205
solid-router Vite 17,167 → 8,025 ² 14,349 → 4,354 40,273 → 15,426 876 (full reload) → 18.6 107 → 89.7

³ On main, this HMR run aborted in 5 of 6 rounds and full-reloaded 13 edits. This PR had no failures and no reloads.

  • tanstack.com (e80188ee7b vs npm main, 6 paired rounds):
    • Build: 18.7 → 12.7 s (−32%).
    • Peak RSS: 3,916 → 3,342 MB (−15%).
    • Dev cold → hydrated: 29.3 → 24.2 s (−17%).
    • First visit of 15 routes, and HMR: parity.
  • Corpus (064d29003a, 129 apps, medians of per-app Δ): build −12.1%, dev ready −18.8%, first render −10.5%, peak RSS −1.7%.
  • 13 large apps: builds are 2–26% faster in 9 and at parity in 3. kroma (barrel-heavy) builds 2.6% faster (774eba05a4).
  • Losses:
    • Build peak RSS: solid-router large +2.5% (+30 MB, e80188ee7b); kroma +3.8% (+64 MB, 774eba05a4).
    • Rsbuild minimal client: +1 / +3 B gzip (raw −22 B; accepted).
    • Server bytes (b25984c166): +308 B raw on Vite *.full, +602 B raw / +78 B gzip on react-start.rsbuild.full (da12a6f6ba).
    • CodSpeed: +0.9–3.3 KB on 3 memory benchmarks (da12a6f6ba; CodSpeed notes base and head ran on different runner types).
    • Rsbuild dev: +29–39 ms per hot update (save → DOM, +11–14% on react-start medium), the cost of putting the original file text back into source maps (see Behaviour changes). Filed upstream as rspack#16348.
  • e18e framework tracker (their harness and starter on 2 CPUs, 12 paired rounds, PR vs the npm packages they test): dev server startup −9.7%, cold build −11.5%, warm build −11.0%. TanStack Start would move from Bump mixin-deep from 1.3.1 to 1.3.2 #5 to Bump handlebars from 4.1.0 to 4.5.3 #4 in cold builds and to Link component ignore the basepath of LocationProvider #3 in warm builds, and stay Bump mixin-deep from 1.3.1 to 1.3.2 #5 in dev startup. The build margins (~2%) are smaller than the tracker's run-to-run spread.
All 11 cells, HMR, Vue, memory, bytes

Medium = 333 route files. Δ CIs are 95% t intervals of the paired log ratios.

cell build dev listen dev cold → hydrated compile-all dev heap after GC build peak RSS
react-start Vite medium 6,440 → 1,599 613 → 210 6,096 → 2,557 7,333 → 2,066 212 → 89.5 1,226 → 947
react-start Vite large 30,418 → 9,016 1,514 → 311 24,801 → 7,401 31,105 → 8,583 704 → 207 2,570 → 1,468
react-start Rsbuild medium ² 6,728 → 1,942 208 → 158 7,856 → 2,111 – 218 → 84.2 1,063 → 871
react-start Rsbuild large ² 27,484 → 6,143 210 → 159 32,702 → 6,501 – 733 → 207 2,819 → 2,081
react-start-deps Vite medium ¹ 6,988 → 1,846 622 → 212 6,709 → 3,126 8,050 → 2,439 236 → 107 1,240 → 1,132
react-router Vite medium 2,742 → 568 585 → 178 3,348 → 1,024 6,525 → 1,282 64.0 → 47.2 751 → 569
react-router Vite large 11,208 → 3,058 1,381 → 278 11,647 → 2,989 26,984 → 5,026 113 → 89.7 1,283 → 1,041
solid-start Vite medium 9,472 → 4,408 623 → 266 7,371 → 3,226 10,719 → 4,561 191 → 92.3 1,334 → 1,121
solid-start Vite large 42,999 → 20,019 1,484 → 357 30,690 → 10,299 43,902 → 19,417 618 → 205 2,799 → 1,975
solid-router Vite medium 4,310 → 1,932 590 → 241 3,814 → 1,543 10,076 → 4,153 63.1 → 54.0 802 → 762
solid-router Vite large 17,167 → 8,025 ² 1,412 → 332 14,349 → 4,354 40,273 → 15,426 107 → 89.7 1,211 → 1,241 ²

¹ react-start plus a generated dependency graph and tanstack.com's manualChunks. Dev SSR fails on both sides here because of a ?url wasm asset in the harness, so <Hydrate> HMR is not measured.

  • Speed: every cell is significantly faster. Build −53…−79%, listen −24…−80%, cold start −53…−80%, compile-all −56…−81%.
  • HMR (p50): route component −15…−98%, loader −9…−55%, server fn −39…−79%. <Hydrate> is −8…−46% on Vite and −30 / −38% on Rsbuild ². Shared-module edits show no significant slowdown.
  • Memory: heap after GC is lower everywhere (Solid Router compile-all n.s.). Peak RSS is lower except for the losses above. 100 edit + revert cycles show no growth (eedc638e42).
  • 500-cycle soak (18ef570edc, KB/cycle, PR vs main): react-start Vite −0.7 vs 11.2, react-router Vite 2.3 vs 6.3, react-start Rsbuild 20.9. The soak found five records that grew with deleted files; all are fixed and tested. The remaining growth is upstream (vite#23907, rsbuild#8736).
  • Start: import protection keeps a parsed AST per module in dev (hundreds of MB in large apps) #8614: import protection in dev adds +2.4 MB of heap, down from +321 MB.
  • [Start] vite build keeps every environment's server-fn compiler alive until the build ends (memory stacks across client → ssr → server) #8794: in Start + Nitro builds, heap is 64.3 MB here, vs 506.5 MB on main and 98.7 MB on main with the issue's fix (fdc48254c8).
  • Vue (1 paired round, medium; Vue Start / Vue Router): build 8,665 → 3,917 / 4,162 → 1,588 ms, cold start 10,692 → 3,073 / 5,017 → 1,349 ms. Every HMR edit applies in place, whereas main full-reloads Vue Router route edits.
  • Other builds (eedc638e42): webpack build −38%, RSC build −74%.
  • Generator without Prettier (498ac9be17): 715 → 94 ms. The react-start build floor drops from 1,629 to 779 ms.
  • Client bytes (18 scenarios, b25984c166):
    • 12 are byte-identical.
    • *.full scenarios are −35…−47 B gzip, thanks to shorter server-fn ids.
    • Rsbuild minimal is larger because Rspack renumbers module ids once our loader request changes.
    • react-start.full brotli +92 B is q11 noise.
  • Server bytes: Vite scenarios are −12…−419 B raw, except lazy handlers, which add +308 raw / ~+200 gzip per *.full scenario. react-start.rsbuild.full is +602 raw / +78 gzip (da12a6f6ba; was +696 / +326):
    • the resolver imports the defining module, which makes __root a second concatenation root, so router modules shared with the route tree stay unhoisted;
    • lazy handlers add their per-file glue;
    • one named cache group merges the server-fn provider chunks into the router chunk, recovering the extra chunk's overhead (−94 raw / −248 gzip; no Rspack version requirement).
  • SSR cold start (a server fn imports a 1.5 MB SDK that GET / never calls; main / eager handlers / this PR): Vite ~88 / ~145 / ~88 ms, Rsbuild ~57 / ~122 / ~57 ms.

Testing

The oracle (packages/compiler-test-utils/oracle) drives real bundlers, dev servers and browsers through public APIs only. Main's bugs are pinned as .fails.

baseline (main's compilers, 80d1890bed) this branch
tests 3,594 4,247
known-bug pins (.fails) 874 45
non-pin failures 0 0
  • Oracle at b25984c166: all 8 unit and integration targets pass (4,247/4,247). With COMPILER_TESTS_PINS=run, each pin fails for its documented reason.
  • CI at e80188ee7b: the Test job passes 881/881 Nx tasks. Compiler Platforms passes on all platforms except Linux arm64, which failed one router-utils unit test, pipeline.test.ts (f), an in-flight dependency-only edit.

Beyond the oracle:

  • Real-app corpus: 190 public repos, each built and dev-tested with npm main and with this PR, in a network-restricted sandbox.
  • Leak check: a canary in the server-only code of 124 in-repo Start apps.
  • Differential fuzzers for compiler output (run against the untransformed source), the route generator, and HMR edit sequences on 5 frameworks.
  • Internal verifier, on in all tests.
  • Coverage-targeted tests.
  • Five independent coherence reviews, each followed by a consolidation pass, plus two review waves on architecture, useless work (profiled) and dead code. They led to staging the placement and plan code, one router planner, one Start session for both Start hosts, ~1,300 lines of dead code removed, and ~30 ms less main-thread CPU per Rsbuild dev edit. Each pass kept client bytes identical and compile time at parity or better.

Every PR bug these found is fixed, with a regression test. The fuzz differences that remain are pre-existing on main or upstream.

Results

Corpus (b25984c166 vs npm main):

  • Builds: 189/190 build on both. The 1 PR-only failure was a type-only import re-emitted as a value import, fixed in 390ca815c3.
  • Bytes: client JS 405.01 → 404.72 MB, initial JS 101.42 → 96.92 MB.
  • Leaks: 0 server-only canaries in client JS on either side.
  • Dev: 158 equal, 18 PR better, 4 PR worse, 6 mixed, 4 both fail.
    • The 4 worse: the type import, a global-boundary reload (main's behaviour), a pre-existing Vite optimizer race, and noise.
  • Route-edit HMR median: 159 → 105 ms (71 pairs).
  • Bugs it found, all fixed:
    • effectful declarations only split code reads stayed eager (+110 KB initial);
    • extracted-export TDZ cycle;
    • server-only deps kept by function-local chains;
    • lost ASI before (/[;
    • handlers' server-only imports loaded at boot (500s; fixed by lazy handlers);
    • Solid Start fix edit not shown after an error.
  • Main sometimes emits fewer chunks; that happens only with two @babel/traverse copies, which break main's DCE.

Leak check (139 builds per side, 5ff36a1816): 0 leaks here. Main leaks in mock mode; that case is pinned.

Fuzzers:

  • Compiler (110,000 cases):
    • 0 env-separation differences.
    • 0 PR reference errors, wrong values or duplicated effects.
    • 1 TDZ fixed; unread imports are now kept, as on main.
  • Generator (51,000 trees, each generated by main and four times by the PR):
    • 0 crashes or nondeterministic outputs.
    • 2 cases fixed.
  • HMR (1,040 sequences):

Coverage: lines 91.1 → 91.5%, branch arms 79.2 → 80.2%. No bug found; 2 unused paths deleted.

Vite 6 / 7 (5ff36a1816): the 13 router-plugin e2e projects pass on Vite 6.0.15, 6.4.5 and 7.3.7. Split Solid and Vue routes update in place, where main full-reloads.

Remaining fail pins (45)

reason # expected behaviour still missing
Kept main's behaviour on purpose 20 Route CSS applies only once the route matches. This needs a per-route asset loader (follow-up).
16 A root route's error/not-found component loads lazily. Root routes are never split: splitting cost SSR CPU on error renders, and the chunk was preloaded anyway.
Bundler limitation 3 On rspack/webpack/rsbuild without sideEffects, an exported component another route's chunk imports loads with that chunk.
2 On Rsbuild dev, a custom module type (.page as javascript/auto) importing a denied file is reported. Rspack rules select by resource.
1 On Rsbuild build, an unused exported component with a boundary emits no async chunk.
Upstream 1 On Rsbuild dev, adding a route's first server fn shows on the open page (rspack#16288).
Not supported yet 1 On Vite dev, a Start library's undeclared CommonJS dependency is pre-bundled.
1 In Vite bundledDev, a factory that becomes a server-fn builder keeps its handler. This needs the full-bundle API.

Behaviour changes

  • routeTree.gen.ts formatting changes once. The file is printed in your quoteStyle/semicolons. enableRouteTreeFormatting now defaults to false (true uses Prettier, loaded lazily). Custom templates are inserted as written.
  • Server-only server fns: a server fn that only server code calls, or that is only returned as data, is not HTTP-callable in production. Dev still answers it, with a warning.
  • serverFns.generateFunctionId receives the stable key (greetUser) as functionName, and default ids are shorter.
  • A server fn an edit removes is an unknown id in dev (Invalid server function ID). Main answered a 500 TypeError.
  • Server-fn handler modules run once, on the first call. A handler stays eager if it writes module state or its module uses top-level await.
  • Exported page components are split (?tsr-split=export~Name), together with their augmentations. On one corpus app: −179 KB initial, +2.4 KB total.
  • Imports read only by moved code move with that code. Unused imports stay, as on main.
  • Stays unsplit, with a warning: route callbacks that read this (Handle route callbacks that reference this during code splitting #8688), and splits that never settle.
  • Route HMR after a render error reloads the page in dev, as on main. This applies to any boundary on Solid/Vue, and to the global boundary on React.
  • Import protection:
    • per-edge mocks export the target's names;
    • build verdicts come after tree-shaking;
    • onViolation returning false gets the real module;
    • traces follow the bundler graph.
  • Route-group layout types match the runtime fullPath: '/', not ''.
  • Source maps on webpack/Rspack/Rsbuild hold the original file text, so devtools show the file as written. Main showed the code splitter's output under the file's name.
  • Vite + @vitejs/plugin-react: an edit that makes a split chunk throw while it loads reloads the page (split components are refresh boundaries, which plugin-react makes self-import, and Vite reloads failures in a circular import). The fixing edit applies in place.
  • Start router options follow one rule on Vite and Rsbuild: client addHmr is always on, and the server honours your codeSplittingOptions.deleteNodes (Rsbuild used to drop it).
  • Rsbuild:
    • server-only routes are pruned from the client;
    • manifests list only a route's own chunks;
    • the persistent cache works;
    • spa.enabled dev serves the shell;
    • server.compress is no longer forced;
    • the server env shares async chunks;
    • RSC CSS HMR needs @rspack/core ≥ 2.2.9 (fix: reset MultiCompiler done state on watch restart web-infra-dev/rspack#15865); peer ranges are unchanged.
    • rsbuild build --watch picks up route files created while watching.
    • vite build --watch of a Start app builds and rebuilds against each edit (the server builds after the client's first build).
    • on Rspack 2.1.0–2.1.2, open tabs reload after a dev warm-cache restart (an Rspack bug fixed in 2.1.3).
  • Solid Start server envs split like the client. Vue Start dev no longer reloads after dependency optimization.
  • Requirements:
    • router-plugin needs Vite ≥ 6;
    • a CommonJS webpack/rspack config needs Node 20.19+ or 22.12+;
    • esbuild + autoCodeSplitting: true throws;
    • @babel/core is no longer transitive, so declare it if a plugin needs it (e.g. React Compiler).
  • Removed:
    • Babel-typed experimental hooks: codeSplittingOptions.compilerPlugins, Start compilerTransforms, serverFnProviderModuleDirectives and StartCompilerPlugin;
    • react-start-rsc/plugin/rscCssTransform, replaced by rsc: { loadCssExpression, providerDirective };
    • router-utils' Babel helpers (router-utils gains /compiler);
    • router-generator's format, inferFullPath, checkRouteFullPathUniqueness and writeIfDifferent. onRouteTreeChanged now receives { model };
    • router-plugin's undocumented plugin.hmr.style (and HmrOptions): the style follows the bundler. On Vite, its only other value turned route HMR off.
  • New dev-only subpaths: @tanstack/router-core/hmr and @tanstack/{react,solid,vue}-router/hmr.

19 changesets. Minor bumps: router-utils, router-generator, router-plugin, router-core, start-plugin-core and react-start-rsc. Everything else gets a patch.

Follow-ups

Upstream issues filed

✅ fixed · ✖ closed without a fix · ○ open

Issues

Other issues:

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 (still needs a re-port after merge): #8804, #7693, #7766, #7872, #8291, #8293, #8277, #8799, #8800, #6748, #8256, #8057, #7925

How to review

The diff is +164k/−75k lines in 2,000 files. The compiler-tests/baseline branch accounts for +102k/−42k of it, and most of the rest is tests and regenerated route trees. Product code (packages/*/src) is +31k/−23k in 211 files.

  1. Core: start with router-utils/src/compiler/README.md, then read identity → source/analysis → units/mutation → rewrite → reach → plan → emit → values/summary/facts → cache/pipeline → verify. Keep tests/helpers.ts and tests/placement.test.ts open alongside.
  2. Planners:
    • Router: router-plugin/ARCHITECTURE-CODE-SPLITTING.md, then src/planners/.
    • Start: start-plugin-core/src/planners/ (start-env, hydrate, rsc-css, edges), with src/compiler/, src/server-fns/ and src/import-protection/.
  3. Hosts:
    • router-plugin/src/hosts/.
    • Start's src/vite/compiler-plugin.ts.
    • Start's src/rsbuild/ (read COMPILER_ARCHITECTURE.md first).
  4. Generator and runtime:
    • router-generator (AGENTS.md, analyze/, model/, print/).
    • router-core/src/hmr.ts and {react,solid,vue}-router/src/hmr.ts.
    • start-client-core/src/createServerFn.ts.
  5. Tests: the oracle's README.md, SPEC.md and KNOWN-BUGS.md.

To reproduce the measurements, see:

  • packages/compiler-test-utils/oracle/README.md
  • benchmarks/compiler-perf/README.md (run.mjs, compare.mjs, soak-analyze.mjs)
  • pnpm benchmark:bundle-size:run
  • scripts/benchmarks/bundle-size/server.mjs

This PR was written with AI assistance: Claude Code agents working under a human architect. Their work was checked against the oracle, e2e, perf and bytes gates above.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@changeset-bot

changeset-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 37e941d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 24 packages
Name Type
@tanstack/router-utils Minor
@tanstack/router-plugin Minor
@tanstack/start-plugin-core Minor
@tanstack/vue-start Patch
@tanstack/react-start-rsc Minor
@tanstack/router-generator Minor
@tanstack/router-cli Patch
@tanstack/router-core Minor
@tanstack/react-router Patch
@tanstack/solid-router Patch
@tanstack/vue-router Patch
@tanstack/start-client-core Patch
@tanstack/react-start Patch
@tanstack/solid-start Patch
@tanstack/router-vite-plugin Patch
@tanstack/react-start-client Patch
@tanstack/react-start-server Patch
@tanstack/solid-start-client Patch
@tanstack/solid-start-server Patch
@tanstack/start-server-core Patch
@tanstack/start-storage-context Patch
@tanstack/vue-start-client Patch
@tanstack/vue-start-server Patch
@tanstack/start-static-server-functions Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@socket-security

socket-security Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

@socket-security

socket-security Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Priority Alert  (click "▶" to expand/collapse) Action
Low priority
Low adoption: npm @yuku-toolchain/darwin-arm64

Location: Package overview

From: pnpm-lock.yaml → npm/yuku@0.26.0 → npm/@yuku-toolchain/darwin-arm64@0.26.0

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@yuku-toolchain/darwin-arm64@0.26.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
Low adoption: npm @yuku-toolchain/darwin-x64

Location: Package overview

From: pnpm-lock.yaml → npm/yuku@0.26.0 → npm/@yuku-toolchain/darwin-x64@0.26.0

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@yuku-toolchain/darwin-x64@0.26.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
Low adoption: npm @yuku-toolchain/linux-arm64

Location: Package overview

From: pnpm-lock.yaml → npm/yuku@0.26.0 → npm/@yuku-toolchain/linux-arm64@0.26.0

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@yuku-toolchain/linux-arm64@0.26.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
Low adoption: npm @yuku-toolchain/linux-x64

Location: Package overview

From: pnpm-lock.yaml → npm/yuku@0.26.0 → npm/@yuku-toolchain/linux-x64@0.26.0

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@yuku-toolchain/linux-x64@0.26.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
Low adoption: npm @yuku-toolchain/win32-arm64

Location: Package overview

From: pnpm-lock.yaml → npm/yuku@0.26.0 → npm/@yuku-toolchain/win32-arm64@0.26.0

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@yuku-toolchain/win32-arm64@0.26.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
Low adoption: npm @yuku-toolchain/win32-x64

Location: Package overview

From: pnpm-lock.yaml → npm/yuku@0.26.0 → npm/@yuku-toolchain/win32-x64@0.26.0

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@yuku-toolchain/win32-x64@0.26.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn

View full report

@nx-cloud

nx-cloud Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 37e941d

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 16m 16s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 50s View ↗

☁️ Nx Cloud last updated this comment at 2026-10-30 03:10:28 UTC

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

14 package(s) bumped directly, 10 bumped as dependents.

🟨 Minor bumps

Package Version Reason
@tanstack/react-start-rsc 0.1.65 → 0.2.0 Changeset
@tanstack/router-core 1.171.39 → 1.172.0 Changeset
@tanstack/router-generator 1.167.46 → 1.168.0 Changeset
@tanstack/router-plugin 1.168.48 → 1.169.0 Changeset
@tanstack/router-utils 1.162.4 → 1.163.0 Changeset
@tanstack/start-plugin-core 1.171.55 → 1.172.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/react-router 1.170.46 → 1.170.47 Changeset
@tanstack/react-start 1.169.4 → 1.169.5 Changeset
@tanstack/router-cli 1.167.46 → 1.167.47 Changeset
@tanstack/solid-router 1.170.43 → 1.170.44 Changeset
@tanstack/solid-start 1.169.4 → 1.169.5 Changeset
@tanstack/start-client-core 1.170.39 → 1.170.40 Changeset
@tanstack/vue-router 1.170.42 → 1.170.43 Changeset
@tanstack/vue-start 1.169.4 → 1.169.5 Changeset
@tanstack/react-start-client 1.168.44 → 1.168.45 Dependent
@tanstack/react-start-server 1.167.52 → 1.167.53 Dependent
@tanstack/router-vite-plugin 1.167.48 → 1.167.49 Dependent
@tanstack/solid-start-client 1.168.42 → 1.168.43 Dependent
@tanstack/solid-start-server 1.167.50 → 1.167.51 Dependent
@tanstack/start-server-core 1.169.45 → 1.169.46 Dependent
@tanstack/start-static-server-functions 1.167.44 → 1.167.45 Dependent
@tanstack/start-storage-context 1.167.41 → 1.167.42 Dependent
@tanstack/vue-start-client 1.167.45 → 1.167.46 Dependent
@tanstack/vue-start-server 1.167.50 → 1.167.51 Dependent

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: facf9d94deae
  • Measured at: 2026-10-30T02:54:39.755Z
  • Baseline source: history:da2adec76dbe
  • Dashboard: bundle-size history

The following scenarios have bundle-size changes compared with the baseline:

Scenario Current (gzip) Initial (gzip) Raw Brotli Trend
react-start.full 99.8 KiB
-47 B
99.7 KiB
-45 B
311.6 KiB
-42 B
86.6 KiB
+92 B
███████████▁
react-start.rsbuild.minimal 100.3 KiB
+1 B
100.2 KiB
+1 B
313.2 KiB
-22 B
86.6 KiB
+20 B
▁▁▁▁▁▁▁▁▁▁▁█
react-start.rsbuild.minimal-iife 100.8 KiB
+3 B
100.6 KiB
+3 B
314.2 KiB
-22 B
87.0 KiB
+1 B
▁▁▁▁▁▁▁▁▁▁▁█
react-start.rsbuild.full 103.5 KiB
-35 B
103.3 KiB
-35 B
322.9 KiB
-56 B
89.2 KiB
-148 B
███████████▁
solid-start.full 51.1 KiB
-38 B
51.0 KiB
-38 B
150.0 KiB
-42 B
45.4 KiB
-104 B
███████████▁
vue-start.full 69.1 KiB
-36 B
68.9 KiB
-39 B
197.5 KiB
-42 B
61.6 KiB
-35 B
███████████▁

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

@Sheraff

Sheraff commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator Author

🤖 Written by Claude (AI) at the maintainer's request; numbers are from this PR's perf harness and CPU profiles, reviewed by a human.

Headroom after this PR (338-route apps, median ms; real = final perf matrix at c4cb7e8; floor = every compiler output replayed, i.e. a zero-cost compiler, from the earlier 498ac9b run. The two come from different runs, so treat ±8% as noise):

app build real / floor / ours dev cold → / hydrated dev compile-all
react-start 1,634 / 779 / 855 2,561 / 1,581 / 980 2,038 / 1,055 / 983
react-router 612 / 419 / 193 1,046 / 856 / 190 1,303 / 996 / 307
solid-start 4,375 / 3,283 / 1,092 3,406 / 2,348 / 1,058 4,624 / 3,693 / 931
solid-router 1,982 / 1,875 / 107 1,490 / 1,317 / 173 4,049 / 3,764 / 285

Compiler-only work can still win at most ~0.2–0.3 s on Router and ~1 s on Start, for React and Solid alike. Solid's floor is 1.5–4× React's because of vite-plugin-solid's Babel pass, which we don't own. Anything bigger means making the bundler do less.

HMR: route, loader and server-fn edits are 15–84% faster than main. Shared-module edits are at or below main in every cell and don't grow with app size (large React Start −2.40 ms [−2.70, −2.10], Solid Start −2.15 [−2.40, −1.90], React Router −0.10 [−0.40, 0.20]; React Start 23.2/23.6/23.5 ms small→large vs main 23.4/24.1/25.6).

Where the Start second goes: the last full profile was taken before the final perf pass (Yuku 216 ms, own core 455, Start planners ~85, libraries ~12). That pass cut per-module core time by 12–23% in-process and compile-all by ~6% end to end. It took tier-0 bindings from file.imports[].local, computed the plan fingerprint lazily, made edgeKey/canonicalJson cheaper, ran hit validation in parallel and skipped emission for identity outputs. Re-profile before picking the next own-code target. Sync time in our transform hooks is ~200 ms; the rest runs after awaits, mostly resolutions.

Dev startup

  1. Lazy route modules (router-core + generator, not the compiler). Loading / transforms 488 client + 469 SSR modules because the generated tree imports every route file; ~1.35 s of that is bundler time even with a free compiler. The idea: a tree whose unmatched routes carry only path info and import() their file on first match. Est. 2.6 s → 0.7–0.9 s (react-router 1.0 → ~0.4). It would also connect the dev client earlier: Start's dev entry imports /@vite/client statically, so HMR connects only once the whole route graph is fetched. Effort high (match-time loading, types, Start manifest, HMR); risk: pathless/beforeLoad-only ancestors.

  2. Resolve less. Start's value prefetch resolves every value-referenced import through the bundler's resolver, and import protection resolves every edge: ~8.4k distinct resolutions per compile-all. The old estimate (0.3–0.4 s, from "+18% without the resolution cache") is withdrawn. That cost was every plan hit re-validating its resolutions, which re-validating only after the file set changes now avoids, at parity with the cache. A Vite resolveId settles in ~0.07 ms, so first-time resolutions sum to ≤ ~0.6 s of async time, mostly overlapped. Count and time them before building anything. Options:

    • Don't resolve bare specifiers that no alias/paths covers and that don't name a discovered Start library. On Vite they end up pre-bundled (opaque) or external (SSR) anyway. This also removes Vue's remaining shared-module HMR cost (3 resolver calls per edit vs main's 2).
    • A stat-probe fast path for .//../, with bundler fallback and a dev-mode self-check.
    • Read target markers only when a marker-bearing file exists (index once, keep it via the watcher).

    Effort low–medium; risk: resolver plugins we bypass.

  3. Gate the Vite host like Rsbuild. Done: one compile-scope rule for both hosts (a dependency is compiled only if import protection's scope includes it or its text names a Start API), asset queries excluded, no no-op rewrites. On tanstack.com this turned a +30% build regression into −24% vs main.

  4. Pre-bundle Start's late dependencies. seroval, seroval-plugins/web and cookie-es are found only while the first page is served, on all three frameworks and on main too, so the optimizer re-runs (~0.75 s measured on Vue). Adding them to the client optimizeDeps.include (nested @tanstack/<fw>-start > … form) avoids that on the first start, or after a lockfile change. Effort trivial; verify that it is on the hydration critical path for React/Solid first.

  5. Listen ~212 ms (react-start medium): generator 40, plugin import ~65 (zod 11.5). ≤ 30 ms left. Swapping zod changes public config types in three packages.

Build / compile-all

  1. One plan for both envs when nothing env-specific happened. SSR compile-all is 903 real vs 461 floor. Most route files have no server code, so their SSR plan equals the client plan. Serve the second env from the first plan when it recorded no Start candidate (edge policy is already per output). Est. 200–300 ms on Start, 0 on Router. Risk: per-env resolvers; validate fingerprints under the second env.

  2. Yuku JS side (asks drafted, not filed): O(1) indexOf, index-based node views, native side-effect query. The ~35 / 70–100 / 40 ms estimates predate tier 0 reading file.imports[].local: modules that never reach tier 1 no longer build the node→index map, so re-measure the gain before filing.

  3. Own code:

    • Placement fast path for one split group with no shared candidates (~15 ms); an invariant checker first, so it can't diverge silently.
    • Memoise dependencyPaths on the plan.
    • Drop the facts deep clone in freeze().

    Honest total ≤ 50–100 ms; re-profile first.

  4. Persistent plan cache across restarts: correct by construction (plans are pure in (env, source, config, summaries) and carry dependency fingerprints), but it skips only our part. After a restart every recorded resolution must be re-validated once, so it also pays (2). Est. compile-all 2.0 → ~1.5 s, cold 2.6 → ~2.1. Better to wait for a bundler-side transform cache (Rsbuild already replays our buildInfo), which also skips oxc/refresh (~0.6 s of the floor).

  5. Workers / Rust: no. Sync hook time is ~200 ms; the wait is resolver round-trips, which a worker cannot shorten, and summaries/registries would have to be shared. Revisit for builds only after (2).

  6. The generator's configResolved runs 3× in builds (94 ms vs 40 in dev). Run it once: −50 ms.

Memory

Dev heap after GC (react-start medium) is 87 MB after / hydrated and 72 MB after compile-all (main: 212 / 282). Ours ≈ plans 11.9 MB (1,038 × ~11.5 KB) + raw facts 0.7. Removing the resolution cache and the emitted-output memo took 5–13 MB off. Next: intern repeated dependency fingerprints and segment arrays in EmitPlan (−5…7 MB). The rest is upstream and now filed: Vite keeps module-graph nodes of deleted files (~2 KB each, vitejs/vite#23907), and Rsbuild's memfs keeps every hot-update asset (~260 KB per edit, web-infra-dev/rsbuild#8736).

Bytes

Client: Router is byte-identical; Start is −35…−47 B gzip (shorter server-fn ids). Rsbuild minimal is +1/+3 B gzip (Rspack renumbers module ids; raw −22). Server: Vite −12…−823 B raw. react-start.rsbuild.full is +432 raw / +132 gzip because the handler stays in its module, so Rspack loses scope hoisting. A cache group that merges the provider chunk into the router chunk recovers part of it, but is blocked by an Rspack panic (web-infra-dev/rspack#16289).

Route files importing another route file's export. A route file that imports an exported component or helper from another route file (import { ProductPage } from './shop.products.$handle') imports it through that route module, which the route tree loads eagerly. The route module's static re-export of the extracted module is then used, so the extracted module and its imports land in the entry chunk, even when every user is a split component (tanstack.com: ProductPage, ~7.5 KB of initial JS). When the importer is itself a route file we compile, the compiler could point the import straight at the extracted module (…?tsr-split=export~ProductPage): the route module's re-export stays unused and is tree-shaken, the binding stays lazy, with no copy and no user change. Route-to-route imports are common, so this could be a significant initial-load win. Needs a spike: HMR identity, the warning that currently tells users to move the export, Rsbuild/webpack.

Effect-free built-in constructors. A module-level new X() counts as a side effect (as in Rollup/Rolldown/esbuild), so const fmt = new Intl.NumberFormat(…) used only by a split component can stay in the route module or pin an earlier declaration (the build now warns). Sound next step: treat standard built-in constructors that are provably effect-free (Map, Set, WeakMap, URL, URLSearchParams, Intl.* formatters, TextEncoder/TextDecoder, AbortController, …) as pure when called with pure arguments, recognised by binding to the global (never when shadowed), like the TanStack pure-API table. Third-party constructors stay effectful (new WebSocket(), analytics clients and new JSConfetti() do real work). First count, across the corpus and the large apps, which constructors actually pin split-only code, to size the gain.

Route CSS stays eager, as on main; fully lazy route CSS needs a per-route asset loader in router-core. Root routes stay unsplit: splitting their error/notFound components cost SSR CPU, and the manifest preloaded that chunk on every page anyway. shared~a+b modules and export~Name forwarders cost one request each; a route-aware advancedChunks hint trades requests, not bytes. Nothing else material.

@pkg-pr-new

pkg-pr-new Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@8817

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@8817

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@8817

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@8817

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@8817

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@8817

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@8817

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@8817

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@8817

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@8817

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@8817

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@8817

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@8817

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@8817

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@8817

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@8817

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@8817

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@8817

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@8817

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@8817

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@8817

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@8817

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@8817

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@8817

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@8817

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@8817

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@8817

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@8817

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@8817

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@8817

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@8817

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@8817

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@8817

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@8817

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@8817

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@8817

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@8817

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@8817

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@8817

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@8817

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@8817

commit: 37e941d

@codspeed

codspeed Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 2 benchmarks

2 warnings, 1 note

⚠️ 49 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

ℹ️ 3 benchmarks have no flamegraph

The flamegraph wasn't generated: most of the time was spent in runtime internals, which would leave nothing useful to analyze.

See affected benchmarks

⚡ 2 improved benchmarks
❌ 2 regressed benchmarks
✅ 190 untouched benchmarks
⏩ 3 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Memory mem client navigation-churn (solid) 109.7 KB 115.4 KB -4.9%
❌ Memory mem client unique-location-churn (solid) 67.6 KB 70.1 KB -3.67%
⚡ Memory mem client interrupted-navigations (vue) 99.8 KB 93.7 KB +6.54%
👁 Memory mem server error-paths redirect (react) 12.4 KB 11.9 KB +4.34%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing compiler-rewrite/yuku (37e941d) with main (da2adec)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@Sheraff

Sheraff commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator Author

🤖 Written by Claude (AI) at the maintainer's request, from a brainstorm over this PR's architecture; reviewed by a human. Revised after later changes to the branch.

What this rewrite unlocks. Before: each transform re-parsed the previous one's output, APIs were matched by name, removal was guessed. Now: one shared analysis per module, recognition by binding identity across re-exports, edits on original ranges with exact maps, and per-output placement by reachability. Effort: S = days, M = weeks, L = a quarter.

Top picks

  1. More lint-like diagnostics with exact positions: e.g. "server fn handler closes over mutable module state", "loader imports a component-only module" → the compiler already knows bindings, which output a reference lands in, and original line:col; positioned warnings already ship for unreadable route options and options that stay unsplit, on every bundler → S–M per rule → broken splits and leaks caught at build time, clickable in the Vite overlay and Rspack/webpack output.
  2. Compile-time route manifest + preload hints: route → split modules and server fn ids come from compile results; only file names come from the bundle (today the manifest scans client chunks for route files) → M → exact Link preloads, per-route modulepreload/early hints, fewer waterfalls.
  3. createServerFn({ serializable: true }): a server fn opted into being sent to the client as data gets a real client builder and is always HTTP-callable → the builder chain is a recognised value; the HTTP-callable set is already collected → S–M → fixes client middleware never running for serialised server fns, instead of a production rejection.
    const getUser = createServerFn({ serializable: true })
      .middleware([authClientMiddleware])
      .handler(() => …)
    // returned from a loader: callable on the client, client middleware runs
  4. User planner API: Babel-free replacement for the removed compiler hooks: read the module analysis, emit edits, outputs, diagnostics → the built-in transforms already use this contract; the internal one hands planners Yuku nodes, so a public version needs a facts-only view or a promise about Yuku's AST → M (mostly the stability promise) → i18n extraction, custom rules, RSC integrations without forking.
  5. Lazy route modules (router-core): path-only route shells that import() the file on first match → the generator has a pure route model; unvisited files stay cold → L (match-time loading, types, manifest, HMR) → largest dev-startup win left (see perf comment).
  6. Automatic server/client boundary inference: split mixed modules per env; treat modules reachable only from server handlers as server-only without a marker → markers are per-file facts and placement already decides per output → M–L, needs semantics design → fewer .server.ts files and leaks.

Also enabled

Compiler APIs

  • ssr: false route code pruned from server output → placement decides per output; the route model needs each file's ssr option and its inheritance → M → no server evaluation of browser-only modules.
  • Split head/scripts (awaited after loaders, like a split loader) → a new split key is a declared root → S each. validateSearch/params schemas run synchronously during matching, so splitting them needs async validation in router-core → M–L.
  • User-defined env markers (e.g. edge-only fns) → recognisers are pluggable; marker kinds and envs are server/client today → M → compile-time pruning for custom runtimes.

Diagnostics / devtools

  • "Why is this in my client bundle?" / "why didn't this split?": root → code chain per output as JSON, plus the fallback reasons the splitter already reports → S.
  • Devtools panel showing per-route compile facts (split groups, server fn ids, import-protection verdicts) → M.
  • Compile-time Link checks (unknown routes, missing params) for JS projects and dynamic strings → route model + Link recognisable by binding → M.

Routing

  • Fully lazy route CSS, loaded on match regardless of components → route CSS stays in the route module as on main; the compiler can list each route's style imports, router-core needs a per-route asset loader → M → per-route CSS without the eager bundle.
  • Cross-file shared-module placement (one lazy module for helpers several routes need) → M → fewer duplicated bytes.

RSC

  • 'use client' reference stubs and 'use server' as planners in the same pass → per-env directive policy exists → M → one model for server fns and RSC.

Bundlers

  • esbuild autoCodeSplitting (refused today) → the core is host-agnostic and derived-module ids work on every bundler → M (side-effects handling).
  • Bun/Farm hosts → a router host is ~500–700 lines over the same core and behaviour suite (Start's is larger) → M each.
  • tsr compile CLI over a directory → S → debugging, codemods.

Maintenance: React/Solid/Vue share every planner, so features reach all three at once.

Sheraff and others added 6 commits October 26, 2026 13:18
Root routes are not split, as on main: their component, errorComponent and
notFoundComponent stay in the root route module in every env and bundler.
Splitting them added a lazy round trip to every not-found and error render on
the server, and the manifest preloaded the chunk on every page anyway. The
J2.7 goal pin goes back to a known-bug pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s change throttle

Vite's chokidar drops a second `change` of the same path within 50 ms of
the previous one, and the compiler is now fast enough that an HMR spec's
next edit landed inside that window (hmr "publishes a failed route refresh
and accepts the next HMR update", "multiple HMR saves"): the edit never
reached the bundler. The file editor spaces writes of one file by 100 ms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…(file:line:col) like the Vite host

The Rsbuild/webpack hosts ended their messages with `\n    at file:line:col`,
which the Rsbuild error overlay spec (and its readers) could not position;
both now use the Vite host's `message (file:line:col)` form. The overlay
spec's line parser reads that form too, with the same assertions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…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>
Sheraff and others added 30 commits October 29, 2026 23:13
…art on Vite and Rsbuild

`startGeneratorConfig` builds the generator configuration of both Start
plugins: the router options as the Start config parsed them (Rsbuild no
longer parses them again), the framework's target, one plugin list, and
Start's footer computed at every print. Start's Vite plugin printed the
first result of a function-form `routeTreeFileFooter` for the whole
session; it now gives the function's current lines like Rsbuild and a
router app's own footer (start-dev-route-tree-footer test: the Vite row
fails before). The Vite route tree plugin uses the shared `escapeRegExp`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te files' real paths

The generator records each route file's real path when it reads it and
publishes the registry under both the scanned and the real path (a
symlinked file or root); a real path that is another scanned route file
stays that file's. The registry view no longer resolves symlinks per
registry, `changedRoutePaths` no longer resolves each changed path (it
could not for a deleted file: the previous registry names its real path
now), and the generator's per-root real-path mapping goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vite's pre-bundled dependencies were opaque twice over: the start-env
prefetch skipped a resolved `node_modules/.vite/` id (`StartEnvConfig
.isOpaque`) besides the raw facts' `opaque` rule. The Vite host now gives
the session its one extra rule for the raw facts (`opaque`); such a
module's summary is null, so its values are unknown to the prefetch as
before. `isOpaque` goes from the planner config and the prefetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ons in the registry

The registry only inserted or overwrote ids, so on Vite an edit that removed
a createServerFn left its id registered: the dev lookup re-exported a missing
wrapper (a TypeError instead of an unknown id). The registry now keeps, per
environment, the server functions each defining module's last compile
reported; every compile replaces its module's snapshot (with none too, an
untouched module included), and the entries are the aggregate. Rsbuild's
finishMake replay replaces its environment's snapshots in the same registry
instead of keeping its own copy; a deleted file is forgotten through one
session operation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t tree drops

The route HMR runtime kept, per route id, the evaluations, split chunks and
(React) the refresh-ignored exports of every route a dev session ever saw,
also after the route was removed or renamed. When it has the router read its
route tree again, it now forgets the ids the router held before and no longer
holds; ids the router never held stay (a route module evaluates before the
route tree module that adds it runs). React's ignored exports remember their
route id and are retired with it.

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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ive in the compiler's hosts

applyViteInvalidation, liveModules and loadDerivedSource are new in this
PR and only Start uses them besides the router host: they move to
router-utils' hosts/vite.ts, so @tanstack/router-plugin/vite gains no
public exports.

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

PlanFacts.publishedSummaryDigest was written and never read; publish()
stays for its side effect. EmitPlan.sourceLength had only test readers,
which now check the published plan's revision.

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

ModuleSummary.rawRevision and hasImportOrExport had only test readers and
enter no digest; buildSummary loses its revision parameter. RawFacts'
rawRevision and ModuleAnalysis.hasImportOrExport stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No gate reads GateContext.env and no prefetch reads PrefetchContext.env or
.config since the router planners merged and start-env stopped reading
its config: the planner contract drops them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- EdgeRecord.output and the edge builder's output parameter: edges are
  keyed by output already;
- Tier1.tail: its last reader went with erasedParts' range;
- the 'region' root kind: no planner roots a region;
- PlanContext.fallbacks: planners use fallbackAt; the toy planners do too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g type Start imports

HmrStyle and RouterPlannerEnv came back with the one-planner merge and
have no importer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s environment type

StartRsbuildEnvironment was StartCompilerEnvironment field for field, a
leftover of the one Start session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AppInfo.env/.mode (plugin callbacks read root), BuildResult.bundler,
OriginalPosition.source, RsbuildPluginHost.dev and RunNodeResult.stderr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The .mdx page's warning is located in the page's source (the late
import-protection pass maps it through MDX's source map), as the next
test already requires: the first test asserts it too instead of
accepting a report without coordinates, and the comments stop calling
that a pinned bug.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments from before the one-router-planner merge said "route planners"
or "split planner"; pointers at reach.ts for rules that moved to
placement/ now name it.

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

`vite build --watch` of a Start app failed before any edit: `Could not
load \0%23tanstack-start-server-fn-resolver`. The raw facts read a
virtual module through every environment's loader in turn; in watch mode
the client and SSR builds run at once, so the live client build's
`this.load` of the SSR-only resolver registered a failed module.

The core's `loadVirtual` now receives the environment asking for the
facts (the one that resolved the id), and the Vite host loads through
that environment's loader only. The Start build harness builds Vite apps
in watch mode too (`watchEdits`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three gaps of `vite build --watch` once the first build succeeds:

- the server environment built at the same time as the client, so its
  server-function resolver listed what the client had compiled so far
  (often none): Start's build loop now waits for the client's first
  watch build, as a one-shot build does;
- every client rebuild emptied the server-function registry while the
  server environment rebuilt from it: a watch rebuild keeps it (each
  compile replaces its module's functions, a deleted file is forgotten);
- no invalidation ran on a file event, so an unchanged importer reused a
  plan validated against stale raw facts (a module an edit marked
  server-only stayed in the client): `watchChange` runs the one
  invalidation operation in build mode too.

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

The server `router` cache group merges the router's chunk with the
server-function providers' to save production bytes; its `chunks` test
ran on every dev rebuild (~1,600 calls, ~6 ms per server compilation).
The dev server needs only the shared group (one instance per module).

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

After every dev compilation `retain` re-identified every plan, raw-facts
entry and import-protection record to drop state of files the compilation
no longer depends on; with the same files as the last compilation (every
plain edit) there is nothing to drop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e first compilation only

The `finishMake` passes looking for modules restored from Rspack's
persistent cache (unjudged importers, unconfirmed edge modules) walked
every module of every dev compilation. Rspack loads the cache's module
graph only in a compiler's first compilation: the persistent cache's
`before_build_module_graph` returns for `is_rebuild` compilations, and
every compilation after the first is a rebuild (Rspack 2.0.0 to 2.2.9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… thrown, on both bundlers

Rsbuild watch builds already let the first build fail (a test relies on
it) and described the rebuild; the Vite watch builds now do the same, and
`StartBuild.firstBuild` holds what the first build reported, so a test
can check that it succeeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…build wants them

Like the router plugin's Vite host: a build without `build.sourcemap`
reads no map, and import protection locates violations through the input
map and the raw files, not through Start's output map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… again when its import changed meanwhile

A module the cache restores must be compiled again when a module it
imports changed what it is while no build ran (here: it became a server
fn builder, so the handler built on it leaves the client).

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-less SWC code frame

Rspack 2.1.6 (web-infra-dev/rspack#14921) prints SWC's code frame header as
`[line:col]`, without the file, which the overlay shows on its `File:` line.
Since 5ebce55 moved the e2e apps to Rspack 2.2.9, the server entry's line
read as null.

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