tanstack-router-scoped-trees
v0.1.1
Published
Generate per-section TanStack Router route trees for faster tests. A community package, not an official TanStack package.
Maintainers
Readme
tanstack-router-scoped-trees
Generate one TanStack Router route tree per section of your app, so a route test loads only the routes it needs.
This is a community package. It is not affiliated with TanStack and not an official TanStack package.
Why
TanStack Router builds one route tree for the whole app. A test for a single billing screen still imports every route in the app before it can render. On a large app that cost is paid in every test file.
This package generates a smaller tree for each section you define (billing, settings, and so on). A billing test imports the billing tree and nothing else.
In a local measurement (one machine, not CI) on a generated app with 1,000 routes, a test file ran 0.8 to 1.6 s faster per process, 25% to 46% less wall time. Details and caveats are in examples/benchmark/RESULTS.md. Your numbers depend on your app.
Install
npm i -D tanstack-router-scoped-treesAlso add @tanstack/router-generator as a devDependency, at the exact version your @tanstack/router-plugin uses. Without it, npm can install a second, newer copy for this package, and the trees would come from a different generator than the dev server's. Find the version and pin it:
npm ls @tanstack/router-generator
npm i -D @tanstack/router-generator@<version under @tanstack/router-plugin>
npm ls @tanstack/router-generatorThe last command must show one version. If it does not, the CLI stops and the Vite plugin and the parity test say which versions differ. See versions.
Set up in five minutes
The code below comes from examples/basic.
Add the plugin after
tanstackRouter()invite.config.ts:import { tanstackRouter } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; import { scopedRouteTrees } from "tanstack-router-scoped-trees/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [tanstackRouter(), scopedRouteTrees(), react()], });Start the dev server once. The plugin detects your sections, writes
scoped-route-trees.config.tsand generates the trees. Review the config:import { defineConfig } from "tanstack-router-scoped-trees"; export default defineConfig({ sections: { billing: ["/billing"], about: ["/about"], settings: ["/settings"], }, });Commit the config and the generated trees (by default in
src/test/route-trees). Add that folder to.prettierignore, next torouteTree.gen.ts, and to any other formatter or linter that rewrites files. A reformatted tree no longer matches a fresh generation, sogenerate --checkwould fail:src/routeTree.gen.ts src/test/route-treesIn one test, swap the full tree for a scoped one:
import { createMemoryHistory, createRouter, RouterProvider } from "@tanstack/react-router"; import { render, screen } from "@testing-library/react"; import { expect, it } from "vitest"; import { routeTree as billingRouteTree } from "./route-trees/billing.gen.js"; it("renders the invoices page", async () => { const router = createRouter({ routeTree: billingRouteTree, history: createMemoryHistory({ initialEntries: ["/billing/invoices"] }), }); render(<RouterProvider router={router} />); expect(await screen.findByRole("heading", { name: "Invoices" })).toBeDefined(); });
Generated files export TanStack's plain routeTree, so rename it on import.
Each section tree also contains the parent routes it needs (the root, and layouts above your routes), so nested routes keep working.
One tree per test file
Import only one route tree in each test file. TanStack's generated trees change the shared root route, so a second import breaks the first. This includes mixing a scoped tree with the full tree.
To assert a redirect out of your section, write that test in its own file and import the full tree there. See pitfalls.
CLI
npx tanstack-router-scoped-trees init
npx tanstack-router-scoped-trees generate
npx tanstack-router-scoped-trees generate --checkinitwritesscoped-route-trees.config.tsfrom the sections it detects.generaterefreshes the full tree and every scoped tree. It also creates the config if it is missing.generate --checkwrites nothing and exits 1 if a tree is stale, a route is in no section, or parity fails. It is the check that compares committed trees with a fresh generation. Use it in CI. See CI.
All commands take --root <dir>, and --version prints the package version.
The CLI reads routing options from tsr.config.json only. Keep every option that changes the generated output there too (quoteStyle, semicolons, routeFileIgnorePattern and so on). If such an option is only set inline in tanstackRouter(), the CLI and the dev server write different full trees and generate --check fails.
Parity test
Add one test that builds every section tree in memory and checks it against what TanStack itself generates from the same files. It does not read your committed trees:
import { checkScopedRouteTrees, formatParityDifferences } from "tanstack-router-scoped-trees";
import { expect, it } from "vitest";
it("keeps every scoped tree in parity with the full route tree", async () => {
expect(formatParityDifferences(await checkScopedRouteTrees())).toBe("");
}, 60_000);It runs one TanStack crawl per section, so give it a longer timeout than the default.
Not supported in v1
- Apps that set
virtualRouteConfig(the tool stops with a message). - Code-based routing. Only file-based routes.
Guides
- Options: every config field and its default.
- Sections: how prefixes, parents and common routes work.
- CI: keep trees fresh and checked.
- Pitfalls: redirects out of a section, and tests that now fail.
- Troubleshooting
- Versions
- Contributing
License
MIT
