Repository navigation
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 Walkthrough
Merge Risk: 🔵 Low · up to
|
|
View your CI Pipeline Execution ↗ for commit cd22d48
☁️ Nx Cloud last updated this comment at |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
🚀 Changeset Version Preview7 package(s) bumped directly, 3 bumped as dependents. 🟨 Minor bumps
🟩 Patch bumps
|
Bundle Size BenchmarksThis pull request does not affect bundle size in any measured scenario. |
Merging this PR will improve performance by 4.1%
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
Exploratory performance experimentsTwo rounds of subagent prototypes on The mechanism descriptions below identify a concrete starting point and the contracts to preserve. Compiler timings are not whole-build timings; gains from separate rows must not be added together. Where a combination was measured, it is explicitly identified.
Suggested order
The worker build prototype is production-only and rejects unsupported custom hooks/split callbacks/HMR. Selective copying, parser-only analysis, and delayed cloning have narrower applicability than their isolated best-case numbers suggest. No full package/e2e/bundle validation matrix was run for these experimental implementations. The existing upstream JSX-reference blocker remains unchanged. Prototypes and raw results remain in local scratch worktrees; they have not been published as implementation commits. |
Potential upstream Yuku API requestsRecording four requests for a future discussion with the Yuku maintainer, based on the exploratory performance experiments. API review was against Yuku commit 1. Independent output trees with source-node and binding provenanceRequest: provide a schema-aware Current gap: Possible implementation: start with one schema-aware copying traversal that also creates provenance mappings and, if useful, parent information. A more ambitious alternative is independent decoder caches over the same immutable backing buffer, avoiding a copy of an already-materialized JavaScript tree. Preserve comments, spans, regex/BigInt literal values, and output mutation isolation. Decoder cache, internal index primitives Evidence: our fused clone/provenance prototype reduced the full splitter workload 31.6%, with peak RSS 266.0 → 241.5 MiB. This is an existing TanStack-side optimization that an upstream primitive could make safer and easier to maintain, not a prediction of another 31.6% improvement. Contract and scope: provenance refers to the immutable source analysis. It must not imply that arbitrary output edits automatically update semantic tables. This is the strongest near-term request; fully transparent copy-on-write is not necessary for an initial version. 2. Precompiled analyzed AST templates with typed holes and explicit binding referencesRequest: allow a compiler-owned fragment to be parsed/analyzed once, then instantiated with expression, statement-list, literal, and binding-reference parameters, without reparsing/reanalyzing every instance. Current gap: typed Possible implementation: an optional template entry point with typed holes and explicit binding ownership; independently mutable instances; predictable comments/spans; and mappings or metadata that let consumers relate generated references to source bindings. It should not silently pretend to recompute whole-module semantics. Keep it optional so ordinary lightweight AST builders do not acquire an eagerly loaded native analyzer dependency. Evidence: expanded provider builders alone reduced Start compilation workloads 22–23%. Fused cloning plus those builders, measured together, improved them 36–39%. Parameterized HMR templates reduced warm snippet construction 8.507 → 4.163 ms (−51.1%) for 50 routes. The latter excludes printing and application HMR latency. Contract and scope: useful to RPC generators, HMR transforms, instrumentation, and framework wrappers generally. The prototype’s literal sentinel is not a production API design; collision-proof holes and explicit reference ownership are the upstream value beyond existing builders. 3. Prepare a source once and emit independent variants from sparse editsRequest: support an immutable prepared module that can emit multiple outputs from source-node references and edit plans, avoiding a full JavaScript AST round trip for every output. Illustrative shape only: const source = prepare(code)
const result = source.emit({
remove: [sourceNodeId],
replace: [{ node: otherNodeId, with: generatedFragment }],
prepend: [generatedImport],
})Current gap: native analysis serializes its result and destroys its native tree. Each codegen call subsequently encodes the JavaScript AST and reconstructs a fresh native tree. Reusable JavaScript Possible implementation: retained native source trees or buffer-backed output views, with stable source node/symbol handles and sparse removals/replacements/insertions. Encode inserted fragments once where possible; let Yuku own efficient reconstruction, printing, comments, and maps. Start with serial independent variants. Native parallel emission could follow, but an initial API need not serialize arbitrary JavaScript visitor callbacks. Potential benefit: eliminate repeated full-tree decoding, copying, encoding, and native reconstruction. A convenience Contract and scope: this is a larger architectural project requiring explicit disposal/lifetime, memory accounting, output isolation, stale-generation checks, and worker ownership. Prefer opaque/versioned handles over exposing the current internal transfer format. TanStack should retain routing policy, chunk ownership, invalidation, and bundler scheduling; Yuku should own generic edit-and-emit mechanics. A joint profile/prototype is the appropriate next step. 4. Indexed subtree-reference queries and declaration ownershipRequest: expose efficient queries such as Current gap: basic scopes, symbols, Possible implementation: lazily cached source-level reference/ownership indexes with compact stable IDs, scoped to the immutable analysis snapshot. Consumers should still compute output-specific liveness and apply their own side-effect-sensitive extraction policies. Existing root-only route dependency data cannot substitute for all-scope ownership. Evidence: carrying immutable cleanup metadata in our prototype reduced index reconstruction 1,250 → 250, but improved actual Vite builds only 0.8–3%. Further subtree-query savings have not been measured. Contract and scope: useful analyzer infrastructure and a complement to the first and third requests, but lower priority than their demonstrated or potential impact. Route-specific chunking and dead-code-removal policy should remain in TanStack. Suggested discussion orderStart with output-tree provenance as a practical collaboration, followed by analyzed templates. Discuss source-backed edit-and-emit as the larger profiling/prototype opportunity. Keep indexed semantic queries as a supporting request. Do not request capabilities that already exist: typed builders, reusable analysis objects, lazy AST access, optional source maps, and For currency: the JSX-reference issue was fixed upstream in #208. At the time of this API review, published 0.11.0—the version used for our experiments—did not include that fix. These requests have only been recorded here; no upstream feature issues have been opened. Update (2026-10-11, Yuku 0.19.0)Still recorded only; no upstream feature issue opened yet. 1. Independent output trees with provenance: still missing in 0.19.0, and still our biggest cost. In the current route-splitter benchmark, copying ( const { program, originalOf } = module.copyAst()
// program: a fresh, mutable tree, deep-equal to module.ast
// originalOf(node): the module.ast node a copied node came from, or null for nodes we createdCopies must be independent of each other. Shared references inside the tree (e.g. 5. Builders for node kinds that share a // NodeOfType<'Property'> = ObjectProperty | BindingProperty
object.properties.push(b.Property({ kind: 'init', key, value, method: false, shorthand: false, computed: false }))
// TS2345: Type 'BindingProperty' is not assignable to type 'ObjectPropertyKind'
// NodeOfType<'ExpressionStatement'> = ExpressionStatement | Directive
const directive: Directive = b.ExpressionStatement({
expression: b.Literal({ value: 'use server', raw: "'use server'" }),
directive: 'use server',
})
// TS2322: Type 'ExpressionStatement' is not assignable to type 'Directive'
// TS2345: Type 'BigIntLiteral' is not assignable to type 'StringLiteral' (b.Literal returns the whole Literal union)#8524 writes Update written with Claude Code (AI-assisted). |
Yuku 0.19.0 is now older than the one-day minimumReleaseAge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions Each test passes on main's compiler and fails on this branch until the behaviour is fixed: - declarations only an unused local inside them reads are removed with their side effects - a namespace no split option reads is copied into the component chunk - an unused function rendering the split component keeps it, and its imports, in the route module - a declaration the split component only reads in a type runs twice - an ambient var merged with an interface is exported from the shared module - a route declared with var in a module-level block is not split - Yuku bugs (pass once Yuku ships the fixes): yuku-toolchain/yuku#255, #256, #257, #258, #259, #260 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ressions Each test passes on main's compiler and fails on this branch until the behaviour is fixed: - declarations only an unused local inside them reads are removed with their side effects from the client output - a middleware chain link wrapped in parentheses or a cast makes the client compile throw "Cannot replace a detached output node" - the source map of a Hydrate chunk names its virtual module id as the source - Yuku bug (passes once Yuku ships the fix): the source map maps a JSX attribute's string value to the attribute name, yuku-toolchain/yuku#254 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…u migration Each test fails on main's compiler (57bd1a9) and passes here. Found by the corpus diff, differential fuzzing, mutation testing, real-app builds and dev-server runs: - router-generator records method, async method and accessor option keys - a declaration only an exported type alias reads stays in the route module - a component only the split component uses leaves the route module when a kept type shares its name - a declaration a type alias and the split component read runs once - a dotted namespace keeps its body and the writes to it in the chunk - route options and route factory calls wrapped in a cast or a non-null assertion are split - a split component reading a route exported through `export { Route }` imports it; one also read by an unsplit option stays declared - component options written as a call with JSX inside (memo(), defineComponent()) compile into their chunk - a split component imports the classic JSX factory the route module exports - a top-level call reading the split component keeps it in the route module - split chunks do not import what only the types of route module code read - split modules import only the classic JSX factories their JSX compiles to Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each test fails on main's compiler (57bd1a9) and passes here. Found by the corpus diff, differential fuzzing and mutation testing: - a createIsomorphicFn passed to .handler() produces RPC callers - import protection finds JSX usages and skips type positions and member properties - an isomorphic fn whose chain link is wrapped in a cast keeps each output's implementation - the client drops a validator whose chain link is wrapped in a cast - Hydrate split={false} wrapped in a cast keeps function children inline - the import-protection mock does not reuse a name the module declares Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, other props stay Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he Yuku migration Each test fails on main's compiler and passes here. Found by mutation testing: - an enum or a namespace that the loader and a split component read directly is initialized once - Hydrate children reading a Route exported under a string name import it (the chunk quotes the export name) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ycle and exported-binding bugs Found by mutation testing #8504 and by checking exported bindings used by split options. Each pin fails on main and on #8504's head: - a classic JSX factory the route module creates and the loader reads is dropped from the split chunk (created again in the chunk on #8504) - an unused reference cycle is copied into every split chunk (a row of the unreferenced side effects table) - the "will not be code-split" warning is missing when an exported component is the route's only split candidate - an exported const only a split component renders stays in the entry chunk (with a control for the build placement harness) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…u migration regressions Each test passes on main's compiler and fails on this branch until the behaviour is fixed: - the deletion of declarations only an unused local inside them reads (same cause as the existing failing tests) also deletes a ReferenceError their initializer threw, a declaration only an uncalled self-reading function reads, a destructuring that runs a getter, and a statement in a function body that only an uncalled nested function reads - the import-protection rewrite drops a JSX pragma that follows a directive when the next statement is a denied import Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tection rewrite Each pin fails on main and on #8504's head for the stated reason: - a type-only import re-exported without `type` (`import type { Settings }` or `import { type Settings }`, then `export { Settings }`) loses its import but keeps the export, so the module fails to load with "Export 'Settings' is not defined": route module and split chunks (code splitter), client and SSR outputs (Start compiler) - rewriting a denied import to the import-protection mock drops the comments that lead it (a JSX pragma, a legal comment) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… read into the shared module An exported function or class that a split route option reads (an exported page component the split component renders, a store class, a helper) now moves into the route's shared module with its private dependencies, and the route module re-exports it from there. Every importer keeps one instance, so instanceof checks and module state agree, and when the shared module has no top-level effects it is marked side-effect free (Vite/rolldown moduleSideEffects, webpack/Rspack factoryMeta), so the bundler can leave it and the imports only it uses out of the route module's chunk. Exports the shared module cannot take stay in the route module: exported split targets, bindings that depend on Route, bindings code staying behind reassigns, and initializers with effects that would overtake effects of statements staying behind. Of those, a stateless exported function (one that reads Route, for example) is declared in the split chunk as on main. Replaces the test pinning the import-from-route-module rule with the shared module behaviour, and adds build-level tests for startup placement and one shared instance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…list the Yuku migration fixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ompiled before their route module
In dev, an edit invalidates the route module, its split chunks and its
shared module together, and the browser requests them again in no fixed
order. The shared and split transforms read the shared-binding set the
route module's transform stored for the file, so requested first they
compile the edited source with the previous set: a removed binding is
still exported ("Export 'suffix' is not defined", cached until the next
edit) and an added one is copied into each chunk instead of shared.
Already on main for a binding two split options share; the exported
component's private dependencies now in the shared module make it reachable
by removing a helper of an exported component a split component renders.
The tests request the modules out of order through a Vite dev server and
compare them with a server started after the edit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Replace the two-row failing middleware test with a matrix: 8 wrappers
(parentheses, double parentheses, `as any`, `as unknown as T`,
`satisfies`, `!`, `<any>x`, a nested combination) x 3 positions (the
wrapped part ends at `.server()` and `.client()` follows; the same inside
a wrapped chain; after `.middleware().validator().server()`). Each row
checks that the client output has no server-only import, server body or
validator body, that the evaluated client chain only runs `.client()`,
and that SSR keeps and runs every link. These pass on main and fail here
("Cannot replace a detached output node").
- A wrapped middleware `.validator()` link is stripped on the client.
- An isomorphic fn with a wrapped client-first link keeps both
implementations.
- A wrapped callee of createServerOnlyFn, createIsomorphicFn (both orders)
and createClientOnlyFn is compiled: main left these calls untransformed,
shipping the server implementation and its imports to the client and
running a client-only fn during SSR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m reads
The dead-code pass keeps declarations that were already unused before the
transform ("initially unused") wherever their enclosing code survives. It
decided what was unused from reference cycles alone, so a read by an unused
local nested inside the declaration itself counted as a use: in
`const plugin = { id: register(), m() { const self = plugin } }` the nested
`self` made `plugin` "used", `self` only survives with `plugin`, and nothing
else kept `plugin`, which was deleted with its `register()` call. The same
happened for a class whose method reads it, a TDZ read, a getter run by a
destructuring, and an uncalled (nested) function reading itself through a
local, together with the declarations only that function reads.
This reached the code splitter's route module, the route HMR compile
without code splitting, the Start compiler outputs and Hydrate chunks;
main kept all of them. Only reads by code that survives now count: any
declaration the liveness pass would drop from the unchanged source is
initially unused too. The tests for the route HMR compile without code
splitting are added to the existing table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plit component dangling
An unused function that renders the split component, directly, through a
helper or from an inline option (`function Unused() { return <Page /> }`),
kept the component and its imports in the route module, so they loaded
with the route and the split was defeated; the chunk declared a second
copy. The function was initially unused, survived as written, and kept
everything it reads alive.
As decided for this migration, it stays as written but no longer keeps
what moves into split chunks in the route module: its reference dangles,
and nothing can call it. Code that can run is not affected: an exported
function, a function staying code reads (a route option, a module-level
call, a callback an unused subscription receives) keeps its dependencies,
which are shared as before. The removal pass now takes the bindings the
split values read and the declarations they depend on (`moved`), and
ignores uses of them by code that cannot run: unused functions and what
they declare. Imports are not moved: the unused function keeps the imports
it reads, as on main.
The table row for unused functions calling a helper of the split component
is updated to the new behaviour, and the unused-function test covers the
three shapes; new tests check that functions code can still call keep
the component.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ads in a type
TypeScript erases type positions, so the migration no longer counts them as
reads: `const value = init(1)` that the split component only names in a type
(`{} as typeof value`, `z.infer<typeof schema>`) is initially unused. The
dead-code pass keeps initially unused declarations wherever their enclosing
code survives, in every emitted module, so the route module kept it and the
chunk declared a second copy: `init(1)` ran twice. main moved it into the
chunk, so it ran once.
A split chunk no longer keeps an initially unused declaration it only reads
in types: it is declared once, in the route module, where it runs as
before. Declarations a chunk reads at runtime are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An instantiated namespace nothing reads, such as
`namespace Unused { export const value = track() }`, is initially unused, so
the dead-code pass kept it with its body in every emitted module, and its
initializer ran again whenever a split chunk loaded. main left at most an
empty namespace in the chunk.
A split chunk no longer keeps an initially unused namespace that nothing in
it reads; the route module keeps it and runs its body once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`declare var readVersion: ReadVersion` merged with `interface readVersion` has no runtime declaration: the environment provides the value. The shared module skipped ambient bindings by Yuku's `Ambient` flag, which is only set when every declaration is `declare`d, and an interface is not. So a shared declaration reading it (`const version = readVersion()`) put it in the shared module's exports, which then failed to load with "Export 'readVersion' is not defined". A binding whose declarations are all `declare`d or types is now never shared or exported, and chunks read the global. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… block
`if (x) { var Route = createFileRoute('/')({ ... }) }` with
`export { Route }` declares a module binding, and main split it. Route
detection treated every factory call outside the module scope, a block
included, as created inside a function: the route was left unsplit, without
route HMR, and the warning said it was created inside a function.
A factory call in a module-level block (`if`, `try`, a bare block, a loop,
a `switch`) now counts as module code; only one inside a function, a
class body or a namespace is a route factory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hain The candidate walk skips the inner links of a chain (`a.b().c()`: only the outermost call is compiled), but only when the link's parent is the member expression. In `(createMiddleware().server(s) as any).client(c)` a wrapper sits in between, so the inner `createMiddleware().server(s)` became a second candidate. The outer candidate's chain walk already looks through wrappers and stripped `.server()` on the client; stripping it again from the second candidate threw "Cannot replace a detached output node". main built these. The skip check now looks through parentheses and TypeScript assertions (`as`, `satisfies`, `!`, `<T>x`) between a link, its member and the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chunk maps A Hydrate chunk was generated with its virtual module id as the filename, so its source map named `file.tsx?tss-hydrate=…` as the source: each chunk showed up as a separate fake file in devtools and error trackers. main's maps named the file itself. The chunk is now generated with the file's id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orm compiles
The route module's transform computed the bindings a route's split modules
share and stored them by file; the `?tsr-shared=1` and `?tsr-split=`
transforms read that stored set. In dev an edit invalidates all three
modules, which are requested again in no fixed order: requested before the
route module, the shared module and chunks compiled the edited source with
the previous set. A removed binding was still exported ("Export 'suffix' is
not defined", cached until the next edit) and an added one was copied into
each chunk instead of shared. main had this for a binding two split options
share; the migration's move of exported components' private dependencies to
the shared module made it easy to hit.
The stored map is removed. Each of the three transforms resolves the
route's groupings (from the code, `splitBehavior` or the default) and its
shared bindings from the code it receives, with one function, reusing the
analysis it compiles with.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as side effects The shared module's transform returned `moduleSideEffects: false` when the module is pure and left the key out otherwise. In `vite build --watch`, rolldown then kept the flag of an earlier build (rolldown/rolldown#11223): after an edit gave the shared module a top-level effect, the effect was left out of the startup chunk. An impure shared module now returns `moduleSideEffects: true`, the default analysis. Webpack and Rspack replace a module's factory metadata on every compilation, so their path needs no change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vite marks a module self-accepting only when its code contains a literal `import.meta.hot.accept(`. Solid route modules with automatic code splitting and all Vue route modules rely on the route HMR code for that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
React Refresh hot-updates a module only when all its exports are components. A shared module that holds an exported component and the private values it reads reloads the page in Vite bundled dev, and needs an extra invalidation round trip in unbundled dev. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt.meta.hot.accept Vite marks a module self-accepting only when its code contains a literal `import.meta.hot.accept(`. The route HMR code called `accept` on a local alias, so a route module was an HMR boundary only when a framework refresh plugin made it one: every edit of a Solid route file with automatic code splitting, of a Solid route file whose components moved to its shared module, and of any Vue route file reloaded the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… from its shared values
React Refresh hot-updates a module only when all its exports are
components. Since exported functions that a split option reads moved to the
shared module with their private dependencies, an exported component and
the private values it reads ended up in one module: in Vite bundled dev
every edit of it reloaded the page and lost state, and in unbundled dev it
needed an invalidation round trip ("Could not Fast Refresh").
For React, the shared bindings that look like components, that no other
shared binding reads and that assign no other shared binding, now go to `?tsr-shared=components`, which imports
the other shared bindings from `?tsr-shared=1`; the values module never
imports it. Each transform computes the split from the code it compiles,
and the components module is side-effect free only when the values module
it imports is too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d and Vue route HMR fix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🎯 Changes
Replaces Babel with Yuku for parsing, analysis, transforms and code generation in
router-utils,router-generator,router-plugin,start-plugin-coreandreact-start-rsc. No Babel fallback, and no new caches: each transform analyzes its file once.Only compiler packages change; the router and Start runtimes are untouched. Client bundles are unchanged in all 18 bundle-size CI scenarios. Real apps see one intended output change: exported functions and classes used by split components (see "What changes for users"). Compiler extension hooks now expose Yuku nodes, so this ships as a minor.
CI is red on purpose: 8 regular tests fail until their regressions are fixed here or in Yuku (see "Intentionally failing tests").
Performance
e2e/react-start/server-functions)Measured on Yuku 0.19.0,
main57bd1a92f9vs this PR at7a079cf71a, Linux x64 (Ryzen 7 255), Node 24.8.0. That's 10 alternating pairs, load-gated. Saving a route file in a Vite dev server takes the same time before and after the last changes (~49 ms to the update). Scripts and earlier raw results are kept out of the merge, browsable ate1da04f762.Performance work is left for follow-up PRs. The biggest remaining cost is copying the AST for each output (
cloneModuleAst, mostlystructuredClone, ~38% of the splitter's time); reusing a route's analysis across its outputs, which this PR deliberately doesn't cache, is another 5–7% on large route builds. The WebAssembly fallback core is as fast as the native one here (129 vs 132 ms, +13 MiB): only parsing and analysis go through it.What changes for users
All of this is in the changeset.
router-utils,router-generator,router-plugin,router-cliandrouter-vite-pluginneed^20.19.0 || >=22.12.0. Yuku is ESM-only, and our CommonJS buildsrequire()it. Start packages already required>=22.12.0.maincopied them into the chunk (two instances); this PR first imported them from the route module, which pulled them into the entry. Startup JS (gzip, entry plus static imports) against that previous behaviour: b2b-saas-starter 353 → 252 KB (main351), tanstack-start-faster 198 → 195 KB (main195), tanstack.com about the same (379 KB,main376). It goes back to the previous size with webpack cached rebuilds, with an appmanualChunksgroup that captures Start packages, or with rolldown before 1.1.5 (tanstack.com has the last two). A stateless exported function that readsRouteis still copied, as onmain.createServerFnnot assigned to a module-level variable (returned from a function, default export, object property, inside a block) is now a build error with a code frame. We built and ran each rejected placement onmain, and none worked: depending on the other Start APIs in the file,mainalready threw "must be assigned to a variable", leaked the handler and its server-only imports to the client (where the call returnedundefined), or failed later in the bundler or at runtime.as any,satisfiesand parentheses are still accepted. Still not detected, as onmain: a function returning an unfinished builder,defineAction(v).handler(H)(pinned in test: pin known compiler bugs as expected failures #8630).mainsplit them: correct when the split options only use module-level code, otherwise a crash at render (options closing over function locals), a build error (a function-local component) or silently the wrong component (a parameter shadowing a module-level one). This PR renders all of these correctly, but regresses the shapesmainsplit correctly: their components load with the route module, React edits full-reload on Vite (mainhot-updated) and can leave a blank page on Rsbuild (whenapp.tsxcreates the router), and Solid drops loader/beforeLoadedits. These are known regressions waiting for a decision on restoring splitting for safe shapes; they have no failing tests yet, and the existing tests only cover the warning. One app route in ~50k real-world files uses this shape.letused only by a split option moves into that chunk, like other declarations already did;errorComponent: X as anywith an exportedXis no longer duplicated into the chunk;<Hydrate children={…}>is split like JSX children;.jsfiles no longer accept TypeScript syntax;exportstatements are rejected, as in TypeScript.Also fixed along the way, each with a regression test that fails on
main:as any,satisfiesor!are split (mainleft the file untransformed, and for server routes shipped theserverhandlers to the client); an object-form loader (loader: { handler }) once the loader is split; the generator records method and accessor option keys.React.memo(() => <p />), a VuedefineComponentsetup) no longer fail to compile in split mode (a 500 in dev); a component read by a split and an unsplit option no longer throws "is not defined";function Page+export { Page }used as the component no longer breaks the build; a component sharing its name with a type is no longer duplicated.@jsxfactory is no longer missing; enums and namespaces read by the loader and the component are declared once; dotted namespaces keep their bodies; value imports only read by types are no longer copied.createServerOnlyFnorcreateIsomorphicFnpassed to a server fn handler;.server()and.validator()chain links in a cast;createServerFndeclared inside a function.(createServerOnlyFn as any)(fn),createServerOnlyFn!(fn)and the same forcreateIsomorphicFnare compiled.mainleft these calls untransformed: the client bundle shipped the server implementation and its imports and ran it in the browser (import protection only caught*.server.*modules). A wrappedcreateClientOnlyFnran during SSR instead of throwing.Route, also when it is exported under a string name; children passed through achildrenprop or a spread;split={false}in a cast.accepton an alias ofimport.meta.hot, and Vite only treats a module as an HMR boundary when its code callsimport.meta.hot.accept(literally.Binaries and platforms
Yuku ships native bindings for 12 platforms, including musl, arm and Windows arm64. Code generation is plain JavaScript.
router-utilsfalls back to Yuku's WebAssembly core when the native binding can't load: platforms without a binding, or a missing, corrupt or wrong-arch binding. It uses WebAssembly directly in WebContainers (StackBlitz, bolt.new). The output is byte-identical to the native core's, and it runs just as fast. The fallback does not work on big-endian hosts (s390x, Yuku chore(deps): bump minimist from 1.2.5 to 1.2.6 in /docs #253), undernode --jitless, and probably on CPUs without Wasm SIMD (seen under qemu, not confirmed on hardware).mainworks there.CI (#8626) runs the compiler tests and a Vite and Rsbuild build on Windows x64, macOS arm64, Linux arm64 and Linux x64 musl. All four passed at
8f02f6ac2e; now the unit tests fail there on exactly the 8 intentional failures, so the builds are skipped.Checked locally, with no failure
maindoesn't also have: npm, pnpm 10 and 11, yarn 1, Yarn 4 including PnP, Bun, Deno, Node 20.19 and 22.12 (ESM and CommonJS), worker threads, Alpine (musl), armv7, and a simulated WebContainer.npm --omit=optionalmakes Yuku fall back to WebAssembly, but Vite 8 apps fail there anyway, onmaintoo: rolldown needs its binding.Also tested in a real WebContainer: the tanstack.com example playground, run locally with tanstack.com#1359, with this PR's packages from pkg.pr.new. The React
start-basicexample works in dev (server functions, code splitting, HMR) and as a served production build; a Router-only example builds and runs too.Yuku versions are pinned exactly through the pnpm catalog. pnpm 11 and yarn 4 refuse packages less than a day old by default, so a Yuku bump must be at least a day old before a release that ships it.
Validation
basicin all 9 Vite/Rsbuild modes; HMR, server functions, RSC, import protection, deferred hydration, streaming, selective SSR and serialization adapters; Solid and Vue Start; the Router code-splitting apps on Vite, Rspack and esbuild. Full run before the campaign; after the exported-bindings change, the React and Solid code-splitting apps and Startbasic(Vite SSR) were re-run.mainon 30 targets (Vite, Rsbuild, Rspack and esbuild × React, Solid and Vue, Router and Start). The output is equivalent apart from formatting, comments, import order and the changes listed above.mainfirst (#8629). Snapshots are now Prettier-formatted (#8658), so the 117 snapshot changes here are real output changes, not formatting.A confidence campaign then compared this PR with
main(57bd1a92f9):compilers.ts,start-compiler/and the Hydrate transform (plus the dead-code helpers, the import-protection rewrite and the HMR plugins). Every surviving mutant that changes behaviour got a test.Intentionally failing tests
Each passes on
mainand fails here until its regression is fixed.Our code, to be fixed in this PR:
Yuku, waiting for upstream fixes (no workarounds here), one test per issue (see the table below): #254, #255, #256, #257, #258, #259 and #260. The two most likely to reach users: #260 (JSX text such as
— bor1stafter a</>fails to parse) and #258 (Solid and Vue route files with an emptyimport type {}break in dev without code splitting). #253 (big-endian hosts) has no test: CI has no big-endian runner.Related
mainas.failstests: 141 pins for 66 root causes. 25 of them already pass on this PR, which fixes their bugs; they stay pinned because the bugs exist onmain. Each fix takes its test from there.mainand this PR, so the migration has to keep that behaviour.main.Upstream Yuku issues
The issues we filed before the campaign are fixed, and this PR needs no workaround for any of them. The ones the campaign found are open:
_,$and Unicode component namesimport X = ns.Ymarksnsas a type referenceexportJSXText.valuenot entity-decoded++operand followed by a line commentfor (init;;)no-inrestriction leaks into parentheses and literalsimport type {} from 'x'printed as invalidimport type 'x'</>lexed as JavaScriptWritten with Claude Code (AI-assisted).
✅ Checklist
🚀 Release Impact
🤖 Generated with Claude Code
Summary by CodeRabbit
^20.19.0 || >=22.12.0. Compiler extensions must migrate to the updated interfaces.