TL;DR: Route tests on big TanStack Router apps import every route. tanstack-router-scoped-trees generates one route tree per section with TanStack's own generator, so a test loads only its section. That saved 0.8 to 1.6 s per test file on a 1,000-route app. Install:
npm i -D tanstack-router-scoped-trees(npm ยท GitHub)
The app I work on has more than 2,500 tests. Most of them are route tests so each of those started like this:
import { routeTree } from "../routeTree.gen";
That one import pulls in every route in the app. So a test for the invoices page also loads settings, admin, onboarding, everything. Every test file pays for it, again and again.
Hand-made test trees don't last
The obvious fix is to build a tiny route tree by hand for each test. I tried it. It's fast, but it's a second copy of your routes, and it stops matching the real ones the first time someone adds a layout or renames a file. Then your tests pass against routes that don't exist anymore.
I wanted the small tree without the second copy.
One tree per section, built by TanStack
So I made tanstack-router-scoped-trees. On the first run it detects your sections and writes a config like this, which you then own:
import { defineConfig } from "tanstack-router-scoped-trees";
export default defineConfig({
sections: {
billing: ["/billing"],
settings: ["/settings"],
},
});
and it writes one route tree per section.
The important bit: it never edits TanStack's generated file. It hands just that section's routes to TanStack's own generator, and a parity check proves the result is identical to what TanStack makes from only those files. You get exactly the tree TanStack Router would make. Parent routes come along too (the root, any layouts above), so nested routes still work.
Your billing test now imports the billing tree and nothing else:
import { routeTree as billingRouteTree } from "./route-trees/billing.gen.js";
const router = createRouter({
routeTree: billingRouteTree,
history: createMemoryHistory({ initialEntries: ["/billing/invoices"] }),
});
Setup takes a few minutes
Install it, plus the generator at the exact version your router plugin already uses (otherwise npm may install a second copy and the trees may not match):
npm i -D tanstack-router-scoped-trees
npm ls @tanstack/router-generator
npm i -D @tanstack/router-generator@<the version router-plugin uses>
Add the plugin after tanstackRouter():
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. It guesses your sections from the first part of each URL (/billing, /settings) and writes the config. From then on the config is yours. Commit it with the generated trees.
CI catches drift
Generated files that silently go stale would be worse than slow tests, so CI checks two things.
npx tanstack-router-scoped-trees generate --check fails if a tree is out of date, or if someone adds a route that's in no section. The error names the route and gives you the config line to paste.
And one parity test rebuilds every section in memory and compares it with what TanStack generates from the same files:
import { checkScopedRouteTrees, formatParityDifferences } from "tanstack-router-scoped-trees";
import { expect, it } from "vitest";
it("keeps every scoped tree in parity", async () => {
expect(formatParityDifferences(await checkScopedRouteTrees())).toBe("");
}, 60_000);
0.8 to 1.6 seconds per test file
To measure it outside my work app, I generated one with 1,000 routes (40 sections of 25) and ran the same test against the full tree and a section tree, 5 runs each, alternating. The section tree saved 0.8 to 1.6 s per test file process, 25% to 46% of the wall time.
That's my laptop, not CI, and your mileage depends on how big your app is. The benchmark is in the repo, so you can run it yourself with npm run bench.
What it won't do
Import only one tree per test file. TanStack's trees share the root route, so a second import breaks the first. If a test redirects out of its section, give it its own file and use the full tree there.
It's file-based routing only for now, and apps using virtualRouteConfig aren't supported yet.
Try it on a big app
It's a community package, not an official TanStack one. If your app has a few hundred routes, I'd really like to know how much it saves you, and where it gets in the way. Issues and PRs welcome: https://github.com/jdvivar/tanstack-router-scoped-trees
Top comments (0)