diff --git a/.changeset/compiler-core-yuku.md b/.changeset/compiler-core-yuku.md new file mode 100644 index 00000000000..dd97d408eee --- /dev/null +++ b/.changeset/compiler-core-yuku.md @@ -0,0 +1,11 @@ +--- +'@tanstack/router-utils': minor +--- + +Add the compiler core shared by the router plugin and TanStack Start, built on Yuku (`@tanstack/router-utils/compiler`): one parse and one plan per module and environment, with every output (the module, its split chunks, Hydrate chunks, server function providers) emitted from the original source with exact source maps. + +The dev caches stay bounded by the live modules: deleted, renamed or no longer imported files leave no plans, raw facts or dependency-index rows behind, and publishing a plan updates only that plan's dependency rows. Each compile records its resolutions in its dependency ledger, and validation asks the bundler's resolver again once a file appeared or disappeared; while the file set is unchanged, the bundler is asked once per environment for each (importer, specifier) pair, so the `export *` chain of a barrel that a thousand modules import from is resolved once, not a thousand times. Emitted outputs are not cached: they are emitted from the frozen plan on request. `TSR_COMPILER_STATS=1` exposes the cache sizes (`compilerStats()`) of the compiler core, the router plugin and the route generator, and `TSR_COMPILER_NO_CACHE` (`plans`, `facts`, `targets`, `all`) disables caches for measurements. + +Removed the Babel helpers (`parseAst`, `generateFromAst`, `deadCodeElimination`, `findReferencedIdentifiers`, `stripTypeExports` and the binding helpers of `compiler-helpers`) and the `@babel/*` and `babel-dead-code-elimination` dependencies. Also removed the unused `logDiff`, `createIdentifier` and `decodeIdentifier` exports and the `ansis` and `diff` dependencies. + +The package requires Node `^20.19.0 || >=22.12.0`: its CommonJS build loads the ESM-only parser through `require(esm)`. diff --git a/.changeset/compiler-one-mutation-model.md b/.changeset/compiler-one-mutation-model.md new file mode 100644 index 00000000000..baa82d162cd --- /dev/null +++ b/.changeset/compiler-one-mutation-model.md @@ -0,0 +1,7 @@ +--- +'@tanstack/router-utils': patch +'@tanstack/router-plugin': patch +'@tanstack/start-plugin-core': patch +--- + +Route code splitting and Start's environment separation decide from one model of what writes a module's bindings. A value computed when the route module loads from a binding some function writes later (`const initial = [n]` next to `function inc() { n++ }`, a snapshot taken by an immediately invoked function, a call reading a member of an object a function mutates) keeps the value the source computed: it stays in the route module and the split chunks import it, instead of being recomputed when the chunk loads. An option value that reads such a binding is kept in place. The route's option values must be pure for declarations only the component reads to move into its chunk: an impure option (`validateSearch: makeSchema()`) now runs after the declarations before it, as in the source. A chain of components each writing a module binding the previous one reads no longer produces a chunk assigning a binding it imports (a `TypeError` at render): re-planning continues until every such option is kept in place. A server-only initialiser that a server function handler only assigns leaves the client with the handler. Nested server functions are no longer hoisted when they read a module binding something mutates. `TSR_COMPILER_VERIFY=1` re-parses every emitted output and fails the compile on a broken one (the test suites set it). diff --git a/.changeset/dev-cold-start-and-invalidate-index.md b/.changeset/dev-cold-start-and-invalidate-index.md new file mode 100644 index 00000000000..02d2c2f6455 --- /dev/null +++ b/.changeset/dev-cold-start-and-invalidate-index.md @@ -0,0 +1,7 @@ +--- +'@tanstack/start-plugin-core': patch +'@tanstack/vue-start': patch +'@tanstack/router-utils': patch +--- + +Vite dev: Start no longer pre-bundles `@tanstack/-router/hmr` on its own (the router package is excluded from optimization, so the subpath is served from source with it); pre-bundling it made a cold-start re-optimization strand the first page load (504 "Outdated Optimize Dep", no hydration, no reload). Vue Start declares `@tanstack/vue-router > @tanstack/vue-store` for the optimizer, like React Start does for `@tanstack/react-store`, so a cold start no longer reloads the page once. The compiler's plan cache finds a changed file's plans through a path index instead of scanning every plan on every hot update. diff --git a/.changeset/dev-hmr-stable-updates.md b/.changeset/dev-hmr-stable-updates.md new file mode 100644 index 00000000000..f65e35f86ff --- /dev/null +++ b/.changeset/dev-hmr-stable-updates.md @@ -0,0 +1,7 @@ +--- +'@tanstack/router-utils': patch +'@tanstack/router-plugin': patch +'@tanstack/start-plugin-core': patch +--- + +Dev HMR: a route file that stops parsing keeps its last facts, so its importers (the route tree) no longer hot-update into a full reload onto the dev server's error page; a dependent whose compiled output did not change is not hot-propagated (a route file gaining an export no longer reloads the page); derived modules a file no longer produces (an extracted export after the export went, a split chunk after `codeSplitGroupings: []`) are dropped from the file's hot update instead of dead-ending into a reload; and a React route component moved between its chunk, the route module and its extracted-export module is swapped in (its React Refresh family changed) instead of kept stale. diff --git a/.changeset/import-protection-bundler-witness.md b/.changeset/import-protection-bundler-witness.md new file mode 100644 index 00000000000..d43cfdf2879 --- /dev/null +++ b/.changeset/import-protection-bundler-witness.md @@ -0,0 +1,5 @@ +--- +'@tanstack/start-plugin-core': patch +--- + +Import protection no longer reads tsconfig to guess whether TypeScript erases an import whose bindings are only used as types: the bundler decides. Such an import of a denied module is reported (and mocked) exactly when Vite's or Rsbuild's TypeScript transform keeps it, in dev and in builds. This fixes the stock Vite template (a solution-style `tsconfig.json` with `verbatimModuleSyntax` in `tsconfig.app.json`), whose builds bundled the real server module unreported, and Rsbuild dev, which reported imports SWC erases. diff --git a/.changeset/import-protection-lazy-locations.md b/.changeset/import-protection-lazy-locations.md new file mode 100644 index 00000000000..6d7b6e035e3 --- /dev/null +++ b/.changeset/import-protection-lazy-locations.md @@ -0,0 +1,6 @@ +--- +'@tanstack/router-utils': patch +'@tanstack/start-plugin-core': patch +--- + +Faster Start builds: import protection locates an import through the bundler's source map only when it reports a violation (every checked module used to combine its input map), a dependency module an app imports is read but no longer parsed and summarised unless its text names something the compiler recognises or re-exports another module, and the compiler asks less of the parser per module. A violation's trace still locates each step in the user's files; a step through a module another plugin rewrote (React Compiler, MDX) is named without a line. diff --git a/.changeset/import-protection-pattern-imports.md b/.changeset/import-protection-pattern-imports.md new file mode 100644 index 00000000000..39070b60149 --- /dev/null +++ b/.changeset/import-protection-pattern-imports.md @@ -0,0 +1,5 @@ +--- +'@tanstack/start-plugin-core': patch +--- + +Import protection checks the imports a bundler expands from a pattern too: a denied file reached only through `import.meta.glob`, a template literal `import()` or `import.meta.webpackContext` is replaced by its mock and reported, in dev and in builds, on Vite and Rsbuild (a build names each module importing it, a dev server the denied file). Rsbuild builds with the `error` behaviour now fail on such an import, and Vite and Rsbuild dev servers report it (they used to mock it silently). diff --git a/.changeset/import-protection-unchecked-importers.md b/.changeset/import-protection-unchecked-importers.md new file mode 100644 index 00000000000..20d565ae1b3 --- /dev/null +++ b/.changeset/import-protection-unchecked-importers.md @@ -0,0 +1,5 @@ +--- +'@tanstack/start-plugin-core': patch +--- + +Import protection: a build warns once about each denied file it mocks whose every importer is exempt (`exclude`, `ignoreImporters`) or outside the scope (`include`, `srcDirectory`), naming the file, its importers and the fix (import the file statically from an exempt module inside the scope). The warning is not a violation: `onViolation` is not called, and the `error` behavior does not fail the build. diff --git a/.changeset/react-start-rsc-compiler.md b/.changeset/react-start-rsc-compiler.md new file mode 100644 index 00000000000..81b4cb6b406 --- /dev/null +++ b/.changeset/react-start-rsc-compiler.md @@ -0,0 +1,7 @@ +--- +'@tanstack/react-start-rsc': minor +--- + +RSC CSS injection runs in Start's compiler. Server functions that only a `'use client'` component imports are in the production manifest (#7943). With RSC, server function provider modules load only what their handlers reach, so a route module's client-only code no longer runs under `react-server`. + +Removed `@tanstack/react-start-rsc/plugin/rscCssTransform` (the Babel transform). diff --git a/.changeset/release-finished-environment-plans.md b/.changeset/release-finished-environment-plans.md new file mode 100644 index 00000000000..279a58bb22e --- /dev/null +++ b/.changeset/release-finished-environment-plans.md @@ -0,0 +1,7 @@ +--- +'@tanstack/start-plugin-core': patch +'@tanstack/router-plugin': patch +'@tanstack/router-utils': patch +--- + +Production builds release an environment's compiler state as soon as that environment is built, instead of keeping every finished environment's state until the whole build ends. A build of several environments (client, SSR, then a server or RSC environment) no longer grows its heap with each one (#8794). diff --git a/.changeset/route-factory-reexport-report.md b/.changeset/route-factory-reexport-report.md new file mode 100644 index 00000000000..59554ee6c59 --- /dev/null +++ b/.changeset/route-factory-reexport-report.md @@ -0,0 +1,5 @@ +--- +'@tanstack/router-plugin': patch +--- + +A route file whose `createFileRoute` (or another route factory) is imported from a module of the app that re-exports the router's is now reported, naming the file and the fix (`import { createFileRoute } from '@tanstack/react-router'`): only the router's own export is recognised, so such a route is not code-split or hot-updated. Under TanStack Start the client build fails instead, since the route's `server`, `ssr` and `headers` options could not be removed from it. diff --git a/.changeset/route-generator-yuku.md b/.changeset/route-generator-yuku.md new file mode 100644 index 00000000000..34022ed0a57 --- /dev/null +++ b/.changeset/route-generator-yuku.md @@ -0,0 +1,19 @@ +--- +'@tanstack/router-generator': minor +'@tanstack/router-cli': patch +--- + +Rebuild the route generator without Babel. + +**`routeTree.gen.ts` formatting changes once.** The route tree is printed directly in your `quoteStyle` and `semicolons`, and `enableRouteTreeFormatting` now defaults to `false`: Prettier is loaded only when you set it to `true`. A committed `routeTree.gen.ts` shows a one-time formatting diff after upgrading. Custom scaffolding templates are inserted as written. + +- Route files are recognised by binding: an aliased `createFileRoute` import gets its id fixed; a `createFileRoute` the app declares itself is left alone; a plain template literal path is reported, not rewritten (#7216). +- The tree is rebuilt from the files on every change: deleting a layout keeps its children, a deleted `routeTree.gen.ts` is written again, edits of the virtual route config and of `__virtual.ts` files are picked up, and a moved route gets its new id without a content change. +- Empty `.ts` route files get a scaffold without JSX (#8480); `.lazy.vue` files and `route.component.vue` pieces belong to their route; `importRoutesUsingAbsolutePaths` applies to lazy imports; unusual file names give valid trees. +- A route file used for two routes, and two routes with one id, are errors naming the files. +- A route file that does not parse fails `tsr generate` and builds, naming the file, the root route file included (main read the root without parsing it, so only the bundler failed on it); a build fails on it even when the plugin instance built or watched before. A dev server or watch session keeps the file's last good route and reports it. +- Writes are atomic per write (also when `tmpDir` is on another filesystem); the generator's own writes no longer regenerate. +- `tsr watch` and the Rspack/webpack plugins watch every route root, including `physical()` mounts. +- `GeneratorPlugin.onRouteTreeChanged` receives the tree model (`{ model }`). The internal helpers `format`, `inferFullPath`, `checkRouteFullPathUniqueness` and `writeIfDifferent` are no longer exported. +- The `GeneratorPlugin.afterTransform` hook is removed: a plugin reads every route from the tree model `onRouteTreeChanged` receives. +- `@tanstack/router-generator` and `@tanstack/router-cli` require Node `^20.19.0 || >=22.12.0`: the CommonJS build and the `tsr` binary load the ESM-only parser through `require(esm)`. diff --git a/.changeset/route-hmr-runtime.md b/.changeset/route-hmr-runtime.md new file mode 100644 index 00000000000..48d74057ccf --- /dev/null +++ b/.changeset/route-hmr-runtime.md @@ -0,0 +1,8 @@ +--- +'@tanstack/router-core': patch +'@tanstack/react-router': patch +'@tanstack/solid-router': patch +'@tanstack/vue-router': patch +--- + +Add the development-only route HMR runtime used by the router plugin's output, on new `/hmr` subpaths (`@tanstack/router-core/hmr`, `@tanstack/{react,solid,vue}-router/hmr`). It updates the live route in place, keeps each split component's lazy wrapper across updates, and with Vite's bundled dev falls back to the original importer when Rolldown throws `MissingFactoryError`. An update patches only the route its module exported, so another router in the same window (module federation) is left alone (#7921). After the route tree is regenerated (a route added or renamed), updates keep reaching the route the rebuilt tree holds. In development, once a router error boundary has shown an error, later route edits reload the page, as without route HMR, so the edit that fixes the error shows (Solid and Vue: any router error boundary; React: only the global boundary, since React Refresh recovers route boundaries in place). Nothing of it reaches production bundles. diff --git a/.changeset/router-core-server-route-tree-cache-edits.md b/.changeset/router-core-server-route-tree-cache-edits.md new file mode 100644 index 00000000000..9fa86afdff0 --- /dev/null +++ b/.changeset/router-core-server-route-tree-cache-edits.md @@ -0,0 +1,5 @@ +--- +'@tanstack/router-core': patch +--- + +A dev server whose `NODE_ENV` is not `development` (for example `NODE_ENV=production vite dev`, or a dev server under test) renders a route's edit on the server: the server's shared route tree cache no longer serves the routes the route tree held before the edit. diff --git a/.changeset/router-plugin-library-build-externals.md b/.changeset/router-plugin-library-build-externals.md new file mode 100644 index 00000000000..0945a255765 --- /dev/null +++ b/.changeset/router-plugin-library-build-externals.md @@ -0,0 +1,5 @@ +--- +'@tanstack/router-plugin': patch +--- + +Fix the Vite plugin failing a library build (`build.lib` with `external` dependencies) at the end of the build: the report of split exports that an entry chunk still holds walked the entry's imports, which also name the external packages, and read a chunk that does not exist (`Cannot read properties of undefined (reading 'modules')`). diff --git a/.changeset/router-plugin-order-check-splitting-only.md b/.changeset/router-plugin-order-check-splitting-only.md new file mode 100644 index 00000000000..42f42e205e4 --- /dev/null +++ b/.changeset/router-plugin-order-check-splitting-only.md @@ -0,0 +1,5 @@ +--- +'@tanstack/router-plugin': patch +--- + +Check the Vite plugin order (the router plugin before the framework's JSX plugin) only when `autoCodeSplitting` is on, as before: without splitting, route HMR works whatever the order, so a config listing `react()` or `solid()` first is not an error. diff --git a/.changeset/router-plugin-yuku.md b/.changeset/router-plugin-yuku.md new file mode 100644 index 00000000000..7820a56fd56 --- /dev/null +++ b/.changeset/router-plugin-yuku.md @@ -0,0 +1,26 @@ +--- +'@tanstack/router-plugin': minor +--- + +Route code splitting and route HMR run on the new compiler core instead of Babel: faster dev startup, HMR and builds, with the same production output shape. + +**What changes for your app when you upgrade:** + +- **Vite 6 or later is required** (the `vite` peer range is now `>=6`): the plugin is tested on Vite 6, 7 and 8; Vite 5 is no longer supported. +- **esbuild refuses `autoCodeSplitting: true`** with a clear error (route generation still works there); use Vite, Rspack or webpack for code splitting. +- **CommonJS consumers need Node 20.19+ or 22.12+** (`engines`): a `webpack.config.js` / `rspack.config.js` that `require()`s the plugin loads the ESM-only parser through `require(esm)`, which Node 22.0–22.11 and 21 do not support. +- **Routes the compiler cannot read statically stay unsplit with one warning per file per build** naming the file: route options built by a helper call (`createFileRoute('/x')(makeOptions())`), options that spread or compute, methods that read `this`, and route modules the compiler cannot parse. Main split some of these silently or crashed. With `codeSplittingOptions.deleteNodes` set (as in every TanStack Start client build), options built by a call or imported from another module, and a spread or computed key that may hold a listed option, **fail the build** naming the file instead: the listed options could not be removed. +- **`routeTree.gen.ts` formatting changes once** (`enableRouteTreeFormatting` now defaults to `false`; see the route generator's changeset). +- **Removed:** `codeSplittingOptions.compilerPlugins` (the experimental Babel hook), and `plugin.hmr.style` with its `HmrOptions` type: the option was undocumented, and on Vite its only other value (`'webpack'`) turned route HMR off; each entry now emits its bundler's hot API. +- **Removed from the root entry** (undocumented internals of the Babel splitter): `unpluginRouterCodeSplitterFactory`, `getObjectPropertyKeyName` and the types `CodeSplitCompilerPlugin`, `ReferenceRouteCompilerPlugin`, `ReferenceRouteCompilerPluginContext` and `VirtualRouteSplitNodeCompilerPluginContext`. Code splitting comes with the bundler entries (`@tanstack/router-plugin/vite`, `/rspack`, `/webpack`). + +- Any route option value splits: identifiers, inline functions, method shorthands (#8623), object-form loaders (#8753), casts, `satisfies`, calls such as `memo(Page)`, and options held in a module-level constant. Module state the route module and its chunks share exists once (#8754, #8505). A component exported under a string name (`export { Page as 'page-view' }`) next to a split component builds (main failed). +- **Exported route components and helpers are split too.** An exported pure function, class or component only split code needs leaves the route module: in a build it moves to a small side-effect-free module of its own that the route module re-exports (one instance for every other module importing it from the route file; the bundler drops it when nobody does), and each split chunk carries its own copy, so the chunk never depends on where the bundler places that module (Rolldown < 1.1.5 and a `manualChunks` group merging into the entry kept it eager). In dev the binding stays in the route module and the chunk copies it the same way. An importer of the route file and the route's chunk therefore hold two instances of such a stateless binding (as on main); state, effects and augmented bindings (`Page.displayName = …`) are never copied, and code with side effects of its own never moves into that module (it stays in the route module, in order). Exported bindings that must stay in the route module (side effects, reassignment, read by the route module itself) and extracted modules an entry chunk loads anyway (another module imports the binding through the route file) are each reported once per file, in dev and in builds, on every bundler. +- **A top-level declaration only split code needs that must stay in the route module is reported** once per declaration, in dev and in builds, on every bundler, positioned at it and naming what keeps it: a side effect written after it that stays (for example an inline `validateSearch: type({ … })`, named with its line), several split options reading it, a top-level change of it, or a value it reads as the file loads. `/* @__PURE__ */` on the call, or `/* #__NO_SIDE_EFFECTS__ */` on a function declared in the route file, lets it split. Calls of the router's own side-effect-free APIs with constant arguments (`getRouteApi`, `linkOptions`, `createLink`, `createRouteMask`, `lazyRouteComponent`, `lazyFn`, `retainSearchParams`, `stripSearchParams`, `parseSearchWith`, `stringifySearchWith`, `createRouterConfig`) are no such side effect when imported from the router package (also under another name or as a namespace member): `const { useSearch } = getRouteApi('/x')` splits with the component. +- **Options removed by `codeSplittingOptions.deleteNodes`** (in every TanStack Start client build: `server`, `ssr`, `headers`) take what only they read out of every output of the route file, split chunks and extracted modules included: a default value in a destructuring declaration that a split component also reads (`const { title, secret = readSecret() } = …`, `secret` read only by a server handler) no longer brings its server-only import into a client chunk, nor does it next to a rest element (`const { secret = readSecret(), ...rest } = …` keeps `secret` without its default, so the key stays out of `rest`); main kept them in the route's shared chunk. +- `createFileRoute` & co. are recognised by their import binding. Routes whose options cannot be read statically, and methods or function expressions that read `this` (#8688), stay in the route module with one warning naming the file. +- Route HMR keeps split components' lazy wrappers across updates in React, Solid and Vue, refreshes edits of split loaders and other non-component options on Rspack and webpack, and needs no framework plugin for `.ts` route modules. +- Production output is decided by the bundler's mode and is identical with `addHmr: false`. +- Rspack and webpack use the plugin's own loader: source maps compose with earlier loaders and name the route file (#6667, #3828); project paths with `'`, `#` or spaces work (#7754). Rspack/webpack no longer rebuild every environment once after the first watch build. +- esbuild refuses `autoCodeSplitting` with a clear error; the route generator keeps working there. +- Removed: `codeSplittingOptions.compilerPlugins` (the experimental Babel hook), the `@babel/*` dependencies and the direct `chokidar` dependency (the route generator's watcher brings it). New entry `@tanstack/router-plugin/planners` exposes the route planners to TanStack Start. diff --git a/.changeset/server-fn-registry-follows-edits.md b/.changeset/server-fn-registry-follows-edits.md new file mode 100644 index 00000000000..b9c49d96e63 --- /dev/null +++ b/.changeset/server-fn-registry-follows-edits.md @@ -0,0 +1,5 @@ +--- +'@tanstack/start-plugin-core': patch +--- + +On the Vite dev server, a server function an edit removes is no longer registered: calling it by its old id fails like any unknown id instead of reaching a module that no longer exports it. diff --git a/.changeset/start-compiler-yuku.md b/.changeset/start-compiler-yuku.md new file mode 100644 index 00000000000..7ad742e0c59 --- /dev/null +++ b/.changeset/start-compiler-yuku.md @@ -0,0 +1,56 @@ +--- +'@tanstack/start-plugin-core': minor +'@tanstack/router-core': minor +'@tanstack/start-client-core': patch +'@tanstack/react-start': patch +'@tanstack/solid-start': patch +'@tanstack/vue-start': patch +--- + +Start's Vite and Rsbuild plugins run on the new compiler core instead of Babel: each module compiles once per environment, with server functions, environment separation, ``, RSC CSS, route code splitting and HMR, and import protection in one pass. Builds and dev startup are 2–4× faster on a 340-route benchmark app. + +**What changes for your app when you upgrade:** + +- **Every server function id changes** (a shorter hash of the file and the function's name). Update CDN rules, WAF allowlists, analytics keyed by id and native clients that call `/_serverFn/` directly. Browser tabs loaded before a deploy fail their next server-function call after it: plan a reload or a blue/green window. +- **`serverFns.generateFunctionId` receives the function's stable name** (`greetUser`) as `functionName`, not the generated handler name (`greetUser_createServerFn_handler`). Custom generators keyed on the old names must be updated. +- **Production builds answer HTTP calls only for server functions that some client module imports.** A function only server code calls, or one returned to the client as data, gets a 404 over HTTP. The dev server still answers and warns once per function, so this shows up only after deploying: check the dev warnings before you ship. +- **A module the compiler cannot parse fails the client build** with an error naming the file, instead of shipping untransformed (its server code included). +- **Route options the compiler cannot read statically fail the client build** with an error naming the file: options built by a call or imported from another module (`createFileRoute('/x')(makeOptions())`), and a spread or computed key that may hold `ssr`, `server` or `headers`. The client build removes those options; main shipped them (and the server code they import) silently. Write them as literal properties of the route options (a spread of a module-level object literal that holds none of them still works). +- **Exported route components and helpers split in Start apps too** (see the router plugin's changeset): each split chunk carries its own copy of a pure exported binding, importers of the route file share one instance, and both Start hosts (Vite and Rsbuild) warn after a client build when a module loaded with the entry imports such a binding through the route file. +- **`routeTree.gen.ts` formatting changes once**: `enableRouteTreeFormatting` now defaults to `false` and the tree is printed in your `quoteStyle` and `semicolons`; commit the one-time diff. +- **Removed (experimental, Babel-typed):** the `compilerTransforms` and `serverFnProviderModuleDirectives` options and the `StartCompilerPlugin`, `StartCompilerImportTransform`, `StartCompilerTransformContext` and `StartCompilerTransformCandidate` types. RSC integrations pass `rsc: { loadCssExpression, providerDirective }` instead. + +**Server functions** + +- **Production builds answer HTTP calls only for server functions that client code imports.** A function only server code calls (another server function, a server route), or one returned to the client as data, is refused when called over HTTP. The dev server still answers such a call and warns once per function. +- `serverFns.generateFunctionId` receives the server function's stable key (its export or variable name, e.g. `greetUser`) as `functionName`, instead of the generated handler name (`greetUser_createServerFn_handler`). Custom generators keyed on the old names must be updated. +- Default production ids are shorter (22 characters). +- Server functions are recognised by binding through re-exports, namespaces, aliases, user factories and stored builders, whatever the code formatting (#7280). With SSR as the provider a server function's module evaluates once and server-side callers call it directly (#7213, #8731); the modules only its handlers import still load on the first call, like before, without running the module's code again (a handler that writes state its module reads, or one in a module that awaits at its top level, loads with its module). `createServerOnlyFn(...)` passed to `.handler()` works (#8446). Nested server functions are hoisted on the server when safe. +- Client middleware sees only the server function's id during SSR, as in the browser. +- Vite `bundledDev`: lazy dependencies load, edits no longer crash the hot update, and SSR reflects edits (#8407, #8062, #7535). + +**Import protection** + +- A denied import is replaced by a mock module of its own that exports the target's names. Build violations are decided after tree-shaking; when `onViolation` returns `false` the import gets the real module and the build reruns until no new import is allowed. +- Exempt importers (`ignoreImporters`, `exclude`, `include`) get the real module; dependencies that mark themselves server-only follow `excludeFiles`; `.vue` and `.mdx` importers are checked on their compiled output; a client entry marked server-only fails the build. +- Reports show the import in the original source with a code frame, and traces follow the bundler graph. In dev, a denied import added by an edit is reported. +- Rsbuild: works with Rspack's persistent cache (`performance.buildCache`). + +**Rsbuild** + +- Server-only routes are pruned from the client route tree (#8545); the client manifest lists only the chunks a route loads (#8780). +- Dev answers the SPA shell with `spa.enabled` (#8543) and no longer forces `server.compress: false` (#8544). +- The server environment shares async chunks instead of duplicating the route modules the server function resolver imports. +- `router.codeSplittingOptions.deleteNodes` applies to the server environment, as on Vite: the listed route options are removed from the server build's route modules (they were kept). The client keeps removing `ssr`, `server` and `headers` instead. +- `rsbuild build --watch` picks up route files created while it watches: they never reached the route tree before. + +**Other** + +- Dev compiler state stays bounded by the live modules (Vite and Rsbuild): deleted, renamed or no longer imported files leave no plans, raw facts or import-protection records behind. +- Editing the imports of a route module no longer fully reloads the page. +- Generator plugins given as `router.plugins` run next to Start's (#7768); Start's route tree footer follows `quoteStyle` and `semicolons`, and a function-form `router.routeTreeFileFooter` gives its lines at every print of the route tree on Vite too (Vite kept its first result for the whole dev server). +- Solid Start server environments split route components like the client, so hydration keys agree. +- Require `exsolve` `^1.1.3`, so memory no longer grows across builds in one process. Entry lookups bypass its process-wide cache, so an entry file created after a dev server restart is found. +- The plugins load the router-core helpers they need from the new `@tanstack/router-core/tooling` entry instead of the router runtime. + +**Removed (experimental, Babel-typed):** the `compilerTransforms` and `serverFnProviderModuleDirectives` options and the `StartCompilerPlugin`, `StartCompilerImportTransform`, `StartCompilerTransformContext` and `StartCompilerTransformCandidate` types. RSC integrations pass `rsc: { loadCssExpression, providerDirective }` instead. diff --git a/.changeset/start-plugin-core-file-url-sources.md b/.changeset/start-plugin-core-file-url-sources.md new file mode 100644 index 00000000000..308294409a2 --- /dev/null +++ b/.changeset/start-plugin-core-file-url-sources.md @@ -0,0 +1,5 @@ +--- +'@tanstack/start-plugin-core': patch +--- + +Fix import protection naming the original file of a violation on Windows when another plugin's source map lists its sources as `file://` URLs: the location read the URL's pathname (`/D:/app/src/page.mdx`) instead of the file path (`D:/app/src/page.mdx`). diff --git a/.changeset/start-plugin-core-rspack-falsy-use.md b/.changeset/start-plugin-core-rspack-falsy-use.md new file mode 100644 index 00000000000..0f48896191e --- /dev/null +++ b/.changeset/start-plugin-core-rspack-falsy-use.md @@ -0,0 +1,7 @@ +--- +'@tanstack/start-plugin-core': patch +--- + +The Rsbuild RSC setup skips falsy entries of a rule's `use` list (allowed since `@rspack/core` 2.2.9, e.g. `use: [cond && 'x-loader']`) when it enables React Server Components on the SWC loaders, instead of reading them as loaders. + +RSC CSS HMR on Rsbuild needs `@rspack/core` >= 2.2.9: earlier versions can report a stale client build after a server component's CSS edit (web-infra-dev/rspack#15865), so the page keeps the old styles until the next edit. The peer ranges are unchanged. diff --git a/.changeset/start-rsbuild-dev-stale-dependents.md b/.changeset/start-rsbuild-dev-stale-dependents.md new file mode 100644 index 00000000000..80234f4514b --- /dev/null +++ b/.changeset/start-rsbuild-dev-stale-dependents.md @@ -0,0 +1,6 @@ +--- +'@tanstack/start-plugin-core': patch +'@tanstack/router-utils': patch +--- + +Rsbuild dev: an edit rebuilds the edited module and only the modules compiled against facts the edit changed (a comment in a module hundreds of modules import rebuilds that module alone), and a file that appears and takes a resolution (`./lib` finding a new `lib.ts` before `lib/index.ts`) has its importers compiled again. A dev server or build restarted on Rspack's persistent cache compiles again a restored module whose imports changed while it was down, and keeps tracking the restored modules afterwards. diff --git a/.changeset/start-vite-build-watch-client-references.md b/.changeset/start-vite-build-watch-client-references.md new file mode 100644 index 00000000000..481445295e7 --- /dev/null +++ b/.changeset/start-vite-build-watch-client-references.md @@ -0,0 +1,5 @@ +--- +'@tanstack/start-plugin-core': patch +--- + +`vite build --watch` of a Start app: a server function the client stops importing is no longer callable over HTTP after the rebuild. The server rebuilds when a file of the client's build changes, and its function resolver waits for the client's rebuild of the same edit. diff --git a/.changeset/start-vite-build-watch.md b/.changeset/start-vite-build-watch.md new file mode 100644 index 00000000000..114888fb28b --- /dev/null +++ b/.changeset/start-vite-build-watch.md @@ -0,0 +1,5 @@ +--- +'@tanstack/start-plugin-core': patch +--- + +`vite build --watch` of a Start app builds the server once the client's first build is done, so the server's function resolver lists every function the client calls; a rebuild keeps them, and judges unchanged modules against what an edit changed in the modules they import. diff --git a/.changeset/webpack-family-source-text.md b/.changeset/webpack-family-source-text.md new file mode 100644 index 00000000000..bc2736e7114 --- /dev/null +++ b/.changeset/webpack-family-source-text.md @@ -0,0 +1,7 @@ +--- +'@tanstack/router-plugin': patch +'@tanstack/start-plugin-core': patch +'@tanstack/router-utils': patch +--- + +Rspack, webpack and Rsbuild source maps hold the original text of every file the compilers transform (route files and their split chunks, Start modules, `` chunks), so devtools and error trackers show the file as written instead of the transformed code. In development the text is added to the emitted map files, after the bundler hashed the modules: an edit hot-updates only the modules whose code changed. With a devtool that puts maps inside the code (`eval-*`, `inline-*`), the module maps carry it. diff --git a/.github/workflows/compiler-platforms.yml b/.github/workflows/compiler-platforms.yml index 84afc2c837f..349bbce3f8b 100644 --- a/.github/workflows/compiler-platforms.yml +++ b/.github/workflows/compiler-platforms.yml @@ -2,8 +2,17 @@ name: Compiler Platforms # The main PR workflow only runs on Linux x64 (glibc). The compilers load a # native Yuku binary per platform and handle platform-specific paths, so run -# their unit tests and a Vite and Rsbuild production build on the other -# platforms we ship to. +# their fast test tier (test:unit, the compiler core's included; its client +# checks run in pages of a headless Chromium), a small integration smoke set +# (test:integration:smoke: real Vite, Rspack, webpack and Rsbuild builds, +# including apps under paths with `#`, `%`, spaces and non-ASCII characters, +# and modules the compiler cannot parse) and a Vite and Rsbuild production +# build on the other platforms we ship to. The rest of the slow tier +# (test:integration) runs on Linux x64 in the main PR workflow. +# +# Chromium: Playwright's own build on the glibc platforms; Alpine (musl) has +# no Playwright build, so the job installs the distribution's Chromium and +# names it to the test browser helper (TANSTACK_COMPILER_TESTS_CHROMIUM). on: pull_request: @@ -12,6 +21,8 @@ on: - 'packages/router-generator/**' - 'packages/router-plugin/**' - 'packages/start-plugin-core/**' + - 'packages/compiler-test-utils/**' + - 'packages/react-start-rsc/**' - 'pnpm-lock.yaml' - '.github/workflows/compiler-platforms.yml' schedule: @@ -57,7 +68,9 @@ jobs: - name: Install system packages if: ${{ matrix.container }} shell: sh - run: apk add --no-cache bash git + # Chromium and its fonts for the browser-backed tests (Playwright has + # no musl build; the helper launches this executable instead) + run: apk add --no-cache bash git chromium font-noto - name: Keep LF line endings # Snapshots are stored with LF. Windows runners check out CRLF by default, # and with `* text=auto` disabling autocrlf alone still uses the native eol @@ -81,6 +94,8 @@ jobs: if: ${{ matrix.container }} run: npm install --global "$(node -p "require('./package.json').packageManager")" - name: Install dependencies + # The compiler packages, what their tests build (the start-plugin-core + # tiers depend on the React, Solid and Vue Start builds) and the e2e app run: >- pnpm install --frozen-lockfile --filter . @@ -88,12 +103,39 @@ jobs: --filter "@tanstack/router-generator..." --filter "@tanstack/router-plugin..." --filter "@tanstack/start-plugin-core..." + --filter "@tanstack/react-start-rsc..." + --filter "@tanstack/react-start..." + --filter "@tanstack/solid-start..." + --filter "@tanstack/vue-start..." --filter "tanstack-react-start-e2e-basic..." - - name: Unit tests + - name: Install Chromium (Playwright) + if: ${{ !matrix.container }} + # `--with-deps` installs the shared libraries Chromium needs on Linux + # (a no-op where they are present); the macOS and Windows images need none + run: | + if [ "$RUNNER_OS" = Linux ]; then + pnpm exec playwright install --with-deps chromium + else + pnpm exec playwright install chromium + fi + - name: Use the system Chromium (container) + if: ${{ matrix.container }} + run: echo "TANSTACK_COMPILER_TESTS_CHROMIUM=$(command -v chromium-browser || command -v chromium)" >> "$GITHUB_ENV" + - name: Unit tests (fast tier) run: >- pnpm nx run-many --targets=test:unit --projects=@tanstack/router-utils,@tanstack/router-generator,@tanstack/router-plugin,@tanstack/start-plugin-core --skip-nx-cache --outputStyle=static + - name: Noncompiler tests (runtime and build plumbing) + run: >- + pnpm nx run-many --targets=test:noncompiler + --projects=@tanstack/start-plugin-core,@tanstack/react-start-rsc + --skip-nx-cache --outputStyle=static + - name: Integration smoke (no browser) + run: >- + pnpm nx run-many --targets=test:integration:smoke + --projects=@tanstack/router-plugin,@tanstack/start-plugin-core + --skip-nx-cache --outputStyle=static - name: Production builds run: >- pnpm nx run-many --targets=build:vite:ssr,build:rsbuild:ssr diff --git a/.prettierignore b/.prettierignore index de501452adc..3e5feb96725 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,15 +5,12 @@ **/coverage **/dist pnpm-lock.yaml -**/snapshots **/.vercel **/.output **/node_modules node_modules **/test-results -**/tests/generator/file-modification/routes/(test)/* -**/tests/generator/**/routeTree*.snapshot.ts /.nx/workspace-data **/src/routeTree.gen.ts packages/router-plugin/tests/**/test-files/** diff --git a/AGENTS.md b/AGENTS.md index f47b5103e19..eb0757cacd1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ CI=1 NX_DAEMON=false pnpm nx run-many -p -t --skipRemoteCache ``` - Run one Nx command at a time. For a ~20-second startup/graph stall, stop, run `pnpm nx reset`, and retry once, then escalate. Do not apply that timeout to running tests. -- For code changes, run affected packages' `test:unit`, `test:types`, and `test:eslint` where available, including affected consumers; use `test:e2e` for browser/app behavior and `test:build` for exports/build changes. Root `pnpm test` includes the full e2e suite. Published-code changes require `pnpm changeset`. +- For code changes, run affected packages' `test:unit`, `test:integration`, `test:noncompiler`, `test:types`, and `test:eslint` where available, including affected consumers; use `test:e2e` for browser/app behavior and `test:build` for exports/build changes. Root `pnpm test` includes the full e2e suite. Published-code changes require `pnpm changeset`. ## Runtime rules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b6e808af1f..12ba6656d1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,6 +65,7 @@ Run the checks relevant to your changes while developing: - `pnpm test:eslint` checks linting for affected packages. - `pnpm test:types` checks types for affected packages. - `pnpm test:unit` runs unit tests for affected packages. +- `pnpm test:integration` runs the slow tier of the compiler packages (`router-plugin`, `start-plugin-core`, `compiler-test-utils`): tests that start real builds, dev servers or browsers. - `pnpm test:build` checks builds for affected packages. - `pnpm test:e2e` runs end-to-end tests and requires the Playwright browsers. diff --git a/benchmarks/AGENTS.md b/benchmarks/AGENTS.md index 9d56a2411aa..c8af3dba2e7 100644 --- a/benchmarks/AGENTS.md +++ b/benchmarks/AGENTS.md @@ -2,11 +2,12 @@ Read the relevant README before running benchmarks. Compare baseline and candidate with the same workload and production builds; add a focused case when existing scenarios miss the changed mechanism. Keep workloads and benchmark names stable for historical comparisons. -| Changed behavior | Guide | Nx project | -| ------------------------------------------ | ---------------------------------- | -------------------------------------------------------- | -| Standalone Router navigation/rendering CPU | [client-nav](client-nav/README.md) | `@benchmarks/client-nav` | -| Start request/SSR CPU | [ssr](ssr/README.md) | `@benchmarks/ssr` | -| Client/server retention and allocations | [memory](memory/README.md) | `@benchmarks/memory-client`, `@benchmarks/memory-server` | +| Changed behavior | Guide | Nx project | +| ------------------------------------------ | ---------------------------------------- | ------------------------------------------------------------ | +| Standalone Router navigation/rendering CPU | [client-nav](client-nav/README.md) | `@benchmarks/client-nav` | +| Start request/SSR CPU | [ssr](ssr/README.md) | `@benchmarks/ssr` | +| Client/server retention and allocations | [memory](memory/README.md) | `@benchmarks/memory-client`, `@benchmarks/memory-server` | +| Compiler/plugin dev, HMR and build time | [compiler-perf](compiler-perf/README.md) | `@benchmarks/compiler-perf` (run `scripts/run.mjs` directly) | Use `test:perf:` through Nx. Plain memory runs only smoke-test; memory metrics require CodSpeed. Client navigation benchmarks do not cover Start hydration or client server-function calls. diff --git a/benchmarks/bundle-size/.gitignore b/benchmarks/bundle-size/.gitignore index bd05a98cee4..57d925bd7c5 100644 --- a/benchmarks/bundle-size/.gitignore +++ b/benchmarks/bundle-size/.gitignore @@ -9,3 +9,5 @@ results/*.js results/*.log results/runs/ !results/.gitkeep +dist-server +dist-server-maps diff --git a/benchmarks/bundle-size/README.md b/benchmarks/bundle-size/README.md index 2185d6b43f9..8d0b8dbb9fe 100644 --- a/benchmarks/bundle-size/README.md +++ b/benchmarks/bundle-size/README.md @@ -170,3 +170,20 @@ The measurement script supports optional interfaces for historical backfilling: These are intended for one-off scripts that replay historical commits and append results to the same history dataset shape used for chart generation. If `--append-history` points at a `data.js` file, output is written as `window.BENCHMARK_DATA = ...` for direct GitHub Pages compatibility. + +## Server Output + +`scripts/benchmarks/bundle-size/server.mjs` builds the Start scenarios with +their server environment (`measure.mjs` builds the client only) and reports +server bytes (total, entry chunk), the chunks and modules loaded for a first +request to `/`, and cold start time (process spawn → first SSR response): + +```bash +node scripts/benchmarks/bundle-size/server.mjs --repo-root . \ + --dist-dir benchmarks/bundle-size/dist-server --build \ + --maps-dist-dir benchmarks/bundle-size/dist-server-maps --build-maps \ + --out server.json [--baseline ] +``` + +To compare checkouts, run it with `--repo-root ` and dist +directories inside that checkout (chunks resolve their externals from there). diff --git a/benchmarks/compiler-perf/.gitignore b/benchmarks/compiler-perf/.gitignore new file mode 100644 index 00000000000..4cd272dd4f2 --- /dev/null +++ b/benchmarks/compiler-perf/.gitignore @@ -0,0 +1,2 @@ +apps/ +results/ diff --git a/benchmarks/compiler-perf/README.md b/benchmarks/compiler-perf/README.md new file mode 100644 index 00000000000..b9db98c1810 --- /dev/null +++ b/benchmarks/compiler-perf/README.md @@ -0,0 +1,268 @@ +# Compiler performance harness + +Measures, on large generated apps, how the TanStack compiler (route generator, +code splitting, HMR injection, Start server-function and environment +transforms, `` handling, import protection) affects what users wait for: + +1. **Dev startup**: process start → server listening → first page hydrated in + Chromium, plus a "compile everything" variant that isolates transform + throughput. +2. **HMR**: file save → updated DOM, per kind of edit. +3. **Production build**: wall-clock of `vite build` (client + SSR) and + `rsbuild build`. + +and attributes main-thread time to our plugins (CPU profiles plus per-plugin +hook timing). Optional scenarios measure the **floor** (the same scenarios +with the compiler's outputs replayed, i.e. what they would cost with a +zero-cost compiler) and **memory** (peak RSS, heap after GC, HMR leak check). + +## Workloads + +`scripts/generate.mjs` writes deterministic (seeded) apps into `apps/` (git-ignored): + +| variant | contents | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `react-start` | TanStack Start (Vite and Rsbuild configs): 338 route files (10 sections × 3 groups, nested layouts, pathless/group/splat/param routes, 5 server routes), loaders, `validateSearch`/`loaderDeps`, `beforeLoad`, pending/error/notFound components, 106 server functions (20 files + inline, validators, middleware), `createIsomorphicFn`/`createServerOnlyFn`/`createClientOnlyFn`, `ClientOnly`, 10 `` boundaries, `*.server.ts` modules, 40 shared components | +| `react-router` | the same route tree as a client-only Router app with `autoCodeSplitting`, no Start features | +| `solid-start` | `react-start` written for Solid (signals, ``/``, `class`, accessor router hooks); Vite only (`vite-plugin-solid`; no Rsbuild config, which would need `@rsbuild/plugin-babel` + `@rsbuild/plugin-solid`). Same route tree, server functions and probes | +| `solid-router` | `react-router` written for Solid | +| `react-start-deps` | `react-start` plus a large generated dependency graph under the app's own `node_modules` (`@bench/icons`: a side-effect-free barrel of 2,000 icon components; `@bench/utils`: a chain of 400 modules; `@bench/start-lib`: a Start library calling `createServerFn`, wrapping `` and importing a 2 MB `engine.wasm?url` asset) that every route imports, with tanstack.com's `manualChunks` group of Start's runtime in its Vite config. Opt-in (`--variants react-start-deps`); no Rsbuild config. Shows dependency-scope regressions of the compilers (what is compiled, read or parsed under `node_modules`: tanstack.com issue 4 compiled 22.6 k dependency modules and read a 13 MB asset as text) | + +Every result records a hash of the generated sources (`workloads..hash`); +`compare.mjs` warns when baseline and candidate inputs differ. Apps resolve +dependencies through this package, which links the built workspace packages. + +## Running + +From the repository root of any checkout that contains this directory: + +```sh +CI=1 pnpm install --frozen-lockfile +# Builds @tanstack/react-start, router-plugin, react-router through Nx, then runs everything. +node benchmarks/compiler-perf/scripts/run.mjs --label --out +``` + +Results: `/results.json` (all samples + stats), `/summary.md`, +`/run.log`, and `/profiles/` (CPU profiles + hook timings). +Without `--out`, results go to `benchmarks/compiler-perf/results/