Repository navigation
Conversation
The dev refresh marker (`page.$$tsrRefreshId = …`) was written onto the export of another module when a split option named an imported binding (or a member of one): a frozen component threw "object is not extensible". React Refresh registers a component where it is declared; the marker is only written on this module's own declarations. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- dev `log: 'always'`: a compile the plan cache answered reported its violations twice and asked `onViolation` again (`afterCompile` already reports every compile; the extra `reportAgain` is gone). - `needAdditionalPass` is a bail hook: answering `false` silenced later taps asking for a pass; the tap now answers `undefined` when it has no vote. - the warning ledger resets on `watchRun` too, so a module a dev compilation rebuilds reports its compiler warnings again. - modules kept across a compilation report again under their canonical resource (the RSC proxy query is dropped), the identity their edges were registered under. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ck soak pages; router-utils import order Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `require()` edge carried no names, so import protection's mock of a
denied CommonJS or unknown target exported `default` only and
`const { secret } = require('./x.server')` read `undefined` instead of
the safe mock. The analysis records the keys the importer destructures,
the static member reads of the binding holding the result or of the call,
and `'*'` when the result escapes (`[]` for `import()`); the edge records
of both tiers carry them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ute module (#8688) The router calls a method or function-expression callback as a method of the options object; a split chunk's `lazyFn` wrapper calls the chunk's export detached, changing its `this`. Such a callback stays in the route module with a `not-split:this-reference` warning naming the file (J2.8: informed, never broken); `this` of nested functions and classes, and arrow functions (the module's `this` wherever they evaluate), split as before. Also enables route HMR in the frozen-import regression test, where the development marker is written. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nner Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
In dev, an HTTP call by id may run any server function: dev compiles lazily, so it cannot know what the client references, and a page left open across a restart must keep working (J4.4). The refusal of server fns no client code references applies to production builds only. SPEC J4.2 and G13 say so; the Vite dev RSC pin that asserted the refusal (and its now unused page helper) is removed, and KNOWN-BUGS lists the production pin only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…duction builds with a separate provider (R44) With RSC the provider environment builds before the client (plugin-rsc: rsc, client, ssr; Rsbuild: interleaved by the RSC coordinator), so its manifest was generated before the client's references were known and every registered server function was HTTP-callable in production (J4.2). The manifest must still list every function: server-origin calls (the SSR RPC stubs, a handler calling another function) resolve through it. The gate is therefore by call origin, decided where the client build's references are known: - Vite: the caller (ssr) environment builds last; its resolver module is now a gate over the set of client-referenced ids in front of its own forward to the provider (served by a second virtual module). - Rsbuild: the manifest carries a placeholder literal for the set, which the server asset pass that already fills the Start manifest (after the client build exists) replaces with the client-referenced ids. - Rsbuild RSC also never listed a function reached only through a provider module (the resolver's rebuild is what builds those modules, after the manifest was generated): the resolver is now regenerated and rebuilt while its content changes, bounded. Tests: react-start-rsc `rsc-start-build` and `rsc-start-rsbuild-build` "a server function only server code calls is not callable from the browser" (the real request envelope sent to the private function's URL is refused, to the exposed one answered; the handler's own call of it works). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…may answer any server fn Takes the oracle's deletion of react-start-rsc rsc-start-private.test.ts (conflicted with our comment update on its pin) and the rsc-start-page.ts trim; nothing else imports the removed `page` helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… plan's violations once The compile that registers a plan's violations prints them; a compile the plan cache answered again (an importer Rspack rebuilt for a dependency's edit) still asks `onViolation`, as every compile does under `always`, but no longer prints them a second time. The warning ledger keeps its once-per-run contract (the oracle pins "warns once per file" across dev rebuilds), so the `watchRun` reset is dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n compiler stats Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss and perf fixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lation's files only Rspack reports a deleted file only while a module depends on it, so a module the app stopped importing and then deleted got no unlink: its plans, raw facts, import-protection records and per-resource fingerprints stayed for the session (soak: plans +20, fingerprints +100 per 100 cycles). After each dev compilation the session retains what the environment's fileDependencies still contain (RawFactsCache.retain, ImportProtection.retain). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… growth Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A known package resolves to its own specifier (D4), and dependencyPaths turned it into a file for the host's addDependency: Rspack watched '@tanstack/react-start' as a missing path, reported it removed, and rebuilt every environment once after the first watch-mode build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nded its compiles on Rspack's MultiCompiler runs a compiler again when a dependency was invalidated while it compiled (running-outdated), from nodeDone on the next tick after its done hooks; the MultiCompiler's done (onAfterBuild) fires before that. The tracker settled the edit and closed the watcher in between, so the SSR bundle kept pre-edit code (start-compiler-build- inputs 'rsbuild build --watch > an edit of an aliased env-only factory...'). A compiler now counts as compiling until a macrotask after its afterDone, by which a re-run has announced itself (invalid). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ause fix Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> # Conflicts: # packages/router-utils/tests/pipeline.test.ts
Replace the 15 lane changesets with six: compiler core (router-utils), route generator, router plugin, route HMR runtime (framework routers), Start, and React Start RSC. Adds what lanes C2/C3a left out: dev-only /hmr subpaths, production HTTP gating of server-only server fns with the dev warning, #8688 and #7943. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- esbuild: autoCodeSplitting is refused; the esbuild installation docs and quickstart examples no longer enable it - automatic code splitting: callbacks reading this stay unsplit; route CSS moves with the split components only when all are split - Start: server functions only server code calls are not HTTP-callable in production (dev answers and warns); generateFunctionId arguments and the default ids; onViolation timing in builds; route CSS loading Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for the compiler core - router-utils/src/compiler/README.md: the model (one analysis per env and module, rewrite tree, reachability = splitting = DCE by provenance, OutputBuilder emit with source maps, summaries and values), the pipeline phases, plan cache + dependency ledger + invalidate(), hosts, where each planner lives, adding a planner, tests and perf harnesses - router-plugin/ARCHITECTURE-CODE-SPLITTING.md: replaces the Babel-era document with the planners' outputs, rules and route HMR - AGENTS.md pointers (packages, router-utils, router-plugin, start-plugin-core) and the Rsbuild architecture links; drops references to handleRouteUpdate and to deleted INTERNALS.md files Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es and knob runs; document soak and compiler knobs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erprints Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
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. Comment |
🦋 Changeset detectedLatest commit: 78db3da The changes in this PR will be included in the next version bump. This PR includes changesets to release 24 packages
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 |
Remove the unused logDiff (with ansis and diff) and createIdentifier/decodeIdentifier exports from router-utils, and router-plugin's redundant chokidar dependency: the watcher lives in router-generator, which declares it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
revision/digestOf (plan freshness, digests, edge keys), the default server function id and the Hydrate file hash use crypto.hash instead of createHash().update().digest(). Same bytes; available since Node 20.12 (engines: >=20.19). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…remapping and lightningcss on first use jiti is only needed for virtual route configs, @jridgewell/remapping only when a Rspack/webpack loader receives another loader's input map, and lightningcss only for inline CSS with rewritable urls: none of them loads at plugin startup anymore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y binding and through re-exports (R79) `RecognizerTable.pureCalls` lists exports of known packages whose call is pure when its arguments are. Tier 0 recognises imports of them by binding (renamed, or a namespace member) before any statement is classified; phase A follows an import of a project module the module calls at its evaluation through re-exports over summaries (a package is never read: a third party's purity is never assumed). Known packages' summaries flag those exports `noSideEffects` (no longer the factories', which nothing read) and export resolution carries the flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… effect in route modules (R79)
One table (router planner, `routerPureCalls`), per framework package:
getRouteApi, linkOptions, createLink, createRouteMask, lazyRouteComponent,
lazyFn, retainSearchParams, stripSearchParams, parseSearchWith,
stringifySearchWith, createRouterConfig (React, Vue). Read in every
framework: each constructs and returns a value, reads no global state,
registers nothing and never throws. Excluded: redirect/notFound (mutate
their argument, throw on request), createSerializationAdapter (throws on
an invalid key in development), the route factories (route units).
The router plugin's session knows the router packages by specifier, and
its hosts pass the bundler's resolver (Vite plugin context / dev
environment container, webpack/Rspack loader `getResolve`), so an app
module re-exporting an API is followed. Start's recognizer reuses the
table.
Tests: tamery's shape (an effect, `const { useParams } = getRouteApi(…)`,
an inline `validateSearch` call that stays) loads the API with its chunk
and warns only of the effect, directly, renamed and through an app
re-export; an impure argument keeps the call an effect; an API the route
module keeps no longer pins an earlier effect; `Route.useParams()` in the
split component unchanged. Vite, Rspack, webpack, Rsbuild; Start Vite and
Rsbuild.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… raw-facts build-time changes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…raw facts; dependencies opaque to summaries; no parentOf on hot paths): t3/reduce-compiler-allocations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re APIs by binding only; app re-exports are not followed Maintainer decision: drop the phase-A re-export follower and everything it needed: `seedPureImports`, the `noSideEffects` flag on known facts' pure exports and on export resolution, the router plugin hosts' resolvers (Vite plugin context / dev environment container, webpack and Rspack loader `getResolve`): the router plugin resolves nothing again. Direct imports from the router packages (renamed, namespace members) stay pure callees in Tier 0. Tests of the removed behaviour deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e the file set is unchanged The value analysis of every module that imports from a barrel follows the barrel's export-star chain, and nothing shared the resolver's answers across compiles: a monorepo importing a workspace UI kit from ~1,100 modules made 58k host resolves for 13k unique (env, importer, specifier) triples, +9% build wall and +13% peak RSS vs main. The environment's PlanCache now memoises the resolver's answers, importer → specifier → canonical id (the pending answer while it settles, so concurrent compiles share one call), for one fileSetVersion: the same authority that already re-validates the plans' recorded resolutions, dropped as one when a file appears or disappears and released with the environment's plans. Unresolved pairs are not kept (the file may be about to appear; the add event re-resolves them). The keys and ids are strings the compiles already hold, so the memo costs its Map entries only. In dev, each resolution still yields one event-loop turn, so a dev server's pending requests get in between an update's resolutions as they did while every resolution was resolver I/O. Host resolves equal the unique resolved triples; the app builds at parity with main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…solution): t3/share-analysis-resolutions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on view, nested server-fn order, opaque digests - `#__NO_SIDE_EFFECTS__` on a callee the module declares counts whatever the order of declaration and call (function declarations are hoisted) and in Tier 0 too: one seeding (`seedNoSideEffectDeclarations`) before any statement is classified; units.ts no longer marks callees as it goes. - `codeSplittingOptions.defaultBehavior: []` splits nothing unless `splitBehavior` or the route says so, as on main. - The mutation model takes the output's liveness filter (R1): a write inside code the environment erases imposes nothing; reach.ts and the route planner read the env's live view (no spurious `eager-export:reassigned` on the client for a server-only write). - Server-fn sites are planned in source order, so a nested site inside a stubbed handler is dead there whichever import binding declares it. - An opaque package's facts keep a null summary after `getTarget` (its digest never changes); `getTarget` never answers a read in flight. - The fallback-rounds bound is the region count after the first round (regions accumulate across rounds; the guard could not fire before). - Prose: README, eager-exports.ts and the warning state one rule for what chunks copy and what they import. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nked and un-planned route files, shared tsconfig reader - Start Vite bundled dev: `watchChange` runs in one environment and applies the one invalidation to every environment's module graph (no per-env eviction that left the other environments' dependents stale). - Start Rsbuild: the session dedupes a watch cycle's file events across the environment compilers (path, kind, mtime, size). - Registry changes: `changedRoutePaths` lists the real path of a symlinked route file too; the webpack-family hosts (router Rspack/webpack, Start Rsbuild) tell the watching compiler the files changed (`invalidateFiles`), so modules whose rules or plan changed are built again; the Vite router host hard-invalidates the changed file's own modules (a file that was no route has no plan to evict). Start's Rsbuild generator reads the registry through the shared `RouteRegistryView`. - Both Start hosts read `verbatimModuleSyntax` through the tsconfig `extends` chain (one reader, `src/tsconfig.ts`). - Start's Rsbuild `finishMake` rebuilds (client tree, import-protection re-judge) share one helper (`rebuildModules`). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tion, align prose - The eight `.fails` pins asserting that a split chunk renders the instance importers get are deleted: the chunk copies a pure exported binding by rule (R77, "Fix: none planned"); the rendering rows next to them stay, and the `%s` errorComponent rows assert rendering and "split or reported" only. - Warning assertions in the four new files check position and count, not wording (harness rule); parse warnings are deduped like every other warning (code and wording). - Prose aligned with the code: KNOWN-BUGS (R77 entry), planners/hmr.ts, core/config.ts `@default`, split.ts refresh identity, diagnostics.ts, server-fns/ids.ts rank under RSC, generator comments, router-core hmr.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… compile Dead code (audit §3 and a symbol-occurrence sweep): RewriteSet.claimLive, DependencyLedger.paths, WarningLedger.reset(path), unread fields of the split decisions and route recognition, the HMR planners' second rename table (PlanContext.nameOf replaces it), CompileRequest.env.routerSplit, the `-start` alternative of ROUTER_PACKAGE_RE, the duplicated HmrStyle, import-protection's old ImportGraph/buildTrace and unread members, Rsbuild R72 remnants and copied rebuild/route-digest helpers, unused Queue and generator helpers, FunctionIdAllocator.has/get, ServerFnRegistry.tokenOf, StartEnvFacts.useClient, the duplicated provider self-accept unit. Less work per compile: the route-registry fingerprint is the route id (three canonicalJson+sha1 per compile gone); edge keys are a plain join (sha1 only for edge-module ids); the specifier segment of a substituted edge is cut at substitution instead of per edge per output at freeze (markSegment and the Tier 0 marker deleted); the units of each import are listed once per plan instead of filtered per import per output; the dependency paths of a compile are computed on demand (only the Rspack and webpack hosts read them). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A bare import or a re-export evaluates before the module body wherever it is written: `effectStays` no longer counts one after an effectful cluster a chunk needs (it moved only when nothing followed it) or before an augmented export (it stayed, `eager-split:order` / kept). Two placement tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…es; delete the tsconfig reader R80. Whether TypeScript erases a value import whose bindings are unused or only used as types is the bundler's transform's decision (oxc reads the nearest tsconfig through references/extends unless its options override it; SWC never reads tsconfig). The root tsconfig reader guessed it, and was wrong for the stock Vite template (solution-style root: the real server module was bundled unreported) and for Rsbuild (false reports, or a leak under `tools.swc` verbatim). The rule verdict and the substitution stay in the pre-TS plan. In dev such an edge is substituted in both behaviours and recorded pending; it is decided (`onViolation`, report, or the error) when the bundler requests its edge module, which only post-TS code does: Vite's edge `resolveId` (import analysis, Rolldown), Rsbuild's edge-module rule loader. A veto serves the allowed form; an `error` violation fails every request. Build is unchanged (the R30 witness: an erased edge module is absent from the bundle). Tests: v2-witness (the five tsconfig layouts x Vite/Rsbuild x build/dev, the oxc/SWC option rows, error on every request, dev vetoes). Deleted: the planner's tsconfig rows and the v2-type-only verbatim-through-extends rows (the Rsbuild dev row pinned the over-report; the Vite rows duplicate the v2-witness extends layout). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the persistent cache again An erasable edge is decided when Rspack builds its edge module. A dev server started on its persistent cache restores that module without a build, so the violation went unreported in the new session (and a previous session's veto decided its code). At `finishMake`, after the restored importers are compiled again, an edge module whose pending violations this session never confirmed is rebuilt: the edge-module loader decides them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ule code on demand Locating a violation asked Vite for the combined input map; in a pre transform with an empty chain Vite builds an hires identity map over the whole input (~70 us per module, 85 % of the cost of an erasable violation nothing requests). The input is compared with the file as written first: a match has no map to read. Edge-module code is computed only when a listener takes it (Rspack's virtual files); Vite builds it at `load`. IP-heavy fixture (48 erasable denied imports), paired 10 rounds: TS-erased dev +0.6 +/- 1.7 %, build +0.7 +/- 3.9 % vs 6aee390 (was +1.9 % dev). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ler keeps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dependents-index row per path per compile DependentsIndex.add is only ever called for in-flight rows: drop the parameter and its dead branch. The ledger adds a path's row once, not once per fingerprint of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
guardStatementStart took the '/' of a generated '/* @__PURE__ */' annotation for a token that could continue the previous statement, so every lazyRouteComponent replacement walked the AST from the root to find whether it starts a statement. It now reads the first token past leading comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A barrel's re-exports were deduplicated with Array.includes per specifier (quadratic: 26 ms for a 2,000-module barrel). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… edit Like the split and Hydrate planners: without an edit or claim of its own the environment planner erased nothing, so no declaration became orphaned by it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ON copy Every freeze round-tripped the planner facts through JSON to keep cached plans to plain data. Planners build fresh facts per plan; the verify pass (TSR_COMPILER_VERIFY, on in every test run) now fails a compile whose facts are not JSON data, so the rule is checked where it is broken instead of paid for in every compile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ver envs only Covers the order the coherence review suspected (the nested chain's import binding declared first, so its site is enumerated before the outer one): the client stubs the outer handler and registers the outer fn only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…renders in its owner The env render tree decided claim liveness with the core's statement-level DCE edits included. When placement drops the statement of a function that holds a hoisted nested server fn (a route component moved to its chunk, or dead code that would pull a chunk's dependency), the handler's claim died with it while the hoisted chain still rendered: the SSR route module shipped the raw handler (`.handler(rawFn)`, its reads unbound, the lazy loader unused), and every call failed. Reach decided liveness on the planner tree, so the handler module existed too. Claim liveness is now decided by planner edits only. The verifier gains check (g): no output reads a module binding of the source it neither declares nor imports (it fired on this case). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tput in verify (d) The verifier skipped `custom` outputs like a copying provider output when counting effects, but the in-place provider's lazy handler module imports from the env module: an effectful cluster only a handler reads moves there (R29) and was reported as "emitted 0 times". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every output of a file carried the whole file text in its map, and Rspack/webpack hash a module's map with its code: an edit anywhere in a file hot-updated every output of it. A `<Hydrate>`-content edit on Rsbuild re-emitted the unchanged route module and split chunks, whose route HMR refreshed the route (loaders, server fns): +250 ms per edit on the react-start medium app vs main. Main's Babel maps had no sourcesContent either; Vite's dev server fills it in from `sources`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…revision Rspack/webpack hot-update a module only when its output changed. A handler-only edit left the client stub `createClientRpc(id)` unchanged, so the route kept its loader data from the old handler (Rsbuild dev-server test "the next call runs the edited handler"); it passed before only because every output's map held the whole file. In dev the stub now also names its file's revision (an argument the runtime ignores): the client module of a file declaring server fns changes with every edit of the file, as Vite updates every module of the file; other outputs stay byte-stable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing it 6e14f86 dropped markSegment, which also skipped an edge whose specifier no slice of the output held. Dynamic edges had no other check, so every output recorded every `import()` of the file: a route's server module judged the `lazy(() => import('./X.client'))` its split component holds (or no output holds: `ssr: false`), and in dev `behavior: 'error'` failed that route module, so every page of the app failed. The edge is recorded again only where a slice of the output holds its specifier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…their suspension, not on 5 ms (f) asserted that the edge-policy verdict was asked 5 ms after the compile started; on a loaded or slow runner (CI Linux arm64) phase C is not reached within that window (the dev resolution yields add macrotask turns), so the check saw no call. Each test now awaits the point it suspends at (the verdict asked, the planner's prefetch, the host's resolution); the assertions are unchanged. Reproduced 3/80 with 16 concurrent runs on two CPUs before, 0/208 after. 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. 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-eliminationand 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.?tsr-split=…,?tss-hydrate=…,?tss-serverfn-split,?tss-serverfn-handler) and facts (import edges, server-fn registry, diagnostics, dependencies).start-env → hydrate → rsc-css → route-hmr → route-split → import-edges. APIs are recognised by binding, through an abstract value domain. Cross-module summaries come from raw text, never from the bundler pipeline.OutputBuilderwrites VLQ maps directly and composes them with input maps.?tss-serverfn-handler, which loads on the first call. The defining module runs once.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.invalidate(); no Yuku objects are retained. We own the hosts: a native Vite plugin, a Rspack/webpack loader, and Start Rsbuild loaders (withbuildInforeplay for the persistent cache). unplugin remains only for the generator and esbuild.Performance
Measured with
benchmarks/compiler-perfat774eba05a4against mainda2adec76d. Cells marked ² were re-measured ate80188ee7b. 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.³ On main, this HMR run aborted in 5 of 6 rounds and full-reloaded 13 edits. This PR had no failures and no reloads.
e80188ee7bvs npm main, 6 paired rounds):064d29003a, 129 apps, medians of per-app Δ): build −12.1%, dev ready −18.8%, first render −10.5%, peak RSS −1.7%.774eba05a4).e80188ee7b); kroma +3.8% (+64 MB,774eba05a4).b25984c166): +308 B raw on Vite*.full, +696 B raw on react-start.rsbuild.full.e80188ee7b).All 11 cells, HMR, Vue, memory, bytes
Medium = 333 route files. Δ CIs are 95% t intervals of the paired log ratios.
¹ react-start plus a generated dependency graph and tanstack.com's
manualChunks. Dev SSR fails on both sides here because of a?urlwasm asset in the harness, so<Hydrate>HMR is not measured.<Hydrate>is −8…−46% on Vite and −30 / −38% on Rsbuild ². Shared-module edits show no significant slowdown.eedc638e42).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).fdc48254c8).eedc638e42): webpack build −38%, RSC build −74%.498ac9be17): 715 → 94 ms. The react-start build floor drops from 1,629 to 779 ms.b25984c166):*.fullscenarios are −35…−47 B gzip, thanks to shorter server-fn ids.*.fullscenario. react-start.rsbuild.full is +696 raw / +326 gzip:__roota second concatenation root;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.80d1890bed).fails)b25984c166: all 8 unit and integration targets pass (4,247/4,247). WithCOMPILER_TESTS_PINS=run, each pin fails for its documented reason.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:
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 (
b25984c166vs npm main):390ca815c3.(/[;@babel/traversecopies, which break main's DCE.Leak check (139 builds per side,
5ff36a1816): 0 leaks here. Main leaks inmockmode; that case is pinned.Fuzzers:
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)
sideEffects, an exported component another route's chunk imports loads with that chunk..pageasjavascript/auto) importing a denied file is reported. Rspack rules select by resource.bundledDev, a factory that becomes a server-fn builder keeps its handler. This needs the full-bundle API.Behaviour changes
routeTree.gen.tsformatting changes once. The file is printed in yourquoteStyle/semicolons.enableRouteTreeFormattingnow defaults tofalse(trueuses Prettier, loaded lazily). Custom templates are inserted as written.serverFns.generateFunctionIdreceives the stable key (greetUser) asfunctionName, and default ids are shorter.?tsr-split=export~Name), together with their augmentations. On one corpus app: −179 KB initial, +2.4 KB total.this(Handle route callbacks that reference this during code splitting #8688), and splits that never settle.onViolationreturningfalsegets the real module;fullPath:'/', not''.spa.enableddev serves the shell;server.compressis no longer forced;@rspack/core≥ 2.2.9 (fix: reset MultiCompiler done state on watch restart web-infra-dev/rspack#15865); peer ranges are unchanged.autoCodeSplitting: truethrows;@babel/coreis no longer transitive, so declare it if a plugin needs it (e.g. React Compiler).codeSplittingOptions.compilerPlugins, StartcompilerTransforms,serverFnProviderModuleDirectivesandStartCompilerPlugin;react-start-rsc/plugin/rscCssTransform, replaced byrsc: { loadCssExpression, providerDirective };/compiler);format,inferFullPath,checkRouteFullPathUniquenessandwriteIfDifferent.onRouteTreeChangednow receives{ model }.@tanstack/router-core/hmrand@tanstack/{react,solid,vue}-router/hmr.16 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
createServerFn({ serializable: true }).__rootcache group: unblocked now that the Rspack panic (rspack#16289) is fixed.tsr generatewrite different routeTree.gen.ts (Start footer added, then stripped) #8437 CLI half, server routes with a.tsextension gets the wrong boilerplate while creating the file. #4462, Vite HMR blank page when editing__root.tsx(Solid SSR) #6331 (solid-refresh#91), [@tanstack/react-start/plugin/rsbuild] tanstackStart() eclipses the client environment's output.assetPrefix, blocking downstream plugins #7827 andssr: falseroutes still bundle components in server output #6400.Upstream issues filed
✅ fixed · ✖ closed without a fix · ○ open
Yuku: 26 ✅ fixed, 1 ✖ closed. Four of the fixes (Duplicated render after
nevigate#288, Routes transitions don't use transitions? #289, How to resolve act(...) errors in components containing Link #291, Update peerDependency react-location to match higher versions #292) land in the next Yuku release. fix: render errorElement when route loader is rejected #290 was closed because the fix is deferred to Yuku's upcoming transformer; we don't need it.All 27
✅ #202 purity comments · ✅ #203 codegen SIGBUS · ✅ #204 JSX refs for
_/$/Unicode · ✅ #229import X = ns.Y· ✅ #230 decorators afterexport· ✅ #231 JSXText entities · ✅ #232 false duplicate export · ✅ #237 empty JSX container comments · ✅ #244 WASM stale bytes · ✅ #247unwraptyping · ✅ #248copyAst()· ✅ #251export {} from· ✅ #252import()specifiers · ✅ #253 big-endian decoding · ✅ #254 JSX attribute mapping · ✅ #255 postfix++parens · ✅ #256for (init;;)no-in· ✅ #257 private method overloads · ✅ #258import type {}· ✅ #259super()in computed key · ✅ #260 JSX text after</>· ✅ #287(a?.b)!.c· ✅ #288 JSXText escaping · ✅ #289 stale JSXTextraw· ✖ #290stripkeeps type-only exports · ✅ #291 surrogate-pair spans · ✅ #292 Annex B functionsVite:
Rolldown:
this.load()returns null in bundled-dev HMRmoduleSideEffectsin watch modeMissingFactoryErrorRspack:
Rsbuild:
solid-refresh (all ○):
__root.tsx(Solid SSR) #6331)Others:
\ncounts as a line breaktypeof xkeepsxencodeInto('')returns stale dataIssues
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 #8544Other issues:
tsr generatewrite different routeTree.gen.ts (Start footer added, then stripped) #8437, partially addressed: Start's footer followsquoteStyle/semicolons, buttsr generatestill writes none.strictExecutionOrder: trueonssr.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/baselinebranch 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.router-utils/src/compiler/README.md, then readidentity→source/analysis→units/mutation→rewrite→reach→plan→emit→values/summary/facts→cache/pipeline→verify. Keeptests/helpers.tsandtests/placement.test.tsopen alongside.router-plugin/ARCHITECTURE-CODE-SPLITTING.md, thensrc/planners/.start-plugin-core/src/planners/(start-env,hydrate,rsc-css,edges), withsrc/compiler/,src/server-fns/andsrc/import-protection/.router-plugin/src/hosts/.src/vite/compiler-plugin.ts.src/rsbuild/(readCOMPILER_ARCHITECTURE.mdfirst).router-generator(AGENTS.md,analyze/,model/,print/).router-core/src/hmr.tsand{react,solid,vue}-router/src/hmr.ts.start-client-core/src/createServerFn.ts.README.md,SPEC.mdandKNOWN-BUGS.md.To reproduce the measurements, see:
packages/compiler-test-utils/oracle/README.mdbenchmarks/compiler-perf/README.md(run.mjs,compare.mjs,soak-analyze.mjs)pnpm benchmark:bundle-size:runscripts/benchmarks/bundle-size/server.mjsThis 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