Repository navigation
Conversation
…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>
…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>
4 of 5 tasks
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-eliminationand the Prettier hot path are gone.Why:
Architecture in brief
Full map:
packages/router-utils/src/compiler/README.md.?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.OutputBuilder(direct VLQ maps, composed with input maps).invalidate(): validated reuse, per-env resolvers, no Yuku objects retained (heap test).buildInforeplay 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)80d1890bed).fails)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 atc4cb7e889a; since then only router-generator's test harness changed, and its integration tests also pass atd0cb0e3ff6.fdc48254c8adds 2 passing tests (per-environment release, below) and changes no other result.The 45 remaining pins:
shows-component/-error/-not-found/-pending/-nothing; Vite + Rsbuild, dev + build)errorComponent/notFoundComponentloads lazily (Vite, Rspack, webpack, Rsbuild; with and withoutsideEffects)sideEffects: an exported component another route's chunk imports loads with its chunk.pageasjavascript/auto) importing a denied file/specifierimport()inside it (unreferenced file)bundledDev: a factory that becomes a server-fn builder makes the client drop the handlerUnit 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 (
PRworkflow, Test job =test:pr: affected lint, unit, integration, e2e, types and build targets) at6b90fe7291: 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
6b90fe7291andd0cb0e3ff6. CodSpeed atd0cb0e3ff6: 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 (
c4cb7e889avs mainda2adec76d; harnessbenchmarks/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.
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. Atc4cb7e889athe 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/hmrsubpath: 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.tsimport added and removed, and a factory that stops or starts exportingcreateServerFn. 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]:ArrayBuffersThe 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)
buildEndfix 98.7 MB, this PR 64.3 MB (fdc48254c8). When an environment's production build ends, its plans and import-protection state are freed; dev and watch are unchanged.Bytes
Client (
pnpm benchmark:bundle-size:run, 18 scenarios, atd0cb0e3ff6vs mainda2adec76d; byte-identical atfdc48254c8):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, atd0cb0e3ff6vs main; identical atfdc48254c8):__root.tsx, which becomes a second concatenation root. Rspack then cannot scope-hoist the router modules__rootshares 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 atd0cb0e3ff6) is within noise (−2.1…+3.5%). The*.fullVite servers load one more chunk (the route module shared with the resolver), per D6.Behaviour changes / migration notes
routeTree.gen.tsformatting changes once. The generator prints the tree directly in yourquoteStyle/semicolons.enableRouteTreeFormattingnow defaults tofalse; set it totrueto keep Prettier formatting (Prettier is then loaded lazily). Custom scaffolding templates are inserted as written.serverFns.generateFunctionIdreceives the stable key (greetUser) asfunctionName, notgreetUser_createServerFn_handler. Default production ids are shorter.autoCodeSplitting: truethrows; the generator still works with esbuild. Docs and the esbuild quickstart examples are updated.vitepeer>=6). CommonJS consumers (awebpack.config.js/rspack.config.jsthatrequire()s the plugin) need Node 20.19+ or 22.12+.codeSplittingOptions.compilerPlugins; StartcompilerTransforms,serverFnProviderModuleDirectives,StartCompilerPluginand related types;@tanstack/react-start-rsc/plugin/rscCssTransform. RSC integrations passrsc: { loadCssExpression, providerDirective }.parseAst,generateFromAst,deadCodeElimination, …). It gains@tanstack/router-utils/compiler. router-generator no longer exportsformat,inferFullPath,checkRouteFullPathUniquenessorwriteIfDifferent, andGeneratorPlugin.onRouteTreeChangedreceives{ model }.@tanstack/router-core/hmrand@tanstack/{react,solid,vue}-router/hmr(the route HMR runtime; nothing reaches production).component,errorComponentandnotFoundComponentstay in the root route module.this(methods/function expressions) stay unsplit with a warning (Handle route callbacks that reference this during code splitting #8688).onViolationreturningfalsegets the real module (fixed-point rebuild). Traces follow the bundler graph, and reports point at the import.spa.enableddev serves the shell,server.compressis no longer forced, and the server env shares async chunks.@tanstack/vue-router > @tanstack/vue-storeis 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-externalsandrouter-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)
createServerFn({ serializable: true }).loadRouteChunk. Route CSS placement is unchanged from main; the 20 route-CSS pins wait on this.tsr generatewrite different routeTree.gen.ts (Start footer added, then stripped) #8437 CLI half (tsr generatewriting Start's footer); server routes with a.tsextension gets the wrong boilerplate while creating the file. #4462 (.tsserver-route scaffolds); Vite HMR blank page when editing__root.tsx(Solid SSR) #6331 (Solid SSR root HMR: solid-refresh bug, reported upstream as solid-refresh#91, nothing in this repo); [@tanstack/react-start/plugin/rsbuild] tanstackStart() eclipses the client environment's output.assetPrefix, blocking downstream plugins #7827 (Rsbuild final-config ordering).ssr: falseroutes still bundle components in server output #6400 (ssr: falseroute code pruned from the server: needs inherited-ssrknowledge, design in notes); with RSC, a server-fn module's state exists once for server fns and once for server components (allowed; docs note).Upstream issues filed
✅ = fixed upstream.
_/$/Unicode names, ✅ #229import X = ns.Yreference space, ✅ #230 decorators printed afterexport, ✅ #231JSXText.valueentity decoding, ✅ #232 false duplicate export (type + value), ✅ #237 comments in empty JSX containers, ✅ #244 WASM stale bytes for empty source (WebContainers), ✅ #247unwraptyping, ✅ #248module.copyAst(), ✅ #251export {} from "m"module record, ✅ #252 parenthesized/templateimport()specifiers, ✅ #253 big-endian AST decoding, ✅ #254 JSX attribute value mapping, ✅ #255 postfix++parentheses before a line comment, ✅ #256for (init;;)no-inleak, ✅ #257 private method overloads, ✅ #258import type {} from 'x', ✅ #259super()in a nested computed key, ✅ #260 JSX text after</>, ✅ #287(a?.b)!.cprintingraw, #290stripkeeps type-only exports, #291 diagnostic span splits a surrogate pair, #292 Annex B block functionsthis.load()returns null during bundled-dev HMR, #11223 watch mode keeps stalemoduleSideEffects: false, #11300 bundled-devMissingFactoryError(this PR carries main's importer fallback)__root.tsx(Solid SSR) #6331)\nas a line breaktypeof xtype query keepsxaliveTextEncoder.encodeInto('')returns the previous resultIssues
server.handlersstrip — server code and node-only imports ship in the client bundle #8514autoCodeSplitting: falsedoes not avoid it) #7754Split…when two @babel/traverse copies are installed #8715bundledDev: true#7535.andhandler/server/client, silently disabling transform after AST-roundtripping pre-plugins #7280autoCodeSplittingis enabled #6667server.compress: false, overriding user config #8544Partially addresses #8437 (Start's footer follows quoteStyle/semicolons;
tsr generatestill 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.0peer 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
__exportAllhelper is placed in a dynamically imported chunk); workaround:strictExecutionOrder: trueon thessrenvironment.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/baselinebranch is +102k/−42k in 1,468 files. Most of it is tests (+103k/−41k inpackages/*/tests, +16k in the newcompiler-test-utils) and regenerated route trees. Product code is +39k/−22k in 231 files. Suggested order:packages/router-utils/src/compiler/README.md, thenidentity→source/analysis→units→rewrite→reach→plan→emit/output-builder→values/summary/resolve/facts→cache/host/pipeline. Read alongsidetests/helpers.ts(toy planners, memory host) andtests/placement.test.ts.router-plugin/ARCHITECTURE-CODE-SPLITTING.md,src/planners/(route→decisions→split→hmr).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/.router-plugin/src/hosts/; Startsrc/vite/compiler-plugin.ts(+planning.ts,bundled-dev.ts) andsrc/rsbuild/(COMPILER_ARCHITECTURE.mdfirst).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.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
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