diff --git a/.changeset/proud-clubs-buy.md b/.changeset/proud-clubs-buy.md deleted file mode 100644 index 4418e5101f..0000000000 --- a/.changeset/proud-clubs-buy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@tanstack/solid-router': patch ---- - -Remove unnecessary setTimeout from Match components diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index 866db1399b..d0deda36a7 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -98,6 +98,7 @@ jobs: --output benchmarks/bundle-size/results/pr-comment.md \ --base-sha "$BASE_SHA" \ --dashboard-url "https://${REPOSITORY_OWNER}.github.io/${REPOSITORY_NAME}/benchmarks/bundle-size/" + cat benchmarks/bundle-size/results/pr-comment.md - name: Upsert Sticky PR Comment env: diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml index cd47ca3ba1..a56bb70334 100644 --- a/.github/workflows/client-nav-benchmarks.yml +++ b/.github/workflows/client-nav-benchmarks.yml @@ -3,11 +3,18 @@ name: Benchmarks on: push: + # Broad on purpose: CodSpeed compares each PR against the run on its + # merge-base commit. Skipping main pushes that don't touch packages/** + # (pnpm config, CI, root tooling) leaves those commits without a baseline + # run, and CodSpeed silently falls back to an older, possibly outlier run — + # which then flags phantom regressions on every PR until the next eligible + # push. Only paths that cannot affect benchmark results are excluded. branches: - 'main' - paths: - - 'packages/**' - - 'benchmarks/**' + paths-ignore: + - 'docs/**' + - 'examples/**' + - 'e2e/**' pull_request: paths: - 'packages/**' @@ -36,6 +43,17 @@ jobs: benchmark: - client-nav - ssr + - memory-server + - memory-client + include: + - benchmark: client-nav + mode: simulation + - benchmark: ssr + mode: simulation + - benchmark: memory-server + mode: memory + - benchmark: memory-client + mode: memory runs-on: ubuntu-latest steps: - name: Checkout @@ -46,8 +64,19 @@ jobs: - name: Setup Tools uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + # Run the build outside of the CodSpeed action to avoid being slowed by intrumentation overhead. + # (and then run the task itself with --excludeTaskDependencies) + - name: Prepare ${{ matrix.benchmark }}:${{ matrix.framework }} benchmark + run: >- + pnpm nx run + @benchmarks/${{ matrix.benchmark }}:build:${{ matrix.framework }} + - name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0 with: - mode: simulation - run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }} + mode: ${{ matrix.mode }} + run: >- + WITH_INSTRUMENTATION=1 + pnpm nx run + @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }} + --excludeTaskDependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb5c69751c..115c3445e6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,7 @@ jobs: commit: 'ci: changeset release' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_OPTIONS: --max-old-space-size=8192 - name: Create GitHub Release if: steps.changesets.outputs.published == 'true' run: node scripts/create-github-release.mjs ${{ steps.dist-tag.outputs.prerelease == 'true' && '--prerelease' }} ${{ steps.dist-tag.outputs.latest == 'true' && '--latest' }} diff --git a/.gitignore b/.gitignore index 4c5184f4d8..1699592784 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ vite.config.ts.timestamp_* # eslint-plugin-start perf fixtures /e2e/eslint-plugin-start/src/perf/generated + +# local memory flame profiles +benchmarks/memory/**/.profiles/ diff --git a/benchmarks/bundle-size/package.json b/benchmarks/bundle-size/package.json index 98fd18a35f..ef4580c4b7 100644 --- a/benchmarks/bundle-size/package.json +++ b/benchmarks/bundle-size/package.json @@ -40,7 +40,7 @@ "vue": "^3.5.16" }, "devDependencies": { - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tanstack/router-plugin": "workspace:^", "@types/react": "^19.0.8", diff --git a/benchmarks/client-nav/README.md b/benchmarks/client-nav/README.md index 44b58107e3..6179c7f0b8 100644 --- a/benchmarks/client-nav/README.md +++ b/benchmarks/client-nav/README.md @@ -1,16 +1,80 @@ # Client Navigation Benchmarks -Cross-framework client-side navigation benchmarks for: +Cross-framework client-side CPU benchmarks for: - `@tanstack/react-router` - `@tanstack/solid-router` - `@tanstack/vue-router` +The benchmarks run in jsdom against production builds of real apps, and are +tracked in CI by CodSpeed (simulation mode). + +> **Scope:** these benchmarks cover the standalone client router only. The +> client side of TanStack Start (hydration of a server-rendered document, +> streamed payload consumption, server-function calls from the client, ...) is +> not covered here; the server side of Start is covered by `benchmarks/ssr`. + ## Layout -- `react/` - React benchmark + Vitest config -- `solid/` - Solid benchmark + Vitest config -- `vue/` - Vue benchmark + Vitest config +- `react/`, `solid/`, `vue/` - baseline benchmark (mixed navigation loop) + Vitest config +- `vitest.react.config.ts`, `vitest.solid.config.ts`, `vitest.vue.config.ts` - per-framework aggregate configs that run the baseline first, then scenario projects +- `scenarios/harness.ts` - shared scenario runner (mount, link-click steps, `onRendered` synchronization) +- `scenarios//shared.ts` - framework-agnostic scenario definition (workload data, step sequence, assertions, bench options) +- `scenarios///` - isolated scenario apps + +Scenario app layout: + +```text +scenarios/// + vite.config.ts + speed.bench.ts + speed.flame.ts + setup.ts + project.json + tsconfig.json + src/ + main.tsx + routeTree.gen.ts + routes/ +``` + +Scenario apps use file-based routing (`@tanstack/router-plugin`) with a +generated `routeTree.gen.ts`, like a regular user app. Each scenario uses one +app per framework instead of sharing routes in the baseline app. This keeps +route-tree size and router options isolated so one scenario cannot shift +another scenario's numbers. The existing baseline apps and bench names stay +stable for CodSpeed continuity. + +## Scenario Responsibilities + +Each scenario isolates one client-side responsibility so benchmark changes can +be attributed to a specific feature area. + +| Scenario | Client-side responsibility | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `react/`, `solid/`, `vue/` baseline apps | Mixed navigation loop: path params, search params, route context, and `useParams`/`useSearch`/`useLoaderData` selector subscriptions. | +| `async-pipeline` | The router's async pipeline via counted 0ms timer hops: async loaders (transition-held navigation), async `beforeLoad` context, and parallel nested async loaders. Component-level `Await`/Suspense is excluded: React 19 throttles Suspense reveals by ~300ms wall-clock, which is inherently non-deterministic to benchmark. | +| `control-flow` | Loader-thrown `redirect` (including a 2-hop chain), `notFound()` with `notFoundComponent`, loader errors with `errorComponent`, and boundary reset on recovery navigation. | +| `head` | `HeadContent` per-navigation work: nested route `head()` evaluation, title/meta/link dedupe across matches, and head tag DOM updates during navigation. | +| `history` | History push/replace/back/forward traversal, location masking, registered-but-never-blocking `useBlocker`, and `useCanGoBack`/`useLocation` subscriptions. | +| `links` | Per-navigation cost of ~200 mounted ``s: link prop building, active-state recompute across `activeOptions` variants, `activeProps` swaps, and `useMatchRoute` probes (the `MatchRoute` component is avoided: vue-router's implementation leaks one subscription per render). | +| `loaders` | Client loader dispatch: always-stale re-runs (`staleTime: 0`), cached revisits (re-run once per lap by the `invalidate` step), `loaderDeps`-keyed caching, `router.invalidate()`, and `useLoaderData` selectors. | +| `mount` | Cold start: `createRouter` (route-tree processing) + first render + initial `router.load()` + unmount, with a fresh router per mount. The only scenario measuring router creation. | +| `nested-params` | Deep nesting (8 dynamic levels): per-level `params.parse`/`stringify`, `beforeLoad` context accumulation across matches, and per-level `useParams`/`useRouteContext` subscriptions. Param values include characters requiring percent-encoding (as do `route-tree-scale`'s), so segment encode/decode paths run on every navigation. | +| `preload` | Intent preloading from hover events, programmatic `router.preloadRoute`, deterministic preload cache behavior (`defaultPreloadStaleTime: 0`), and commit-time cache maintenance. | +| `rewrites` | Composed client-side location rewrites: router `basepath` plus a locale input/output rewrite pair, running on every href build and location parse (the client analog of the SSR `rewrites` scenario). | +| `route-tree-scale` | Route matching and link-target resolution on a wide (~40 route) tree mixing static, dynamic, prefixed-param, splat, pathless-layout, and route-group paths, with `autoCodeSplitting` enabled so navigations also resolve lazy route chunks. | +| `search-params` | `validateSearch` execution, search middlewares (`retainSearchParams`/`stripSearchParams`), functional search updaters, structural sharing, and `useSearch` selector subscriptions. | + +## Conventions + +- Apps are built with `NODE_ENV=production` (`minify: false`) into `dist/app.js`; benches import the built bundle, so production package builds and production JSX output are measured, not dev transforms. +- Scenarios behave like a real user app: navigation happens through `` clicks dispatched on real anchor elements (unless a scenario specifically measures the imperative API), the router uses the default browser history, and `scrollRestoration` is enabled. +- Each benchmark iteration advances a fixed, circular sequence of steps; every step awaits the router's `onRendered` event, so render work is included and steps cannot overlap. No two consecutive steps may target the same location, and the sequence ends back on the initial route. +- Setup runs one warm-up lap through the sequence and asserts each step's observable output (e.g. `document.title`), so a scenario that silently stops doing its work fails instead of reporting a fast time. +- Determinism: no wall-clock timers (async work is resolved promises or counted `setTimeout(0)` hops), `staleTime`/`gcTime` only ever `0` or effectively-infinite, no `Math.random`/`Date.now` in the measured loop. Scroll restoration uses `getScrollRestorationKey: (location) => location.pathname` so its cache stays bounded (the default random per-entry key grows one cache entry per push navigation). The `mount` scenario opts out of `scrollRestoration` entirely and destroys its history on unmount — both register page-lifetime globals that would leak across its mount/unmount loop. +- Push-only step laps grow jsdom's session-history entry list over a run (jsdom never truncates entries behind the current index). Per-push cost stays O(1) so timings are unaffected; only the `history` scenario needs — and has — a lap that is depth-stationary. +- Calibration: pick per-iteration step counts so a bench's `vitest bench` run stays roughly between 8 and 30 seconds (long enough to average out, short enough for CI). ## Run @@ -20,7 +84,7 @@ Run all benchmarks through Nx so dependency builds are part of the graph: CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf --outputStyle=stream --skipRemoteCache ``` -Run framework-specific benchmarks: +Run framework-specific benchmarks (baseline + all scenarios): ```bash CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:react --outputStyle=stream --skipRemoteCache @@ -28,15 +92,25 @@ CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:solid --output CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:vue --outputStyle=stream --skipRemoteCache ``` +Run a single scenario app manually (after building it through Nx): + +```bash +CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav--:build:client --outputStyle=stream --skipRemoteCache +cd benchmarks/client-nav && NODE_ENV=production vitest bench --config ./scenarios///vite.config.ts +``` + Run framework-specific flame benchmarks (10 second loop, profiled with `@platformatic/flame`, forced to `NODE_ENV=production`): ```bash +# baseline CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:react --outputStyle=stream --skipRemoteCache CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:solid --outputStyle=stream --skipRemoteCache CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:vue --outputStyle=stream --skipRemoteCache +# scenarios +CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav--:test:flame --outputStyle=stream --skipRemoteCache ``` -Typecheck benchmark sources: +Typecheck benchmark sources (baseline + scenarios): ```bash CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:types --outputStyle=stream --skipRemoteCache diff --git a/benchmarks/client-nav/jsdom.ts b/benchmarks/client-nav/jsdom.ts index 05535e693d..855c2a2316 100644 --- a/benchmarks/client-nav/jsdom.ts +++ b/benchmarks/client-nav/jsdom.ts @@ -30,6 +30,11 @@ setGlobal('MutationObserver', window.MutationObserver) setGlobal('sessionStorage', window.sessionStorage) setGlobal('localStorage', window.localStorage) setGlobal('getComputedStyle', window.getComputedStyle.bind(window)) +setGlobal('addEventListener', window.addEventListener.bind(window)) +setGlobal('removeEventListener', window.removeEventListener.bind(window)) +setGlobal('dispatchEvent', window.dispatchEvent.bind(window)) +setGlobal('CustomEvent', window.CustomEvent) +setGlobal('Event', window.Event) setGlobal( 'requestAnimationFrame', @@ -45,5 +50,6 @@ setGlobal( ) window.scrollTo = () => {} +setGlobal('scrollTo', () => {}) export { window } diff --git a/benchmarks/client-nav/package.json b/benchmarks/client-nav/package.json index 99835d0c75..04b2888cd0 100644 --- a/benchmarks/client-nav/package.json +++ b/benchmarks/client-nav/package.json @@ -10,9 +10,9 @@ "test:flame:solid": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./solid/speed.flame.ts", "test:flame:vue": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./vue/speed.flame.ts", "test:perf": "NODE_ENV=production vitest bench --config ./vitest.config.ts", - "test:perf:react": "NODE_ENV=production vitest bench --config ./react/vite.config.ts ./react/speed.bench.ts", - "test:perf:solid": "NODE_ENV=production vitest bench --config ./solid/vite.config.ts ./solid/speed.bench.ts", - "test:perf:vue": "NODE_ENV=production vitest bench --config ./vue/vite.config.ts ./vue/speed.bench.ts", + "test:perf:react": "NODE_ENV=production vitest bench --config ./vitest.react.config.ts", + "test:perf:solid": "NODE_ENV=production vitest bench --config ./vitest.solid.config.ts", + "test:perf:vue": "NODE_ENV=production vitest bench --config ./vitest.vue.config.ts", "test:types": "pnpm run test:types:react && pnpm run test:types:solid && pnpm run test:types:vue", "test:types:react": "tsc -p ./react/tsconfig.json --noEmit", "test:types:solid": "tsc -p ./solid/tsconfig.json --noEmit", @@ -32,6 +32,7 @@ "devDependencies": { "@codspeed/vitest-plugin": "^5.5.0", "@platformatic/flame": "^1.6.0", + "@tanstack/router-plugin": "workspace:^", "@testing-library/react": "^16.2.0", "@types/jsdom": "28.0.0", "@vitejs/plugin-react": "^6.0.1", @@ -52,6 +53,23 @@ "@tanstack/react-router" ], "target": "build" + }, + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-react", + "@benchmarks/client-nav-control-flow-react", + "@benchmarks/client-nav-head-react", + "@benchmarks/client-nav-history-react", + "@benchmarks/client-nav-links-react", + "@benchmarks/client-nav-loaders-react", + "@benchmarks/client-nav-mount-react", + "@benchmarks/client-nav-nested-params-react", + "@benchmarks/client-nav-preload-react", + "@benchmarks/client-nav-rewrites-react", + "@benchmarks/client-nav-route-tree-scale-react", + "@benchmarks/client-nav-search-params-react" + ], + "target": "build:client" } ] }, @@ -63,6 +81,23 @@ "@tanstack/solid-router" ], "target": "build" + }, + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-solid", + "@benchmarks/client-nav-control-flow-solid", + "@benchmarks/client-nav-head-solid", + "@benchmarks/client-nav-history-solid", + "@benchmarks/client-nav-links-solid", + "@benchmarks/client-nav-loaders-solid", + "@benchmarks/client-nav-mount-solid", + "@benchmarks/client-nav-nested-params-solid", + "@benchmarks/client-nav-preload-solid", + "@benchmarks/client-nav-rewrites-solid", + "@benchmarks/client-nav-route-tree-scale-solid", + "@benchmarks/client-nav-search-params-solid" + ], + "target": "build:client" } ] }, @@ -74,6 +109,23 @@ "@tanstack/vue-router" ], "target": "build" + }, + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-vue", + "@benchmarks/client-nav-control-flow-vue", + "@benchmarks/client-nav-head-vue", + "@benchmarks/client-nav-history-vue", + "@benchmarks/client-nav-links-vue", + "@benchmarks/client-nav-loaders-vue", + "@benchmarks/client-nav-mount-vue", + "@benchmarks/client-nav-nested-params-vue", + "@benchmarks/client-nav-preload-vue", + "@benchmarks/client-nav-rewrites-vue", + "@benchmarks/client-nav-route-tree-scale-vue", + "@benchmarks/client-nav-search-params-vue" + ], + "target": "build:client" } ] }, @@ -124,7 +176,48 @@ "test:types": { "cache": false, "dependsOn": [ - "^build" + "^build", + { + "projects": [ + "@benchmarks/client-nav-async-pipeline-react", + "@benchmarks/client-nav-async-pipeline-solid", + "@benchmarks/client-nav-async-pipeline-vue", + "@benchmarks/client-nav-control-flow-react", + "@benchmarks/client-nav-control-flow-solid", + "@benchmarks/client-nav-control-flow-vue", + "@benchmarks/client-nav-head-react", + "@benchmarks/client-nav-head-solid", + "@benchmarks/client-nav-head-vue", + "@benchmarks/client-nav-history-react", + "@benchmarks/client-nav-history-solid", + "@benchmarks/client-nav-history-vue", + "@benchmarks/client-nav-links-react", + "@benchmarks/client-nav-links-solid", + "@benchmarks/client-nav-links-vue", + "@benchmarks/client-nav-loaders-react", + "@benchmarks/client-nav-loaders-solid", + "@benchmarks/client-nav-loaders-vue", + "@benchmarks/client-nav-mount-react", + "@benchmarks/client-nav-mount-solid", + "@benchmarks/client-nav-mount-vue", + "@benchmarks/client-nav-nested-params-react", + "@benchmarks/client-nav-nested-params-solid", + "@benchmarks/client-nav-nested-params-vue", + "@benchmarks/client-nav-preload-react", + "@benchmarks/client-nav-preload-solid", + "@benchmarks/client-nav-preload-vue", + "@benchmarks/client-nav-rewrites-react", + "@benchmarks/client-nav-route-tree-scale-react", + "@benchmarks/client-nav-rewrites-solid", + "@benchmarks/client-nav-route-tree-scale-solid", + "@benchmarks/client-nav-rewrites-vue", + "@benchmarks/client-nav-route-tree-scale-vue", + "@benchmarks/client-nav-search-params-react", + "@benchmarks/client-nav-search-params-solid", + "@benchmarks/client-nav-search-params-vue" + ], + "target": "test:types:client" + } ] } } diff --git a/benchmarks/client-nav/react/vite.config.ts b/benchmarks/client-nav/react/vite.config.ts index ffa1d41be8..adc31ddd64 100644 --- a/benchmarks/client-nav/react/vite.config.ts +++ b/benchmarks/client-nav/react/vite.config.ts @@ -1,8 +1,14 @@ +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import codspeedPlugin from '@codspeed/vitest-plugin' +// Anchor the project root to the package directory so this config resolves +// identically when run directly and as part of an aggregate `projects` config. +const rootDir = fileURLToPath(new URL('..', import.meta.url)) + export default defineConfig({ + root: rootDir, define: { 'process.env.NODE_ENV': JSON.stringify('production'), }, @@ -25,6 +31,6 @@ export default defineConfig({ name: '@benchmarks/client-nav (react)', watch: false, environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], + setupFiles: [`${rootDir}vitest.setup.ts`], }, }) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/project.json b/benchmarks/client-nav/scenarios/async-pipeline/react/project.json new file mode 100644 index 0000000000..44ab55ccfe --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-async-pipeline-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/async-pipeline/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/setup.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts new file mode 100644 index 0000000000..68c5f97c2f --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-async-pipeline', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-async-pipeline navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/speed.flame.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx new file mode 100644 index 0000000000..4b141859e6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/main.tsx @@ -0,0 +1,37 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPendingMs: 0, + defaultPendingMinMs: 0, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..2b3336859a --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routeTree.gen.ts @@ -0,0 +1,140 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as NestedRouteImport } from './routes/nested' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as NestedIdRouteImport } from './routes/nested.$id' +import { Route as CtxIdRouteImport } from './routes/ctx.$id' + +const NestedRoute = NestedRouteImport.update({ + id: '/nested', + path: '/nested', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const NestedIdRoute = NestedIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => NestedRoute, +} as any) +const CtxIdRoute = CtxIdRouteImport.update({ + id: '/ctx/$id', + path: '/ctx/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + id: '__root__' | '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + NestedRoute: typeof NestedRouteWithChildren + CtxIdRoute: typeof CtxIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/nested': { + id: '/nested' + path: '/nested' + fullPath: '/nested' + preLoaderRoute: typeof NestedRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/nested/$id': { + id: '/nested/$id' + path: '/$id' + fullPath: '/nested/$id' + preLoaderRoute: typeof NestedIdRouteImport + parentRoute: typeof NestedRoute + } + '/ctx/$id': { + id: '/ctx/$id' + path: '/ctx/$id' + fullPath: '/ctx/$id' + preLoaderRoute: typeof CtxIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface NestedRouteChildren { + NestedIdRoute: typeof NestedIdRoute +} + +const NestedRouteChildren: NestedRouteChildren = { + NestedIdRoute: NestedIdRoute, +} + +const NestedRouteWithChildren = + NestedRoute._addFileChildren(NestedRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + NestedRoute: NestedRouteWithChildren, + CtxIdRoute: CtxIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/__root.tsx new file mode 100644 index 0000000000..1cc8566678 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/__root.tsx @@ -0,0 +1,50 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const ids = ['1', '2'] + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/ctx.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/ctx.$id.tsx new file mode 100644 index 0000000000..d644376b7b --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/ctx.$id.tsx @@ -0,0 +1,29 @@ +import { createFileRoute } from '@tanstack/react-router' +import { + ctxSeedValue, + ctxStateValue, + deferredHops, + hopDelay, +} from '../../../shared' + +export const Route = createFileRoute('/ctx/$id')({ + staleTime: 0, + gcTime: 0, + beforeLoad: async ({ params }) => { + await hopDelay(deferredHops) + return { ctxSeed: ctxSeedValue(params.id) } + }, + loader: ({ params, context }) => { + if (context.ctxSeed !== ctxSeedValue(params.id)) { + throw new Error('beforeLoad context missing in loader') + } + return { value: ctxStateValue(params.id) } + }, + component: CtxPage, +}) + +function CtxPage() { + const data = Route.useLoaderData() + + return
{data.value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/index.tsx new file mode 100644 index 0000000000..5b6d70e744 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.$id.tsx new file mode 100644 index 0000000000..bc002c303f --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { deferredHops, hopDelay, nestedStateValue } from '../../../shared' + +export const Route = createFileRoute('/nested/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: nestedStateValue(params.id) } + }, + component: NestedPage, +}) + +function NestedPage() { + const data = Route.useLoaderData() + + return
{data.value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.tsx new file mode 100644 index 0000000000..443f592373 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/nested.tsx @@ -0,0 +1,23 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { deferredHops, hopDelay, nestedLayoutValue } from '../../../shared' + +export const Route = createFileRoute('/nested')({ + staleTime: 0, + gcTime: 0, + loader: async () => { + await hopDelay(deferredHops) + return { value: nestedLayoutValue() } + }, + component: NestedLayout, +}) + +function NestedLayout() { + const data = Route.useLoaderData() + + return ( +
+
{data.value}
+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/slow.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..4839b0ca6d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/src/routes/slow.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { deferredHops, hopDelay, slowStateValue } from '../../../shared' + +export const Route = createFileRoute('/slow/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: slowStateValue(params.id) } + }, + component: SlowPage, +}) + +function SlowPage() { + const data = Route.useLoaderData() + + return
{data.value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/tsconfig.json b/benchmarks/client-nav/scenarios/async-pipeline/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/react/vite.config.ts b/benchmarks/client-nav/scenarios/async-pipeline/react/vite.config.ts new file mode 100644 index 0000000000..7161c9a13d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav async-pipeline (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/shared.ts b/benchmarks/client-nav/scenarios/async-pipeline/shared.ts new file mode 100644 index 0000000000..02123309b2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/shared.ts @@ -0,0 +1,113 @@ +/** + * Shared definition of the `async-pipeline` scenario: the router's async + * load pipeline — fully async loaders (transition-held navigations), async + * `beforeLoad` context building, and parallel nested async loaders. All async + * work resolves through counted 0ms timer hops so the workload is + * deterministic. The three framework apps consume these builders so the + * workload is identical modulo the rendering layer. + * + * Note: component-level deferred consumption (`Await`/Suspense) is + * intentionally NOT covered. React throttles every Suspense reveal (with or + * without a fallback, including `pendingComponent`) by ~300ms of wall-clock + * time, which cannot be made deterministic; the async work is exercised + * through the router's own pipeline instead. + */ +import type { ScenarioStep } from '../harness' + +/** + * Resolves after exactly `hops` chained macrotask turns. Uses `setImmediate` + * rather than `setTimeout(0)`: both are counted, deterministic event-loop + * turns, but timers cost ~3-4 syscalls per hop (timerfd + epoll) and CodSpeed + * excludes syscall time from the measure — inconsistently past a threshold — + * which destabilized the hop-heavy benches. + */ +export function hopDelay(hops: number): Promise { + let promise = Promise.resolve() + for (let i = 0; i < hops; i++) { + promise = promise.then( + () => new Promise((resolve) => setImmediate(() => resolve())), + ) + } + return promise +} + +/** Number of 0ms hops every async loader/beforeLoad takes before resolving. */ +export const deferredHops = 2 + +function checksum(seed: number) { + let value = seed | 0 + for (let index = 0; index < 20; index++) { + value = (value * 1664525 + 1013904223 + index) >>> 0 + } + return value +} + +export function slowStateValue(id: string) { + return `state-${id}-${checksum(Number(id) * 71 + 3)}` +} + +export function ctxSeedValue(id: string) { + return checksum(Number(id) * 13 + 1) +} + +export function ctxStateValue(id: string) { + return `ctx-${id}-${checksum(ctxSeedValue(id) + 5)}` +} + +export function nestedLayoutValue() { + return `layout-${checksum(97)}` +} + +export function nestedStateValue(id: string) { + return `nested-${id}-${checksum(Number(id) * 41 + 11)}` +} + +function markerText(testId: string) { + return ( + document.querySelector(`[data-testid="${testId}"]`)?.textContent ?? + undefined + ) +} + +function step(testId: string, isSettled: () => boolean): ScenarioStep { + return { type: 'click', testId, isSettled } +} + +export const steps: ReadonlyArray = [ + step('go-slow-1', () => markerText('slow-state') === slowStateValue('1')), + step('go-ctx-1', () => markerText('ctx-state') === ctxStateValue('1')), + step( + 'go-nested-1', + () => + markerText('nested-layout') === nestedLayoutValue() && + markerText('nested-state') === nestedStateValue('1'), + ), + step('go-slow-2', () => markerText('slow-state') === slowStateValue('2')), + step('go-ctx-2', () => markerText('ctx-state') === ctxStateValue('2')), + step( + 'go-nested-2', + () => markerText('nested-state') === nestedStateValue('2'), + ), + step('go-home', () => markerText('home-state') === 'home'), +] + +export function assertStepResult(stepIndex: number) { + const current = steps[stepIndex]! + if (typeof current === 'string' || !('isSettled' in current)) { + return + } + if (!current.isSettled!()) { + throw new Error( + `Unexpected document state after async-pipeline step ${stepIndex}`, + ) + } +} + +// Two laps through the 7-step sequence per benchmark iteration. +export const ticksPerIteration = 14 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/project.json b/benchmarks/client-nav/scenarios/async-pipeline/solid/project.json new file mode 100644 index 0000000000..2fce426b1e --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-async-pipeline-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/async-pipeline/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/setup.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts new file mode 100644 index 0000000000..d1f620f447 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-async-pipeline', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-async-pipeline navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx new file mode 100644 index 0000000000..c9268ec31c --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/main.tsx @@ -0,0 +1,34 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPendingMs: 0, + defaultPendingMinMs: 0, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..7c15baf4e9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routeTree.gen.ts @@ -0,0 +1,140 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as NestedRouteImport } from './routes/nested' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as NestedIdRouteImport } from './routes/nested.$id' +import { Route as CtxIdRouteImport } from './routes/ctx.$id' + +const NestedRoute = NestedRouteImport.update({ + id: '/nested', + path: '/nested', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const NestedIdRoute = NestedIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => NestedRoute, +} as any) +const CtxIdRoute = CtxIdRouteImport.update({ + id: '/ctx/$id', + path: '/ctx/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + id: '__root__' | '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + NestedRoute: typeof NestedRouteWithChildren + CtxIdRoute: typeof CtxIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/nested': { + id: '/nested' + path: '/nested' + fullPath: '/nested' + preLoaderRoute: typeof NestedRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/nested/$id': { + id: '/nested/$id' + path: '/$id' + fullPath: '/nested/$id' + preLoaderRoute: typeof NestedIdRouteImport + parentRoute: typeof NestedRoute + } + '/ctx/$id': { + id: '/ctx/$id' + path: '/ctx/$id' + fullPath: '/ctx/$id' + preLoaderRoute: typeof CtxIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface NestedRouteChildren { + NestedIdRoute: typeof NestedIdRoute +} + +const NestedRouteChildren: NestedRouteChildren = { + NestedIdRoute: NestedIdRoute, +} + +const NestedRouteWithChildren = + NestedRoute._addFileChildren(NestedRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + NestedRoute: NestedRouteWithChildren, + CtxIdRoute: CtxIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..7d3647fee8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/__root.tsx @@ -0,0 +1,46 @@ +import { For } from 'solid-js' +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const ids = ['1', '2'] + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/ctx.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/ctx.$id.tsx new file mode 100644 index 0000000000..e757b57b9d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/ctx.$id.tsx @@ -0,0 +1,29 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { + ctxSeedValue, + ctxStateValue, + deferredHops, + hopDelay, +} from '../../../shared' + +export const Route = createFileRoute('/ctx/$id')({ + staleTime: 0, + gcTime: 0, + beforeLoad: async ({ params }) => { + await hopDelay(deferredHops) + return { ctxSeed: ctxSeedValue(params.id) } + }, + loader: ({ params, context }) => { + if (context.ctxSeed !== ctxSeedValue(params.id)) { + throw new Error('beforeLoad context missing in loader') + } + return { value: ctxStateValue(params.id) } + }, + component: CtxPage, +}) + +function CtxPage() { + const data = Route.useLoaderData() + + return
{data().value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/index.tsx new file mode 100644 index 0000000000..12907d440d --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.$id.tsx new file mode 100644 index 0000000000..5518ccf498 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { deferredHops, hopDelay, nestedStateValue } from '../../../shared' + +export const Route = createFileRoute('/nested/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: nestedStateValue(params.id) } + }, + component: NestedPage, +}) + +function NestedPage() { + const data = Route.useLoaderData() + + return
{data().value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.tsx new file mode 100644 index 0000000000..fdc939dc95 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/nested.tsx @@ -0,0 +1,23 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { deferredHops, hopDelay, nestedLayoutValue } from '../../../shared' + +export const Route = createFileRoute('/nested')({ + staleTime: 0, + gcTime: 0, + loader: async () => { + await hopDelay(deferredHops) + return { value: nestedLayoutValue() } + }, + component: NestedLayout, +}) + +function NestedLayout() { + const data = Route.useLoaderData() + + return ( +
+
{data().value}
+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/slow.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..4c229407e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/src/routes/slow.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { deferredHops, hopDelay, slowStateValue } from '../../../shared' + +export const Route = createFileRoute('/slow/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: slowStateValue(params.id) } + }, + component: SlowPage, +}) + +function SlowPage() { + const data = Route.useLoaderData() + + return
{data().value}
+} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/tsconfig.json b/benchmarks/client-nav/scenarios/async-pipeline/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/solid/vite.config.ts b/benchmarks/client-nav/scenarios/async-pipeline/solid/vite.config.ts new file mode 100644 index 0000000000..5107c1f056 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav async-pipeline (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/project.json b/benchmarks/client-nav/scenarios/async-pipeline/vue/project.json new file mode 100644 index 0000000000..911bb61e38 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-async-pipeline-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/async-pipeline/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/setup.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts new file mode 100644 index 0000000000..eeb568323c --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-async-pipeline', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-async-pipeline navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx new file mode 100644 index 0000000000..d8e659fb09 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/main.tsx @@ -0,0 +1,41 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPendingMs: 0, + defaultPendingMinMs: 0, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..ce26dc62d1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routeTree.gen.ts @@ -0,0 +1,140 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as NestedRouteImport } from './routes/nested' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as NestedIdRouteImport } from './routes/nested.$id' +import { Route as CtxIdRouteImport } from './routes/ctx.$id' + +const NestedRoute = NestedRouteImport.update({ + id: '/nested', + path: '/nested', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const NestedIdRoute = NestedIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => NestedRoute, +} as any) +const CtxIdRoute = CtxIdRouteImport.update({ + id: '/ctx/$id', + path: '/ctx/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/nested': typeof NestedRouteWithChildren + '/ctx/$id': typeof CtxIdRoute + '/nested/$id': typeof NestedIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + id: '__root__' | '/' | '/nested' | '/ctx/$id' | '/nested/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + NestedRoute: typeof NestedRouteWithChildren + CtxIdRoute: typeof CtxIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/nested': { + id: '/nested' + path: '/nested' + fullPath: '/nested' + preLoaderRoute: typeof NestedRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/nested/$id': { + id: '/nested/$id' + path: '/$id' + fullPath: '/nested/$id' + preLoaderRoute: typeof NestedIdRouteImport + parentRoute: typeof NestedRoute + } + '/ctx/$id': { + id: '/ctx/$id' + path: '/ctx/$id' + fullPath: '/ctx/$id' + preLoaderRoute: typeof CtxIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface NestedRouteChildren { + NestedIdRoute: typeof NestedIdRoute +} + +const NestedRouteChildren: NestedRouteChildren = { + NestedIdRoute: NestedIdRoute, +} + +const NestedRouteWithChildren = + NestedRoute._addFileChildren(NestedRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + NestedRoute: NestedRouteWithChildren, + CtxIdRoute: CtxIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..c477ab7e68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/__root.tsx @@ -0,0 +1,53 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const ids = ['1', '2'] + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/ctx.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/ctx.$id.tsx new file mode 100644 index 0000000000..d995f1ff6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/ctx.$id.tsx @@ -0,0 +1,32 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { + ctxSeedValue, + ctxStateValue, + deferredHops, + hopDelay, +} from '../../../shared' + +const CtxPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () =>
{data.value.value}
+ }, +}) + +export const Route = createFileRoute('/ctx/$id')({ + staleTime: 0, + gcTime: 0, + beforeLoad: async ({ params }) => { + await hopDelay(deferredHops) + return { ctxSeed: ctxSeedValue(params.id) } + }, + loader: ({ params, context }) => { + if (context.ctxSeed !== ctxSeedValue(params.id)) { + throw new Error('beforeLoad context missing in loader') + } + return { value: ctxStateValue(params.id) } + }, + component: CtxPage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/index.tsx new file mode 100644 index 0000000000..128e132190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/index.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+
home
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.$id.tsx new file mode 100644 index 0000000000..bbeaaa7dd7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.$id.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { deferredHops, hopDelay, nestedStateValue } from '../../../shared' + +const NestedPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () =>
{data.value.value}
+ }, +}) + +export const Route = createFileRoute('/nested/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: nestedStateValue(params.id) } + }, + component: NestedPage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.tsx new file mode 100644 index 0000000000..763657dfc6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/nested.tsx @@ -0,0 +1,26 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { deferredHops, hopDelay, nestedLayoutValue } from '../../../shared' + +const NestedLayout = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () => ( +
+
{data.value.value}
+ +
+ ) + }, +}) + +export const Route = createFileRoute('/nested')({ + staleTime: 0, + gcTime: 0, + loader: async () => { + await hopDelay(deferredHops) + return { value: nestedLayoutValue() } + }, + component: NestedLayout, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/slow.$id.tsx b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..c9afccba97 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/src/routes/slow.$id.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { deferredHops, hopDelay, slowStateValue } from '../../../shared' + +const SlowPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + + return () =>
{data.value.value}
+ }, +}) + +export const Route = createFileRoute('/slow/$id')({ + staleTime: 0, + gcTime: 0, + loader: async ({ params }) => { + await hopDelay(deferredHops) + return { value: slowStateValue(params.id) } + }, + component: SlowPage, +}) diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/tsconfig.json b/benchmarks/client-nav/scenarios/async-pipeline/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/async-pipeline/vue/vite.config.ts b/benchmarks/client-nav/scenarios/async-pipeline/vue/vite.config.ts new file mode 100644 index 0000000000..e16a8ee828 --- /dev/null +++ b/benchmarks/client-nav/scenarios/async-pipeline/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav async-pipeline (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/project.json b/benchmarks/client-nav/scenarios/control-flow/react/project.json new file mode 100644 index 0000000000..e3e580911f --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-control-flow-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/control-flow/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/setup.ts b/benchmarks/client-nav/scenarios/control-flow/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts new file mode 100644 index 0000000000..55a2224d3a --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-control-flow', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-control-flow navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/speed.flame.ts b/benchmarks/client-nav/scenarios/control-flow/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx new file mode 100644 index 0000000000..9a0c3d3b50 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/main.tsx @@ -0,0 +1,37 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + // The scenario intentionally throws loader errors every lap; keep React 19's + // default per-caught-error console reporting out of the measured loop. + const reactRoot = createRoot(container, { onCaughtError: () => {} }) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/control-flow/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..86edcc8472 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routeTree.gen.ts @@ -0,0 +1,156 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as TargetRouteImport } from './routes/target' +import { Route as Hop2RouteImport } from './routes/hop2' +import { Route as Hop1RouteImport } from './routes/hop1' +import { Route as BrokenRouteImport } from './routes/broken' +import { Route as IndexRouteImport } from './routes/index' +import { Route as MissingIdRouteImport } from './routes/missing.$id' + +const TargetRoute = TargetRouteImport.update({ + id: '/target', + path: '/target', + getParentRoute: () => rootRouteImport, +} as any) +const Hop2Route = Hop2RouteImport.update({ + id: '/hop2', + path: '/hop2', + getParentRoute: () => rootRouteImport, +} as any) +const Hop1Route = Hop1RouteImport.update({ + id: '/hop1', + path: '/hop1', + getParentRoute: () => rootRouteImport, +} as any) +const BrokenRoute = BrokenRouteImport.update({ + id: '/broken', + path: '/broken', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + id: + | '__root__' + | '/' + | '/broken' + | '/hop1' + | '/hop2' + | '/target' + | '/missing/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BrokenRoute: typeof BrokenRoute + Hop1Route: typeof Hop1Route + Hop2Route: typeof Hop2Route + TargetRoute: typeof TargetRoute + MissingIdRoute: typeof MissingIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/target': { + id: '/target' + path: '/target' + fullPath: '/target' + preLoaderRoute: typeof TargetRouteImport + parentRoute: typeof rootRouteImport + } + '/hop2': { + id: '/hop2' + path: '/hop2' + fullPath: '/hop2' + preLoaderRoute: typeof Hop2RouteImport + parentRoute: typeof rootRouteImport + } + '/hop1': { + id: '/hop1' + path: '/hop1' + fullPath: '/hop1' + preLoaderRoute: typeof Hop1RouteImport + parentRoute: typeof rootRouteImport + } + '/broken': { + id: '/broken' + path: '/broken' + fullPath: '/broken' + preLoaderRoute: typeof BrokenRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BrokenRoute: BrokenRoute, + Hop1Route: Hop1Route, + Hop2Route: Hop2Route, + TargetRoute: TargetRoute, + MissingIdRoute: MissingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/__root.tsx new file mode 100644 index 0000000000..63d5feb901 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/__root.tsx @@ -0,0 +1,52 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function LocationMarker() { + const pathname = useLocation({ select: (location) => location.pathname }) + return {pathname} +} + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/broken.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/broken.tsx new file mode 100644 index 0000000000..2dc4a26486 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/broken.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { errorMessage } from '../../../shared' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export const Route = createFileRoute('/broken')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw new Error(errorMessage) + }, + errorComponent: BrokenError, +}) + +function BrokenError(props: ErrorComponentProps) { + return
{props.error.message}
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop1.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop1.tsx new file mode 100644 index 0000000000..aacba39384 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop1.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/hop1')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/hop2' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop2.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop2.tsx new file mode 100644 index 0000000000..db31abe9f3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/hop2.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/hop2')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/target' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/index.tsx new file mode 100644 index 0000000000..24e856ea15 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/missing.$id.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..0c807bb2bc --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/missing.$id.tsx @@ -0,0 +1,23 @@ +import { createFileRoute, notFound } from '@tanstack/react-router' + +export const Route = createFileRoute('/missing/$id')({ + staleTime: 0, + gcTime: 0, + loader: ({ params }) => { + if (params.id === 'gone') { + throw notFound() + } + return { id: params.id } + }, + component: MissingPage, + notFoundComponent: MissingNotFound, +}) + +function MissingPage() { + const data = Route.useLoaderData() + return
{data.id}
+} + +function MissingNotFound() { + return
missing not found
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/src/routes/target.tsx b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/target.tsx new file mode 100644 index 0000000000..280f65835e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/src/routes/target.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/target')({ + component: TargetPage, +}) + +function TargetPage() { + return
target
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/tsconfig.json b/benchmarks/client-nav/scenarios/control-flow/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/control-flow/react/vite.config.ts b/benchmarks/client-nav/scenarios/control-flow/react/vite.config.ts new file mode 100644 index 0000000000..1aa2c1fa35 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav control-flow (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/shared.ts b/benchmarks/client-nav/scenarios/control-flow/shared.ts new file mode 100644 index 0000000000..c4f368bee1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/shared.ts @@ -0,0 +1,91 @@ +/** + * Shared definition of the `control-flow` scenario: loader-thrown redirects + * (including a 2-hop chain), notFound() rendering, loader errors through the + * route error boundary, and recovery navigation resetting the boundary. + */ +import type { ScenarioStep } from '../harness' + +export const errorMessage = 'bench-error' + +export const steps: ReadonlyArray = [ + { + type: 'click', + testId: 'go-hop1', + // The click lands on /hop1 whose loader chains redirects to /target; wait + // until the post-redirect location is committed before finishing the step. + isSettled: () => + document.querySelector('[data-testid="loc"]')?.textContent === '/target', + }, + 'go-missing-ok', + 'go-missing-gone', + 'go-broken', + 'go-target', + 'go-missing-gone', + 'home', +] + +interface StepExpectation { + loc: string + present: ReadonlyArray + absent?: ReadonlyArray + errorText?: string +} + +const expectations: ReadonlyArray = [ + { loc: '/target', present: ['target-state'] }, + { loc: '/missing/exists', present: ['missing-state'] }, + { + loc: '/missing/gone', + present: ['not-found-state'], + absent: ['missing-state'], + }, + { loc: '/broken', present: ['error-state'], errorText: errorMessage }, + { loc: '/target', present: ['target-state'], absent: ['error-state'] }, + { loc: '/missing/gone', present: ['not-found-state'] }, + { loc: '/', present: ['home-state'] }, +] + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expectation = expectations[stepIndex % expectations.length]! + + const loc = container.querySelector('[data-testid="loc"]') + if (loc?.textContent !== expectation.loc) { + throw new Error( + `Expected location "${expectation.loc}" after step ${stepIndex}, received "${loc?.textContent ?? ''}"`, + ) + } + + for (const testId of expectation.present) { + const element = container.querySelector(`[data-testid="${testId}"]`) + if (!element) { + throw new Error( + `Expected [data-testid="${testId}"] after step ${stepIndex}`, + ) + } + if ( + expectation.errorText && + !element.textContent?.includes(expectation.errorText) + ) { + throw new Error( + `Expected [data-testid="${testId}"] to include "${expectation.errorText}", received "${element.textContent}"`, + ) + } + } + + for (const testId of expectation.absent ?? []) { + if (container.querySelector(`[data-testid="${testId}"]`)) { + throw new Error( + `Expected [data-testid="${testId}"] to be absent after step ${stepIndex}`, + ) + } + } +} + +// Two laps through the 7-step sequence per benchmark iteration. +export const ticksPerIteration = 14 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/project.json b/benchmarks/client-nav/scenarios/control-flow/solid/project.json new file mode 100644 index 0000000000..2b5154d385 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-control-flow-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/control-flow/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/setup.ts b/benchmarks/client-nav/scenarios/control-flow/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts new file mode 100644 index 0000000000..abbb85a575 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-control-flow', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-control-flow navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/control-flow/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx new file mode 100644 index 0000000000..b43608e191 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/control-flow/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..44d88e9f8d --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routeTree.gen.ts @@ -0,0 +1,156 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as TargetRouteImport } from './routes/target' +import { Route as Hop2RouteImport } from './routes/hop2' +import { Route as Hop1RouteImport } from './routes/hop1' +import { Route as BrokenRouteImport } from './routes/broken' +import { Route as IndexRouteImport } from './routes/index' +import { Route as MissingIdRouteImport } from './routes/missing.$id' + +const TargetRoute = TargetRouteImport.update({ + id: '/target', + path: '/target', + getParentRoute: () => rootRouteImport, +} as any) +const Hop2Route = Hop2RouteImport.update({ + id: '/hop2', + path: '/hop2', + getParentRoute: () => rootRouteImport, +} as any) +const Hop1Route = Hop1RouteImport.update({ + id: '/hop1', + path: '/hop1', + getParentRoute: () => rootRouteImport, +} as any) +const BrokenRoute = BrokenRouteImport.update({ + id: '/broken', + path: '/broken', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + id: + | '__root__' + | '/' + | '/broken' + | '/hop1' + | '/hop2' + | '/target' + | '/missing/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BrokenRoute: typeof BrokenRoute + Hop1Route: typeof Hop1Route + Hop2Route: typeof Hop2Route + TargetRoute: typeof TargetRoute + MissingIdRoute: typeof MissingIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/target': { + id: '/target' + path: '/target' + fullPath: '/target' + preLoaderRoute: typeof TargetRouteImport + parentRoute: typeof rootRouteImport + } + '/hop2': { + id: '/hop2' + path: '/hop2' + fullPath: '/hop2' + preLoaderRoute: typeof Hop2RouteImport + parentRoute: typeof rootRouteImport + } + '/hop1': { + id: '/hop1' + path: '/hop1' + fullPath: '/hop1' + preLoaderRoute: typeof Hop1RouteImport + parentRoute: typeof rootRouteImport + } + '/broken': { + id: '/broken' + path: '/broken' + fullPath: '/broken' + preLoaderRoute: typeof BrokenRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BrokenRoute: BrokenRoute, + Hop1Route: Hop1Route, + Hop2Route: Hop2Route, + TargetRoute: TargetRoute, + MissingIdRoute: MissingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..1bce9e8a3e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/__root.tsx @@ -0,0 +1,52 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function LocationMarker() { + const pathname = useLocation({ select: (location) => location.pathname }) + return {pathname()} +} + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/broken.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/broken.tsx new file mode 100644 index 0000000000..9931ef9c58 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/broken.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { errorMessage } from '../../../shared' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export const Route = createFileRoute('/broken')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw new Error(errorMessage) + }, + errorComponent: BrokenError, +}) + +function BrokenError(props: ErrorComponentProps) { + return
{props.error.message}
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop1.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop1.tsx new file mode 100644 index 0000000000..1aedb481ec --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop1.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hop1')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/hop2' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop2.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop2.tsx new file mode 100644 index 0000000000..91b601c516 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/hop2.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hop2')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/target' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/index.tsx new file mode 100644 index 0000000000..cc46764de5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/missing.$id.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..9c0493fb48 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/missing.$id.tsx @@ -0,0 +1,23 @@ +import { createFileRoute, notFound } from '@tanstack/solid-router' + +export const Route = createFileRoute('/missing/$id')({ + staleTime: 0, + gcTime: 0, + loader: ({ params }) => { + if (params.id === 'gone') { + throw notFound() + } + return { id: params.id } + }, + component: MissingPage, + notFoundComponent: MissingNotFound, +}) + +function MissingPage() { + const data = Route.useLoaderData() + return
{data().id}
+} + +function MissingNotFound() { + return
missing not found
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/target.tsx b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/target.tsx new file mode 100644 index 0000000000..87de4e59a7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/src/routes/target.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/target')({ + component: TargetPage, +}) + +function TargetPage() { + return
target
+} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/tsconfig.json b/benchmarks/client-nav/scenarios/control-flow/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/control-flow/solid/vite.config.ts b/benchmarks/client-nav/scenarios/control-flow/solid/vite.config.ts new file mode 100644 index 0000000000..1c6244d3d1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav control-flow (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/project.json b/benchmarks/client-nav/scenarios/control-flow/vue/project.json new file mode 100644 index 0000000000..7370687988 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-control-flow-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/control-flow/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/setup.ts b/benchmarks/client-nav/scenarios/control-flow/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts new file mode 100644 index 0000000000..551b88e68f --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-control-flow', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-control-flow navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/control-flow/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/control-flow/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..60fc846f89 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routeTree.gen.ts @@ -0,0 +1,156 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as TargetRouteImport } from './routes/target' +import { Route as Hop2RouteImport } from './routes/hop2' +import { Route as Hop1RouteImport } from './routes/hop1' +import { Route as BrokenRouteImport } from './routes/broken' +import { Route as IndexRouteImport } from './routes/index' +import { Route as MissingIdRouteImport } from './routes/missing.$id' + +const TargetRoute = TargetRouteImport.update({ + id: '/target', + path: '/target', + getParentRoute: () => rootRouteImport, +} as any) +const Hop2Route = Hop2RouteImport.update({ + id: '/hop2', + path: '/hop2', + getParentRoute: () => rootRouteImport, +} as any) +const Hop1Route = Hop1RouteImport.update({ + id: '/hop1', + path: '/hop1', + getParentRoute: () => rootRouteImport, +} as any) +const BrokenRoute = BrokenRouteImport.update({ + id: '/broken', + path: '/broken', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/broken': typeof BrokenRoute + '/hop1': typeof Hop1Route + '/hop2': typeof Hop2Route + '/target': typeof TargetRoute + '/missing/$id': typeof MissingIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/broken' | '/hop1' | '/hop2' | '/target' | '/missing/$id' + id: + | '__root__' + | '/' + | '/broken' + | '/hop1' + | '/hop2' + | '/target' + | '/missing/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BrokenRoute: typeof BrokenRoute + Hop1Route: typeof Hop1Route + Hop2Route: typeof Hop2Route + TargetRoute: typeof TargetRoute + MissingIdRoute: typeof MissingIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/target': { + id: '/target' + path: '/target' + fullPath: '/target' + preLoaderRoute: typeof TargetRouteImport + parentRoute: typeof rootRouteImport + } + '/hop2': { + id: '/hop2' + path: '/hop2' + fullPath: '/hop2' + preLoaderRoute: typeof Hop2RouteImport + parentRoute: typeof rootRouteImport + } + '/hop1': { + id: '/hop1' + path: '/hop1' + fullPath: '/hop1' + preLoaderRoute: typeof Hop1RouteImport + parentRoute: typeof rootRouteImport + } + '/broken': { + id: '/broken' + path: '/broken' + fullPath: '/broken' + preLoaderRoute: typeof BrokenRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BrokenRoute: BrokenRoute, + Hop1Route: Hop1Route, + Hop2Route: Hop2Route, + TargetRoute: TargetRoute, + MissingIdRoute: MissingIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..9695c4b32d --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/__root.tsx @@ -0,0 +1,57 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/vue-router' + +const LocationMarker = Vue.defineComponent({ + setup() { + const pathname = useLocation({ select: (location) => location.pathname }) + return () => {pathname.value} + }, +}) + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/broken.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/broken.tsx new file mode 100644 index 0000000000..c67fa2817e --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/broken.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { errorMessage } from '../../../shared' + +const BrokenError = Vue.defineComponent({ + props: { + error: { type: Error, required: true }, + }, + setup(props) { + return () =>
{props.error.message}
+ }, +}) + +export const Route = createFileRoute('/broken')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw new Error(errorMessage) + }, + errorComponent: BrokenError, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop1.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop1.tsx new file mode 100644 index 0000000000..ed00cefe3f --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop1.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/vue-router' + +export const Route = createFileRoute('/hop1')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/hop2' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop2.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop2.tsx new file mode 100644 index 0000000000..f6d4b1c1e0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/hop2.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, redirect } from '@tanstack/vue-router' + +export const Route = createFileRoute('/hop2')({ + staleTime: 0, + gcTime: 0, + loader: () => { + throw redirect({ to: '/target' }) + }, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f3ffc58610 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () =>
home
+ }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/missing.$id.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..022ea656a6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/missing.$id.tsx @@ -0,0 +1,28 @@ +import * as Vue from 'vue' +import { createFileRoute, notFound } from '@tanstack/vue-router' + +const MissingPage = Vue.defineComponent({ + setup() { + const data = Route.useLoaderData() + return () =>
{data.value.id}
+ }, +}) + +const MissingNotFound = Vue.defineComponent({ + setup() { + return () =>
missing not found
+ }, +}) + +export const Route = createFileRoute('/missing/$id')({ + staleTime: 0, + gcTime: 0, + loader: ({ params }) => { + if (params.id === 'gone') { + throw notFound() + } + return { id: params.id } + }, + component: MissingPage, + notFoundComponent: MissingNotFound, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/target.tsx b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/target.tsx new file mode 100644 index 0000000000..4e22d0993a --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/src/routes/target.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const TargetPage = Vue.defineComponent({ + setup() { + return () =>
target
+ }, +}) + +export const Route = createFileRoute('/target')({ + component: TargetPage, +}) diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/tsconfig.json b/benchmarks/client-nav/scenarios/control-flow/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/control-flow/vue/vite.config.ts b/benchmarks/client-nav/scenarios/control-flow/vue/vite.config.ts new file mode 100644 index 0000000000..4694cf69c8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/control-flow/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav control-flow (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/harness.ts b/benchmarks/client-nav/scenarios/harness.ts new file mode 100644 index 0000000000..7b993d0ed8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/harness.ts @@ -0,0 +1,371 @@ +import { getRequiredLink, waitForRequiredLink } from '../setup-helpers' + +export interface ScenarioRouter { + load: () => Promise + subscribe: (eventType: 'onRendered', callback: () => void) => () => void + navigate: (options: any) => Promise + preloadRoute: (options: any) => Promise + invalidate: () => Promise + history: { + back: () => void + forward: () => void + go: (index: number) => void + } +} + +export interface MountedScenarioApp { + router: ScenarioRouter + unmount: () => void +} + +/** + * A single benchmark step. Plain strings are link-click steps (the common + * case). Steps that change the router location (`click`, `navigate`, `back`, + * `forward`, `go`) are awaited through the router's `onRendered` event; steps + * that do background work (`hover`, `preload`, `invalidate`) are awaited + * through a fixed number of 0ms timer hops so the loop stays deterministic. + */ +export type ScenarioStep = + | string + | { + type: 'click' + testId: string + /** + * Optional post-render settle predicate for steps that trigger work + * finishing after `onRendered` (e.g. deferred data resolving through a + * Suspense boundary). The step keeps taking 0ms timer hops until the + * predicate returns true (bounded, throws when exceeded). + */ + isSettled?: () => boolean + } + | { + /** + * Dispatches a single `mouseover` on the link, which triggers + * `preload: 'intent'` exactly once in every adapter: React synthesizes + * `mouseEnter` from `mouseover`, and Solid/Vue attach a native + * `mouseover` preload handler. (Solid/Vue also listen to `mouseenter` — + * dispatching both events would run the preload pipeline twice for them + * but only once for React.) Then settles via counted 0ms hops. + */ + type: 'hover' + testId: string + settleHops?: number + } + | { type: 'navigate'; getOptions: () => object } + | { type: 'preload'; getOptions: () => object; settleHops?: number } + | { type: 'invalidate'; settleHops?: number } + | { type: 'back' } + | { type: 'forward' } + | { type: 'go'; delta: number } + +/** + * One macrotask turn via `setImmediate` rather than `setTimeout(0)`: both + * yield deterministically to the event loop (timers, MessageChannel, React's + * Node scheduler), but a timer costs ~3-4 syscalls (timerfd + epoll) per hop + * while an immediate costs ~1. CodSpeed excludes syscall time from the + * measure — inconsistently past a threshold, which flagged the hop-heavy + * benches as "skipped" — so the loop must stay syscall-lean. + */ +const timerHop = () => + new Promise((resolve) => setImmediate(() => resolve())) + +async function settle(hops: number) { + for (let i = 0; i < hops; i++) { + await timerHop() + } +} + +const MAX_SETTLE_HOPS = 100 + +async function settleUntil(isSettled: () => boolean, label: string) { + for (let i = 0; i < MAX_SETTLE_HOPS; i++) { + if (isSettled()) { + return + } + await timerHop() + } + throw new Error( + `Step "${label}" did not settle within ${MAX_SETTLE_HOPS} timer hops`, + ) +} + +/** + * Reroute zero-delay `window.setTimeout` calls onto `setImmediate` for the + * duration of the benchmark. jsdom delivers history traversals through two + * nested `window.setTimeout(0)` tasks; Node clamps those to 1ms of wall + * time, so with nothing else pending the event loop blocks in `epoll_wait` + * until each timer expires — and that blocked wall-time is recorded as + * highly variable syscall time (5-26ms across runs), enough for CodSpeed to + * skip-warn the history benches. Zero-delay timeouts carry no ordering + * semantics a check-phase immediate doesn't satisfy, and the suite's + * conventions already forbid real-delay timers in measured code; non-zero + * delays are passed through untouched. + */ +function patchZeroDelayTimeouts() { + const originalSetTimeout = window.setTimeout.bind(window) + const originalClearTimeout = window.clearTimeout.bind(window) + const immediates = new Map() + // Negative ids cannot collide with real jsdom timer ids. + let nextImmediateId = -2 + + window.setTimeout = (( + handler: (...handlerArgs: Array) => void, + delay?: number, + ...args: Array + ) => { + if (!delay) { + const id = nextImmediateId-- + immediates.set( + id, + setImmediate(() => { + immediates.delete(id) + handler(...args) + }), + ) + return id + } + return originalSetTimeout(handler, delay, ...args) + }) as typeof window.setTimeout + + window.clearTimeout = ((id: number) => { + const immediate = immediates.get(id) + if (immediate) { + clearImmediate(immediate) + immediates.delete(id) + return + } + originalClearTimeout(id) + }) as typeof window.clearTimeout + + return () => { + window.setTimeout = originalSetTimeout as typeof window.setTimeout + window.clearTimeout = originalClearTimeout as typeof window.clearTimeout + } +} + +export interface ScenarioSetupOptions { + frameworkLabel: string + mount: (container: HTMLElement) => MountedScenarioApp + /** + * Circular sequence of steps advanced by `tick()`. Link steps must target + * links rendered from the start (e.g. in the root layout), no two + * consecutive location-changing steps may target the same location (a + * same-location click never triggers `onRendered`), and the sequence must + * leave the app back on the initial route so the warm-up lap ends in the + * starting state. + */ + steps: ReadonlyArray + /** Sanity check run once per step during the warm-up lap in `before()`. */ + assertAfterStep?: (stepIndex: number, container: HTMLElement) => void + /** + * URL the window is reset to before mounting (default '/'). Scenarios whose + * router uses a `basepath` or an input rewrite must start on an external + * URL that maps to their initial route. + */ + initialUrl?: string +} + +function warnAboutDevMode(frameworkLabel: string) { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `client-nav scenario benchmark is running without NODE_ENV=production; ${frameworkLabel} dev overhead will dominate results.`, + ) + } +} + +/** + * Shared scenario runner: mounts the prebuilt app, then advances a circular + * sequence of steps, synchronizing each step so its work is fully part of the + * measured tick and steps cannot overlap. + */ +export function createScenarioSetup(options: ScenarioSetupOptions) { + warnAboutDevMode(options.frameworkLabel) + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let restoreTimeouts = () => {} + let stepIndex = 0 + let next: () => Promise = () => + Promise.reject(new Error('Benchmark not initialized')) + + async function before() { + stepIndex = 0 + restoreTimeouts = patchZeroDelayTimeouts() + window.history.replaceState(null, '', options.initialUrl ?? '/') + container = document.createElement('div') + document.body.append(container) + + const { router, unmount: dispose } = options.mount(container) + unmount = dispose + + let resolveRendered: () => void = () => {} + unsub = router.subscribe('onRendered', () => { + resolveRendered() + }) + + const cachedLinks = new Map() + const rendered = () => + new Promise((resolveNext) => { + resolveRendered = resolveNext + }) + + const runStep = async (step: ScenarioStep) => { + if (typeof step === 'string') { + step = { type: 'click', testId: step } + } + + switch (step.type) { + case 'click': { + const renderedPromise = rendered() + getRequiredLink(container!, step.testId, cachedLinks).dispatchEvent( + new MouseEvent('click', { + bubbles: true, + cancelable: true, + button: 0, + }), + ) + await renderedPromise + if (step.isSettled) { + await settleUntil(step.isSettled, `click ${step.testId}`) + } + return + } + case 'hover': { + const link = getRequiredLink(container!, step.testId, cachedLinks) + link.dispatchEvent( + new MouseEvent('mouseover', { bubbles: true, cancelable: true }), + ) + await settle(step.settleHops ?? 4) + return + } + case 'navigate': { + const renderedPromise = rendered() + router.navigate(step.getOptions()) + await renderedPromise + return + } + case 'preload': { + await router.preloadRoute(step.getOptions()) + await settle(step.settleHops ?? 2) + return + } + case 'invalidate': { + await router.invalidate() + await settle(step.settleHops ?? 2) + return + } + case 'back': { + const renderedPromise = rendered() + router.history.back() + await renderedPromise + return + } + case 'forward': { + const renderedPromise = rendered() + router.history.forward() + await renderedPromise + return + } + case 'go': { + const renderedPromise = rendered() + router.history.go(step.delta) + await renderedPromise + return + } + } + } + + await router.load() + + const linkTestIds = new Set() + for (const step of options.steps) { + if (typeof step === 'string') { + linkTestIds.add(step) + } else if (step.type === 'click' || step.type === 'hover') { + linkTestIds.add(step.testId) + } + } + for (const testId of linkTestIds) { + await waitForRequiredLink(container, testId, cachedLinks) + } + + // One warm-up lap that also sanity-checks each step's observable output, + // ending back on the initial route so measurement starts from a known state. + for (const [index, step] of options.steps.entries()) { + await runStep(step) + options.assertAfterStep?.(index, container) + } + + next = () => { + const step = options.steps[stepIndex % options.steps.length]! + stepIndex += 1 + return runStep(step) + } + } + + function after() { + unmount?.() + container?.remove() + unsub() + restoreTimeouts() + restoreTimeouts = () => {} + unmount = undefined + container = undefined + } + + function tick() { + return next() + } + + return { + before, + tick, + after, + } +} + +export interface MountLoopSetupOptions { + frameworkLabel: string + mount: (container: HTMLElement) => MountedScenarioApp + /** Test id that must appear in the container before a mount counts as done. */ + readyTestId: string + assertReady?: (container: HTMLElement) => void +} + +/** + * Mount-loop runner for cold-start scenarios: every tick creates a fresh + * container, mounts the app (router creation included), waits for the initial + * render to commit, then unmounts and cleans up. + */ +export function createMountLoopSetup(options: MountLoopSetupOptions) { + warnAboutDevMode(options.frameworkLabel) + + async function tick() { + window.history.replaceState(null, '', '/') + const container = document.createElement('div') + document.body.append(container) + + const { router, unmount } = options.mount(container) + + try { + await router.load() + await settleUntil( + () => + container.querySelector(`[data-testid="${options.readyTestId}"]`) !== + null, + `mount ${options.readyTestId}`, + ) + options.assertReady?.(container) + } finally { + unmount() + container.remove() + } + } + + return { + before: async () => {}, + tick, + after: () => {}, + } +} diff --git a/benchmarks/client-nav/scenarios/head/react/project.json b/benchmarks/client-nav/scenarios/head/react/project.json new file mode 100644 index 0000000000..1becf30fb9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-head-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/head/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/head/react/setup.ts b/benchmarks/client-nav/scenarios/head/react/setup.ts new file mode 100644 index 0000000000..1a28b0ca94 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, stepTestIds } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps: stepTestIds, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/head/react/speed.bench.ts b/benchmarks/client-nav/scenarios/head/react/speed.bench.ts new file mode 100644 index 0000000000..c71b426c56 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-head', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-head navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/head/react/speed.flame.ts b/benchmarks/client-nav/scenarios/head/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/main.tsx b/benchmarks/client-nav/scenarios/head/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/head/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..5c80a3104e --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routeTree.gen.ts @@ -0,0 +1,145 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DocsSectionRouteImport } from './routes/docs.$section' +import { Route as ArticlesIdRouteImport } from './routes/articles.$id' + +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DocsSectionRoute = DocsSectionRouteImport.update({ + id: '/$section', + path: '/$section', + getParentRoute: () => DocsRoute, +} as any) +const ArticlesIdRoute = ArticlesIdRouteImport.update({ + id: '/articles/$id', + path: '/articles/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + id: + | '__root__' + | '/' + | '/docs' + | '/settings' + | '/articles/$id' + | '/docs/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRouteWithChildren + SettingsRoute: typeof SettingsRoute + ArticlesIdRoute: typeof ArticlesIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/docs/$section': { + id: '/docs/$section' + path: '/$section' + fullPath: '/docs/$section' + preLoaderRoute: typeof DocsSectionRouteImport + parentRoute: typeof DocsRoute + } + '/articles/$id': { + id: '/articles/$id' + path: '/articles/$id' + fullPath: '/articles/$id' + preLoaderRoute: typeof ArticlesIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface DocsRouteChildren { + DocsSectionRoute: typeof DocsSectionRoute +} + +const DocsRouteChildren: DocsRouteChildren = { + DocsSectionRoute: DocsSectionRoute, +} + +const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRouteWithChildren, + SettingsRoute: SettingsRoute, + ArticlesIdRoute: ArticlesIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/__root.tsx new file mode 100644 index 0000000000..3d61dad0e9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/__root.tsx @@ -0,0 +1,59 @@ +import { + HeadContent, + Link, + Outlet, + createRootRoute, +} from '@tanstack/react-router' +import { articleIds, docsSections, rootHead } from '../../../shared' + +export const Route = createRootRoute({ + head: rootHead, + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/articles.$id.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/articles.$id.tsx new file mode 100644 index 0000000000..6f5b78d60e --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/articles.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { articleDescription, articleHead, articleTitle } from '../../../shared' + +export const Route = createFileRoute('/articles/$id')({ + head: ({ params }) => articleHead(params.id), + component: ArticlePage, +}) + +function ArticlePage() { + const params = Route.useParams() + + return ( +
+

{articleTitle(params.id)}

+

{articleDescription(params.id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/docs.$section.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.$section.tsx new file mode 100644 index 0000000000..6ed1935ada --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.$section.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { docsDescription, docsSectionHead, docsTitle } from '../../../shared' + +export const Route = createFileRoute('/docs/$section')({ + head: ({ params }) => docsSectionHead(params.section), + component: DocsSectionPage, +}) + +function DocsSectionPage() { + const params = Route.useParams() + + return ( +
+

{docsTitle(params.section)}

+

{docsDescription(params.section)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.tsx new file mode 100644 index 0000000000..a2da216ddb --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/docs.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { docsLayoutHead } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + head: docsLayoutHead, + component: DocsLayout, +}) + +function DocsLayout() { + return ( +
+

Docs

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/index.tsx new file mode 100644 index 0000000000..6ac318452f --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/index.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeHead, homeTitle } from '../../../shared' + +export const Route = createFileRoute('/')({ + head: homeHead, + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeTitle}

+

Landing page

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/head/react/src/routes/settings.tsx new file mode 100644 index 0000000000..cd73902bff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/src/routes/settings.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { settingsHead, settingsTitle } from '../../../shared' + +export const Route = createFileRoute('/settings')({ + head: settingsHead, + component: SettingsPage, +}) + +function SettingsPage() { + return ( +
+

{settingsTitle}

+

Account settings

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/react/tsconfig.json b/benchmarks/client-nav/scenarios/head/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/head/react/vite.config.ts b/benchmarks/client-nav/scenarios/head/react/vite.config.ts new file mode 100644 index 0000000000..086bd60bb4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav head (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/head/shared.ts b/benchmarks/client-nav/scenarios/head/shared.ts new file mode 100644 index 0000000000..db185d602b --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/shared.ts @@ -0,0 +1,152 @@ +/** + * Shared definition of the `head` scenario: route head() payloads, the click + * sequence, and per-step sanity assertions. The three framework apps consume + * these builders so the workload is identical modulo the rendering layer. + */ + +const siteName = 'Head Bench' + +export const docsSections = ['api', 'guide', 'cli'] as const +export const articleIds = ['1', '2', '3'] as const + +export const homeTitle = `Home | ${siteName}` +export const settingsTitle = `Settings | ${siteName}` + +export function docsTitle(section: string) { + return `Docs ${section} | ${siteName}` +} + +export function docsDescription(section: string) { + return `Documentation for the ${section} section of the head benchmark app.` +} + +export function articleTitle(id: string) { + return `Article ${id} | ${siteName}` +} + +export function articleDescription(id: string) { + return `Long-form article number ${id}, with a description sized like a realistic summary of the page content.` +} + +export function rootHead() { + return { + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'application-name', content: siteName }, + { name: 'description', content: 'Client head-management benchmark' }, + { name: 'theme-color', content: '#111827' }, + { property: 'og:type', content: 'website' }, + { property: 'og:site_name', content: siteName }, + ], + links: [ + { rel: 'icon', href: '/favicon.ico' }, + { rel: 'preconnect', href: 'https://assets.example.com' }, + ], + } +} + +export function homeHead() { + return { + meta: [ + { title: homeTitle }, + { name: 'description', content: 'Landing page of the head benchmark' }, + { property: 'og:title', content: homeTitle }, + ], + links: [{ rel: 'canonical', href: 'https://example.com/' }], + } +} + +export function docsLayoutHead() { + return { + meta: [ + // Overridden by the child section route on every docs navigation, so the + // dedupe path is exercised for both `name` and `property` entries. + { name: 'description', content: 'Documentation hub' }, + { property: 'og:title', content: `Docs | ${siteName}` }, + { name: 'docsearch:index', content: 'head-bench-docs' }, + ], + links: [{ rel: 'search', href: '/docs/opensearch.xml' }], + } +} + +export function docsSectionHead(section: string) { + return { + meta: [ + { title: docsTitle(section) }, + { name: 'description', content: docsDescription(section) }, + { name: 'keywords', content: `docs,${section},router,benchmark` }, + { property: 'og:title', content: docsTitle(section) }, + { property: 'og:description', content: docsDescription(section) }, + { property: 'og:url', content: `https://example.com/docs/${section}` }, + { name: 'twitter:card', content: 'summary' }, + { name: 'twitter:title', content: docsTitle(section) }, + ], + links: [ + { rel: 'canonical', href: `https://example.com/docs/${section}` }, + { rel: 'prev', href: `https://example.com/docs/${section}/intro` }, + ], + } +} + +export function articleHead(id: string) { + return { + meta: [ + { title: articleTitle(id) }, + { name: 'description', content: articleDescription(id) }, + { name: 'author', content: 'Bench Author' }, + { property: 'og:title', content: articleTitle(id) }, + { property: 'og:description', content: articleDescription(id) }, + { property: 'og:url', content: `https://example.com/articles/${id}` }, + { property: 'og:image', content: `https://example.com/covers/${id}.png` }, + { property: 'article:published_time', content: '2026-01-01T00:00:00Z' }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'twitter:title', content: articleTitle(id) }, + ], + links: [{ rel: 'canonical', href: `https://example.com/articles/${id}` }], + } +} + +export function settingsHead() { + return { + meta: [ + { title: settingsTitle }, + // Same names as root entries, so the child-wins dedupe path runs. + { name: 'description', content: 'Account settings' }, + { name: 'theme-color', content: '#f9fafb' }, + { name: 'robots', content: 'noindex' }, + ], + links: [], + } +} + +export const steps = [ + { testId: 'go-docs-api', title: docsTitle('api') }, + { testId: 'go-article-1', title: articleTitle('1') }, + { testId: 'go-docs-guide', title: docsTitle('guide') }, + { testId: 'go-settings', title: settingsTitle }, + { testId: 'go-article-2', title: articleTitle('2') }, + { testId: 'go-docs-cli', title: docsTitle('cli') }, + { testId: 'go-article-3', title: articleTitle('3') }, + { testId: 'go-home', title: homeTitle }, +] as const + +export const stepTestIds = steps.map((step) => step.testId) + +export function assertStepResult(stepIndex: number) { + const expected = steps[stepIndex]!.title + if (document.title !== expected) { + throw new Error( + `Expected document.title to be "${expected}" after step ${stepIndex}, received "${document.title}"`, + ) + } +} + +// Two laps through the 8-step sequence per benchmark iteration. +export const ticksPerIteration = 16 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/head/solid/project.json b/benchmarks/client-nav/scenarios/head/solid/project.json new file mode 100644 index 0000000000..2575a19b89 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-head-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/head/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/head/solid/setup.ts b/benchmarks/client-nav/scenarios/head/solid/setup.ts new file mode 100644 index 0000000000..4ee924acff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, stepTestIds } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps: stepTestIds, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts new file mode 100644 index 0000000000..ea0a5ca818 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-head', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-head navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/head/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/head/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/main.tsx b/benchmarks/client-nav/scenarios/head/solid/src/main.tsx new file mode 100644 index 0000000000..b43608e191 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/head/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..ead6a4436a --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routeTree.gen.ts @@ -0,0 +1,145 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DocsSectionRouteImport } from './routes/docs.$section' +import { Route as ArticlesIdRouteImport } from './routes/articles.$id' + +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DocsSectionRoute = DocsSectionRouteImport.update({ + id: '/$section', + path: '/$section', + getParentRoute: () => DocsRoute, +} as any) +const ArticlesIdRoute = ArticlesIdRouteImport.update({ + id: '/articles/$id', + path: '/articles/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + id: + | '__root__' + | '/' + | '/docs' + | '/settings' + | '/articles/$id' + | '/docs/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRouteWithChildren + SettingsRoute: typeof SettingsRoute + ArticlesIdRoute: typeof ArticlesIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/docs/$section': { + id: '/docs/$section' + path: '/$section' + fullPath: '/docs/$section' + preLoaderRoute: typeof DocsSectionRouteImport + parentRoute: typeof DocsRoute + } + '/articles/$id': { + id: '/articles/$id' + path: '/articles/$id' + fullPath: '/articles/$id' + preLoaderRoute: typeof ArticlesIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface DocsRouteChildren { + DocsSectionRoute: typeof DocsSectionRoute +} + +const DocsRouteChildren: DocsRouteChildren = { + DocsSectionRoute: DocsSectionRoute, +} + +const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRouteWithChildren, + SettingsRoute: SettingsRoute, + ArticlesIdRoute: ArticlesIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..52ca7da373 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/__root.tsx @@ -0,0 +1,58 @@ +import { For } from 'solid-js' +import { + HeadContent, + Link, + Outlet, + createRootRoute, +} from '@tanstack/solid-router' +import { articleIds, docsSections, rootHead } from '../../../shared' + +export const Route = createRootRoute({ + head: rootHead, + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/articles.$id.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/articles.$id.tsx new file mode 100644 index 0000000000..dab11ada18 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/articles.$id.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { articleDescription, articleHead, articleTitle } from '../../../shared' + +export const Route = createFileRoute('/articles/$id')({ + head: ({ params }) => articleHead(params.id), + component: ArticlePage, +}) + +function ArticlePage() { + const params = Route.useParams() + + return ( +
+

{articleTitle(params().id)}

+

{articleDescription(params().id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.$section.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.$section.tsx new file mode 100644 index 0000000000..7ab1123e7a --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.$section.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { docsDescription, docsSectionHead, docsTitle } from '../../../shared' + +export const Route = createFileRoute('/docs/$section')({ + head: ({ params }) => docsSectionHead(params.section), + component: DocsSectionPage, +}) + +function DocsSectionPage() { + const params = Route.useParams() + + return ( +
+

{docsTitle(params().section)}

+

{docsDescription(params().section)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.tsx new file mode 100644 index 0000000000..7f84b214d0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/docs.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { docsLayoutHead } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + head: docsLayoutHead, + component: DocsLayout, +}) + +function DocsLayout() { + return ( +
+

Docs

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/index.tsx new file mode 100644 index 0000000000..229eecc8c1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/index.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { homeHead, homeTitle } from '../../../shared' + +export const Route = createFileRoute('/')({ + head: homeHead, + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeTitle}

+

Landing page

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/head/solid/src/routes/settings.tsx new file mode 100644 index 0000000000..88424caef3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/src/routes/settings.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { settingsHead, settingsTitle } from '../../../shared' + +export const Route = createFileRoute('/settings')({ + head: settingsHead, + component: SettingsPage, +}) + +function SettingsPage() { + return ( +
+

{settingsTitle}

+

Account settings

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/head/solid/tsconfig.json b/benchmarks/client-nav/scenarios/head/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/head/solid/vite.config.ts b/benchmarks/client-nav/scenarios/head/solid/vite.config.ts new file mode 100644 index 0000000000..7b5a1e22b4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav head (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/project.json b/benchmarks/client-nav/scenarios/head/vue/project.json new file mode 100644 index 0000000000..5ad7909d74 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-head-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/head/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/head/vue/setup.ts b/benchmarks/client-nav/scenarios/head/vue/setup.ts new file mode 100644 index 0000000000..3dd6bc95a5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, stepTestIds } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps: stepTestIds, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts new file mode 100644 index 0000000000..3630032e44 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-head', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-head navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/head/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/head/vue/src/main.tsx b/benchmarks/client-nav/scenarios/head/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/head/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..b115854c26 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routeTree.gen.ts @@ -0,0 +1,145 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DocsSectionRouteImport } from './routes/docs.$section' +import { Route as ArticlesIdRouteImport } from './routes/articles.$id' + +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DocsSectionRoute = DocsSectionRouteImport.update({ + id: '/$section', + path: '/$section', + getParentRoute: () => DocsRoute, +} as any) +const ArticlesIdRoute = ArticlesIdRouteImport.update({ + id: '/articles/$id', + path: '/articles/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRouteWithChildren + '/settings': typeof SettingsRoute + '/articles/$id': typeof ArticlesIdRoute + '/docs/$section': typeof DocsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/settings' | '/articles/$id' | '/docs/$section' + id: + | '__root__' + | '/' + | '/docs' + | '/settings' + | '/articles/$id' + | '/docs/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRouteWithChildren + SettingsRoute: typeof SettingsRoute + ArticlesIdRoute: typeof ArticlesIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/docs/$section': { + id: '/docs/$section' + path: '/$section' + fullPath: '/docs/$section' + preLoaderRoute: typeof DocsSectionRouteImport + parentRoute: typeof DocsRoute + } + '/articles/$id': { + id: '/articles/$id' + path: '/articles/$id' + fullPath: '/articles/$id' + preLoaderRoute: typeof ArticlesIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface DocsRouteChildren { + DocsSectionRoute: typeof DocsSectionRoute +} + +const DocsRouteChildren: DocsRouteChildren = { + DocsSectionRoute: DocsSectionRoute, +} + +const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRouteWithChildren, + SettingsRoute: SettingsRoute, + ArticlesIdRoute: ArticlesIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..ad87699fef --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/__root.tsx @@ -0,0 +1,58 @@ +import * as Vue from 'vue' +import { + HeadContent, + Link, + Outlet, + createRootRoute, +} from '@tanstack/vue-router' +import { articleIds, docsSections, rootHead } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + ) + }, +}) + +export const Route = createRootRoute({ + head: rootHead, + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/articles.$id.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/articles.$id.tsx new file mode 100644 index 0000000000..43a6fade58 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/articles.$id.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { articleDescription, articleHead, articleTitle } from '../../../shared' + +const ArticlePage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{articleTitle(params.value.id)}

+

{articleDescription(params.value.id)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/articles/$id')({ + head: ({ params }) => articleHead(params.id), + component: ArticlePage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.$section.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.$section.tsx new file mode 100644 index 0000000000..ebb59d61a2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.$section.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { docsDescription, docsSectionHead, docsTitle } from '../../../shared' + +const DocsSectionPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{docsTitle(params.value.section)}

+

{docsDescription(params.value.section)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/docs/$section')({ + head: ({ params }) => docsSectionHead(params.section), + component: DocsSectionPage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.tsx new file mode 100644 index 0000000000..feabbc4d4a --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/docs.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { docsLayoutHead } from '../../../shared' + +const DocsLayout = Vue.defineComponent({ + setup() { + return () => ( +
+

Docs

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/docs')({ + head: docsLayoutHead, + component: DocsLayout, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/index.tsx new file mode 100644 index 0000000000..421478833f --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/index.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeHead, homeTitle } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

{homeTitle}

+

Landing page

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + head: homeHead, + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/head/vue/src/routes/settings.tsx new file mode 100644 index 0000000000..1a519e16c2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/src/routes/settings.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { settingsHead, settingsTitle } from '../../../shared' + +const SettingsPage = Vue.defineComponent({ + setup() { + return () => ( +
+

{settingsTitle}

+

Account settings

+
+ ) + }, +}) + +export const Route = createFileRoute('/settings')({ + head: settingsHead, + component: SettingsPage, +}) diff --git a/benchmarks/client-nav/scenarios/head/vue/tsconfig.json b/benchmarks/client-nav/scenarios/head/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/head/vue/vite.config.ts b/benchmarks/client-nav/scenarios/head/vue/vite.config.ts new file mode 100644 index 0000000000..0ad2067925 --- /dev/null +++ b/benchmarks/client-nav/scenarios/head/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav head (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/history/react/project.json b/benchmarks/client-nav/scenarios/history/react/project.json new file mode 100644 index 0000000000..a2fe0001be --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-history-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/history/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/history/react/setup.ts b/benchmarks/client-nav/scenarios/history/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/history/react/speed.bench.ts b/benchmarks/client-nav/scenarios/history/react/speed.bench.ts new file mode 100644 index 0000000000..df2c951c03 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-history', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-history navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/history/react/speed.flame.ts b/benchmarks/client-nav/scenarios/history/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/main.tsx b/benchmarks/client-nav/scenarios/history/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/history/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..61cc83c4c8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as GalleryRouteImport } from './routes/gallery' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PhotosPhotoIdRouteImport } from './routes/photos.$photoId' +import { Route as PagesNRouteImport } from './routes/pages.$n' + +const GalleryRoute = GalleryRouteImport.update({ + id: '/gallery', + path: '/gallery', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PhotosPhotoIdRoute = PhotosPhotoIdRouteImport.update({ + id: '/photos/$photoId', + path: '/photos/$photoId', + getParentRoute: () => rootRouteImport, +} as any) +const PagesNRoute = PagesNRouteImport.update({ + id: '/pages/$n', + path: '/pages/$n', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + id: '__root__' | '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + GalleryRoute: typeof GalleryRoute + PagesNRoute: typeof PagesNRoute + PhotosPhotoIdRoute: typeof PhotosPhotoIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/gallery': { + id: '/gallery' + path: '/gallery' + fullPath: '/gallery' + preLoaderRoute: typeof GalleryRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/photos/$photoId': { + id: '/photos/$photoId' + path: '/photos/$photoId' + fullPath: '/photos/$photoId' + preLoaderRoute: typeof PhotosPhotoIdRouteImport + parentRoute: typeof rootRouteImport + } + '/pages/$n': { + id: '/pages/$n' + path: '/pages/$n' + fullPath: '/pages/$n' + preLoaderRoute: typeof PagesNRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + GalleryRoute: GalleryRoute, + PagesNRoute: PagesNRoute, + PhotosPhotoIdRoute: PhotosPhotoIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/__root.tsx new file mode 100644 index 0000000000..240d8719d4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/__root.tsx @@ -0,0 +1,66 @@ +import { + Link, + Outlet, + createRootRoute, + useBlocker, + useCanGoBack, + useLocation, +} from '@tanstack/react-router' +import { maskedPhotoId, shouldBlockFn } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + const canGoBack = useCanGoBack() + + useBlocker({ shouldBlockFn }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/gallery.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/gallery.tsx new file mode 100644 index 0000000000..55094556a6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/gallery.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/gallery')({ + component: GalleryPage, +}) + +function GalleryPage() { + return ( +
+

Gallery

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/index.tsx new file mode 100644 index 0000000000..932a72f2ef --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/pages.$n.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/pages.$n.tsx new file mode 100644 index 0000000000..b9c5e2f8d0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/pages.$n.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { pageLabel } from '../../../shared' + +export const Route = createFileRoute('/pages/$n')({ + component: PageComponent, +}) + +function PageComponent() { + const params = Route.useParams() + + return ( +
+

{pageLabel(params.n)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/src/routes/photos.$photoId.tsx b/benchmarks/client-nav/scenarios/history/react/src/routes/photos.$photoId.tsx new file mode 100644 index 0000000000..793b531c89 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/src/routes/photos.$photoId.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { photoLabel } from '../../../shared' + +export const Route = createFileRoute('/photos/$photoId')({ + component: PhotoComponent, +}) + +function PhotoComponent() { + const params = Route.useParams() + + return ( +
+

{photoLabel(params.photoId)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/react/tsconfig.json b/benchmarks/client-nav/scenarios/history/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/history/react/vite.config.ts b/benchmarks/client-nav/scenarios/history/react/vite.config.ts new file mode 100644 index 0000000000..61a1b2feef --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav history (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/history/shared.ts b/benchmarks/client-nav/scenarios/history/shared.ts new file mode 100644 index 0000000000..9ddfa6c104 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/shared.ts @@ -0,0 +1,89 @@ +/** + * Shared definition of the `history` scenario: push/replace/back/forward + * traversal, location masking, blocker registration overhead (the blocker + * never blocks), and useLocation/useCanGoBack subscriptions. + * + * The step lap is designed to keep the history depth stationary: it ends back + * on the initial entry, so the first push of the next lap truncates the stale + * forward tail and the session history never grows past 4 entries. + */ +import type { ScenarioStep } from '../harness' + +export const pageIds = ['1', '2', '3'] as const +export const maskedPhotoId = '42' + +export function pageLabel(n: string) { + return `Page ${n}` +} + +export function photoLabel(id: string) { + return `Photo ${id}` +} + +/** Registered on every navigation, never blocks. */ +export const shouldBlockFn = () => false + +interface HistoryStepExpectation { + step: ScenarioStep + /** Expected router location pathname (the REAL location, even when masked). */ + pathname: string + /** Expected window.location pathname when it differs (location masking). */ + windowPathname?: string + /** Marker test id that must be present in the container after the step. */ + marker?: string +} + +export const stepExpectations: ReadonlyArray = [ + { step: 'p-1', pathname: '/pages/1', marker: 'page-state' }, + { step: 'p-2', pathname: '/pages/2', marker: 'page-state' }, + { + step: 'photo-masked', + pathname: `/photos/${maskedPhotoId}`, + windowPathname: '/gallery', + marker: 'photo-state', + }, + { step: { type: 'back' }, pathname: '/pages/2', marker: 'page-state' }, + { step: { type: 'back' }, pathname: '/pages/1', marker: 'page-state' }, + { step: { type: 'forward' }, pathname: '/pages/2', marker: 'page-state' }, + { step: 'p-3-replace', pathname: '/pages/3', marker: 'page-state' }, + { step: { type: 'back' }, pathname: '/pages/1', marker: 'page-state' }, + { step: { type: 'back' }, pathname: '/', marker: 'home-state' }, +] + +export const steps = stepExpectations.map((expectation) => expectation.step) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = stepExpectations[stepIndex]! + + const loc = container.querySelector('[data-testid="loc"]')?.textContent + if (loc !== expected.pathname) { + throw new Error( + `Expected location marker to be "${expected.pathname}" after step ${stepIndex}, received "${loc}"`, + ) + } + + const windowPathname = expected.windowPathname ?? expected.pathname + if (window.location.pathname !== windowPathname) { + throw new Error( + `Expected window.location.pathname to be "${windowPathname}" after step ${stepIndex}, received "${window.location.pathname}"`, + ) + } + + if ( + expected.marker && + !container.querySelector(`[data-testid="${expected.marker}"]`) + ) { + throw new Error( + `Expected marker "${expected.marker}" to be rendered after step ${stepIndex}`, + ) + } +} + +// Two laps through the 9-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/history/solid/project.json b/benchmarks/client-nav/scenarios/history/solid/project.json new file mode 100644 index 0000000000..6f4bba1ca5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-history-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/history/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/history/solid/setup.ts b/benchmarks/client-nav/scenarios/history/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts new file mode 100644 index 0000000000..8e814bc133 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-history', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-history navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/history/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/history/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/main.tsx b/benchmarks/client-nav/scenarios/history/solid/src/main.tsx new file mode 100644 index 0000000000..b43608e191 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/history/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..a6ab2a4fc8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as GalleryRouteImport } from './routes/gallery' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PhotosPhotoIdRouteImport } from './routes/photos.$photoId' +import { Route as PagesNRouteImport } from './routes/pages.$n' + +const GalleryRoute = GalleryRouteImport.update({ + id: '/gallery', + path: '/gallery', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PhotosPhotoIdRoute = PhotosPhotoIdRouteImport.update({ + id: '/photos/$photoId', + path: '/photos/$photoId', + getParentRoute: () => rootRouteImport, +} as any) +const PagesNRoute = PagesNRouteImport.update({ + id: '/pages/$n', + path: '/pages/$n', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + id: '__root__' | '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + GalleryRoute: typeof GalleryRoute + PagesNRoute: typeof PagesNRoute + PhotosPhotoIdRoute: typeof PhotosPhotoIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/gallery': { + id: '/gallery' + path: '/gallery' + fullPath: '/gallery' + preLoaderRoute: typeof GalleryRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/photos/$photoId': { + id: '/photos/$photoId' + path: '/photos/$photoId' + fullPath: '/photos/$photoId' + preLoaderRoute: typeof PhotosPhotoIdRouteImport + parentRoute: typeof rootRouteImport + } + '/pages/$n': { + id: '/pages/$n' + path: '/pages/$n' + fullPath: '/pages/$n' + preLoaderRoute: typeof PagesNRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + GalleryRoute: GalleryRoute, + PagesNRoute: PagesNRoute, + PhotosPhotoIdRoute: PhotosPhotoIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..edf7368929 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/__root.tsx @@ -0,0 +1,66 @@ +import { + Link, + Outlet, + createRootRoute, + useBlocker, + useCanGoBack, + useLocation, +} from '@tanstack/solid-router' +import { maskedPhotoId, shouldBlockFn } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + const canGoBack = useCanGoBack() + + useBlocker({ shouldBlockFn }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/gallery.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/gallery.tsx new file mode 100644 index 0000000000..b72d1c6195 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/gallery.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/gallery')({ + component: GalleryPage, +}) + +function GalleryPage() { + return ( +
+

Gallery

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/index.tsx new file mode 100644 index 0000000000..44a0b8045a --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/pages.$n.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/pages.$n.tsx new file mode 100644 index 0000000000..1def6674c6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/pages.$n.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { pageLabel } from '../../../shared' + +export const Route = createFileRoute('/pages/$n')({ + component: PageComponent, +}) + +function PageComponent() { + const params = Route.useParams() + + return ( +
+

{pageLabel(params().n)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/src/routes/photos.$photoId.tsx b/benchmarks/client-nav/scenarios/history/solid/src/routes/photos.$photoId.tsx new file mode 100644 index 0000000000..ab2fe06e3f --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/src/routes/photos.$photoId.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { photoLabel } from '../../../shared' + +export const Route = createFileRoute('/photos/$photoId')({ + component: PhotoComponent, +}) + +function PhotoComponent() { + const params = Route.useParams() + + return ( +
+

{photoLabel(params().photoId)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/history/solid/tsconfig.json b/benchmarks/client-nav/scenarios/history/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/history/solid/vite.config.ts b/benchmarks/client-nav/scenarios/history/solid/vite.config.ts new file mode 100644 index 0000000000..de0eda543c --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav history (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/project.json b/benchmarks/client-nav/scenarios/history/vue/project.json new file mode 100644 index 0000000000..17fca887ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-history-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/history/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/history/vue/setup.ts b/benchmarks/client-nav/scenarios/history/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts new file mode 100644 index 0000000000..be2956bd85 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-history', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-history navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/history/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/history/vue/src/main.tsx b/benchmarks/client-nav/scenarios/history/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/history/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..cee1b68870 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as GalleryRouteImport } from './routes/gallery' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PhotosPhotoIdRouteImport } from './routes/photos.$photoId' +import { Route as PagesNRouteImport } from './routes/pages.$n' + +const GalleryRoute = GalleryRouteImport.update({ + id: '/gallery', + path: '/gallery', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PhotosPhotoIdRoute = PhotosPhotoIdRouteImport.update({ + id: '/photos/$photoId', + path: '/photos/$photoId', + getParentRoute: () => rootRouteImport, +} as any) +const PagesNRoute = PagesNRouteImport.update({ + id: '/pages/$n', + path: '/pages/$n', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/gallery': typeof GalleryRoute + '/pages/$n': typeof PagesNRoute + '/photos/$photoId': typeof PhotosPhotoIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + id: '__root__' | '/' | '/gallery' | '/pages/$n' | '/photos/$photoId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + GalleryRoute: typeof GalleryRoute + PagesNRoute: typeof PagesNRoute + PhotosPhotoIdRoute: typeof PhotosPhotoIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/gallery': { + id: '/gallery' + path: '/gallery' + fullPath: '/gallery' + preLoaderRoute: typeof GalleryRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/photos/$photoId': { + id: '/photos/$photoId' + path: '/photos/$photoId' + fullPath: '/photos/$photoId' + preLoaderRoute: typeof PhotosPhotoIdRouteImport + parentRoute: typeof rootRouteImport + } + '/pages/$n': { + id: '/pages/$n' + path: '/pages/$n' + fullPath: '/pages/$n' + preLoaderRoute: typeof PagesNRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + GalleryRoute: GalleryRoute, + PagesNRoute: PagesNRoute, + PhotosPhotoIdRoute: PhotosPhotoIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..7f7dff159c --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/__root.tsx @@ -0,0 +1,69 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useBlocker, + useCanGoBack, + useLocation, +} from '@tanstack/vue-router' +import { maskedPhotoId, shouldBlockFn } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + const pathname = useLocation({ select: (location) => location.pathname }) + const canGoBack = useCanGoBack() + + useBlocker({ shouldBlockFn }) + + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/gallery.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/gallery.tsx new file mode 100644 index 0000000000..3a397b1843 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/gallery.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const GalleryPage = Vue.defineComponent({ + setup() { + return () => ( +
+

Gallery

+
+ ) + }, +}) + +export const Route = createFileRoute('/gallery')({ + component: GalleryPage, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/index.tsx new file mode 100644 index 0000000000..fbc5b957a7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/index.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Home

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/pages.$n.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/pages.$n.tsx new file mode 100644 index 0000000000..560e83ed29 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/pages.$n.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { pageLabel } from '../../../shared' + +const PageComponent = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{pageLabel(params.value.n)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/pages/$n')({ + component: PageComponent, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/src/routes/photos.$photoId.tsx b/benchmarks/client-nav/scenarios/history/vue/src/routes/photos.$photoId.tsx new file mode 100644 index 0000000000..186566fe72 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/src/routes/photos.$photoId.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { photoLabel } from '../../../shared' + +const PhotoComponent = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{photoLabel(params.value.photoId)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/photos/$photoId')({ + component: PhotoComponent, +}) diff --git a/benchmarks/client-nav/scenarios/history/vue/tsconfig.json b/benchmarks/client-nav/scenarios/history/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/history/vue/vite.config.ts b/benchmarks/client-nav/scenarios/history/vue/vite.config.ts new file mode 100644 index 0000000000..58986d6526 --- /dev/null +++ b/benchmarks/client-nav/scenarios/history/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav history (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/links/react/project.json b/benchmarks/client-nav/scenarios/links/react/project.json new file mode 100644 index 0000000000..52c9df8a68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-links-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/links/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/links/react/setup.ts b/benchmarks/client-nav/scenarios/links/react/setup.ts new file mode 100644 index 0000000000..5409df903c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/links/react/speed.bench.ts b/benchmarks/client-nav/scenarios/links/react/speed.bench.ts new file mode 100644 index 0000000000..68bb869182 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-links', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-links navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/links/react/speed.flame.ts b/benchmarks/client-nav/scenarios/links/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/main.tsx b/benchmarks/client-nav/scenarios/links/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/links/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..97ec042c1c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' | '/items/$id' + id: '__root__' | '/' | '/about' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx new file mode 100644 index 0000000000..7a48510e8a --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/__root.tsx @@ -0,0 +1,104 @@ +import { + Link, + Outlet, + createRootRoute, + useMatchRoute, +} from '@tanstack/react-router' +import { + itemIds, + matchProbeIds, + stepItemIds, + variantSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function StatusPanel() { + const matchRoute = useMatchRoute() + + return ( + + ) +} + +function LinkGrid() { + return ( +
+ {itemIds.map((id) => ( +
+ + {`Item ${id}`} + + + {`Item ${id} exact`} + + + {`Item ${id} search`} + + + {`Item ${id} hash`} + + + {({ isActive }) => (isActive ? `Item ${id} on` : `Item ${id} off`)} + +
+ ))} +
+ ) +} + +function RootComponent() { + return ( + <> + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/about.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/about.tsx new file mode 100644 index 0000000000..0e4afc9297 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/about.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { aboutMarker } from '../../../shared' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return ( +
+

{aboutMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/index.tsx new file mode 100644 index 0000000000..2b8c83ebab --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/src/routes/items.$id.tsx b/benchmarks/client-nav/scenarios/links/react/src/routes/items.$id.tsx new file mode 100644 index 0000000000..23121edb5c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/src/routes/items.$id.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { itemMarker } from '../../../shared' + +export const Route = createFileRoute('/items/$id')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemMarker(params.id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/react/tsconfig.json b/benchmarks/client-nav/scenarios/links/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/links/react/vite.config.ts b/benchmarks/client-nav/scenarios/links/react/vite.config.ts new file mode 100644 index 0000000000..9e9cd48895 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav links (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/links/shared.ts b/benchmarks/client-nav/scenarios/links/shared.ts new file mode 100644 index 0000000000..6e75ea045c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/shared.ts @@ -0,0 +1,133 @@ +/** + * Shared definition of the `links` scenario: a large persistent grid of + * ``s in the root layout so every navigation recomputes link props and + * active state across many activeOptions variants, plus `useMatchRoute` / + * `MatchRoute` consumers. The three framework apps consume these constants so + * the workload is identical modulo the rendering layer. + */ +import type { ScenarioStep } from '../harness' + +// 40 item ids x 5 link variants = 200 links mounted in the root layout. +export const LINK_ITEM_COUNT = 40 +export const LINK_VARIANT_COUNT = 5 + +export const itemIds = Array.from({ length: LINK_ITEM_COUNT }, (_, index) => + String(index + 1), +) + +// Static search object attached to the includeSearch:false variant. +export const variantSearch = { tab: 'specs', page: 2 } + +// Item ids that get a `go-item-` test id on their plain variant link. +export const stepItemIds = ['1', '7', '13', '25', '40'] + +// Ids probed by the useMatchRoute checks in the root StatusPanel. +export const matchProbeIds = [ + '1', + '5', + '7', + '13', + '20', + '25', + '30', + '35', + '40', + '2', +] + +export function itemMarker(id: string) { + return `item-${id}` +} + +export const homeMarker = 'home' +export const aboutMarker = 'about' + +interface StepDef { + testId: string + marker: string + /** Expected number of `.active-link` elements after the step. */ + activeCount: number + /** When one link is active, its href must include this substring. */ + activeHrefPart?: string +} + +export const stepDefs: ReadonlyArray = [ + { + testId: 'go-item-1', + marker: itemMarker('1'), + activeCount: 1, + activeHrefPart: '/items/1', + }, + { + testId: 'go-item-7', + marker: itemMarker('7'), + activeCount: 1, + activeHrefPart: '/items/7', + }, + { testId: 'go-about', marker: aboutMarker, activeCount: 0 }, + { + testId: 'go-item-13', + marker: itemMarker('13'), + activeCount: 1, + activeHrefPart: '/items/13', + }, + { + testId: 'go-item-25', + marker: itemMarker('25'), + activeCount: 1, + activeHrefPart: '/items/25', + }, + { + testId: 'go-item-40', + marker: itemMarker('40'), + activeCount: 1, + activeHrefPart: '/items/40', + }, + { + testId: 'go-item-7', + marker: itemMarker('7'), + activeCount: 1, + activeHrefPart: '/items/7', + }, + { testId: 'go-home', marker: homeMarker, activeCount: 0 }, +] + +export const scenarioSteps: ReadonlyArray = stepDefs.map( + (step) => step.testId, +) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const step = stepDefs[stepIndex]! + + const marker = container.querySelector('[data-testid="page-state"]') + if (marker?.textContent !== step.marker) { + throw new Error( + `Expected page marker "${step.marker}" after step ${stepIndex}, received "${marker?.textContent}"`, + ) + } + + const activeLinks = container.querySelectorAll('a.active-link') + if (activeLinks.length !== step.activeCount) { + throw new Error( + `Expected ${step.activeCount} active link(s) after step ${stepIndex}, received ${activeLinks.length}`, + ) + } + if (step.activeHrefPart) { + const href = activeLinks[0]!.getAttribute('href') ?? '' + if (!href.includes(step.activeHrefPart)) { + throw new Error( + `Expected the active link href to include "${step.activeHrefPart}" after step ${stepIndex}, received "${href}"`, + ) + } + } +} + +// One lap through the 8-step sequence per benchmark iteration; each step +// recomputes all 200 links plus the match probes. +export const ticksPerIteration = 8 + +export const benchOptions = { + warmupIterations: 50, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/links/solid/project.json b/benchmarks/client-nav/scenarios/links/solid/project.json new file mode 100644 index 0000000000..ef1dab4c50 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-links-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/links/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/links/solid/setup.ts b/benchmarks/client-nav/scenarios/links/solid/setup.ts new file mode 100644 index 0000000000..34270fe8a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts new file mode 100644 index 0000000000..beba86fb6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-links', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-links navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/links/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/links/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/main.tsx b/benchmarks/client-nav/scenarios/links/solid/src/main.tsx new file mode 100644 index 0000000000..b43608e191 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/links/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..b61431e40d --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' | '/items/$id' + id: '__root__' | '/' | '/about' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..a889a26854 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/__root.tsx @@ -0,0 +1,119 @@ +import { For } from 'solid-js' +import { + Link, + Outlet, + createRootRoute, + useMatchRoute, +} from '@tanstack/solid-router' +import { + itemIds, + matchProbeIds, + stepItemIds, + variantSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function StatusPanel() { + const matchRoute = useMatchRoute() + + return ( + + ) +} + +function LinkGrid() { + return ( +
+ + {(id) => ( +
+ + {`Item ${id}`} + + + {`Item ${id} exact`} + + + {`Item ${id} search`} + + + {`Item ${id} hash`} + + + {({ isActive }) => + isActive ? `Item ${id} on` : `Item ${id} off` + } + +
+ )} +
+
+ ) +} + +function RootComponent() { + return ( + <> + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/about.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/about.tsx new file mode 100644 index 0000000000..a2a71678b7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/about.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { aboutMarker } from '../../../shared' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return ( +
+

{aboutMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/index.tsx new file mode 100644 index 0000000000..72fbfe387a --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/src/routes/items.$id.tsx b/benchmarks/client-nav/scenarios/links/solid/src/routes/items.$id.tsx new file mode 100644 index 0000000000..64d84f81f4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/src/routes/items.$id.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { itemMarker } from '../../../shared' + +export const Route = createFileRoute('/items/$id')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemMarker(params().id)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/links/solid/tsconfig.json b/benchmarks/client-nav/scenarios/links/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/links/solid/vite.config.ts b/benchmarks/client-nav/scenarios/links/solid/vite.config.ts new file mode 100644 index 0000000000..9a5b3d138b --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav links (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/project.json b/benchmarks/client-nav/scenarios/links/vue/project.json new file mode 100644 index 0000000000..fe1e68fad3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-links-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/links/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/links/vue/setup.ts b/benchmarks/client-nav/scenarios/links/vue/setup.ts new file mode 100644 index 0000000000..f70a154909 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts new file mode 100644 index 0000000000..54b99d1273 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-links', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-links navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/links/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/links/vue/src/main.tsx b/benchmarks/client-nav/scenarios/links/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/links/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..cee64be43c --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' | '/items/$id' + id: '__root__' | '/' | '/about' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..04f99677fd --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/__root.tsx @@ -0,0 +1,120 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useMatchRoute, +} from '@tanstack/vue-router' +import { + itemIds, + matchProbeIds, + stepItemIds, + variantSearch, +} from '../../../shared' + +const StatusPanel = Vue.defineComponent({ + setup() { + const matchRoute = useMatchRoute() + const probes = matchProbeIds.map((id) => ({ + id, + match: matchRoute({ to: '/items/$id', params: { id } }), + })) + // Static probes use the setup-scoped matchRoute instead of : + // vue-router's MatchRoute creates an undisposed watcher per render (one + // leaked subscription per navigation), so all three apps use + // useMatchRoute for cross-framework parity. + const atHome = matchRoute({ to: '/' }) + const atAbout = matchRoute({ to: '/about' }) + const underRoot = matchRoute({ to: '/', fuzzy: true }) + const underAbout = matchRoute({ to: '/about', fuzzy: true }) + const aboutSearch = matchRoute({ to: '/about', includeSearch: true }) + + return () => ( + + ) + }, +}) + +const LinkGrid = Vue.defineComponent({ + setup() { + return () => ( +
+ {itemIds.map((id) => ( +
+ + {`Item ${id}`} + + + {`Item ${id} exact`} + + + {`Item ${id} search`} + + + {`Item ${id} hash`} + + + {({ isActive }: { isActive: boolean }) => + isActive ? `Item ${id} on` : `Item ${id} off` + } + +
+ ))} +
+ ) + }, +}) + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/about.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/about.tsx new file mode 100644 index 0000000000..5cc1473bff --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/about.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { aboutMarker } from '../../../shared' + +const AboutPage = Vue.defineComponent({ + setup() { + return () => ( +
+

{aboutMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f04a9f5796 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeMarker } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

{homeMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/src/routes/items.$id.tsx b/benchmarks/client-nav/scenarios/links/vue/src/routes/items.$id.tsx new file mode 100644 index 0000000000..0dc5efa3d3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/src/routes/items.$id.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { itemMarker } from '../../../shared' + +const ItemPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{itemMarker(params.value.id)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/items/$id')({ + component: ItemPage, +}) diff --git a/benchmarks/client-nav/scenarios/links/vue/tsconfig.json b/benchmarks/client-nav/scenarios/links/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/links/vue/vite.config.ts b/benchmarks/client-nav/scenarios/links/vue/vite.config.ts new file mode 100644 index 0000000000..d1cf9f561d --- /dev/null +++ b/benchmarks/client-nav/scenarios/links/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav links (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/react/project.json b/benchmarks/client-nav/scenarios/loaders/react/project.json new file mode 100644 index 0000000000..d3dbe585d4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-loaders-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/loaders/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/setup.ts b/benchmarks/client-nav/scenarios/loaders/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts new file mode 100644 index 0000000000..7c44f1a0f7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-loaders', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-loaders navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/loaders/react/speed.flame.ts b/benchmarks/client-nav/scenarios/loaders/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/loaders/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..50f0a8863b --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DepsRouteImport } from './routes/deps' +import { Route as IndexRouteImport } from './routes/index' +import { Route as FreshIdRouteImport } from './routes/fresh.$id' +import { Route as CachedIdRouteImport } from './routes/cached.$id' + +const DepsRoute = DepsRouteImport.update({ + id: '/deps', + path: '/deps', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const FreshIdRoute = FreshIdRouteImport.update({ + id: '/fresh/$id', + path: '/fresh/$id', + getParentRoute: () => rootRouteImport, +} as any) +const CachedIdRoute = CachedIdRouteImport.update({ + id: '/cached/$id', + path: '/cached/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + id: '__root__' | '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DepsRoute: typeof DepsRoute + CachedIdRoute: typeof CachedIdRoute + FreshIdRoute: typeof FreshIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/deps': { + id: '/deps' + path: '/deps' + fullPath: '/deps' + preLoaderRoute: typeof DepsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/fresh/$id': { + id: '/fresh/$id' + path: '/fresh/$id' + fullPath: '/fresh/$id' + preLoaderRoute: typeof FreshIdRouteImport + parentRoute: typeof rootRouteImport + } + '/cached/$id': { + id: '/cached/$id' + path: '/cached/$id' + fullPath: '/cached/$id' + preLoaderRoute: typeof CachedIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DepsRoute: DepsRoute, + CachedIdRoute: CachedIdRoute, + FreshIdRoute: FreshIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/__root.tsx new file mode 100644 index 0000000000..6309763cf3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/__root.tsx @@ -0,0 +1,70 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/cached.$id.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/cached.$id.tsx new file mode 100644 index 0000000000..9b954185cc --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/cached.$id.tsx @@ -0,0 +1,55 @@ +import { createFileRoute } from '@tanstack/react-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/cached/$id')({ + loader: ({ params }) => { + const items = makeItems(`cached-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: CachedPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + void computeChecksum(value) + return null +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + void computeChecksum(value) + return null +} + +function CachedPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Cached

+
{`c-${params.id}-${loaderData.checksum}`}
+
    + {loaderData.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/deps.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/deps.tsx new file mode 100644 index 0000000000..92afe04a21 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/deps.tsx @@ -0,0 +1,65 @@ +import { createFileRoute } from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' +import { + computeChecksum, + itemsChecksum, + makeItems, + normalizePage, +} from '../../../shared' + +export const Route = createFileRoute('/deps')({ + validateSearch: (search: Record & SearchSchemaInput) => ({ + page: normalizePage(search.page), + }), + loaderDeps: ({ search }) => ({ page: search.page }), + loader: ({ deps }) => { + const items = makeItems(`deps-${deps.page}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: DepsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + void computeChecksum(value) + return null +} + +function DepsSubscriber() { + const value = Route.useLoaderDeps({ + select: (deps) => computeChecksum(deps.page * 17), + }) + + void computeChecksum(value) + return null +} + +function DepsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Deps

+
{`d-${search.page}-${loaderData.checksum}`}
+
    + {loaderData.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/fresh.$id.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/fresh.$id.tsx new file mode 100644 index 0000000000..fb7cbf2f00 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/fresh.$id.tsx @@ -0,0 +1,55 @@ +import { createFileRoute } from '@tanstack/react-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/fresh/$id')({ + loader: ({ params }) => { + const items = makeItems(`fresh-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 0, + gcTime: 0, + component: FreshPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + void computeChecksum(value) + return null +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + void computeChecksum(value) + return null +} + +function FreshPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Fresh

+
{`f-${params.id}-${loaderData.checksum}`}
+
    + {loaderData.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/loaders/react/src/routes/index.tsx new file mode 100644 index 0000000000..9203c00841 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Loaders Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/tsconfig.json b/benchmarks/client-nav/scenarios/loaders/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/loaders/react/vite.config.ts b/benchmarks/client-nav/scenarios/loaders/react/vite.config.ts new file mode 100644 index 0000000000..6fa28bcd35 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav loaders (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/shared.ts b/benchmarks/client-nav/scenarios/loaders/shared.ts new file mode 100644 index 0000000000..f744246160 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/shared.ts @@ -0,0 +1,134 @@ +/** + * Shared definition of the `loaders` scenario: deterministic loader data + * derivation, the click sequence, and per-step sanity assertions. The three + * framework apps consume these builders so the workload is identical modulo + * the rendering layer. + * + * Isolates: client loader dispatch — always-stale re-runs (staleTime 0), + * pure cache hits (staleTime 1e9), loaderDeps-keyed caching, + * router.invalidate(), and useLoaderData selector subscriptions. + */ + +import type { ScenarioStep } from '../harness' + +export function computeChecksum(seed: number) { + let value = Math.trunc(seed) | 0 + + for (let index = 0; index < 40; index++) { + value = (value * 1664525 + 1013904223 + index) >>> 0 + } + + return value +} + +export interface LoaderItem { + id: string + name: string + value: number + flag: boolean +} + +export function makeItems(seedText: string): Array { + let seed = 0 + for (let index = 0; index < seedText.length; index++) { + seed = (seed * 31 + seedText.charCodeAt(index)) >>> 0 + } + + const items: Array = [] + let value = seed + + for (let index = 0; index < 50; index++) { + value = (value * 1664525 + 1013904223) >>> 0 + items.push({ + id: `${seedText}-${index}`, + name: `Item ${index} of ${seedText}`, + value: value % 100000, + flag: value % 3 === 0, + }) + } + + return items +} + +export function itemsChecksum(items: Array) { + let sum = 0 + for (const item of items) { + sum = (sum + item.value + (item.flag ? 1 : 0)) >>> 0 + } + return sum +} + +export function normalizePage(value: unknown) { + const page = Number(value) + return Number.isFinite(page) && page > 0 ? Math.trunc(page) : 1 +} + +export function freshMarkerText(id: string) { + return `f-${id}-${itemsChecksum(makeItems(`fresh-${id}`))}` +} + +export function cachedMarkerText(id: string) { + return `c-${id}-${itemsChecksum(makeItems(`cached-${id}`))}` +} + +export function depsMarkerText(page: number) { + return `d-${page}-${itemsChecksum(makeItems(`deps-${page}`))}` +} + +export const steps: ReadonlyArray = [ + 'go-fresh-1', + 'go-fresh-2', + 'go-cached-1', + 'go-cached-2', + 'go-deps-1', + 'go-deps-2', + // Invalidates the currently matched routes (the deps route), re-running its + // loader with unchanged deps. Note: this also marks the cached fresh/cached + // matches invalid, so each of those loaders re-runs once per lap on its next + // visit — deterministic, identical work every lap. + { type: 'invalidate' }, + 'go-home', +] + +interface ExpectedState { + testId: string + text?: string +} + +const expectedStates: ReadonlyArray = [ + { testId: 'fresh-state', text: freshMarkerText('1') }, + { testId: 'fresh-state', text: freshMarkerText('2') }, + { testId: 'cached-state', text: cachedMarkerText('1') }, + { testId: 'cached-state', text: cachedMarkerText('2') }, + { testId: 'deps-state', text: depsMarkerText(1) }, + { testId: 'deps-state', text: depsMarkerText(2) }, + // After invalidate the deps loader re-ran with the same deps. + { testId: 'deps-state', text: depsMarkerText(2) }, + { testId: 'home-state' }, +] + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = expectedStates[stepIndex]! + const element = container.querySelector(`[data-testid="${expected.testId}"]`) + + if (!element) { + throw new Error( + `Expected marker "${expected.testId}" to exist after step ${stepIndex}`, + ) + } + + if (expected.text !== undefined && element.textContent !== expected.text) { + throw new Error( + `Expected marker "${expected.testId}" to read "${expected.text}" after step ${stepIndex}, received "${element.textContent}"`, + ) + } +} + +// Two laps through the 8-step sequence per benchmark iteration. +export const ticksPerIteration = 16 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/project.json b/benchmarks/client-nav/scenarios/loaders/solid/project.json new file mode 100644 index 0000000000..071e6bc385 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-loaders-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/loaders/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/setup.ts b/benchmarks/client-nav/scenarios/loaders/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts new file mode 100644 index 0000000000..4c97823d11 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-loaders', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-loaders navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/loaders/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/loaders/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx new file mode 100644 index 0000000000..b43608e191 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/loaders/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..bdec400237 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DepsRouteImport } from './routes/deps' +import { Route as IndexRouteImport } from './routes/index' +import { Route as FreshIdRouteImport } from './routes/fresh.$id' +import { Route as CachedIdRouteImport } from './routes/cached.$id' + +const DepsRoute = DepsRouteImport.update({ + id: '/deps', + path: '/deps', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const FreshIdRoute = FreshIdRouteImport.update({ + id: '/fresh/$id', + path: '/fresh/$id', + getParentRoute: () => rootRouteImport, +} as any) +const CachedIdRoute = CachedIdRouteImport.update({ + id: '/cached/$id', + path: '/cached/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + id: '__root__' | '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DepsRoute: typeof DepsRoute + CachedIdRoute: typeof CachedIdRoute + FreshIdRoute: typeof FreshIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/deps': { + id: '/deps' + path: '/deps' + fullPath: '/deps' + preLoaderRoute: typeof DepsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/fresh/$id': { + id: '/fresh/$id' + path: '/fresh/$id' + fullPath: '/fresh/$id' + preLoaderRoute: typeof FreshIdRouteImport + parentRoute: typeof rootRouteImport + } + '/cached/$id': { + id: '/cached/$id' + path: '/cached/$id' + fullPath: '/cached/$id' + preLoaderRoute: typeof CachedIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DepsRoute: DepsRoute, + CachedIdRoute: CachedIdRoute, + FreshIdRoute: FreshIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..30350f7907 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/__root.tsx @@ -0,0 +1,66 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/cached.$id.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/cached.$id.tsx new file mode 100644 index 0000000000..4838141b22 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/cached.$id.tsx @@ -0,0 +1,65 @@ +import { For, createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/cached/$id')({ + loader: ({ params }) => { + const items = makeItems(`cached-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: CachedPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect( + () => { + void props.value() + }, + () => {}, + ) + + return null +} + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return computeChecksum(value())} /> +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return computeChecksum(value())} /> +} + +function CachedPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Cached

+
{`c-${params().id}-${loaderData().checksum}`}
+
    + + {(item) =>
  • {item.name}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/deps.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/deps.tsx new file mode 100644 index 0000000000..258c7bc23b --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/deps.tsx @@ -0,0 +1,75 @@ +import { For, createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' +import { + computeChecksum, + itemsChecksum, + makeItems, + normalizePage, +} from '../../../shared' + +export const Route = createFileRoute('/deps')({ + validateSearch: (search: Record & SearchSchemaInput) => ({ + page: normalizePage(search.page), + }), + loaderDeps: ({ search }) => ({ page: search.page }), + loader: ({ deps }) => { + const items = makeItems(`deps-${deps.page}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: DepsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect( + () => { + void props.value() + }, + () => {}, + ) + + return null +} + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return computeChecksum(value())} /> +} + +function DepsSubscriber() { + const value = Route.useLoaderDeps({ + select: (deps) => computeChecksum(deps.page * 17), + }) + + return computeChecksum(value())} /> +} + +function DepsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Deps

+
{`d-${search().page}-${loaderData().checksum}`}
+
    + + {(item) =>
  • {item.name}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/fresh.$id.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/fresh.$id.tsx new file mode 100644 index 0000000000..29bc35a650 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/fresh.$id.tsx @@ -0,0 +1,65 @@ +import { For, createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +export const Route = createFileRoute('/fresh/$id')({ + loader: ({ params }) => { + const items = makeItems(`fresh-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 0, + gcTime: 0, + component: FreshPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect( + () => { + void props.value() + }, + () => {}, + ) + + return null +} + +function SumSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return computeChecksum(value())} /> +} + +function FirstItemSubscriber() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return computeChecksum(value())} /> +} + +function FreshPage() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Fresh

+
{`f-${params().id}-${loaderData().checksum}`}
+
    + + {(item) =>
  • {item.name}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/index.tsx new file mode 100644 index 0000000000..7287d47d0a --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Loaders Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/tsconfig.json b/benchmarks/client-nav/scenarios/loaders/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/loaders/solid/vite.config.ts b/benchmarks/client-nav/scenarios/loaders/solid/vite.config.ts new file mode 100644 index 0000000000..ea913ac578 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav loaders (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/project.json b/benchmarks/client-nav/scenarios/loaders/vue/project.json new file mode 100644 index 0000000000..69bd95837d --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-loaders-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/loaders/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/setup.ts b/benchmarks/client-nav/scenarios/loaders/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts new file mode 100644 index 0000000000..eeb9cefaeb --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-loaders', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-loaders navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/loaders/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/loaders/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..25a82df477 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routeTree.gen.ts @@ -0,0 +1,113 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DepsRouteImport } from './routes/deps' +import { Route as IndexRouteImport } from './routes/index' +import { Route as FreshIdRouteImport } from './routes/fresh.$id' +import { Route as CachedIdRouteImport } from './routes/cached.$id' + +const DepsRoute = DepsRouteImport.update({ + id: '/deps', + path: '/deps', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const FreshIdRoute = FreshIdRouteImport.update({ + id: '/fresh/$id', + path: '/fresh/$id', + getParentRoute: () => rootRouteImport, +} as any) +const CachedIdRoute = CachedIdRouteImport.update({ + id: '/cached/$id', + path: '/cached/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/deps': typeof DepsRoute + '/cached/$id': typeof CachedIdRoute + '/fresh/$id': typeof FreshIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/deps' | '/cached/$id' | '/fresh/$id' + id: '__root__' | '/' | '/deps' | '/cached/$id' | '/fresh/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DepsRoute: typeof DepsRoute + CachedIdRoute: typeof CachedIdRoute + FreshIdRoute: typeof FreshIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/deps': { + id: '/deps' + path: '/deps' + fullPath: '/deps' + preLoaderRoute: typeof DepsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/fresh/$id': { + id: '/fresh/$id' + path: '/fresh/$id' + fullPath: '/fresh/$id' + preLoaderRoute: typeof FreshIdRouteImport + parentRoute: typeof rootRouteImport + } + '/cached/$id': { + id: '/cached/$id' + path: '/cached/$id' + fullPath: '/cached/$id' + preLoaderRoute: typeof CachedIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DepsRoute: DepsRoute, + CachedIdRoute: CachedIdRoute, + FreshIdRoute: FreshIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..b9102a92c4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/__root.tsx @@ -0,0 +1,69 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/cached.$id.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/cached.$id.tsx new file mode 100644 index 0000000000..a426e35765 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/cached.$id.tsx @@ -0,0 +1,68 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const SumSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FirstItemSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const CachedPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Cached

+
+ {`c-${params.value.id}-${loaderData.value.checksum}`} +
+
    + {loaderData.value.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/cached/$id')({ + loader: ({ params }) => { + const items = makeItems(`cached-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: CachedPage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/deps.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/deps.tsx new file mode 100644 index 0000000000..f987d15d34 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/deps.tsx @@ -0,0 +1,78 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' +import { + computeChecksum, + itemsChecksum, + makeItems, + normalizePage, +} from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const SumSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const DepsSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderDeps({ + select: (deps) => computeChecksum(deps.page * 17), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const DepsPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Deps

+
+ {`d-${search.value.page}-${loaderData.value.checksum}`} +
+
    + {loaderData.value.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/deps')({ + validateSearch: (search: Record & SearchSchemaInput) => ({ + page: normalizePage(search.page), + }), + loaderDeps: ({ search }) => ({ page: search.page }), + loader: ({ deps }) => { + const items = makeItems(`deps-${deps.page}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 1e9, + gcTime: 1e9, + component: DepsPage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/fresh.$id.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/fresh.$id.tsx new file mode 100644 index 0000000000..64eec85cfa --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/fresh.$id.tsx @@ -0,0 +1,68 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { computeChecksum, itemsChecksum, makeItems } from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const SumSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.checksum), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FirstItemSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useLoaderData({ + select: (data) => computeChecksum(data.items[0]?.value ?? 0), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FreshPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Fresh

+
+ {`f-${params.value.id}-${loaderData.value.checksum}`} +
+
    + {loaderData.value.items.slice(0, 5).map((item) => ( +
  • {item.name}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/fresh/$id')({ + loader: ({ params }) => { + const items = makeItems(`fresh-${params.id}`) + return { items, checksum: itemsChecksum(items) } + }, + staleTime: 0, + gcTime: 0, + component: FreshPage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/index.tsx new file mode 100644 index 0000000000..c9724c90a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Loaders Bench

+
home
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/loaders/vue/tsconfig.json b/benchmarks/client-nav/scenarios/loaders/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/loaders/vue/vite.config.ts b/benchmarks/client-nav/scenarios/loaders/vue/vite.config.ts new file mode 100644 index 0000000000..350fadf387 --- /dev/null +++ b/benchmarks/client-nav/scenarios/loaders/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav loaders (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/mount/react/project.json b/benchmarks/client-nav/scenarios/mount/react/project.json new file mode 100644 index 0000000000..61839943b3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-mount-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/mount/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/mount/react/setup.ts b/benchmarks/client-nav/scenarios/mount/react/setup.ts new file mode 100644 index 0000000000..ed0426c696 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/setup.ts @@ -0,0 +1,17 @@ +import { createMountLoopSetup } from '../../harness' +import { assertReady, readyTestId } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createMountLoopSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + readyTestId, + assertReady, + }) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts new file mode 100644 index 0000000000..8f7967c42c --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-mount', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-mount loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/mount/react/speed.flame.ts b/benchmarks/client-nav/scenarios/mount/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/main.tsx b/benchmarks/client-nav/scenarios/mount/react/src/main.tsx new file mode 100644 index 0000000000..78fe2082aa --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/main.tsx @@ -0,0 +1,47 @@ +import { + RouterProvider, + createBrowserHistory, + createRouter, +} from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' +import type { RouterHistory } from '@tanstack/react-router' + +/** + * Unlike the other scenarios, this app is mounted and unmounted in a loop, so + * it must not register page-lifetime globals that have no dispose path: + * - the history is created explicitly and `destroy()`ed on unmount — the + * default per-router `createBrowserHistory()` monkey-patches + * `window.history.pushState`/`replaceState`, chaining one wrapper per + * router and degrading every later mount; + * - `scrollRestoration` stays off — enabling it registers per-router + * document/window listeners that retain every router ever created. + */ +export function createTestRouter(history: RouterHistory) { + return createRouter({ + routeTree, + history, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const history = createBrowserHistory() + const router = createTestRouter(history) + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + history.destroy() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/mount/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..a796abe0ec --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routeTree.gen.ts @@ -0,0 +1,264 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ShopRouteImport } from './routes/shop' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as SearchRouteImport } from './routes/search' +import { Route as BlogRouteImport } from './routes/blog' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ShopIndexRouteImport } from './routes/shop.index' +import { Route as BlogIndexRouteImport } from './routes/blog.index' +import { Route as ShopProductIdRouteImport } from './routes/shop.$productId' +import { Route as BlogSlugRouteImport } from './routes/blog.$slug' + +const ShopRoute = ShopRouteImport.update({ + id: '/shop', + path: '/shop', + getParentRoute: () => rootRouteImport, +} as any) +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const SearchRoute = SearchRouteImport.update({ + id: '/search', + path: '/search', + getParentRoute: () => rootRouteImport, +} as any) +const BlogRoute = BlogRouteImport.update({ + id: '/blog', + path: '/blog', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ShopIndexRoute = ShopIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShopRoute, +} as any) +const BlogIndexRoute = BlogIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => BlogRoute, +} as any) +const ShopProductIdRoute = ShopProductIdRouteImport.update({ + id: '/$productId', + path: '/$productId', + getParentRoute: () => ShopRoute, +} as any) +const BlogSlugRoute = BlogSlugRouteImport.update({ + id: '/$slug', + path: '/$slug', + getParentRoute: () => BlogRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog': typeof BlogIndexRoute + '/shop': typeof ShopIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/search' + | '/settings' + | '/blog/$slug' + | '/shop/$productId' + | '/blog' + | '/shop' + id: + | '__root__' + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + BlogRoute: typeof BlogRouteWithChildren + SearchRoute: typeof SearchRoute + SettingsRoute: typeof SettingsRoute + ShopRoute: typeof ShopRouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/shop': { + id: '/shop' + path: '/shop' + fullPath: '/shop' + preLoaderRoute: typeof ShopRouteImport + parentRoute: typeof rootRouteImport + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteImport + parentRoute: typeof rootRouteImport + } + '/blog': { + id: '/blog' + path: '/blog' + fullPath: '/blog' + preLoaderRoute: typeof BlogRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/shop/': { + id: '/shop/' + path: '/' + fullPath: '/shop/' + preLoaderRoute: typeof ShopIndexRouteImport + parentRoute: typeof ShopRoute + } + '/blog/': { + id: '/blog/' + path: '/' + fullPath: '/blog/' + preLoaderRoute: typeof BlogIndexRouteImport + parentRoute: typeof BlogRoute + } + '/shop/$productId': { + id: '/shop/$productId' + path: '/$productId' + fullPath: '/shop/$productId' + preLoaderRoute: typeof ShopProductIdRouteImport + parentRoute: typeof ShopRoute + } + '/blog/$slug': { + id: '/blog/$slug' + path: '/$slug' + fullPath: '/blog/$slug' + preLoaderRoute: typeof BlogSlugRouteImport + parentRoute: typeof BlogRoute + } + } +} + +interface BlogRouteChildren { + BlogSlugRoute: typeof BlogSlugRoute + BlogIndexRoute: typeof BlogIndexRoute +} + +const BlogRouteChildren: BlogRouteChildren = { + BlogSlugRoute: BlogSlugRoute, + BlogIndexRoute: BlogIndexRoute, +} + +const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren) + +interface ShopRouteChildren { + ShopProductIdRoute: typeof ShopProductIdRoute + ShopIndexRoute: typeof ShopIndexRoute +} + +const ShopRouteChildren: ShopRouteChildren = { + ShopProductIdRoute: ShopProductIdRoute, + ShopIndexRoute: ShopIndexRoute, +} + +const ShopRouteWithChildren = ShopRoute._addFileChildren(ShopRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + BlogRoute: BlogRouteWithChildren, + SearchRoute: SearchRoute, + SettingsRoute: SettingsRoute, + ShopRoute: ShopRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/__root.tsx new file mode 100644 index 0000000000..0deab171d8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/__root.tsx @@ -0,0 +1,41 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/about.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/about.tsx new file mode 100644 index 0000000000..3fedde283b --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return

About

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.$slug.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.$slug.tsx new file mode 100644 index 0000000000..f995d95d94 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.$slug.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' +import { articleData } from '../../../shared' + +export const Route = createFileRoute('/blog/$slug')({ + loader: ({ params }) => articleData(params.slug), + component: ArticlePage, +}) + +function ArticlePage() { + const article = Route.useLoaderData() + + return
{`${article.title} (${article.words} words)`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.index.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.index.tsx new file mode 100644 index 0000000000..437187eb4f --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/blog/')({ + component: BlogIndexPage, +}) + +function BlogIndexPage() { + return

All articles

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.tsx new file mode 100644 index 0000000000..ab4fbeb482 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/blog.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { blogContext } from '../../../shared' + +export const Route = createFileRoute('/blog')({ + beforeLoad: () => blogContext(), + component: BlogLayout, +}) + +function BlogLayout() { + return ( +
+

Blog

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/index.tsx new file mode 100644 index 0000000000..dbc99d8f66 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/index.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeItems, readyTestId } from '../../../shared' + +export const Route = createFileRoute('/')({ + loader: () => homeItems(), + component: HomePage, +}) + +function HomePage() { + const items = Route.useLoaderData() + + return ( +
+

{`Home (${items.length} items)`}

+
    + {items.map((item) => ( +
  • {`${item.label}: ${item.score}`}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/search.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/search.tsx new file mode 100644 index 0000000000..faba50fc72 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/search.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { normalizePage, normalizeQuery } from '../../../shared' + +export const Route = createFileRoute('/search')({ + validateSearch: (search: Record) => ({ + q: normalizeQuery(search.q), + page: normalizePage(search.page), + }), + component: SearchPage, +}) + +function SearchPage() { + const search = Route.useSearch() + + return

{`Results for "${search.q}" page ${search.page}`}

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/settings.tsx new file mode 100644 index 0000000000..1f46d9d885 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings')({ + component: SettingsPage, +}) + +function SettingsPage() { + return

Settings

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.$productId.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.$productId.tsx new file mode 100644 index 0000000000..7b729a3dca --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.$productId.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' +import { productData } from '../../../shared' + +export const Route = createFileRoute('/shop/$productId')({ + loader: ({ params }) => productData(params.productId), + component: ProductPage, +}) + +function ProductPage() { + const product = Route.useLoaderData() + + return
{`${product.name}: ${product.price}`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.index.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.index.tsx new file mode 100644 index 0000000000..dbca94357a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/shop/')({ + component: ShopIndexPage, +}) + +function ShopIndexPage() { + return

All products

+} diff --git a/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.tsx b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.tsx new file mode 100644 index 0000000000..5243d7bb0e --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/src/routes/shop.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { shopContext } from '../../../shared' + +export const Route = createFileRoute('/shop')({ + beforeLoad: () => shopContext(), + component: ShopLayout, +}) + +function ShopLayout() { + return ( +
+

Shop

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/react/tsconfig.json b/benchmarks/client-nav/scenarios/mount/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/mount/react/vite.config.ts b/benchmarks/client-nav/scenarios/mount/react/vite.config.ts new file mode 100644 index 0000000000..68ee46738a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav mount (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/mount/shared.ts b/benchmarks/client-nav/scenarios/mount/shared.ts new file mode 100644 index 0000000000..a8f3d2c7bd --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/shared.ts @@ -0,0 +1,75 @@ +/** + * Shared definition of the `mount` scenario: cold start of a moderate, + * realistic app — router creation (processRouteTree), first render, initial + * `router.load()`, and unmount. This is the only scenario where router + * construction is inside the measured loop; the wider tree exists as + * realistic route-processing weight even though only the index route loads. + */ + +export const readyTestId = 'mount-ready' + +export const homeItemCount = 5 + +export function homeItems() { + return Array.from({ length: homeItemCount }, (_, index) => ({ + id: `item-${index}`, + label: `Featured item ${index}`, + score: index * 17 + 3, + })) +} + +export const homeReadyText = `Home (${homeItemCount} items)` + +export function shopContext() { + return { shopSeed: 7 } +} + +export function blogContext() { + return { blogSeed: 13 } +} + +export function productData(productId: string) { + return { + id: productId, + name: `Product ${productId}`, + price: productId.length * 100 + 42, + } +} + +export function articleData(slug: string) { + return { + slug, + title: `Article ${slug}`, + words: slug.length * 250, + } +} + +export function normalizePage(value: unknown) { + const page = Number(value) + return Number.isFinite(page) && page > 0 ? Math.trunc(page) : 1 +} + +export function normalizeQuery(value: unknown) { + return typeof value === 'string' ? value : '' +} + +export function assertReady(container: HTMLElement) { + const marker = container.querySelector(`[data-testid="${readyTestId}"]`) + if (marker?.textContent !== homeReadyText) { + throw new Error( + `Expected mount marker to be "${homeReadyText}", received "${marker?.textContent}"`, + ) + } +} + +// Mounts per benchmark iteration. +// 6 mounts per iteration: a single mount simulates to only ~8ms of CPU, and +// very short measures amplify the relative impact of allocator/GC +// quantization; 6 keeps the measure comfortably above ~50ms simulated. +export const ticksPerIteration = 6 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/project.json b/benchmarks/client-nav/scenarios/mount/solid/project.json new file mode 100644 index 0000000000..4349570654 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-mount-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/mount/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/setup.ts b/benchmarks/client-nav/scenarios/mount/solid/setup.ts new file mode 100644 index 0000000000..810ea38c91 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/setup.ts @@ -0,0 +1,17 @@ +import { createMountLoopSetup } from '../../harness' +import { assertReady, readyTestId } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createMountLoopSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + readyTestId, + assertReady, + }) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts new file mode 100644 index 0000000000..1bce4c6750 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-mount', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-mount loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/mount/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/mount/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx new file mode 100644 index 0000000000..520d8cff89 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/main.tsx @@ -0,0 +1,46 @@ +import { render } from '@solidjs/web' +import { + RouterProvider, + createBrowserHistory, + createRouter, +} from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import type { RouterHistory } from '@tanstack/solid-router' + +/** + * Unlike the other scenarios, this app is mounted and unmounted in a loop, so + * it must not register page-lifetime globals that have no dispose path: + * - the history is created explicitly and `destroy()`ed on unmount — the + * default per-router `createBrowserHistory()` monkey-patches + * `window.history.pushState`/`replaceState`, chaining one wrapper per + * router and degrading every later mount; + * - `scrollRestoration` stays off — enabling it registers per-router + * document/window listeners that retain every router ever created. + */ +export function createTestRouter(history: RouterHistory) { + return createRouter({ + routeTree, + history, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const history = createBrowserHistory() + const router = createTestRouter(history) + + const dispose = render(() => , container) + + return { + router, + unmount() { + dispose() + history.destroy() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/mount/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..dc0acc183d --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routeTree.gen.ts @@ -0,0 +1,264 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ShopRouteImport } from './routes/shop' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as SearchRouteImport } from './routes/search' +import { Route as BlogRouteImport } from './routes/blog' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ShopIndexRouteImport } from './routes/shop.index' +import { Route as BlogIndexRouteImport } from './routes/blog.index' +import { Route as ShopProductIdRouteImport } from './routes/shop.$productId' +import { Route as BlogSlugRouteImport } from './routes/blog.$slug' + +const ShopRoute = ShopRouteImport.update({ + id: '/shop', + path: '/shop', + getParentRoute: () => rootRouteImport, +} as any) +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const SearchRoute = SearchRouteImport.update({ + id: '/search', + path: '/search', + getParentRoute: () => rootRouteImport, +} as any) +const BlogRoute = BlogRouteImport.update({ + id: '/blog', + path: '/blog', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ShopIndexRoute = ShopIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShopRoute, +} as any) +const BlogIndexRoute = BlogIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => BlogRoute, +} as any) +const ShopProductIdRoute = ShopProductIdRouteImport.update({ + id: '/$productId', + path: '/$productId', + getParentRoute: () => ShopRoute, +} as any) +const BlogSlugRoute = BlogSlugRouteImport.update({ + id: '/$slug', + path: '/$slug', + getParentRoute: () => BlogRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog': typeof BlogIndexRoute + '/shop': typeof ShopIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/search' + | '/settings' + | '/blog/$slug' + | '/shop/$productId' + | '/blog' + | '/shop' + id: + | '__root__' + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + BlogRoute: typeof BlogRouteWithChildren + SearchRoute: typeof SearchRoute + SettingsRoute: typeof SettingsRoute + ShopRoute: typeof ShopRouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/shop': { + id: '/shop' + path: '/shop' + fullPath: '/shop' + preLoaderRoute: typeof ShopRouteImport + parentRoute: typeof rootRouteImport + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteImport + parentRoute: typeof rootRouteImport + } + '/blog': { + id: '/blog' + path: '/blog' + fullPath: '/blog' + preLoaderRoute: typeof BlogRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/shop/': { + id: '/shop/' + path: '/' + fullPath: '/shop/' + preLoaderRoute: typeof ShopIndexRouteImport + parentRoute: typeof ShopRoute + } + '/blog/': { + id: '/blog/' + path: '/' + fullPath: '/blog/' + preLoaderRoute: typeof BlogIndexRouteImport + parentRoute: typeof BlogRoute + } + '/shop/$productId': { + id: '/shop/$productId' + path: '/$productId' + fullPath: '/shop/$productId' + preLoaderRoute: typeof ShopProductIdRouteImport + parentRoute: typeof ShopRoute + } + '/blog/$slug': { + id: '/blog/$slug' + path: '/$slug' + fullPath: '/blog/$slug' + preLoaderRoute: typeof BlogSlugRouteImport + parentRoute: typeof BlogRoute + } + } +} + +interface BlogRouteChildren { + BlogSlugRoute: typeof BlogSlugRoute + BlogIndexRoute: typeof BlogIndexRoute +} + +const BlogRouteChildren: BlogRouteChildren = { + BlogSlugRoute: BlogSlugRoute, + BlogIndexRoute: BlogIndexRoute, +} + +const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren) + +interface ShopRouteChildren { + ShopProductIdRoute: typeof ShopProductIdRoute + ShopIndexRoute: typeof ShopIndexRoute +} + +const ShopRouteChildren: ShopRouteChildren = { + ShopProductIdRoute: ShopProductIdRoute, + ShopIndexRoute: ShopIndexRoute, +} + +const ShopRouteWithChildren = ShopRoute._addFileChildren(ShopRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + BlogRoute: BlogRouteWithChildren, + SearchRoute: SearchRoute, + SettingsRoute: SettingsRoute, + ShopRoute: ShopRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..f5c3be2641 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/__root.tsx @@ -0,0 +1,41 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/about.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/about.tsx new file mode 100644 index 0000000000..f74d85aa06 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return

About

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.$slug.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.$slug.tsx new file mode 100644 index 0000000000..82caee757a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.$slug.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { articleData } from '../../../shared' + +export const Route = createFileRoute('/blog/$slug')({ + loader: ({ params }) => articleData(params.slug), + component: ArticlePage, +}) + +function ArticlePage() { + const article = Route.useLoaderData() + + return
{`${article().title} (${article().words} words)`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.index.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.index.tsx new file mode 100644 index 0000000000..8e75108aca --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/blog/')({ + component: BlogIndexPage, +}) + +function BlogIndexPage() { + return

All articles

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.tsx new file mode 100644 index 0000000000..1e2cfbd019 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/blog.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { blogContext } from '../../../shared' + +export const Route = createFileRoute('/blog')({ + beforeLoad: () => blogContext(), + component: BlogLayout, +}) + +function BlogLayout() { + return ( +
+

Blog

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/index.tsx new file mode 100644 index 0000000000..30665ba224 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/index.tsx @@ -0,0 +1,23 @@ +import { For } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { homeItems, readyTestId } from '../../../shared' + +export const Route = createFileRoute('/')({ + loader: () => homeItems(), + component: HomePage, +}) + +function HomePage() { + const items = Route.useLoaderData() + + return ( +
+

{`Home (${items().length} items)`}

+
    + + {(item) =>
  • {`${item.label}: ${item.score}`}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/search.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/search.tsx new file mode 100644 index 0000000000..3f8b2d9dd7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/search.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { normalizePage, normalizeQuery } from '../../../shared' + +export const Route = createFileRoute('/search')({ + validateSearch: (search: Record) => ({ + q: normalizeQuery(search.q), + page: normalizePage(search.page), + }), + component: SearchPage, +}) + +function SearchPage() { + const search = Route.useSearch() + + return

{`Results for "${search().q}" page ${search().page}`}

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/settings.tsx new file mode 100644 index 0000000000..c6b95424d9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/settings')({ + component: SettingsPage, +}) + +function SettingsPage() { + return

Settings

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.$productId.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.$productId.tsx new file mode 100644 index 0000000000..71b0411d6b --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.$productId.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { productData } from '../../../shared' + +export const Route = createFileRoute('/shop/$productId')({ + loader: ({ params }) => productData(params.productId), + component: ProductPage, +}) + +function ProductPage() { + const product = Route.useLoaderData() + + return
{`${product().name}: ${product().price}`}
+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.index.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.index.tsx new file mode 100644 index 0000000000..e33c8779f9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/shop/')({ + component: ShopIndexPage, +}) + +function ShopIndexPage() { + return

All products

+} diff --git a/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.tsx b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.tsx new file mode 100644 index 0000000000..ff2ca78653 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/src/routes/shop.tsx @@ -0,0 +1,16 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { shopContext } from '../../../shared' + +export const Route = createFileRoute('/shop')({ + beforeLoad: () => shopContext(), + component: ShopLayout, +}) + +function ShopLayout() { + return ( +
+

Shop

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/tsconfig.json b/benchmarks/client-nav/scenarios/mount/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/mount/solid/vite.config.ts b/benchmarks/client-nav/scenarios/mount/solid/vite.config.ts new file mode 100644 index 0000000000..910d332f9c --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav mount (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/project.json b/benchmarks/client-nav/scenarios/mount/vue/project.json new file mode 100644 index 0000000000..f2dbd10440 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-mount-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/mount/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/setup.ts b/benchmarks/client-nav/scenarios/mount/vue/setup.ts new file mode 100644 index 0000000000..3976e71c51 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/setup.ts @@ -0,0 +1,17 @@ +import { createMountLoopSetup } from '../../harness' +import { assertReady, readyTestId } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createMountLoopSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + readyTestId, + assertReady, + }) +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts new file mode 100644 index 0000000000..d2e4b72c84 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-mount', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-mount loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/mount/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx new file mode 100644 index 0000000000..6d238e2832 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/main.tsx @@ -0,0 +1,51 @@ +import * as Vue from 'vue' +import { + RouterProvider, + createBrowserHistory, + createRouter, +} from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' +import type { RouterHistory } from '@tanstack/vue-router' + +/** + * Unlike the other scenarios, this app is mounted and unmounted in a loop, so + * it must not register page-lifetime globals that have no dispose path: + * - the history is created explicitly and `destroy()`ed on unmount — the + * default per-router `createBrowserHistory()` monkey-patches + * `window.history.pushState`/`replaceState`, chaining one wrapper per + * router and degrading every later mount; + * - `scrollRestoration` stays off — enabling it registers per-router + * document/window listeners that retain every router ever created. + */ +export function createTestRouter(history: RouterHistory) { + return createRouter({ + routeTree, + history, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const history = createBrowserHistory() + const router = createTestRouter(history) + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + history.destroy() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/mount/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..98643d63c9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routeTree.gen.ts @@ -0,0 +1,264 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ShopRouteImport } from './routes/shop' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as SearchRouteImport } from './routes/search' +import { Route as BlogRouteImport } from './routes/blog' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ShopIndexRouteImport } from './routes/shop.index' +import { Route as BlogIndexRouteImport } from './routes/blog.index' +import { Route as ShopProductIdRouteImport } from './routes/shop.$productId' +import { Route as BlogSlugRouteImport } from './routes/blog.$slug' + +const ShopRoute = ShopRouteImport.update({ + id: '/shop', + path: '/shop', + getParentRoute: () => rootRouteImport, +} as any) +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => rootRouteImport, +} as any) +const SearchRoute = SearchRouteImport.update({ + id: '/search', + path: '/search', + getParentRoute: () => rootRouteImport, +} as any) +const BlogRoute = BlogRouteImport.update({ + id: '/blog', + path: '/blog', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ShopIndexRoute = ShopIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShopRoute, +} as any) +const BlogIndexRoute = BlogIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => BlogRoute, +} as any) +const ShopProductIdRoute = ShopProductIdRouteImport.update({ + id: '/$productId', + path: '/$productId', + getParentRoute: () => ShopRoute, +} as any) +const BlogSlugRoute = BlogSlugRouteImport.update({ + id: '/$slug', + path: '/$slug', + getParentRoute: () => BlogRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog': typeof BlogIndexRoute + '/shop': typeof ShopIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRouteWithChildren + '/search': typeof SearchRoute + '/settings': typeof SettingsRoute + '/shop': typeof ShopRouteWithChildren + '/blog/$slug': typeof BlogSlugRoute + '/shop/$productId': typeof ShopProductIdRoute + '/blog/': typeof BlogIndexRoute + '/shop/': typeof ShopIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/search' + | '/settings' + | '/blog/$slug' + | '/shop/$productId' + | '/blog' + | '/shop' + id: + | '__root__' + | '/' + | '/about' + | '/blog' + | '/search' + | '/settings' + | '/shop' + | '/blog/$slug' + | '/shop/$productId' + | '/blog/' + | '/shop/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + BlogRoute: typeof BlogRouteWithChildren + SearchRoute: typeof SearchRoute + SettingsRoute: typeof SettingsRoute + ShopRoute: typeof ShopRouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/shop': { + id: '/shop' + path: '/shop' + fullPath: '/shop' + preLoaderRoute: typeof ShopRouteImport + parentRoute: typeof rootRouteImport + } + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport + parentRoute: typeof rootRouteImport + } + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteImport + parentRoute: typeof rootRouteImport + } + '/blog': { + id: '/blog' + path: '/blog' + fullPath: '/blog' + preLoaderRoute: typeof BlogRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/shop/': { + id: '/shop/' + path: '/' + fullPath: '/shop/' + preLoaderRoute: typeof ShopIndexRouteImport + parentRoute: typeof ShopRoute + } + '/blog/': { + id: '/blog/' + path: '/' + fullPath: '/blog/' + preLoaderRoute: typeof BlogIndexRouteImport + parentRoute: typeof BlogRoute + } + '/shop/$productId': { + id: '/shop/$productId' + path: '/$productId' + fullPath: '/shop/$productId' + preLoaderRoute: typeof ShopProductIdRouteImport + parentRoute: typeof ShopRoute + } + '/blog/$slug': { + id: '/blog/$slug' + path: '/$slug' + fullPath: '/blog/$slug' + preLoaderRoute: typeof BlogSlugRouteImport + parentRoute: typeof BlogRoute + } + } +} + +interface BlogRouteChildren { + BlogSlugRoute: typeof BlogSlugRoute + BlogIndexRoute: typeof BlogIndexRoute +} + +const BlogRouteChildren: BlogRouteChildren = { + BlogSlugRoute: BlogSlugRoute, + BlogIndexRoute: BlogIndexRoute, +} + +const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren) + +interface ShopRouteChildren { + ShopProductIdRoute: typeof ShopProductIdRoute + ShopIndexRoute: typeof ShopIndexRoute +} + +const ShopRouteChildren: ShopRouteChildren = { + ShopProductIdRoute: ShopProductIdRoute, + ShopIndexRoute: ShopIndexRoute, +} + +const ShopRouteWithChildren = ShopRoute._addFileChildren(ShopRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + BlogRoute: BlogRouteWithChildren, + SearchRoute: SearchRoute, + SettingsRoute: SettingsRoute, + ShopRoute: ShopRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..5c2b7b52d0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/__root.tsx @@ -0,0 +1,44 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/about.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/about.tsx new file mode 100644 index 0000000000..254ecca29c --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const AboutPage = Vue.defineComponent({ + setup() { + return () =>

About

+ }, +}) + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.$slug.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.$slug.tsx new file mode 100644 index 0000000000..3b2e855a6e --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.$slug.tsx @@ -0,0 +1,18 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { articleData } from '../../../shared' + +const ArticlePage = Vue.defineComponent({ + setup() { + const article = Route.useLoaderData() + + return () => ( +
{`${article.value.title} (${article.value.words} words)`}
+ ) + }, +}) + +export const Route = createFileRoute('/blog/$slug')({ + loader: ({ params }) => articleData(params.slug), + component: ArticlePage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.index.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.index.tsx new file mode 100644 index 0000000000..d3c2bd0305 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const BlogIndexPage = Vue.defineComponent({ + setup() { + return () =>

All articles

+ }, +}) + +export const Route = createFileRoute('/blog/')({ + component: BlogIndexPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.tsx new file mode 100644 index 0000000000..fe9532cc6f --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/blog.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { blogContext } from '../../../shared' + +const BlogLayout = Vue.defineComponent({ + setup() { + return () => ( +
+

Blog

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/blog')({ + beforeLoad: () => blogContext(), + component: BlogLayout, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/index.tsx new file mode 100644 index 0000000000..b0be4d78bb --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/index.tsx @@ -0,0 +1,27 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeItems, readyTestId } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + const items = Route.useLoaderData() + + return () => ( +
+

{`Home (${items.value.length} items)`}

+
    + {items.value.map((item) => ( +
  • {`${item.label}: ${item.score}`}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + loader: () => homeItems(), + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/search.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/search.tsx new file mode 100644 index 0000000000..09fdb29c1e --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/search.tsx @@ -0,0 +1,21 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { normalizePage, normalizeQuery } from '../../../shared' + +const SearchPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + + return () => ( +

{`Results for "${search.value.q}" page ${search.value.page}`}

+ ) + }, +}) + +export const Route = createFileRoute('/search')({ + validateSearch: (search: Record) => ({ + q: normalizeQuery(search.q), + page: normalizePage(search.page), + }), + component: SearchPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/settings.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/settings.tsx new file mode 100644 index 0000000000..3fc0485606 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const SettingsPage = Vue.defineComponent({ + setup() { + return () =>

Settings

+ }, +}) + +export const Route = createFileRoute('/settings')({ + component: SettingsPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.$productId.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.$productId.tsx new file mode 100644 index 0000000000..897559599a --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.$productId.tsx @@ -0,0 +1,18 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { productData } from '../../../shared' + +const ProductPage = Vue.defineComponent({ + setup() { + const product = Route.useLoaderData() + + return () => ( +
{`${product.value.name}: ${product.value.price}`}
+ ) + }, +}) + +export const Route = createFileRoute('/shop/$productId')({ + loader: ({ params }) => productData(params.productId), + component: ProductPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.index.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.index.tsx new file mode 100644 index 0000000000..1040520f03 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const ShopIndexPage = Vue.defineComponent({ + setup() { + return () =>

All products

+ }, +}) + +export const Route = createFileRoute('/shop/')({ + component: ShopIndexPage, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.tsx b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.tsx new file mode 100644 index 0000000000..184807cfa0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/src/routes/shop.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { shopContext } from '../../../shared' + +const ShopLayout = Vue.defineComponent({ + setup() { + return () => ( +
+

Shop

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/shop')({ + beforeLoad: () => shopContext(), + component: ShopLayout, +}) diff --git a/benchmarks/client-nav/scenarios/mount/vue/tsconfig.json b/benchmarks/client-nav/scenarios/mount/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/mount/vue/vite.config.ts b/benchmarks/client-nav/scenarios/mount/vue/vite.config.ts new file mode 100644 index 0000000000..107e1037b6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/mount/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav mount (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/react/project.json b/benchmarks/client-nav/scenarios/nested-params/react/project.json new file mode 100644 index 0000000000..800f8af9a8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-nested-params-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/nested-params/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/setup.ts b/benchmarks/client-nav/scenarios/nested-params/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts new file mode 100644 index 0000000000..321b1ad230 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-nested-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-nested-params navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/react/speed.flame.ts b/benchmarks/client-nav/scenarios/nested-params/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/nested-params/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..95d4b46623 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routeTree.gen.ts @@ -0,0 +1,291 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as LARouteImport } from './routes/l.$a' +import { Route as LABRouteImport } from './routes/l.$a.$b' +import { Route as LABCRouteImport } from './routes/l.$a.$b.$c' +import { Route as LABCDRouteImport } from './routes/l.$a.$b.$c.$d' +import { Route as LABCDERouteImport } from './routes/l.$a.$b.$c.$d.$e' +import { Route as LABCDEFRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f' +import { Route as LABCDEFGRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g' +import { Route as LABCDEFGHRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g.$h' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const LARoute = LARouteImport.update({ + id: '/l/$a', + path: '/l/$a', + getParentRoute: () => rootRouteImport, +} as any) +const LABRoute = LABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => LARoute, +} as any) +const LABCRoute = LABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => LABRoute, +} as any) +const LABCDRoute = LABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => LABCRoute, +} as any) +const LABCDERoute = LABCDERouteImport.update({ + id: '/$e', + path: '/$e', + getParentRoute: () => LABCDRoute, +} as any) +const LABCDEFRoute = LABCDEFRouteImport.update({ + id: '/$f', + path: '/$f', + getParentRoute: () => LABCDERoute, +} as any) +const LABCDEFGRoute = LABCDEFGRouteImport.update({ + id: '/$g', + path: '/$g', + getParentRoute: () => LABCDEFRoute, +} as any) +const LABCDEFGHRoute = LABCDEFGHRouteImport.update({ + id: '/$h', + path: '/$h', + getParentRoute: () => LABCDEFGRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + id: + | '__root__' + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LARoute: typeof LARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a': { + id: '/l/$a' + path: '/l/$a' + fullPath: '/l/$a' + preLoaderRoute: typeof LARouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a/$b': { + id: '/l/$a/$b' + path: '/$b' + fullPath: '/l/$a/$b' + preLoaderRoute: typeof LABRouteImport + parentRoute: typeof LARoute + } + '/l/$a/$b/$c': { + id: '/l/$a/$b/$c' + path: '/$c' + fullPath: '/l/$a/$b/$c' + preLoaderRoute: typeof LABCRouteImport + parentRoute: typeof LABRoute + } + '/l/$a/$b/$c/$d': { + id: '/l/$a/$b/$c/$d' + path: '/$d' + fullPath: '/l/$a/$b/$c/$d' + preLoaderRoute: typeof LABCDRouteImport + parentRoute: typeof LABCRoute + } + '/l/$a/$b/$c/$d/$e': { + id: '/l/$a/$b/$c/$d/$e' + path: '/$e' + fullPath: '/l/$a/$b/$c/$d/$e' + preLoaderRoute: typeof LABCDERouteImport + parentRoute: typeof LABCDRoute + } + '/l/$a/$b/$c/$d/$e/$f': { + id: '/l/$a/$b/$c/$d/$e/$f' + path: '/$f' + fullPath: '/l/$a/$b/$c/$d/$e/$f' + preLoaderRoute: typeof LABCDEFRouteImport + parentRoute: typeof LABCDERoute + } + '/l/$a/$b/$c/$d/$e/$f/$g': { + id: '/l/$a/$b/$c/$d/$e/$f/$g' + path: '/$g' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g' + preLoaderRoute: typeof LABCDEFGRouteImport + parentRoute: typeof LABCDEFRoute + } + '/l/$a/$b/$c/$d/$e/$f/$g/$h': { + id: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + path: '/$h' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + preLoaderRoute: typeof LABCDEFGHRouteImport + parentRoute: typeof LABCDEFGRoute + } + } +} + +interface LABCDEFGRouteChildren { + LABCDEFGHRoute: typeof LABCDEFGHRoute +} + +const LABCDEFGRouteChildren: LABCDEFGRouteChildren = { + LABCDEFGHRoute: LABCDEFGHRoute, +} + +const LABCDEFGRouteWithChildren = LABCDEFGRoute._addFileChildren( + LABCDEFGRouteChildren, +) + +interface LABCDEFRouteChildren { + LABCDEFGRoute: typeof LABCDEFGRouteWithChildren +} + +const LABCDEFRouteChildren: LABCDEFRouteChildren = { + LABCDEFGRoute: LABCDEFGRouteWithChildren, +} + +const LABCDEFRouteWithChildren = + LABCDEFRoute._addFileChildren(LABCDEFRouteChildren) + +interface LABCDERouteChildren { + LABCDEFRoute: typeof LABCDEFRouteWithChildren +} + +const LABCDERouteChildren: LABCDERouteChildren = { + LABCDEFRoute: LABCDEFRouteWithChildren, +} + +const LABCDERouteWithChildren = + LABCDERoute._addFileChildren(LABCDERouteChildren) + +interface LABCDRouteChildren { + LABCDERoute: typeof LABCDERouteWithChildren +} + +const LABCDRouteChildren: LABCDRouteChildren = { + LABCDERoute: LABCDERouteWithChildren, +} + +const LABCDRouteWithChildren = LABCDRoute._addFileChildren(LABCDRouteChildren) + +interface LABCRouteChildren { + LABCDRoute: typeof LABCDRouteWithChildren +} + +const LABCRouteChildren: LABCRouteChildren = { + LABCDRoute: LABCDRouteWithChildren, +} + +const LABCRouteWithChildren = LABCRoute._addFileChildren(LABCRouteChildren) + +interface LABRouteChildren { + LABCRoute: typeof LABCRouteWithChildren +} + +const LABRouteChildren: LABRouteChildren = { + LABCRoute: LABCRouteWithChildren, +} + +const LABRouteWithChildren = LABRoute._addFileChildren(LABRouteChildren) + +interface LARouteChildren { + LABRoute: typeof LABRouteWithChildren +} + +const LARouteChildren: LARouteChildren = { + LABRoute: LABRouteWithChildren, +} + +const LARouteWithChildren = LARoute._addFileChildren(LARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LARoute: LARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/__root.tsx new file mode 100644 index 0000000000..ef2c7b1b18 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/__root.tsx @@ -0,0 +1,50 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' +import { leafParamSets, midParams } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/index.tsx new file mode 100644 index 0000000000..24e856ea15 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx new file mode 100644 index 0000000000..9789e28040 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx @@ -0,0 +1,63 @@ +import { createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g/$h')({ + params: { + parse: (params) => ({ ...params, h: normalizeParam(params.h) }), + stringify: (params) => ({ ...params, h: String(params.h) }), + }, + beforeLoad: ({ params }) => ({ ctxH: smallHash(params.h) }), + component: LevelH, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.h) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.h}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxH }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxH * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelH() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[ + params.a, + params.b, + params.c, + params.d, + params.e, + params.f, + params.g, + params.h, + ].join('.')} +
+ + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx new file mode 100644 index 0000000000..650b32ce96 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g')({ + params: { + parse: (params) => ({ ...params, g: normalizeParam(params.g) }), + stringify: (params) => ({ ...params, g: String(params.g) }), + }, + beforeLoad: ({ params }) => ({ ctxG: smallHash(params.g) }), + component: LevelG, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.g) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.g}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxG }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxG * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelG() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx new file mode 100644 index 0000000000..ca18e3ccd2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.$f.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f')({ + params: { + parse: (params) => ({ ...params, f: normalizeParam(params.f) }), + stringify: (params) => ({ ...params, f: String(params.f) }), + }, + beforeLoad: ({ params }) => ({ ctxF: smallHash(params.f) }), + component: LevelF, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.f) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.f}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxF }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxF * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelF() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.tsx new file mode 100644 index 0000000000..782c1808c7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.$e.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e')({ + params: { + parse: (params) => ({ ...params, e: normalizeParam(params.e) }), + stringify: (params) => ({ ...params, e: String(params.e) }), + }, + beforeLoad: ({ params }) => ({ ctxE: smallHash(params.e) }), + component: LevelE, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.e) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.e}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxE }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxE * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelE() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.tsx new file mode 100644 index 0000000000..a874bbcb63 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.$d.tsx @@ -0,0 +1,55 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d')({ + params: { + parse: (params) => ({ ...params, d: normalizeParam(params.d) }), + stringify: (params) => ({ ...params, d: String(params.d) }), + }, + beforeLoad: ({ params }) => ({ ctxD: smallHash(params.d) }), + component: LevelD, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.d) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.d}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxD }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxD * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelD() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[params.a, params.b, params.c, params.d].join('.')} +
+ + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.tsx new file mode 100644 index 0000000000..94aba79453 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.$c.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c')({ + params: { + parse: (params) => ({ ...params, c: normalizeParam(params.c) }), + stringify: (params) => ({ ...params, c: String(params.c) }), + }, + beforeLoad: ({ params }) => ({ ctxC: smallHash(params.c) }), + component: LevelC, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.c) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.c}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxC }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxC * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelC() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.tsx new file mode 100644 index 0000000000..1960b68276 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.$b.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b')({ + params: { + parse: (params) => ({ ...params, b: normalizeParam(params.b) }), + stringify: (params) => ({ ...params, b: String(params.b) }), + }, + beforeLoad: ({ params }) => ({ ctxB: smallHash(params.b) }), + component: LevelB, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.b) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.b}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxB }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxB * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelB() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.tsx b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.tsx new file mode 100644 index 0000000000..85d1de6f68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/src/routes/l.$a.tsx @@ -0,0 +1,51 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a')({ + params: { + parse: (params) => ({ ...params, a: normalizeParam(params.a) }), + stringify: (params) => ({ ...params, a: String(params.a) }), + }, + beforeLoad: ({ params }) => ({ ctxA: smallHash(params.a) }), + component: LevelA, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.a) }) + void value + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.a}:2`), + }) + void value + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxA }) + void value + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxA * 31 + 7) >>> 0, + }) + void value + return null +} + +function LevelA() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/tsconfig.json b/benchmarks/client-nav/scenarios/nested-params/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/nested-params/react/vite.config.ts b/benchmarks/client-nav/scenarios/nested-params/react/vite.config.ts new file mode 100644 index 0000000000..672b1850ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav nested-params (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/shared.ts b/benchmarks/client-nav/scenarios/nested-params/shared.ts new file mode 100644 index 0000000000..06c968bb18 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/shared.ts @@ -0,0 +1,143 @@ +/** + * Shared definition of the `nested-params` scenario: deep dynamic nesting with + * per-level params.parse/stringify, beforeLoad context accumulation, and + * useParams/useRouteContext subscribers at every level. The three framework + * apps consume these builders so the workload is identical modulo the + * rendering layer. + */ + +export interface LeafParams { + a: string + b: string + c: string + d: string + e: string + f: string + g: string + h: string +} + +// Several values need percent-encoding in the URL (spaces, `&`, `%`, `+`, +// unicode) so every navigation exercises the param encode/decode paths, not +// just plain-ASCII interpolation. +const baseParams: LeafParams = { + a: 'alpha', + b: 'bravo & co', + c: 'charlie 100%', + d: 'delta', + e: 'écho-café', + f: 'foxtrot+one', + g: 'golf', + h: 'hôtel', +} + +/** + * set 0: base; set 1: only the deepest param differs (only the leaf re-runs); + * set 2: a mid param differs (levels d..h re-run); set 3: the top param + * differs (every level re-runs). + */ +export const leafParamSets: ReadonlyArray = [ + baseParams, + { ...baseParams, h: 'hilton' }, + { ...baseParams, d: 'dover' }, + { ...baseParams, a: 'axel' }, +] + +export const midParams = { + a: baseParams.a, + b: baseParams.b, + c: baseParams.c, + d: baseParams.d, +} + +export function normalizeParam(value: unknown) { + const raw = String(value) + return raw.length > 24 ? raw.slice(0, 24) : raw +} + +export function smallHash(value: string | number) { + const input = `${value}` + let hash = 5381 + for (let index = 0; index < input.length; index++) { + hash = ((hash * 33) ^ input.charCodeAt(index)) >>> 0 + } + return hash +} + +export function leafMarker(params: LeafParams) { + return [ + params.a, + params.b, + params.c, + params.d, + params.e, + params.f, + params.g, + params.h, + ] + .map(normalizeParam) + .join('.') +} + +export function midMarker(params: typeof midParams) { + return [params.a, params.b, params.c, params.d].map(normalizeParam).join('.') +} + +export const steps = [ + 'leaf-1', + 'leaf-2', + 'leaf-3', + 'leaf-4', + 'mid-4', + 'home', +] as const + +function requireText(container: HTMLElement, testId: string, expected: string) { + const element = container.querySelector(`[data-testid="${testId}"]`) + if (!element || element.textContent !== expected) { + throw new Error( + `Expected [data-testid="${testId}"] to render "${expected}", received "${element?.textContent ?? ''}"`, + ) + } +} + +function requireAbsent(container: HTMLElement, testId: string) { + if (container.querySelector(`[data-testid="${testId}"]`)) { + throw new Error(`Expected [data-testid="${testId}"] to be absent`) + } +} + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const step = steps[stepIndex % steps.length]! + switch (step) { + case 'leaf-1': + requireText(container, 'leaf-state', leafMarker(leafParamSets[0]!)) + break + case 'leaf-2': + requireText(container, 'leaf-state', leafMarker(leafParamSets[1]!)) + break + case 'leaf-3': + requireText(container, 'leaf-state', leafMarker(leafParamSets[2]!)) + break + case 'leaf-4': + requireText(container, 'leaf-state', leafMarker(leafParamSets[3]!)) + break + case 'mid-4': + requireAbsent(container, 'leaf-state') + requireText(container, 'mid-state', midMarker(midParams)) + break + case 'home': + requireAbsent(container, 'leaf-state') + requireAbsent(container, 'mid-state') + break + } +} + +// Three laps through the 6-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/project.json b/benchmarks/client-nav/scenarios/nested-params/solid/project.json new file mode 100644 index 0000000000..eb0b91bd6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-nested-params-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/nested-params/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/setup.ts b/benchmarks/client-nav/scenarios/nested-params/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts new file mode 100644 index 0000000000..d35980f612 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-nested-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-nested-params navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/nested-params/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx new file mode 100644 index 0000000000..b43608e191 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/nested-params/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..9065c9294f --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routeTree.gen.ts @@ -0,0 +1,291 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as LARouteImport } from './routes/l.$a' +import { Route as LABRouteImport } from './routes/l.$a.$b' +import { Route as LABCRouteImport } from './routes/l.$a.$b.$c' +import { Route as LABCDRouteImport } from './routes/l.$a.$b.$c.$d' +import { Route as LABCDERouteImport } from './routes/l.$a.$b.$c.$d.$e' +import { Route as LABCDEFRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f' +import { Route as LABCDEFGRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g' +import { Route as LABCDEFGHRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g.$h' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const LARoute = LARouteImport.update({ + id: '/l/$a', + path: '/l/$a', + getParentRoute: () => rootRouteImport, +} as any) +const LABRoute = LABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => LARoute, +} as any) +const LABCRoute = LABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => LABRoute, +} as any) +const LABCDRoute = LABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => LABCRoute, +} as any) +const LABCDERoute = LABCDERouteImport.update({ + id: '/$e', + path: '/$e', + getParentRoute: () => LABCDRoute, +} as any) +const LABCDEFRoute = LABCDEFRouteImport.update({ + id: '/$f', + path: '/$f', + getParentRoute: () => LABCDERoute, +} as any) +const LABCDEFGRoute = LABCDEFGRouteImport.update({ + id: '/$g', + path: '/$g', + getParentRoute: () => LABCDEFRoute, +} as any) +const LABCDEFGHRoute = LABCDEFGHRouteImport.update({ + id: '/$h', + path: '/$h', + getParentRoute: () => LABCDEFGRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + id: + | '__root__' + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LARoute: typeof LARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a': { + id: '/l/$a' + path: '/l/$a' + fullPath: '/l/$a' + preLoaderRoute: typeof LARouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a/$b': { + id: '/l/$a/$b' + path: '/$b' + fullPath: '/l/$a/$b' + preLoaderRoute: typeof LABRouteImport + parentRoute: typeof LARoute + } + '/l/$a/$b/$c': { + id: '/l/$a/$b/$c' + path: '/$c' + fullPath: '/l/$a/$b/$c' + preLoaderRoute: typeof LABCRouteImport + parentRoute: typeof LABRoute + } + '/l/$a/$b/$c/$d': { + id: '/l/$a/$b/$c/$d' + path: '/$d' + fullPath: '/l/$a/$b/$c/$d' + preLoaderRoute: typeof LABCDRouteImport + parentRoute: typeof LABCRoute + } + '/l/$a/$b/$c/$d/$e': { + id: '/l/$a/$b/$c/$d/$e' + path: '/$e' + fullPath: '/l/$a/$b/$c/$d/$e' + preLoaderRoute: typeof LABCDERouteImport + parentRoute: typeof LABCDRoute + } + '/l/$a/$b/$c/$d/$e/$f': { + id: '/l/$a/$b/$c/$d/$e/$f' + path: '/$f' + fullPath: '/l/$a/$b/$c/$d/$e/$f' + preLoaderRoute: typeof LABCDEFRouteImport + parentRoute: typeof LABCDERoute + } + '/l/$a/$b/$c/$d/$e/$f/$g': { + id: '/l/$a/$b/$c/$d/$e/$f/$g' + path: '/$g' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g' + preLoaderRoute: typeof LABCDEFGRouteImport + parentRoute: typeof LABCDEFRoute + } + '/l/$a/$b/$c/$d/$e/$f/$g/$h': { + id: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + path: '/$h' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + preLoaderRoute: typeof LABCDEFGHRouteImport + parentRoute: typeof LABCDEFGRoute + } + } +} + +interface LABCDEFGRouteChildren { + LABCDEFGHRoute: typeof LABCDEFGHRoute +} + +const LABCDEFGRouteChildren: LABCDEFGRouteChildren = { + LABCDEFGHRoute: LABCDEFGHRoute, +} + +const LABCDEFGRouteWithChildren = LABCDEFGRoute._addFileChildren( + LABCDEFGRouteChildren, +) + +interface LABCDEFRouteChildren { + LABCDEFGRoute: typeof LABCDEFGRouteWithChildren +} + +const LABCDEFRouteChildren: LABCDEFRouteChildren = { + LABCDEFGRoute: LABCDEFGRouteWithChildren, +} + +const LABCDEFRouteWithChildren = + LABCDEFRoute._addFileChildren(LABCDEFRouteChildren) + +interface LABCDERouteChildren { + LABCDEFRoute: typeof LABCDEFRouteWithChildren +} + +const LABCDERouteChildren: LABCDERouteChildren = { + LABCDEFRoute: LABCDEFRouteWithChildren, +} + +const LABCDERouteWithChildren = + LABCDERoute._addFileChildren(LABCDERouteChildren) + +interface LABCDRouteChildren { + LABCDERoute: typeof LABCDERouteWithChildren +} + +const LABCDRouteChildren: LABCDRouteChildren = { + LABCDERoute: LABCDERouteWithChildren, +} + +const LABCDRouteWithChildren = LABCDRoute._addFileChildren(LABCDRouteChildren) + +interface LABCRouteChildren { + LABCDRoute: typeof LABCDRouteWithChildren +} + +const LABCRouteChildren: LABCRouteChildren = { + LABCDRoute: LABCDRouteWithChildren, +} + +const LABCRouteWithChildren = LABCRoute._addFileChildren(LABCRouteChildren) + +interface LABRouteChildren { + LABCRoute: typeof LABCRouteWithChildren +} + +const LABRouteChildren: LABRouteChildren = { + LABCRoute: LABCRouteWithChildren, +} + +const LABRouteWithChildren = LABRoute._addFileChildren(LABRouteChildren) + +interface LARouteChildren { + LABRoute: typeof LABRouteWithChildren +} + +const LARouteChildren: LARouteChildren = { + LABRoute: LABRouteWithChildren, +} + +const LARouteWithChildren = LARoute._addFileChildren(LARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LARoute: LARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..6fe7ab3e3d --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/__root.tsx @@ -0,0 +1,50 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +import { leafParamSets, midParams } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/index.tsx new file mode 100644 index 0000000000..cc46764de5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx new file mode 100644 index 0000000000..fccc6ca7bb --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx @@ -0,0 +1,84 @@ +import { createRenderEffect } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g/$h')({ + params: { + parse: (params) => ({ ...params, h: normalizeParam(params.h) }), + stringify: (params) => ({ ...params, h: String(params.h) }), + }, + beforeLoad: ({ params }) => ({ ctxH: smallHash(params.h) }), + component: LevelH, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.h) }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.h}:2`), + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxH }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxH * 31 + 7) >>> 0, + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function LevelH() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[ + params().a, + params().b, + params().c, + params().d, + params().e, + params().f, + params().g, + params().h, + ].join('.')} +
+ + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx new file mode 100644 index 0000000000..7274a19aac --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx @@ -0,0 +1,72 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g')({ + params: { + parse: (params) => ({ ...params, g: normalizeParam(params.g) }), + stringify: (params) => ({ ...params, g: String(params.g) }), + }, + beforeLoad: ({ params }) => ({ ctxG: smallHash(params.g) }), + component: LevelG, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.g) }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.g}:2`), + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxG }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxG * 31 + 7) >>> 0, + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function LevelG() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx new file mode 100644 index 0000000000..a7ec5597f7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.$f.tsx @@ -0,0 +1,72 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f')({ + params: { + parse: (params) => ({ ...params, f: normalizeParam(params.f) }), + stringify: (params) => ({ ...params, f: String(params.f) }), + }, + beforeLoad: ({ params }) => ({ ctxF: smallHash(params.f) }), + component: LevelF, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.f) }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.f}:2`), + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxF }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxF * 31 + 7) >>> 0, + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function LevelF() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.tsx new file mode 100644 index 0000000000..79c6b118e0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.$e.tsx @@ -0,0 +1,72 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e')({ + params: { + parse: (params) => ({ ...params, e: normalizeParam(params.e) }), + stringify: (params) => ({ ...params, e: String(params.e) }), + }, + beforeLoad: ({ params }) => ({ ctxE: smallHash(params.e) }), + component: LevelE, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.e) }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.e}:2`), + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxE }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxE * 31 + 7) >>> 0, + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function LevelE() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.tsx new file mode 100644 index 0000000000..e0dc4c10d2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.$d.tsx @@ -0,0 +1,76 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c/$d')({ + params: { + parse: (params) => ({ ...params, d: normalizeParam(params.d) }), + stringify: (params) => ({ ...params, d: String(params.d) }), + }, + beforeLoad: ({ params }) => ({ ctxD: smallHash(params.d) }), + component: LevelD, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.d) }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.d}:2`), + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxD }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxD * 31 + 7) >>> 0, + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function LevelD() { + const params = Route.useParams() + return ( + <> + + + + +
+ {[params().a, params().b, params().c, params().d].join('.')} +
+ + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.tsx new file mode 100644 index 0000000000..3a0df7eb71 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.$c.tsx @@ -0,0 +1,72 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b/$c')({ + params: { + parse: (params) => ({ ...params, c: normalizeParam(params.c) }), + stringify: (params) => ({ ...params, c: String(params.c) }), + }, + beforeLoad: ({ params }) => ({ ctxC: smallHash(params.c) }), + component: LevelC, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.c) }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.c}:2`), + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxC }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxC * 31 + 7) >>> 0, + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function LevelC() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.tsx new file mode 100644 index 0000000000..6133e72b48 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.$b.tsx @@ -0,0 +1,72 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a/$b')({ + params: { + parse: (params) => ({ ...params, b: normalizeParam(params.b) }), + stringify: (params) => ({ ...params, b: String(params.b) }), + }, + beforeLoad: ({ params }) => ({ ctxB: smallHash(params.b) }), + component: LevelB, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.b) }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.b}:2`), + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxB }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxB * 31 + 7) >>> 0, + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function LevelB() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.tsx b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.tsx new file mode 100644 index 0000000000..7bd378431b --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/src/routes/l.$a.tsx @@ -0,0 +1,72 @@ +import { createRenderEffect } from 'solid-js' +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { normalizeParam, smallHash } from '../../../shared' + +export const Route = createFileRoute('/l/$a')({ + params: { + parse: (params) => ({ ...params, a: normalizeParam(params.a) }), + stringify: (params) => ({ ...params, a: String(params.a) }), + }, + beforeLoad: ({ params }) => ({ ctxA: smallHash(params.a) }), + component: LevelA, +}) + +function ParamsSubscriberOne() { + const value = Route.useParams({ select: (params) => smallHash(params.a) }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ParamsSubscriberTwo() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.a}:2`), + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberOne() { + const value = Route.useRouteContext({ select: (context) => context.ctxA }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function ContextSubscriberTwo() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxA * 31 + 7) >>> 0, + }) + createRenderEffect( + () => { + void value() + }, + () => {}, + ) + return null +} + +function LevelA() { + return ( + <> + + + + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/tsconfig.json b/benchmarks/client-nav/scenarios/nested-params/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/nested-params/solid/vite.config.ts b/benchmarks/client-nav/scenarios/nested-params/solid/vite.config.ts new file mode 100644 index 0000000000..f5c9e13c0f --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav nested-params (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/project.json b/benchmarks/client-nav/scenarios/nested-params/vue/project.json new file mode 100644 index 0000000000..83c234382c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-nested-params-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/nested-params/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/setup.ts b/benchmarks/client-nav/scenarios/nested-params/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts new file mode 100644 index 0000000000..fac8703c34 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-nested-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-nested-params navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/nested-params/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/nested-params/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..a7d1049b49 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routeTree.gen.ts @@ -0,0 +1,291 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as LARouteImport } from './routes/l.$a' +import { Route as LABRouteImport } from './routes/l.$a.$b' +import { Route as LABCRouteImport } from './routes/l.$a.$b.$c' +import { Route as LABCDRouteImport } from './routes/l.$a.$b.$c.$d' +import { Route as LABCDERouteImport } from './routes/l.$a.$b.$c.$d.$e' +import { Route as LABCDEFRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f' +import { Route as LABCDEFGRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g' +import { Route as LABCDEFGHRouteImport } from './routes/l.$a.$b.$c.$d.$e.$f.$g.$h' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const LARoute = LARouteImport.update({ + id: '/l/$a', + path: '/l/$a', + getParentRoute: () => rootRouteImport, +} as any) +const LABRoute = LABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => LARoute, +} as any) +const LABCRoute = LABCRouteImport.update({ + id: '/$c', + path: '/$c', + getParentRoute: () => LABRoute, +} as any) +const LABCDRoute = LABCDRouteImport.update({ + id: '/$d', + path: '/$d', + getParentRoute: () => LABCRoute, +} as any) +const LABCDERoute = LABCDERouteImport.update({ + id: '/$e', + path: '/$e', + getParentRoute: () => LABCDRoute, +} as any) +const LABCDEFRoute = LABCDEFRouteImport.update({ + id: '/$f', + path: '/$f', + getParentRoute: () => LABCDERoute, +} as any) +const LABCDEFGRoute = LABCDEFGRouteImport.update({ + id: '/$g', + path: '/$g', + getParentRoute: () => LABCDEFRoute, +} as any) +const LABCDEFGHRoute = LABCDEFGHRouteImport.update({ + id: '/$h', + path: '/$h', + getParentRoute: () => LABCDEFGRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l/$a': typeof LARouteWithChildren + '/l/$a/$b': typeof LABRouteWithChildren + '/l/$a/$b/$c': typeof LABCRouteWithChildren + '/l/$a/$b/$c/$d': typeof LABCDRouteWithChildren + '/l/$a/$b/$c/$d/$e': typeof LABCDERouteWithChildren + '/l/$a/$b/$c/$d/$e/$f': typeof LABCDEFRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g': typeof LABCDEFGRouteWithChildren + '/l/$a/$b/$c/$d/$e/$f/$g/$h': typeof LABCDEFGHRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + id: + | '__root__' + | '/' + | '/l/$a' + | '/l/$a/$b' + | '/l/$a/$b/$c' + | '/l/$a/$b/$c/$d' + | '/l/$a/$b/$c/$d/$e' + | '/l/$a/$b/$c/$d/$e/$f' + | '/l/$a/$b/$c/$d/$e/$f/$g' + | '/l/$a/$b/$c/$d/$e/$f/$g/$h' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LARoute: typeof LARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a': { + id: '/l/$a' + path: '/l/$a' + fullPath: '/l/$a' + preLoaderRoute: typeof LARouteImport + parentRoute: typeof rootRouteImport + } + '/l/$a/$b': { + id: '/l/$a/$b' + path: '/$b' + fullPath: '/l/$a/$b' + preLoaderRoute: typeof LABRouteImport + parentRoute: typeof LARoute + } + '/l/$a/$b/$c': { + id: '/l/$a/$b/$c' + path: '/$c' + fullPath: '/l/$a/$b/$c' + preLoaderRoute: typeof LABCRouteImport + parentRoute: typeof LABRoute + } + '/l/$a/$b/$c/$d': { + id: '/l/$a/$b/$c/$d' + path: '/$d' + fullPath: '/l/$a/$b/$c/$d' + preLoaderRoute: typeof LABCDRouteImport + parentRoute: typeof LABCRoute + } + '/l/$a/$b/$c/$d/$e': { + id: '/l/$a/$b/$c/$d/$e' + path: '/$e' + fullPath: '/l/$a/$b/$c/$d/$e' + preLoaderRoute: typeof LABCDERouteImport + parentRoute: typeof LABCDRoute + } + '/l/$a/$b/$c/$d/$e/$f': { + id: '/l/$a/$b/$c/$d/$e/$f' + path: '/$f' + fullPath: '/l/$a/$b/$c/$d/$e/$f' + preLoaderRoute: typeof LABCDEFRouteImport + parentRoute: typeof LABCDERoute + } + '/l/$a/$b/$c/$d/$e/$f/$g': { + id: '/l/$a/$b/$c/$d/$e/$f/$g' + path: '/$g' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g' + preLoaderRoute: typeof LABCDEFGRouteImport + parentRoute: typeof LABCDEFRoute + } + '/l/$a/$b/$c/$d/$e/$f/$g/$h': { + id: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + path: '/$h' + fullPath: '/l/$a/$b/$c/$d/$e/$f/$g/$h' + preLoaderRoute: typeof LABCDEFGHRouteImport + parentRoute: typeof LABCDEFGRoute + } + } +} + +interface LABCDEFGRouteChildren { + LABCDEFGHRoute: typeof LABCDEFGHRoute +} + +const LABCDEFGRouteChildren: LABCDEFGRouteChildren = { + LABCDEFGHRoute: LABCDEFGHRoute, +} + +const LABCDEFGRouteWithChildren = LABCDEFGRoute._addFileChildren( + LABCDEFGRouteChildren, +) + +interface LABCDEFRouteChildren { + LABCDEFGRoute: typeof LABCDEFGRouteWithChildren +} + +const LABCDEFRouteChildren: LABCDEFRouteChildren = { + LABCDEFGRoute: LABCDEFGRouteWithChildren, +} + +const LABCDEFRouteWithChildren = + LABCDEFRoute._addFileChildren(LABCDEFRouteChildren) + +interface LABCDERouteChildren { + LABCDEFRoute: typeof LABCDEFRouteWithChildren +} + +const LABCDERouteChildren: LABCDERouteChildren = { + LABCDEFRoute: LABCDEFRouteWithChildren, +} + +const LABCDERouteWithChildren = + LABCDERoute._addFileChildren(LABCDERouteChildren) + +interface LABCDRouteChildren { + LABCDERoute: typeof LABCDERouteWithChildren +} + +const LABCDRouteChildren: LABCDRouteChildren = { + LABCDERoute: LABCDERouteWithChildren, +} + +const LABCDRouteWithChildren = LABCDRoute._addFileChildren(LABCDRouteChildren) + +interface LABCRouteChildren { + LABCDRoute: typeof LABCDRouteWithChildren +} + +const LABCRouteChildren: LABCRouteChildren = { + LABCDRoute: LABCDRouteWithChildren, +} + +const LABCRouteWithChildren = LABCRoute._addFileChildren(LABCRouteChildren) + +interface LABRouteChildren { + LABCRoute: typeof LABCRouteWithChildren +} + +const LABRouteChildren: LABRouteChildren = { + LABCRoute: LABCRouteWithChildren, +} + +const LABRouteWithChildren = LABRoute._addFileChildren(LABRouteChildren) + +interface LARouteChildren { + LABRoute: typeof LABRouteWithChildren +} + +const LARouteChildren: LARouteChildren = { + LABRoute: LABRouteWithChildren, +} + +const LARouteWithChildren = LARoute._addFileChildren(LARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LARoute: LARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..28dd4e847a --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/__root.tsx @@ -0,0 +1,53 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' +import { leafParamSets, midParams } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f3ffc58610 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () =>
home
+ }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx new file mode 100644 index 0000000000..756c31e1a9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.$h.tsx @@ -0,0 +1,86 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.h), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.h}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxH, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxH * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelH = Vue.defineComponent({ + setup() { + const params = Route.useParams() + return () => ( + <> + + + + +
+ {[ + params.value.a, + params.value.b, + params.value.c, + params.value.d, + params.value.e, + params.value.f, + params.value.g, + params.value.h, + ].join('.')} +
+ + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g/$h')({ + params: { + parse: (params) => ({ ...params, h: normalizeParam(params.h) }), + stringify: (params) => ({ ...params, h: String(params.h) }), + }, + beforeLoad: ({ params }) => ({ ctxH: smallHash(params.h) }), + component: LevelH, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx new file mode 100644 index 0000000000..d6b9ccde83 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.$g.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.g), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.g}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxG, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxG * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelG = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f/$g')({ + params: { + parse: (params) => ({ ...params, g: normalizeParam(params.g) }), + stringify: (params) => ({ ...params, g: String(params.g) }), + }, + beforeLoad: ({ params }) => ({ ctxG: smallHash(params.g) }), + component: LevelG, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx new file mode 100644 index 0000000000..76ed24904c --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.$f.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.f), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.f}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxF, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxF * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelF = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e/$f')({ + params: { + parse: (params) => ({ ...params, f: normalizeParam(params.f) }), + stringify: (params) => ({ ...params, f: String(params.f) }), + }, + beforeLoad: ({ params }) => ({ ctxF: smallHash(params.f) }), + component: LevelF, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.tsx new file mode 100644 index 0000000000..e24d3dc373 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.$e.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.e), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.e}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxE, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxE * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelE = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d/$e')({ + params: { + parse: (params) => ({ ...params, e: normalizeParam(params.e) }), + stringify: (params) => ({ ...params, e: String(params.e) }), + }, + beforeLoad: ({ params }) => ({ ctxE: smallHash(params.e) }), + component: LevelE, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.tsx new file mode 100644 index 0000000000..7785638ee7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.$d.tsx @@ -0,0 +1,83 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.d), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.d}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxD, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxD * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelD = Vue.defineComponent({ + setup() { + const params = Route.useParams() + return () => ( + <> + + + + +
+ {[ + params.value.a, + params.value.b, + params.value.c, + params.value.d, + ].join('.')} +
+ + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c/$d')({ + params: { + parse: (params) => ({ ...params, d: normalizeParam(params.d) }), + stringify: (params) => ({ ...params, d: String(params.d) }), + }, + beforeLoad: ({ params }) => ({ ctxD: smallHash(params.d) }), + component: LevelD, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.tsx new file mode 100644 index 0000000000..0d0adc65ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.$c.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.c), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.c}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxC, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxC * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelC = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b/$c')({ + params: { + parse: (params) => ({ ...params, c: normalizeParam(params.c) }), + stringify: (params) => ({ ...params, c: String(params.c) }), + }, + beforeLoad: ({ params }) => ({ ctxC: smallHash(params.c) }), + component: LevelC, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.tsx new file mode 100644 index 0000000000..f013d865af --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.$b.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.b), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.b}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxB, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxB * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelB = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a/$b')({ + params: { + parse: (params) => ({ ...params, b: normalizeParam(params.b) }), + stringify: (params) => ({ ...params, b: String(params.b) }), + }, + beforeLoad: ({ params }) => ({ ctxB: smallHash(params.b) }), + component: LevelB, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.tsx b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.tsx new file mode 100644 index 0000000000..d5764c0985 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/src/routes/l.$a.tsx @@ -0,0 +1,74 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { normalizeParam, smallHash } from '../../../shared' + +const ParamsSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(params.a), + }) + return () => { + void value.value + return null + } + }, +}) + +const ParamsSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useParams({ + select: (params) => smallHash(`${params.a}:2`), + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberOne = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => context.ctxA, + }) + return () => { + void value.value + return null + } + }, +}) + +const ContextSubscriberTwo = Vue.defineComponent({ + setup() { + const value = Route.useRouteContext({ + select: (context) => (context.ctxA * 31 + 7) >>> 0, + }) + return () => { + void value.value + return null + } + }, +}) + +const LevelA = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + + + + ) + }, +}) + +export const Route = createFileRoute('/l/$a')({ + params: { + parse: (params) => ({ ...params, a: normalizeParam(params.a) }), + stringify: (params) => ({ ...params, a: String(params.a) }), + }, + beforeLoad: ({ params }) => ({ ctxA: smallHash(params.a) }), + component: LevelA, +}) diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/tsconfig.json b/benchmarks/client-nav/scenarios/nested-params/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/nested-params/vue/vite.config.ts b/benchmarks/client-nav/scenarios/nested-params/vue/vite.config.ts new file mode 100644 index 0000000000..59a4af48ef --- /dev/null +++ b/benchmarks/client-nav/scenarios/nested-params/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav nested-params (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/preload/react/project.json b/benchmarks/client-nav/scenarios/preload/react/project.json new file mode 100644 index 0000000000..f095228746 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-preload-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/preload/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/preload/react/setup.ts b/benchmarks/client-nav/scenarios/preload/react/setup.ts new file mode 100644 index 0000000000..5409df903c --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts new file mode 100644 index 0000000000..c869ebdcfb --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-preload', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-preload interaction loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/preload/react/speed.flame.ts b/benchmarks/client-nav/scenarios/preload/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/main.tsx b/benchmarks/client-nav/scenarios/preload/react/src/main.tsx new file mode 100644 index 0000000000..6f8942c118 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/main.tsx @@ -0,0 +1,38 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPreload: 'intent', + defaultPreloadDelay: 0, + defaultPreloadStaleTime: 0, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/preload/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..0c76e85974 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SectionsSectionRouteImport } from './routes/sections.$section' + +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SectionsSectionRoute = SectionsSectionRouteImport.update({ + id: '/sections/$section', + path: '/sections/$section', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/sections/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/sections/$section' + id: '__root__' | '/' | '/docs' | '/sections/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRoute + SectionsSectionRoute: typeof SectionsSectionRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sections/$section': { + id: '/sections/$section' + path: '/sections/$section' + fullPath: '/sections/$section' + preLoaderRoute: typeof SectionsSectionRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRoute, + SectionsSectionRoute: SectionsSectionRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/__root.tsx new file mode 100644 index 0000000000..b659fe4d26 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/__root.tsx @@ -0,0 +1,32 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' +import { sections } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/docs.tsx new file mode 100644 index 0000000000..1f87530bac --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/docs.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { docsMarker } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + component: DocsPage, +}) + +function DocsPage() { + return ( +
+

{docsMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/index.tsx new file mode 100644 index 0000000000..2b8c83ebab --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/src/routes/sections.$section.tsx b/benchmarks/client-nav/scenarios/preload/react/src/routes/sections.$section.tsx new file mode 100644 index 0000000000..42962b8be7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/src/routes/sections.$section.tsx @@ -0,0 +1,32 @@ +import { createFileRoute } from '@tanstack/react-router' +import { sectionItems } from '../../../shared' + +export const Route = createFileRoute('/sections/$section')({ + loader: ({ params }) => { + const items = sectionItems(params.section) + const checksum = items.reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) + return { items, checksum } + }, + staleTime: 0, + gcTime: 0, + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + const data = Route.useLoaderData() + + return ( +
+

{`${params.section}:${data.checksum}`}

+
    + {data.items.map((item) => ( +
  • {`${item.name}=${item.value}`}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/react/tsconfig.json b/benchmarks/client-nav/scenarios/preload/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/preload/react/vite.config.ts b/benchmarks/client-nav/scenarios/preload/react/vite.config.ts new file mode 100644 index 0000000000..ff51686959 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav preload (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/preload/shared.ts b/benchmarks/client-nav/scenarios/preload/shared.ts new file mode 100644 index 0000000000..d061f99451 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/shared.ts @@ -0,0 +1,119 @@ +/** + * Shared definition of the `preload` scenario: intent preloading via link + * hover, programmatic `router.preloadRoute`, and commit-time preload-cache + * maintenance. The router is configured with `defaultPreload: 'intent'`, + * `defaultPreloadDelay: 0` (no wall-clock debounce timer) and + * `defaultPreloadStaleTime: 0` — the default 30s wall-clock threshold would + * flip cache-hit behavior mid-run; with 0 every hover re-runs the preload + * work, keeping the loop stationary. + * + * Hover-intent preloading was verified manually per adapter (standalone jsdom + * run inspecting the router's cached matches after dispatching `mouseover` on + * a link). The harness hover step dispatches a single `mouseover`, which + * triggers the preload pipeline exactly once in every adapter: React + * synthesizes mouseEnter from it, Solid/Vue attach a native `mouseover` + * handler. (Solid/Vue also listen to `mouseenter`; dispatching both events + * would preload twice for them but once for React.) + */ +import type { ScenarioStep } from '../harness' + +export const sections = ['a', 'b', 'c', 'd', 'e'] as const + +export const SECTION_ITEM_COUNT = 30 + +/** Deterministic LCG stream seeded from a string — no Math.random. */ +function seededValues(seedText: string, count: number) { + let value = 0 + for (let i = 0; i < seedText.length; i++) { + value = (value * 31 + seedText.charCodeAt(i)) >>> 0 + } + + const values: Array = [] + for (let i = 0; i < count; i++) { + value = (value * 1664525 + 1013904223) >>> 0 + values.push(value % 100_000) + } + return values +} + +export interface SectionItem { + name: string + value: number +} + +export function sectionItems(section: string): Array { + return seededValues(`section-${section}`, SECTION_ITEM_COUNT).map( + (value, index) => ({ + name: `item-${section}-${index}`, + value, + }), + ) +} + +export function sectionChecksum(section: string) { + return sectionItems(section).reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) +} + +export function sectionMarker(section: string) { + return `${section}:${sectionChecksum(section)}` +} + +export const docsMarker = 'docs' +export const homeMarker = 'home' + +interface StepDef { + step: ScenarioStep + /** Expected page marker after the step; undefined for non-navigation steps. */ + marker?: string +} + +export const stepDefs: ReadonlyArray = [ + { step: { type: 'hover', testId: 's-a' } }, + { step: { type: 'hover', testId: 's-b' } }, + { step: 's-a', marker: sectionMarker('a') }, + { step: { type: 'hover', testId: 's-c' } }, + { step: 's-c', marker: sectionMarker('c') }, + { + step: { + type: 'preload', + getOptions: () => ({ + to: '/sections/$section', + params: { section: 'd' }, + }), + }, + }, + { step: 's-d', marker: sectionMarker('d') }, + { step: { type: 'hover', testId: 's-e' } }, + { step: 'go-docs', marker: docsMarker }, + { step: 'go-home', marker: homeMarker }, +] + +export const scenarioSteps: ReadonlyArray = stepDefs.map( + (def) => def.step, +) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const def = stepDefs[stepIndex]! + if (!def.marker) { + return + } + + const marker = container.querySelector('[data-testid="page-state"]') + if (marker?.textContent !== def.marker) { + throw new Error( + `Expected page marker "${def.marker}" after step ${stepIndex}, received "${marker?.textContent}"`, + ) + } +} + +// Two laps through the 10-step sequence per benchmark iteration. +export const ticksPerIteration = 20 + +export const benchOptions = { + warmupIterations: 50, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/project.json b/benchmarks/client-nav/scenarios/preload/solid/project.json new file mode 100644 index 0000000000..a186879335 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-preload-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/preload/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/setup.ts b/benchmarks/client-nav/scenarios/preload/solid/setup.ts new file mode 100644 index 0000000000..34270fe8a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts new file mode 100644 index 0000000000..ff2771985b --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-preload', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-preload interaction loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/preload/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/preload/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx new file mode 100644 index 0000000000..6e944942f8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/main.tsx @@ -0,0 +1,35 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPreload: 'intent', + defaultPreloadDelay: 0, + defaultPreloadStaleTime: 0, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/preload/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..645d925238 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SectionsSectionRouteImport } from './routes/sections.$section' + +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SectionsSectionRoute = SectionsSectionRouteImport.update({ + id: '/sections/$section', + path: '/sections/$section', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/sections/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/sections/$section' + id: '__root__' | '/' | '/docs' | '/sections/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRoute + SectionsSectionRoute: typeof SectionsSectionRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sections/$section': { + id: '/sections/$section' + path: '/sections/$section' + fullPath: '/sections/$section' + preLoaderRoute: typeof SectionsSectionRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRoute, + SectionsSectionRoute: SectionsSectionRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..a805ed0feb --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/__root.tsx @@ -0,0 +1,34 @@ +import { For } from 'solid-js' +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' +import { sections } from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/docs.tsx new file mode 100644 index 0000000000..b077cfd18f --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/docs.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { docsMarker } from '../../../shared' + +export const Route = createFileRoute('/docs')({ + component: DocsPage, +}) + +function DocsPage() { + return ( +
+

{docsMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/index.tsx new file mode 100644 index 0000000000..72fbfe387a --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { homeMarker } from '../../../shared' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

{homeMarker}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/src/routes/sections.$section.tsx b/benchmarks/client-nav/scenarios/preload/solid/src/routes/sections.$section.tsx new file mode 100644 index 0000000000..2a7fb2ad1b --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/src/routes/sections.$section.tsx @@ -0,0 +1,33 @@ +import { For } from 'solid-js' +import { createFileRoute } from '@tanstack/solid-router' +import { sectionItems } from '../../../shared' + +export const Route = createFileRoute('/sections/$section')({ + loader: ({ params }) => { + const items = sectionItems(params.section) + const checksum = items.reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) + return { items, checksum } + }, + staleTime: 0, + gcTime: 0, + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + const data = Route.useLoaderData() + + return ( +
+

{`${params().section}:${data().checksum}`}

+
    + + {(item) =>
  • {`${item.name}=${item.value}`}
  • } +
    +
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/tsconfig.json b/benchmarks/client-nav/scenarios/preload/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/preload/solid/vite.config.ts b/benchmarks/client-nav/scenarios/preload/solid/vite.config.ts new file mode 100644 index 0000000000..b542834469 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav preload (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/project.json b/benchmarks/client-nav/scenarios/preload/vue/project.json new file mode 100644 index 0000000000..404476723d --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-preload-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/preload/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/setup.ts b/benchmarks/client-nav/scenarios/preload/vue/setup.ts new file mode 100644 index 0000000000..f70a154909 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, scenarioSteps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps: scenarioSteps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts new file mode 100644 index 0000000000..2e790e9240 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-preload', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-preload interaction loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/preload/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx new file mode 100644 index 0000000000..1e325392a4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/main.tsx @@ -0,0 +1,42 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultPreload: 'intent', + defaultPreloadDelay: 0, + defaultPreloadStaleTime: 0, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/preload/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..05a751060b --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DocsRouteImport } from './routes/docs' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SectionsSectionRouteImport } from './routes/sections.$section' + +const DocsRoute = DocsRouteImport.update({ + id: '/docs', + path: '/docs', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SectionsSectionRoute = SectionsSectionRouteImport.update({ + id: '/sections/$section', + path: '/sections/$section', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/docs': typeof DocsRoute + '/sections/$section': typeof SectionsSectionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/docs' | '/sections/$section' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/docs' | '/sections/$section' + id: '__root__' | '/' | '/docs' | '/sections/$section' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DocsRoute: typeof DocsRoute + SectionsSectionRoute: typeof SectionsSectionRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/docs': { + id: '/docs' + path: '/docs' + fullPath: '/docs' + preLoaderRoute: typeof DocsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sections/$section': { + id: '/sections/$section' + path: '/sections/$section' + fullPath: '/sections/$section' + preLoaderRoute: typeof SectionsSectionRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DocsRoute: DocsRoute, + SectionsSectionRoute: SectionsSectionRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..bf91ba3770 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/__root.tsx @@ -0,0 +1,35 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' +import { sections } from '../../../shared' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/docs.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/docs.tsx new file mode 100644 index 0000000000..07d0a989ad --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/docs.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { docsMarker } from '../../../shared' + +const DocsPage = Vue.defineComponent({ + setup() { + return () => ( +
+

{docsMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/docs')({ + component: DocsPage, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/index.tsx new file mode 100644 index 0000000000..f04a9f5796 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { homeMarker } from '../../../shared' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

{homeMarker}

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/src/routes/sections.$section.tsx b/benchmarks/client-nav/scenarios/preload/vue/src/routes/sections.$section.tsx new file mode 100644 index 0000000000..aeb617a86f --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/src/routes/sections.$section.tsx @@ -0,0 +1,37 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { sectionItems } from '../../../shared' + +const SectionPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + const data = Route.useLoaderData() + + return () => ( +
+

+ {`${params.value.section}:${data.value.checksum}`} +

+
    + {data.value.items.map((item) => ( +
  • {`${item.name}=${item.value}`}
  • + ))} +
+
+ ) + }, +}) + +export const Route = createFileRoute('/sections/$section')({ + loader: ({ params }) => { + const items = sectionItems(params.section) + const checksum = items.reduce( + (sum, item) => (sum + item.value) % 1_000_000_007, + 0, + ) + return { items, checksum } + }, + staleTime: 0, + gcTime: 0, + component: SectionPage, +}) diff --git a/benchmarks/client-nav/scenarios/preload/vue/tsconfig.json b/benchmarks/client-nav/scenarios/preload/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/preload/vue/vite.config.ts b/benchmarks/client-nav/scenarios/preload/vue/vite.config.ts new file mode 100644 index 0000000000..25e64b5bef --- /dev/null +++ b/benchmarks/client-nav/scenarios/preload/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav preload (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/react/project.json b/benchmarks/client-nav/scenarios/rewrites/react/project.json new file mode 100644 index 0000000000..d98c6e1412 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-rewrites-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/rewrites/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/setup.ts b/benchmarks/client-nav/scenarios/rewrites/react/setup.ts new file mode 100644 index 0000000000..0c5df273ed --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/setup.ts @@ -0,0 +1,18 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, initialUrl, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + initialUrl, + }) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/speed.bench.ts b/benchmarks/client-nav/scenarios/rewrites/react/speed.bench.ts new file mode 100644 index 0000000000..9b77a79838 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-rewrites', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-rewrites navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/react/speed.flame.ts b/benchmarks/client-nav/scenarios/rewrites/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/main.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/main.tsx new file mode 100644 index 0000000000..47746bb0fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/main.tsx @@ -0,0 +1,38 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' +import { basepath, localeRewrite } from '../../shared' + +export function createTestRouter() { + return createRouter({ + routeTree, + basepath, + rewrite: localeRewrite, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/rewrites/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..b6ab8c8be4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routeTree.gen.ts @@ -0,0 +1,103 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PARoute = PARouteImport.update({ + id: '/p/$a', + path: '/p/$a', + getParentRoute: () => rootRouteImport, +} as any) +const PABRoute = PABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => PARoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/p/$a' | '/p/$a/$b' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/p/$a' | '/p/$a/$b' + id: '__root__' | '/' | '/p/$a' | '/p/$a/$b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a': { + id: '/p/$a' + path: '/p/$a' + fullPath: '/p/$a' + preLoaderRoute: typeof PARouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a/$b': { + id: '/p/$a/$b' + path: '/$b' + fullPath: '/p/$a/$b' + preLoaderRoute: typeof PABRouteImport + parentRoute: typeof PARoute + } + } +} + +interface PARouteChildren { + PABRoute: typeof PABRoute +} + +const PARouteChildren: PARouteChildren = { + PABRoute: PABRoute, +} + +const PARouteWithChildren = PARoute._addFileChildren(PARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/__root.tsx new file mode 100644 index 0000000000..ad14ff51f1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' + +export const Route = createRootRoute({ + validateSearch: (search: Record & SearchSchemaInput) => + ({ + _locale: typeof search._locale === 'string' ? search._locale : undefined, + }) as { _locale?: string }, + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/index.tsx new file mode 100644 index 0000000000..821cfe4a7c --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.$b.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.$b.tsx new file mode 100644 index 0000000000..ec1163e5c2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.$b.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' +import { itemLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a/$b')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemLabel(params.a, params.b)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.tsx b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.tsx new file mode 100644 index 0000000000..f4937494f0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/src/routes/p.$a.tsx @@ -0,0 +1,17 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { sectionLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a')({ + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + + return ( +
+

{sectionLabel(params.a)}

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/tsconfig.json b/benchmarks/client-nav/scenarios/rewrites/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/rewrites/react/vite.config.ts b/benchmarks/client-nav/scenarios/rewrites/react/vite.config.ts new file mode 100644 index 0000000000..8c07cff4fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav rewrites (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/shared.ts b/benchmarks/client-nav/scenarios/rewrites/shared.ts new file mode 100644 index 0000000000..c45b15d503 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/shared.ts @@ -0,0 +1,101 @@ +/** + * Shared definition of the `rewrites` scenario: composed client-side location + * rewrites — router `basepath` plus a locale input/output rewrite (the client + * analog of the SSR `rewrites` scenario). Externally URLs look like + * `/app/{locale}/p/...`; internally the router sees `/p/...` with the locale + * carried in the `_locale` search param. Every navigation runs the composed + * output rewrite when building hrefs/committing and the input rewrite when + * parsing the committed location. + */ +import type { ScenarioStep } from '../harness' + +export const basepath = '/app' +export const defaultLocale = 'en' + +const localePrefixRe = /^\/(fr|de|es)(\/.*)$/ + +// Framework-agnostic rewrite pair consumed by all three apps' createRouter. +export const localeRewrite = { + input: ({ url }: { url: URL }) => { + const match = url.pathname.match(localePrefixRe) + + if (match) { + url.pathname = match[2]! + url.searchParams.set('_locale', match[1]!) + return url + } + + return undefined + }, + output: ({ url }: { url: URL }) => { + const locale = url.searchParams.get('_locale') + + if (locale) { + url.searchParams.delete('_locale') + url.pathname = `/${locale}${url.pathname}` + } + + return url + }, +} + +export function sectionLabel(a: string) { + return `Section ${a}` +} + +export function itemLabel(a: string, b: string) { + return `Item ${a}/${b}` +} + +interface StepDef { + testId: string + /** Internal router pathname expected after the step. */ + routerPath: string + /** External window.location pathname expected after the step. */ + windowPath: string +} + +// Circular lap: alternates sections, depths, and locales so every step runs +// the composed rewrites in both directions, then returns to the start. +export const stepDefs: ReadonlyArray = [ + { testId: 'go-x', routerPath: '/p/x', windowPath: '/app/p/x' }, + { testId: 'go-x-1-fr', routerPath: '/p/x/1', windowPath: '/app/fr/p/x/1' }, + { testId: 'go-y-de', routerPath: '/p/y', windowPath: '/app/de/p/y' }, + { testId: 'go-y-2', routerPath: '/p/y/2', windowPath: '/app/p/y/2' }, + { testId: 'go-x-es', routerPath: '/p/x', windowPath: '/app/es/p/x' }, + { testId: 'go-home', routerPath: '/', windowPath: '/app/' }, +] as const + +export const steps: ReadonlyArray = stepDefs.map( + (step) => step.testId, +) + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = stepDefs[stepIndex]! + const marker = container.querySelector('[data-testid="loc"]') + const routerPath = marker?.textContent + + if (routerPath !== expected.routerPath) { + throw new Error( + `Expected router pathname "${expected.routerPath}" after step ${stepIndex}, received "${routerPath}"`, + ) + } + + if (window.location.pathname !== expected.windowPath) { + throw new Error( + `Expected window pathname "${expected.windowPath}" after step ${stepIndex}, received "${window.location.pathname}"`, + ) + } +} + +// The app starts on the external root inside the basepath. +export const initialUrl = '/app' + +// Three laps through the 6-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/project.json b/benchmarks/client-nav/scenarios/rewrites/solid/project.json new file mode 100644 index 0000000000..4b2f763032 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-rewrites-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/rewrites/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/setup.ts b/benchmarks/client-nav/scenarios/rewrites/solid/setup.ts new file mode 100644 index 0000000000..5796df712d --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/setup.ts @@ -0,0 +1,18 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, initialUrl, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + initialUrl, + }) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/rewrites/solid/speed.bench.ts new file mode 100644 index 0000000000..30d3436071 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-rewrites', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-rewrites navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/rewrites/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/main.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/main.tsx new file mode 100644 index 0000000000..9e7fbfe87f --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/main.tsx @@ -0,0 +1,35 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import { basepath, localeRewrite } from '../../shared' + +export function createTestRouter() { + return createRouter({ + routeTree, + basepath, + rewrite: localeRewrite, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/rewrites/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..2d35f899c9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routeTree.gen.ts @@ -0,0 +1,103 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PARoute = PARouteImport.update({ + id: '/p/$a', + path: '/p/$a', + getParentRoute: () => rootRouteImport, +} as any) +const PABRoute = PABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => PARoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/p/$a' | '/p/$a/$b' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/p/$a' | '/p/$a/$b' + id: '__root__' | '/' | '/p/$a' | '/p/$a/$b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a': { + id: '/p/$a' + path: '/p/$a' + fullPath: '/p/$a' + preLoaderRoute: typeof PARouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a/$b': { + id: '/p/$a/$b' + path: '/$b' + fullPath: '/p/$a/$b' + preLoaderRoute: typeof PABRouteImport + parentRoute: typeof PARoute + } + } +} + +interface PARouteChildren { + PABRoute: typeof PABRoute +} + +const PARouteChildren: PARouteChildren = { + PABRoute: PABRoute, +} + +const PARouteWithChildren = PARoute._addFileChildren(PARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..3a78f2c3cf --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + validateSearch: (search: Record & SearchSchemaInput) => + ({ + _locale: typeof search._locale === 'string' ? search._locale : undefined, + }) as { _locale?: string }, + component: RootComponent, +}) + +function RootComponent() { + const pathname = useLocation({ select: (location) => location.pathname }) + + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/index.tsx new file mode 100644 index 0000000000..5ee381ff05 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Home

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx new file mode 100644 index 0000000000..febcad35e3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.$b.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { itemLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a/$b')({ + component: ItemPage, +}) + +function ItemPage() { + const params = Route.useParams() + + return ( +
+

{itemLabel(params().a, params().b)}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.tsx b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.tsx new file mode 100644 index 0000000000..adfb7bc3b6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/src/routes/p.$a.tsx @@ -0,0 +1,17 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { sectionLabel } from '../../../shared' + +export const Route = createFileRoute('/p/$a')({ + component: SectionPage, +}) + +function SectionPage() { + const params = Route.useParams() + + return ( +
+

{sectionLabel(params().a)}

+ +
+ ) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/tsconfig.json b/benchmarks/client-nav/scenarios/rewrites/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/rewrites/solid/vite.config.ts b/benchmarks/client-nav/scenarios/rewrites/solid/vite.config.ts new file mode 100644 index 0000000000..41613c2ad4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav rewrites (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/project.json b/benchmarks/client-nav/scenarios/rewrites/vue/project.json new file mode 100644 index 0000000000..9b7cb5d198 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-rewrites-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/rewrites/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/setup.ts b/benchmarks/client-nav/scenarios/rewrites/vue/setup.ts new file mode 100644 index 0000000000..e26b4d05ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/setup.ts @@ -0,0 +1,18 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, initialUrl, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + initialUrl, + }) +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/rewrites/vue/speed.bench.ts new file mode 100644 index 0000000000..c10b8723bb --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-rewrites', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-rewrites navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/rewrites/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/main.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/main.tsx new file mode 100644 index 0000000000..83e430038d --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/main.tsx @@ -0,0 +1,42 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' +import { basepath, localeRewrite } from '../../shared' + +export function createTestRouter() { + return createRouter({ + routeTree, + basepath, + rewrite: localeRewrite, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/rewrites/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..6eca4e8ce5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routeTree.gen.ts @@ -0,0 +1,103 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PARouteImport } from './routes/p.$a' +import { Route as PABRouteImport } from './routes/p.$a.$b' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PARoute = PARouteImport.update({ + id: '/p/$a', + path: '/p/$a', + getParentRoute: () => rootRouteImport, +} as any) +const PABRoute = PABRouteImport.update({ + id: '/$b', + path: '/$b', + getParentRoute: () => PARoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/p/$a': typeof PARouteWithChildren + '/p/$a/$b': typeof PABRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/p/$a' | '/p/$a/$b' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/p/$a' | '/p/$a/$b' + id: '__root__' | '/' | '/p/$a' | '/p/$a/$b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + PARoute: typeof PARouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a': { + id: '/p/$a' + path: '/p/$a' + fullPath: '/p/$a' + preLoaderRoute: typeof PARouteImport + parentRoute: typeof rootRouteImport + } + '/p/$a/$b': { + id: '/p/$a/$b' + path: '/$b' + fullPath: '/p/$a/$b' + preLoaderRoute: typeof PABRouteImport + parentRoute: typeof PARoute + } + } +} + +interface PARouteChildren { + PABRoute: typeof PABRoute +} + +const PARouteChildren: PARouteChildren = { + PABRoute: PABRoute, +} + +const PARouteWithChildren = PARoute._addFileChildren(PARouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + PARoute: PARouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..ae89645005 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/__root.tsx @@ -0,0 +1,64 @@ +import * as Vue from 'vue' +import { + Link, + Outlet, + createRootRoute, + useLocation, +} from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + const pathname = useLocation({ select: (location) => location.pathname }) + + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + validateSearch: (search: Record & SearchSchemaInput) => + ({ + _locale: typeof search._locale === 'string' ? search._locale : undefined, + }) as { _locale?: string }, + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/index.tsx new file mode 100644 index 0000000000..d5405b52d2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/index.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Home

+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx new file mode 100644 index 0000000000..062c0903b9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.$b.tsx @@ -0,0 +1,19 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' +import { itemLabel } from '../../../shared' + +const ItemPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{itemLabel(params.value.a, params.value.b)}

+
+ ) + }, +}) + +export const Route = createFileRoute('/p/$a/$b')({ + component: ItemPage, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.tsx b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.tsx new file mode 100644 index 0000000000..552700d402 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/src/routes/p.$a.tsx @@ -0,0 +1,20 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { sectionLabel } from '../../../shared' + +const SectionPage = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
+

{sectionLabel(params.value.a)}

+ +
+ ) + }, +}) + +export const Route = createFileRoute('/p/$a')({ + component: SectionPage, +}) diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/tsconfig.json b/benchmarks/client-nav/scenarios/rewrites/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/rewrites/vue/vite.config.ts b/benchmarks/client-nav/scenarios/rewrites/vue/vite.config.ts new file mode 100644 index 0000000000..39e22b6c3e --- /dev/null +++ b/benchmarks/client-nav/scenarios/rewrites/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav rewrites (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/project.json b/benchmarks/client-nav/scenarios/route-tree-scale/react/project.json new file mode 100644 index 0000000000..3291738f79 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-route-tree-scale-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/route-tree-scale/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/setup.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts new file mode 100644 index 0000000000..50cc26d5d6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-route-tree-scale', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-route-tree-scale navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.flame.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx new file mode 100644 index 0000000000..ef0b99555c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/main.tsx @@ -0,0 +1,35 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..b58b75180f --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routeTree.gen.ts @@ -0,0 +1,881 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as SecFRouteImport } from './routes/sec-f' +import { Route as SecERouteImport } from './routes/sec-e' +import { Route as SecDRouteImport } from './routes/sec-d' +import { Route as SecCRouteImport } from './routes/sec-c' +import { Route as SecBRouteImport } from './routes/sec-b' +import { Route as SecARouteImport } from './routes/sec-a' +import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SecFIndexRouteImport } from './routes/sec-f.index' +import { Route as SecEIndexRouteImport } from './routes/sec-e.index' +import { Route as SecDIndexRouteImport } from './routes/sec-d.index' +import { Route as SecCIndexRouteImport } from './routes/sec-c.index' +import { Route as SecBIndexRouteImport } from './routes/sec-b.index' +import { Route as SecAIndexRouteImport } from './routes/sec-a.index' +import { Route as SecFSettingsRouteImport } from './routes/sec-f.settings' +import { Route as SecFAboutRouteImport } from './routes/sec-f.about' +import { Route as SecFIdRouteImport } from './routes/sec-f.$id' +import { Route as SecESettingsRouteImport } from './routes/sec-e.settings' +import { Route as SecEAboutRouteImport } from './routes/sec-e.about' +import { Route as SecEIdRouteImport } from './routes/sec-e.$id' +import { Route as SecDSettingsRouteImport } from './routes/sec-d.settings' +import { Route as SecDAboutRouteImport } from './routes/sec-d.about' +import { Route as SecDIdRouteImport } from './routes/sec-d.$id' +import { Route as SecCSettingsRouteImport } from './routes/sec-c.settings' +import { Route as SecCAboutRouteImport } from './routes/sec-c.about' +import { Route as SecCIdRouteImport } from './routes/sec-c.$id' +import { Route as SecBSettingsRouteImport } from './routes/sec-b.settings' +import { Route as SecBAboutRouteImport } from './routes/sec-b.about' +import { Route as SecBIdRouteImport } from './routes/sec-b.$id' +import { Route as SecASettingsRouteImport } from './routes/sec-a.settings' +import { Route as SecAAboutRouteImport } from './routes/sec-a.about' +import { Route as SecAIdRouteImport } from './routes/sec-a.$id' +import { Route as ReleaseVChar123versionChar125RouteImport } from './routes/release.v{$version}' +import { Route as FilesSplatRouteImport } from './routes/files.$' +import { Route as LayoutBetaRouteImport } from './routes/_layout.beta' +import { Route as LayoutAlphaRouteImport } from './routes/_layout.alpha' +import { Route as marketingPromoRouteImport } from './routes/(marketing)/promo' +import { Route as marketingPricingRouteImport } from './routes/(marketing)/pricing' + +const SecFRoute = SecFRouteImport.update({ + id: '/sec-f', + path: '/sec-f', + getParentRoute: () => rootRouteImport, +} as any) +const SecERoute = SecERouteImport.update({ + id: '/sec-e', + path: '/sec-e', + getParentRoute: () => rootRouteImport, +} as any) +const SecDRoute = SecDRouteImport.update({ + id: '/sec-d', + path: '/sec-d', + getParentRoute: () => rootRouteImport, +} as any) +const SecCRoute = SecCRouteImport.update({ + id: '/sec-c', + path: '/sec-c', + getParentRoute: () => rootRouteImport, +} as any) +const SecBRoute = SecBRouteImport.update({ + id: '/sec-b', + path: '/sec-b', + getParentRoute: () => rootRouteImport, +} as any) +const SecARoute = SecARouteImport.update({ + id: '/sec-a', + path: '/sec-a', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SecFIndexRoute = SecFIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecFRoute, +} as any) +const SecEIndexRoute = SecEIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecERoute, +} as any) +const SecDIndexRoute = SecDIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecDRoute, +} as any) +const SecCIndexRoute = SecCIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecCRoute, +} as any) +const SecBIndexRoute = SecBIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecBRoute, +} as any) +const SecAIndexRoute = SecAIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecARoute, +} as any) +const SecFSettingsRoute = SecFSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecFRoute, +} as any) +const SecFAboutRoute = SecFAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecFRoute, +} as any) +const SecFIdRoute = SecFIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecFRoute, +} as any) +const SecESettingsRoute = SecESettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecERoute, +} as any) +const SecEAboutRoute = SecEAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecERoute, +} as any) +const SecEIdRoute = SecEIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecERoute, +} as any) +const SecDSettingsRoute = SecDSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecDRoute, +} as any) +const SecDAboutRoute = SecDAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecDRoute, +} as any) +const SecDIdRoute = SecDIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecDRoute, +} as any) +const SecCSettingsRoute = SecCSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecCRoute, +} as any) +const SecCAboutRoute = SecCAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecCRoute, +} as any) +const SecCIdRoute = SecCIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecCRoute, +} as any) +const SecBSettingsRoute = SecBSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecBRoute, +} as any) +const SecBAboutRoute = SecBAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecBRoute, +} as any) +const SecBIdRoute = SecBIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecBRoute, +} as any) +const SecASettingsRoute = SecASettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecARoute, +} as any) +const SecAAboutRoute = SecAAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecARoute, +} as any) +const SecAIdRoute = SecAIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecARoute, +} as any) +const ReleaseVChar123versionChar125Route = + ReleaseVChar123versionChar125RouteImport.update({ + id: '/release/v{$version}', + path: '/release/v{$version}', + getParentRoute: () => rootRouteImport, + } as any) +const FilesSplatRoute = FilesSplatRouteImport.update({ + id: '/files/$', + path: '/files/$', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutBetaRoute = LayoutBetaRouteImport.update({ + id: '/beta', + path: '/beta', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutAlphaRoute = LayoutAlphaRouteImport.update({ + id: '/alpha', + path: '/alpha', + getParentRoute: () => LayoutRoute, +} as any) +const marketingPromoRoute = marketingPromoRouteImport.update({ + id: '/(marketing)/promo', + path: '/promo', + getParentRoute: () => rootRouteImport, +} as any) +const marketingPricingRoute = marketingPricingRouteImport.update({ + id: '/(marketing)/pricing', + path: '/pricing', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a': typeof SecAIndexRoute + '/sec-b': typeof SecBIndexRoute + '/sec-c': typeof SecCIndexRoute + '/sec-d': typeof SecDIndexRoute + '/sec-e': typeof SecEIndexRoute + '/sec-f': typeof SecFIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/(marketing)/pricing': typeof marketingPricingRoute + '/(marketing)/promo': typeof marketingPromoRoute + '/_layout/alpha': typeof LayoutAlphaRoute + '/_layout/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + id: + | '__root__' + | '/' + | '/_layout' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/(marketing)/pricing' + | '/(marketing)/promo' + | '/_layout/alpha' + | '/_layout/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + SecARoute: typeof SecARouteWithChildren + SecBRoute: typeof SecBRouteWithChildren + SecCRoute: typeof SecCRouteWithChildren + SecDRoute: typeof SecDRouteWithChildren + SecERoute: typeof SecERouteWithChildren + SecFRoute: typeof SecFRouteWithChildren + marketingPricingRoute: typeof marketingPricingRoute + marketingPromoRoute: typeof marketingPromoRoute + FilesSplatRoute: typeof FilesSplatRoute + ReleaseVChar123versionChar125Route: typeof ReleaseVChar123versionChar125Route +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/sec-f': { + id: '/sec-f' + path: '/sec-f' + fullPath: '/sec-f' + preLoaderRoute: typeof SecFRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-e': { + id: '/sec-e' + path: '/sec-e' + fullPath: '/sec-e' + preLoaderRoute: typeof SecERouteImport + parentRoute: typeof rootRouteImport + } + '/sec-d': { + id: '/sec-d' + path: '/sec-d' + fullPath: '/sec-d' + preLoaderRoute: typeof SecDRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-c': { + id: '/sec-c' + path: '/sec-c' + fullPath: '/sec-c' + preLoaderRoute: typeof SecCRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-b': { + id: '/sec-b' + path: '/sec-b' + fullPath: '/sec-b' + preLoaderRoute: typeof SecBRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-a': { + id: '/sec-a' + path: '/sec-a' + fullPath: '/sec-a' + preLoaderRoute: typeof SecARouteImport + parentRoute: typeof rootRouteImport + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-f/': { + id: '/sec-f/' + path: '/' + fullPath: '/sec-f/' + preLoaderRoute: typeof SecFIndexRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/': { + id: '/sec-e/' + path: '/' + fullPath: '/sec-e/' + preLoaderRoute: typeof SecEIndexRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/': { + id: '/sec-d/' + path: '/' + fullPath: '/sec-d/' + preLoaderRoute: typeof SecDIndexRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/': { + id: '/sec-c/' + path: '/' + fullPath: '/sec-c/' + preLoaderRoute: typeof SecCIndexRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/': { + id: '/sec-b/' + path: '/' + fullPath: '/sec-b/' + preLoaderRoute: typeof SecBIndexRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/': { + id: '/sec-a/' + path: '/' + fullPath: '/sec-a/' + preLoaderRoute: typeof SecAIndexRouteImport + parentRoute: typeof SecARoute + } + '/sec-f/settings': { + id: '/sec-f/settings' + path: '/settings' + fullPath: '/sec-f/settings' + preLoaderRoute: typeof SecFSettingsRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/about': { + id: '/sec-f/about' + path: '/about' + fullPath: '/sec-f/about' + preLoaderRoute: typeof SecFAboutRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/$id': { + id: '/sec-f/$id' + path: '/$id' + fullPath: '/sec-f/$id' + preLoaderRoute: typeof SecFIdRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/settings': { + id: '/sec-e/settings' + path: '/settings' + fullPath: '/sec-e/settings' + preLoaderRoute: typeof SecESettingsRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/about': { + id: '/sec-e/about' + path: '/about' + fullPath: '/sec-e/about' + preLoaderRoute: typeof SecEAboutRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/$id': { + id: '/sec-e/$id' + path: '/$id' + fullPath: '/sec-e/$id' + preLoaderRoute: typeof SecEIdRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/settings': { + id: '/sec-d/settings' + path: '/settings' + fullPath: '/sec-d/settings' + preLoaderRoute: typeof SecDSettingsRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/about': { + id: '/sec-d/about' + path: '/about' + fullPath: '/sec-d/about' + preLoaderRoute: typeof SecDAboutRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/$id': { + id: '/sec-d/$id' + path: '/$id' + fullPath: '/sec-d/$id' + preLoaderRoute: typeof SecDIdRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/settings': { + id: '/sec-c/settings' + path: '/settings' + fullPath: '/sec-c/settings' + preLoaderRoute: typeof SecCSettingsRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/about': { + id: '/sec-c/about' + path: '/about' + fullPath: '/sec-c/about' + preLoaderRoute: typeof SecCAboutRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/$id': { + id: '/sec-c/$id' + path: '/$id' + fullPath: '/sec-c/$id' + preLoaderRoute: typeof SecCIdRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/settings': { + id: '/sec-b/settings' + path: '/settings' + fullPath: '/sec-b/settings' + preLoaderRoute: typeof SecBSettingsRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/about': { + id: '/sec-b/about' + path: '/about' + fullPath: '/sec-b/about' + preLoaderRoute: typeof SecBAboutRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/$id': { + id: '/sec-b/$id' + path: '/$id' + fullPath: '/sec-b/$id' + preLoaderRoute: typeof SecBIdRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/settings': { + id: '/sec-a/settings' + path: '/settings' + fullPath: '/sec-a/settings' + preLoaderRoute: typeof SecASettingsRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/about': { + id: '/sec-a/about' + path: '/about' + fullPath: '/sec-a/about' + preLoaderRoute: typeof SecAAboutRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/$id': { + id: '/sec-a/$id' + path: '/$id' + fullPath: '/sec-a/$id' + preLoaderRoute: typeof SecAIdRouteImport + parentRoute: typeof SecARoute + } + '/release/v{$version}': { + id: '/release/v{$version}' + path: '/release/v{$version}' + fullPath: '/release/v{$version}' + preLoaderRoute: typeof ReleaseVChar123versionChar125RouteImport + parentRoute: typeof rootRouteImport + } + '/files/$': { + id: '/files/$' + path: '/files/$' + fullPath: '/files/$' + preLoaderRoute: typeof FilesSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout/beta': { + id: '/_layout/beta' + path: '/beta' + fullPath: '/beta' + preLoaderRoute: typeof LayoutBetaRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/alpha': { + id: '/_layout/alpha' + path: '/alpha' + fullPath: '/alpha' + preLoaderRoute: typeof LayoutAlphaRouteImport + parentRoute: typeof LayoutRoute + } + '/(marketing)/promo': { + id: '/(marketing)/promo' + path: '/promo' + fullPath: '/promo' + preLoaderRoute: typeof marketingPromoRouteImport + parentRoute: typeof rootRouteImport + } + '/(marketing)/pricing': { + id: '/(marketing)/pricing' + path: '/pricing' + fullPath: '/pricing' + preLoaderRoute: typeof marketingPricingRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface LayoutRouteChildren { + LayoutAlphaRoute: typeof LayoutAlphaRoute + LayoutBetaRoute: typeof LayoutBetaRoute +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAlphaRoute: LayoutAlphaRoute, + LayoutBetaRoute: LayoutBetaRoute, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface SecARouteChildren { + SecAIdRoute: typeof SecAIdRoute + SecAAboutRoute: typeof SecAAboutRoute + SecASettingsRoute: typeof SecASettingsRoute + SecAIndexRoute: typeof SecAIndexRoute +} + +const SecARouteChildren: SecARouteChildren = { + SecAIdRoute: SecAIdRoute, + SecAAboutRoute: SecAAboutRoute, + SecASettingsRoute: SecASettingsRoute, + SecAIndexRoute: SecAIndexRoute, +} + +const SecARouteWithChildren = SecARoute._addFileChildren(SecARouteChildren) + +interface SecBRouteChildren { + SecBIdRoute: typeof SecBIdRoute + SecBAboutRoute: typeof SecBAboutRoute + SecBSettingsRoute: typeof SecBSettingsRoute + SecBIndexRoute: typeof SecBIndexRoute +} + +const SecBRouteChildren: SecBRouteChildren = { + SecBIdRoute: SecBIdRoute, + SecBAboutRoute: SecBAboutRoute, + SecBSettingsRoute: SecBSettingsRoute, + SecBIndexRoute: SecBIndexRoute, +} + +const SecBRouteWithChildren = SecBRoute._addFileChildren(SecBRouteChildren) + +interface SecCRouteChildren { + SecCIdRoute: typeof SecCIdRoute + SecCAboutRoute: typeof SecCAboutRoute + SecCSettingsRoute: typeof SecCSettingsRoute + SecCIndexRoute: typeof SecCIndexRoute +} + +const SecCRouteChildren: SecCRouteChildren = { + SecCIdRoute: SecCIdRoute, + SecCAboutRoute: SecCAboutRoute, + SecCSettingsRoute: SecCSettingsRoute, + SecCIndexRoute: SecCIndexRoute, +} + +const SecCRouteWithChildren = SecCRoute._addFileChildren(SecCRouteChildren) + +interface SecDRouteChildren { + SecDIdRoute: typeof SecDIdRoute + SecDAboutRoute: typeof SecDAboutRoute + SecDSettingsRoute: typeof SecDSettingsRoute + SecDIndexRoute: typeof SecDIndexRoute +} + +const SecDRouteChildren: SecDRouteChildren = { + SecDIdRoute: SecDIdRoute, + SecDAboutRoute: SecDAboutRoute, + SecDSettingsRoute: SecDSettingsRoute, + SecDIndexRoute: SecDIndexRoute, +} + +const SecDRouteWithChildren = SecDRoute._addFileChildren(SecDRouteChildren) + +interface SecERouteChildren { + SecEIdRoute: typeof SecEIdRoute + SecEAboutRoute: typeof SecEAboutRoute + SecESettingsRoute: typeof SecESettingsRoute + SecEIndexRoute: typeof SecEIndexRoute +} + +const SecERouteChildren: SecERouteChildren = { + SecEIdRoute: SecEIdRoute, + SecEAboutRoute: SecEAboutRoute, + SecESettingsRoute: SecESettingsRoute, + SecEIndexRoute: SecEIndexRoute, +} + +const SecERouteWithChildren = SecERoute._addFileChildren(SecERouteChildren) + +interface SecFRouteChildren { + SecFIdRoute: typeof SecFIdRoute + SecFAboutRoute: typeof SecFAboutRoute + SecFSettingsRoute: typeof SecFSettingsRoute + SecFIndexRoute: typeof SecFIndexRoute +} + +const SecFRouteChildren: SecFRouteChildren = { + SecFIdRoute: SecFIdRoute, + SecFAboutRoute: SecFAboutRoute, + SecFSettingsRoute: SecFSettingsRoute, + SecFIndexRoute: SecFIndexRoute, +} + +const SecFRouteWithChildren = SecFRoute._addFileChildren(SecFRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + SecARoute: SecARouteWithChildren, + SecBRoute: SecBRouteWithChildren, + SecCRoute: SecCRouteWithChildren, + SecDRoute: SecDRouteWithChildren, + SecERoute: SecERouteWithChildren, + SecFRoute: SecFRouteWithChildren, + marketingPricingRoute: marketingPricingRoute, + marketingPromoRoute: marketingPromoRoute, + FilesSplatRoute: FilesSplatRoute, + ReleaseVChar123versionChar125Route: ReleaseVChar123versionChar125Route, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/pricing.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/pricing.tsx new file mode 100644 index 0000000000..ac2a376549 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/pricing.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/(marketing)/pricing')({ + component: Page, +}) + +function Page() { + return
pricing
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/promo.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/promo.tsx new file mode 100644 index 0000000000..b71962254e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/(marketing)/promo.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/(marketing)/promo')({ + component: Page, +}) + +function Page() { + return
promo
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx new file mode 100644 index 0000000000..7ed47da924 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/__root.tsx @@ -0,0 +1,58 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.alpha.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.alpha.tsx new file mode 100644 index 0000000000..893419cef5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.alpha.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/alpha')({ + component: Page, +}) + +function Page() { + return
alpha
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.beta.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.beta.tsx new file mode 100644 index 0000000000..d20c6007ee --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.beta.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/beta')({ + component: Page, +}) + +function Page() { + return
beta
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.tsx new file mode 100644 index 0000000000..961feb50f1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/_layout.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/files.$.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/files.$.tsx new file mode 100644 index 0000000000..028aa43d5a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/files.$.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/files/$')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`files:${params._splat}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/index.tsx new file mode 100644 index 0000000000..8a7ffca9e7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Page, +}) + +function Page() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/release.v{$version}.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/release.v{$version}.tsx new file mode 100644 index 0000000000..dfe8e31905 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/release.v{$version}.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/release/v{$version}')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`release:${params.version}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.$id.tsx new file mode 100644 index 0000000000..8be053a040 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-a:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.about.tsx new file mode 100644 index 0000000000..54c1b28618 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/about')({ + component: Page, +}) + +function Page() { + return
sec-a:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.index.tsx new file mode 100644 index 0000000000..3c793a94f5 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/')({ + component: Page, +}) + +function Page() { + return
sec-a:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.settings.tsx new file mode 100644 index 0000000000..0037f6463d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a/settings')({ + component: Page, +}) + +function Page() { + return
sec-a:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.tsx new file mode 100644 index 0000000000..846f96b67b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-a.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-a')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.$id.tsx new file mode 100644 index 0000000000..0dd7f94feb --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-b:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.about.tsx new file mode 100644 index 0000000000..fde4647472 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/about')({ + component: Page, +}) + +function Page() { + return
sec-b:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.index.tsx new file mode 100644 index 0000000000..51b69cf664 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/')({ + component: Page, +}) + +function Page() { + return
sec-b:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.settings.tsx new file mode 100644 index 0000000000..977b7438b1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b/settings')({ + component: Page, +}) + +function Page() { + return
sec-b:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.tsx new file mode 100644 index 0000000000..77d7e6475d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-b.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-b')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.$id.tsx new file mode 100644 index 0000000000..eb3be796c0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-c:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.about.tsx new file mode 100644 index 0000000000..7910acf332 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/about')({ + component: Page, +}) + +function Page() { + return
sec-c:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.index.tsx new file mode 100644 index 0000000000..f6ae102f2d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/')({ + component: Page, +}) + +function Page() { + return
sec-c:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.settings.tsx new file mode 100644 index 0000000000..bf2be30788 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c/settings')({ + component: Page, +}) + +function Page() { + return
sec-c:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.tsx new file mode 100644 index 0000000000..77ac5644c6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-c.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-c')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.$id.tsx new file mode 100644 index 0000000000..d34aee8972 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-d:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.about.tsx new file mode 100644 index 0000000000..1f095cb2ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/about')({ + component: Page, +}) + +function Page() { + return
sec-d:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.index.tsx new file mode 100644 index 0000000000..8fc698a4d4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/')({ + component: Page, +}) + +function Page() { + return
sec-d:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.settings.tsx new file mode 100644 index 0000000000..76c7a178ea --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d/settings')({ + component: Page, +}) + +function Page() { + return
sec-d:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.tsx new file mode 100644 index 0000000000..5f3d281b5e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-d.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-d')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.$id.tsx new file mode 100644 index 0000000000..6e31d0c5d1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-e:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.about.tsx new file mode 100644 index 0000000000..5a2835917b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/about')({ + component: Page, +}) + +function Page() { + return
sec-e:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.index.tsx new file mode 100644 index 0000000000..0f23b8d484 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/')({ + component: Page, +}) + +function Page() { + return
sec-e:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.settings.tsx new file mode 100644 index 0000000000..b566a1ffaa --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e/settings')({ + component: Page, +}) + +function Page() { + return
sec-e:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.tsx new file mode 100644 index 0000000000..e573a71d98 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-e.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-e')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.$id.tsx new file mode 100644 index 0000000000..a71c17f96e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-f:${params.id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.about.tsx new file mode 100644 index 0000000000..06831063ad --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/about')({ + component: Page, +}) + +function Page() { + return
sec-f:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.index.tsx new file mode 100644 index 0000000000..4cd54b6a7f --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/')({ + component: Page, +}) + +function Page() { + return
sec-f:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.settings.tsx new file mode 100644 index 0000000000..92bdca5e44 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f/settings')({ + component: Page, +}) + +function Page() { + return
sec-f:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.tsx new file mode 100644 index 0000000000..2fa27873e8 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/src/routes/sec-f.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/sec-f')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/tsconfig.json b/benchmarks/client-nav/scenarios/route-tree-scale/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts new file mode 100644 index 0000000000..686907a506 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/react/vite.config.ts @@ -0,0 +1,43 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + // Real file-based apps code-split by default; this scenario keeps it on + // so navigations also exercise lazy route-chunk resolution. + autoCodeSplitting: true, + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav route-tree-scale (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts b/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts new file mode 100644 index 0000000000..8af5996804 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/shared.ts @@ -0,0 +1,64 @@ +/** + * Shared definition of the `route-tree-scale` scenario: route matching, + * location building, and link-target resolution over a wide route tree + * (~40 routes) mixing static segments, dynamic params, prefixed params, + * splats, pathless layouts, and route groups. Navigations jump between + * distant branches so matching cannot reuse the previous branch. + */ + +export const sections = [ + 'sec-a', + 'sec-b', + 'sec-c', + 'sec-d', + 'sec-e', + 'sec-f', +] as const + +export const steps = [ + 'go-sec-a-id', + 'go-sec-f-settings', + 'go-files', + 'go-release', + 'go-alpha', + 'go-promo', + 'go-sec-c-id', + 'go-sec-d-about', + 'go-home', +] as const + +// The dynamic ids and the splat carry characters that need percent-encoding +// (spaces, `&`, `%`, `+`, unicode) so matching also exercises the segment +// encode/decode paths. +const expectedMarkers = [ + 'sec-a:item 11%', + 'sec-f:settings', + 'files:x/ü y/z', + 'release:9', + 'alpha', + 'promo', + 'sec-c:q&a+42', + 'sec-d:about', + 'home', +] as const + +export function assertStepResult(stepIndex: number) { + const expected = expectedMarkers[stepIndex]! + const actual = document.querySelector( + '[data-testid="scale-state"]', + )?.textContent + if (actual !== expected) { + throw new Error( + `Expected scale-state marker "${expected}" after step ${stepIndex}, received "${actual}"`, + ) + } +} + +// Three laps through the 9-step sequence per benchmark iteration. +export const ticksPerIteration = 27 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/project.json b/benchmarks/client-nav/scenarios/route-tree-scale/solid/project.json new file mode 100644 index 0000000000..c568e41196 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-route-tree-scale-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/route-tree-scale/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/setup.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts new file mode 100644 index 0000000000..3d7a22a027 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-route-tree-scale', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-route-tree-scale navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx new file mode 100644 index 0000000000..b43608e191 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/main.tsx @@ -0,0 +1,32 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..20b4af6146 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routeTree.gen.ts @@ -0,0 +1,881 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as SecFRouteImport } from './routes/sec-f' +import { Route as SecERouteImport } from './routes/sec-e' +import { Route as SecDRouteImport } from './routes/sec-d' +import { Route as SecCRouteImport } from './routes/sec-c' +import { Route as SecBRouteImport } from './routes/sec-b' +import { Route as SecARouteImport } from './routes/sec-a' +import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SecFIndexRouteImport } from './routes/sec-f.index' +import { Route as SecEIndexRouteImport } from './routes/sec-e.index' +import { Route as SecDIndexRouteImport } from './routes/sec-d.index' +import { Route as SecCIndexRouteImport } from './routes/sec-c.index' +import { Route as SecBIndexRouteImport } from './routes/sec-b.index' +import { Route as SecAIndexRouteImport } from './routes/sec-a.index' +import { Route as SecFSettingsRouteImport } from './routes/sec-f.settings' +import { Route as SecFAboutRouteImport } from './routes/sec-f.about' +import { Route as SecFIdRouteImport } from './routes/sec-f.$id' +import { Route as SecESettingsRouteImport } from './routes/sec-e.settings' +import { Route as SecEAboutRouteImport } from './routes/sec-e.about' +import { Route as SecEIdRouteImport } from './routes/sec-e.$id' +import { Route as SecDSettingsRouteImport } from './routes/sec-d.settings' +import { Route as SecDAboutRouteImport } from './routes/sec-d.about' +import { Route as SecDIdRouteImport } from './routes/sec-d.$id' +import { Route as SecCSettingsRouteImport } from './routes/sec-c.settings' +import { Route as SecCAboutRouteImport } from './routes/sec-c.about' +import { Route as SecCIdRouteImport } from './routes/sec-c.$id' +import { Route as SecBSettingsRouteImport } from './routes/sec-b.settings' +import { Route as SecBAboutRouteImport } from './routes/sec-b.about' +import { Route as SecBIdRouteImport } from './routes/sec-b.$id' +import { Route as SecASettingsRouteImport } from './routes/sec-a.settings' +import { Route as SecAAboutRouteImport } from './routes/sec-a.about' +import { Route as SecAIdRouteImport } from './routes/sec-a.$id' +import { Route as ReleaseVChar123versionChar125RouteImport } from './routes/release.v{$version}' +import { Route as FilesSplatRouteImport } from './routes/files.$' +import { Route as LayoutBetaRouteImport } from './routes/_layout.beta' +import { Route as LayoutAlphaRouteImport } from './routes/_layout.alpha' +import { Route as marketingPromoRouteImport } from './routes/(marketing)/promo' +import { Route as marketingPricingRouteImport } from './routes/(marketing)/pricing' + +const SecFRoute = SecFRouteImport.update({ + id: '/sec-f', + path: '/sec-f', + getParentRoute: () => rootRouteImport, +} as any) +const SecERoute = SecERouteImport.update({ + id: '/sec-e', + path: '/sec-e', + getParentRoute: () => rootRouteImport, +} as any) +const SecDRoute = SecDRouteImport.update({ + id: '/sec-d', + path: '/sec-d', + getParentRoute: () => rootRouteImport, +} as any) +const SecCRoute = SecCRouteImport.update({ + id: '/sec-c', + path: '/sec-c', + getParentRoute: () => rootRouteImport, +} as any) +const SecBRoute = SecBRouteImport.update({ + id: '/sec-b', + path: '/sec-b', + getParentRoute: () => rootRouteImport, +} as any) +const SecARoute = SecARouteImport.update({ + id: '/sec-a', + path: '/sec-a', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SecFIndexRoute = SecFIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecFRoute, +} as any) +const SecEIndexRoute = SecEIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecERoute, +} as any) +const SecDIndexRoute = SecDIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecDRoute, +} as any) +const SecCIndexRoute = SecCIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecCRoute, +} as any) +const SecBIndexRoute = SecBIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecBRoute, +} as any) +const SecAIndexRoute = SecAIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecARoute, +} as any) +const SecFSettingsRoute = SecFSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecFRoute, +} as any) +const SecFAboutRoute = SecFAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecFRoute, +} as any) +const SecFIdRoute = SecFIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecFRoute, +} as any) +const SecESettingsRoute = SecESettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecERoute, +} as any) +const SecEAboutRoute = SecEAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecERoute, +} as any) +const SecEIdRoute = SecEIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecERoute, +} as any) +const SecDSettingsRoute = SecDSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecDRoute, +} as any) +const SecDAboutRoute = SecDAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecDRoute, +} as any) +const SecDIdRoute = SecDIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecDRoute, +} as any) +const SecCSettingsRoute = SecCSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecCRoute, +} as any) +const SecCAboutRoute = SecCAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecCRoute, +} as any) +const SecCIdRoute = SecCIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecCRoute, +} as any) +const SecBSettingsRoute = SecBSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecBRoute, +} as any) +const SecBAboutRoute = SecBAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecBRoute, +} as any) +const SecBIdRoute = SecBIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecBRoute, +} as any) +const SecASettingsRoute = SecASettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecARoute, +} as any) +const SecAAboutRoute = SecAAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecARoute, +} as any) +const SecAIdRoute = SecAIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecARoute, +} as any) +const ReleaseVChar123versionChar125Route = + ReleaseVChar123versionChar125RouteImport.update({ + id: '/release/v{$version}', + path: '/release/v{$version}', + getParentRoute: () => rootRouteImport, + } as any) +const FilesSplatRoute = FilesSplatRouteImport.update({ + id: '/files/$', + path: '/files/$', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutBetaRoute = LayoutBetaRouteImport.update({ + id: '/beta', + path: '/beta', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutAlphaRoute = LayoutAlphaRouteImport.update({ + id: '/alpha', + path: '/alpha', + getParentRoute: () => LayoutRoute, +} as any) +const marketingPromoRoute = marketingPromoRouteImport.update({ + id: '/(marketing)/promo', + path: '/promo', + getParentRoute: () => rootRouteImport, +} as any) +const marketingPricingRoute = marketingPricingRouteImport.update({ + id: '/(marketing)/pricing', + path: '/pricing', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a': typeof SecAIndexRoute + '/sec-b': typeof SecBIndexRoute + '/sec-c': typeof SecCIndexRoute + '/sec-d': typeof SecDIndexRoute + '/sec-e': typeof SecEIndexRoute + '/sec-f': typeof SecFIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/(marketing)/pricing': typeof marketingPricingRoute + '/(marketing)/promo': typeof marketingPromoRoute + '/_layout/alpha': typeof LayoutAlphaRoute + '/_layout/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + id: + | '__root__' + | '/' + | '/_layout' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/(marketing)/pricing' + | '/(marketing)/promo' + | '/_layout/alpha' + | '/_layout/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + SecARoute: typeof SecARouteWithChildren + SecBRoute: typeof SecBRouteWithChildren + SecCRoute: typeof SecCRouteWithChildren + SecDRoute: typeof SecDRouteWithChildren + SecERoute: typeof SecERouteWithChildren + SecFRoute: typeof SecFRouteWithChildren + marketingPricingRoute: typeof marketingPricingRoute + marketingPromoRoute: typeof marketingPromoRoute + FilesSplatRoute: typeof FilesSplatRoute + ReleaseVChar123versionChar125Route: typeof ReleaseVChar123versionChar125Route +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/sec-f': { + id: '/sec-f' + path: '/sec-f' + fullPath: '/sec-f' + preLoaderRoute: typeof SecFRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-e': { + id: '/sec-e' + path: '/sec-e' + fullPath: '/sec-e' + preLoaderRoute: typeof SecERouteImport + parentRoute: typeof rootRouteImport + } + '/sec-d': { + id: '/sec-d' + path: '/sec-d' + fullPath: '/sec-d' + preLoaderRoute: typeof SecDRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-c': { + id: '/sec-c' + path: '/sec-c' + fullPath: '/sec-c' + preLoaderRoute: typeof SecCRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-b': { + id: '/sec-b' + path: '/sec-b' + fullPath: '/sec-b' + preLoaderRoute: typeof SecBRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-a': { + id: '/sec-a' + path: '/sec-a' + fullPath: '/sec-a' + preLoaderRoute: typeof SecARouteImport + parentRoute: typeof rootRouteImport + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-f/': { + id: '/sec-f/' + path: '/' + fullPath: '/sec-f/' + preLoaderRoute: typeof SecFIndexRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/': { + id: '/sec-e/' + path: '/' + fullPath: '/sec-e/' + preLoaderRoute: typeof SecEIndexRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/': { + id: '/sec-d/' + path: '/' + fullPath: '/sec-d/' + preLoaderRoute: typeof SecDIndexRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/': { + id: '/sec-c/' + path: '/' + fullPath: '/sec-c/' + preLoaderRoute: typeof SecCIndexRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/': { + id: '/sec-b/' + path: '/' + fullPath: '/sec-b/' + preLoaderRoute: typeof SecBIndexRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/': { + id: '/sec-a/' + path: '/' + fullPath: '/sec-a/' + preLoaderRoute: typeof SecAIndexRouteImport + parentRoute: typeof SecARoute + } + '/sec-f/settings': { + id: '/sec-f/settings' + path: '/settings' + fullPath: '/sec-f/settings' + preLoaderRoute: typeof SecFSettingsRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/about': { + id: '/sec-f/about' + path: '/about' + fullPath: '/sec-f/about' + preLoaderRoute: typeof SecFAboutRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/$id': { + id: '/sec-f/$id' + path: '/$id' + fullPath: '/sec-f/$id' + preLoaderRoute: typeof SecFIdRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/settings': { + id: '/sec-e/settings' + path: '/settings' + fullPath: '/sec-e/settings' + preLoaderRoute: typeof SecESettingsRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/about': { + id: '/sec-e/about' + path: '/about' + fullPath: '/sec-e/about' + preLoaderRoute: typeof SecEAboutRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/$id': { + id: '/sec-e/$id' + path: '/$id' + fullPath: '/sec-e/$id' + preLoaderRoute: typeof SecEIdRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/settings': { + id: '/sec-d/settings' + path: '/settings' + fullPath: '/sec-d/settings' + preLoaderRoute: typeof SecDSettingsRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/about': { + id: '/sec-d/about' + path: '/about' + fullPath: '/sec-d/about' + preLoaderRoute: typeof SecDAboutRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/$id': { + id: '/sec-d/$id' + path: '/$id' + fullPath: '/sec-d/$id' + preLoaderRoute: typeof SecDIdRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/settings': { + id: '/sec-c/settings' + path: '/settings' + fullPath: '/sec-c/settings' + preLoaderRoute: typeof SecCSettingsRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/about': { + id: '/sec-c/about' + path: '/about' + fullPath: '/sec-c/about' + preLoaderRoute: typeof SecCAboutRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/$id': { + id: '/sec-c/$id' + path: '/$id' + fullPath: '/sec-c/$id' + preLoaderRoute: typeof SecCIdRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/settings': { + id: '/sec-b/settings' + path: '/settings' + fullPath: '/sec-b/settings' + preLoaderRoute: typeof SecBSettingsRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/about': { + id: '/sec-b/about' + path: '/about' + fullPath: '/sec-b/about' + preLoaderRoute: typeof SecBAboutRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/$id': { + id: '/sec-b/$id' + path: '/$id' + fullPath: '/sec-b/$id' + preLoaderRoute: typeof SecBIdRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/settings': { + id: '/sec-a/settings' + path: '/settings' + fullPath: '/sec-a/settings' + preLoaderRoute: typeof SecASettingsRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/about': { + id: '/sec-a/about' + path: '/about' + fullPath: '/sec-a/about' + preLoaderRoute: typeof SecAAboutRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/$id': { + id: '/sec-a/$id' + path: '/$id' + fullPath: '/sec-a/$id' + preLoaderRoute: typeof SecAIdRouteImport + parentRoute: typeof SecARoute + } + '/release/v{$version}': { + id: '/release/v{$version}' + path: '/release/v{$version}' + fullPath: '/release/v{$version}' + preLoaderRoute: typeof ReleaseVChar123versionChar125RouteImport + parentRoute: typeof rootRouteImport + } + '/files/$': { + id: '/files/$' + path: '/files/$' + fullPath: '/files/$' + preLoaderRoute: typeof FilesSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout/beta': { + id: '/_layout/beta' + path: '/beta' + fullPath: '/beta' + preLoaderRoute: typeof LayoutBetaRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/alpha': { + id: '/_layout/alpha' + path: '/alpha' + fullPath: '/alpha' + preLoaderRoute: typeof LayoutAlphaRouteImport + parentRoute: typeof LayoutRoute + } + '/(marketing)/promo': { + id: '/(marketing)/promo' + path: '/promo' + fullPath: '/promo' + preLoaderRoute: typeof marketingPromoRouteImport + parentRoute: typeof rootRouteImport + } + '/(marketing)/pricing': { + id: '/(marketing)/pricing' + path: '/pricing' + fullPath: '/pricing' + preLoaderRoute: typeof marketingPricingRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface LayoutRouteChildren { + LayoutAlphaRoute: typeof LayoutAlphaRoute + LayoutBetaRoute: typeof LayoutBetaRoute +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAlphaRoute: LayoutAlphaRoute, + LayoutBetaRoute: LayoutBetaRoute, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface SecARouteChildren { + SecAIdRoute: typeof SecAIdRoute + SecAAboutRoute: typeof SecAAboutRoute + SecASettingsRoute: typeof SecASettingsRoute + SecAIndexRoute: typeof SecAIndexRoute +} + +const SecARouteChildren: SecARouteChildren = { + SecAIdRoute: SecAIdRoute, + SecAAboutRoute: SecAAboutRoute, + SecASettingsRoute: SecASettingsRoute, + SecAIndexRoute: SecAIndexRoute, +} + +const SecARouteWithChildren = SecARoute._addFileChildren(SecARouteChildren) + +interface SecBRouteChildren { + SecBIdRoute: typeof SecBIdRoute + SecBAboutRoute: typeof SecBAboutRoute + SecBSettingsRoute: typeof SecBSettingsRoute + SecBIndexRoute: typeof SecBIndexRoute +} + +const SecBRouteChildren: SecBRouteChildren = { + SecBIdRoute: SecBIdRoute, + SecBAboutRoute: SecBAboutRoute, + SecBSettingsRoute: SecBSettingsRoute, + SecBIndexRoute: SecBIndexRoute, +} + +const SecBRouteWithChildren = SecBRoute._addFileChildren(SecBRouteChildren) + +interface SecCRouteChildren { + SecCIdRoute: typeof SecCIdRoute + SecCAboutRoute: typeof SecCAboutRoute + SecCSettingsRoute: typeof SecCSettingsRoute + SecCIndexRoute: typeof SecCIndexRoute +} + +const SecCRouteChildren: SecCRouteChildren = { + SecCIdRoute: SecCIdRoute, + SecCAboutRoute: SecCAboutRoute, + SecCSettingsRoute: SecCSettingsRoute, + SecCIndexRoute: SecCIndexRoute, +} + +const SecCRouteWithChildren = SecCRoute._addFileChildren(SecCRouteChildren) + +interface SecDRouteChildren { + SecDIdRoute: typeof SecDIdRoute + SecDAboutRoute: typeof SecDAboutRoute + SecDSettingsRoute: typeof SecDSettingsRoute + SecDIndexRoute: typeof SecDIndexRoute +} + +const SecDRouteChildren: SecDRouteChildren = { + SecDIdRoute: SecDIdRoute, + SecDAboutRoute: SecDAboutRoute, + SecDSettingsRoute: SecDSettingsRoute, + SecDIndexRoute: SecDIndexRoute, +} + +const SecDRouteWithChildren = SecDRoute._addFileChildren(SecDRouteChildren) + +interface SecERouteChildren { + SecEIdRoute: typeof SecEIdRoute + SecEAboutRoute: typeof SecEAboutRoute + SecESettingsRoute: typeof SecESettingsRoute + SecEIndexRoute: typeof SecEIndexRoute +} + +const SecERouteChildren: SecERouteChildren = { + SecEIdRoute: SecEIdRoute, + SecEAboutRoute: SecEAboutRoute, + SecESettingsRoute: SecESettingsRoute, + SecEIndexRoute: SecEIndexRoute, +} + +const SecERouteWithChildren = SecERoute._addFileChildren(SecERouteChildren) + +interface SecFRouteChildren { + SecFIdRoute: typeof SecFIdRoute + SecFAboutRoute: typeof SecFAboutRoute + SecFSettingsRoute: typeof SecFSettingsRoute + SecFIndexRoute: typeof SecFIndexRoute +} + +const SecFRouteChildren: SecFRouteChildren = { + SecFIdRoute: SecFIdRoute, + SecFAboutRoute: SecFAboutRoute, + SecFSettingsRoute: SecFSettingsRoute, + SecFIndexRoute: SecFIndexRoute, +} + +const SecFRouteWithChildren = SecFRoute._addFileChildren(SecFRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + SecARoute: SecARouteWithChildren, + SecBRoute: SecBRouteWithChildren, + SecCRoute: SecCRouteWithChildren, + SecDRoute: SecDRouteWithChildren, + SecERoute: SecERouteWithChildren, + SecFRoute: SecFRouteWithChildren, + marketingPricingRoute: marketingPricingRoute, + marketingPromoRoute: marketingPromoRoute, + FilesSplatRoute: FilesSplatRoute, + ReleaseVChar123versionChar125Route: ReleaseVChar123versionChar125Route, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/pricing.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/pricing.tsx new file mode 100644 index 0000000000..0965507de6 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/pricing.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/(marketing)/pricing')({ + component: Page, +}) + +function Page() { + return
pricing
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/promo.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/promo.tsx new file mode 100644 index 0000000000..220bd06a68 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/(marketing)/promo.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/(marketing)/promo')({ + component: Page, +}) + +function Page() { + return
promo
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..a4e4113d9a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/__root.tsx @@ -0,0 +1,58 @@ +import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + <> + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.alpha.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.alpha.tsx new file mode 100644 index 0000000000..234b8488a9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.alpha.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/alpha')({ + component: Page, +}) + +function Page() { + return
alpha
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.beta.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.beta.tsx new file mode 100644 index 0000000000..39bc716efa --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.beta.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout/beta')({ + component: Page, +}) + +function Page() { + return
beta
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.tsx new file mode 100644 index 0000000000..f1e499d3b3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/_layout.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_layout')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/files.$.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/files.$.tsx new file mode 100644 index 0000000000..47b1a5eea9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/files.$.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/files/$')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`files:${params()._splat}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/index.tsx new file mode 100644 index 0000000000..25b1c03c1a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Page, +}) + +function Page() { + return
home
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/release.v{$version}.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/release.v{$version}.tsx new file mode 100644 index 0000000000..0dbcee35ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/release.v{$version}.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/release/v{$version}')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`release:${params().version}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.$id.tsx new file mode 100644 index 0000000000..18731a2844 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-a:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.about.tsx new file mode 100644 index 0000000000..1699360a1c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/about')({ + component: Page, +}) + +function Page() { + return
sec-a:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.index.tsx new file mode 100644 index 0000000000..e71e525d07 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/')({ + component: Page, +}) + +function Page() { + return
sec-a:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.settings.tsx new file mode 100644 index 0000000000..49d470190e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a/settings')({ + component: Page, +}) + +function Page() { + return
sec-a:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.tsx new file mode 100644 index 0000000000..2861169fb9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-a.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-a')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.$id.tsx new file mode 100644 index 0000000000..86fe71d7c0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-b:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.about.tsx new file mode 100644 index 0000000000..586b829cd0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/about')({ + component: Page, +}) + +function Page() { + return
sec-b:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.index.tsx new file mode 100644 index 0000000000..7098416658 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/')({ + component: Page, +}) + +function Page() { + return
sec-b:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.settings.tsx new file mode 100644 index 0000000000..c2e0f47650 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b/settings')({ + component: Page, +}) + +function Page() { + return
sec-b:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.tsx new file mode 100644 index 0000000000..f2905da9ca --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-b.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-b')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.$id.tsx new file mode 100644 index 0000000000..3e07e8033a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-c:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.about.tsx new file mode 100644 index 0000000000..86ed5043a3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/about')({ + component: Page, +}) + +function Page() { + return
sec-c:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.index.tsx new file mode 100644 index 0000000000..aa9ed2dc6c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/')({ + component: Page, +}) + +function Page() { + return
sec-c:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.settings.tsx new file mode 100644 index 0000000000..d0dd3e952c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c/settings')({ + component: Page, +}) + +function Page() { + return
sec-c:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.tsx new file mode 100644 index 0000000000..fa53825238 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-c.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-c')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.$id.tsx new file mode 100644 index 0000000000..3722012d7c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-d:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.about.tsx new file mode 100644 index 0000000000..860d2b9e5b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/about')({ + component: Page, +}) + +function Page() { + return
sec-d:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.index.tsx new file mode 100644 index 0000000000..ec7b8eaf2c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/')({ + component: Page, +}) + +function Page() { + return
sec-d:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.settings.tsx new file mode 100644 index 0000000000..d83e3885fd --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d/settings')({ + component: Page, +}) + +function Page() { + return
sec-d:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.tsx new file mode 100644 index 0000000000..e71391e2a3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-d.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-d')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.$id.tsx new file mode 100644 index 0000000000..e58d9d77ac --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-e:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.about.tsx new file mode 100644 index 0000000000..3ef4d2c763 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/about')({ + component: Page, +}) + +function Page() { + return
sec-e:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.index.tsx new file mode 100644 index 0000000000..88bf7ca271 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/')({ + component: Page, +}) + +function Page() { + return
sec-e:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.settings.tsx new file mode 100644 index 0000000000..8a9945926d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e/settings')({ + component: Page, +}) + +function Page() { + return
sec-e:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.tsx new file mode 100644 index 0000000000..8b5ef24519 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-e.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-e')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.$id.tsx new file mode 100644 index 0000000000..2b81ef96ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/$id')({ + component: Page, +}) + +function Page() { + const params = Route.useParams() + + return
{`sec-f:${params().id}`}
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.about.tsx new file mode 100644 index 0000000000..1c4e3a5474 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.about.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/about')({ + component: Page, +}) + +function Page() { + return
sec-f:about
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.index.tsx new file mode 100644 index 0000000000..65803ee848 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/')({ + component: Page, +}) + +function Page() { + return
sec-f:index
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.settings.tsx new file mode 100644 index 0000000000..5761138262 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.settings.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f/settings')({ + component: Page, +}) + +function Page() { + return
sec-f:settings
+} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.tsx new file mode 100644 index 0000000000..c7f25eecdd --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/src/routes/sec-f.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/sec-f')({ + component: Layout, +}) + +function Layout() { + return +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/tsconfig.json b/benchmarks/client-nav/scenarios/route-tree-scale/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts new file mode 100644 index 0000000000..dd0bd234e0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/solid/vite.config.ts @@ -0,0 +1,51 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + // Real file-based apps code-split by default; this scenario keeps it on + // so navigations also exercise lazy route-chunk resolution. + autoCodeSplitting: true, + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav route-tree-scale (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/project.json b/benchmarks/client-nav/scenarios/route-tree-scale/vue/project.json new file mode 100644 index 0000000000..f613070038 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-route-tree-scale-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/route-tree-scale/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/setup.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts new file mode 100644 index 0000000000..a767b8d220 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-route-tree-scale', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-route-tree-scale navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx new file mode 100644 index 0000000000..c96d3f3a2e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/main.tsx @@ -0,0 +1,39 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..a03c051a79 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routeTree.gen.ts @@ -0,0 +1,881 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as SecFRouteImport } from './routes/sec-f' +import { Route as SecERouteImport } from './routes/sec-e' +import { Route as SecDRouteImport } from './routes/sec-d' +import { Route as SecCRouteImport } from './routes/sec-c' +import { Route as SecBRouteImport } from './routes/sec-b' +import { Route as SecARouteImport } from './routes/sec-a' +import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SecFIndexRouteImport } from './routes/sec-f.index' +import { Route as SecEIndexRouteImport } from './routes/sec-e.index' +import { Route as SecDIndexRouteImport } from './routes/sec-d.index' +import { Route as SecCIndexRouteImport } from './routes/sec-c.index' +import { Route as SecBIndexRouteImport } from './routes/sec-b.index' +import { Route as SecAIndexRouteImport } from './routes/sec-a.index' +import { Route as SecFSettingsRouteImport } from './routes/sec-f.settings' +import { Route as SecFAboutRouteImport } from './routes/sec-f.about' +import { Route as SecFIdRouteImport } from './routes/sec-f.$id' +import { Route as SecESettingsRouteImport } from './routes/sec-e.settings' +import { Route as SecEAboutRouteImport } from './routes/sec-e.about' +import { Route as SecEIdRouteImport } from './routes/sec-e.$id' +import { Route as SecDSettingsRouteImport } from './routes/sec-d.settings' +import { Route as SecDAboutRouteImport } from './routes/sec-d.about' +import { Route as SecDIdRouteImport } from './routes/sec-d.$id' +import { Route as SecCSettingsRouteImport } from './routes/sec-c.settings' +import { Route as SecCAboutRouteImport } from './routes/sec-c.about' +import { Route as SecCIdRouteImport } from './routes/sec-c.$id' +import { Route as SecBSettingsRouteImport } from './routes/sec-b.settings' +import { Route as SecBAboutRouteImport } from './routes/sec-b.about' +import { Route as SecBIdRouteImport } from './routes/sec-b.$id' +import { Route as SecASettingsRouteImport } from './routes/sec-a.settings' +import { Route as SecAAboutRouteImport } from './routes/sec-a.about' +import { Route as SecAIdRouteImport } from './routes/sec-a.$id' +import { Route as ReleaseVChar123versionChar125RouteImport } from './routes/release.v{$version}' +import { Route as FilesSplatRouteImport } from './routes/files.$' +import { Route as LayoutBetaRouteImport } from './routes/_layout.beta' +import { Route as LayoutAlphaRouteImport } from './routes/_layout.alpha' +import { Route as marketingPromoRouteImport } from './routes/(marketing)/promo' +import { Route as marketingPricingRouteImport } from './routes/(marketing)/pricing' + +const SecFRoute = SecFRouteImport.update({ + id: '/sec-f', + path: '/sec-f', + getParentRoute: () => rootRouteImport, +} as any) +const SecERoute = SecERouteImport.update({ + id: '/sec-e', + path: '/sec-e', + getParentRoute: () => rootRouteImport, +} as any) +const SecDRoute = SecDRouteImport.update({ + id: '/sec-d', + path: '/sec-d', + getParentRoute: () => rootRouteImport, +} as any) +const SecCRoute = SecCRouteImport.update({ + id: '/sec-c', + path: '/sec-c', + getParentRoute: () => rootRouteImport, +} as any) +const SecBRoute = SecBRouteImport.update({ + id: '/sec-b', + path: '/sec-b', + getParentRoute: () => rootRouteImport, +} as any) +const SecARoute = SecARouteImport.update({ + id: '/sec-a', + path: '/sec-a', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SecFIndexRoute = SecFIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecFRoute, +} as any) +const SecEIndexRoute = SecEIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecERoute, +} as any) +const SecDIndexRoute = SecDIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecDRoute, +} as any) +const SecCIndexRoute = SecCIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecCRoute, +} as any) +const SecBIndexRoute = SecBIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecBRoute, +} as any) +const SecAIndexRoute = SecAIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SecARoute, +} as any) +const SecFSettingsRoute = SecFSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecFRoute, +} as any) +const SecFAboutRoute = SecFAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecFRoute, +} as any) +const SecFIdRoute = SecFIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecFRoute, +} as any) +const SecESettingsRoute = SecESettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecERoute, +} as any) +const SecEAboutRoute = SecEAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecERoute, +} as any) +const SecEIdRoute = SecEIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecERoute, +} as any) +const SecDSettingsRoute = SecDSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecDRoute, +} as any) +const SecDAboutRoute = SecDAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecDRoute, +} as any) +const SecDIdRoute = SecDIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecDRoute, +} as any) +const SecCSettingsRoute = SecCSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecCRoute, +} as any) +const SecCAboutRoute = SecCAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecCRoute, +} as any) +const SecCIdRoute = SecCIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecCRoute, +} as any) +const SecBSettingsRoute = SecBSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecBRoute, +} as any) +const SecBAboutRoute = SecBAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecBRoute, +} as any) +const SecBIdRoute = SecBIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecBRoute, +} as any) +const SecASettingsRoute = SecASettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => SecARoute, +} as any) +const SecAAboutRoute = SecAAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => SecARoute, +} as any) +const SecAIdRoute = SecAIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => SecARoute, +} as any) +const ReleaseVChar123versionChar125Route = + ReleaseVChar123versionChar125RouteImport.update({ + id: '/release/v{$version}', + path: '/release/v{$version}', + getParentRoute: () => rootRouteImport, + } as any) +const FilesSplatRoute = FilesSplatRouteImport.update({ + id: '/files/$', + path: '/files/$', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutBetaRoute = LayoutBetaRouteImport.update({ + id: '/beta', + path: '/beta', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutAlphaRoute = LayoutAlphaRouteImport.update({ + id: '/alpha', + path: '/alpha', + getParentRoute: () => LayoutRoute, +} as any) +const marketingPromoRoute = marketingPromoRouteImport.update({ + id: '/(marketing)/promo', + path: '/promo', + getParentRoute: () => rootRouteImport, +} as any) +const marketingPricingRoute = marketingPricingRouteImport.update({ + id: '/(marketing)/pricing', + path: '/pricing', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/pricing': typeof marketingPricingRoute + '/promo': typeof marketingPromoRoute + '/alpha': typeof LayoutAlphaRoute + '/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a': typeof SecAIndexRoute + '/sec-b': typeof SecBIndexRoute + '/sec-c': typeof SecCIndexRoute + '/sec-d': typeof SecDIndexRoute + '/sec-e': typeof SecEIndexRoute + '/sec-f': typeof SecFIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren + '/sec-a': typeof SecARouteWithChildren + '/sec-b': typeof SecBRouteWithChildren + '/sec-c': typeof SecCRouteWithChildren + '/sec-d': typeof SecDRouteWithChildren + '/sec-e': typeof SecERouteWithChildren + '/sec-f': typeof SecFRouteWithChildren + '/(marketing)/pricing': typeof marketingPricingRoute + '/(marketing)/promo': typeof marketingPromoRoute + '/_layout/alpha': typeof LayoutAlphaRoute + '/_layout/beta': typeof LayoutBetaRoute + '/files/$': typeof FilesSplatRoute + '/release/v{$version}': typeof ReleaseVChar123versionChar125Route + '/sec-a/$id': typeof SecAIdRoute + '/sec-a/about': typeof SecAAboutRoute + '/sec-a/settings': typeof SecASettingsRoute + '/sec-b/$id': typeof SecBIdRoute + '/sec-b/about': typeof SecBAboutRoute + '/sec-b/settings': typeof SecBSettingsRoute + '/sec-c/$id': typeof SecCIdRoute + '/sec-c/about': typeof SecCAboutRoute + '/sec-c/settings': typeof SecCSettingsRoute + '/sec-d/$id': typeof SecDIdRoute + '/sec-d/about': typeof SecDAboutRoute + '/sec-d/settings': typeof SecDSettingsRoute + '/sec-e/$id': typeof SecEIdRoute + '/sec-e/about': typeof SecEAboutRoute + '/sec-e/settings': typeof SecESettingsRoute + '/sec-f/$id': typeof SecFIdRoute + '/sec-f/about': typeof SecFAboutRoute + '/sec-f/settings': typeof SecFSettingsRoute + '/sec-a/': typeof SecAIndexRoute + '/sec-b/': typeof SecBIndexRoute + '/sec-c/': typeof SecCIndexRoute + '/sec-d/': typeof SecDIndexRoute + '/sec-e/': typeof SecEIndexRoute + '/sec-f/': typeof SecFIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/pricing' + | '/promo' + | '/alpha' + | '/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + id: + | '__root__' + | '/' + | '/_layout' + | '/sec-a' + | '/sec-b' + | '/sec-c' + | '/sec-d' + | '/sec-e' + | '/sec-f' + | '/(marketing)/pricing' + | '/(marketing)/promo' + | '/_layout/alpha' + | '/_layout/beta' + | '/files/$' + | '/release/v{$version}' + | '/sec-a/$id' + | '/sec-a/about' + | '/sec-a/settings' + | '/sec-b/$id' + | '/sec-b/about' + | '/sec-b/settings' + | '/sec-c/$id' + | '/sec-c/about' + | '/sec-c/settings' + | '/sec-d/$id' + | '/sec-d/about' + | '/sec-d/settings' + | '/sec-e/$id' + | '/sec-e/about' + | '/sec-e/settings' + | '/sec-f/$id' + | '/sec-f/about' + | '/sec-f/settings' + | '/sec-a/' + | '/sec-b/' + | '/sec-c/' + | '/sec-d/' + | '/sec-e/' + | '/sec-f/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren + SecARoute: typeof SecARouteWithChildren + SecBRoute: typeof SecBRouteWithChildren + SecCRoute: typeof SecCRouteWithChildren + SecDRoute: typeof SecDRouteWithChildren + SecERoute: typeof SecERouteWithChildren + SecFRoute: typeof SecFRouteWithChildren + marketingPricingRoute: typeof marketingPricingRoute + marketingPromoRoute: typeof marketingPromoRoute + FilesSplatRoute: typeof FilesSplatRoute + ReleaseVChar123versionChar125Route: typeof ReleaseVChar123versionChar125Route +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/sec-f': { + id: '/sec-f' + path: '/sec-f' + fullPath: '/sec-f' + preLoaderRoute: typeof SecFRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-e': { + id: '/sec-e' + path: '/sec-e' + fullPath: '/sec-e' + preLoaderRoute: typeof SecERouteImport + parentRoute: typeof rootRouteImport + } + '/sec-d': { + id: '/sec-d' + path: '/sec-d' + fullPath: '/sec-d' + preLoaderRoute: typeof SecDRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-c': { + id: '/sec-c' + path: '/sec-c' + fullPath: '/sec-c' + preLoaderRoute: typeof SecCRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-b': { + id: '/sec-b' + path: '/sec-b' + fullPath: '/sec-b' + preLoaderRoute: typeof SecBRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-a': { + id: '/sec-a' + path: '/sec-a' + fullPath: '/sec-a' + preLoaderRoute: typeof SecARouteImport + parentRoute: typeof rootRouteImport + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/sec-f/': { + id: '/sec-f/' + path: '/' + fullPath: '/sec-f/' + preLoaderRoute: typeof SecFIndexRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/': { + id: '/sec-e/' + path: '/' + fullPath: '/sec-e/' + preLoaderRoute: typeof SecEIndexRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/': { + id: '/sec-d/' + path: '/' + fullPath: '/sec-d/' + preLoaderRoute: typeof SecDIndexRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/': { + id: '/sec-c/' + path: '/' + fullPath: '/sec-c/' + preLoaderRoute: typeof SecCIndexRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/': { + id: '/sec-b/' + path: '/' + fullPath: '/sec-b/' + preLoaderRoute: typeof SecBIndexRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/': { + id: '/sec-a/' + path: '/' + fullPath: '/sec-a/' + preLoaderRoute: typeof SecAIndexRouteImport + parentRoute: typeof SecARoute + } + '/sec-f/settings': { + id: '/sec-f/settings' + path: '/settings' + fullPath: '/sec-f/settings' + preLoaderRoute: typeof SecFSettingsRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/about': { + id: '/sec-f/about' + path: '/about' + fullPath: '/sec-f/about' + preLoaderRoute: typeof SecFAboutRouteImport + parentRoute: typeof SecFRoute + } + '/sec-f/$id': { + id: '/sec-f/$id' + path: '/$id' + fullPath: '/sec-f/$id' + preLoaderRoute: typeof SecFIdRouteImport + parentRoute: typeof SecFRoute + } + '/sec-e/settings': { + id: '/sec-e/settings' + path: '/settings' + fullPath: '/sec-e/settings' + preLoaderRoute: typeof SecESettingsRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/about': { + id: '/sec-e/about' + path: '/about' + fullPath: '/sec-e/about' + preLoaderRoute: typeof SecEAboutRouteImport + parentRoute: typeof SecERoute + } + '/sec-e/$id': { + id: '/sec-e/$id' + path: '/$id' + fullPath: '/sec-e/$id' + preLoaderRoute: typeof SecEIdRouteImport + parentRoute: typeof SecERoute + } + '/sec-d/settings': { + id: '/sec-d/settings' + path: '/settings' + fullPath: '/sec-d/settings' + preLoaderRoute: typeof SecDSettingsRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/about': { + id: '/sec-d/about' + path: '/about' + fullPath: '/sec-d/about' + preLoaderRoute: typeof SecDAboutRouteImport + parentRoute: typeof SecDRoute + } + '/sec-d/$id': { + id: '/sec-d/$id' + path: '/$id' + fullPath: '/sec-d/$id' + preLoaderRoute: typeof SecDIdRouteImport + parentRoute: typeof SecDRoute + } + '/sec-c/settings': { + id: '/sec-c/settings' + path: '/settings' + fullPath: '/sec-c/settings' + preLoaderRoute: typeof SecCSettingsRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/about': { + id: '/sec-c/about' + path: '/about' + fullPath: '/sec-c/about' + preLoaderRoute: typeof SecCAboutRouteImport + parentRoute: typeof SecCRoute + } + '/sec-c/$id': { + id: '/sec-c/$id' + path: '/$id' + fullPath: '/sec-c/$id' + preLoaderRoute: typeof SecCIdRouteImport + parentRoute: typeof SecCRoute + } + '/sec-b/settings': { + id: '/sec-b/settings' + path: '/settings' + fullPath: '/sec-b/settings' + preLoaderRoute: typeof SecBSettingsRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/about': { + id: '/sec-b/about' + path: '/about' + fullPath: '/sec-b/about' + preLoaderRoute: typeof SecBAboutRouteImport + parentRoute: typeof SecBRoute + } + '/sec-b/$id': { + id: '/sec-b/$id' + path: '/$id' + fullPath: '/sec-b/$id' + preLoaderRoute: typeof SecBIdRouteImport + parentRoute: typeof SecBRoute + } + '/sec-a/settings': { + id: '/sec-a/settings' + path: '/settings' + fullPath: '/sec-a/settings' + preLoaderRoute: typeof SecASettingsRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/about': { + id: '/sec-a/about' + path: '/about' + fullPath: '/sec-a/about' + preLoaderRoute: typeof SecAAboutRouteImport + parentRoute: typeof SecARoute + } + '/sec-a/$id': { + id: '/sec-a/$id' + path: '/$id' + fullPath: '/sec-a/$id' + preLoaderRoute: typeof SecAIdRouteImport + parentRoute: typeof SecARoute + } + '/release/v{$version}': { + id: '/release/v{$version}' + path: '/release/v{$version}' + fullPath: '/release/v{$version}' + preLoaderRoute: typeof ReleaseVChar123versionChar125RouteImport + parentRoute: typeof rootRouteImport + } + '/files/$': { + id: '/files/$' + path: '/files/$' + fullPath: '/files/$' + preLoaderRoute: typeof FilesSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout/beta': { + id: '/_layout/beta' + path: '/beta' + fullPath: '/beta' + preLoaderRoute: typeof LayoutBetaRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/alpha': { + id: '/_layout/alpha' + path: '/alpha' + fullPath: '/alpha' + preLoaderRoute: typeof LayoutAlphaRouteImport + parentRoute: typeof LayoutRoute + } + '/(marketing)/promo': { + id: '/(marketing)/promo' + path: '/promo' + fullPath: '/promo' + preLoaderRoute: typeof marketingPromoRouteImport + parentRoute: typeof rootRouteImport + } + '/(marketing)/pricing': { + id: '/(marketing)/pricing' + path: '/pricing' + fullPath: '/pricing' + preLoaderRoute: typeof marketingPricingRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface LayoutRouteChildren { + LayoutAlphaRoute: typeof LayoutAlphaRoute + LayoutBetaRoute: typeof LayoutBetaRoute +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAlphaRoute: LayoutAlphaRoute, + LayoutBetaRoute: LayoutBetaRoute, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +interface SecARouteChildren { + SecAIdRoute: typeof SecAIdRoute + SecAAboutRoute: typeof SecAAboutRoute + SecASettingsRoute: typeof SecASettingsRoute + SecAIndexRoute: typeof SecAIndexRoute +} + +const SecARouteChildren: SecARouteChildren = { + SecAIdRoute: SecAIdRoute, + SecAAboutRoute: SecAAboutRoute, + SecASettingsRoute: SecASettingsRoute, + SecAIndexRoute: SecAIndexRoute, +} + +const SecARouteWithChildren = SecARoute._addFileChildren(SecARouteChildren) + +interface SecBRouteChildren { + SecBIdRoute: typeof SecBIdRoute + SecBAboutRoute: typeof SecBAboutRoute + SecBSettingsRoute: typeof SecBSettingsRoute + SecBIndexRoute: typeof SecBIndexRoute +} + +const SecBRouteChildren: SecBRouteChildren = { + SecBIdRoute: SecBIdRoute, + SecBAboutRoute: SecBAboutRoute, + SecBSettingsRoute: SecBSettingsRoute, + SecBIndexRoute: SecBIndexRoute, +} + +const SecBRouteWithChildren = SecBRoute._addFileChildren(SecBRouteChildren) + +interface SecCRouteChildren { + SecCIdRoute: typeof SecCIdRoute + SecCAboutRoute: typeof SecCAboutRoute + SecCSettingsRoute: typeof SecCSettingsRoute + SecCIndexRoute: typeof SecCIndexRoute +} + +const SecCRouteChildren: SecCRouteChildren = { + SecCIdRoute: SecCIdRoute, + SecCAboutRoute: SecCAboutRoute, + SecCSettingsRoute: SecCSettingsRoute, + SecCIndexRoute: SecCIndexRoute, +} + +const SecCRouteWithChildren = SecCRoute._addFileChildren(SecCRouteChildren) + +interface SecDRouteChildren { + SecDIdRoute: typeof SecDIdRoute + SecDAboutRoute: typeof SecDAboutRoute + SecDSettingsRoute: typeof SecDSettingsRoute + SecDIndexRoute: typeof SecDIndexRoute +} + +const SecDRouteChildren: SecDRouteChildren = { + SecDIdRoute: SecDIdRoute, + SecDAboutRoute: SecDAboutRoute, + SecDSettingsRoute: SecDSettingsRoute, + SecDIndexRoute: SecDIndexRoute, +} + +const SecDRouteWithChildren = SecDRoute._addFileChildren(SecDRouteChildren) + +interface SecERouteChildren { + SecEIdRoute: typeof SecEIdRoute + SecEAboutRoute: typeof SecEAboutRoute + SecESettingsRoute: typeof SecESettingsRoute + SecEIndexRoute: typeof SecEIndexRoute +} + +const SecERouteChildren: SecERouteChildren = { + SecEIdRoute: SecEIdRoute, + SecEAboutRoute: SecEAboutRoute, + SecESettingsRoute: SecESettingsRoute, + SecEIndexRoute: SecEIndexRoute, +} + +const SecERouteWithChildren = SecERoute._addFileChildren(SecERouteChildren) + +interface SecFRouteChildren { + SecFIdRoute: typeof SecFIdRoute + SecFAboutRoute: typeof SecFAboutRoute + SecFSettingsRoute: typeof SecFSettingsRoute + SecFIndexRoute: typeof SecFIndexRoute +} + +const SecFRouteChildren: SecFRouteChildren = { + SecFIdRoute: SecFIdRoute, + SecFAboutRoute: SecFAboutRoute, + SecFSettingsRoute: SecFSettingsRoute, + SecFIndexRoute: SecFIndexRoute, +} + +const SecFRouteWithChildren = SecFRoute._addFileChildren(SecFRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, + SecARoute: SecARouteWithChildren, + SecBRoute: SecBRouteWithChildren, + SecCRoute: SecCRouteWithChildren, + SecDRoute: SecDRouteWithChildren, + SecERoute: SecERouteWithChildren, + SecFRoute: SecFRouteWithChildren, + marketingPricingRoute: marketingPricingRoute, + marketingPromoRoute: marketingPromoRoute, + FilesSplatRoute: FilesSplatRoute, + ReleaseVChar123versionChar125Route: ReleaseVChar123versionChar125Route, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/pricing.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/pricing.tsx new file mode 100644 index 0000000000..f606c10b4b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/pricing.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
pricing
+ }, +}) + +export const Route = createFileRoute('/(marketing)/pricing')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/promo.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/promo.tsx new file mode 100644 index 0000000000..61c3eb3ec9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/(marketing)/promo.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
promo
+ }, +}) + +export const Route = createFileRoute('/(marketing)/promo')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..d2e1483106 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute } from '@tanstack/vue-router' + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.alpha.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.alpha.tsx new file mode 100644 index 0000000000..ff6ad5350e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.alpha.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
alpha
+ }, +}) + +export const Route = createFileRoute('/_layout/alpha')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.beta.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.beta.tsx new file mode 100644 index 0000000000..2f7cab6c0a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.beta.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
beta
+ }, +}) + +export const Route = createFileRoute('/_layout/beta')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.tsx new file mode 100644 index 0000000000..e8221f0f65 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/_layout.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/_layout')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/files.$.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/files.$.tsx new file mode 100644 index 0000000000..221fae01be --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/files.$.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`files:${params.value._splat}`}
+ ) + }, +}) + +export const Route = createFileRoute('/files/$')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/index.tsx new file mode 100644 index 0000000000..b3e94f37c4 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
home
+ }, +}) + +export const Route = createFileRoute('/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/release.v{$version}.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/release.v{$version}.tsx new file mode 100644 index 0000000000..86b1a2c8f0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/release.v{$version}.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`release:${params.value.version}`}
+ ) + }, +}) + +export const Route = createFileRoute('/release/v{$version}')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.$id.tsx new file mode 100644 index 0000000000..864074940d --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-a:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-a/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.about.tsx new file mode 100644 index 0000000000..715ab9548c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-a:about
+ }, +}) + +export const Route = createFileRoute('/sec-a/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.index.tsx new file mode 100644 index 0000000000..baef08547c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-a:index
+ }, +}) + +export const Route = createFileRoute('/sec-a/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.settings.tsx new file mode 100644 index 0000000000..9e6a56b913 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-a:settings
+ }, +}) + +export const Route = createFileRoute('/sec-a/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.tsx new file mode 100644 index 0000000000..a9fb557aea --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-a.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-a')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.$id.tsx new file mode 100644 index 0000000000..9a03a8fe01 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-b:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-b/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.about.tsx new file mode 100644 index 0000000000..2ef1100995 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-b:about
+ }, +}) + +export const Route = createFileRoute('/sec-b/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.index.tsx new file mode 100644 index 0000000000..652e5f895c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-b:index
+ }, +}) + +export const Route = createFileRoute('/sec-b/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.settings.tsx new file mode 100644 index 0000000000..e8fa36377e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-b:settings
+ }, +}) + +export const Route = createFileRoute('/sec-b/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.tsx new file mode 100644 index 0000000000..ba31612633 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-b.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-b')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.$id.tsx new file mode 100644 index 0000000000..7f0407d84e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-c:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-c/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.about.tsx new file mode 100644 index 0000000000..d26776d439 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-c:about
+ }, +}) + +export const Route = createFileRoute('/sec-c/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.index.tsx new file mode 100644 index 0000000000..1835eff9b7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-c:index
+ }, +}) + +export const Route = createFileRoute('/sec-c/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.settings.tsx new file mode 100644 index 0000000000..1bf85fd6d9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-c:settings
+ }, +}) + +export const Route = createFileRoute('/sec-c/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.tsx new file mode 100644 index 0000000000..009f814b93 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-c.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-c')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.$id.tsx new file mode 100644 index 0000000000..ff5aab05e3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-d:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-d/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.about.tsx new file mode 100644 index 0000000000..25a8078626 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-d:about
+ }, +}) + +export const Route = createFileRoute('/sec-d/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.index.tsx new file mode 100644 index 0000000000..cd9b89664a --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-d:index
+ }, +}) + +export const Route = createFileRoute('/sec-d/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.settings.tsx new file mode 100644 index 0000000000..8e4309de32 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-d:settings
+ }, +}) + +export const Route = createFileRoute('/sec-d/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.tsx new file mode 100644 index 0000000000..6b12b514da --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-d.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-d')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.$id.tsx new file mode 100644 index 0000000000..4525051967 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-e:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-e/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.about.tsx new file mode 100644 index 0000000000..71a32bd36b --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-e:about
+ }, +}) + +export const Route = createFileRoute('/sec-e/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.index.tsx new file mode 100644 index 0000000000..9b467ca53e --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-e:index
+ }, +}) + +export const Route = createFileRoute('/sec-e/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.settings.tsx new file mode 100644 index 0000000000..a01066488c --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-e:settings
+ }, +}) + +export const Route = createFileRoute('/sec-e/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.tsx new file mode 100644 index 0000000000..38f4da91fb --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-e.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-e')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.$id.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.$id.tsx new file mode 100644 index 0000000000..c740f9d223 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.$id.tsx @@ -0,0 +1,16 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + const params = Route.useParams() + + return () => ( +
{`sec-f:${params.value.id}`}
+ ) + }, +}) + +export const Route = createFileRoute('/sec-f/$id')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.about.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.about.tsx new file mode 100644 index 0000000000..229dab22a2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.about.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-f:about
+ }, +}) + +export const Route = createFileRoute('/sec-f/about')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.index.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.index.tsx new file mode 100644 index 0000000000..14e3f46a13 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.index.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-f:index
+ }, +}) + +export const Route = createFileRoute('/sec-f/')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.settings.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.settings.tsx new file mode 100644 index 0000000000..da2c7483b9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.settings.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const Page = Vue.defineComponent({ + setup() { + return () =>
sec-f:settings
+ }, +}) + +export const Route = createFileRoute('/sec-f/settings')({ + component: Page, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.tsx b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.tsx new file mode 100644 index 0000000000..3b389c8144 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/src/routes/sec-f.tsx @@ -0,0 +1,12 @@ +import * as Vue from 'vue' +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +const Layout = Vue.defineComponent({ + setup() { + return () => + }, +}) + +export const Route = createFileRoute('/sec-f')({ + component: Layout, +}) diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/tsconfig.json b/benchmarks/client-nav/scenarios/route-tree-scale/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts b/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts new file mode 100644 index 0000000000..a547406198 --- /dev/null +++ b/benchmarks/client-nav/scenarios/route-tree-scale/vue/vite.config.ts @@ -0,0 +1,45 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + // Real file-based apps code-split by default; this scenario keeps it on + // so navigations also exercise lazy route-chunk resolution. + autoCodeSplitting: true, + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav route-tree-scale (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/react/project.json b/benchmarks/client-nav/scenarios/search-params/react/project.json new file mode 100644 index 0000000000..f2d743f5f9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-search-params-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/search-params/react/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/setup.ts b/benchmarks/client-nav/scenarios/search-params/react/setup.ts new file mode 100644 index 0000000000..130583b190 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'React', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts new file mode 100644 index 0000000000..c534611bd0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-search-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-search-params navigation loop (react)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/search-params/react/speed.flame.ts b/benchmarks/client-nav/scenarios/search-params/react/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx new file mode 100644 index 0000000000..a68ef7bbb2 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/main.tsx @@ -0,0 +1,36 @@ +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultStructuralSharing: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const reactRoot = createRoot(container) + reactRoot.render() + + return { + router, + unmount() { + reactRoot.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/search-params/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..7380b704e1 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ProductsRouteImport } from './routes/products' +import { Route as CatalogRouteImport } from './routes/catalog' +import { Route as IndexRouteImport } from './routes/index' + +const ProductsRoute = ProductsRouteImport.update({ + id: '/products', + path: '/products', + getParentRoute: () => rootRouteImport, +} as any) +const CatalogRoute = CatalogRouteImport.update({ + id: '/catalog', + path: '/catalog', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/catalog' | '/products' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/catalog' | '/products' + id: '__root__' | '/' | '/catalog' | '/products' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CatalogRoute: typeof CatalogRoute + ProductsRoute: typeof ProductsRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/products': { + id: '/products' + path: '/products' + fullPath: '/products' + preLoaderRoute: typeof ProductsRouteImport + parentRoute: typeof rootRouteImport + } + '/catalog': { + id: '/catalog' + path: '/catalog' + fullPath: '/catalog' + preLoaderRoute: typeof CatalogRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CatalogRoute: CatalogRoute, + ProductsRoute: ProductsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/__root.tsx new file mode 100644 index 0000000000..86399d0175 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/__root.tsx @@ -0,0 +1,116 @@ +import { + Link, + Outlet, + createRootRoute, + useSearch, +} from '@tanstack/react-router' +import { + changedCategories, + computeChecksum, + initialProductsSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const rootSubscribers = Array.from({ length: 4 }, (_, index) => index) + +function RootSearchSubscriber() { + const value = useSearch({ + strict: false, + select: (search) => computeChecksum(Number(search.page ?? 0)), + }) + + void computeChecksum(value) + return null +} + +function RootComponent() { + return ( + <> + {rootSubscribers.map((index) => ( + + ))} + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/catalog.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/catalog.tsx new file mode 100644 index 0000000000..73f59c13b0 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/catalog.tsx @@ -0,0 +1,42 @@ +import { createFileRoute, retainSearchParams } from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' +import { + catalogMarkerText, + computeChecksum, + normalizeCatalogSearch, +} from '../../../shared' + +export const Route = createFileRoute('/catalog')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeCatalogSearch(search), + search: { + middlewares: [retainSearchParams(['perPage', 'sort'])], + }, + component: CatalogPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function ViewSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.view.length * 5 + search.page * 11), + }) + + void computeChecksum(value) + return null +} + +function CatalogPage() { + const search = Route.useSearch() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} +

Catalog

+
{catalogMarkerText(search)}
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/index.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/index.tsx new file mode 100644 index 0000000000..5879ad518d --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Search Params Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/src/routes/products.tsx b/benchmarks/client-nav/scenarios/search-params/react/src/routes/products.tsx new file mode 100644 index 0000000000..d22e99c4bf --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/src/routes/products.tsx @@ -0,0 +1,91 @@ +import { + createFileRoute, + retainSearchParams, + stripSearchParams, +} from '@tanstack/react-router' +import type { SearchSchemaInput } from '@tanstack/react-router' +import { + computeChecksum, + normalizeProductsSearch, + productsLoaderChecksum, + productsMarkerText, +} from '../../../shared' + +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeProductsSearch(search), + search: { + middlewares: [ + retainSearchParams(['perPage', 'sort']), + stripSearchParams({ page: 1 }), + ], + }, + loaderDeps: ({ search }) => ({ + page: search.page, + sort: search.sort, + filters: search.filters, + }), + loader: ({ deps }) => ({ + checksum: productsLoaderChecksum(deps), + }), + staleTime: 1e9, + gcTime: 1e9, + component: ProductsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PageSubscriber() { + const value = Route.useSearch({ + select: (search) => computeChecksum(search.page * 31 + search.perPage), + }) + + void computeChecksum(value) + return null +} + +function FiltersSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum( + search.filters.categories.length * 7 + + (search.filters.price.max - search.filters.price.min) + + search.filters.tags.length * 3, + ), + }) + + void computeChecksum(value) + return null +} + +function QuerySubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.q.length * 13 + search.sort.length), + }) + + void computeChecksum(value) + return null +} + +function ProductsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Products

+
{productsMarkerText(search)}
+

{`checksum ${loaderData.checksum}`}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/tsconfig.json b/benchmarks/client-nav/scenarios/search-params/react/tsconfig.json new file mode 100644 index 0000000000..1d59cbb450 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/search-params/react/vite.config.ts b/benchmarks/client-nav/scenarios/search-params/react/vite.config.ts new file mode 100644 index 0000000000..b220ae7d0d --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/react/vite.config.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'react', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav search-params (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/shared.ts b/benchmarks/client-nav/scenarios/search-params/shared.ts new file mode 100644 index 0000000000..343e319ded --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/shared.ts @@ -0,0 +1,200 @@ +/** + * Shared definition of the `search-params` scenario: search schema + * normalizers, the click sequence, and per-step sanity assertions. The three + * framework apps consume these builders so the workload is identical modulo + * the rendering layer. + * + * Isolates: validateSearch execution, search middlewares + * (retainSearchParams/stripSearchParams), parse/stringify of medium-complex + * search objects, functional search updaters, structural sharing, and + * useSearch selector subscriptions. + */ + +import type { ScenarioStep } from '../harness' + +export function computeChecksum(seed: number) { + let value = Math.trunc(seed) | 0 + + for (let index = 0; index < 40; index++) { + value = (value * 1664525 + 1013904223 + index) >>> 0 + } + + return value +} + +export interface ProductsSearch { + page: number + perPage: number + sort: 'asc' | 'desc' + q: string + filters: { + categories: Array + price: { min: number; max: number } + tags: Array + } +} + +export interface CatalogSearch { + view: 'grid' | 'list' + page: number + perPage: number + sort: 'asc' | 'desc' +} + +function normalizeNumber(value: unknown, fallback: number) { + const num = Number(value) + return Number.isFinite(num) && num >= 0 ? Math.trunc(num) : fallback +} + +function normalizeString(value: unknown, fallback: string) { + return typeof value === 'string' ? value : fallback +} + +function normalizeSort(value: unknown): 'asc' | 'desc' { + return value === 'desc' ? 'desc' : 'asc' +} + +function normalizeStringArray(value: unknown): Array { + if (!Array.isArray(value)) { + return [] + } + return value.filter((entry): entry is string => typeof entry === 'string') +} + +export function normalizeProductsSearch( + search: Record, +): ProductsSearch { + const filters = + typeof search.filters === 'object' && search.filters !== null + ? (search.filters as Record) + : {} + const price = + typeof filters.price === 'object' && filters.price !== null + ? (filters.price as Record) + : {} + + return { + page: Math.max(1, normalizeNumber(search.page, 1)), + perPage: Math.max(1, normalizeNumber(search.perPage, 20)), + sort: normalizeSort(search.sort), + q: normalizeString(search.q, ''), + filters: { + categories: normalizeStringArray(filters.categories), + price: { + min: normalizeNumber(price.min, 0), + max: normalizeNumber(price.max, 1000), + }, + tags: normalizeStringArray(filters.tags), + }, + } +} + +export function normalizeCatalogSearch( + search: Record, +): CatalogSearch { + return { + view: search.view === 'list' ? 'list' : 'grid', + page: Math.max(1, normalizeNumber(search.page, 1)), + perPage: Math.max(1, normalizeNumber(search.perPage, 20)), + sort: normalizeSort(search.sort), + } +} + +export const initialProductsSearch = { + page: 2, + perPage: 50, + sort: 'desc', + q: 'gadget', + filters: { + categories: ['audio', 'video'], + price: { min: 10, max: 200 }, + tags: ['new', 'sale'], + }, +} satisfies ProductsSearch + +export const changedCategories = ['x-ray', 'yield', 'zeta'] + +export function productsMarkerText(search: ProductsSearch) { + return `${search.page}|${search.sort}|${search.filters.categories.join(',')}` +} + +export function catalogMarkerText(search: CatalogSearch) { + return `${search.view}|${search.page}|${search.perPage}|${search.sort}` +} + +export function productsLoaderChecksum(deps: { + page: number + sort: string + filters: ProductsSearch['filters'] +}) { + return computeChecksum( + deps.page * 31 + + deps.sort.length * 7 + + deps.filters.categories.join('').length * 13 + + deps.filters.tags.length * 3 + + (deps.filters.price.max - deps.filters.price.min), + ) +} + +export const steps: ReadonlyArray = [ + 'go-products', + 'products-next-page', + 'products-categories', + 'products-sort', + 'products-reset', + 'go-catalog', + 'catalog-next-page', + 'products-partial', + 'go-home', +] + +interface ExpectedState { + testId: string + text?: string +} + +const expectedStates: ReadonlyArray = [ + // Full search literal from home. + { testId: 'products-state', text: '2|desc|audio,video' }, + // Functional updater bumping the page. + { testId: 'products-state', text: '3|desc|audio,video' }, + // Functional updater replacing filters.categories. + { testId: 'products-state', text: '3|desc|x-ray,yield,zeta' }, + // Functional updater flipping sort desc -> asc. + { testId: 'products-state', text: '3|asc|x-ray,yield,zeta' }, + // Functional updater resetting page to 1 (stripSearchParams removes it). + { testId: 'products-state', text: '1|asc|x-ray,yield,zeta' }, + // search: true carries the current products search into catalog. + { testId: 'catalog-state', text: 'grid|1|50|asc' }, + // Functional updater bumping the catalog page. + { testId: 'catalog-state', text: 'grid|2|50|asc' }, + // Partial search literal; retainSearchParams fills perPage/sort from catalog. + { testId: 'products-state', text: '1|asc|' }, + { testId: 'home-state' }, +] + +export function assertStepResult(stepIndex: number, container: HTMLElement) { + const expected = expectedStates[stepIndex]! + const element = container.querySelector(`[data-testid="${expected.testId}"]`) + + if (!element) { + throw new Error( + `Expected marker "${expected.testId}" to exist after step ${stepIndex}`, + ) + } + + if (expected.text !== undefined && element.textContent !== expected.text) { + throw new Error( + `Expected marker "${expected.testId}" to read "${expected.text}" after step ${stepIndex}, received "${element.textContent}"`, + ) + } +} + +// Two laps through the 9-step sequence per benchmark iteration. +export const ticksPerIteration = 18 + +export const benchOptions = { + warmupIterations: 100, + time: 10_000, + throws: true, +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/project.json b/benchmarks/client-nav/scenarios/search-params/solid/project.json new file mode 100644 index 0000000000..af48217e03 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-search-params-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/search-params/solid/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/setup.ts b/benchmarks/client-nav/scenarios/search-params/solid/setup.ts new file mode 100644 index 0000000000..6a46971603 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Solid', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts new file mode 100644 index 0000000000..df6a24dc15 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-search-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-search-params navigation loop (solid)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/search-params/solid/speed.flame.ts b/benchmarks/client-nav/scenarios/search-params/solid/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx new file mode 100644 index 0000000000..16814f03f7 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/main.tsx @@ -0,0 +1,33 @@ +import { render } from '@solidjs/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultStructuralSharing: true, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const unmount = render(() => , container) + + return { + router, + unmount, + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/search-params/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..96d211d64c --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ProductsRouteImport } from './routes/products' +import { Route as CatalogRouteImport } from './routes/catalog' +import { Route as IndexRouteImport } from './routes/index' + +const ProductsRoute = ProductsRouteImport.update({ + id: '/products', + path: '/products', + getParentRoute: () => rootRouteImport, +} as any) +const CatalogRoute = CatalogRouteImport.update({ + id: '/catalog', + path: '/catalog', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/catalog' | '/products' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/catalog' | '/products' + id: '__root__' | '/' | '/catalog' | '/products' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CatalogRoute: typeof CatalogRoute + ProductsRoute: typeof ProductsRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/products': { + id: '/products' + path: '/products' + fullPath: '/products' + preLoaderRoute: typeof ProductsRouteImport + parentRoute: typeof rootRouteImport + } + '/catalog': { + id: '/catalog' + path: '/catalog' + fullPath: '/catalog' + preLoaderRoute: typeof CatalogRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CatalogRoute: CatalogRoute, + ProductsRoute: ProductsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..e6f9592e07 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/__root.tsx @@ -0,0 +1,123 @@ +import { createRenderEffect } from 'solid-js' +import { + Link, + Outlet, + createRootRoute, + useSearch, +} from '@tanstack/solid-router' +import { + changedCategories, + computeChecksum, + initialProductsSearch, +} from '../../../shared' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +const rootSubscribers = Array.from({ length: 4 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect( + () => { + void props.value() + }, + () => {}, + ) + + return null +} + +function RootSearchSubscriber() { + const value = useSearch({ + strict: false, + select: (search) => computeChecksum(Number(search.page ?? 0)), + }) + + return computeChecksum(value())} /> +} + +function RootComponent() { + return ( + <> + {rootSubscribers.map(() => ( + + ))} + + + + ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/catalog.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/catalog.tsx new file mode 100644 index 0000000000..7f3fcf7078 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/catalog.tsx @@ -0,0 +1,53 @@ +import { createRenderEffect } from 'solid-js' +import { createFileRoute, retainSearchParams } from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' +import { + catalogMarkerText, + computeChecksum, + normalizeCatalogSearch, +} from '../../../shared' + +export const Route = createFileRoute('/catalog')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeCatalogSearch(search), + search: { + middlewares: [retainSearchParams(['perPage', 'sort'])], + }, + component: CatalogPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect( + () => { + void props.value() + }, + () => {}, + ) + + return null +} + +function ViewSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.view.length * 5 + search.page * 11), + }) + + return computeChecksum(value())} /> +} + +function CatalogPage() { + const search = Route.useSearch() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} +

Catalog

+
{catalogMarkerText(search())}
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/index.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/index.tsx new file mode 100644 index 0000000000..0bbb913d83 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + return ( +
+

Search Params Bench

+
home
+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/src/routes/products.tsx b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/products.tsx new file mode 100644 index 0000000000..1319aabae3 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/src/routes/products.tsx @@ -0,0 +1,100 @@ +import { createRenderEffect } from 'solid-js' +import { + createFileRoute, + retainSearchParams, + stripSearchParams, +} from '@tanstack/solid-router' +import type { SearchSchemaInput } from '@tanstack/solid-router' +import { + computeChecksum, + normalizeProductsSearch, + productsLoaderChecksum, + productsMarkerText, +} from '../../../shared' + +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeProductsSearch(search), + search: { + middlewares: [ + retainSearchParams(['perPage', 'sort']), + stripSearchParams({ page: 1 }), + ], + }, + loaderDeps: ({ search }) => ({ + page: search.page, + sort: search.sort, + filters: search.filters, + }), + loader: ({ deps }) => ({ + checksum: productsLoaderChecksum(deps), + }), + staleTime: 1e9, + gcTime: 1e9, + component: ProductsPage, +}) + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +function PerfValue(props: { value: () => number }) { + createRenderEffect( + () => { + void props.value() + }, + () => {}, + ) + + return null +} + +function PageSubscriber() { + const value = Route.useSearch({ + select: (search) => computeChecksum(search.page * 31 + search.perPage), + }) + + return computeChecksum(value())} /> +} + +function FiltersSubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum( + search.filters.categories.length * 7 + + (search.filters.price.max - search.filters.price.min) + + search.filters.tags.length * 3, + ), + }) + + return computeChecksum(value())} /> +} + +function QuerySubscriber() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.q.length * 13 + search.sort.length), + }) + + return computeChecksum(value())} /> +} + +function ProductsPage() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return ( +
+ {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} + {subscriberIndexes.map(() => ( + + ))} +

Products

+
{productsMarkerText(search())}
+

{`checksum ${loaderData().checksum}`}

+
+ ) +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/tsconfig.json b/benchmarks/client-nav/scenarios/search-params/solid/tsconfig.json new file mode 100644 index 0000000000..8c57ff876a --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/search-params/solid/vite.config.ts b/benchmarks/client-nav/scenarios/search-params/solid/vite.config.ts new file mode 100644 index 0000000000..c2f4b32806 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/solid/vite.config.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'solid', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + resolve: { + conditions: ['solid', 'browser'], + }, + test: { + name: '@benchmarks/client-nav search-params (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/project.json b/benchmarks/client-nav/scenarios/search-params/vue/project.json new file mode 100644 index 0000000000..47b9712a2c --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/project.json @@ -0,0 +1,40 @@ +{ + "name": "@benchmarks/client-nav-search-params-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router", "@tanstack/router-plugin"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": ["build:client"], + "options": { + "command": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./scenarios/search-params/vue/speed.flame.ts", + "cwd": "benchmarks/client-nav" + } + } + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/setup.ts b/benchmarks/client-nav/scenarios/search-params/vue/setup.ts new file mode 100644 index 0000000000..97b74b3892 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/setup.ts @@ -0,0 +1,17 @@ +import { createScenarioSetup } from '../../harness' +import { assertStepResult, steps } from '../shared' +import type * as App from './src/main' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createScenarioSetup({ + frameworkLabel: 'Vue', + mount: mountTestApp, + steps, + assertAfterStep: assertStepResult, + }) +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts b/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts new file mode 100644 index 0000000000..0b394bbe27 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/speed.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { benchOptions, ticksPerIteration } from '../shared' +import { setup } from './setup' + +describe('client-search-params', () => { + const test = setup() + + /** + * Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`, + * so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic. + * + * But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`, + * but it does support `beforeAll` and `afterAll`. + * + * So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended. + */ + + beforeAll(test.before) + afterAll(test.after) + + bench( + 'client-search-params navigation loop (vue)', + async () => { + for (let i = 0; i < ticksPerIteration; i++) { + await test.tick() + } + }, + { + ...benchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/speed.flame.ts b/benchmarks/client-nav/scenarios/search-params/vue/speed.flame.ts new file mode 100644 index 0000000000..f4a0b4e6ff --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/speed.flame.ts @@ -0,0 +1,18 @@ +import { window } from '../../../jsdom.ts' +import { setup } from './setup.ts' + +const DURATION_MS = 10_000 + +const test = setup() + +try { + await test.before() + + const startedAt = performance.now() + while (performance.now() - startedAt < DURATION_MS) { + await test.tick() + } +} finally { + test.after() + window.close() +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx new file mode 100644 index 0000000000..1454b8d547 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/main.tsx @@ -0,0 +1,40 @@ +import * as Vue from 'vue' +import { RouterProvider, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function createTestRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + // Key the scroll-restoration cache by pathname instead of the default + // random per-entry location key: with push navigations the default mints + // a fresh key per navigation and the module-level cache grows one entry + // per push for the whole run, which is non-stationary. + getScrollRestorationKey: (location) => location.pathname, + defaultStructuralSharing: true, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} + +export function mountTestApp(container: HTMLElement) { + const router = createTestRouter() + + const component = + const app = Vue.createApp({ + render: () => component, + }) + + app.mount(container) + + return { + router, + unmount() { + app.unmount() + }, + } +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routeTree.gen.ts b/benchmarks/client-nav/scenarios/search-params/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..b95b667803 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ProductsRouteImport } from './routes/products' +import { Route as CatalogRouteImport } from './routes/catalog' +import { Route as IndexRouteImport } from './routes/index' + +const ProductsRoute = ProductsRouteImport.update({ + id: '/products', + path: '/products', + getParentRoute: () => rootRouteImport, +} as any) +const CatalogRoute = CatalogRouteImport.update({ + id: '/catalog', + path: '/catalog', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/catalog': typeof CatalogRoute + '/products': typeof ProductsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/catalog' | '/products' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/catalog' | '/products' + id: '__root__' | '/' | '/catalog' | '/products' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CatalogRoute: typeof CatalogRoute + ProductsRoute: typeof ProductsRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/products': { + id: '/products' + path: '/products' + fullPath: '/products' + preLoaderRoute: typeof ProductsRouteImport + parentRoute: typeof rootRouteImport + } + '/catalog': { + id: '/catalog' + path: '/catalog' + fullPath: '/catalog' + preLoaderRoute: typeof CatalogRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CatalogRoute: CatalogRoute, + ProductsRoute: ProductsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/__root.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..b4cb07f2ae --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/__root.tsx @@ -0,0 +1,114 @@ +import * as Vue from 'vue' +import { Link, Outlet, createRootRoute, useSearch } from '@tanstack/vue-router' +import { + changedCategories, + computeChecksum, + initialProductsSearch, +} from '../../../shared' + +const rootSubscribers = Array.from({ length: 4 }, (_, index) => index) + +const RootSearchSubscriber = Vue.defineComponent({ + setup() { + const value = useSearch({ + strict: false, + select: (search) => computeChecksum(Number(search.page ?? 0)), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const RootComponent = Vue.defineComponent({ + setup() { + return () => ( + <> + {rootSubscribers.map((index) => ( + + ))} + + + + ) + }, +}) + +export const Route = createRootRoute({ + component: RootComponent, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/catalog.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/catalog.tsx new file mode 100644 index 0000000000..6a09efdd4a --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/catalog.tsx @@ -0,0 +1,49 @@ +import * as Vue from 'vue' +import { createFileRoute, retainSearchParams } from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' +import { + catalogMarkerText, + computeChecksum, + normalizeCatalogSearch, +} from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const ViewSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.view.length * 5 + search.page * 11), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const CatalogPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} +

Catalog

+
{catalogMarkerText(search.value)}
+
+ ) + }, +}) + +export const Route = createFileRoute('/catalog')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeCatalogSearch(search), + search: { + middlewares: [retainSearchParams(['perPage', 'sort'])], + }, + component: CatalogPage, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/index.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/index.tsx new file mode 100644 index 0000000000..081aa7b4fd --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/index.tsx @@ -0,0 +1,17 @@ +import * as Vue from 'vue' +import { createFileRoute } from '@tanstack/vue-router' + +const HomePage = Vue.defineComponent({ + setup() { + return () => ( +
+

Search Params Bench

+
home
+
+ ) + }, +}) + +export const Route = createFileRoute('/')({ + component: HomePage, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/src/routes/products.tsx b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/products.tsx new file mode 100644 index 0000000000..8be1683e14 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/src/routes/products.tsx @@ -0,0 +1,108 @@ +import * as Vue from 'vue' +import { + createFileRoute, + retainSearchParams, + stripSearchParams, +} from '@tanstack/vue-router' +import type { SearchSchemaInput } from '@tanstack/vue-router' +import { + computeChecksum, + normalizeProductsSearch, + productsLoaderChecksum, + productsMarkerText, +} from '../../../shared' + +const subscriberIndexes = Array.from({ length: 2 }, (_, index) => index) + +const PageSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => computeChecksum(search.page * 31 + search.perPage), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const FiltersSubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => + computeChecksum( + search.filters.categories.length * 7 + + (search.filters.price.max - search.filters.price.min) + + search.filters.tags.length * 3, + ), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const QuerySubscriber = Vue.defineComponent({ + setup() { + const value = Route.useSearch({ + select: (search) => + computeChecksum(search.q.length * 13 + search.sort.length), + }) + + return () => { + void computeChecksum(value.value) + return null + } + }, +}) + +const ProductsPage = Vue.defineComponent({ + setup() { + const search = Route.useSearch() + const loaderData = Route.useLoaderData() + + return () => ( +
+ {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} + {subscriberIndexes.map((index) => ( + + ))} +

Products

+
+ {productsMarkerText(search.value)} +
+

{`checksum ${loaderData.value.checksum}`}

+
+ ) + }, +}) + +export const Route = createFileRoute('/products')({ + validateSearch: (search: Record & SearchSchemaInput) => + normalizeProductsSearch(search), + search: { + middlewares: [ + retainSearchParams(['perPage', 'sort']), + stripSearchParams({ page: 1 }), + ], + }, + loaderDeps: ({ search }) => ({ + page: search.page, + sort: search.sort, + filters: search.filters, + }), + loader: ({ deps }) => ({ + checksum: productsLoaderChecksum(deps), + }), + staleTime: 1e9, + gcTime: 1e9, + component: ProductsPage, +}) diff --git a/benchmarks/client-nav/scenarios/search-params/vue/tsconfig.json b/benchmarks/client-nav/scenarios/search-params/vue/tsconfig.json new file mode 100644 index 0000000000..7786fc3da9 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "allowImportingTsExtensions": true, + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "speed.bench.ts", + "speed.flame.ts", + "setup.ts", + "vite.config.ts", + "../shared.ts", + "../../harness.ts", + "../../../setup-helpers.ts", + "../../../jsdom.ts", + "../../../vitest.setup.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/client-nav/scenarios/search-params/vue/vite.config.ts b/benchmarks/client-nav/scenarios/search-params/vue/vite.config.ts new file mode 100644 index 0000000000..9a3b82d066 --- /dev/null +++ b/benchmarks/client-nav/scenarios/search-params/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackRouter({ + target: 'vue', + routesDirectory: `${rootDir}src/routes`, + generatedRouteTree: `${rootDir}src/routeTree.gen.ts`, + }), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/main.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/client-nav search-params (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/client-nav/solid/vite.config.ts b/benchmarks/client-nav/solid/vite.config.ts index 24db8fe745..bd11fe8de9 100644 --- a/benchmarks/client-nav/solid/vite.config.ts +++ b/benchmarks/client-nav/solid/vite.config.ts @@ -1,8 +1,14 @@ +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import solid from 'vite-plugin-solid' import codspeedPlugin from '@codspeed/vitest-plugin' +// Anchor the project root to the package directory so this config resolves +// identically when run directly and as part of an aggregate `projects` config. +const rootDir = fileURLToPath(new URL('..', import.meta.url)) + export default defineConfig({ + root: rootDir, define: { 'process.env.NODE_ENV': JSON.stringify('production'), }, @@ -28,7 +34,7 @@ export default defineConfig({ name: '@benchmarks/client-nav (solid)', watch: false, environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], + setupFiles: [`${rootDir}vitest.setup.ts`], server: { deps: { inline: [/@solidjs/, /@tanstack\/solid-store/], diff --git a/benchmarks/client-nav/vitest.config.ts b/benchmarks/client-nav/vitest.config.ts index 14776452ed..808e7fc724 100644 --- a/benchmarks/client-nav/vitest.config.ts +++ b/benchmarks/client-nav/vitest.config.ts @@ -7,6 +7,9 @@ export default defineConfig({ './react/vite.config.ts', './solid/vite.config.ts', './vue/vite.config.ts', + './scenarios/*/react/vite.config.ts', + './scenarios/*/solid/vite.config.ts', + './scenarios/*/vue/vite.config.ts', ], }, }) diff --git a/benchmarks/client-nav/vitest.react.config.ts b/benchmarks/client-nav/vitest.react.config.ts new file mode 100644 index 0000000000..a397ad929e --- /dev/null +++ b/benchmarks/client-nav/vitest.react.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: ['./react/vite.config.ts', './scenarios/*/react/vite.config.ts'], + }, +}) diff --git a/benchmarks/client-nav/vitest.solid.config.ts b/benchmarks/client-nav/vitest.solid.config.ts new file mode 100644 index 0000000000..b8827b86aa --- /dev/null +++ b/benchmarks/client-nav/vitest.solid.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: ['./solid/vite.config.ts', './scenarios/*/solid/vite.config.ts'], + }, +}) diff --git a/benchmarks/client-nav/vitest.vue.config.ts b/benchmarks/client-nav/vitest.vue.config.ts new file mode 100644 index 0000000000..17960d7569 --- /dev/null +++ b/benchmarks/client-nav/vitest.vue.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: ['./vue/vite.config.ts', './scenarios/*/vue/vite.config.ts'], + }, +}) diff --git a/benchmarks/client-nav/vue/vite.config.ts b/benchmarks/client-nav/vue/vite.config.ts index d917c82e2d..4c44c09fdd 100644 --- a/benchmarks/client-nav/vue/vite.config.ts +++ b/benchmarks/client-nav/vue/vite.config.ts @@ -1,9 +1,15 @@ +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import codspeedPlugin from '@codspeed/vitest-plugin' +// Anchor the project root to the package directory so this config resolves +// identically when run directly and as part of an aggregate `projects` config. +const rootDir = fileURLToPath(new URL('..', import.meta.url)) + export default defineConfig({ + root: rootDir, define: { 'process.env.NODE_ENV': JSON.stringify('production'), }, @@ -27,6 +33,6 @@ export default defineConfig({ name: '@benchmarks/client-nav (vue)', watch: false, environment: 'jsdom', - setupFiles: ['./vitest.setup.ts'], + setupFiles: [`${rootDir}vitest.setup.ts`], }, }) diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md new file mode 100644 index 0000000000..cf871f3439 --- /dev/null +++ b/benchmarks/memory/README.md @@ -0,0 +1,202 @@ +# Memory Benchmarks + +Dedicated memory benchmarks for TanStack Router / Start, measured with the +CodSpeed **memory instrument** (`mode: memory` in +`.github/workflows/client-nav-benchmarks.yml`). The workflow runs on every +push to `main` except docs/examples/e2e-only pushes: CodSpeed compares a PR +against the run on its merge-base commit, and a main commit without a run makes +CodSpeed silently fall back to an older base — a single outlier base run then +flags phantom regressions on every PR until the next run lands. (Falling back +across a docs-only commit is safe: the previous run's benchmark-relevant code +is identical.) Two separate benchmarks: + +- `server/` (`@benchmarks/memory-server`) — React/Solid/Vue Start apps, requests against + the built server handler (`handler.fetch`), Node environment. +- `client/` (`@benchmarks/memory-client`) — router-only React/Solid/Vue apps in jsdom. + +These deliberately do **not** reuse the CPU scenarios in `benchmarks/ssr` and +`benchmarks/client-nav`: memory benches need their own iteration counts, +payload sizes, and route shapes, and tuning those must never shift the CPU +baselines. Each scenario keeps a framework level (`react/`, `solid/`, `vue/`) +so framework ports can be added without renames. + +## Layout + +```text +benchmarks/memory// + package.json Nx targets: build:, test:perf:, test:flame:, test:types + bench-utils.ts memoryBenchOptions, seeded LCG (+ sequential request loop on the server side) + vitest..config.ts aggregates scenarios/*//vite.config.ts + scenarios/// + one isolated app per scenario + setup.ts + memory.bench.ts + memory.flame.ts +``` + +One app per scenario; apps and bench names are stable once landed (CodSpeed +continuity). Never grow an existing scenario for a new case — add a scenario. +`setup.ts` imports the built app and exports the concrete workload; +`memory.bench.ts` registers `bench(...)` directly, and `memory.flame.ts` runs the +same workload through the Flame profiler. + +## How the memory instrument executes a bench + +- The bench function is warmed up, then **measured exactly once**, starting + after a forced GC. Under plain `vitest bench` the suites only smoke-test: + timing output is meaningless; real numbers come from CodSpeed. +- Under CodSpeed the bench fn runs several warmup invocations plus the + measured one **on the same mount**, so bench fns must be idempotent and + module-level counters/LCGs are used where ids must never repeat across + invocations. +- Plain `vitest bench` never runs suite hooks (`beforeAll`/`afterAll`) and + only honors tinybench's `setup`/`teardown` options; the CodSpeed runner + does the exact opposite. Client benches therefore register **both** — in + any given mode exactly one pair runs. +- The process runs with V8 determinism flags (predictable GC schedule, + `--no-opt`). Never call `global.gc()` manually in **churn** scenarios — + their signal is accumulation across iterations, which a forced collection + masks. **Peak** scenarios do the opposite: they set + `pinGcBetweenIterations` on the request loop so a collection runs between + iterations. Their signal is the footprint of a single request, and without + pinned GC points the measured peak flips by a whole payload depending on + whether iteration i's garbage is collected before iteration i+1 allocates. + Because of `--no-opt`, allocation counts overstate production; numbers are + for regression tracking, not absolute claims. +- Keep each bench under **~1.5M allocations** (instrument overhead grows past + 2M); this is the main constraint when tuning iteration counts. + +## Bench shapes and signals + +- **Churn (leak detector):** N sequential iterations at steady state. If one + iteration leaks L bytes, peak grows by ~N·L; healthy builds show a flat + timeline floor independent of N. Tuning check: doubling N must leave peak + roughly unchanged. +- **Peak (footprint):** one (or very few) large operations; peak memory + scaling with the workload is the signal. + +## Scenarios + +### Server + +| Scenario | Shape | Guards against | +| ----------------------- | ----- | --------------------------------------------------------- | +| `request-churn` | churn | cross-request retention in document SSR (unique URLs) | +| `server-fn-churn` | churn | retention in the server-function RPC path | +| `error-paths` | churn | redirect/notFound/error/unmatched paths pinning contexts | +| `aborted-requests` | churn | dangling streams/listeners after mid-stream client aborts | +| `peak-large-page` | peak | per-request peak scaling with page size | +| `streaming-peak` | peak | streaming buffering O(document) instead of O(chunk) | +| `serialization-payload` | peak | double-buffering / string-copy blowups in dehydration | + +### Client + +| Scenario | Shape | Guards against | +| ------------------------- | ----- | -------------------------------------------------------- | +| `navigation-churn` | churn | per-navigation retention at steady state | +| `unique-location-churn` | churn | unbounded href/search-keyed caches (never-repeated URLs) | +| `preload-churn` | churn | preload-cache eviction not releasing memory | +| `loader-data-retention` | churn | departed routes' loader data staying pinned (gcTime 0) | +| `mount-unmount` | churn | router instances not collectable after dispose | +| `interrupted-navigations` | churn | superseded navigations retaining closures/contexts | + +## Conventions + +- Strictly sequential work: at most one request/navigation in flight; each + server response is fully consumed before the next request. Pairing a single + navigation with its render signal via `Promise.all([navigate, rendered])` + is fine — never overlap distinct work items. +- Randomness only via the seeded LCG in `bench-utils.ts`; no `Math.random`, + `Date.now`, or timers with a nonzero delay — anywhere async ordering must be + staged (`streaming-peak`'s deferred sections, `aborted-requests`' deferred + loader data and post-abort drain), chain **0ms `setTimeout` hops** and count + event-loop turns instead. A wall-clock delay races renderer work differently + depending on runner load and instrumentation overhead, which makes the + single measured run non-reproducible; 0ms hops fire in registration order in + the timers phase, so the interleaving is a pure function of the schedule. +- Sanity assertions run once at module load and throw on wrong + status/markers, so a bench can never silently measure the wrong thing. +- Server requests follow `benchmarks/ssr` conventions: document GETs send + `accept: text/html`, server-fn requests send `sec-fetch-site: same-origin` + with bodies precomputed at module level. +- Client apps export `mountTestApp` from `app.tsx`; benches import the built + `dist/app.js`; navigations use `replace: true`; unmount does full teardown + (framework root, `__TSR_ROUTER__`, `history.destroy()`); large loader payloads + are never rendered into the DOM. +- `NODE_ENV=production` everywhere (the Nx targets set it). + +## Run + +Smoke-test the CodSpeed/Vitest benchmark entrypoints and typecheck the +scenarios: + +```bash +pnpm nx run @benchmarks/memory-server:test:perf:react --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-server:test:perf:solid --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-server:test:perf:vue --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client:test:perf:react --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client:test:perf:solid --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client:test:perf:vue --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-server:test:types --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client:test:types --outputStyle=stream --skipRemoteCache +``` + +Local attribution profiling, without CodSpeed CLI/login/sudo/upload, uses +`@datadog/pprof` heap sampling and `@platformatic/flame` only to render the +captured pprof files as HTML/Markdown. These targets rebuild the scenarios with +`--sourcemap true` so the generated profile reports can point back to source; +the normal CodSpeed benchmark builds are unchanged. Local aggregate scripts run +with `--parallel=1`, and scenario `test:flame` targets opt out of Nx parallelism +so profiling workloads do not overlap and bias each other. The Vitest aggregate +configs also set `fileParallelism: false` so benchmark files run sequentially +inside `test:perf:react`. + +```bash +pnpm benchmark:memory:server:flame +pnpm benchmark:memory:client:flame +pnpm benchmark:memory:server:flame:solid +pnpm benchmark:memory:client:flame:solid +pnpm benchmark:memory:server:flame:vue +pnpm benchmark:memory:client:flame:vue +``` + +To profile one scenario, run its `test:flame` target directly: + +```bash +pnpm nx run @benchmarks/memory-server-request-churn-react:test:flame --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client-navigation-churn-react:test:flame --outputStyle=stream --skipRemoteCache +``` + +Flame writes reports under the scenario's ignored `.profiles//` +directory, including `heap-profile-*.html` and `heap-profile-*.md`. The +`memory.flame.ts` entrypoints run the same workload shape as `memory.bench.ts` +but manually start profiling after sanity/setup work and stop it after the +measured workload. Treat these profiles as diagnostic heap-sampling attribution; +they are not CodSpeed memory metrics such as peak memory, allocated bytes, or +allocation counts. The heap sampler is stopped before profile conversion and +Flame report generation, so Flame/pprof report-generation work should not appear +as part of the captured workload. Flame runs do not force GC before profiling; +doing so would perturb the workload and still would not make heap sampling +equivalent to CodSpeed memory metrics. + +Clean local Flame profile output with: + +```bash +pnpm --filter @benchmarks/memory-server clean:profiles +pnpm --filter @benchmarks/memory-client clean:profiles +``` + +Client memory benches are useful for regression tracking of router/React/jsdom +integration behavior, especially retained route/cache data. They are not pure +browser-memory measurements, and local Flame attribution can include jsdom, +React DOM, and profiler shutdown frames. + +Real memory measurement, locally (requires the CodSpeed CLI, `codspeed setup` +once to install the memory executor, and sudo; **uploads results to the +CodSpeed dashboard** — local runs do not affect PR baselines): + +```bash +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-server:test:perf:react +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-server:test:perf:solid +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-server:test:perf:vue +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-client:test:perf:react +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-client:test:perf:solid +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-client:test:perf:vue +``` diff --git a/benchmarks/memory/client/bench-utils.ts b/benchmarks/memory/client/bench-utils.ts new file mode 100644 index 0000000000..0cbaedbd71 --- /dev/null +++ b/benchmarks/memory/client/bench-utils.ts @@ -0,0 +1,20 @@ +export const memoryBenchOptions = { + iterations: 1, + warmupIterations: 1, + time: 0, + warmupTime: 0, + throws: true, +} + +export function createDeterministicRandom(seed: number) { + let state = seed >>> 0 + + return () => { + state = (state * 1664525 + 1013904223) >>> 0 + return state / 0x100000000 + } +} + +export function randomSegment(random: () => number) { + return Math.floor(random() * 1_000_000_000).toString(36) +} diff --git a/benchmarks/memory/client/benchmark.ts b/benchmarks/memory/client/benchmark.ts new file mode 100644 index 0000000000..8b877b0740 --- /dev/null +++ b/benchmarks/memory/client/benchmark.ts @@ -0,0 +1,7 @@ +export interface ClientMemoryWorkload { + name: string + before?: () => Promise | void + run: () => Promise | void + sanity: () => Promise | void + after?: () => Promise | void +} diff --git a/benchmarks/memory/client/flame-runner.ts b/benchmarks/memory/client/flame-runner.ts new file mode 100644 index 0000000000..04941fda67 --- /dev/null +++ b/benchmarks/memory/client/flame-runner.ts @@ -0,0 +1,14 @@ +import { profileFlameWorkload } from '../flame-control.ts' +import { window } from './jsdom.ts' +import type { ClientMemoryWorkload } from './benchmark.ts' + +export async function runClientFlameBenchmark(workload: ClientMemoryWorkload) { + try { + await workload.sanity() + await workload.before?.() + await profileFlameWorkload(workload.run, workload.name) + } finally { + await workload.after?.() + window.close() + } +} diff --git a/benchmarks/memory/client/jsdom.ts b/benchmarks/memory/client/jsdom.ts new file mode 100644 index 0000000000..4df480df68 --- /dev/null +++ b/benchmarks/memory/client/jsdom.ts @@ -0,0 +1,51 @@ +import { JSDOM } from 'jsdom' + +const dom = new JSDOM('', { + url: 'http://localhost/', +}) + +const { window } = dom + +function setGlobal(name: string, value: unknown) { + Object.defineProperty(globalThis, name, { + value, + configurable: true, + writable: true, + }) +} + +setGlobal('window', window) +setGlobal('document', window.document) +setGlobal('self', window) +setGlobal('navigator', window.navigator) +setGlobal('location', window.location) +setGlobal('history', window.history) +setGlobal('HTMLElement', window.HTMLElement) +setGlobal('Element', window.Element) +setGlobal('SVGElement', window.SVGElement) +setGlobal('DocumentFragment', window.DocumentFragment) +setGlobal('Node', window.Node) +setGlobal('MouseEvent', window.MouseEvent) +setGlobal('MutationObserver', window.MutationObserver) +setGlobal('sessionStorage', window.sessionStorage) +setGlobal('localStorage', window.localStorage) +setGlobal('getComputedStyle', window.getComputedStyle.bind(window)) + +setGlobal( + 'requestAnimationFrame', + window.requestAnimationFrame?.bind(window) ?? + ((callback: (time: number) => void) => + setTimeout(() => callback(performance.now()), 16)), +) + +setGlobal( + 'cancelAnimationFrame', + window.cancelAnimationFrame?.bind(window) ?? + ((handle: number) => clearTimeout(handle)), +) + +const scrollTo = () => {} +window.scrollTo = scrollTo +setGlobal('scrollTo', scrollTo) + +export { window } diff --git a/benchmarks/memory/client/lifecycle.ts b/benchmarks/memory/client/lifecycle.ts new file mode 100644 index 0000000000..c4a62f0f96 --- /dev/null +++ b/benchmarks/memory/client/lifecycle.ts @@ -0,0 +1,46 @@ +export type Framework = 'react' | 'solid' | 'vue' + +export type MountedApp = { + router: unknown + unmount: () => void +} + +export type MountTestApp = (container: HTMLDivElement) => MountedApp + +const frameworkNames = { + react: 'React', + solid: 'Solid', + vue: 'Vue', +} satisfies Record + +export function noop() {} + +export function warnClientMemoryDevMode(framework: Framework) { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `memory client benchmark is running without NODE_ENV=production; ${frameworkNames[framework]} dev overhead will dominate results.`, + ) + } +} + +export function createBenchContainer() { + const container = document.createElement('div') + document.body.append(container) + + return container +} + +export function removeBenchContainer(container: HTMLDivElement | undefined) { + container?.remove() +} + +export function nextAnimationFrame() { + return new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) +} + +export async function drainMicrotasks() { + await Promise.resolve() + await Promise.resolve() +} diff --git a/benchmarks/memory/client/package.json b/benchmarks/memory/client/package.json new file mode 100644 index 0000000000..c80683e8df --- /dev/null +++ b/benchmarks/memory/client/package.json @@ -0,0 +1,261 @@ +{ + "name": "@benchmarks/memory-client", + "private": true, + "type": "module", + "scripts": { + "clean:profiles": "rm -rf scenarios/*/*/.profiles" + }, + "imports": { + "#memory-client/benchmark": "./benchmark.ts", + "#memory-client/bench-utils": "./bench-utils.ts", + "#memory-client/flame-runner": "./flame-runner.ts", + "#memory-client/lifecycle": "./lifecycle.ts" + }, + "dependencies": { + "@solidjs/web": "2.0.0-beta.15", + "@tanstack/react-router": "workspace:*", + "@tanstack/router-core": "workspace:*", + "@tanstack/solid-router": "workspace:*", + "@tanstack/vue-router": "workspace:*", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "solid-js": "2.0.0-beta.15", + "vue": "^3.5.16" + }, + "devDependencies": { + "@codspeed/vitest-plugin": "^5.5.0", + "@datadog/pprof": "^5.13.2", + "@platformatic/flame": "^1.6.0", + "@testing-library/react": "^16.2.0", + "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-vue": "^6.0.5", + "@vitejs/plugin-vue-jsx": "^5.1.5", + "@types/jsdom": "28.0.0", + "jsdom": "29.1.1", + "typescript": "^6.0.2", + "vite": "^8.0.14", + "vite-plugin-solid": "^3.0.0-next.5", + "vitest": "^4.1.4" + }, + "nx": { + "targets": { + "build:react": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-react", + "@benchmarks/memory-client-unique-location-churn-react", + "@benchmarks/memory-client-preload-churn-react", + "@benchmarks/memory-client-loader-data-retention-react", + "@benchmarks/memory-client-mount-unmount-react", + "@benchmarks/memory-client-interrupted-navigations-react" + ], + "target": "build:client" + } + ] + }, + "build:solid": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-solid", + "@benchmarks/memory-client-unique-location-churn-solid", + "@benchmarks/memory-client-preload-churn-solid", + "@benchmarks/memory-client-loader-data-retention-solid", + "@benchmarks/memory-client-mount-unmount-solid", + "@benchmarks/memory-client-interrupted-navigations-solid" + ], + "target": "build:client" + } + ] + }, + "build:vue": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-vue", + "@benchmarks/memory-client-unique-location-churn-vue", + "@benchmarks/memory-client-preload-churn-vue", + "@benchmarks/memory-client-loader-data-retention-vue", + "@benchmarks/memory-client-mount-unmount-vue", + "@benchmarks/memory-client-interrupted-navigations-vue" + ], + "target": "build:client" + } + ] + }, + "build:react:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-react", + "@benchmarks/memory-client-unique-location-churn-react", + "@benchmarks/memory-client-preload-churn-react", + "@benchmarks/memory-client-loader-data-retention-react", + "@benchmarks/memory-client-mount-unmount-react", + "@benchmarks/memory-client-interrupted-navigations-react" + ], + "target": "build:client:flame" + } + ] + }, + "build:solid:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-solid", + "@benchmarks/memory-client-unique-location-churn-solid", + "@benchmarks/memory-client-preload-churn-solid", + "@benchmarks/memory-client-loader-data-retention-solid", + "@benchmarks/memory-client-mount-unmount-solid", + "@benchmarks/memory-client-interrupted-navigations-solid" + ], + "target": "build:client:flame" + } + ] + }, + "build:vue:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-vue", + "@benchmarks/memory-client-unique-location-churn-vue", + "@benchmarks/memory-client-preload-churn-vue", + "@benchmarks/memory-client-loader-data-retention-vue", + "@benchmarks/memory-client-mount-unmount-vue", + "@benchmarks/memory-client-interrupted-navigations-vue" + ], + "target": "build:client:flame" + } + ] + }, + "test:flame:react": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-react", + "@benchmarks/memory-client-unique-location-churn-react", + "@benchmarks/memory-client-preload-churn-react", + "@benchmarks/memory-client-loader-data-retention-react", + "@benchmarks/memory-client-mount-unmount-react", + "@benchmarks/memory-client-interrupted-navigations-react" + ], + "target": "test:flame" + } + ] + }, + "test:flame:solid": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-solid", + "@benchmarks/memory-client-unique-location-churn-solid", + "@benchmarks/memory-client-preload-churn-solid", + "@benchmarks/memory-client-loader-data-retention-solid", + "@benchmarks/memory-client-mount-unmount-solid", + "@benchmarks/memory-client-interrupted-navigations-solid" + ], + "target": "test:flame" + } + ] + }, + "test:flame:vue": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-vue", + "@benchmarks/memory-client-unique-location-churn-vue", + "@benchmarks/memory-client-preload-churn-vue", + "@benchmarks/memory-client-loader-data-retention-vue", + "@benchmarks/memory-client-mount-unmount-vue", + "@benchmarks/memory-client-interrupted-navigations-vue" + ], + "target": "test:flame" + } + ] + }, + "test:perf:react": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:react" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.react.config.ts", + "cwd": "benchmarks/memory/client" + } + }, + "test:perf:solid": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:solid" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.solid.config.ts", + "cwd": "benchmarks/memory/client" + } + }, + "test:perf:vue": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:vue" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.vue.config.ts", + "cwd": "benchmarks/memory/client" + } + }, + "test:types": { + "executor": "nx:noop", + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-react", + "@benchmarks/memory-client-unique-location-churn-react", + "@benchmarks/memory-client-preload-churn-react", + "@benchmarks/memory-client-loader-data-retention-react", + "@benchmarks/memory-client-mount-unmount-react", + "@benchmarks/memory-client-interrupted-navigations-react", + "@benchmarks/memory-client-navigation-churn-solid", + "@benchmarks/memory-client-unique-location-churn-solid", + "@benchmarks/memory-client-preload-churn-solid", + "@benchmarks/memory-client-loader-data-retention-solid", + "@benchmarks/memory-client-mount-unmount-solid", + "@benchmarks/memory-client-interrupted-navigations-solid", + "@benchmarks/memory-client-navigation-churn-vue", + "@benchmarks/memory-client-unique-location-churn-vue", + "@benchmarks/memory-client-preload-churn-vue", + "@benchmarks/memory-client-loader-data-retention-vue", + "@benchmarks/memory-client-mount-unmount-vue", + "@benchmarks/memory-client-interrupted-navigations-vue" + ], + "target": "test:types:client" + } + ] + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json new file mode 100644 index 0000000000..0ed437c9ca --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-interrupted-navigations-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts new file mode 100644 index 0000000000..6e072ad5f3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts @@ -0,0 +1,19 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} = (await import(/* @vite-ignore */ appModulePath)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx new file mode 100644 index 0000000000..bd605a78ee --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx @@ -0,0 +1,37 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export { + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} from '../../slow-loaders' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..475085a433 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as FastIdRouteImport } from './routes/fast.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FastIdRoute = FastIdRouteImport.update({ + id: '/fast/$id', + path: '/fast/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/fast/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/fast/$id' | '/slow/$id' + id: '__root__' | '/' | '/fast/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + FastIdRoute: typeof FastIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/fast/$id': { + id: '/fast/$id' + path: '/fast/$id' + fullPath: '/fast/$id' + preLoaderRoute: typeof FastIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + FastIdRoute: FastIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx new file mode 100644 index 0000000000..ac27b20f79 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx new file mode 100644 index 0000000000..d5fd7ade46 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/react-router' +import { fixedTimestamp } from '../../../slow-loaders' + +export const Route = createFileRoute('/fast/$id')({ + loader: ({ params }) => ({ + id: params.id, + kind: 'fast' as const, + ts: fixedTimestamp, + }), + component: FastComponent, +}) + +function FastComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.kind}:${data.id}:${data.ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx new file mode 100644 index 0000000000..a425ac79c4 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
shell
+} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..61bbf328b7 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/react-router' +import { getSlowLoaderDeferred } from '../../../slow-loaders' + +export const Route = createFileRoute('/slow/$id')({ + loader: async ({ params }) => { + const deferred = getSlowLoaderDeferred(params.id) + + return await deferred.promise + }, + component: SlowComponent, +}) + +function SlowComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.kind}:${data.id}:${data.ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts new file mode 100644 index 0000000000..048a9cc833 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client interrupted-navigations (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts new file mode 100644 index 0000000000..f7f3a7cd8b --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts @@ -0,0 +1,320 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' +import { + createBenchContainer, + drainMicrotasks, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type NavigationSettlement = + | { + status: 'fulfilled' + value: void + } + | { + status: 'rejected' + reason: unknown + } + +type ResolveAllSlowLoaders = () => void +type ResolveSlowLoader = (id: string) => void +type SlowLoaderRegistry = { + has: (id: string) => boolean +} + +type RenderedEvent = { + toLocation: { + pathname: string + } +} + +type InterruptedNavigationRouter = { + latestLoadPromise: Promise | undefined + load: () => Promise + navigate: (options: { + to: '/fast/$id' | '/slow/$id' + params: { id: string } + replace: true + }) => Promise + subscribe: ( + event: 'onRendered', + listener: (event: RenderedEvent) => void, + ) => () => void +} + +const interruptedNavigationIterations = 150 +const interruptedNavigationPairs = createInterruptedNavigationPairs( + interruptedNavigationIterations, +) + +const uninitialized = () => + Promise.reject( + new Error('interrupted-navigations benchmark is not initialized'), + ) + +const uninitializedSettlement = () => + Promise.resolve({ + status: 'rejected', + reason: new Error('interrupted-navigations benchmark is not initialized'), + }) + +function createInterruptedNavigationPairs(iterations: number) { + const random = createDeterministicRandom(13) + + return Array.from({ length: iterations }, (_, index) => ({ + slowId: `slow-${index}-${randomSegment(random)}`, + fastId: `fast-${index}-${randomSegment(random)}`, + })) +} + +function formatReason(reason: unknown) { + if (reason instanceof Error) { + return `${reason.name}: ${reason.message}` + } + + return String(reason) +} + +function assertSlowNavigationSettlement(settlement: NavigationSettlement) { + if (settlement.status === 'fulfilled') { + if (settlement.value !== undefined) { + throw new Error('Expected slow navigation to fulfill with undefined') + } + + return + } + + if ( + reasonHasAbortShape(settlement.reason) || + reasonHasCancellationShape(settlement.reason) + ) { + return + } + + throw new Error( + `Expected slow navigation to settle as void or cancellation, got ${formatReason( + settlement.reason, + )}`, + ) +} + +async function awaitExpectedLoadSettlement(loadPromise: Promise) { + try { + await loadPromise + } catch (reason) { + if (reasonHasAbortShape(reason) || reasonHasCancellationShape(reason)) { + return + } + + throw reason + } +} + +function reasonHasAbortShape(reason: unknown) { + return reason instanceof DOMException && reason.name === 'AbortError' +} + +function reasonHasCancellationShape(reason: unknown) { + return ( + reason instanceof Error && + (reason.name === 'AbortError' || reason.name === 'CancelledError') + ) +} + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, + resolveAllSlowLoaders: ResolveAllSlowLoaders, + resolveSlowLoader: ResolveSlowLoader, + slowLoaderRegistry: SlowLoaderRegistry, +) { + warnClientMemoryDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop + let expectedRenderedPath: string | undefined = undefined + let navigateFast: (id: string) => Promise = uninitialized + let startSlowNavigation: (id: string) => Promise = + uninitializedSettlement + let getLatestLoadPromise: () => Promise | undefined = () => undefined + + function assertRenderedPage(page: 'shell' | 'fast', id?: string) { + const element = container?.querySelector('[data-bench-page]') + const actualPage = element?.dataset.benchPage + const actualId = element?.dataset.benchId + + if (actualPage !== page) { + throw new Error(`Expected rendered page ${page}, got ${actualPage}`) + } + + if (id !== undefined && actualId !== id) { + throw new Error(`Expected rendered id ${id}, got ${actualId}`) + } + } + + async function waitForRenderedPage(page: 'shell' | 'fast', id?: string) { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedPage(page, id) + return + } catch { + await nextAnimationFrame() + } + } + + assertRenderedPage(page, id) + } + + async function waitForSlowLoader(id: string) { + for (let attempt = 0; attempt < 20; attempt++) { + if (slowLoaderRegistry.has(id)) { + return + } + + await drainMicrotasks() + } + + throw new Error(`Slow loader was not registered for id: ${id}`) + } + + function waitForNextRender(pathname: string) { + expectedRenderedPath = pathname + + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = createBenchContainer() + + const mounted = mountTestApp(container) + const router = mounted.router as InterruptedNavigationRouter + unmount = mounted.unmount + getLatestLoadPromise = () => router.latestLoadPromise + + unsub = router.subscribe('onRendered', (event) => { + if ( + expectedRenderedPath && + event.toLocation.pathname !== expectedRenderedPath + ) { + return + } + + const resolve = resolveRendered + resolveRendered = noop + expectedRenderedPath = undefined + resolve() + }) + + navigateFast = async (id) => { + const rendered = waitForNextRender(`/fast/${id}`) + await Promise.all([ + router.navigate({ + to: '/fast/$id', + params: { id }, + replace: true, + }), + rendered, + ]) + } + + startSlowNavigation = (id) => { + const navigation = router.navigate({ + to: '/slow/$id', + params: { id }, + replace: true, + }) + + return navigation + .then((value): NavigationSettlement => ({ status: 'fulfilled', value })) + .catch( + (reason: unknown): NavigationSettlement => ({ + status: 'rejected', + reason, + }), + ) + } + + await router.load() + await waitForRenderedPage('shell') + } + + function after() { + resolveAllSlowLoaders() + unmount() + removeBenchContainer(container) + unsub() + + container = undefined + unmount = noop + unsub = noop + resolveRendered = noop + expectedRenderedPath = undefined + navigateFast = uninitialized + startSlowNavigation = uninitializedSettlement + getLatestLoadPromise = () => undefined + } + + async function interrupt( + slowId: string, + fastId: string, + assertSettlement = true, + ) { + const slowNavigation = startSlowNavigation(slowId) + + await waitForSlowLoader(slowId) + const slowLoadPromise = getLatestLoadPromise() + + if (!slowLoadPromise) { + throw new Error(`Slow navigation did not start a load for id: ${slowId}`) + } + + await navigateFast(fastId) + resolveSlowLoader(slowId) + + const settlement = await slowNavigation + + if (assertSettlement) { + assertSlowNavigationSettlement(settlement) + } + + await awaitExpectedLoadSettlement(slowLoadPromise) + await drainMicrotasks() + } + + return { + name: `mem client interrupted-navigations (${framework})`, + before, + interrupt, + async run() { + for (const pair of interruptedNavigationPairs) { + await interrupt(pair.slowId, pair.fastId) + } + }, + async sanity() { + await before() + + try { + assertRenderedPage('shell') + await interrupt('sanity-slow', 'sanity-fast', false) + assertRenderedPage('fast', 'sanity-fast') + } finally { + after() + } + }, + after, + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/slow-loaders.ts new file mode 100644 index 0000000000..c70ccac3da --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/slow-loaders.ts @@ -0,0 +1,54 @@ +export const fixedTimestamp = 1_700_000_000_000 + +type SlowLoaderPayload = { + id: string + kind: 'slow' + ts: number +} + +type SlowLoaderDeferred = { + promise: Promise + resolve: () => void +} + +export const slowLoaderRegistry = new Map() + +export function getSlowLoaderDeferred(id: string) { + const existing = slowLoaderRegistry.get(id) + + if (existing) { + return existing + } + + let resolvePromise!: (payload: SlowLoaderPayload) => void + const promise = new Promise((resolve) => { + resolvePromise = resolve + }) + + const deferred: SlowLoaderDeferred = { + promise, + resolve() { + slowLoaderRegistry.delete(id) + resolvePromise({ id, kind: 'slow', ts: fixedTimestamp }) + }, + } + + slowLoaderRegistry.set(id, deferred) + return deferred +} + +export function resolveSlowLoader(id: string) { + const deferred = slowLoaderRegistry.get(id) + + if (!deferred) { + throw new Error(`No pending slow loader for id: ${id}`) + } + + deferred.resolve() +} + +export function resolveAllSlowLoaders() { + for (const id of Array.from(slowLoaderRegistry.keys())) { + resolveSlowLoader(id) + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json new file mode 100644 index 0000000000..4aff39500e --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-interrupted-navigations-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts new file mode 100644 index 0000000000..e736111663 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts @@ -0,0 +1,19 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} = (await import(/* @vite-ignore */ appModulePath)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx new file mode 100644 index 0000000000..9264e1cc51 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx @@ -0,0 +1,35 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from '@solidjs/web' +import { getRouter } from './router' + +export { + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} from '../../slow-loaders' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..f42255963c --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as FastIdRouteImport } from './routes/fast.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FastIdRoute = FastIdRouteImport.update({ + id: '/fast/$id', + path: '/fast/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/fast/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/fast/$id' | '/slow/$id' + id: '__root__' | '/' | '/fast/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + FastIdRoute: typeof FastIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/fast/$id': { + id: '/fast/$id' + path: '/fast/$id' + fullPath: '/fast/$id' + preLoaderRoute: typeof FastIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + FastIdRoute: FastIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx new file mode 100644 index 0000000000..884aa3438f --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx new file mode 100644 index 0000000000..511e4712b7 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { fixedTimestamp } from '../../../slow-loaders' + +export const Route = createFileRoute('/fast/$id')({ + loader: ({ params }) => ({ + id: params.id, + kind: 'fast' as const, + ts: fixedTimestamp, + }), + component: FastComponent, +}) + +function FastComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data().kind}:${data().id}:${data().ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx new file mode 100644 index 0000000000..592b2d666e --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
shell
+} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..9aaaef5568 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { getSlowLoaderDeferred } from '../../../slow-loaders' + +export const Route = createFileRoute('/slow/$id')({ + loader: async ({ params }) => { + const deferred = getSlowLoaderDeferred(params.id) + + return await deferred.promise + }, + component: SlowComponent, +}) + +function SlowComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data().kind}:${data().id}:${data().ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json new file mode 100644 index 0000000000..9558353c88 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts new file mode 100644 index 0000000000..e710e0fd12 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client interrupted-navigations (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json new file mode 100644 index 0000000000..01288cb235 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-interrupted-navigations-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts new file mode 100644 index 0000000000..260e45b64b --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts @@ -0,0 +1,19 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} = (await import(/* @vite-ignore */ appModulePath)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx new file mode 100644 index 0000000000..1abb4410dc --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx @@ -0,0 +1,42 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export { + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} from '../../slow-loaders' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..5ea61a89a3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as FastIdRouteImport } from './routes/fast.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FastIdRoute = FastIdRouteImport.update({ + id: '/fast/$id', + path: '/fast/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/fast/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/fast/$id' | '/slow/$id' + id: '__root__' | '/' | '/fast/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + FastIdRoute: typeof FastIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/fast/$id': { + id: '/fast/$id' + path: '/fast/$id' + fullPath: '/fast/$id' + preLoaderRoute: typeof FastIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + FastIdRoute: FastIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx new file mode 100644 index 0000000000..4655fe8424 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx new file mode 100644 index 0000000000..3bd820bd07 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { fixedTimestamp } from '../../../slow-loaders' + +export const Route = createFileRoute('/fast/$id')({ + loader: ({ params }: { params: { id: string } }) => ({ + id: params.id, + kind: 'fast' as const, + ts: fixedTimestamp, + }), + component: FastComponent, +}) + +function FastComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.value.kind}:${data.value.id}:${data.value.ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx new file mode 100644 index 0000000000..c30a51e1e5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
shell
+} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..68885fb918 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { getSlowLoaderDeferred } from '../../../slow-loaders' + +export const Route = createFileRoute('/slow/$id')({ + loader: async ({ params }: { params: { id: string } }) => { + const deferred = getSlowLoaderDeferred(params.id) + + return await deferred.promise + }, + component: SlowComponent, +}) + +function SlowComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.value.kind}:${data.value.id}:${data.value.ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts new file mode 100644 index 0000000000..b2912ce9d1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client interrupted-navigations (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/loader-data.ts new file mode 100644 index 0000000000..3bd59c82ef --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/loader-data.ts @@ -0,0 +1,48 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' + +export const loaderPayloadRecordCount = 512 + +const loaderPayloadCharsPerRecord = 1024 + +type LoaderRecord = { + key: string + value: string +} + +export function createLoaderData(id: string) { + const random = createDeterministicRandom(hashId(id)) + const records: Array = [] + + for (let index = 0; index < loaderPayloadRecordCount; index++) { + records.push({ + key: `${id}:${index}:${randomSegment(random)}`, + value: createRecordValue(id, index, random), + }) + } + + return { id, records } +} + +function createRecordValue(id: string, index: number, random: () => number) { + const firstSegment = randomSegment(random) + const secondSegment = randomSegment(random) + const segment = `${id}:${index}:${firstSegment}:${secondSegment}:` + + return segment + .repeat(Math.ceil(loaderPayloadCharsPerRecord / segment.length)) + .slice(0, loaderPayloadCharsPerRecord) +} + +function hashId(id: string) { + let hash = 2166136261 + + for (let index = 0; index < id.length; index++) { + hash ^= id.charCodeAt(index) + hash = Math.imul(hash, 16777619) + } + + return hash >>> 0 +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json new file mode 100644 index 0000000000..713038e20f --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-loader-data-retention-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts new file mode 100644 index 0000000000..fafaf13e6f --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { loaderPayloadRecordCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, + loaderPayloadRecordCount, +) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx new file mode 100644 index 0000000000..5eef150c2c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx @@ -0,0 +1,33 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export { loaderPayloadRecordCount } from '../../loader-data' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..ea96d809c8 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts @@ -0,0 +1,102 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ShellRouteImport } from './routes/shell' +import { Route as PageIdRouteImport } from './routes/page.$id' +import { Route as ShellIndexRouteImport } from './routes/shell.index' + +const ShellRoute = ShellRouteImport.update({ + id: '/shell', + path: '/shell', + getParentRoute: () => rootRouteImport, +} as any) +const PageIdRoute = PageIdRouteImport.update({ + id: '/page/$id', + path: '/page/$id', + getParentRoute: () => rootRouteImport, +} as any) +const ShellIndexRoute = ShellIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShellRoute, +} as any) + +export interface FileRoutesByFullPath { + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRoutesByTo { + '/page/$id': typeof PageIdRoute + '/shell': typeof ShellIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/shell' | '/page/$id' | '/shell/' + fileRoutesByTo: FileRoutesByTo + to: '/page/$id' | '/shell' + id: '__root__' | '/shell' | '/page/$id' | '/shell/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ShellRoute: typeof ShellRouteWithChildren + PageIdRoute: typeof PageIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/shell': { + id: '/shell' + path: '/shell' + fullPath: '/shell' + preLoaderRoute: typeof ShellRouteImport + parentRoute: typeof rootRouteImport + } + '/page/$id': { + id: '/page/$id' + path: '/page/$id' + fullPath: '/page/$id' + preLoaderRoute: typeof PageIdRouteImport + parentRoute: typeof rootRouteImport + } + '/shell/': { + id: '/shell/' + path: '/' + fullPath: '/shell/' + preLoaderRoute: typeof ShellIndexRouteImport + parentRoute: typeof ShellRoute + } + } +} + +interface ShellRouteChildren { + ShellIndexRoute: typeof ShellIndexRoute +} + +const ShellRouteChildren: ShellRouteChildren = { + ShellIndexRoute: ShellIndexRoute, +} + +const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ShellRoute: ShellRouteWithChildren, + PageIdRoute: PageIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx new file mode 100644 index 0000000000..5a5c8210d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx @@ -0,0 +1,19 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/shell'], + }), + routeTree, + defaultGcTime: 0, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx new file mode 100644 index 0000000000..63af9ed429 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createLoaderData } from '../../../loader-data' + +export const Route = createFileRoute('/page/$id')({ + loader: ({ params }) => createLoaderData(params.id), + component: PageComponent, +}) + +function PageComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.id}:${data.records.length}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx new file mode 100644 index 0000000000..d78bb8bfb9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/shell/')({ + component: ShellIndexComponent, +}) + +function ShellIndexComponent() { + return
ready
+} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx new file mode 100644 index 0000000000..024a58c243 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/shell')({ + component: ShellComponent, +}) + +function ShellComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts new file mode 100644 index 0000000000..555e5f5612 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client loader-data-retention (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts new file mode 100644 index 0000000000..bd90c023f0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts @@ -0,0 +1,187 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' +import { + createBenchContainer, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type RenderEvent = { + toLocation: { + pathname: string + } +} + +type LoaderDataRouter = { + load: () => Promise + navigate: (options: { + to: '/page/$id' + params: { id: string } + replace: true + }) => Promise + subscribe: ( + event: 'onRendered', + listener: (event: RenderEvent) => void, + ) => () => void +} + +const loaderDataRetentionNavigationCount = 20 +const pageIds = createPageIds() + +const uninitialized = () => + Promise.reject( + new Error('loader-data-retention benchmark is not initialized'), + ) + +function createPageIds() { + const random = createDeterministicRandom(11) + + return Array.from( + { length: loaderDataRetentionNavigationCount }, + (_, index) => `${index}-${randomSegment(random)}`, + ) +} + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, + loaderPayloadRecordCount: number, +) { + warnClientMemoryDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop + let expectedRenderedPath: string | undefined = undefined + let navigateTo: (id: string) => Promise = uninitialized + + function assertRenderedShell() { + const actual = + container?.querySelector('[data-bench-page]')?.dataset + .benchPage + + if (actual !== 'shell') { + throw new Error(`Expected rendered shell page, got ${actual}`) + } + } + + function assertRenderedPage(id: string) { + const page = container?.querySelector( + '[data-bench-page="page"]', + ) + const actualId = page?.dataset.benchId + const actualCount = page?.dataset.benchCount + const expectedCount = String(loaderPayloadRecordCount) + + if (actualId !== id || actualCount !== expectedCount) { + throw new Error( + `Expected rendered page ${id}:${expectedCount}, got ${actualId}:${actualCount}`, + ) + } + } + + async function waitForRenderedShell() { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedShell() + return + } catch { + await nextAnimationFrame() + } + } + + assertRenderedShell() + } + + function waitForNextRender(pathname: string) { + expectedRenderedPath = pathname + + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = createBenchContainer() + + const mounted = mountTestApp(container) + const router = mounted.router as LoaderDataRouter + unmount = mounted.unmount + + unsub = router.subscribe('onRendered', (event) => { + if ( + expectedRenderedPath && + event.toLocation.pathname !== expectedRenderedPath + ) { + return + } + + const resolve = resolveRendered + resolveRendered = noop + expectedRenderedPath = undefined + resolve() + }) + + navigateTo = async (id) => { + const pathname = `/page/${id}` + const rendered = waitForNextRender(pathname) + + await router.navigate({ + to: '/page/$id', + params: { id }, + replace: true, + }) + await rendered + assertRenderedPage(id) + } + + await router.load() + await waitForRenderedShell() + } + + function after() { + unmount() + removeBenchContainer(container) + unsub() + + container = undefined + unmount = noop + unsub = noop + resolveRendered = noop + expectedRenderedPath = undefined + navigateTo = uninitialized + } + + return { + name: `mem client loader-data-retention (${framework})`, + before, + navigate: (id: string) => navigateTo(id), + async run() { + for (const id of pageIds) { + await navigateTo(id) + } + }, + async sanity() { + await before() + + try { + assertRenderedShell() + await navigateTo('sanity-a') + assertRenderedPage('sanity-a') + } finally { + after() + } + }, + after, + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json new file mode 100644 index 0000000000..6c85f51896 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-loader-data-retention-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts new file mode 100644 index 0000000000..b7c80a3e49 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { loaderPayloadRecordCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, + loaderPayloadRecordCount, +) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx new file mode 100644 index 0000000000..1b47cda1f9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx @@ -0,0 +1,31 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from '@solidjs/web' +import { getRouter } from './router' + +export { loaderPayloadRecordCount } from '../../loader-data' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..a1946d187d --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts @@ -0,0 +1,102 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ShellRouteImport } from './routes/shell' +import { Route as ShellIndexRouteImport } from './routes/shell.index' +import { Route as PageIdRouteImport } from './routes/page.$id' + +const ShellRoute = ShellRouteImport.update({ + id: '/shell', + path: '/shell', + getParentRoute: () => rootRouteImport, +} as any) +const ShellIndexRoute = ShellIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShellRoute, +} as any) +const PageIdRoute = PageIdRouteImport.update({ + id: '/page/$id', + path: '/page/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRoutesByTo { + '/page/$id': typeof PageIdRoute + '/shell': typeof ShellIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/shell' | '/page/$id' | '/shell/' + fileRoutesByTo: FileRoutesByTo + to: '/page/$id' | '/shell' + id: '__root__' | '/shell' | '/page/$id' | '/shell/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ShellRoute: typeof ShellRouteWithChildren + PageIdRoute: typeof PageIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/shell': { + id: '/shell' + path: '/shell' + fullPath: '/shell' + preLoaderRoute: typeof ShellRouteImport + parentRoute: typeof rootRouteImport + } + '/shell/': { + id: '/shell/' + path: '/' + fullPath: '/shell/' + preLoaderRoute: typeof ShellIndexRouteImport + parentRoute: typeof ShellRoute + } + '/page/$id': { + id: '/page/$id' + path: '/page/$id' + fullPath: '/page/$id' + preLoaderRoute: typeof PageIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface ShellRouteChildren { + ShellIndexRoute: typeof ShellIndexRoute +} + +const ShellRouteChildren: ShellRouteChildren = { + ShellIndexRoute: ShellIndexRoute, +} + +const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ShellRoute: ShellRouteWithChildren, + PageIdRoute: PageIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx new file mode 100644 index 0000000000..06afdb10d3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx @@ -0,0 +1,19 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/shell'], + }), + routeTree, + defaultGcTime: 0, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx new file mode 100644 index 0000000000..fb6c438fa5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createLoaderData } from '../../../loader-data' + +export const Route = createFileRoute('/page/$id')({ + loader: ({ params }) => createLoaderData(params.id), + component: PageComponent, +}) + +function PageComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data().id}:${data().records.length}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx new file mode 100644 index 0000000000..2f4f5daee2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/shell/')({ + component: ShellIndexComponent, +}) + +function ShellIndexComponent() { + return
ready
+} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx new file mode 100644 index 0000000000..c1e38a1aaf --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/shell')({ + component: ShellComponent, +}) + +function ShellComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json new file mode 100644 index 0000000000..9558353c88 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts new file mode 100644 index 0000000000..58d6b77acc --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client loader-data-retention (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json new file mode 100644 index 0000000000..22504c906f --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-loader-data-retention-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts new file mode 100644 index 0000000000..a83a1f70d1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { loaderPayloadRecordCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, + loaderPayloadRecordCount, +) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx new file mode 100644 index 0000000000..c843b4feb5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx @@ -0,0 +1,38 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export { loaderPayloadRecordCount } from '../../loader-data' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..5b09d365e7 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts @@ -0,0 +1,102 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ShellRouteImport } from './routes/shell' +import { Route as PageIdRouteImport } from './routes/page.$id' +import { Route as ShellIndexRouteImport } from './routes/shell.index' + +const ShellRoute = ShellRouteImport.update({ + id: '/shell', + path: '/shell', + getParentRoute: () => rootRouteImport, +} as any) +const PageIdRoute = PageIdRouteImport.update({ + id: '/page/$id', + path: '/page/$id', + getParentRoute: () => rootRouteImport, +} as any) +const ShellIndexRoute = ShellIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShellRoute, +} as any) + +export interface FileRoutesByFullPath { + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRoutesByTo { + '/page/$id': typeof PageIdRoute + '/shell': typeof ShellIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/shell' | '/page/$id' | '/shell/' + fileRoutesByTo: FileRoutesByTo + to: '/page/$id' | '/shell' + id: '__root__' | '/shell' | '/page/$id' | '/shell/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ShellRoute: typeof ShellRouteWithChildren + PageIdRoute: typeof PageIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/shell': { + id: '/shell' + path: '/shell' + fullPath: '/shell' + preLoaderRoute: typeof ShellRouteImport + parentRoute: typeof rootRouteImport + } + '/page/$id': { + id: '/page/$id' + path: '/page/$id' + fullPath: '/page/$id' + preLoaderRoute: typeof PageIdRouteImport + parentRoute: typeof rootRouteImport + } + '/shell/': { + id: '/shell/' + path: '/' + fullPath: '/shell/' + preLoaderRoute: typeof ShellIndexRouteImport + parentRoute: typeof ShellRoute + } + } +} + +interface ShellRouteChildren { + ShellIndexRoute: typeof ShellIndexRoute +} + +const ShellRouteChildren: ShellRouteChildren = { + ShellIndexRoute: ShellIndexRoute, +} + +const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ShellRoute: ShellRouteWithChildren, + PageIdRoute: PageIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx new file mode 100644 index 0000000000..84286e1dae --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx @@ -0,0 +1,19 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/shell'], + }), + routeTree, + defaultGcTime: 0, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx new file mode 100644 index 0000000000..9f4b3c4470 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createLoaderData } from '../../../loader-data' + +export const Route = createFileRoute('/page/$id')({ + loader: ({ params }: { params: { id: string } }) => + createLoaderData(params.id), + component: PageComponent, +}) + +function PageComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.value.id}:${data.value.records.length}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx new file mode 100644 index 0000000000..640ffeeb5c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/shell/')({ + component: ShellIndexComponent, +}) + +function ShellIndexComponent() { + return
ready
+} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx new file mode 100644 index 0000000000..f8a598bc34 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/shell')({ + component: ShellComponent, +}) + +function ShellComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts new file mode 100644 index 0000000000..bbf2fd02d8 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client loader-data-retention (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/project.json b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json new file mode 100644 index 0000000000..05d8aaa4bf --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-mount-unmount-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts new file mode 100644 index 0000000000..70603b5c34 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx new file mode 100644 index 0000000000..bdf76eaa24 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx @@ -0,0 +1,29 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..5efcb2cf72 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ARouteImport } from './routes/a' + +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' + fileRoutesByTo: FileRoutesByTo + to: '/a' + id: '__root__' | '/a' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx new file mode 100644 index 0000000000..9cb85c5a1b --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx new file mode 100644 index 0000000000..1e268d3c42 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx @@ -0,0 +1,12 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/a')({ + loader: () => ({ id: 'a' }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return
{data.id}
+} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts new file mode 100644 index 0000000000..bb0f3c2e0a --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client mount-unmount (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/shared.ts b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts new file mode 100644 index 0000000000..01cfe8c46a --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts @@ -0,0 +1,74 @@ +import { + createBenchContainer, + drainMicrotasks, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type RenderRouter = { + load: () => Promise + subscribe: (event: 'onRendered', listener: () => void) => () => void +} + +const mountUnmountIterations = 100 + +function assertEmptyBody() { + if (document.body.childNodes.length !== 0) { + throw new Error( + `Expected document.body to be empty, found ${document.body.childNodes.length} child node(s)`, + ) + } +} + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, +) { + warnClientMemoryDevMode(framework) + + async function cycle() { + const container = createBenchContainer() + + let unmount = noop + let unsubscribe = noop + + try { + const mounted = mountTestApp(container) + const router = mounted.router as RenderRouter + unmount = mounted.unmount + + const rendered = new Promise((resolve) => { + unsubscribe = router.subscribe('onRendered', () => { + resolve() + }) + }) + + await router.load() + await rendered + unsubscribe() + unsubscribe = noop + } finally { + unmount() + removeBenchContainer(container) + unsubscribe() + await drainMicrotasks() + } + } + + return { + name: `mem client mount-unmount (${framework})`, + cycle, + async run() { + for (let index = 0; index < mountUnmountIterations; index++) { + await cycle() + } + }, + async sanity() { + assertEmptyBody() + await cycle() + assertEmptyBody() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/project.json b/benchmarks/memory/client/scenarios/mount-unmount/solid/project.json new file mode 100644 index 0000000000..63e8e950db --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-mount-unmount-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts new file mode 100644 index 0000000000..493425c6a2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx new file mode 100644 index 0000000000..e82e21bd97 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx @@ -0,0 +1,27 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from '@solidjs/web' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..a3f9739290 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ARouteImport } from './routes/a' + +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' + fileRoutesByTo: FileRoutesByTo + to: '/a' + id: '__root__' | '/a' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx new file mode 100644 index 0000000000..e8c14f12f6 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx new file mode 100644 index 0000000000..36c4be538c --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx @@ -0,0 +1,12 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/a')({ + loader: () => ({ id: 'a' }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return
{data().id}
+} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json new file mode 100644 index 0000000000..9558353c88 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts new file mode 100644 index 0000000000..5f0ab26252 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client mount-unmount (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/project.json b/benchmarks/memory/client/scenarios/mount-unmount/vue/project.json new file mode 100644 index 0000000000..157673cf16 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-mount-unmount-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts new file mode 100644 index 0000000000..a38df04788 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx new file mode 100644 index 0000000000..27b82fcbe1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx @@ -0,0 +1,34 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..1a54b81033 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ARouteImport } from './routes/a' + +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' + fileRoutesByTo: FileRoutesByTo + to: '/a' + id: '__root__' | '/a' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx new file mode 100644 index 0000000000..e083495cd2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx new file mode 100644 index 0000000000..cbcd543ce5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx @@ -0,0 +1,12 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/a')({ + loader: () => ({ id: 'a' }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return
{data.value.id}
+} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts new file mode 100644 index 0000000000..0f7573a408 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client mount-unmount (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/project.json b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json new file mode 100644 index 0000000000..b424277bc6 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-navigation-churn-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts new file mode 100644 index 0000000000..70603b5c34 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx new file mode 100644 index 0000000000..d714e7bfdd --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx @@ -0,0 +1,31 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..3cca81cabb --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as BRouteImport } from './routes/b' +import { Route as ARouteImport } from './routes/a' + +const BRoute = BRouteImport.update({ + id: '/b', + path: '/b', + getParentRoute: () => rootRouteImport, +} as any) +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' | '/b' + fileRoutesByTo: FileRoutesByTo + to: '/a' | '/b' + id: '__root__' | '/a' | '/b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute + BRoute: typeof BRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/b': { + id: '/b' + path: '/b' + fullPath: '/b' + preLoaderRoute: typeof BRouteImport + parentRoute: typeof rootRouteImport + } + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, + BRoute: BRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx new file mode 100644 index 0000000000..9cb85c5a1b --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx new file mode 100644 index 0000000000..bdd32d0b81 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/a')({ + loader: () => ({ name: 'a', ts: fixedTimestamp }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return
{`${data.name}:${data.ts}`}
+} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx new file mode 100644 index 0000000000..05deef729d --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/b')({ + loader: () => ({ name: 'b', ts: fixedTimestamp }), + component: BComponent, +}) + +function BComponent() { + const data = Route.useLoaderData() + + return
{`${data.name}:${data.ts}`}
+} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts new file mode 100644 index 0000000000..b05b999297 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client navigation-churn (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/shared.ts b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts new file mode 100644 index 0000000000..7eaf1a8b66 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts @@ -0,0 +1,129 @@ +import { + createBenchContainer, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type Target = '/a' | '/b' + +type NavigationRouter = { + load: () => Promise + navigate: (options: { to: Target; replace: true }) => Promise + subscribe: (event: 'onRendered', listener: () => void) => () => void +} + +const navigationChurnIterations = 300 + +const uninitialized = () => + Promise.reject(new Error('navigation-churn benchmark is not initialized')) + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, +) { + warnClientMemoryDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop + let navigateTo: (target: Target) => Promise = uninitialized + + function assertRenderedPage(target: Target) { + const expected = target.slice(1) + const actual = + container?.querySelector('[data-bench-page]')?.dataset + .benchPage + + if (actual !== expected) { + throw new Error(`Expected rendered page ${expected}, got ${actual}`) + } + } + + async function waitForRenderedPage(target: Target) { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedPage(target) + return + } catch { + await nextAnimationFrame() + } + } + + assertRenderedPage(target) + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = createBenchContainer() + + const mounted = mountTestApp(container) + const router = mounted.router as NavigationRouter + unmount = mounted.unmount + + unsub = router.subscribe('onRendered', () => { + resolveRendered() + }) + + navigateTo = async (target) => { + const rendered = waitForNextRender() + await Promise.all([ + router.navigate({ + to: target, + replace: true, + }), + rendered, + ]) + } + + await router.load() + await waitForRenderedPage('/a') + } + + function after() { + unmount() + removeBenchContainer(container) + unsub() + + container = undefined + unmount = noop + unsub = noop + resolveRendered = noop + navigateTo = uninitialized + } + + return { + name: `mem client navigation-churn (${framework})`, + before, + navigate: (target: Target) => navigateTo(target), + async run() { + for (let index = 0; index < navigationChurnIterations; index++) { + await navigateTo(index % 2 === 0 ? '/b' : '/a') + } + }, + async sanity() { + await before() + + try { + assertRenderedPage('/a') + await navigateTo('/b') + assertRenderedPage('/b') + } finally { + after() + } + }, + after, + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/project.json b/benchmarks/memory/client/scenarios/navigation-churn/solid/project.json new file mode 100644 index 0000000000..2c581e6188 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-navigation-churn-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts new file mode 100644 index 0000000000..493425c6a2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx new file mode 100644 index 0000000000..5917ef0191 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx @@ -0,0 +1,29 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from '@solidjs/web' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..59d7fad058 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as BRouteImport } from './routes/b' +import { Route as ARouteImport } from './routes/a' + +const BRoute = BRouteImport.update({ + id: '/b', + path: '/b', + getParentRoute: () => rootRouteImport, +} as any) +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' | '/b' + fileRoutesByTo: FileRoutesByTo + to: '/a' | '/b' + id: '__root__' | '/a' | '/b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute + BRoute: typeof BRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/b': { + id: '/b' + path: '/b' + fullPath: '/b' + preLoaderRoute: typeof BRouteImport + parentRoute: typeof rootRouteImport + } + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, + BRoute: BRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx new file mode 100644 index 0000000000..e8c14f12f6 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx new file mode 100644 index 0000000000..0b7838fdbe --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/a')({ + loader: () => ({ name: 'a', ts: fixedTimestamp }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return
{`${data().name}:${data().ts}`}
+} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx new file mode 100644 index 0000000000..ec9b513ba9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/b')({ + loader: () => ({ name: 'b', ts: fixedTimestamp }), + component: BComponent, +}) + +function BComponent() { + const data = Route.useLoaderData() + + return
{`${data().name}:${data().ts}`}
+} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json new file mode 100644 index 0000000000..9558353c88 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts new file mode 100644 index 0000000000..6b6735891a --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client navigation-churn (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/project.json b/benchmarks/memory/client/scenarios/navigation-churn/vue/project.json new file mode 100644 index 0000000000..f96d03d7d3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-navigation-churn-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts new file mode 100644 index 0000000000..a38df04788 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx new file mode 100644 index 0000000000..fad2251cc0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx @@ -0,0 +1,36 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..12c9640358 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as BRouteImport } from './routes/b' +import { Route as ARouteImport } from './routes/a' + +const BRoute = BRouteImport.update({ + id: '/b', + path: '/b', + getParentRoute: () => rootRouteImport, +} as any) +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' | '/b' + fileRoutesByTo: FileRoutesByTo + to: '/a' | '/b' + id: '__root__' | '/a' | '/b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute + BRoute: typeof BRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/b': { + id: '/b' + path: '/b' + fullPath: '/b' + preLoaderRoute: typeof BRouteImport + parentRoute: typeof rootRouteImport + } + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, + BRoute: BRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx new file mode 100644 index 0000000000..e083495cd2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx new file mode 100644 index 0000000000..183228e748 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/vue-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/a')({ + loader: () => ({ name: 'a', ts: fixedTimestamp }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return ( +
{`${data.value.name}:${data.value.ts}`}
+ ) +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx new file mode 100644 index 0000000000..3a18ec97a9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/vue-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/b')({ + loader: () => ({ name: 'b', ts: fixedTimestamp }), + component: BComponent, +}) + +function BComponent() { + const data = Route.useLoaderData() + + return ( +
{`${data.value.name}:${data.value.ts}`}
+ ) +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts new file mode 100644 index 0000000000..d26ce23d07 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client navigation-churn (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/item-payload.ts new file mode 100644 index 0000000000..4a86ea7244 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/item-payload.ts @@ -0,0 +1,51 @@ +const payloadByteLength = 20 * 1024 +const payloadChunkCount = 32 +const payloadChunkSize = payloadByteLength / payloadChunkCount +const trackedLoaderIdPrefix = 'sanity-' +const trackedItemLoaderCalls = new Map() + +export function createItemPayload(id: string) { + let seed = hashId(id) + const chunks = new Array(payloadChunkCount) + + for (let index = 0; index < payloadChunkCount; index++) { + seed = (seed * 1664525 + 1013904223) >>> 0 + chunks[index] = createPayloadChunk(id, index, seed) + } + + return { + id, + chunks, + byteLength: payloadByteLength, + } +} + +export function trackItemLoaderCall(id: string) { + if (!id.startsWith(trackedLoaderIdPrefix)) { + return + } + + trackedItemLoaderCalls.set(id, (trackedItemLoaderCalls.get(id) ?? 0) + 1) +} + +export function getTrackedItemLoaderCount(id: string) { + return trackedItemLoaderCalls.get(id) ?? 0 +} + +function createPayloadChunk(id: string, index: number, seed: number) { + const token = `${id}:${index.toString(36)}:${seed.toString(36)}:` + const repeatCount = Math.ceil(payloadChunkSize / token.length) + + return token.repeat(repeatCount).slice(0, payloadChunkSize) +} + +function hashId(id: string) { + let hash = 2166136261 + + for (let index = 0; index < id.length; index++) { + hash ^= id.charCodeAt(index) + hash = Math.imul(hash, 16777619) + } + + return hash >>> 0 +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts b/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/project.json b/benchmarks/memory/client/scenarios/preload-churn/react/project.json new file mode 100644 index 0000000000..238d98c816 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-preload-churn-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts new file mode 100644 index 0000000000..31cd97d135 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { getTrackedItemLoaderCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, + getTrackedItemLoaderCount, +) diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx new file mode 100644 index 0000000000..b75d1fe34d --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx @@ -0,0 +1,33 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export { getTrackedItemLoaderCount } from '../../item-payload' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..84aaebb9c9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx new file mode 100644 index 0000000000..69e3180173 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx @@ -0,0 +1,18 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx new file mode 100644 index 0000000000..af735d705b --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
index
+} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx new file mode 100644 index 0000000000..2f52acd528 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createItemPayload, trackItemLoaderCall } from '../../../item-payload' + +export const Route = createFileRoute('/items/$id')({ + loader: ({ params }) => { + trackItemLoaderCall(params.id) + return createItemPayload(params.id) + }, + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.id}:${data.byteLength}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts b/benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts new file mode 100644 index 0000000000..f9c426656b --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client preload-churn (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/shared.ts b/benchmarks/memory/client/scenarios/preload-churn/shared.ts new file mode 100644 index 0000000000..8cea4aca81 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/shared.ts @@ -0,0 +1,220 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' +import { + createBenchContainer, + drainMicrotasks, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type GetTrackedItemLoaderCount = (id: string) => number + +type PreloadRouter = { + load: () => Promise + preloadRoute: (options: { + to: '/items/$id' + params: { id: string } + }) => Promise + navigate: ( + options: + | { + to: '/items/$id' + params: { id: string } + replace: true + } + | { + to: '/' + replace: true + }, + ) => Promise + subscribe: (event: 'onRendered', listener: () => void) => () => void +} + +// Fixed id for the eviction navigations interleaved into the bench loop; its +// payload is a constant-size steady-state resident, never part of the signal. +const evictionItemId = 'nav-evict' +const preloadChurnIterations = 200 +// A navigation commit is what triggers the router's clearExpiredCache -- +// preloaded matches (defaultPreloadGcTime: 0) are only evicted then, never +// during a preload-only loop. Interleaving a navigation every few preloads is +// what makes the flat floor assert "eviction releases preloaded payloads". +const preloadsPerEvictionNavigation = 10 +// Module-level so ids stay unique across runner invocations on one mount; a +// per-invocation LCG would replay identical ids, and every preload after the +// first invocation would dedupe against cachedMatches instead of doing work. +const benchmarkRandom = createDeterministicRandom(0x706c6f61) +let preloadCounter = 0 + +const uninitialized = async (_id: string) => { + throw new Error('preload-churn benchmark is not initialized') +} + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, + getTrackedItemLoaderCount: GetTrackedItemLoaderCount, +) { + warnClientMemoryDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let router: PreloadRouter | undefined = undefined + let unmount = noop + let unsub = noop + let resolveRendered = noop + let evictionParity = 0 + let preloadItem: (id: string) => Promise = uninitialized + let navigateToItem: (id: string) => Promise = uninitialized + let navigateToIndex: () => Promise = () => + uninitialized('navigate-to-index') + + function assertRenderedIndex() { + const actual = + container?.querySelector('[data-bench-page]')?.dataset + .benchPage + + if (actual !== 'index') { + throw new Error(`Expected rendered index page, got ${actual}`) + } + } + + async function waitForRenderedIndex() { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedIndex() + return + } catch { + await nextAnimationFrame() + } + } + + assertRenderedIndex() + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = createBenchContainer() + + const mounted = mountTestApp(container) + router = mounted.router as PreloadRouter + unmount = mounted.unmount + + unsub = router.subscribe('onRendered', () => { + resolveRendered() + }) + + preloadItem = async (id) => { + await router!.preloadRoute({ + to: '/items/$id', + params: { id }, + }) + await drainMicrotasks() + } + + navigateToItem = async (id) => { + const rendered = waitForNextRender() + await Promise.all([ + router!.navigate({ + to: '/items/$id', + params: { id }, + replace: true, + }), + rendered, + ]) + } + + navigateToIndex = async () => { + const rendered = waitForNextRender() + await Promise.all([ + router!.navigate({ + to: '/', + replace: true, + }), + rendered, + ]) + } + + await router.load() + await waitForRenderedIndex() + } + + function after() { + unmount() + removeBenchContainer(container) + unsub() + + container = undefined + router = undefined + unmount = noop + unsub = noop + resolveRendered = noop + evictionParity = 0 + preloadItem = uninitialized + navigateToItem = uninitialized + navigateToIndex = () => uninitialized('navigate-to-index') + } + + // Alternate between two distinct locations so every eviction navigation + // changes the href (a same-href navigate would never commit or render). + async function evictPreloads() { + evictionParity = (evictionParity + 1) % 2 + + if (evictionParity === 1) { + await navigateToItem(evictionItemId) + } else { + await navigateToIndex() + } + } + + return { + name: `mem client preload-churn (${framework})`, + before, + preload: (id: string) => preloadItem(id), + evictPreloads, + async run() { + for (let index = 0; index < preloadChurnIterations; index++) { + await preloadItem( + `${(preloadCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`, + ) + + if ((index + 1) % preloadsPerEvictionNavigation === 0) { + await evictPreloads() + } + } + }, + async sanity() { + await before() + + try { + assertRenderedIndex() + + const id = 'sanity-preloaded-item' + const initialLoaderCount = getTrackedItemLoaderCount(id) + await preloadItem(id) + + const preloadedLoaderCount = getTrackedItemLoaderCount(id) + if (preloadedLoaderCount !== initialLoaderCount + 1) { + throw new Error( + `Expected preload to run item loader once, got ${preloadedLoaderCount - initialLoaderCount}`, + ) + } + } finally { + after() + } + }, + after, + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/project.json b/benchmarks/memory/client/scenarios/preload-churn/solid/project.json new file mode 100644 index 0000000000..16ef8af968 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-preload-churn-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts new file mode 100644 index 0000000000..4db457f561 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { getTrackedItemLoaderCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, + getTrackedItemLoaderCount, +) diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx new file mode 100644 index 0000000000..b5b0a1d555 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx @@ -0,0 +1,31 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from '@solidjs/web' +import { getRouter } from './router' + +export { getTrackedItemLoaderCount } from '../../item-payload' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..27749ab2af --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx new file mode 100644 index 0000000000..faed87f1f4 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx @@ -0,0 +1,18 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx new file mode 100644 index 0000000000..b870b8bfd5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
index
+} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx new file mode 100644 index 0000000000..15fa2a34ab --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createItemPayload, trackItemLoaderCall } from '../../../item-payload' + +export const Route = createFileRoute('/items/$id')({ + loader: ({ params }) => { + trackItemLoaderCall(params.id) + return createItemPayload(params.id) + }, + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data().id}:${data().byteLength}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json new file mode 100644 index 0000000000..9558353c88 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts new file mode 100644 index 0000000000..c86a6fcbb3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client preload-churn (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/project.json b/benchmarks/memory/client/scenarios/preload-churn/vue/project.json new file mode 100644 index 0000000000..e13388890f --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-preload-churn-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts new file mode 100644 index 0000000000..4aad4af31e --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { getTrackedItemLoaderCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, + getTrackedItemLoaderCount, +) diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx new file mode 100644 index 0000000000..890447d88b --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx @@ -0,0 +1,38 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export { getTrackedItemLoaderCount } from '../../item-payload' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..a2216f2b67 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx new file mode 100644 index 0000000000..86534112d7 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx @@ -0,0 +1,18 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx new file mode 100644 index 0000000000..b4f871306c --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
index
+} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx new file mode 100644 index 0000000000..2a29245926 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createItemPayload, trackItemLoaderCall } from '../../../item-payload' + +export const Route = createFileRoute('/items/$id')({ + loader: ({ params }: { params: { id: string } }) => { + trackItemLoaderCall(params.id) + return createItemPayload(params.id) + }, + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.value.id}:${data.value.byteLength}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts new file mode 100644 index 0000000000..ea7ed7b695 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client preload-churn (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json new file mode 100644 index 0000000000..1572552650 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-unique-location-churn-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts new file mode 100644 index 0000000000..70603b5c34 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx new file mode 100644 index 0000000000..d714e7bfdd --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx @@ -0,0 +1,31 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..1165f57523 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/items/$id' + id: '__root__' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx new file mode 100644 index 0000000000..50ad269060 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/items/initial?q=q-initial'], + }), + routeTree, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx new file mode 100644 index 0000000000..c9e823f6c2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/react-router' + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }) => ({ q: search.q }), + loader: ({ params, deps }) => ({ + id: params.id, + q: deps.q, + checksum: params.id.length + deps.q.length, + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
{`${data.id}:${data.q}:${data.checksum}`}
+ ) +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts new file mode 100644 index 0000000000..f424c57cef --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client unique-location-churn (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts new file mode 100644 index 0000000000..3b36c7811e --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts @@ -0,0 +1,149 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' +import { + createBenchContainer, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type ItemLocation = { + id: string + q: string +} + +type NavigationRouter = { + load: () => Promise + navigate: (options: { + to: '/items/$id' + params: { id: string } + search: { q: string } + replace: true + }) => Promise + subscribe: (event: 'onRendered', listener: () => void) => () => void +} + +const uniqueLocationChurnIterations = 300 +// Module-level so ids stay unique across runner invocations on one mount; the +// counter prefix removes any residual LCG birthday-collision risk. +const benchmarkRandom = createDeterministicRandom(0xdecafbad) +let locationCounter = 0 + +const uninitialized = () => + Promise.reject( + new Error('unique-location-churn benchmark is not initialized'), + ) + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, +) { + warnClientMemoryDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop + let navigateTo: (location: ItemLocation) => Promise = uninitialized + + function assertRenderedId(expected: string) { + const actual = + container?.querySelector('[data-bench-id]')?.dataset.benchId + + if (actual !== expected) { + throw new Error(`Expected rendered item id ${expected}, got ${actual}`) + } + } + + async function waitForRenderedId(expected: string) { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedId(expected) + return + } catch { + await nextAnimationFrame() + } + } + + assertRenderedId(expected) + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = createBenchContainer() + + const mounted = mountTestApp(container) + const router = mounted.router as NavigationRouter + unmount = mounted.unmount + + unsub = router.subscribe('onRendered', () => { + resolveRendered() + }) + + navigateTo = async (location) => { + const rendered = waitForNextRender() + await Promise.all([ + router.navigate({ + to: '/items/$id', + params: { id: location.id }, + search: { q: location.q }, + replace: true, + }), + rendered, + ]) + } + + await router.load() + await waitForRenderedId('initial') + } + + function after() { + unmount() + removeBenchContainer(container) + unsub() + + container = undefined + unmount = noop + unsub = noop + resolveRendered = noop + navigateTo = uninitialized + } + + return { + name: `mem client unique-location-churn (${framework})`, + before, + navigate: (location: ItemLocation) => navigateTo(location), + async run() { + for (let index = 0; index < uniqueLocationChurnIterations; index++) { + const id = `${(locationCounter++).toString(36)}-${randomSegment(benchmarkRandom)}` + const q = `q-${randomSegment(benchmarkRandom)}` + + await navigateTo({ id, q }) + } + }, + async sanity() { + await before() + + try { + await navigateTo({ id: 'sanity-one', q: 'q-sanity-one' }) + assertRenderedId('sanity-one') + } finally { + after() + } + }, + after, + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json new file mode 100644 index 0000000000..5be22de7a4 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-unique-location-churn-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts new file mode 100644 index 0000000000..493425c6a2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx new file mode 100644 index 0000000000..5917ef0191 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx @@ -0,0 +1,29 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from '@solidjs/web' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..4a451735ff --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/items/$id' + id: '__root__' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx new file mode 100644 index 0000000000..2969b06914 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/items/initial?q=q-initial'], + }), + routeTree, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx new file mode 100644 index 0000000000..9724922b8d --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/solid-router' + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }) => ({ q: search.q }), + loader: ({ params, deps }) => ({ + id: params.id, + q: deps.q, + checksum: params.id.length + deps.q.length, + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
{`${data().id}:${data().q}:${data().checksum}`}
+ ) +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json new file mode 100644 index 0000000000..9558353c88 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts new file mode 100644 index 0000000000..d4acf0019a --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client unique-location-churn (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json new file mode 100644 index 0000000000..6c05cf80b0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-unique-location-churn-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts new file mode 100644 index 0000000000..a38df04788 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx new file mode 100644 index 0000000000..fad2251cc0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx @@ -0,0 +1,36 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..d3e9db0345 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/items/$id' + id: '__root__' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx new file mode 100644 index 0000000000..72f9ab565b --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/items/initial?q=q-initial'], + }), + routeTree, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx new file mode 100644 index 0000000000..1854367142 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx @@ -0,0 +1,34 @@ +import { createFileRoute } from '@tanstack/vue-router' + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }: { search: ItemSearch }) => ({ q: search.q }), + loader: ({ + params, + deps, + }: { + params: { id: string } + deps: { q: string } + }) => ({ + id: params.id, + q: deps.q, + checksum: params.id.length + deps.q.length, + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
{`${data.value.id}:${data.value.q}:${data.value.checksum}`}
+ ) +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts new file mode 100644 index 0000000000..423126019b --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client unique-location-churn (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/tsconfig.json b/benchmarks/memory/client/tsconfig.json new file mode 100644 index 0000000000..9280d5c42f --- /dev/null +++ b/benchmarks/memory/client/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": ["bench-utils.ts", "lifecycle.ts", "vitest.setup.ts"] +} diff --git a/benchmarks/memory/client/vitest.react.config.ts b/benchmarks/memory/client/vitest.react.config.ts new file mode 100644 index 0000000000..44643b4d60 --- /dev/null +++ b/benchmarks/memory/client/vitest.react.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/react/vite.config.ts'], + }, +}) diff --git a/benchmarks/memory/client/vitest.setup.ts b/benchmarks/memory/client/vitest.setup.ts new file mode 100644 index 0000000000..ce3127275a --- /dev/null +++ b/benchmarks/memory/client/vitest.setup.ts @@ -0,0 +1,9 @@ +// @ts-expect-error +global.IS_REACT_ACT_ENVIRONMENT = true + +const scrollTo = () => {} + +window.scrollTo = scrollTo +globalThis.scrollTo = scrollTo + +export {} diff --git a/benchmarks/memory/client/vitest.solid.config.ts b/benchmarks/memory/client/vitest.solid.config.ts new file mode 100644 index 0000000000..5c8185cdd9 --- /dev/null +++ b/benchmarks/memory/client/vitest.solid.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/solid/vite.config.ts'], + }, +}) diff --git a/benchmarks/memory/client/vitest.vue.config.ts b/benchmarks/memory/client/vitest.vue.config.ts new file mode 100644 index 0000000000..01768185ee --- /dev/null +++ b/benchmarks/memory/client/vitest.vue.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/vue/vite.config.ts'], + }, +}) diff --git a/benchmarks/memory/flame-control.ts b/benchmarks/memory/flame-control.ts new file mode 100644 index 0000000000..0d6f42df33 --- /dev/null +++ b/benchmarks/memory/flame-control.ts @@ -0,0 +1,123 @@ +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' + +const flameEnabled = process.env.TSR_MEMORY_FLAME === '1' +const heapIntervalBytes = 512 * 1024 +const heapStackDepth = 64 + +interface HeapProfile { + encode: () => Uint8Array +} + +interface HeapProfiler { + start: (intervalBytes: number, stackDepth: number) => void + stop: () => void + v8Profile: () => unknown + convertProfile: ( + v8Profile: unknown, + ignoreSamplePath?: string, + sourceMapper?: unknown, + ) => HeapProfile +} + +interface SourceMapperConstructor { + create: (searchDirs: Array) => Promise +} + +interface PprofModule { + heap: HeapProfiler + SourceMapper: SourceMapperConstructor +} + +function loadPprof() { + const requireFrom = process.env.TSR_MEMORY_REQUIRE_FROM + ? path.resolve(process.env.TSR_MEMORY_REQUIRE_FROM) + : import.meta.url + const require = createRequire(requireFrom) + + return require('@datadog/pprof') as PprofModule +} + +function getSourcemapDirs() { + return (process.env.TSR_MEMORY_SOURCEMAP_DIRS ?? '') + .split(path.delimiter) + .filter(Boolean) +} + +async function createSourceMapper(pprof: PprofModule) { + const sourcemapDirs = getSourcemapDirs() + + if (sourcemapDirs.length === 0) { + return undefined + } + + return pprof.SourceMapper.create(sourcemapDirs) +} + +function formatTimestamp() { + return new Date().toISOString().replace(/[:.]/g, '-') +} + +function formatProfileFileName(profileName?: string) { + const profileNameSlug = profileName + ?.trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + const profileNamePart = profileNameSlug ? `${profileNameSlug}-` : '' + + return `heap-profile-${profileNamePart}${formatTimestamp()}.pb` +} + +export async function profileFlameWorkload( + workload: () => Promise | void, + profileName?: string, +) { + if (!flameEnabled) { + await workload() + return + } + + const profileDir = process.env.TSR_MEMORY_PROFILE_DIR + + if (!profileDir) { + throw new Error('TSR_MEMORY_PROFILE_DIR must be set for flame profiling') + } + + fs.mkdirSync(profileDir, { recursive: true }) + + const pprof = loadPprof() + const sourceMapper = await createSourceMapper(pprof) + + pprof.heap.start(heapIntervalBytes, heapStackDepth) + + let workloadError: unknown = undefined + let v8Profile: unknown = undefined + + try { + await workload() + } catch (error) { + workloadError = error + } finally { + try { + v8Profile = pprof.heap.v8Profile() + } finally { + pprof.heap.stop() + } + } + + const heapProfile = pprof.heap.convertProfile( + v8Profile, + undefined, + sourceMapper, + ) + const profilePath = path.join(profileDir, formatProfileFileName(profileName)) + + fs.writeFileSync(profilePath, heapProfile.encode()) + console.log(`Heap profile written to: ${profilePath}`) + + if (workloadError) { + throw workloadError + } +} diff --git a/benchmarks/memory/run-flame.mjs b/benchmarks/memory/run-flame.mjs new file mode 100644 index 0000000000..cc568f3f77 --- /dev/null +++ b/benchmarks/memory/run-flame.mjs @@ -0,0 +1,117 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process' +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import process from 'node:process' + +const [, , entrypointArg, sourcemapDirArg] = process.argv + +if (!entrypointArg || !sourcemapDirArg) { + console.error( + 'Usage: node benchmarks/memory/run-flame.mjs ', + ) + process.exit(1) +} + +const entrypointPath = path.resolve(entrypointArg) +const sourcemapDirPath = path.resolve(sourcemapDirArg) + +if (!fs.existsSync(entrypointPath)) { + console.error(`Flame entrypoint not found: ${entrypointPath}`) + process.exit(1) +} + +if (!fs.existsSync(sourcemapDirPath)) { + console.error(`Flame sourcemap directory not found: ${sourcemapDirPath}`) + process.exit(1) +} + +const profileDir = path.join( + path.dirname(entrypointPath), + '.profiles', + new Date().toISOString().replace(/[:.]/g, '-'), +) + +fs.mkdirSync(profileDir, { recursive: true }) + +const entrypointRequire = createRequire(entrypointPath) +const { generateFlamegraph, generateMarkdown } = entrypointRequire( + '@platformatic/flame', +) + +process.env.NODE_ENV = 'production' + +console.log(`Flame profile directory: ${profileDir}`) + +const childProcess = spawn(process.execPath, [entrypointPath], { + cwd: profileDir, + env: { + ...process.env, + NODE_ENV: 'production', + TSR_MEMORY_FLAME: '1', + TSR_MEMORY_PROFILE_DIR: profileDir, + TSR_MEMORY_REQUIRE_FROM: entrypointPath, + TSR_MEMORY_SOURCEMAP_DIRS: sourcemapDirPath, + }, + stdio: 'inherit', +}) + +console.log(`Flame profiling process: ${childProcess.pid}`) + +const childCode = await new Promise((resolve) => { + childProcess.on('error', (error) => { + console.error(`Failed to start Flame profiled process: ${error.message}`) + resolve(1) + }) + + childProcess.on('close', (code, signal) => { + if (signal) { + console.error(`Flame profiled process exited via signal ${signal}`) + resolve(1) + return + } + + resolve(code ?? 0) + }) +}) + +const heapProfilePaths = fs + .readdirSync(profileDir) + .filter((fileName) => /^heap-profile-.*\.pb$/.test(fileName)) + .sort() + .map((fileName) => path.join(profileDir, fileName)) + +for (const heapProfilePath of heapProfilePaths) { + const htmlPath = heapProfilePath.replace(/\.pb$/, '.html') + const mdPath = heapProfilePath.replace(/\.pb$/, '.md') + + console.log(`Generating heap flamegraph: ${htmlPath}`) + try { + await generateFlamegraph(heapProfilePath, htmlPath) + } catch (error) { + console.warn(`Failed to generate heap flamegraph: ${error.message}`) + } + + console.log(`Generating heap markdown: ${mdPath}`) + try { + await generateMarkdown(heapProfilePath, mdPath, { format: 'detailed' }) + } catch (error) { + console.warn(`Failed to generate heap markdown: ${error.message}`) + } +} + +const heapReportPaths = fs + .readdirSync(profileDir) + .filter((fileName) => /^heap-profile-.*\.(?:html|md)$/.test(fileName)) + .sort() + .map((fileName) => path.join(profileDir, fileName)) + +if (heapReportPaths.length > 0) { + console.log('Generated heap profile reports:') + for (const heapReportPath of heapReportPaths) { + console.log(` ${heapReportPath}`) + } +} + +process.exit(childCode) diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts new file mode 100644 index 0000000000..b8a09fdd8e --- /dev/null +++ b/benchmarks/memory/server/bench-utils.ts @@ -0,0 +1,200 @@ +export interface StartRequestHandler { + fetch: (request: Request) => Promise | Response +} + +type RunSequentialRequestLoopRandomOptions = + | { + seed: number + random?: never + } + | { + random: () => number + seed?: never + } + +export type RunSequentialRequestLoopOptions = + RunSequentialRequestLoopRandomOptions & { + iterations?: number + buildRequest: (random: () => number, index: number) => Request + validateResponse?: (response: Response, request: Request) => void + // Whether V8 collects iteration i's garbage before iteration i+1 + // allocates its payload is not reproducible run to run — the measured + // peak flips by a whole payload depending on GC timing, which shifts + // with runner hardware. Forcing a collection between iterations pins + // the GC points so peak deterministically measures the largest + // single-iteration footprint plus any reachable accumulation. + // Accumulation signals stay visible: leaked or cached objects are + // still referenced, so a forced collection cannot reclaim them — it + // only removes floating garbage, whose collection timing is the + // dominant cross-run noise source. + pinGcBetweenIterations?: boolean + // For peak-shape scenarios ONLY (flat inter-iteration heap floor): + // verify each pinned collection actually returned the heap to the + // floor, extending the barrier when a runner's late teardown holds the + // payload past the fixed window. Churn/abort scenarios must not set + // this: their floor legitimately drifts, so the verification caps out + // chronically and the extra collections it fires destabilize the peak. + verifyGcFloor?: boolean + } + +export const memoryBenchOptions = { + iterations: 1, + warmupIterations: 1, + time: 0, + warmupTime: 0, + throws: true, +} + +export function createDeterministicRandom(seed: number) { + let state = seed >>> 0 + + return () => { + state = (state * 1664525 + 1013904223) >>> 0 + return state / 0x100000000 + } +} + +export function randomSegment(random: () => number) { + return Math.floor(random() * 1_000_000_000).toString(36) +} + +export async function drainResponse(response: Response) { + const reader = response.body?.getReader() + + if (!reader) { + return + } + + try { + while (true) { + const result = await reader.read() + + if (result.done) { + break + } + } + } finally { + reader.releaseLock() + } +} + +export async function runSequentialRequestLoop( + handler: StartRequestHandler, + options: RunSequentialRequestLoopOptions, +) { + const { + iterations = 10, + buildRequest, + validateResponse, + pinGcBetweenIterations = false, + verifyGcFloor = false, + } = options + const random = + options.seed !== undefined + ? createDeterministicRandom(options.seed) + : options.random + const validate = + validateResponse ?? + ((response: Response, request: Request) => { + if (response.status !== 200) { + throw new Error( + `Request failed with non-200 status ${response.status} (${request.url})`, + ) + } + }) + + const gcPinState = verifyGcFloor ? createGcPinState() : undefined + + for (let index = 0; index < iterations; index++) { + const request = buildRequest(random, index) + const response = await handler.fetch(request) + + validate(response, request) + + await drainResponse(response) + + if (pinGcBetweenIterations) { + await settleAndPinGc(gcPinState) + } + } +} + +// Renderer/stream teardown spans several event-loop turns (React and Vue +// need more than the two turns the original barrier allowed for), and a +// too-short settle window leaks a whole payload of garbage past the +// collection point, flipping the measured peak bimodally between runs. +// The settle length is a fixed count on purpose: an adaptive exit (collect +// until the post-GC heap size stops moving) makes the collection points +// land at data-dependent turns, which re-introduces exactly the run-to-run +// variance this barrier exists to remove — heap-size readings never fully +// stabilize under the instrumented CI environment, so the exit point (and +// with it every subsequent GC point) shifted between identical runs. +const settleTurnsBeforeGc = 16 +const maxSettleTurns = 64 +// Below every scenario's payload size, above normal inter-iteration jitter. +const settledHeapSlackBytes = 256 * 1024 + +// Tracks the smallest post-collection heap size seen in a workload run: +// the inter-iteration floor the heap must return to before the next +// iteration may start. +export interface GcPinState { + floorHeapUsed: number +} + +export function createGcPinState(): GcPinState { + return { floorHeapUsed: Infinity } +} + +// Settle trailing renderer/stream teardown with a fixed number of 0ms timer +// hops (one full event-loop turn each, microtasks flushing between hops), +// then pin a collection point. The second collection one turn later picks +// up whatever the first collection's finalizers released. gc() is present +// under CodSpeed (--expose-gc); in plain smoke runs there is nothing to +// pin, so only the settle hops run. +// +// The collection is then verified against the inter-iteration heap floor: +// on some runners the response teardown holds the payload past the fixed +// settle window (released only by a later internal timer), and that one +// still-reachable payload survives the collection and bleeds into the next +// iteration's measured peak. When the post-collection heap has not returned +// to the floor, keep hopping and collecting — bounded — until the payload +// is actually released, so the barrier self-heals in exactly the case where +// the fixed-count barrier fails. A workload that genuinely accumulates +// reachable memory raises the floor as it goes (the floor is the minimum +// seen), so accumulation still measures; it only pays the turn cap. +export async function settleAndPinGc(state?: GcPinState) { + for (let turn = 0; turn < settleTurnsBeforeGc; turn++) { + await new Promise((resolve) => setTimeout(resolve, 0)) + } + + const gc = (globalThis as { gc?: () => void }).gc + + if (!gc) { + return + } + + gc() + await new Promise((resolve) => setTimeout(resolve, 0)) + gc() + + if (!state) { + return + } + + let heapUsed = process.memoryUsage().heapUsed + + for ( + let turn = settleTurnsBeforeGc; + turn < maxSettleTurns && + heapUsed > state.floorHeapUsed + settledHeapSlackBytes; + turn++ + ) { + await new Promise((resolve) => setTimeout(resolve, 0)) + gc() + heapUsed = process.memoryUsage().heapUsed + } + + if (heapUsed < state.floorHeapUsed) { + state.floorHeapUsed = heapUsed + } +} diff --git a/benchmarks/memory/server/benchmark.ts b/benchmarks/memory/server/benchmark.ts new file mode 100644 index 0000000000..301a9f956d --- /dev/null +++ b/benchmarks/memory/server/benchmark.ts @@ -0,0 +1,9 @@ +export interface ServerMemoryWorkload { + name: string + run: () => Promise | void +} + +export interface ServerMemoryWorkloadGroup { + sanity: () => Promise | void + workloads: Array +} diff --git a/benchmarks/memory/server/flame-runner.ts b/benchmarks/memory/server/flame-runner.ts new file mode 100644 index 0000000000..1930085aad --- /dev/null +++ b/benchmarks/memory/server/flame-runner.ts @@ -0,0 +1,12 @@ +import { profileFlameWorkload } from '../flame-control.ts' +import type { ServerMemoryWorkloadGroup } from './benchmark.ts' + +export async function runServerFlameBenchmark( + workloadGroup: ServerMemoryWorkloadGroup, +) { + await workloadGroup.sanity() + + for (const workload of workloadGroup.workloads) { + await profileFlameWorkload(workload.run, workload.name) + } +} diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json new file mode 100644 index 0000000000..b31e963c1b --- /dev/null +++ b/benchmarks/memory/server/package.json @@ -0,0 +1,270 @@ +{ + "name": "@benchmarks/memory-server", + "private": true, + "type": "module", + "scripts": { + "clean:profiles": "rm -rf scenarios/*/*/.profiles" + }, + "imports": { + "#memory-server/benchmark": "./benchmark.ts", + "#memory-server/bench-utils": "./bench-utils.ts", + "#memory-server/flame-runner": "./flame-runner.ts" + }, + "dependencies": { + "@solidjs/web": "2.0.0-beta.15", + "@tanstack/react-router": "workspace:*", + "@tanstack/react-start": "workspace:*", + "@tanstack/solid-router": "workspace:*", + "@tanstack/solid-start": "workspace:*", + "@tanstack/vue-router": "workspace:*", + "@tanstack/vue-start": "workspace:*", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "solid-js": "2.0.0-beta.15", + "vue": "^3.5.16" + }, + "devDependencies": { + "@codspeed/vitest-plugin": "^5.5.0", + "@datadog/pprof": "^5.13.2", + "@platformatic/flame": "^1.6.0", + "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-vue-jsx": "^5.1.5", + "typescript": "^6.0.2", + "vite": "^8.0.14", + "vite-plugin-solid": "^3.0.0-next.5", + "vitest": "^4.1.4" + }, + "nx": { + "targets": { + "build:react": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-react", + "@benchmarks/memory-server-server-fn-churn-react", + "@benchmarks/memory-server-error-paths-react", + "@benchmarks/memory-server-aborted-requests-react", + "@benchmarks/memory-server-peak-large-page-react", + "@benchmarks/memory-server-streaming-peak-react", + "@benchmarks/memory-server-serialization-payload-react" + ], + "target": "build:ssr" + } + ] + }, + "build:solid": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-solid", + "@benchmarks/memory-server-server-fn-churn-solid", + "@benchmarks/memory-server-error-paths-solid", + "@benchmarks/memory-server-aborted-requests-solid", + "@benchmarks/memory-server-peak-large-page-solid", + "@benchmarks/memory-server-streaming-peak-solid", + "@benchmarks/memory-server-serialization-payload-solid" + ], + "target": "build:ssr" + } + ] + }, + "build:vue": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-vue", + "@benchmarks/memory-server-server-fn-churn-vue", + "@benchmarks/memory-server-error-paths-vue", + "@benchmarks/memory-server-aborted-requests-vue", + "@benchmarks/memory-server-peak-large-page-vue", + "@benchmarks/memory-server-streaming-peak-vue", + "@benchmarks/memory-server-serialization-payload-vue" + ], + "target": "build:ssr" + } + ] + }, + "build:react:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-react", + "@benchmarks/memory-server-server-fn-churn-react", + "@benchmarks/memory-server-error-paths-react", + "@benchmarks/memory-server-aborted-requests-react", + "@benchmarks/memory-server-peak-large-page-react", + "@benchmarks/memory-server-streaming-peak-react", + "@benchmarks/memory-server-serialization-payload-react" + ], + "target": "build:ssr:flame" + } + ] + }, + "build:solid:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-solid", + "@benchmarks/memory-server-server-fn-churn-solid", + "@benchmarks/memory-server-error-paths-solid", + "@benchmarks/memory-server-aborted-requests-solid", + "@benchmarks/memory-server-peak-large-page-solid", + "@benchmarks/memory-server-streaming-peak-solid", + "@benchmarks/memory-server-serialization-payload-solid" + ], + "target": "build:ssr:flame" + } + ] + }, + "build:vue:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-vue", + "@benchmarks/memory-server-server-fn-churn-vue", + "@benchmarks/memory-server-error-paths-vue", + "@benchmarks/memory-server-aborted-requests-vue", + "@benchmarks/memory-server-peak-large-page-vue", + "@benchmarks/memory-server-streaming-peak-vue", + "@benchmarks/memory-server-serialization-payload-vue" + ], + "target": "build:ssr:flame" + } + ] + }, + "test:flame:react": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-react", + "@benchmarks/memory-server-server-fn-churn-react", + "@benchmarks/memory-server-error-paths-react", + "@benchmarks/memory-server-aborted-requests-react", + "@benchmarks/memory-server-peak-large-page-react", + "@benchmarks/memory-server-streaming-peak-react", + "@benchmarks/memory-server-serialization-payload-react" + ], + "target": "test:flame" + } + ] + }, + "test:flame:solid": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-solid", + "@benchmarks/memory-server-server-fn-churn-solid", + "@benchmarks/memory-server-error-paths-solid", + "@benchmarks/memory-server-aborted-requests-solid", + "@benchmarks/memory-server-peak-large-page-solid", + "@benchmarks/memory-server-streaming-peak-solid", + "@benchmarks/memory-server-serialization-payload-solid" + ], + "target": "test:flame" + } + ] + }, + "test:flame:vue": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-vue", + "@benchmarks/memory-server-server-fn-churn-vue", + "@benchmarks/memory-server-error-paths-vue", + "@benchmarks/memory-server-aborted-requests-vue", + "@benchmarks/memory-server-peak-large-page-vue", + "@benchmarks/memory-server-streaming-peak-vue", + "@benchmarks/memory-server-serialization-payload-vue" + ], + "target": "test:flame" + } + ] + }, + "test:perf:react": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:react" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.react.config.ts", + "cwd": "benchmarks/memory/server" + } + }, + "test:perf:solid": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:solid" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.solid.config.ts", + "cwd": "benchmarks/memory/server" + } + }, + "test:perf:vue": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:vue" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.vue.config.ts", + "cwd": "benchmarks/memory/server" + } + }, + "test:types": { + "executor": "nx:noop", + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-react", + "@benchmarks/memory-server-server-fn-churn-react", + "@benchmarks/memory-server-error-paths-react", + "@benchmarks/memory-server-aborted-requests-react", + "@benchmarks/memory-server-peak-large-page-react", + "@benchmarks/memory-server-streaming-peak-react", + "@benchmarks/memory-server-serialization-payload-react", + "@benchmarks/memory-server-request-churn-solid", + "@benchmarks/memory-server-server-fn-churn-solid", + "@benchmarks/memory-server-error-paths-solid", + "@benchmarks/memory-server-aborted-requests-solid", + "@benchmarks/memory-server-peak-large-page-solid", + "@benchmarks/memory-server-streaming-peak-solid", + "@benchmarks/memory-server-serialization-payload-solid", + "@benchmarks/memory-server-request-churn-vue", + "@benchmarks/memory-server-server-fn-churn-vue", + "@benchmarks/memory-server-error-paths-vue", + "@benchmarks/memory-server-aborted-requests-vue", + "@benchmarks/memory-server-peak-large-page-vue", + "@benchmarks/memory-server-streaming-peak-vue", + "@benchmarks/memory-server-serialization-payload-vue" + ], + "target": "test:types:ssr" + } + ] + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/deferred-records.ts b/benchmarks/memory/server/scenarios/aborted-requests/deferred-records.ts new file mode 100644 index 0000000000..4886e3ee80 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/deferred-records.ts @@ -0,0 +1,18 @@ +const recordCount = 20 + +export type RecordGroup = 'alpha' | 'beta' + +export interface DeferredRecord { + id: string + label: string +} + +export function makeAbortedRequestRecords( + id: string, + group: RecordGroup, +): Array { + return Array.from({ length: recordCount }, (_, index) => ({ + id: `${group}-${id}-${index}`, + label: `deferred-${group}-${id}-${index}`, + })) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/project.json b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json new file mode 100644 index 0000000000..bea90c8766 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-aborted-requests-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..fbbc25ecfb --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx new file mode 100644 index 0000000000..08d534a779 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
aborted-requests-index
+} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..ecd9ef524d --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx @@ -0,0 +1,66 @@ +import { Await, createFileRoute } from '@tanstack/react-router' +import { Suspense } from 'react' +import { makeAbortedRequestRecords } from '../../../deferred-records' + +function resolveAfterMicrotasks(microtasks: number, value: () => T) { + let promise = Promise.resolve() + + for (let index = 0; index < microtasks; index++) { + promise = promise.then(() => undefined) + } + + return promise.then(value) +} + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + eager: `eager-${params.id}`, + alpha: resolveAfterMicrotasks(32, () => + makeAbortedRequestRecords(params.id, 'alpha'), + ), + beta: resolveAfterMicrotasks(64, () => + makeAbortedRequestRecords(params.id, 'beta'), + ), + }), + component: StreamComponent, +}) + +function StreamComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.eager}

+ loading-alpha

+ } + > + + {(records) => ( +
    + {records.map((record) => ( +
  • {record.label}
  • + ))} +
+ )} +
+
+ loading-beta

+ } + > + + {(records) => ( +
    + {records.map((record) => ( +
  • {record.label}
  • + ))} +
+ )} +
+
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts new file mode 100644 index 0000000000..2ce925e406 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server aborted-requests (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/shared.ts b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts new file mode 100644 index 0000000000..f7f63c17e6 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts @@ -0,0 +1,273 @@ +import { settleAndPinGc } from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +type AbortedRequestReadMode = 'first-chunk' | 'shell-before-deferred' +type AbortedRequestCancelMode = 'plain' | 'swallow-abort-error' + +type AbortedRequestMode = { + readMode: AbortedRequestReadMode + cancelMode: AbortedRequestCancelMode +} + +const abortedRequestIterations = 40 +let abortedRequestCounter = 0 +const eagerMarker = 'data-bench="aborted-requests-eager"' +const alphaFallbackMarker = 'data-bench="aborted-requests-alpha-fallback"' +const betaFallbackMarker = 'data-bench="aborted-requests-beta-fallback"' +const alphaFirstRecord = (id: string) => `deferred-alpha-${id}-0` +const alphaLastRecord = (id: string) => `deferred-alpha-${id}-19` +const betaFirstRecord = (id: string) => `deferred-beta-${id}-0` +const betaLastRecord = (id: string) => `deferred-beta-${id}-19` + +const textDecoder = new TextDecoder() +const abortedRequestModes: Record = { + react: { + readMode: 'first-chunk', + cancelMode: 'plain', + }, + solid: { + readMode: 'first-chunk', + cancelMode: 'plain', + }, + vue: { + readMode: 'shell-before-deferred', + cancelMode: 'swallow-abort-error', + }, +} + +const documentRequestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildStreamRequest(id: string, signal?: AbortSignal) { + const init: RequestInit = { ...documentRequestInit } + + if (signal) { + init.signal = signal + } + + return new Request(`http://localhost/stream/${id}`, init) +} + +function validateDocumentResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } +} + +async function readFirstChunk(response: Response, request: Request) { + if (!response.body) { + throw new Error(`Expected response body for ${request.url}`) + } + + const reader = response.body.getReader() + const result = await reader.read() + const value = result.value + + if (result.done || !value || value.byteLength === 0) { + await reader.cancel() + throw new Error(`Expected a non-empty first chunk for ${request.url}`) + } + + return { + reader, + value, + } +} + +async function readShellBeforeDeferred( + response: Response, + request: Request, + id: string, +) { + if (!response.body) { + throw new Error(`Expected response body for ${request.url}`) + } + + const reader = response.body.getReader() + let text = '' + + while (true) { + const result = await reader.read() + const value = result.value + + if (result.done || !value || value.byteLength === 0) { + await reader.cancel() + throw new Error(`Expected shell content for ${request.url}`) + } + + text += textDecoder.decode(value, { stream: true }) + + for (const marker of [ + alphaFirstRecord(id), + alphaLastRecord(id), + betaFirstRecord(id), + betaLastRecord(id), + ]) { + if (text.includes(marker)) { + await reader.cancel() + throw new Error( + `Shell chunks already included deferred content ${marker}`, + ) + } + } + + if ( + text.includes(eagerMarker) && + text.includes(alphaFallbackMarker) && + text.includes(betaFallbackMarker) + ) { + return { + reader, + text, + } + } + } +} + +async function readSanityStream( + response: Response, + request: Request, + mode: AbortedRequestMode, + id: string, +) { + if (mode.readMode === 'shell-before-deferred') { + return readShellBeforeDeferred(response, request, id) + } + + const { reader, value } = await readFirstChunk(response, request) + + return { + reader, + text: textDecoder.decode(value), + } +} + +function readLoopStream( + mode: AbortedRequestReadMode, + response: Response, + request: Request, + id: string, +) { + if (mode === 'shell-before-deferred') { + return readShellBeforeDeferred(response, request, id) + } + + return readFirstChunk(response, request) +} + +async function cancelReader( + reader: ReadableStreamDefaultReader, + mode: AbortedRequestCancelMode, +) { + if (mode === 'swallow-abort-error') { + try { + await reader.cancel() + } catch (error) { + if (!(error instanceof DOMException && error.name === 'AbortError')) { + throw error + } + } + + return + } + + await reader.cancel() +} + +// Post-abort settlement barrier. Settles renderer teardown across a fixed +// number of event-loop turns, then (under CodSpeed) pins a collection point +// after every aborted request, so the measured peak cannot drift with how +// much floating garbage the previous iterations happened to leave behind. +async function drainCancellation() { + await settleAndPinGc() +} + +async function assertAbortedRequestsSanity( + handler: StartRequestHandler, + mode: AbortedRequestMode, +) { + const fullId = 'sanity-full' + const fullRequest = buildStreamRequest(fullId) + const fullResponse = await handler.fetch(fullRequest) + validateDocumentResponse(fullResponse, fullRequest) + + const fullBody = await fullResponse.text() + + if (!fullBody.includes(eagerMarker)) { + throw new Error('Expected full sanity response to include the eager marker') + } + + const midStreamId = 'sanity-mid-stream' + const controller = new AbortController() + const midStreamRequest = buildStreamRequest(midStreamId, controller.signal) + const midStreamResponse = await handler.fetch(midStreamRequest) + validateDocumentResponse(midStreamResponse, midStreamRequest) + + const { reader, text } = await readSanityStream( + midStreamResponse, + midStreamRequest, + mode, + midStreamId, + ) + + if (!text.includes(eagerMarker)) { + throw new Error('Expected sanity stream to include the eager marker') + } + + // reader.cancel() is the response-stream cancellation path if the handler + // does not observe Request.signal for this in-process request. + controller.abort() + await cancelReader(reader, mode.cancelMode) + await drainCancellation() +} + +async function runAbortedRequestLoop( + handler: StartRequestHandler, + mode: AbortedRequestMode, +) { + for (let index = 0; index < abortedRequestIterations; index++) { + const controller = new AbortController() + const id = `abort-${(abortedRequestCounter++).toString(36)}` + const request = buildStreamRequest(id, controller.signal) + const response = await handler.fetch(request) + validateDocumentResponse(response, request) + + const { reader } = await readLoopStream( + mode.readMode, + response, + request, + id, + ) + controller.abort() + await cancelReader(reader, mode.cancelMode) + await drainCancellation() + } +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const mode = abortedRequestModes[framework] + const run = () => runAbortedRequestLoop(handler, mode) + + return { + sanity: () => assertAbortedRequestsSanity(handler, mode), + workloads: [ + { + name: `mem server aborted-requests (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/project.json b/benchmarks/memory/server/scenarios/aborted-requests/solid/project.json new file mode 100644 index 0000000000..345fcc0720 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-aborted-requests-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..04ebed80f0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..6e83136ea0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from '@solidjs/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx new file mode 100644 index 0000000000..3aac56e824 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
aborted-requests-index
+} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..0ddc6d1447 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx @@ -0,0 +1,141 @@ +import { Await, createFileRoute } from '@tanstack/solid-router' +import { Loading } from 'solid-js' +import { + makeAbortedRequestRecords, + type DeferredRecord, + type RecordGroup, +} from '../../../deferred-records' + +const alphaResolveTicks = 4 +const betaResolveTicks = 6 +const abortProbeAlphaResolveTicks = 40 +const abortProbeBetaResolveTicks = 60 + +function isAbortProbeId(id: string) { + return id === 'sanity-mid-stream' || id.startsWith('abort-') +} + +function getResolveTicks(id: string, group: RecordGroup) { + if (isAbortProbeId(id)) { + return group === 'alpha' + ? abortProbeAlphaResolveTicks + : abortProbeBetaResolveTicks + } + + return group === 'alpha' ? alphaResolveTicks : betaResolveTicks +} + +// One tick = one 0ms timers-phase hop. Counting event-loop turns instead of +// milliseconds keeps the resolve/abort interleaving a pure function of the +// event-loop schedule: a wall-clock delay races the abort differently +// depending on runner load and instrumentation overhead, which made this +// benchmark's single measured run swing between runs. +function resolveAfterTicks( + ticks: number, + signal: AbortSignal, + value: () => T, + abortedValue: () => T, +) { + return new Promise((resolve) => { + if (signal.aborted) { + resolve(abortedValue()) + return + } + + let remaining = ticks + let timeoutId: ReturnType + + const onAbort = () => { + clearTimeout(timeoutId) + resolve(abortedValue()) + } + + const step = () => { + remaining -= 1 + + if (remaining <= 0) { + signal.removeEventListener('abort', onAbort) + resolve(value()) + return + } + + timeoutId = setTimeout(step, 0) + } + + signal.addEventListener('abort', onAbort, { once: true }) + timeoutId = setTimeout(step, 0) + }) +} + +function makeDeferredRecords( + id: string, + group: RecordGroup, + signal: AbortSignal, +) { + return resolveAfterTicks( + getResolveTicks(id, group), + signal, + () => makeAbortedRequestRecords(id, group), + () => [], + ) +} + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params, abortController }) => ({ + eager: `eager-${params.id}`, + alpha: makeDeferredRecords(params.id, 'alpha', abortController.signal), + beta: makeDeferredRecords(params.id, 'beta', abortController.signal), + }), + component: StreamComponent, +}) + +function StreamComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data().eager}

+ loading-alpha

+ } + > + + {(records) => ( + + )} + +
+ loading-beta

+ } + > + + {(records) => ( + + )} + +
+
+ ) +} + +function DeferredRecords(props: { + records: Array + dataBench: string +}) { + return ( +
    + {props.records.map((record) => ( +
  • {record.label}
  • + ))} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json new file mode 100644 index 0000000000..cdb88448d7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts new file mode 100644 index 0000000000..5ee4e152af --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts @@ -0,0 +1,47 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server aborted-requests (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/project.json b/benchmarks/memory/server/scenarios/aborted-requests/vue/project.json new file mode 100644 index 0000000000..8ca7d84660 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-aborted-requests-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..192f1d6dc7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx new file mode 100644 index 0000000000..af662b29f7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
aborted-requests-index
+} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..b723c7f69e --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx @@ -0,0 +1,136 @@ +import { Await, createFileRoute } from '@tanstack/vue-router' +import { Suspense } from 'vue' +import { + makeAbortedRequestRecords, + type DeferredRecord, + type RecordGroup, +} from '../../../deferred-records' + +const alphaResolveTicks = 4 +const betaResolveTicks = 6 +const abortProbeAlphaResolveTicks = 40 +const abortProbeBetaResolveTicks = 60 + +function isAbortProbeId(id: string) { + return id === 'sanity-mid-stream' || id.startsWith('abort-') +} + +function getResolveTicks(id: string, group: RecordGroup) { + if (isAbortProbeId(id)) { + return group === 'alpha' + ? abortProbeAlphaResolveTicks + : abortProbeBetaResolveTicks + } + + return group === 'alpha' ? alphaResolveTicks : betaResolveTicks +} + +// One tick = one 0ms timers-phase hop. Counting event-loop turns instead of +// milliseconds keeps the resolve/abort interleaving a pure function of the +// event-loop schedule: a wall-clock delay races the abort differently +// depending on runner load and instrumentation overhead, which made this +// benchmark's single measured run swing between runs. +function resolveAfterTicks( + ticks: number, + signal: AbortSignal, + value: () => T, + abortedValue: () => T, +) { + return new Promise((resolve) => { + if (signal.aborted) { + resolve(abortedValue()) + return + } + + let remaining = ticks + let timeoutId: ReturnType + + const onAbort = () => { + clearTimeout(timeoutId) + resolve(abortedValue()) + } + + const step = () => { + remaining -= 1 + + if (remaining <= 0) { + signal.removeEventListener('abort', onAbort) + resolve(value()) + return + } + + timeoutId = setTimeout(step, 0) + } + + signal.addEventListener('abort', onAbort, { once: true }) + timeoutId = setTimeout(step, 0) + }) +} + +function makeDeferredRecords( + id: string, + group: RecordGroup, + signal: AbortSignal, +) { + return resolveAfterTicks( + getResolveTicks(id, group), + signal, + () => makeAbortedRequestRecords(id, group), + () => [], + ) +} + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params, abortController }) => ({ + eager: `eager-${params.id}`, + alpha: makeDeferredRecords(params.id, 'alpha', abortController.signal), + beta: makeDeferredRecords(params.id, 'beta', abortController.signal), + }), + component: StreamComponent, +}) + +function StreamComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.value.eager}

+

loading-alpha

+

loading-beta

+ + {{ + default: () => ( + ) => ( +
    + {records.map((record) => ( +
  • {record.label}
  • + ))} +
+ )} + /> + ), + fallback: () => null, + }} +
+ + {{ + default: () => ( + ) => ( +
    + {records.map((record) => ( +
  • {record.label}
  • + ))} +
+ )} + /> + ), + fallback: () => null, + }} +
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts new file mode 100644 index 0000000000..5ccc9f5d1d --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server aborted-requests (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/project.json b/benchmarks/memory/server/scenarios/error-paths/react/project.json new file mode 100644 index 0000000000..6a399fe41e --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-error-paths-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..fd373be6b4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts @@ -0,0 +1,146 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as TargetIdRouteImport } from './routes/target.$id' +import { Route as MissingIdRouteImport } from './routes/missing.$id' +import { Route as FromIdRouteImport } from './routes/from.$id' +import { Route as BoomIdRouteImport } from './routes/boom.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const TargetIdRoute = TargetIdRouteImport.update({ + id: '/target/$id', + path: '/target/$id', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FromIdRoute = FromIdRouteImport.update({ + id: '/from/$id', + path: '/from/$id', + getParentRoute: () => rootRouteImport, +} as any) +const BoomIdRoute = BoomIdRouteImport.update({ + id: '/boom/$id', + path: '/boom/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + id: + | '__root__' + | '/' + | '/boom/$id' + | '/from/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BoomIdRoute: typeof BoomIdRoute + FromIdRoute: typeof FromIdRoute + MissingIdRoute: typeof MissingIdRoute + TargetIdRoute: typeof TargetIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/target/$id': { + id: '/target/$id' + path: '/target/$id' + fullPath: '/target/$id' + preLoaderRoute: typeof TargetIdRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + '/from/$id': { + id: '/from/$id' + path: '/from/$id' + fullPath: '/from/$id' + preLoaderRoute: typeof FromIdRouteImport + parentRoute: typeof rootRouteImport + } + '/boom/$id': { + id: '/boom/$id' + path: '/boom/$id' + fullPath: '/boom/$id' + preLoaderRoute: typeof BoomIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BoomIdRoute: BoomIdRoute, + FromIdRoute: FromIdRoute, + MissingIdRoute: MissingIdRoute, + TargetIdRoute: TargetIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx new file mode 100644 index 0000000000..e31bd0abf7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx @@ -0,0 +1,23 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + defaultNotFoundComponent: DefaultNotFound, + }) +} + +// Without this, every unmatched-URL request logs a router warning in +// non-production ad-hoc runs. +function DefaultNotFound() { + return

error-paths-unmatched

+} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx new file mode 100644 index 0000000000..fafa81a9bb --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/boom/$id')({ + loader: ({ params }) => { + throw new Error(`boom-${params.id}`) + }, + errorComponent: BoomErrorComponent, + component: BoomComponent, +}) + +function BoomErrorComponent() { + return
error-paths-error-boundary
+} + +function BoomComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx new file mode 100644 index 0000000000..4c45e76c92 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/from/$id')({ + loader: ({ params }) => { + const { id } = params + + throw redirect({ to: '/target/$id', params: { id }, statusCode: 302 }) + }, + component: FromComponent, +}) + +function FromComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx new file mode 100644 index 0000000000..c5492486b4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
error-paths-index
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..84640e9712 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx @@ -0,0 +1,19 @@ +import { createFileRoute, notFound } from '@tanstack/react-router' + +export const Route = createFileRoute('/missing/$id')({ + loader: () => { + throw notFound() + }, + notFoundComponent: MissingNotFoundComponent, + component: MissingComponent, +}) + +function MissingNotFoundComponent() { + return ( +
error-paths-not-found-boundary
+ ) +} + +function MissingComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx new file mode 100644 index 0000000000..1c13394cb2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/target/$id')({ + component: TargetComponent, +}) + +function TargetComponent() { + const params = Route.useParams() + + return
{`target-${params.id}`}
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts b/benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts new file mode 100644 index 0000000000..bc67b1cfc7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server error-paths (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/shared.ts b/benchmarks/memory/server/scenarios/error-paths/shared.ts new file mode 100644 index 0000000000..57e229b7e3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/shared.ts @@ -0,0 +1,198 @@ +import { + createDeterministicRandom, + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +// Sized to sit just above the 2s measured-run floor on CI (per-iteration +// cost with the pinned collection is ~0.13-0.19s across frameworks). +const errorPathsIterations = 18 +const redirectSeed = 0xdecafbad +const notFoundSeed = 0xdecafb0d +const errorSeed = 0xdecafbed +const unmatchedSeed = 0xdecaf00d +const redirectStatus = 302 +const notFoundStatus = 404 +const errorStatus = 500 +// Module-level so each error-path bench keeps advancing across runner invocations. +const redirectRandom = createDeterministicRandom(redirectSeed) +const notFoundRandom = createDeterministicRandom(notFoundSeed) +const errorRandom = createDeterministicRandom(errorSeed) +const unmatchedRandom = createDeterministicRandom(unmatchedSeed) +let redirectCounter = 0 +let notFoundCounter = 0 +let errorCounter = 0 +let unmatchedCounter = 0 + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildRedirectRequest(random: () => number) { + const id = `${(redirectCounter++).toString(36)}-${randomSegment(random)}` + + return new Request(`http://localhost/from/${id}`, requestInit) +} + +function buildNotFoundRequest(random: () => number) { + const id = `${(notFoundCounter++).toString(36)}-${randomSegment(random)}` + + return new Request(`http://localhost/missing/${id}`, requestInit) +} + +function buildErrorRequest(random: () => number) { + const id = `${(errorCounter++).toString(36)}-${randomSegment(random)}` + + return new Request(`http://localhost/boom/${id}`, requestInit) +} + +function buildUnmatchedRequest(random: () => number) { + const id = `${(unmatchedCounter++).toString(36)}-${randomSegment(random)}` + + return new Request(`http://localhost/nope/${id}`, requestInit) +} + +function getRequestId(request: Request) { + const id = new URL(request.url).pathname.split('/').pop() + + if (!id) { + throw new Error(`Expected request id in ${request.url}`) + } + + return id +} + +function validateRedirectResponse(response: Response, request: Request) { + if (response.status !== redirectStatus) { + throw new Error( + `Expected status ${redirectStatus} for ${request.url}, got ${response.status}`, + ) + } + + const id = getRequestId(request) + const location = response.headers.get('location') + + if (location !== `/target/${id}`) { + throw new Error(`Expected redirect location /target/${id}, got ${location}`) + } +} + +function validateNotFoundResponse(response: Response, request: Request) { + if (response.status !== notFoundStatus) { + throw new Error( + `Expected status ${notFoundStatus} for ${request.url}, got ${response.status}`, + ) + } +} + +function validateErrorResponse(response: Response, request: Request) { + if (response.status !== errorStatus) { + throw new Error( + `Expected status ${errorStatus} for ${request.url}, got ${response.status}`, + ) + } +} + +async function assertStatusSanity( + handler: StartRequestHandler, + request: Request, + validateResponse: (response: Response, request: Request) => void, +) { + const response = await handler.fetch(request) + validateResponse(response, request) + await response.text() +} + +async function assertErrorPathsSanity(handler: StartRequestHandler) { + await assertStatusSanity( + handler, + new Request('http://localhost/from/sanity-redirect', requestInit), + validateRedirectResponse, + ) + await assertStatusSanity( + handler, + new Request('http://localhost/missing/sanity-missing', requestInit), + validateNotFoundResponse, + ) + await assertStatusSanity( + handler, + new Request('http://localhost/boom/sanity-error', requestInit), + validateErrorResponse, + ) + await assertStatusSanity( + handler, + new Request('http://localhost/nope/sanity-unmatched', requestInit), + validateNotFoundResponse, + ) +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const runRedirect = () => + runSequentialRequestLoop(handler, { + random: redirectRandom, + iterations: errorPathsIterations, + buildRequest: buildRedirectRequest, + validateResponse: validateRedirectResponse, + pinGcBetweenIterations: true, + }) + + const runNotFound = () => + runSequentialRequestLoop(handler, { + random: notFoundRandom, + iterations: errorPathsIterations, + buildRequest: buildNotFoundRequest, + validateResponse: validateNotFoundResponse, + pinGcBetweenIterations: true, + }) + + const runError = () => + runSequentialRequestLoop(handler, { + random: errorRandom, + iterations: errorPathsIterations, + buildRequest: buildErrorRequest, + validateResponse: validateErrorResponse, + pinGcBetweenIterations: true, + }) + + const runUnmatched = () => + runSequentialRequestLoop(handler, { + random: unmatchedRandom, + iterations: errorPathsIterations, + buildRequest: buildUnmatchedRequest, + validateResponse: validateNotFoundResponse, + pinGcBetweenIterations: true, + }) + + return { + sanity: () => assertErrorPathsSanity(handler), + workloads: [ + { + name: `mem server error-paths redirect (${framework})`, + run: runRedirect, + }, + { + name: `mem server error-paths not-found (${framework})`, + run: runNotFound, + }, + { + name: `mem server error-paths error (${framework})`, + run: runError, + }, + { + name: `mem server error-paths unmatched (${framework})`, + run: runUnmatched, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/project.json b/benchmarks/memory/server/scenarios/error-paths/solid/project.json new file mode 100644 index 0000000000..77fcfaf578 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-error-paths-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts b/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..32a4d56d3a --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts @@ -0,0 +1,146 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as TargetIdRouteImport } from './routes/target.$id' +import { Route as MissingIdRouteImport } from './routes/missing.$id' +import { Route as FromIdRouteImport } from './routes/from.$id' +import { Route as BoomIdRouteImport } from './routes/boom.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const TargetIdRoute = TargetIdRouteImport.update({ + id: '/target/$id', + path: '/target/$id', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FromIdRoute = FromIdRouteImport.update({ + id: '/from/$id', + path: '/from/$id', + getParentRoute: () => rootRouteImport, +} as any) +const BoomIdRoute = BoomIdRouteImport.update({ + id: '/boom/$id', + path: '/boom/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + id: + | '__root__' + | '/' + | '/boom/$id' + | '/from/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BoomIdRoute: typeof BoomIdRoute + FromIdRoute: typeof FromIdRoute + MissingIdRoute: typeof MissingIdRoute + TargetIdRoute: typeof TargetIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/target/$id': { + id: '/target/$id' + path: '/target/$id' + fullPath: '/target/$id' + preLoaderRoute: typeof TargetIdRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + '/from/$id': { + id: '/from/$id' + path: '/from/$id' + fullPath: '/from/$id' + preLoaderRoute: typeof FromIdRouteImport + parentRoute: typeof rootRouteImport + } + '/boom/$id': { + id: '/boom/$id' + path: '/boom/$id' + fullPath: '/boom/$id' + preLoaderRoute: typeof BoomIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BoomIdRoute: BoomIdRoute, + FromIdRoute: FromIdRoute, + MissingIdRoute: MissingIdRoute, + TargetIdRoute: TargetIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx new file mode 100644 index 0000000000..ae51f8b5e9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx @@ -0,0 +1,23 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + defaultNotFoundComponent: DefaultNotFound, + }) +} + +// Without this, every unmatched-URL request logs a router warning in +// non-production ad-hoc runs. +function DefaultNotFound() { + return

error-paths-unmatched

+} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..6e83136ea0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from '@solidjs/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx new file mode 100644 index 0000000000..bebc638caa --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/boom/$id')({ + loader: ({ params }) => { + throw new Error(`boom-${params.id}`) + }, + errorComponent: BoomErrorComponent, + component: BoomComponent, +}) + +function BoomErrorComponent() { + return
error-paths-error-boundary
+} + +function BoomComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx new file mode 100644 index 0000000000..e96b7864f2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/from/$id')({ + loader: ({ params }) => { + const { id } = params + + throw redirect({ to: '/target/$id', params: { id }, statusCode: 302 }) + }, + component: FromComponent, +}) + +function FromComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx new file mode 100644 index 0000000000..43282af801 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
error-paths-index
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..1b9ec5e29d --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx @@ -0,0 +1,19 @@ +import { createFileRoute, notFound } from '@tanstack/solid-router' + +export const Route = createFileRoute('/missing/$id')({ + loader: () => { + throw notFound() + }, + notFoundComponent: MissingNotFoundComponent, + component: MissingComponent, +}) + +function MissingNotFoundComponent() { + return ( +
error-paths-not-found-boundary
+ ) +} + +function MissingComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx new file mode 100644 index 0000000000..4196fc1183 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/target/$id')({ + component: TargetComponent, +}) + +function TargetComponent() { + const params = Route.useParams() + + return
{`target-${params().id}`}
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json new file mode 100644 index 0000000000..cdb88448d7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts b/benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts new file mode 100644 index 0000000000..401d1e5f25 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts @@ -0,0 +1,47 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server error-paths (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/project.json b/benchmarks/memory/server/scenarios/error-paths/vue/project.json new file mode 100644 index 0000000000..8472470bde --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-error-paths-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts b/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..ea5bafa386 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts @@ -0,0 +1,146 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as TargetIdRouteImport } from './routes/target.$id' +import { Route as MissingIdRouteImport } from './routes/missing.$id' +import { Route as FromIdRouteImport } from './routes/from.$id' +import { Route as BoomIdRouteImport } from './routes/boom.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const TargetIdRoute = TargetIdRouteImport.update({ + id: '/target/$id', + path: '/target/$id', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FromIdRoute = FromIdRouteImport.update({ + id: '/from/$id', + path: '/from/$id', + getParentRoute: () => rootRouteImport, +} as any) +const BoomIdRoute = BoomIdRouteImport.update({ + id: '/boom/$id', + path: '/boom/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + id: + | '__root__' + | '/' + | '/boom/$id' + | '/from/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BoomIdRoute: typeof BoomIdRoute + FromIdRoute: typeof FromIdRoute + MissingIdRoute: typeof MissingIdRoute + TargetIdRoute: typeof TargetIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/target/$id': { + id: '/target/$id' + path: '/target/$id' + fullPath: '/target/$id' + preLoaderRoute: typeof TargetIdRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + '/from/$id': { + id: '/from/$id' + path: '/from/$id' + fullPath: '/from/$id' + preLoaderRoute: typeof FromIdRouteImport + parentRoute: typeof rootRouteImport + } + '/boom/$id': { + id: '/boom/$id' + path: '/boom/$id' + fullPath: '/boom/$id' + preLoaderRoute: typeof BoomIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BoomIdRoute: BoomIdRoute, + FromIdRoute: FromIdRoute, + MissingIdRoute: MissingIdRoute, + TargetIdRoute: TargetIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx new file mode 100644 index 0000000000..d933b93df2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/boom/$id')({ + loader: ({ params }) => { + throw new Error(`boom-${params.id}`) + }, + errorComponent: BoomErrorComponent, + component: BoomComponent, +}) + +function BoomErrorComponent() { + return
error-paths-error-boundary
+} + +function BoomComponent() { + return <> +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx new file mode 100644 index 0000000000..6ffba96bce --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/vue-router' + +export const Route = createFileRoute('/from/$id')({ + loader: ({ params }) => { + const { id } = params + + throw redirect({ to: '/target/$id', params: { id }, statusCode: 302 }) + }, + component: FromComponent, +}) + +function FromComponent() { + return <> +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx new file mode 100644 index 0000000000..53fb604c60 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
error-paths-index
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..0e4972f5c6 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx @@ -0,0 +1,19 @@ +import { createFileRoute, notFound } from '@tanstack/vue-router' + +export const Route = createFileRoute('/missing/$id')({ + loader: () => { + throw notFound() + }, + notFoundComponent: MissingNotFoundComponent, + component: MissingComponent, +}) + +function MissingNotFoundComponent() { + return ( +
error-paths-not-found-boundary
+ ) +} + +function MissingComponent() { + return <> +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx new file mode 100644 index 0000000000..17ade6bf5b --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/target/$id')({ + component: TargetComponent, +}) + +function TargetComponent() { + const params = Route.useParams() + + return
{`target-${params.value.id}`}
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts b/benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts new file mode 100644 index 0000000000..9a0ca28a25 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server error-paths (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/large-page-data.ts new file mode 100644 index 0000000000..8f77c58731 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/large-page-data.ts @@ -0,0 +1,111 @@ +export interface LargePageRecord { + id: string + name: string + description: string +} + +export interface LargePageLevelData { + level: number + marker: string + records: Array +} + +const recordCount = 200 +const descriptionLength = 104 + +export function makeLargePageLevelData(level: number, seed: number) { + const random = createDeterministicRandom(seed) + + return { + level, + marker: makeLargePageMarker(level), + records: Array.from({ length: recordCount }, (_, index) => { + const idToken = randomSegment(random) + const descriptionTokenA = randomSegment(random) + const descriptionTokenB = randomSegment(random) + + return { + id: `l${level}-${index}-${idToken}`, + name: makeLargePageRecordName(level, index), + description: makeDescription( + level, + index, + descriptionTokenA, + descriptionTokenB, + ), + } + }), + } satisfies LargePageLevelData +} + +export function makeLargePageHead(loaderData: LargePageLevelData | undefined) { + if (!loaderData) { + return { + meta: [{ title: 'Peak Large Page' }], + } + } + + const first = loaderData.records[0]! + const last = loaderData.records[loaderData.records.length - 1]! + + return { + meta: [ + { title: `Peak Large Page L${loaderData.level} ${first.name}` }, + { + name: `peak-large-page-l${loaderData.level}-count`, + content: String(loaderData.records.length), + }, + { + name: `peak-large-page-l${loaderData.level}-first-id`, + content: first.id, + }, + { + name: `peak-large-page-l${loaderData.level}-first-name`, + content: first.name, + }, + { + name: `peak-large-page-l${loaderData.level}-last-id`, + content: last.id, + }, + { + name: `peak-large-page-l${loaderData.level}-description`, + content: first.description, + }, + ], + } +} + +function makeLargePageRecordName(level: number, index: number) { + return `peak-large-page-l${level}-record-${index}` +} + +function makeLargePageMarker(level: number) { + return `peak-large-page-level-${level}` +} + +function makeDescription( + level: number, + index: number, + tokenA: string, + tokenB: string, +) { + return `Level ${level} record ${index} uses seeded fragments ${tokenA} and ${tokenB} to keep a fresh deterministic loader payload.`.padEnd( + descriptionLength, + 'x', + ) +} + +// Local copy of the LCG from benchmarks/memory/server/bench-utils.ts - app +// source cannot import from outside the app root. Keep in sync. +function createDeterministicRandom(seed: number) { + let state = seed >>> 0 + + return () => { + state = (state * 1664525 + 1013904223) >>> 0 + return state / 0x100000000 + } +} + +function randomSegment(random: () => number) { + return Math.floor(random() * 1_000_000_000).toString(36) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/project.json b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json new file mode 100644 index 0000000000..699a243a03 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-peak-large-page-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..60a1bd1fbc --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts @@ -0,0 +1,305 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as L1RouteImport } from './routes/l1' +import { Route as IndexRouteImport } from './routes/index' +import { Route as L1L2RouteImport } from './routes/l1.l2' +import { Route as L1L2L3RouteImport } from './routes/l1.l2.l3' +import { Route as L1L2L3L4RouteImport } from './routes/l1.l2.l3.l4' +import { Route as L1L2L3L4L5RouteImport } from './routes/l1.l2.l3.l4.l5' +import { Route as L1L2L3L4L5L6RouteImport } from './routes/l1.l2.l3.l4.l5.l6' +import { Route as L1L2L3L4L5L6L7RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7' +import { Route as L1L2L3L4L5L6L7L8RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7.l8' + +const L1Route = L1RouteImport.update({ + id: '/l1', + path: '/l1', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const L1L2Route = L1L2RouteImport.update({ + id: '/l2', + path: '/l2', + getParentRoute: () => L1Route, +} as any) +const L1L2L3Route = L1L2L3RouteImport.update({ + id: '/l3', + path: '/l3', + getParentRoute: () => L1L2Route, +} as any) +const L1L2L3L4Route = L1L2L3L4RouteImport.update({ + id: '/l4', + path: '/l4', + getParentRoute: () => L1L2L3Route, +} as any) +const L1L2L3L4L5Route = L1L2L3L4L5RouteImport.update({ + id: '/l5', + path: '/l5', + getParentRoute: () => L1L2L3L4Route, +} as any) +const L1L2L3L4L5L6Route = L1L2L3L4L5L6RouteImport.update({ + id: '/l6', + path: '/l6', + getParentRoute: () => L1L2L3L4L5Route, +} as any) +const L1L2L3L4L5L6L7Route = L1L2L3L4L5L6L7RouteImport.update({ + id: '/l7', + path: '/l7', + getParentRoute: () => L1L2L3L4L5L6Route, +} as any) +const L1L2L3L4L5L6L7L8Route = L1L2L3L4L5L6L7L8RouteImport.update({ + id: '/l8', + path: '/l8', + getParentRoute: () => L1L2L3L4L5L6L7Route, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + id: + | '__root__' + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + L1Route: typeof L1RouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/l1': { + id: '/l1' + path: '/l1' + fullPath: '/l1' + preLoaderRoute: typeof L1RouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l1/l2': { + id: '/l1/l2' + path: '/l2' + fullPath: '/l1/l2' + preLoaderRoute: typeof L1L2RouteImport + parentRoute: typeof L1Route + } + '/l1/l2/l3': { + id: '/l1/l2/l3' + path: '/l3' + fullPath: '/l1/l2/l3' + preLoaderRoute: typeof L1L2L3RouteImport + parentRoute: typeof L1L2Route + } + '/l1/l2/l3/l4': { + id: '/l1/l2/l3/l4' + path: '/l4' + fullPath: '/l1/l2/l3/l4' + preLoaderRoute: typeof L1L2L3L4RouteImport + parentRoute: typeof L1L2L3Route + } + '/l1/l2/l3/l4/l5': { + id: '/l1/l2/l3/l4/l5' + path: '/l5' + fullPath: '/l1/l2/l3/l4/l5' + preLoaderRoute: typeof L1L2L3L4L5RouteImport + parentRoute: typeof L1L2L3L4Route + } + '/l1/l2/l3/l4/l5/l6': { + id: '/l1/l2/l3/l4/l5/l6' + path: '/l6' + fullPath: '/l1/l2/l3/l4/l5/l6' + preLoaderRoute: typeof L1L2L3L4L5L6RouteImport + parentRoute: typeof L1L2L3L4L5Route + } + '/l1/l2/l3/l4/l5/l6/l7': { + id: '/l1/l2/l3/l4/l5/l6/l7' + path: '/l7' + fullPath: '/l1/l2/l3/l4/l5/l6/l7' + preLoaderRoute: typeof L1L2L3L4L5L6L7RouteImport + parentRoute: typeof L1L2L3L4L5L6Route + } + '/l1/l2/l3/l4/l5/l6/l7/l8': { + id: '/l1/l2/l3/l4/l5/l6/l7/l8' + path: '/l8' + fullPath: '/l1/l2/l3/l4/l5/l6/l7/l8' + preLoaderRoute: typeof L1L2L3L4L5L6L7L8RouteImport + parentRoute: typeof L1L2L3L4L5L6L7Route + } + } +} + +interface L1L2L3L4L5L6L7RouteChildren { + L1L2L3L4L5L6L7L8Route: typeof L1L2L3L4L5L6L7L8Route +} + +const L1L2L3L4L5L6L7RouteChildren: L1L2L3L4L5L6L7RouteChildren = { + L1L2L3L4L5L6L7L8Route: L1L2L3L4L5L6L7L8Route, +} + +const L1L2L3L4L5L6L7RouteWithChildren = L1L2L3L4L5L6L7Route._addFileChildren( + L1L2L3L4L5L6L7RouteChildren, +) + +interface L1L2L3L4L5L6RouteChildren { + L1L2L3L4L5L6L7Route: typeof L1L2L3L4L5L6L7RouteWithChildren +} + +const L1L2L3L4L5L6RouteChildren: L1L2L3L4L5L6RouteChildren = { + L1L2L3L4L5L6L7Route: L1L2L3L4L5L6L7RouteWithChildren, +} + +const L1L2L3L4L5L6RouteWithChildren = L1L2L3L4L5L6Route._addFileChildren( + L1L2L3L4L5L6RouteChildren, +) + +interface L1L2L3L4L5RouteChildren { + L1L2L3L4L5L6Route: typeof L1L2L3L4L5L6RouteWithChildren +} + +const L1L2L3L4L5RouteChildren: L1L2L3L4L5RouteChildren = { + L1L2L3L4L5L6Route: L1L2L3L4L5L6RouteWithChildren, +} + +const L1L2L3L4L5RouteWithChildren = L1L2L3L4L5Route._addFileChildren( + L1L2L3L4L5RouteChildren, +) + +interface L1L2L3L4RouteChildren { + L1L2L3L4L5Route: typeof L1L2L3L4L5RouteWithChildren +} + +const L1L2L3L4RouteChildren: L1L2L3L4RouteChildren = { + L1L2L3L4L5Route: L1L2L3L4L5RouteWithChildren, +} + +const L1L2L3L4RouteWithChildren = L1L2L3L4Route._addFileChildren( + L1L2L3L4RouteChildren, +) + +interface L1L2L3RouteChildren { + L1L2L3L4Route: typeof L1L2L3L4RouteWithChildren +} + +const L1L2L3RouteChildren: L1L2L3RouteChildren = { + L1L2L3L4Route: L1L2L3L4RouteWithChildren, +} + +const L1L2L3RouteWithChildren = + L1L2L3Route._addFileChildren(L1L2L3RouteChildren) + +interface L1L2RouteChildren { + L1L2L3Route: typeof L1L2L3RouteWithChildren +} + +const L1L2RouteChildren: L1L2RouteChildren = { + L1L2L3Route: L1L2L3RouteWithChildren, +} + +const L1L2RouteWithChildren = L1L2Route._addFileChildren(L1L2RouteChildren) + +interface L1RouteChildren { + L1L2Route: typeof L1L2RouteWithChildren +} + +const L1RouteChildren: L1RouteChildren = { + L1L2Route: L1L2RouteWithChildren, +} + +const L1RouteWithChildren = L1Route._addFileChildren(L1RouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + L1Route: L1RouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx new file mode 100644 index 0000000000..25099720e4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
peak-large-page-index
+} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx new file mode 100644 index 0000000000..0651ba886d --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ + loader: () => makeLargePageLevelData(8, 0x5eed_1008), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelEightComponent, +}) + +function LevelEightComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx new file mode 100644 index 0000000000..1f89e6f454 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ + loader: () => makeLargePageLevelData(7, 0x5eed_1007), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSevenComponent, +}) + +function LevelSevenComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx new file mode 100644 index 0000000000..1a8c98ceb2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ + loader: () => makeLargePageLevelData(6, 0x5eed_1006), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSixComponent, +}) + +function LevelSixComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx new file mode 100644 index 0000000000..3e9afad0b9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ + loader: () => makeLargePageLevelData(5, 0x5eed_1005), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFiveComponent, +}) + +function LevelFiveComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx new file mode 100644 index 0000000000..73c29bc402 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4')({ + loader: () => makeLargePageLevelData(4, 0x5eed_1004), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFourComponent, +}) + +function LevelFourComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx new file mode 100644 index 0000000000..e5b57463ad --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3')({ + loader: () => makeLargePageLevelData(3, 0x5eed_1003), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelThreeComponent, +}) + +function LevelThreeComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx new file mode 100644 index 0000000000..18999233cc --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2')({ + loader: () => makeLargePageLevelData(2, 0x5eed_1002), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelTwoComponent, +}) + +function LevelTwoComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx new file mode 100644 index 0000000000..8fa7a5e0dc --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1')({ + loader: () => makeLargePageLevelData(1, 0x5eed_1001), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelOneComponent, +}) + +function LevelOneComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts new file mode 100644 index 0000000000..adaacda86e --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server peak-large-page (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/shared.ts b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts new file mode 100644 index 0000000000..578fc9c5a2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts @@ -0,0 +1,70 @@ +import { runSequentialRequestLoop } from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +const benchmarkSeed = 0x5eed_0005 +const peakLargePageIterations = 20 +const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8' +const levelEightMarker = 'data-bench="peak-large-page-level-8"' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildPeakLargePageRequest() { + return new Request(peakLargePageUrl, requestInit) +} + +function validatePeakLargePageResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } +} + +function validatePeakLargePageBody(body: string) { + if (!body.includes(levelEightMarker)) { + throw new Error('Expected peak-large-page level-8 marker in response body') + } +} + +async function assertPeakLargePageSanity(handler: StartRequestHandler) { + const request = new Request(peakLargePageUrl, requestInit) + const response = await handler.fetch(request) + const body = await response.text() + + validatePeakLargePageResponse(response, request) + validatePeakLargePageBody(body) +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: peakLargePageIterations, + buildRequest: buildPeakLargePageRequest, + validateResponse: validatePeakLargePageResponse, + pinGcBetweenIterations: true, + verifyGcFloor: true, + }) + + return { + sanity: () => assertPeakLargePageSanity(handler), + workloads: [ + { + name: `mem server peak-large-page (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/project.json b/benchmarks/memory/server/scenarios/peak-large-page/solid/project.json new file mode 100644 index 0000000000..85e8f83224 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-peak-large-page-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..c3ccdaaa0e --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts @@ -0,0 +1,305 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as L1RouteImport } from './routes/l1' +import { Route as IndexRouteImport } from './routes/index' +import { Route as L1L2RouteImport } from './routes/l1.l2' +import { Route as L1L2L3RouteImport } from './routes/l1.l2.l3' +import { Route as L1L2L3L4RouteImport } from './routes/l1.l2.l3.l4' +import { Route as L1L2L3L4L5RouteImport } from './routes/l1.l2.l3.l4.l5' +import { Route as L1L2L3L4L5L6RouteImport } from './routes/l1.l2.l3.l4.l5.l6' +import { Route as L1L2L3L4L5L6L7RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7' +import { Route as L1L2L3L4L5L6L7L8RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7.l8' + +const L1Route = L1RouteImport.update({ + id: '/l1', + path: '/l1', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const L1L2Route = L1L2RouteImport.update({ + id: '/l2', + path: '/l2', + getParentRoute: () => L1Route, +} as any) +const L1L2L3Route = L1L2L3RouteImport.update({ + id: '/l3', + path: '/l3', + getParentRoute: () => L1L2Route, +} as any) +const L1L2L3L4Route = L1L2L3L4RouteImport.update({ + id: '/l4', + path: '/l4', + getParentRoute: () => L1L2L3Route, +} as any) +const L1L2L3L4L5Route = L1L2L3L4L5RouteImport.update({ + id: '/l5', + path: '/l5', + getParentRoute: () => L1L2L3L4Route, +} as any) +const L1L2L3L4L5L6Route = L1L2L3L4L5L6RouteImport.update({ + id: '/l6', + path: '/l6', + getParentRoute: () => L1L2L3L4L5Route, +} as any) +const L1L2L3L4L5L6L7Route = L1L2L3L4L5L6L7RouteImport.update({ + id: '/l7', + path: '/l7', + getParentRoute: () => L1L2L3L4L5L6Route, +} as any) +const L1L2L3L4L5L6L7L8Route = L1L2L3L4L5L6L7L8RouteImport.update({ + id: '/l8', + path: '/l8', + getParentRoute: () => L1L2L3L4L5L6L7Route, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + id: + | '__root__' + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + L1Route: typeof L1RouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/l1': { + id: '/l1' + path: '/l1' + fullPath: '/l1' + preLoaderRoute: typeof L1RouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l1/l2': { + id: '/l1/l2' + path: '/l2' + fullPath: '/l1/l2' + preLoaderRoute: typeof L1L2RouteImport + parentRoute: typeof L1Route + } + '/l1/l2/l3': { + id: '/l1/l2/l3' + path: '/l3' + fullPath: '/l1/l2/l3' + preLoaderRoute: typeof L1L2L3RouteImport + parentRoute: typeof L1L2Route + } + '/l1/l2/l3/l4': { + id: '/l1/l2/l3/l4' + path: '/l4' + fullPath: '/l1/l2/l3/l4' + preLoaderRoute: typeof L1L2L3L4RouteImport + parentRoute: typeof L1L2L3Route + } + '/l1/l2/l3/l4/l5': { + id: '/l1/l2/l3/l4/l5' + path: '/l5' + fullPath: '/l1/l2/l3/l4/l5' + preLoaderRoute: typeof L1L2L3L4L5RouteImport + parentRoute: typeof L1L2L3L4Route + } + '/l1/l2/l3/l4/l5/l6': { + id: '/l1/l2/l3/l4/l5/l6' + path: '/l6' + fullPath: '/l1/l2/l3/l4/l5/l6' + preLoaderRoute: typeof L1L2L3L4L5L6RouteImport + parentRoute: typeof L1L2L3L4L5Route + } + '/l1/l2/l3/l4/l5/l6/l7': { + id: '/l1/l2/l3/l4/l5/l6/l7' + path: '/l7' + fullPath: '/l1/l2/l3/l4/l5/l6/l7' + preLoaderRoute: typeof L1L2L3L4L5L6L7RouteImport + parentRoute: typeof L1L2L3L4L5L6Route + } + '/l1/l2/l3/l4/l5/l6/l7/l8': { + id: '/l1/l2/l3/l4/l5/l6/l7/l8' + path: '/l8' + fullPath: '/l1/l2/l3/l4/l5/l6/l7/l8' + preLoaderRoute: typeof L1L2L3L4L5L6L7L8RouteImport + parentRoute: typeof L1L2L3L4L5L6L7Route + } + } +} + +interface L1L2L3L4L5L6L7RouteChildren { + L1L2L3L4L5L6L7L8Route: typeof L1L2L3L4L5L6L7L8Route +} + +const L1L2L3L4L5L6L7RouteChildren: L1L2L3L4L5L6L7RouteChildren = { + L1L2L3L4L5L6L7L8Route: L1L2L3L4L5L6L7L8Route, +} + +const L1L2L3L4L5L6L7RouteWithChildren = L1L2L3L4L5L6L7Route._addFileChildren( + L1L2L3L4L5L6L7RouteChildren, +) + +interface L1L2L3L4L5L6RouteChildren { + L1L2L3L4L5L6L7Route: typeof L1L2L3L4L5L6L7RouteWithChildren +} + +const L1L2L3L4L5L6RouteChildren: L1L2L3L4L5L6RouteChildren = { + L1L2L3L4L5L6L7Route: L1L2L3L4L5L6L7RouteWithChildren, +} + +const L1L2L3L4L5L6RouteWithChildren = L1L2L3L4L5L6Route._addFileChildren( + L1L2L3L4L5L6RouteChildren, +) + +interface L1L2L3L4L5RouteChildren { + L1L2L3L4L5L6Route: typeof L1L2L3L4L5L6RouteWithChildren +} + +const L1L2L3L4L5RouteChildren: L1L2L3L4L5RouteChildren = { + L1L2L3L4L5L6Route: L1L2L3L4L5L6RouteWithChildren, +} + +const L1L2L3L4L5RouteWithChildren = L1L2L3L4L5Route._addFileChildren( + L1L2L3L4L5RouteChildren, +) + +interface L1L2L3L4RouteChildren { + L1L2L3L4L5Route: typeof L1L2L3L4L5RouteWithChildren +} + +const L1L2L3L4RouteChildren: L1L2L3L4RouteChildren = { + L1L2L3L4L5Route: L1L2L3L4L5RouteWithChildren, +} + +const L1L2L3L4RouteWithChildren = L1L2L3L4Route._addFileChildren( + L1L2L3L4RouteChildren, +) + +interface L1L2L3RouteChildren { + L1L2L3L4Route: typeof L1L2L3L4RouteWithChildren +} + +const L1L2L3RouteChildren: L1L2L3RouteChildren = { + L1L2L3L4Route: L1L2L3L4RouteWithChildren, +} + +const L1L2L3RouteWithChildren = + L1L2L3Route._addFileChildren(L1L2L3RouteChildren) + +interface L1L2RouteChildren { + L1L2L3Route: typeof L1L2L3RouteWithChildren +} + +const L1L2RouteChildren: L1L2RouteChildren = { + L1L2L3Route: L1L2L3RouteWithChildren, +} + +const L1L2RouteWithChildren = L1L2Route._addFileChildren(L1L2RouteChildren) + +interface L1RouteChildren { + L1L2Route: typeof L1L2RouteWithChildren +} + +const L1RouteChildren: L1RouteChildren = { + L1L2Route: L1L2RouteWithChildren, +} + +const L1RouteWithChildren = L1Route._addFileChildren(L1RouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + L1Route: L1RouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..6e83136ea0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from '@solidjs/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx new file mode 100644 index 0000000000..111ffaa36a --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
peak-large-page-index
+} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx new file mode 100644 index 0000000000..b0f94893fb --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ + loader: () => makeLargePageLevelData(8, 0x5eed_1008), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelEightComponent, +}) + +function LevelEightComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx new file mode 100644 index 0000000000..f8a3819c85 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ + loader: () => makeLargePageLevelData(7, 0x5eed_1007), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSevenComponent, +}) + +function LevelSevenComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx new file mode 100644 index 0000000000..7a438ddbd2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ + loader: () => makeLargePageLevelData(6, 0x5eed_1006), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSixComponent, +}) + +function LevelSixComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx new file mode 100644 index 0000000000..2e05985ac5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ + loader: () => makeLargePageLevelData(5, 0x5eed_1005), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFiveComponent, +}) + +function LevelFiveComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx new file mode 100644 index 0000000000..4f38f0cb76 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4')({ + loader: () => makeLargePageLevelData(4, 0x5eed_1004), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFourComponent, +}) + +function LevelFourComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx new file mode 100644 index 0000000000..3f1f113fe3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3')({ + loader: () => makeLargePageLevelData(3, 0x5eed_1003), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelThreeComponent, +}) + +function LevelThreeComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx new file mode 100644 index 0000000000..bd338fd53f --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2')({ + loader: () => makeLargePageLevelData(2, 0x5eed_1002), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelTwoComponent, +}) + +function LevelTwoComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx new file mode 100644 index 0000000000..9dcfd7448a --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1')({ + loader: () => makeLargePageLevelData(1, 0x5eed_1001), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelOneComponent, +}) + +function LevelOneComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json new file mode 100644 index 0000000000..cdb88448d7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts new file mode 100644 index 0000000000..27d4dd65f7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts @@ -0,0 +1,47 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server peak-large-page (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/project.json b/benchmarks/memory/server/scenarios/peak-large-page/vue/project.json new file mode 100644 index 0000000000..f424a2134f --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-peak-large-page-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..26cc9f6058 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts @@ -0,0 +1,305 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as L1RouteImport } from './routes/l1' +import { Route as IndexRouteImport } from './routes/index' +import { Route as L1L2RouteImport } from './routes/l1.l2' +import { Route as L1L2L3RouteImport } from './routes/l1.l2.l3' +import { Route as L1L2L3L4RouteImport } from './routes/l1.l2.l3.l4' +import { Route as L1L2L3L4L5RouteImport } from './routes/l1.l2.l3.l4.l5' +import { Route as L1L2L3L4L5L6RouteImport } from './routes/l1.l2.l3.l4.l5.l6' +import { Route as L1L2L3L4L5L6L7RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7' +import { Route as L1L2L3L4L5L6L7L8RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7.l8' + +const L1Route = L1RouteImport.update({ + id: '/l1', + path: '/l1', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const L1L2Route = L1L2RouteImport.update({ + id: '/l2', + path: '/l2', + getParentRoute: () => L1Route, +} as any) +const L1L2L3Route = L1L2L3RouteImport.update({ + id: '/l3', + path: '/l3', + getParentRoute: () => L1L2Route, +} as any) +const L1L2L3L4Route = L1L2L3L4RouteImport.update({ + id: '/l4', + path: '/l4', + getParentRoute: () => L1L2L3Route, +} as any) +const L1L2L3L4L5Route = L1L2L3L4L5RouteImport.update({ + id: '/l5', + path: '/l5', + getParentRoute: () => L1L2L3L4Route, +} as any) +const L1L2L3L4L5L6Route = L1L2L3L4L5L6RouteImport.update({ + id: '/l6', + path: '/l6', + getParentRoute: () => L1L2L3L4L5Route, +} as any) +const L1L2L3L4L5L6L7Route = L1L2L3L4L5L6L7RouteImport.update({ + id: '/l7', + path: '/l7', + getParentRoute: () => L1L2L3L4L5L6Route, +} as any) +const L1L2L3L4L5L6L7L8Route = L1L2L3L4L5L6L7L8RouteImport.update({ + id: '/l8', + path: '/l8', + getParentRoute: () => L1L2L3L4L5L6L7Route, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + id: + | '__root__' + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + L1Route: typeof L1RouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/l1': { + id: '/l1' + path: '/l1' + fullPath: '/l1' + preLoaderRoute: typeof L1RouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l1/l2': { + id: '/l1/l2' + path: '/l2' + fullPath: '/l1/l2' + preLoaderRoute: typeof L1L2RouteImport + parentRoute: typeof L1Route + } + '/l1/l2/l3': { + id: '/l1/l2/l3' + path: '/l3' + fullPath: '/l1/l2/l3' + preLoaderRoute: typeof L1L2L3RouteImport + parentRoute: typeof L1L2Route + } + '/l1/l2/l3/l4': { + id: '/l1/l2/l3/l4' + path: '/l4' + fullPath: '/l1/l2/l3/l4' + preLoaderRoute: typeof L1L2L3L4RouteImport + parentRoute: typeof L1L2L3Route + } + '/l1/l2/l3/l4/l5': { + id: '/l1/l2/l3/l4/l5' + path: '/l5' + fullPath: '/l1/l2/l3/l4/l5' + preLoaderRoute: typeof L1L2L3L4L5RouteImport + parentRoute: typeof L1L2L3L4Route + } + '/l1/l2/l3/l4/l5/l6': { + id: '/l1/l2/l3/l4/l5/l6' + path: '/l6' + fullPath: '/l1/l2/l3/l4/l5/l6' + preLoaderRoute: typeof L1L2L3L4L5L6RouteImport + parentRoute: typeof L1L2L3L4L5Route + } + '/l1/l2/l3/l4/l5/l6/l7': { + id: '/l1/l2/l3/l4/l5/l6/l7' + path: '/l7' + fullPath: '/l1/l2/l3/l4/l5/l6/l7' + preLoaderRoute: typeof L1L2L3L4L5L6L7RouteImport + parentRoute: typeof L1L2L3L4L5L6Route + } + '/l1/l2/l3/l4/l5/l6/l7/l8': { + id: '/l1/l2/l3/l4/l5/l6/l7/l8' + path: '/l8' + fullPath: '/l1/l2/l3/l4/l5/l6/l7/l8' + preLoaderRoute: typeof L1L2L3L4L5L6L7L8RouteImport + parentRoute: typeof L1L2L3L4L5L6L7Route + } + } +} + +interface L1L2L3L4L5L6L7RouteChildren { + L1L2L3L4L5L6L7L8Route: typeof L1L2L3L4L5L6L7L8Route +} + +const L1L2L3L4L5L6L7RouteChildren: L1L2L3L4L5L6L7RouteChildren = { + L1L2L3L4L5L6L7L8Route: L1L2L3L4L5L6L7L8Route, +} + +const L1L2L3L4L5L6L7RouteWithChildren = L1L2L3L4L5L6L7Route._addFileChildren( + L1L2L3L4L5L6L7RouteChildren, +) + +interface L1L2L3L4L5L6RouteChildren { + L1L2L3L4L5L6L7Route: typeof L1L2L3L4L5L6L7RouteWithChildren +} + +const L1L2L3L4L5L6RouteChildren: L1L2L3L4L5L6RouteChildren = { + L1L2L3L4L5L6L7Route: L1L2L3L4L5L6L7RouteWithChildren, +} + +const L1L2L3L4L5L6RouteWithChildren = L1L2L3L4L5L6Route._addFileChildren( + L1L2L3L4L5L6RouteChildren, +) + +interface L1L2L3L4L5RouteChildren { + L1L2L3L4L5L6Route: typeof L1L2L3L4L5L6RouteWithChildren +} + +const L1L2L3L4L5RouteChildren: L1L2L3L4L5RouteChildren = { + L1L2L3L4L5L6Route: L1L2L3L4L5L6RouteWithChildren, +} + +const L1L2L3L4L5RouteWithChildren = L1L2L3L4L5Route._addFileChildren( + L1L2L3L4L5RouteChildren, +) + +interface L1L2L3L4RouteChildren { + L1L2L3L4L5Route: typeof L1L2L3L4L5RouteWithChildren +} + +const L1L2L3L4RouteChildren: L1L2L3L4RouteChildren = { + L1L2L3L4L5Route: L1L2L3L4L5RouteWithChildren, +} + +const L1L2L3L4RouteWithChildren = L1L2L3L4Route._addFileChildren( + L1L2L3L4RouteChildren, +) + +interface L1L2L3RouteChildren { + L1L2L3L4Route: typeof L1L2L3L4RouteWithChildren +} + +const L1L2L3RouteChildren: L1L2L3RouteChildren = { + L1L2L3L4Route: L1L2L3L4RouteWithChildren, +} + +const L1L2L3RouteWithChildren = + L1L2L3Route._addFileChildren(L1L2L3RouteChildren) + +interface L1L2RouteChildren { + L1L2L3Route: typeof L1L2L3RouteWithChildren +} + +const L1L2RouteChildren: L1L2RouteChildren = { + L1L2L3Route: L1L2L3RouteWithChildren, +} + +const L1L2RouteWithChildren = L1L2Route._addFileChildren(L1L2RouteChildren) + +interface L1RouteChildren { + L1L2Route: typeof L1L2RouteWithChildren +} + +const L1RouteChildren: L1RouteChildren = { + L1L2Route: L1L2RouteWithChildren, +} + +const L1RouteWithChildren = L1Route._addFileChildren(L1RouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + L1Route: L1RouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx new file mode 100644 index 0000000000..e9a57d46fd --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
peak-large-page-index
+} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx new file mode 100644 index 0000000000..bd079aa1a0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ + loader: () => makeLargePageLevelData(8, 0x5eed_1008), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelEightComponent, +}) + +function LevelEightComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx new file mode 100644 index 0000000000..7f69f6f438 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ + loader: () => makeLargePageLevelData(7, 0x5eed_1007), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSevenComponent, +}) + +function LevelSevenComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx new file mode 100644 index 0000000000..75a7c717e3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ + loader: () => makeLargePageLevelData(6, 0x5eed_1006), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSixComponent, +}) + +function LevelSixComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx new file mode 100644 index 0000000000..f390ae3db3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ + loader: () => makeLargePageLevelData(5, 0x5eed_1005), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFiveComponent, +}) + +function LevelFiveComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx new file mode 100644 index 0000000000..6b30bd50dc --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4')({ + loader: () => makeLargePageLevelData(4, 0x5eed_1004), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFourComponent, +}) + +function LevelFourComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx new file mode 100644 index 0000000000..6ec6f32fea --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3')({ + loader: () => makeLargePageLevelData(3, 0x5eed_1003), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelThreeComponent, +}) + +function LevelThreeComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx new file mode 100644 index 0000000000..e5b4cf0de1 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2')({ + loader: () => makeLargePageLevelData(2, 0x5eed_1002), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelTwoComponent, +}) + +function LevelTwoComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx new file mode 100644 index 0000000000..a29e465cd4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1')({ + loader: () => makeLargePageLevelData(1, 0x5eed_1001), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelOneComponent, +}) + +function LevelOneComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts new file mode 100644 index 0000000000..0f238bd7f7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server peak-large-page (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/project.json b/benchmarks/memory/server/scenarios/request-churn/react/project.json new file mode 100644 index 0000000000..d997054b95 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-request-churn-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..d22f1bee18 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx new file mode 100644 index 0000000000..e6335b0d83 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
request-churn-index
+} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx new file mode 100644 index 0000000000..5fd86ee5fb --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx @@ -0,0 +1,40 @@ +import { createFileRoute } from '@tanstack/react-router' + +const itemIndexes = Array.from({ length: 5 }, (_, index) => index) + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }) => ({ q: search.q }), + loader: ({ params, deps }) => ({ + id: params.id, + title: `Item ${params.id}`, + q: deps.q, + items: itemIndexes.map((index) => ({ + id: `${params.id}-${index}`, + label: `${deps.q}-${index}`, + })), + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.title}

+

{data.q}

+
    + {data.items.map((item) => ( +
  • {item.label}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts b/benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts new file mode 100644 index 0000000000..1e956958ec --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server request-churn (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/shared.ts b/benchmarks/memory/server/scenarios/request-churn/shared.ts new file mode 100644 index 0000000000..a196eec8b5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/shared.ts @@ -0,0 +1,83 @@ +import { + createDeterministicRandom, + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +const benchmarkSeed = 0xdecafbad +const requestChurnIterations = 40 +const itemPageMarker = 'data-bench="request-churn-item"' +// Module-level so CodSpeed warmups and measurement never replay URLs. +const benchmarkRandom = createDeterministicRandom(benchmarkSeed) +let requestCounter = 0 + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function validateItemResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } +} + +function validateItemBody(body: string) { + if (!body.includes(itemPageMarker)) { + throw new Error('Expected request-churn item marker in response body') + } +} + +async function assertRequestChurnSanity(handler: StartRequestHandler) { + const response = await handler.fetch( + new Request('http://localhost/items/sanity-item?q=q-sanity', requestInit), + ) + const body = await response.text() + + if (response.status !== 200) { + throw new Error(`Expected sanity status 200, got ${response.status}`) + } + + validateItemBody(body) +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + function buildItemRequest(random: () => number) { + const counter = (requestCounter++).toString(36) + const id = `${counter}-${randomSegment(random)}` + const q = `q-${randomSegment(random)}` + + return new Request(`http://localhost/items/${id}?q=${q}`, requestInit) + } + + const run = () => + runSequentialRequestLoop(handler, { + random: benchmarkRandom, + iterations: requestChurnIterations, + buildRequest: buildItemRequest, + validateResponse: validateItemResponse, + pinGcBetweenIterations: true, + }) + + return { + sanity: () => assertRequestChurnSanity(handler), + workloads: [ + { + name: `mem server request-churn (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/project.json b/benchmarks/memory/server/scenarios/request-churn/solid/project.json new file mode 100644 index 0000000000..3f3434ee24 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-request-churn-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts b/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..8f49ec5d1f --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx b/benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..6e83136ea0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from '@solidjs/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/index.tsx new file mode 100644 index 0000000000..d32d31f301 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
request-churn-index
+} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/items.$id.tsx b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/items.$id.tsx new file mode 100644 index 0000000000..e687e22741 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/items.$id.tsx @@ -0,0 +1,40 @@ +import { createFileRoute } from '@tanstack/solid-router' + +const itemIndexes = Array.from({ length: 5 }, (_, index) => index) + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }) => ({ q: search.q }), + loader: ({ params, deps }) => ({ + id: params.id, + title: `Item ${params.id}`, + q: deps.q, + items: itemIndexes.map((index) => ({ + id: `${params.id}-${index}`, + label: `${deps.q}-${index}`, + })), + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data().title}

+

{data().q}

+
    + {data().items.map((item) => ( +
  • {item.label}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/tsconfig.json b/benchmarks/memory/server/scenarios/request-churn/solid/tsconfig.json new file mode 100644 index 0000000000..cdb88448d7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/vite.config.ts b/benchmarks/memory/server/scenarios/request-churn/solid/vite.config.ts new file mode 100644 index 0000000000..fbf8df9491 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/vite.config.ts @@ -0,0 +1,47 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server request-churn (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/request-churn/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/project.json b/benchmarks/memory/server/scenarios/request-churn/vue/project.json new file mode 100644 index 0000000000..11eb1d868b --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-request-churn-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts b/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/request-churn/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..86ccc14a0c --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/src/router.tsx b/benchmarks/memory/server/scenarios/request-churn/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/index.tsx new file mode 100644 index 0000000000..30751100a4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
request-churn-index
+} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/items.$id.tsx b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/items.$id.tsx new file mode 100644 index 0000000000..a98d036565 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/items.$id.tsx @@ -0,0 +1,40 @@ +import { createFileRoute } from '@tanstack/vue-router' + +const itemIndexes = Array.from({ length: 5 }, (_, index) => index) + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }) => ({ q: search.q }), + loader: ({ params, deps }) => ({ + id: params.id, + title: `Item ${params.id}`, + q: deps.q, + items: itemIndexes.map((index) => ({ + id: `${params.id}-${index}`, + label: `${deps.q}-${index}`, + })), + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.value.title}

+

{data.value.q}

+
    + {data.value.items.map((item) => ( +
  • {item.label}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/tsconfig.json b/benchmarks/memory/server/scenarios/request-churn/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/vite.config.ts b/benchmarks/memory/server/scenarios/request-churn/vue/vite.config.ts new file mode 100644 index 0000000000..a551a96e2f --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server request-churn (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/project.json b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json new file mode 100644 index 0000000000..467b40d734 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-serialization-payload-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..7e370fd84c --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DataIdRouteImport } from './routes/data.$id' + +const DataIdRoute = DataIdRouteImport.update({ + id: '/data/$id', + path: '/data/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesByTo { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/data/$id': typeof DataIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/data/$id' + fileRoutesByTo: FileRoutesByTo + to: '/data/$id' + id: '__root__' | '/data/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + DataIdRoute: typeof DataIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/data/$id': { + id: '/data/$id' + path: '/data/$id' + fullPath: '/data/$id' + preLoaderRoute: typeof DataIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + DataIdRoute: DataIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx b/benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx new file mode 100644 index 0000000000..cfba0dbd62 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/react-router' +import { makeSerializationPayload } from '../../../serialization-payload' + +export const Route = createFileRoute('/data/$id')({ + loader: ({ params }) => makeSerializationPayload(params.id), + component: DataComponent, +}) + +function DataComponent() { + const data = Route.useLoaderData() + + return ( +
Map size: {data.lookup.size}
+ ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts new file mode 100644 index 0000000000..de5549de3b --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server serialization-payload (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/serialization-payload.ts b/benchmarks/memory/server/scenarios/serialization-payload/serialization-payload.ts new file mode 100644 index 0000000000..b6f401640e --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/serialization-payload.ts @@ -0,0 +1,115 @@ +const mapEntryCount = 500 +const setEntryCount = 500 +const temporalEntryCount = 500 +const nestedTreeDepth = 5 +const nestedTreeBreadth = 6 +const payloadTextLength = 150 + +export interface MapPayloadValue { + index: number + label: string + createdAt: Date + count: bigint + text: string +} + +export interface NestedPayloadNode { + id: string + depth: number + text: string + values: Array + children: Array +} + +export interface SerializationPayload { + id: string + lookup: Map + tags: Set + dates: Array + bigints: Array + tree: NestedPayloadNode +} + +export function makeSerializationPayload(id: string): SerializationPayload { + const hash = hashId(id) + const baseTimestamp = Date.UTC(2024, 0, 1) + hash + + return { + id, + lookup: new Map( + Array.from( + { length: mapEntryCount }, + (_, index): [string, MapPayloadValue] => [ + makeMapKey(id, index), + { + index, + label: `${id}-map-${index}`, + createdAt: new Date(baseTimestamp + index * 1_000), + count: BigInt(hash) * 10_000n + BigInt(index), + text: makePayloadText(id, `map-${index}`), + }, + ], + ), + ), + tags: new Set( + Array.from({ length: setEntryCount }, (_, index) => + makePayloadText(id, `set-${index}`), + ), + ), + dates: Array.from( + { length: temporalEntryCount }, + (_, index) => new Date(baseTimestamp + index * 60_000), + ), + bigints: Array.from( + { length: temporalEntryCount }, + (_, index) => BigInt(hash) * 1_000_000n + BigInt(index), + ), + tree: makeNestedTree(id, 0, 'root', hash), + } +} + +function makeMapKey(id: string, index: number) { + return `map-${id}-${index.toString().padStart(3, '0')}` +} + +function makeNestedTree( + id: string, + depth: number, + path: string, + hash: number, +): NestedPayloadNode { + return { + id: `${id}-node-${path}`, + depth, + text: makePayloadText(id, `tree-${depth}-${path}`), + values: [hash, depth, path.length], + children: + depth === nestedTreeDepth + ? [] + : Array.from({ length: nestedTreeBreadth }, (_, index) => + makeNestedTree(id, depth + 1, `${path}-${index}`, hash), + ), + } +} + +function makePayloadText(id: string, segment: string) { + const filler = 'abcdefghijklmnopqrstuvwxyz0123456789' + let value = `${id}|${segment}|` + + while (value.length < payloadTextLength) { + value += filler + } + + return value.slice(0, payloadTextLength) +} + +function hashId(id: string) { + let hash = 2_166_136_261 + + for (let index = 0; index < id.length; index++) { + hash ^= id.charCodeAt(index) + hash = Math.imul(hash, 16_777_619) + } + + return hash >>> 0 +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/shared.ts b/benchmarks/memory/server/scenarios/serialization-payload/shared.ts new file mode 100644 index 0000000000..ce095b5e10 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/shared.ts @@ -0,0 +1,77 @@ +import { + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +const benchmarkSeed = 0x51eaa11 +const serializationPayloadIterations = 12 +const payloadPageMarker = 'data-bench="serialization-payload"' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildPayloadRequest(random: () => number, index: number) { + const id = `payload-${index}-${randomSegment(random)}` + + return new Request(`http://localhost/data/${id}`, requestInit) +} + +function validatePayloadResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } +} + +function validatePayloadBody(body: string) { + if (!body.includes(payloadPageMarker)) { + throw new Error('Expected serialization-payload marker in response body') + } +} + +async function assertSerializationPayloadSanity(handler: StartRequestHandler) { + const request = new Request( + 'http://localhost/data/sanity-payload', + requestInit, + ) + const response = await handler.fetch(request) + const body = await response.text() + + validatePayloadResponse(response, request) + validatePayloadBody(body) +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: serializationPayloadIterations, + buildRequest: buildPayloadRequest, + validateResponse: validatePayloadResponse, + pinGcBetweenIterations: true, + verifyGcFloor: true, + }) + + return { + sanity: () => assertSerializationPayloadSanity(handler), + workloads: [ + { + name: `mem server serialization-payload (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/project.json b/benchmarks/memory/server/scenarios/serialization-payload/solid/project.json new file mode 100644 index 0000000000..6f381ef389 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-serialization-payload-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..082cbb290c --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DataIdRouteImport } from './routes/data.$id' + +const DataIdRoute = DataIdRouteImport.update({ + id: '/data/$id', + path: '/data/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesByTo { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/data/$id': typeof DataIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/data/$id' + fileRoutesByTo: FileRoutesByTo + to: '/data/$id' + id: '__root__' | '/data/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + DataIdRoute: typeof DataIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/data/$id': { + id: '/data/$id' + path: '/data/$id' + fullPath: '/data/$id' + preLoaderRoute: typeof DataIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + DataIdRoute: DataIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/src/router.tsx b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..6e83136ea0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from '@solidjs/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/data.$id.tsx b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/data.$id.tsx new file mode 100644 index 0000000000..8cdbdb9046 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/data.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { makeSerializationPayload } from '../../../serialization-payload' + +export const Route = createFileRoute('/data/$id')({ + loader: ({ params }) => makeSerializationPayload(params.id), + component: DataComponent, +}) + +function DataComponent() { + const data = Route.useLoaderData() + + return ( +
+ Map size: {data().lookup.size} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/tsconfig.json b/benchmarks/memory/server/scenarios/serialization-payload/solid/tsconfig.json new file mode 100644 index 0000000000..cdb88448d7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/vite.config.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/vite.config.ts new file mode 100644 index 0000000000..926646ee12 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/vite.config.ts @@ -0,0 +1,47 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server serialization-payload (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/project.json b/benchmarks/memory/server/scenarios/serialization-payload/vue/project.json new file mode 100644 index 0000000000..425b50a0d5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-serialization-payload-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..8adce01d75 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DataIdRouteImport } from './routes/data.$id' + +const DataIdRoute = DataIdRouteImport.update({ + id: '/data/$id', + path: '/data/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesByTo { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/data/$id': typeof DataIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/data/$id' + fileRoutesByTo: FileRoutesByTo + to: '/data/$id' + id: '__root__' | '/data/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + DataIdRoute: typeof DataIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/data/$id': { + id: '/data/$id' + path: '/data/$id' + fullPath: '/data/$id' + preLoaderRoute: typeof DataIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + DataIdRoute: DataIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/src/router.tsx b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/data.$id.tsx b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/data.$id.tsx new file mode 100644 index 0000000000..e6b4cfadfb --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/data.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { makeSerializationPayload } from '../../../serialization-payload' + +export const Route = createFileRoute('/data/$id')({ + loader: ({ params }) => makeSerializationPayload(params.id), + component: DataComponent, +}) + +function DataComponent() { + const data = Route.useLoaderData() + + return ( +
+ Map size: {data.value.lookup.size} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/tsconfig.json b/benchmarks/memory/server/scenarios/serialization-payload/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/vite.config.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/vite.config.ts new file mode 100644 index 0000000000..41a5596adf --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server serialization-payload (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json new file mode 100644 index 0000000000..841bbedca0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-server-fn-churn-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts new file mode 100644 index 0000000000..22770278cd --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts @@ -0,0 +1,24 @@ +import { createMiddleware, createServerFn } from '@tanstack/react-start' +import { + makeServerFnChurnPayload, + validateServerFnInput, +} from '../../server-fn-payload' + +const contextMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => + next({ + context: { + ctx: 'ctx-server-fn-churn', + }, + }), +) + +export const churnGet = createServerFn({ method: 'GET' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) + +export const churnPost = createServerFn({ method: 'POST' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..10e933da3d --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' + id: '__root__' | '/' | '/api/fn-urls' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts new file mode 100644 index 0000000000..da74a20f62 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + get: churnGet.url, + post: churnPost.url, + }), + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx new file mode 100644 index 0000000000..de0acfc2b3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ memory-server-fn-churn-index +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts new file mode 100644 index 0000000000..68d9ed839b --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server server-fn-churn (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/server-fn-payload.ts b/benchmarks/memory/server/scenarios/server-fn-churn/server-fn-payload.ts new file mode 100644 index 0000000000..0f23390a6c --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/server-fn-payload.ts @@ -0,0 +1,33 @@ +export type ServerFnInput = { + id: string +} + +export type ServerFnChurnContext = { + ctx: string +} + +const recordIndexes = Array.from({ length: 5 }, (_, index) => index) + +export function validateServerFnInput(input: unknown): ServerFnInput { + const payload = input as Partial | null + + if (typeof payload?.id !== 'string') { + throw new Error('invalid server-fn churn input') + } + + return { id: payload.id } +} + +export function makeServerFnChurnPayload( + data: ServerFnInput, + context: ServerFnChurnContext, +) { + return { + id: data.id, + ctx: context.ctx, + payload: recordIndexes.map((index) => ({ + id: `${data.id}-${index}`, + label: `record-${index}`, + })), + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts b/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts new file mode 100644 index 0000000000..6e2959e757 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts @@ -0,0 +1,235 @@ +import { + createDeterministicRandom, + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +type FnUrls = { + get: string + post: string +} + +type PayloadFixture = { + id: string + body: string + query: string +} + +type SerovalNode = + | { + t: 1 + s: string + } + | { + t: 10 + i: number + p: { + k: Array + v: Array + } + o: number + } + +const benchmarkSeed = 0xdecafbad +const payloadSeed = 0x51f0cafe +const fixtureCount = 16 +// Sized to sit just above the 2s measured-run floor on CI (per-iteration +// cost with the pinned collection is ~0.08-0.11s across frameworks). +const serverFnChurnIterations = 30 +const origin = 'http://localhost' +const tssContentTypeFramed = 'application/x-tss-framed' +const acceptHeader = `${tssContentTypeFramed}, application/x-ndjson, application/json` +const xTssSerialized = 'x-tss-serialized' +const contextMarker = 'ctx-server-fn-churn' + +const commonHeaders = { + 'x-tsr-serverFn': 'true', + 'sec-fetch-site': 'same-origin', + accept: acceptHeader, +} satisfies HeadersInit + +const postHeaders = { + ...commonHeaders, + 'content-type': 'application/json', +} satisfies HeadersInit + +// Hand-rolled copy of Start's seroval RPC wire format so POST bodies can be +// precomputed at module level. Coupled to the internal protocol on purpose; +// the module-load sanity check below throws loudly if the protocol drifts. +function stringNode(value: string): SerovalNode { + return { t: 1, s: value } +} + +function objectNode( + id: number, + entries: Array, +): SerovalNode { + return { + t: 10, + i: id, + p: { + k: entries.map(([key]) => key), + v: entries.map(([, value]) => value), + }, + o: 0, + } +} + +function serializePayload(id: string) { + return JSON.stringify({ + t: objectNode(0, [['data', objectNode(1, [['id', stringNode(id)]])]]), + f: 63, + m: [], + }) +} + +function createFixtures(kind: 'get' | 'post') { + const random = createDeterministicRandom(payloadSeed ^ kind.length) + + return Array.from({ length: fixtureCount }, (_, index): PayloadFixture => { + const id = [kind, index, randomSegment(random), randomSegment(random)].join( + '-', + ) + const body = serializePayload(id) + + return { + id, + body, + query: `?${new URLSearchParams({ payload: body })}`, + } + }) +} + +const getFixtures = createFixtures('get') +const postFixtures = createFixtures('post') + +async function discoverUrls(handler: StartRequestHandler) { + const response = await handler.fetch(new Request(`${origin}/api/fn-urls`)) + const text = await response.text() + + if (response.status !== 200) { + throw new Error( + `URL discovery failed with status ${response.status}: ${text}`, + ) + } + + let urls: Partial + + try { + urls = JSON.parse(text) as Partial + } catch (error) { + throw new Error(`URL discovery returned invalid JSON: ${text}`, { + cause: error, + }) + } + + if (typeof urls.get !== 'string' || typeof urls.post !== 'string') { + throw new Error(`URL discovery returned invalid payload: ${text}`) + } + + return urls as FnUrls +} + +function buildGetRequest(url: string, fixture: PayloadFixture) { + return new Request(`${origin}${url}${fixture.query}`, { + method: 'GET', + headers: commonHeaders, + }) +} + +function buildPostRequest(url: string, fixture: PayloadFixture) { + return new Request(`${origin}${url}`, { + method: 'POST', + headers: postHeaders, + body: fixture.body, + }) +} + +function validateServerFnResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } + + if (!response.headers.get(xTssSerialized)) { + throw new Error(`Expected ${xTssSerialized} header for ${request.url}`) + } +} + +function validateEchoedBody( + body: string, + request: Request, + expectedId: string, +) { + if (!body.includes(expectedId)) { + throw new Error(`Expected echoed id ${expectedId} in ${request.url}`) + } + + if (!body.includes(contextMarker)) { + throw new Error( + `Expected context marker ${contextMarker} in ${request.url}`, + ) + } +} + +async function assertServerFnChurnSanity( + handler: StartRequestHandler, + urls: FnUrls, +) { + const getFixture = getFixtures[0]! + const getRequest = buildGetRequest(urls.get, getFixture) + const getResponse = await handler.fetch(getRequest) + const getBody = await getResponse.text() + + validateServerFnResponse(getResponse, getRequest) + validateEchoedBody(getBody, getRequest, getFixture.id) + + const postFixture = postFixtures[0]! + const postRequest = buildPostRequest(urls.post, postFixture) + const postResponse = await handler.fetch(postRequest) + const postBody = await postResponse.text() + + validateServerFnResponse(postResponse, postRequest) + validateEchoedBody(postBody, postRequest, postFixture.id) +} + +export async function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const urls = await discoverUrls(handler) + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: serverFnChurnIterations, + pinGcBetweenIterations: true, + buildRequest: (_random, index) => { + const fixtureIndex = Math.floor(index / 2) % fixtureCount + + if (index % 2 === 0) { + const fixture = getFixtures[fixtureIndex]! + return buildGetRequest(urls.get, fixture) + } else { + const fixture = postFixtures[fixtureIndex]! + return buildPostRequest(urls.post, fixture) + } + }, + validateResponse: validateServerFnResponse, + }) + + return { + sanity: () => assertServerFnChurnSanity(handler, urls), + workloads: [ + { + name: `mem server server-fn-churn (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/project.json b/benchmarks/memory/server/scenarios/server-fn-churn/solid/project.json new file mode 100644 index 0000000000..53aca0cea6 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-server-fn-churn-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts new file mode 100644 index 0000000000..31669dff50 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts @@ -0,0 +1,24 @@ +import { createMiddleware, createServerFn } from '@tanstack/solid-start' +import { + makeServerFnChurnPayload, + validateServerFnInput, +} from '../../server-fn-payload' + +const contextMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => + next({ + context: { + ctx: 'ctx-server-fn-churn', + }, + }), +) + +export const churnGet = createServerFn({ method: 'GET' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) + +export const churnPost = createServerFn({ method: 'POST' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..298ac3168d --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' + id: '__root__' | '/' | '/api/fn-urls' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/router.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..6e83136ea0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from '@solidjs/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/api.fn-urls.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/api.fn-urls.ts new file mode 100644 index 0000000000..2063684034 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/api.fn-urls.ts @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + get: churnGet.url, + post: churnPost.url, + }), + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/index.tsx new file mode 100644 index 0000000000..99fea371f8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ memory-server-fn-churn-index +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/tsconfig.json b/benchmarks/memory/server/scenarios/server-fn-churn/solid/tsconfig.json new file mode 100644 index 0000000000..cdb88448d7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/vite.config.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/vite.config.ts new file mode 100644 index 0000000000..8d6d226e97 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/vite.config.ts @@ -0,0 +1,47 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server server-fn-churn (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/project.json b/benchmarks/memory/server/scenarios/server-fn-churn/vue/project.json new file mode 100644 index 0000000000..c5809a259b --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-server-fn-churn-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts new file mode 100644 index 0000000000..0fa679f76f --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts @@ -0,0 +1,24 @@ +import { createMiddleware, createServerFn } from '@tanstack/vue-start' +import { + makeServerFnChurnPayload, + validateServerFnInput, +} from '../../server-fn-payload' + +const contextMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => + next({ + context: { + ctx: 'ctx-server-fn-churn', + }, + }), +) + +export const churnGet = createServerFn({ method: 'GET' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) + +export const churnPost = createServerFn({ method: 'POST' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..b226f0a25a --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' + id: '__root__' | '/' | '/api/fn-urls' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/router.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/api.fn-urls.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/api.fn-urls.ts new file mode 100644 index 0000000000..ff99e89e4c --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/api.fn-urls.ts @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + get: churnGet.url, + post: churnPost.url, + }), + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/index.tsx new file mode 100644 index 0000000000..0a39b9301c --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ memory-server-fn-churn-index +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/tsconfig.json b/benchmarks/memory/server/scenarios/server-fn-churn/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/vite.config.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/vite.config.ts new file mode 100644 index 0000000000..0e249b9f3b --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server server-fn-churn (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/deferred-section-data.ts b/benchmarks/memory/server/scenarios/streaming-peak/deferred-section-data.ts new file mode 100644 index 0000000000..b30352ef39 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/deferred-section-data.ts @@ -0,0 +1,37 @@ +const deferredRecordCount = 250 +const recordValueLength = 128 + +interface DeferredRecord { + id: string + value: string +} + +export interface DeferredSectionPayload { + index: number + records: Array +} + +export function makeDeferredSectionPayload( + id: string, + sectionIndex: number, +): DeferredSectionPayload { + return { + index: sectionIndex, + records: Array.from({ length: deferredRecordCount }, (_, recordIndex) => ({ + id: `${id}-${sectionIndex}-${recordIndex}`, + value: makeRecordValue(id, sectionIndex, recordIndex), + })), + } +} + +function makeRecordValue( + id: string, + sectionIndex: number, + recordIndex: number, +) { + const token = `${id}:${sectionIndex}:${recordIndex}:streaming-peak-record;` + + return token + .repeat(Math.ceil(recordValueLength / token.length)) + .slice(0, recordValueLength) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/project.json b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json new file mode 100644 index 0000000000..6fc1dcdda0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-streaming-peak-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..fbbc25ecfb --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx new file mode 100644 index 0000000000..669fdef2e1 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
streaming-peak-index
+} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..886cb64d45 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx @@ -0,0 +1,98 @@ +import { Await, createFileRoute } from '@tanstack/react-router' +import { Suspense } from 'react' +import { + makeDeferredSectionPayload, + type DeferredSectionPayload, +} from '../../../deferred-section-data' + +const fallbackFlushTicks = 4 + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + eager: `streaming-peak-eager-${params.id}`, + deferred0: makeDeferredSection(params.id, 0), + deferred1: makeDeferredSection(params.id, 1), + deferred2: makeDeferredSection(params.id, 2), + deferred3: makeDeferredSection(params.id, 3), + }), + component: StreamComponent, +}) + +// Deferred sections must settle strictly AFTER React's shell flush, which +// React schedules via setImmediate internally. Microtask chains drain during +// router load (sections resolve before the Suspense boundaries are even +// reached) and setImmediate chains registered at loader time win the race +// against React's flush — either way no fallback ever streams and the bench +// stops exercising multi-flush streaming. Timers-phase callbacks reliably +// lose that race, but a millisecond delay makes the loss margin wall-clock +// dependent, so this chains 0ms hops instead: each hop yields one full +// event-loop turn (immediates and microtasks included), making the flush +// ordering a function of turn count, not runner speed. Distinct hop counts +// keep section ordering deterministic. +function afterFallbackFlush(sectionIndex: number) { + return new Promise((resolve) => { + let remaining = fallbackFlushTicks + sectionIndex + + const step = () => { + remaining -= 1 + + if (remaining <= 0) { + resolve() + return + } + + setTimeout(step, 0) + } + + setTimeout(step, 0) + }) +} + +function makeDeferredSection(id: string, sectionIndex: number) { + return afterFallbackFlush(sectionIndex).then(() => + makeDeferredSectionPayload(id, sectionIndex), + ) +} + +function StreamComponent() { + const data = Route.useLoaderData() + const deferredSections = [ + { index: 0, promise: data.deferred0 }, + { index: 1, promise: data.deferred1 }, + { index: 2, promise: data.deferred2 }, + { index: 3, promise: data.deferred3 }, + ] as const + + return ( +
+

{data.eager}

+ {deferredSections.map(({ index, promise }) => ( + + streaming-peak-fallback-{index} +

+ } + > + + {(section) => } + +
+ ))} +
+ ) +} + +function DeferredSection({ section }: { section: DeferredSectionPayload }) { + const marker = `streaming-peak-deferred-${section.index}` + + return ( +
+

{marker}

+ {section.records.map((record) => ( +

{record.value}

+ ))} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts new file mode 100644 index 0000000000..5af0f0d138 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server streaming-peak (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/shared.ts b/benchmarks/memory/server/scenarios/streaming-peak/shared.ts new file mode 100644 index 0000000000..6b45470329 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/shared.ts @@ -0,0 +1,114 @@ +import { + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +const benchmarkSeed = 0xdecafbad +const streamingPeakIterations = 20 +const fallbackMarker = 'streaming-peak-fallback-0' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildStreamingRequest(random: () => number, index: number) { + return new Request( + `http://localhost/stream/${index}-${randomSegment(random)}`, + requestInit, + ) +} + +function validateStreamingResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } +} + +function getResponseReader(response: Response) { + const reader = response.body?.getReader() + + if (!reader) { + throw new Error('Expected streaming response body') + } + + return reader +} + +async function readStreamingBody(response: Response) { + const reader = getResponseReader(response) + const decoder = new TextDecoder() + let body = '' + let chunkCount = 0 + + while (true) { + const result = await reader.read() + + if (result.done) { + break + } + + chunkCount++ + body += decoder.decode(result.value, { stream: true }) + } + + body += decoder.decode() + + return { body, chunkCount } +} + +async function assertStreamingPeakSanity(handler: StartRequestHandler) { + const chunkedRequest = new Request( + 'http://localhost/stream/sanity-chunked', + requestInit, + ) + const chunkedResponse = await handler.fetch(chunkedRequest) + + validateStreamingResponse(chunkedResponse, chunkedRequest) + + const chunked = await readStreamingBody(chunkedResponse) + + if (chunked.chunkCount <= 1) { + throw new Error( + `Expected chunked sanity response to produce multiple chunks, got ${chunked.chunkCount}`, + ) + } + + if (!chunked.body.includes(fallbackMarker)) { + throw new Error('Expected streaming-peak fallback marker in response body') + } +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: streamingPeakIterations, + buildRequest: buildStreamingRequest, + validateResponse: validateStreamingResponse, + pinGcBetweenIterations: true, + verifyGcFloor: true, + }) + + return { + sanity: () => assertStreamingPeakSanity(handler), + workloads: [ + { + name: `mem server streaming-peak chunked (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts new file mode 100644 index 0000000000..138ad2bbde --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts @@ -0,0 +1,14 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' + +// TODO(solid-router-v2): re-enable once router-core's SSR stream transformer +// supports Solid 2's streaming shape. Solid 2's renderToStream emits +// `` at the end of the initial shell and streams deferred +// Loading-boundary HTML after it, so transformStreamWithRouter tail-buffers +// every deferred section and throws "SSR stream tail exceeded maximum buffer" +// (MAX_TAIL_CHARS = 64 kB) for this scenario's ~200 kB of deferred payload. +// React and Vue keep deferred chunks before ``, which is what the +// transformer assumes. +describe('memory', () => { + bench.skip('mem server streaming-peak (solid)', () => {}, memoryBenchOptions) +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/project.json b/benchmarks/memory/server/scenarios/streaming-peak/solid/project.json new file mode 100644 index 0000000000..80f44d1063 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-streaming-peak-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..04ebed80f0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/src/router.tsx b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..6e83136ea0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from '@solidjs/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/index.tsx new file mode 100644 index 0000000000..5815e36b3d --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
streaming-peak-index
+} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..a1de56098a --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/stream.$id.tsx @@ -0,0 +1,91 @@ +import { Await, createFileRoute } from '@tanstack/solid-router' +import { Loading } from 'solid-js' +import { + makeDeferredSectionPayload, + type DeferredSectionPayload, +} from '../../../deferred-section-data' + +const fallbackFlushTicks = 12 + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + eager: `streaming-peak-eager-${params.id}`, + deferred0: makeDeferredSection(params.id, 0), + deferred1: makeDeferredSection(params.id, 1), + deferred2: makeDeferredSection(params.id, 2), + deferred3: makeDeferredSection(params.id, 3), + }), + component: StreamComponent, +}) + +// Deferred sections must settle after the framework shell flush so the bench +// continues exercising multi-flush streaming with visible fallback chunks. +// Chained 0ms timers-phase hops give the renderer a deterministic number of +// full event-loop turns to flush, instead of a wall-clock delay whose margin +// varies with runner load; distinct hop counts keep section ordering stable. +function afterFallbackFlush(sectionIndex: number) { + return new Promise((resolve) => { + let remaining = fallbackFlushTicks + sectionIndex + + const step = () => { + remaining -= 1 + + if (remaining <= 0) { + resolve() + return + } + + setTimeout(step, 0) + } + + setTimeout(step, 0) + }) +} + +function makeDeferredSection(id: string, sectionIndex: number) { + return afterFallbackFlush(sectionIndex).then(() => + makeDeferredSectionPayload(id, sectionIndex), + ) +} + +function StreamComponent() { + const data = Route.useLoaderData() + const deferredSections = () => + [ + { index: 0, promise: data().deferred0 }, + { index: 1, promise: data().deferred1 }, + { index: 2, promise: data().deferred2 }, + { index: 3, promise: data().deferred3 }, + ] as const + + return ( +
+

{data().eager}

+ {deferredSections().map(({ index, promise }) => ( + <> +

+ streaming-peak-fallback-{index} +

+ + + {(section) => } + + + + ))} +
+ ) +} + +function DeferredSection(props: { section: DeferredSectionPayload }) { + const marker = () => `streaming-peak-deferred-${props.section.index}` + + return ( +
+

{marker()}

+ {props.section.records.map((record) => ( +

{record.value}

+ ))} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/tsconfig.json b/benchmarks/memory/server/scenarios/streaming-peak/solid/tsconfig.json new file mode 100644 index 0000000000..cdb88448d7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/vite.config.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/vite.config.ts new file mode 100644 index 0000000000..18bf5b2d04 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/vite.config.ts @@ -0,0 +1,47 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server streaming-peak (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/project.json b/benchmarks/memory/server/scenarios/streaming-peak/vue/project.json new file mode 100644 index 0000000000..55771aebe3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-streaming-peak-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..192f1d6dc7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/src/router.tsx b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/index.tsx new file mode 100644 index 0000000000..bc55b49adc --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
streaming-peak-index
+} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..0c71a8d3a0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/stream.$id.tsx @@ -0,0 +1,98 @@ +import { Await, createFileRoute } from '@tanstack/vue-router' +import { Suspense } from 'vue' +import { + makeDeferredSectionPayload, + type DeferredSectionPayload, +} from '../../../deferred-section-data' + +const fallbackFlushTicks = 20 + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + eager: `streaming-peak-eager-${params.id}`, + deferred0: makeDeferredSection(params.id, 0), + deferred1: makeDeferredSection(params.id, 1), + deferred2: makeDeferredSection(params.id, 2), + deferred3: makeDeferredSection(params.id, 3), + }), + component: StreamComponent, +}) + +// Deferred sections must settle strictly AFTER Vue's shell has a chance to +// flush, so fallbacks are emitted before deferred section content. +// Chained 0ms timers-phase hops give the renderer a deterministic number of +// full event-loop turns to flush, instead of a wall-clock delay whose margin +// varies with runner load; distinct hop counts keep section ordering stable. +function afterFallbackFlush(sectionIndex: number) { + return new Promise((resolve) => { + let remaining = fallbackFlushTicks + sectionIndex + + const step = () => { + remaining -= 1 + + if (remaining <= 0) { + resolve() + return + } + + setTimeout(step, 0) + } + + setTimeout(step, 0) + }) +} + +function makeDeferredSection(id: string, sectionIndex: number) { + return afterFallbackFlush(sectionIndex).then(() => + makeDeferredSectionPayload(id, sectionIndex), + ) +} + +function StreamComponent() { + const data = Route.useLoaderData() + const deferredSections = [ + { index: 0, promise: data.value.deferred0 }, + { index: 1, promise: data.value.deferred1 }, + { index: 2, promise: data.value.deferred2 }, + { index: 3, promise: data.value.deferred3 }, + ] as const + + return ( +
+

{data.value.eager}

+ {deferredSections.map(({ index, promise }) => ( + <> +

+ streaming-peak-fallback-{index} +

+ + {{ + default: () => ( + ( + + )} + /> + ), + fallback: () => null, + }} + + + ))} +
+ ) +} + +function DeferredSection({ section }: { section: DeferredSectionPayload }) { + const marker = `streaming-peak-deferred-${section.index}` + + return ( +
+

{marker}

+ {section.records.map((record) => ( +

{record.value}

+ ))} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/tsconfig.json b/benchmarks/memory/server/scenarios/streaming-peak/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/vite.config.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/vite.config.ts new file mode 100644 index 0000000000..7cd481da0b --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + // Keep lazily-compiled code alive for the whole run: the pinned + // collections age code fast enough for V8 to flush unused bytecode, + // and the mid-measurement recompile injects a multi-MB allocation + // burst at a run-dependent time. + execArgv: [ + '--no-flush-bytecode', + // Pre-size the V8 heap so no space has to grow mid-measurement: + // heap-growth events allocate several MB at a run-dependent moment, + // which flips the measured peak bimodally between identical runs. + '--initial-old-space-size=64', + '--min-semi-space-size=16', + '--max-semi-space-size=16', + ], + name: '@benchmarks/memory-server streaming-peak (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/tsconfig.json b/benchmarks/memory/server/tsconfig.json new file mode 100644 index 0000000000..5d5dcbf825 --- /dev/null +++ b/benchmarks/memory/server/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": ["bench-utils.ts"] +} diff --git a/benchmarks/memory/server/vitest.react.config.ts b/benchmarks/memory/server/vitest.react.config.ts new file mode 100644 index 0000000000..44643b4d60 --- /dev/null +++ b/benchmarks/memory/server/vitest.react.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/react/vite.config.ts'], + }, +}) diff --git a/benchmarks/memory/server/vitest.solid.config.ts b/benchmarks/memory/server/vitest.solid.config.ts new file mode 100644 index 0000000000..5c8185cdd9 --- /dev/null +++ b/benchmarks/memory/server/vitest.solid.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/solid/vite.config.ts'], + }, +}) diff --git a/benchmarks/memory/server/vitest.vue.config.ts b/benchmarks/memory/server/vitest.vue.config.ts new file mode 100644 index 0000000000..01768185ee --- /dev/null +++ b/benchmarks/memory/server/vitest.vue.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/vue/vite.config.ts'], + }, +}) diff --git a/docs/start/framework/react/guide/server-functions.md b/docs/start/framework/react/guide/server-functions.md index d762667bd6..903e5d7b86 100644 --- a/docs/start/framework/react/guide/server-functions.md +++ b/docs/start/framework/react/guide/server-functions.md @@ -21,6 +21,9 @@ const time = await getServerTime() Server functions provide server capabilities (database access, environment variables, file system) while maintaining type safety across the network boundary. +> [!NOTE] +> Server functions are meant to be called by your TanStack Start application. They are easy to use from your app code, and Start handles serialization across the client/server boundary. If you need an endpoint that can be called from outside your Start app, use [server routes](./server-routes) instead. + ## Same-Origin Requests Server functions are same-origin RPC endpoints for your application. Browser requests to server functions should come from the same origin, verified with Fetch Metadata (`Sec-Fetch-Site`), `Origin`, or `Referer` headers. Use server routes for public APIs or endpoints that intentionally support cross-origin requests. diff --git a/docs/start/framework/react/guide/server-routes.md b/docs/start/framework/react/guide/server-routes.md index 542b274147..598f586450 100644 --- a/docs/start/framework/react/guide/server-routes.md +++ b/docs/start/framework/react/guide/server-routes.md @@ -24,6 +24,9 @@ export const Route = createFileRoute('/hello')({ }) ``` +> [!NOTE] +> Server routes are meant for HTTP endpoints that need to be called from outside your TanStack Start application. If you only need to call server-side logic from within your Start app and want Start to handle serialization for you, use [server functions](./server-functions) instead. + ## Server Routes and App Routes Because server routes can be defined in the same directory as your app routes, you can even use the same file for both! diff --git a/e2e/react-router/basic-file-based/tests/open-redirect-prevention.spec.ts b/e2e/react-router/basic-file-based/tests/open-redirect-prevention.spec.ts index 3ad83fb455..2f0fe256cd 100644 --- a/e2e/react-router/basic-file-based/tests/open-redirect-prevention.spec.ts +++ b/e2e/react-router/basic-file-based/tests/open-redirect-prevention.spec.ts @@ -69,10 +69,7 @@ test.describe('Open redirect prevention', () => { page, baseURL, }) => { - // When control characters are stripped from paths like /%0d/evil.com/ - // the result could be //evil.com/ which is a protocol-relative URL - // Our fix collapses these to /evil.com/ to prevent external redirects - // This is already tested above, but we verify the collapsed path works + // %0d is kept encoded, so /%0d/test-path/ stays as-is and won't become //test-path/ await page.goto('/%0d/test-path/') await page.waitForLoadState('networkidle') @@ -80,8 +77,6 @@ test.describe('Open redirect prevention', () => { expect(page.url().startsWith(baseURL!)).toBe(true) const url = new URL(page.url()) expect(url.origin).toBe(new URL(baseURL!).origin) - // Path should be collapsed to /test-path (not //test-path/) - expect(url.pathname).toMatch(/^\/test-path\/?$/) }) }) diff --git a/e2e/react-router/rspack-basic-file-based/package.json b/e2e/react-router/rspack-basic-file-based/package.json index b19a27af6d..74659b51e1 100644 --- a/e2e/react-router/rspack-basic-file-based/package.json +++ b/e2e/react-router/rspack-basic-file-based/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tailwindcss/postcss": "^4.2.2", "@tanstack/router-e2e-utils": "workspace:^", diff --git a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/package.json b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/package.json index 5609c6cef0..a7b9eb58d4 100644 --- a/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/package.json +++ b/e2e/react-router/rspack-basic-virtual-named-export-config-file-based/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tailwindcss/postcss": "^4.2.2", "@tanstack/router-e2e-utils": "workspace:^", diff --git a/e2e/react-start/basic-cloudflare/package.json b/e2e/react-start/basic-cloudflare/package.json index 291dc8fcf2..4d90b4124e 100644 --- a/e2e/react-start/basic-cloudflare/package.json +++ b/e2e/react-start/basic-cloudflare/package.json @@ -9,7 +9,7 @@ "build": "vite build && tsc --noEmit", "preview": "vite preview", "cf-typegen": "wrangler types", - "postinstall": "npm run cf-typegen", + "postinstall": "WRANGLER_LOG=warn npm run --silent cf-typegen", "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" }, "dependencies": { diff --git a/e2e/react-start/basic/package.json b/e2e/react-start/basic/package.json index 4a6a92d1c5..37a3f4a0e2 100644 --- a/e2e/react-start/basic/package.json +++ b/e2e/react-start/basic/package.json @@ -12,6 +12,7 @@ "build:vite": "vite build && tsc --noEmit", "build:rsbuild": "rsbuild build && tsc --noEmit", "preview": "vite preview", + "preview:rsbuild": "rsbuild preview", "start": "node server.js", "test:e2e:startDummyServer": "node -e 'import(\"./tests/setup/global.setup.ts\").then(m => m.default())' & node -e 'import(\"./tests/setup/waitForDummyServer.ts\").then(m => m.default())'", "test:e2e:stopDummyServer": "node -e 'import(\"./tests/setup/global.teardown.ts\").then(m => m.default())'", @@ -31,7 +32,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/vite": "^4.2.2", @@ -86,6 +87,10 @@ { "toolchain": "rsbuild", "mode": "prerender" + }, + { + "toolchain": "rsbuild", + "mode": "preview" } ] } diff --git a/e2e/react-start/basic/playwright.config.ts b/e2e/react-start/basic/playwright.config.ts index 4d92fc032d..db07d18608 100644 --- a/e2e/react-start/basic/playwright.config.ts +++ b/e2e/react-start/basic/playwright.config.ts @@ -7,6 +7,7 @@ import { import packageJson from './package.json' with { type: 'json' } const mode = process.env.MODE ?? 'ssr' +const toolchain = process.env.E2E_TOOLCHAIN ?? 'vite' const e2ePortKey = process.env.E2E_PORT_KEY ?? packageJson.name const distDir = process.env.E2E_DIST_DIR ?? 'dist' @@ -24,9 +25,13 @@ const PORT = await getTestServerPort(e2ePortKey) const START_PORT = await getTestServerPort(`${e2ePortKey}_start`) const EXTERNAL_PORT = await getDummyServerPort(e2ePortKey) const baseURL = `http://localhost:${PORT}` +const previewCommand = + toolchain === 'rsbuild' + ? `pnpm preview:rsbuild --port ${PORT}` + : `pnpm preview --outDir ${distDir} --port ${PORT}` const commandByMode = mode === 'preview' - ? `pnpm run test:e2e:startDummyServer && pnpm preview --outDir ${distDir} --port ${PORT}` + ? `pnpm run test:e2e:startDummyServer && ${previewCommand}` : `pnpm run test:e2e:startDummyServer && pnpm start` /** * See https://playwright.dev/docs/test-configuration. @@ -50,6 +55,7 @@ export default defineConfig({ stdout: 'pipe', env: { MODE: mode, + E2E_TOOLCHAIN: toolchain, VITE_NODE_ENV: 'test', VITE_EXTERNAL_PORT: String(EXTERNAL_PORT), VITE_SERVER_PORT: String(PORT), diff --git a/e2e/react-start/basic/tests/open-redirect-prevention.spec.ts b/e2e/react-start/basic/tests/open-redirect-prevention.spec.ts index e497109889..bc88c33605 100644 --- a/e2e/react-start/basic/tests/open-redirect-prevention.spec.ts +++ b/e2e/react-start/basic/tests/open-redirect-prevention.spec.ts @@ -77,22 +77,14 @@ test.describe('Open redirect prevention', () => { page, baseURL, }) => { - // When control characters are stripped from paths like /%0d/evil.com/ - // the result could be //evil.com/ which is a protocol-relative URL - // Our fix collapses these to /evil.com/ to prevent external redirects - // This is already tested above, but we verify the collapsed path works - const res = await page.goto('/%0d/test-path/') + // %0d is kept encoded, so /%0d/test-path/ stays as-is and won't become //test-path/ + await page.goto('/%0d/test-path/') await page.waitForLoadState('networkidle') // Should stay on the same origin expect(page.url().startsWith(baseURL!)).toBe(true) const url = new URL(page.url()) expect(url.origin).toBe(new URL(baseURL!).origin) - // Path should be collapsed to /test-path (not //test-path/) - expect(url.pathname).toMatch(/^\/test-path\/?$/) - if (!isSpaMode) { - expect(res?.request().redirectedFrom()?.url()).not.toBe(undefined) - } }) }) diff --git a/e2e/react-start/basic/tests/special-characters.spec.ts b/e2e/react-start/basic/tests/special-characters.spec.ts index e88280e2e1..7cf50c39c8 100644 --- a/e2e/react-start/basic/tests/special-characters.spec.ts +++ b/e2e/react-start/basic/tests/special-characters.spec.ts @@ -64,6 +64,22 @@ test.describe('Unicode route rendering', () => { expect(param).toBe('대|') }) + + test('should not cause redirect loop for curly braces in path params', async ({ + page, + baseURL, + }) => { + // %7B/%7D (curly braces) must not trigger infinite 307 redirects + const res = await page.goto('/specialChars/%7B%7Bapp_name%7D%7D') + await page.waitForLoadState('load') + + // Should not redirect — URL stays encoded + expect(page.url()).toBe(`${baseURL}/specialChars/%7B%7Bapp_name%7D%7D`) + expect(res!.status()).toBe(200) + + const param = await page.getByTestId('special-param').textContent() + expect(param).toBe('{{app_name}}') + }) }) test.describe('Special characters in search params', () => { diff --git a/e2e/react-start/css-inline/package.json b/e2e/react-start/css-inline/package.json index e3e5d79678..a7bc5d023f 100644 --- a/e2e/react-start/css-inline/package.json +++ b/e2e/react-start/css-inline/package.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tanstack/router-e2e-utils": "workspace:^", "@types/node": "^22.10.2", diff --git a/e2e/react-start/custom-server-rsbuild/package.json b/e2e/react-start/custom-server-rsbuild/package.json index 255b07edc6..75634f79f5 100644 --- a/e2e/react-start/custom-server-rsbuild/package.json +++ b/e2e/react-start/custom-server-rsbuild/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tailwindcss/postcss": "^4.2.2", "@tanstack/router-e2e-utils": "workspace:^", diff --git a/e2e/react-start/custom-server-rsbuild/rsbuild.config.ts b/e2e/react-start/custom-server-rsbuild/rsbuild.config.ts index 5f9aa96e16..dec3a6d0fc 100644 --- a/e2e/react-start/custom-server-rsbuild/rsbuild.config.ts +++ b/e2e/react-start/custom-server-rsbuild/rsbuild.config.ts @@ -28,7 +28,7 @@ import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild' // explicit prefix. export default defineConfig({ plugins: [ - pluginReact(), + pluginReact({ splitChunks: false }), tanstackStart({ rsbuild: { installDevServerMiddleware: false, diff --git a/e2e/react-start/deferred-hydration/package.json b/e2e/react-start/deferred-hydration/package.json index 9560a44c41..b225528b49 100644 --- a/e2e/react-start/deferred-hydration/package.json +++ b/e2e/react-start/deferred-hydration/package.json @@ -26,7 +26,7 @@ "react-dom": "^19.0.0" }, "devDependencies": { - "@rsbuild/core": "^2.0.1", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tanstack/router-e2e-utils": "workspace:^", "@types/node": "^22.10.2", diff --git a/e2e/react-start/hmr/package.json b/e2e/react-start/hmr/package.json index 3854488170..b3a58125b9 100644 --- a/e2e/react-start/hmr/package.json +++ b/e2e/react-start/hmr/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/vite": "^4.2.2", diff --git a/e2e/react-start/import-protection/package.json b/e2e/react-start/import-protection/package.json index e4ad4a79ba..65ad70126b 100644 --- a/e2e/react-start/import-protection/package.json +++ b/e2e/react-start/import-protection/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tanstack/router-e2e-utils": "workspace:^", "@types/node": "^22.10.2", diff --git a/e2e/react-start/rsc/package.json b/e2e/react-start/rsc/package.json index 7a499078d3..36f822e660 100644 --- a/e2e/react-start/rsc/package.json +++ b/e2e/react-start/rsc/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tanstack/eslint-plugin-start": "workspace:^", "@tanstack/router-e2e-utils": "workspace:^", diff --git a/e2e/react-start/server-functions/package.json b/e2e/react-start/server-functions/package.json index 279883dbb4..5efdf6c42a 100644 --- a/e2e/react-start/server-functions/package.json +++ b/e2e/react-start/server-functions/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/vite": "^4.2.2", diff --git a/e2e/solid-router/rspack-basic-file-based/package.json b/e2e/solid-router/rspack-basic-file-based/package.json index f4b194f2f4..a92da0c18e 100644 --- a/e2e/solid-router/rspack-basic-file-based/package.json +++ b/e2e/solid-router/rspack-basic-file-based/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-babel": "^1.1.2", "@rsbuild/plugin-solid": "^2.0.0-beta.0", "@tailwindcss/postcss": "^4.2.2", diff --git a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.json b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.json index 9201b46192..ee4be17b99 100644 --- a/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.json +++ b/e2e/solid-router/rspack-basic-virtual-named-export-config-file-based/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-babel": "^1.1.2", "@rsbuild/plugin-solid": "^2.0.0-beta.0", "@tailwindcss/postcss": "^4.2.2", diff --git a/e2e/solid-start/basic-cloudflare/package.json b/e2e/solid-start/basic-cloudflare/package.json index c8f754b179..527c864a8f 100644 --- a/e2e/solid-start/basic-cloudflare/package.json +++ b/e2e/solid-start/basic-cloudflare/package.json @@ -9,7 +9,7 @@ "build": "vite build && tsc --noEmit", "preview": "vite preview", "cf-typegen": "wrangler types", - "postinstall": "npm run cf-typegen", + "postinstall": "WRANGLER_LOG=warn npm run --silent cf-typegen", "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" }, "dependencies": { diff --git a/e2e/solid-start/basic/package.json b/e2e/solid-start/basic/package.json index 817814e6e5..1b7a0b8d36 100644 --- a/e2e/solid-start/basic/package.json +++ b/e2e/solid-start/basic/package.json @@ -28,7 +28,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-babel": "^1.1.2", "@rsbuild/plugin-solid": "^2.0.0-beta.0", "@tailwindcss/postcss": "^4.2.2", diff --git a/e2e/solid-start/deferred-hydration/package.json b/e2e/solid-start/deferred-hydration/package.json index 773b2aecff..dfe71e4a46 100644 --- a/e2e/solid-start/deferred-hydration/package.json +++ b/e2e/solid-start/deferred-hydration/package.json @@ -24,7 +24,7 @@ "solid-js": "2.0.0-beta.15" }, "devDependencies": { - "@rsbuild/core": "^2.0.1", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-babel": "^1.1.2", "@rsbuild/plugin-solid": "^2.0.0-beta.0", "@tanstack/router-e2e-utils": "workspace:^", diff --git a/e2e/vue-router/rspack-basic-file-based/package.json b/e2e/vue-router/rspack-basic-file-based/package.json index 78b7e74a41..5d8a072d87 100644 --- a/e2e/vue-router/rspack-basic-file-based/package.json +++ b/e2e/vue-router/rspack-basic-file-based/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-babel": "^1.1.2", "@rsbuild/plugin-vue": "^1.2.7", "@rsbuild/plugin-vue-jsx": "^2.0.0", diff --git a/e2e/vue-router/rspack-basic-virtual-named-export-config-file-based/package.json b/e2e/vue-router/rspack-basic-virtual-named-export-config-file-based/package.json index 6a5cfead84..11f61237d3 100644 --- a/e2e/vue-router/rspack-basic-virtual-named-export-config-file-based/package.json +++ b/e2e/vue-router/rspack-basic-virtual-named-export-config-file-based/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-babel": "^1.1.2", "@rsbuild/plugin-vue": "^1.2.7", "@rsbuild/plugin-vue-jsx": "^2.0.0", diff --git a/e2e/vue-start/basic-cloudflare/package.json b/e2e/vue-start/basic-cloudflare/package.json index b3c4273718..c7e55f7937 100644 --- a/e2e/vue-start/basic-cloudflare/package.json +++ b/e2e/vue-start/basic-cloudflare/package.json @@ -9,7 +9,7 @@ "build": "vite build && tsc --noEmit", "preview": "vite preview", "cf-typegen": "wrangler types", - "postinstall": "npm run cf-typegen", + "postinstall": "WRANGLER_LOG=warn npm run --silent cf-typegen", "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" }, "dependencies": { diff --git a/e2e/vue-start/basic/package.json b/e2e/vue-start/basic/package.json index 2df94b1a20..073627701a 100644 --- a/e2e/vue-start/basic/package.json +++ b/e2e/vue-start/basic/package.json @@ -27,7 +27,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.1", - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-babel": "^1.0.5", "@rsbuild/plugin-vue": "^1.2.2", "@rsbuild/plugin-vue-jsx": "^1.1.1", diff --git a/examples/react/authenticated-routes-firebase/package.json b/examples/react/authenticated-routes-firebase/package.json index f9111e84f7..58c677ed26 100644 --- a/examples/react/authenticated-routes-firebase/package.json +++ b/examples/react/authenticated-routes-firebase/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "firebase": "^11.4.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/authenticated-routes/package.json b/examples/react/authenticated-routes/package.json index e1e1985dbc..5517800c01 100644 --- a/examples/react/authenticated-routes/package.json +++ b/examples/react/authenticated-routes/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/basic-default-search-params/package.json b/examples/react/basic-default-search-params/package.json index d157c36aa4..97bfd6d8d4 100644 --- a/examples/react/basic-default-search-params/package.json +++ b/examples/react/basic-default-search-params/package.json @@ -11,7 +11,7 @@ "dependencies": { "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/basic-devtools-panel/package.json b/examples/react/basic-devtools-panel/package.json index d6a71e6b56..9ff1c8fefb 100644 --- a/examples/react/basic-devtools-panel/package.json +++ b/examples/react/basic-devtools-panel/package.json @@ -11,7 +11,7 @@ "dependencies": { "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query-devtools": "^5.67.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/basic-file-based/package.json b/examples/react/basic-file-based/package.json index 2d2a65bd32..bca55ff7d5 100644 --- a/examples/react/basic-file-based/package.json +++ b/examples/react/basic-file-based/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/basic-non-nested-devtools/package.json b/examples/react/basic-non-nested-devtools/package.json index c663f35cab..fe6f2a04bd 100644 --- a/examples/react/basic-non-nested-devtools/package.json +++ b/examples/react/basic-non-nested-devtools/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/basic-react-query-file-based/package.json b/examples/react/basic-react-query-file-based/package.json index f5efe842dc..501a22dcc3 100644 --- a/examples/react/basic-react-query-file-based/package.json +++ b/examples/react/basic-react-query-file-based/package.json @@ -12,9 +12,9 @@ "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.90.0", "@tanstack/react-query-devtools": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/basic-react-query/package.json b/examples/react/basic-react-query/package.json index 95b23d62e7..b56851bdac 100644 --- a/examples/react/basic-react-query/package.json +++ b/examples/react/basic-react-query/package.json @@ -12,7 +12,7 @@ "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.90.0", "@tanstack/react-query-devtools": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/basic-ssr-file-based/package.json b/examples/react/basic-ssr-file-based/package.json index 3154f64c52..3fa61c784f 100644 --- a/examples/react/basic-ssr-file-based/package.json +++ b/examples/react/basic-ssr-file-based/package.json @@ -11,8 +11,8 @@ "debug": "node --inspect-brk server" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/react-router": "^1.170.17", + "@tanstack/router-plugin": "^1.168.19", "compression": "^1.8.0", "express": "^5.2.1", "get-port": "^7.1.0", diff --git a/examples/react/basic-ssr-streaming-file-based/package.json b/examples/react/basic-ssr-streaming-file-based/package.json index b02db5be81..b24243e276 100644 --- a/examples/react/basic-ssr-streaming-file-based/package.json +++ b/examples/react/basic-ssr-streaming-file-based/package.json @@ -11,8 +11,8 @@ "debug": "node --inspect-brk server" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/react-router": "^1.170.17", + "@tanstack/router-plugin": "^1.168.19", "compression": "^1.8.0", "express": "^5.2.1", "get-port": "^7.1.0", diff --git a/examples/react/basic-virtual-file-based/package.json b/examples/react/basic-virtual-file-based/package.json index bb69746201..922c460906 100644 --- a/examples/react/basic-virtual-file-based/package.json +++ b/examples/react/basic-virtual-file-based/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "@tanstack/virtual-file-routes": "^1.162.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/basic-virtual-inside-file-based/package.json b/examples/react/basic-virtual-inside-file-based/package.json index 284a3ee3cc..a24bf519d9 100644 --- a/examples/react/basic-virtual-inside-file-based/package.json +++ b/examples/react/basic-virtual-inside-file-based/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "@tanstack/virtual-file-routes": "^1.162.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json index 03787ad0e7..d245cc9f0e 100644 --- a/examples/react/basic/package.json +++ b/examples/react/basic/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/deferred-data/package.json b/examples/react/deferred-data/package.json index de4c82a3b9..b55fb41ccc 100644 --- a/examples/react/deferred-data/package.json +++ b/examples/react/deferred-data/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/i18n-paraglide/package.json b/examples/react/i18n-paraglide/package.json index a4477eb5a3..d9a9602c02 100644 --- a/examples/react/i18n-paraglide/package.json +++ b/examples/react/i18n-paraglide/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/react-router": "^1.170.17", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.1.1", "react-dom": "^19.1.1", "tailwindcss": "^4.2.2" diff --git a/examples/react/kitchen-sink-file-based/package.json b/examples/react/kitchen-sink-file-based/package.json index a89c35d8e9..2e18eb4358 100644 --- a/examples/react/kitchen-sink-file-based/package.json +++ b/examples/react/kitchen-sink-file-based/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "immer": "^10.1.1", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/kitchen-sink-react-query-file-based/package.json b/examples/react/kitchen-sink-react-query-file-based/package.json index 4f9e9f2255..f712e86a8f 100644 --- a/examples/react/kitchen-sink-react-query-file-based/package.json +++ b/examples/react/kitchen-sink-react-query-file-based/package.json @@ -12,9 +12,9 @@ "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.90.0", "@tanstack/react-query-devtools": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "immer": "^10.1.1", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/kitchen-sink-react-query/package.json b/examples/react/kitchen-sink-react-query/package.json index 8940db7373..3003408f70 100644 --- a/examples/react/kitchen-sink-react-query/package.json +++ b/examples/react/kitchen-sink-react-query/package.json @@ -12,7 +12,7 @@ "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.90.0", "@tanstack/react-query-devtools": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "immer": "^10.1.1", "react": "^19.0.0", diff --git a/examples/react/kitchen-sink/package.json b/examples/react/kitchen-sink/package.json index 4fe86d8b37..4414b457f2 100644 --- a/examples/react/kitchen-sink/package.json +++ b/examples/react/kitchen-sink/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "immer": "^10.1.1", "react": "^19.0.0", diff --git a/examples/react/large-file-based/package.json b/examples/react/large-file-based/package.json index 3dd028bb0b..16fd095515 100644 --- a/examples/react/large-file-based/package.json +++ b/examples/react/large-file-based/package.json @@ -13,9 +13,9 @@ "dependencies": { "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/location-masking/package.json b/examples/react/location-masking/package.json index 12450ee97a..76d00658a4 100644 --- a/examples/react/location-masking/package.json +++ b/examples/react/location-masking/package.json @@ -12,7 +12,7 @@ "@radix-ui/react-dialog": "^1.1.6", "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/navigation-blocking/package.json b/examples/react/navigation-blocking/package.json index 238028ff84..757ae9f2e6 100644 --- a/examples/react/navigation-blocking/package.json +++ b/examples/react/navigation-blocking/package.json @@ -11,7 +11,7 @@ "dependencies": { "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/quickstart-esbuild-file-based/package.json b/examples/react/quickstart-esbuild-file-based/package.json index afdee5a742..300650c666 100644 --- a/examples/react/quickstart-esbuild-file-based/package.json +++ b/examples/react/quickstart-esbuild-file-based/package.json @@ -9,9 +9,9 @@ "start": "dev" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/quickstart-file-based/package.json b/examples/react/quickstart-file-based/package.json index b083377a99..31a70cf36c 100644 --- a/examples/react/quickstart-file-based/package.json +++ b/examples/react/quickstart-file-based/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/quickstart-rspack-file-based/package.json b/examples/react/quickstart-rspack-file-based/package.json index c1fdf3a800..f3525defde 100644 --- a/examples/react/quickstart-rspack-file-based/package.json +++ b/examples/react/quickstart-rspack-file-based/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@tailwindcss/postcss": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "postcss": "^8.5.1", "react": "^19.0.0", @@ -17,9 +17,9 @@ "tailwindcss": "^4.2.2" }, "devDependencies": { - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-react": "^2.0.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "typescript": "^6.0.2" diff --git a/examples/react/quickstart-webpack-file-based/package.json b/examples/react/quickstart-webpack-file-based/package.json index 743215e62c..4c778eab28 100644 --- a/examples/react/quickstart-webpack-file-based/package.json +++ b/examples/react/quickstart-webpack-file-based/package.json @@ -7,14 +7,14 @@ "build": "webpack build && tsc --noEmit" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@swc/core": "^1.15.33", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "html-webpack-plugin": "^5.6.3", diff --git a/examples/react/quickstart/package.json b/examples/react/quickstart/package.json index 6e95bc5057..00269e8524 100644 --- a/examples/react/quickstart/package.json +++ b/examples/react/quickstart/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/router-monorepo-react-query/package.json b/examples/react/router-monorepo-react-query/package.json index b10e34bdaf..75532f61b1 100644 --- a/examples/react/router-monorepo-react-query/package.json +++ b/examples/react/router-monorepo-react-query/package.json @@ -12,9 +12,9 @@ "dependencies": { "@tanstack/react-query": "^5.90.0", "@tanstack/react-query-devtools": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1" diff --git a/examples/react/router-monorepo-react-query/packages/router/package.json b/examples/react/router-monorepo-react-query/packages/router/package.json index 622da2aed5..136535b761 100644 --- a/examples/react/router-monorepo-react-query/packages/router/package.json +++ b/examples/react/router-monorepo-react-query/packages/router/package.json @@ -11,8 +11,8 @@ "@router-mono-react-query/post-query": "workspace:*", "@tanstack/history": "^1.162.0", "@tanstack/react-query": "^5.90.0", - "@tanstack/react-router": "^1.170.15", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/react-router": "^1.170.17", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/router-monorepo-simple-lazy/package.json b/examples/react/router-monorepo-simple-lazy/package.json index 8e24568583..dd06440bf3 100644 --- a/examples/react/router-monorepo-simple-lazy/package.json +++ b/examples/react/router-monorepo-simple-lazy/package.json @@ -8,9 +8,9 @@ "dev": "pnpm router build && pnpm post-feature build && pnpm app dev" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1" diff --git a/examples/react/router-monorepo-simple-lazy/packages/router/package.json b/examples/react/router-monorepo-simple-lazy/packages/router/package.json index 3fd61be7b1..8b0b87a474 100644 --- a/examples/react/router-monorepo-simple-lazy/packages/router/package.json +++ b/examples/react/router-monorepo-simple-lazy/packages/router/package.json @@ -9,8 +9,8 @@ "types": "./dist/index.d.ts", "dependencies": { "@tanstack/history": "^1.162.0", - "@tanstack/react-router": "^1.170.15", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/react-router": "^1.170.17", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/router-monorepo-simple/package.json b/examples/react/router-monorepo-simple/package.json index f539e82a54..77cf9ecdf5 100644 --- a/examples/react/router-monorepo-simple/package.json +++ b/examples/react/router-monorepo-simple/package.json @@ -8,9 +8,9 @@ "dev": "pnpm router build && pnpm post-feature build && pnpm app dev" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1" diff --git a/examples/react/router-monorepo-simple/packages/router/package.json b/examples/react/router-monorepo-simple/packages/router/package.json index 8503c8935e..6bdb5ec943 100644 --- a/examples/react/router-monorepo-simple/packages/router/package.json +++ b/examples/react/router-monorepo-simple/packages/router/package.json @@ -9,8 +9,8 @@ "types": "./dist/index.d.ts", "dependencies": { "@tanstack/history": "^1.162.0", - "@tanstack/react-router": "^1.170.15", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/react-router": "^1.170.17", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/scroll-restoration/package.json b/examples/react/scroll-restoration/package.json index ca7dc300fb..7a503801e5 100644 --- a/examples/react/scroll-restoration/package.json +++ b/examples/react/scroll-restoration/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-virtual": "^3.13.0", "react": "^19.0.0", diff --git a/examples/react/search-validator-adapters/package.json b/examples/react/search-validator-adapters/package.json index 93faf3821f..234abba717 100644 --- a/examples/react/search-validator-adapters/package.json +++ b/examples/react/search-validator-adapters/package.json @@ -13,9 +13,9 @@ "@tailwindcss/vite": "^4.2.2", "@tanstack/arktype-adapter": "^1.167.0", "@tanstack/react-query": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "@tanstack/valibot-adapter": "^1.167.0", "@tanstack/zod-adapter": "^1.167.0", "arktype": "^2.1.7", diff --git a/examples/react/start-bare/package.json b/examples/react/start-bare/package.json index 216fbbb3cc..5a15546ef6 100644 --- a/examples/react/start-bare/package.json +++ b/examples/react/start-bare/package.json @@ -10,9 +10,9 @@ "start": "pnpx srvx --prod -s ../client dist/server/server.js" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0", "zod": "^4.4.3" diff --git a/examples/react/start-basic-auth/package.json b/examples/react/start-basic-auth/package.json index 62ad4b367f..abee47dc52 100644 --- a/examples/react/start-basic-auth/package.json +++ b/examples/react/start-basic-auth/package.json @@ -14,9 +14,9 @@ "@libsql/client": "^0.15.15", "@prisma/adapter-libsql": "^7.0.0", "@prisma/client": "^7.0.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/start-basic-authjs/package.json b/examples/react/start-basic-authjs/package.json index 07313c94a8..a28f1c313f 100644 --- a/examples/react/start-basic-authjs/package.json +++ b/examples/react/start-basic-authjs/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@auth/core": "^0.41.1", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0", "start-authjs": "^1.0.0", diff --git a/examples/react/start-basic-cloudflare/package.json b/examples/react/start-basic-cloudflare/package.json index 4c223171eb..f74602ab83 100644 --- a/examples/react/start-basic-cloudflare/package.json +++ b/examples/react/start-basic-cloudflare/package.json @@ -9,12 +9,12 @@ "preview": "vite preview", "deploy": "npm run build && wrangler deploy", "cf-typegen": "wrangler types", - "postinstall": "npm run cf-typegen" + "postinstall": "WRANGLER_LOG=warn npm run --silent cf-typegen" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/react/start-basic-react-query/package.json b/examples/react/start-basic-react-query/package.json index f227d3800b..40a18be142 100644 --- a/examples/react/start-basic-react-query/package.json +++ b/examples/react/start-basic-react-query/package.json @@ -12,10 +12,10 @@ "dependencies": { "@tanstack/react-query": "^5.90.0", "@tanstack/react-query-devtools": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-ssr-query": "^1.167.1", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/start-basic-static/package.json b/examples/react/start-basic-static/package.json index 9cd22f25ee..8ea4c54edc 100644 --- a/examples/react/start-basic-static/package.json +++ b/examples/react/start-basic-static/package.json @@ -10,10 +10,10 @@ "start": "pnpx srvx --prod -s ../client dist/server/server.js" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", - "@tanstack/start-static-server-functions": "^1.167.17", + "@tanstack/react-start": "^1.168.27", + "@tanstack/start-static-server-functions": "^1.167.18", "@vitejs/plugin-react": "^6.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/start-basic/package.json b/examples/react/start-basic/package.json index 08ef993e04..931df2a45d 100644 --- a/examples/react/start-basic/package.json +++ b/examples/react/start-basic/package.json @@ -10,9 +10,9 @@ "start": "node .output/server/index.mjs" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-merge": "^3.6.0", diff --git a/examples/react/start-bun/package.json b/examples/react/start-bun/package.json index e90822df7f..a4ab34310f 100644 --- a/examples/react/start-bun/package.json +++ b/examples/react/start-bun/package.json @@ -15,11 +15,11 @@ "dependencies": { "@tailwindcss/vite": "^4.2.2", "@tanstack/react-devtools": "^0.7.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-ssr-query": "^1.167.1", - "@tanstack/react-start": "^1.168.25", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/react-start": "^1.168.27", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.1.1", "react-dom": "^19.1.1", "tailwindcss": "^4.2.2" diff --git a/examples/react/start-clerk-basic/package.json b/examples/react/start-clerk-basic/package.json index 68ab9e66e4..7e15b08073 100644 --- a/examples/react/start-clerk-basic/package.json +++ b/examples/react/start-clerk-basic/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@clerk/tanstack-react-start": "^0.27.14", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "@vitejs/plugin-react": "^6.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/start-convex-trellaux/package.json b/examples/react/start-convex-trellaux/package.json index d00d67dbc3..f2ec73bfe8 100644 --- a/examples/react/start-convex-trellaux/package.json +++ b/examples/react/start-convex-trellaux/package.json @@ -15,10 +15,10 @@ "@convex-dev/react-query": "0.0.0-alpha.8", "@tanstack/react-query": "^5.90.0", "@tanstack/react-query-devtools": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-ssr-query": "^1.167.1", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "concurrently": "^8.2.2", "convex": "^1.19.0", "ky": "^1.7.4", diff --git a/examples/react/start-counter/package.json b/examples/react/start-counter/package.json index 738914667d..a7a8397b74 100644 --- a/examples/react/start-counter/package.json +++ b/examples/react/start-counter/package.json @@ -10,9 +10,9 @@ "start": "pnpx srvx --prod -s ../client dist/server/server.js" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/examples/react/start-i18n-paraglide/package.json b/examples/react/start-i18n-paraglide/package.json index bdc9193745..936f5e040d 100644 --- a/examples/react/start-i18n-paraglide/package.json +++ b/examples/react/start-i18n-paraglide/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@tanstack/react-devtools": "^0.7.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.1.1", "react-dom": "^19.1.1" }, diff --git a/examples/react/start-large/package.json b/examples/react/start-large/package.json index 65167cdf0d..d7300fc815 100644 --- a/examples/react/start-large/package.json +++ b/examples/react/start-large/package.json @@ -13,9 +13,9 @@ }, "dependencies": { "@tanstack/react-query": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/start-material-ui/package.json b/examples/react/start-material-ui/package.json index b9656721d2..a7623d10f0 100644 --- a/examples/react/start-material-ui/package.json +++ b/examples/react/start-material-ui/package.json @@ -15,9 +15,9 @@ "@emotion/styled": "11.14.0", "@fontsource-variable/roboto": "5.2.5", "@mui/material": "6.4.7", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0", "zod": "^4.4.3" diff --git a/examples/react/start-rscs/package.json b/examples/react/start-rscs/package.json index 58530b2ab1..980f43eff9 100644 --- a/examples/react/start-rscs/package.json +++ b/examples/react/start-rscs/package.json @@ -10,9 +10,9 @@ "start": "node .output/server/index.mjs" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "dexie": "^4.0.10", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/examples/react/start-streaming-data-from-server-functions/package.json b/examples/react/start-streaming-data-from-server-functions/package.json index edb78fee2f..e5d50b902f 100644 --- a/examples/react/start-streaming-data-from-server-functions/package.json +++ b/examples/react/start-streaming-data-from-server-functions/package.json @@ -10,9 +10,9 @@ "start": "pnpx srvx --prod -s ../client dist/server/server.js" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0", "zod": "^4.4.3" diff --git a/examples/react/start-supabase-basic/package.json b/examples/react/start-supabase-basic/package.json index 985b9d5adc..30083cbe1d 100644 --- a/examples/react/start-supabase-basic/package.json +++ b/examples/react/start-supabase-basic/package.json @@ -15,9 +15,9 @@ "dependencies": { "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.48.1", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1" diff --git a/examples/react/start-tailwind-v4/package.json b/examples/react/start-tailwind-v4/package.json index 8e0ad0920a..f8e537c9b4 100644 --- a/examples/react/start-tailwind-v4/package.json +++ b/examples/react/start-tailwind-v4/package.json @@ -10,9 +10,9 @@ "start": "node .output/server/index.mjs" }, "dependencies": { - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-merge": "^3.6.0", diff --git a/examples/react/start-trellaux/package.json b/examples/react/start-trellaux/package.json index f60d918813..29bf8bba30 100644 --- a/examples/react/start-trellaux/package.json +++ b/examples/react/start-trellaux/package.json @@ -12,10 +12,10 @@ "dependencies": { "@tanstack/react-query": "^5.90.0", "@tanstack/react-query-devtools": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-ssr-query": "^1.167.1", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "ky": "^1.7.4", "msw": "^2.7.0", "react": "^19.0.0", diff --git a/examples/react/start-workos/package.json b/examples/react/start-workos/package.json index bbd4d3f546..935c7a0a5f 100644 --- a/examples/react/start-workos/package.json +++ b/examples/react/start-workos/package.json @@ -14,9 +14,9 @@ "license": "MIT", "dependencies": { "@radix-ui/themes": "^3.3.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/react-start": "^1.168.25", + "@tanstack/react-start": "^1.168.27", "@workos/authkit-tanstack-react-start": "^0.5.0", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/examples/react/view-transitions/package.json b/examples/react/view-transitions/package.json index 866f048d6c..a5cc8bc839 100644 --- a/examples/react/view-transitions/package.json +++ b/examples/react/view-transitions/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "react": "^19.0.0", "react-dom": "^19.0.0", "redaxios": "^0.5.1", diff --git a/examples/react/with-framer-motion/package.json b/examples/react/with-framer-motion/package.json index 80b8fe37f5..c4d861b689 100644 --- a/examples/react/with-framer-motion/package.json +++ b/examples/react/with-framer-motion/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", "framer-motion": "^11.18.2", "react": "^19.0.0", diff --git a/examples/react/with-trpc-react-query/package.json b/examples/react/with-trpc-react-query/package.json index 3452b42c98..48e7eaa6cb 100644 --- a/examples/react/with-trpc-react-query/package.json +++ b/examples/react/with-trpc-react-query/package.json @@ -13,9 +13,9 @@ "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.90.0", "@tanstack/react-query-devtools": "^5.90.0", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "@trpc/client": "^11.4.3", "@trpc/server": "^11.4.3", "@trpc/tanstack-react-query": "^11.4.3", diff --git a/examples/react/with-trpc/package.json b/examples/react/with-trpc/package.json index ced1328bf5..3474e12a6b 100644 --- a/examples/react/with-trpc/package.json +++ b/examples/react/with-trpc/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-router": "^1.170.15", + "@tanstack/react-router": "^1.170.17", "@tanstack/react-router-devtools": "^1.167.0", - "@tanstack/router-plugin": "^1.168.18", + "@tanstack/router-plugin": "^1.168.19", "@trpc/client": "^11.4.3", "@trpc/server": "^11.4.3", "express": "^5.2.1", diff --git a/examples/solid/quickstart-rspack-file-based/package.json b/examples/solid/quickstart-rspack-file-based/package.json index 85c7dd9de4..f358741455 100644 --- a/examples/solid/quickstart-rspack-file-based/package.json +++ b/examples/solid/quickstart-rspack-file-based/package.json @@ -17,7 +17,7 @@ "tailwindcss": "^4.2.2" }, "devDependencies": { - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@rsbuild/plugin-babel": "^1.1.2", "@rsbuild/plugin-solid": "^2.0.0-beta.0", "@tanstack/router-plugin": "^1.168.18", diff --git a/examples/solid/start-convex-better-auth/convex/auth.ts b/examples/solid/start-convex-better-auth/convex/auth.ts index e9c2cdb499..7509818aac 100644 --- a/examples/solid/start-convex-better-auth/convex/auth.ts +++ b/examples/solid/start-convex-better-auth/convex/auth.ts @@ -4,6 +4,7 @@ import { betterAuth } from 'better-auth' import { components } from './_generated/api' import { query } from './_generated/server' import type { GenericCtx } from '@convex-dev/better-auth' +import type { BetterAuthOptions } from 'better-auth' import type { DataModel } from './_generated/dataModel' const siteUrl = process.env.SITE_URL! @@ -16,7 +17,7 @@ export const createAuth = ( ctx: GenericCtx, { optionsOnly } = { optionsOnly: false }, ) => { - return betterAuth({ + return betterAuth({ // disable logging when createAuth is called just to generate options. // this is not required, but there's a lot of noise in logs without it. logger: { diff --git a/examples/vue/basic-file-based-jsx/package.json b/examples/vue/basic-file-based-jsx/package.json index 36354edaec..7573628b17 100644 --- a/examples/vue/basic-file-based-jsx/package.json +++ b/examples/vue/basic-file-based-jsx/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/router-plugin": "^1.168.18", - "@tanstack/vue-router": "^1.170.15", + "@tanstack/router-plugin": "^1.168.19", + "@tanstack/vue-router": "^1.170.16", "@tanstack/vue-router-devtools": "^1.167.0", "redaxios": "^0.5.1", "tailwindcss": "^4.2.2", diff --git a/examples/vue/basic-file-based-sfc/package.json b/examples/vue/basic-file-based-sfc/package.json index 9a46ecc019..91581cc30d 100644 --- a/examples/vue/basic-file-based-sfc/package.json +++ b/examples/vue/basic-file-based-sfc/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", - "@tanstack/router-plugin": "^1.168.18", - "@tanstack/vue-router": "^1.170.15", + "@tanstack/router-plugin": "^1.168.19", + "@tanstack/vue-router": "^1.170.16", "@tanstack/vue-router-devtools": "^1.167.0", "redaxios": "^0.5.1", "tailwindcss": "^4.2.2", diff --git a/examples/vue/basic/package.json b/examples/vue/basic/package.json index bfbd3f00bb..046be6614d 100644 --- a/examples/vue/basic/package.json +++ b/examples/vue/basic/package.json @@ -9,7 +9,7 @@ "start": "vite" }, "dependencies": { - "@tanstack/vue-router": "^1.170.15", + "@tanstack/vue-router": "^1.170.16", "@tanstack/vue-router-devtools": "^1.167.0", "redaxios": "^0.5.1", "vue": "^3.5.13", diff --git a/package.json b/package.json index b8ba1f714a..cdc2bf2bc8 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "type": "git", "url": "git+https://github.com/TanStack/router.git" }, - "packageManager": "pnpm@11.1.0", + "packageManager": "pnpm@11.9.0", "engines": { - "pnpm": ">=11.0.0" + "pnpm": ">=11.9.0" }, "type": "module", "scripts": { @@ -29,6 +29,14 @@ "benchmark:bundle-size:analyze": "node scripts/benchmarks/bundle-size/analyze.mjs", "benchmark:client-nav": "nx run @benchmarks/client-nav:test:perf", "benchmark:ssr": "nx run @benchmarks/ssr:test:perf", + "benchmark:memory:client:flame": "nx run @benchmarks/memory-client:test:flame:react --parallel=1", + "benchmark:memory:client:flame:react": "nx run @benchmarks/memory-client:test:flame:react --parallel=1", + "benchmark:memory:client:flame:solid": "nx run @benchmarks/memory-client:test:flame:solid --parallel=1", + "benchmark:memory:client:flame:vue": "nx run @benchmarks/memory-client:test:flame:vue --parallel=1", + "benchmark:memory:server:flame": "nx run @benchmarks/memory-server:test:flame:react --parallel=1", + "benchmark:memory:server:flame:react": "nx run @benchmarks/memory-server:test:flame:react --parallel=1", + "benchmark:memory:server:flame:solid": "nx run @benchmarks/memory-server:test:flame:solid --parallel=1", + "benchmark:memory:server:flame:vue": "nx run @benchmarks/memory-server:test:flame:vue --parallel=1", "build": "nx affected --target=build --exclude=e2e/** --exclude=examples/**", "build:all": "nx run-many --target=build --exclude=examples/** --exclude=e2e/**", "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all", @@ -36,7 +44,7 @@ "format": "prettier --experimental-cli --ignore-unknown '**/*' --write", "changeset": "changeset", "changeset:publish": "changeset publish", - "changeset:version": "changeset version && node scripts/update-example-deps.mjs && pnpm install --no-frozen-lockfile && pnpm format", + "changeset:version": "changeset version && node scripts/update-example-deps.mjs && pnpm install --lockfile-only --ignore-scripts --no-frozen-lockfile && pnpm format", "gpt-generate": "node gpt/generate.js", "set-ts-version": "node scripts/set-ts-version.js", "labeler-generate": "node scripts/generate-labeler-config.ts", @@ -53,26 +61,26 @@ ] }, "devDependencies": { - "@arethetypeswrong/cli": "^0.18.2", + "@arethetypeswrong/cli": "^0.18.4", "@changesets/changelog-github": "^0.7.0", "@changesets/cli": "^2.30.0", "@eslint-react/eslint-plugin": "^1.26.2", "@nx/devkit": "22.7.5", - "@playwright/test": "^1.57.0", + "@playwright/test": "catalog:", "@swc-node/core": "^1.14.1", "@swc-node/register": "^1.11.1", "@tanstack/eslint-config": "0.4.0", - "@tanstack/query-core": "^5.99.0", - "@tanstack/react-query": "^5.99.0", - "@tanstack/solid-query": "^6.0.0-beta.5", + "@tanstack/query-core": "catalog:", + "@tanstack/react-query": "catalog:", + "@tanstack/solid-query": "catalog:", "@tanstack/vite-config": "0.5.2", - "@tanstack/vue-query": "^5.99.0", - "@types/node": "25.0.9", - "@types/react": "^19.2.8", - "@types/react-dom": "^19.2.3", + "@tanstack/vue-query": "catalog:", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", "@vitest/browser": "^4.1.4", "@vitest/ui": "^4.1.4", - "eslint": "^9.22.0", + "eslint": "catalog:", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-unused-imports": "^4.1.4", "jsdom": "^25.0.1", @@ -80,8 +88,8 @@ "nx": "22.7.5", "prettier": "^3.8.0", "publint": "^0.3.16", - "react": "^19.2.3", - "react-dom": "^19.2.3", + "react": "catalog:", + "react-dom": "catalog:", "redaxios": "^0.5.1", "rimraf": "^6.1.2", "tinyglobby": "^0.2.15", @@ -91,7 +99,7 @@ "typescript57": "npm:typescript@5.7", "typescript58": "npm:typescript@5.8", "typescript59": "npm:typescript@5.9", - "vite": "^8.0.14", + "vite": "catalog:", "vitest": "^4.1.4" } } diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index e2a8891a70..dc138ae4ed 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -1,5 +1,26 @@ # @tanstack/react-router +## 1.170.17 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b)]: + - @tanstack/router-core@1.171.14 + +## 1.170.16 + +### Patch Changes + +- [#7595](https://github.com/TanStack/router/pull/7595) [`52db703`](https://github.com/TanStack/router/commit/52db7034fd83b17cc45da9ba775ec253f2a9a149) - Perf improvement of useMatch and derived hooks when navigating away from previous match + +- [#7575](https://github.com/TanStack/router/pull/7575) [`4a93cff`](https://github.com/TanStack/router/commit/4a93cffffca34702e419bc47db827f964fd0cb65) - useMatches can skip useRef for structural sharing during SSR + +- [#7600](https://github.com/TanStack/router/pull/7600) [`f046b06`](https://github.com/TanStack/router/commit/f046b064d32222cad0b7ea061311ca9a474f5990) - Reduce Match rerenders during navigations (PR [#7596](https://github.com/TanStack/router/issues/7596)) + +- [#7577](https://github.com/TanStack/router/pull/7577) [`689d88e`](https://github.com/TanStack/router/commit/689d88e04c48ec76a18453ab8b82d8d58f2668b6) - Reduce bundle size by sharing structuralSharing selector logic + +- [#7627](https://github.com/TanStack/router/pull/7627) [`689c5ab`](https://github.com/TanStack/router/commit/689c5ab32e9bb18d90a5a94c33237b0e418df3e3) - Silence AbortError in renderRouterToStream, this is normal operation + ## 1.170.15 ### Patch Changes diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 59ebe688ee..2e63f8d973 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-router", - "version": "1.170.15", + "version": "1.170.17", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/react-start-client/CHANGELOG.md b/packages/react-start-client/CHANGELOG.md index 1019b96387..a3dd36024e 100644 --- a/packages/react-start-client/CHANGELOG.md +++ b/packages/react-start-client/CHANGELOG.md @@ -1,5 +1,21 @@ # @tanstack/react-start-client +## 1.168.15 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b), [`ba52d2b`](https://github.com/TanStack/router/commit/ba52d2b8f9e95d6becd1be14c020b35f3231e02c)]: + - @tanstack/router-core@1.171.14 + - @tanstack/start-client-core@1.170.13 + - @tanstack/react-router@1.170.17 + +## 1.168.14 + +### Patch Changes + +- Updated dependencies [[`52db703`](https://github.com/TanStack/router/commit/52db7034fd83b17cc45da9ba775ec253f2a9a149), [`4a93cff`](https://github.com/TanStack/router/commit/4a93cffffca34702e419bc47db827f964fd0cb65), [`f046b06`](https://github.com/TanStack/router/commit/f046b064d32222cad0b7ea061311ca9a474f5990), [`689d88e`](https://github.com/TanStack/router/commit/689d88e04c48ec76a18453ab8b82d8d58f2668b6), [`689c5ab`](https://github.com/TanStack/router/commit/689c5ab32e9bb18d90a5a94c33237b0e418df3e3)]: + - @tanstack/react-router@1.170.16 + ## 1.168.13 ### Patch Changes diff --git a/packages/react-start-client/package.json b/packages/react-start-client/package.json index b34c6f4033..634c565af9 100644 --- a/packages/react-start-client/package.json +++ b/packages/react-start-client/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-start-client", - "version": "1.168.13", + "version": "1.168.15", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/react-start-rsc/CHANGELOG.md b/packages/react-start-rsc/CHANGELOG.md index d64d4a1f8e..69627dbebb 100644 --- a/packages/react-start-rsc/CHANGELOG.md +++ b/packages/react-start-rsc/CHANGELOG.md @@ -1,5 +1,26 @@ # @tanstack/react-start-rsc +## 0.1.26 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b), [`ba52d2b`](https://github.com/TanStack/router/commit/ba52d2b8f9e95d6becd1be14c020b35f3231e02c)]: + - @tanstack/router-core@1.171.14 + - @tanstack/start-client-core@1.170.13 + - @tanstack/react-router@1.170.17 + - @tanstack/start-plugin-core@1.171.19 + - @tanstack/start-server-core@1.169.16 + - @tanstack/start-storage-context@1.167.16 + +## 0.1.25 + +### Patch Changes + +- Updated dependencies [[`52db703`](https://github.com/TanStack/router/commit/52db7034fd83b17cc45da9ba775ec253f2a9a149), [`4a93cff`](https://github.com/TanStack/router/commit/4a93cffffca34702e419bc47db827f964fd0cb65), [`96eca43`](https://github.com/TanStack/router/commit/96eca43ab8ee21917a8075521bef6dcf15fc7a43), [`f046b06`](https://github.com/TanStack/router/commit/f046b064d32222cad0b7ea061311ca9a474f5990), [`e499164`](https://github.com/TanStack/router/commit/e499164c722841271af4aa2215483476bdb94c94), [`689d88e`](https://github.com/TanStack/router/commit/689d88e04c48ec76a18453ab8b82d8d58f2668b6), [`689c5ab`](https://github.com/TanStack/router/commit/689c5ab32e9bb18d90a5a94c33237b0e418df3e3)]: + - @tanstack/react-router@1.170.16 + - @tanstack/start-server-core@1.169.15 + - @tanstack/start-plugin-core@1.171.18 + ## 0.1.24 ### Patch Changes diff --git a/packages/react-start-rsc/package.json b/packages/react-start-rsc/package.json index 1f7ce47a02..498a5aff80 100644 --- a/packages/react-start-rsc/package.json +++ b/packages/react-start-rsc/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-start-rsc", - "version": "0.1.24", + "version": "0.1.26", "description": "React Server Components support for TanStack Start", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/react-start-server/CHANGELOG.md b/packages/react-start-server/CHANGELOG.md index 2b3ecb3dc6..a56f3cc481 100644 --- a/packages/react-start-server/CHANGELOG.md +++ b/packages/react-start-server/CHANGELOG.md @@ -1,5 +1,22 @@ # @tanstack/react-start-server +## 1.167.21 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b)]: + - @tanstack/router-core@1.171.14 + - @tanstack/react-router@1.170.17 + - @tanstack/start-server-core@1.169.16 + +## 1.167.20 + +### Patch Changes + +- Updated dependencies [[`52db703`](https://github.com/TanStack/router/commit/52db7034fd83b17cc45da9ba775ec253f2a9a149), [`4a93cff`](https://github.com/TanStack/router/commit/4a93cffffca34702e419bc47db827f964fd0cb65), [`96eca43`](https://github.com/TanStack/router/commit/96eca43ab8ee21917a8075521bef6dcf15fc7a43), [`f046b06`](https://github.com/TanStack/router/commit/f046b064d32222cad0b7ea061311ca9a474f5990), [`689d88e`](https://github.com/TanStack/router/commit/689d88e04c48ec76a18453ab8b82d8d58f2668b6), [`689c5ab`](https://github.com/TanStack/router/commit/689c5ab32e9bb18d90a5a94c33237b0e418df3e3)]: + - @tanstack/react-router@1.170.16 + - @tanstack/start-server-core@1.169.15 + ## 1.167.19 ### Patch Changes diff --git a/packages/react-start-server/package.json b/packages/react-start-server/package.json index 1a10090d2f..62c02c7268 100644 --- a/packages/react-start-server/package.json +++ b/packages/react-start-server/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-start-server", - "version": "1.167.19", + "version": "1.167.21", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/react-start/CHANGELOG.md b/packages/react-start/CHANGELOG.md index a11c4f8699..90a0ad039f 100644 --- a/packages/react-start/CHANGELOG.md +++ b/packages/react-start/CHANGELOG.md @@ -1,5 +1,30 @@ # @tanstack/react-start +## 1.168.27 + +### Patch Changes + +- Updated dependencies [[`ba52d2b`](https://github.com/TanStack/router/commit/ba52d2b8f9e95d6becd1be14c020b35f3231e02c)]: + - @tanstack/start-client-core@1.170.13 + - @tanstack/react-router@1.170.17 + - @tanstack/react-start-client@1.168.15 + - @tanstack/react-start-rsc@0.1.26 + - @tanstack/react-start-server@1.167.21 + - @tanstack/start-plugin-core@1.171.19 + - @tanstack/start-server-core@1.169.16 + +## 1.168.26 + +### Patch Changes + +- Updated dependencies [[`52db703`](https://github.com/TanStack/router/commit/52db7034fd83b17cc45da9ba775ec253f2a9a149), [`4a93cff`](https://github.com/TanStack/router/commit/4a93cffffca34702e419bc47db827f964fd0cb65), [`96eca43`](https://github.com/TanStack/router/commit/96eca43ab8ee21917a8075521bef6dcf15fc7a43), [`f046b06`](https://github.com/TanStack/router/commit/f046b064d32222cad0b7ea061311ca9a474f5990), [`e499164`](https://github.com/TanStack/router/commit/e499164c722841271af4aa2215483476bdb94c94), [`689d88e`](https://github.com/TanStack/router/commit/689d88e04c48ec76a18453ab8b82d8d58f2668b6), [`689c5ab`](https://github.com/TanStack/router/commit/689c5ab32e9bb18d90a5a94c33237b0e418df3e3)]: + - @tanstack/react-router@1.170.16 + - @tanstack/start-server-core@1.169.15 + - @tanstack/start-plugin-core@1.171.18 + - @tanstack/react-start-client@1.168.14 + - @tanstack/react-start-rsc@0.1.25 + - @tanstack/react-start-server@1.167.20 + ## 1.168.25 ### Patch Changes diff --git a/packages/react-start/package.json b/packages/react-start/package.json index f19ff86212..c015ecfcf2 100644 --- a/packages/react-start/package.json +++ b/packages/react-start/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-start", - "version": "1.168.25", + "version": "1.168.27", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", @@ -188,7 +188,7 @@ } }, "devDependencies": { - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@types/node": ">=20" } } diff --git a/packages/router-cli/CHANGELOG.md b/packages/router-cli/CHANGELOG.md index c1f9188e12..7895121dbd 100644 --- a/packages/router-cli/CHANGELOG.md +++ b/packages/router-cli/CHANGELOG.md @@ -1,5 +1,12 @@ # @tanstack/router-cli +## 1.167.18 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/router-generator@1.167.18 + ## 1.167.17 ### Patch Changes diff --git a/packages/router-cli/package.json b/packages/router-cli/package.json index 971aa6c629..50aa6f8d4b 100644 --- a/packages/router-cli/package.json +++ b/packages/router-cli/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/router-cli", - "version": "1.167.17", + "version": "1.167.18", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/router-core/CHANGELOG.md b/packages/router-core/CHANGELOG.md index 45bd316edf..a0082099fa 100644 --- a/packages/router-core/CHANGELOG.md +++ b/packages/router-core/CHANGELOG.md @@ -1,5 +1,15 @@ # @tanstack/router-core +## 1.171.14 + +### Patch Changes + +- [#7695](https://github.com/TanStack/router/pull/7695) [`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b) - fix(router-core): re-encode URL-unsafe characters in `sanitizePathSegment` to prevent infinite redirect loops + + `sanitizePathSegment` now re-encodes characters in the WHATWG URL "path percent-encode set" (`<`, `>`, `"`, `` ` ``, `{`, `}`) and ASCII control characters back to their percent-encoded form, instead of stripping control characters. This prevents mismatches between the original URL and the router's internal representation that previously caused infinite 307 redirect loops on paths containing these characters (e.g. `/%7B%7Btemplate%7D%7D`). + + Fixes [#7587](https://github.com/TanStack/router/issues/7587). + ## 1.171.13 ### Patch Changes diff --git a/packages/router-core/package.json b/packages/router-core/package.json index a1ca664a70..303e7eca24 100644 --- a/packages/router-core/package.json +++ b/packages/router-core/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/router-core", - "version": "1.171.13", + "version": "1.171.14", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 9ff7b82b05..2197dab737 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -909,6 +909,18 @@ export const locationHistoryActions = new WeakMap< HistoryAction >() +type LightweightRouteMatchResult = { + matchedRoutes: ReadonlyArray + fullPath: string + search: Record + params: Record +} + +type LightweightRouteMatchCacheEntry = [ + lastMatchId: string | undefined, + result: LightweightRouteMatchResult, +] + export type CreateRouterFn = < TRouteTree extends AnyRoute, TTrailingSlashOption extends TrailingSlashOption = 'never', @@ -1002,6 +1014,10 @@ export class RouterCore< processedTree!: ProcessedTree resolvePathCache!: LRUCache private routeBranchCache = new WeakMap>() + private lightweightCache = new WeakMap< + ParsedLocation, + LightweightRouteMatchCacheEntry + >() isServer!: boolean pathParamsDecoder?: (encoded: string) => string protocolAllowlist!: Set @@ -1719,12 +1735,15 @@ export class RouterCore< * Only computes fullPath, accumulated search, and params - skipping expensive * operations like AbortController, ControlledPromise, loaderDeps, and full match objects. */ - private matchRoutesLightweight(location: ParsedLocation): { - matchedRoutes: ReadonlyArray - fullPath: string - search: Record - params: Record - } { + private matchRoutesLightweight( + location: ParsedLocation, + ): LightweightRouteMatchResult { + const lastStateMatchId = last(this.stores.matchesId.get()) + const cached = this.lightweightCache.get(location) + if (cached && cached[0] === lastStateMatchId) { + return cached[1] + } + const { matchedRoutes, routeParams } = this.getMatchedRoutes( location.pathname, ) @@ -1753,7 +1772,6 @@ export class RouterCore< } // Determine params: reuse from state if possible, otherwise parse - const lastStateMatchId = last(this.stores.matchesId.get()) const lastStateMatch = lastStateMatchId && this.stores.matchStores.get(lastStateMatchId)?.get() const canReuseParams = @@ -1780,12 +1798,14 @@ export class RouterCore< params = strictParams } - return { + const result = { matchedRoutes, fullPath: lastRoute.fullPath, search: accumulatedSearch, params, } + this.lightweightCache.set(location, [lastStateMatchId, result]) + return result } cancelMatch = (id: string) => { diff --git a/packages/router-core/src/utils.ts b/packages/router-core/src/utils.ts index f017215b5c..91011bfa7f 100644 --- a/packages/router-core/src/utils.ts +++ b/packages/router-core/src/utils.ts @@ -523,15 +523,26 @@ export function findLast( } /** - * Remove control characters that can cause open redirect vulnerabilities. - * Characters like \r (CR) and \n (LF) can trick URL parsers into interpreting - * paths like "/\r/evil.com" as "http://evil.com". + * Re-encode characters that are unsafe in URL paths. + * Includes ASCII control characters (0x00-0x1F, 0x7F) and a subset of the + * WHATWG URL "path percent-encode set" (", <, >, `, {, }). + * + * Space (0x20) is intentionally excluded — decodeURI decodes %20 to space + * and the router stores decoded spaces in location.pathname. The existing + * encodePathLikeUrl already handles re-encoding spaces for outgoing URLs. + * + * These characters are decoded by decodeURI but must remain percent-encoded + * in paths to match how upstream layers (CDNs, edge middleware, browsers) + * interpret the URL, preventing infinite redirect loops and path mismatches. */ +// eslint-disable-next-line no-control-regex +const PATH_UNSAFE_RE = /[\x00-\x1f\x7f"<>`{}]/g + function sanitizePathSegment(segment: string): string { - // Remove ASCII control characters (0x00-0x1F) and DEL (0x7F) - // These include CR (\r = 0x0D), LF (\n = 0x0A), and other potentially dangerous characters - // eslint-disable-next-line no-control-regex - return segment.replace(/[\x00-\x1f\x7f]/g, '') + return segment.replace( + PATH_UNSAFE_RE, + (ch) => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'), + ) } function decodeSegment(segment: string): string { @@ -644,8 +655,9 @@ export function decodePath(path: string) { result = result + decodeSegment(cursor ? path.slice(cursor) : path) // Prevent open redirect via protocol-relative URLs (e.g. "//evil.com") - // After sanitizing control characters, paths like "/\r/evil.com" become "//evil.com" - // Collapse leading double slashes to a single slash + // This is defense-in-depth: since control characters are no longer decoded, + // paths like "/%0d/evil.com" can no longer become "//evil.com". But we keep + // this check to guard against other edge cases. let handledProtocolRelativeURL = false if (result.startsWith('//')) { handledProtocolRelativeURL = true diff --git a/packages/router-core/tests/utils.test.ts b/packages/router-core/tests/utils.test.ts index 95874f230e..fff911df37 100644 --- a/packages/router-core/tests/utils.test.ts +++ b/packages/router-core/tests/utils.test.ts @@ -547,46 +547,45 @@ describe('decodePath', () => { }) describe('open redirect prevention', () => { - it('should strip CR (%0d) to prevent open redirect', () => { - // /%0d/google.com/ decodes to /\r/google.com/ which becomes //google.com/ - // This must be sanitized to prevent protocol-relative URL interpretation + it('should keep CR (%0d) encoded to prevent open redirect', () => { + // %0d stays encoded — no decoding, no stripping, no path mismatch + // Output is uppercase hex per RFC 3986 const result = decodePath('/%0d/google.com/') - expect(result.path).toBe('/google.com/') + expect(result.path).toBe('/%0D/google.com/') expect(result.path).not.toMatch(/^\/\//) - expect(result.handledProtocolRelativeURL).toBe(true) + expect(result.handledProtocolRelativeURL).toBe(false) }) - it('should strip LF (%0a) to prevent open redirect', () => { + it('should keep LF (%0a) encoded to prevent open redirect', () => { const result = decodePath('/%0a/evil.com/') - expect(result.path).toBe('/evil.com/') + expect(result.path).toBe('/%0A/evil.com/') expect(result.path).not.toMatch(/^\/\//) - expect(result.handledProtocolRelativeURL).toBe(true) + expect(result.handledProtocolRelativeURL).toBe(false) }) - it('should strip CRLF (%0d%0a) to prevent open redirect', () => { + it('should keep CRLF (%0d%0a) encoded to prevent open redirect', () => { const result = decodePath('/%0d%0a/evil.com/') - expect(result.path).toBe('/evil.com/') + expect(result.path).toBe('/%0D%0A/evil.com/') expect(result.path).not.toMatch(/^\/\//) - expect(result.handledProtocolRelativeURL).toBe(true) + expect(result.handledProtocolRelativeURL).toBe(false) }) - it('should strip multiple control characters to prevent open redirect', () => { + it('should keep multiple control characters encoded', () => { const result = decodePath('/%0d%0d%0d/evil.com/') - expect(result.path).toBe('/evil.com/') + expect(result.path).toBe('/%0D%0D%0D/evil.com/') expect(result.path).not.toMatch(/^\/\//) - expect(result.handledProtocolRelativeURL).toBe(true) + expect(result.handledProtocolRelativeURL).toBe(false) }) - it('should strip null bytes and other control characters', () => { + it('should keep null bytes encoded', () => { const result = decodePath('/%00/test/') - expect(result.path).toBe('/test/') - expect(result.handledProtocolRelativeURL).toBe(true) + expect(result.path).toBe('/%00/test/') + expect(result.handledProtocolRelativeURL).toBe(false) }) it('should collapse leading double slashes to prevent protocol-relative URLs', () => { - // After stripping control chars, ensure we don't end up with //evil.com - const result = decodePath('/%0d%0a/evil.com/path') - // Should resolve to localhost, not evil.com + // Direct // input should still be collapsed as defense-in-depth + const result = decodePath('//evil.com/path') const url = new URL(result.path, 'http://localhost:3000') expect(url.origin).toBe('http://localhost:3000') expect(result.handledProtocolRelativeURL).toBe(true) @@ -608,6 +607,34 @@ describe('decodePath', () => { expect(result.handledProtocolRelativeURL).toBe(true) }) }) + + describe('WHATWG path percent-encode set preserved', () => { + it('should keep curly braces encoded', () => { + const result = decodePath('/%7B%7Bapp_name%7D%7D/Makefile') + expect(result.path).toBe('/%7B%7Bapp_name%7D%7D/Makefile') + }) + + it('should keep angle brackets encoded', () => { + expect(decodePath('/%3Ctest%3E').path).toBe('/%3Ctest%3E') + }) + + it('should keep double quotes encoded', () => { + expect(decodePath('/foo%22bar').path).toBe('/foo%22bar') + }) + + it('should keep backticks encoded', () => { + expect(decodePath('/back%60tick').path).toBe('/back%60tick') + }) + + it('should decode space (handled by encodePathLikeUrl for outgoing URLs)', () => { + expect(decodePath('/file%20name').path).toBe('/file name') + }) + + it('should still decode safe characters', () => { + // Regular letters/unicode should still be decoded + expect(decodePath('/%D1%88%D0%B5%D0%BB%D0%BB%D1%8B').path).toBe('/шеллы') + }) + }) }) /** diff --git a/packages/router-generator/CHANGELOG.md b/packages/router-generator/CHANGELOG.md index 572760d02b..cf16585672 100644 --- a/packages/router-generator/CHANGELOG.md +++ b/packages/router-generator/CHANGELOG.md @@ -1,5 +1,12 @@ # @tanstack/router-generator +## 1.167.18 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b)]: + - @tanstack/router-core@1.171.14 + ## 1.167.17 ### Patch Changes diff --git a/packages/router-generator/package.json b/packages/router-generator/package.json index 466b250ab9..8abf6f6d0b 100644 --- a/packages/router-generator/package.json +++ b/packages/router-generator/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/router-generator", - "version": "1.167.17", + "version": "1.167.18", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/router-plugin/CHANGELOG.md b/packages/router-plugin/CHANGELOG.md index ef868d8194..50dca919fa 100644 --- a/packages/router-plugin/CHANGELOG.md +++ b/packages/router-plugin/CHANGELOG.md @@ -1,5 +1,14 @@ # @tanstack/router-plugin +## 1.168.19 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b)]: + - @tanstack/router-core@1.171.14 + - @tanstack/react-router@1.170.17 + - @tanstack/router-generator@1.167.18 + ## 1.168.18 ### Patch Changes diff --git a/packages/router-plugin/package.json b/packages/router-plugin/package.json index 6f9d0735cb..485eb70525 100644 --- a/packages/router-plugin/package.json +++ b/packages/router-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/router-plugin", - "version": "1.168.18", + "version": "1.168.19", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/router-vite-plugin/CHANGELOG.md b/packages/router-vite-plugin/CHANGELOG.md index 1540a5e991..dbd469a8d2 100644 --- a/packages/router-vite-plugin/CHANGELOG.md +++ b/packages/router-vite-plugin/CHANGELOG.md @@ -1,5 +1,12 @@ # @tanstack/router-vite-plugin +## 1.167.19 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/router-plugin@1.168.19 + ## 1.167.18 ### Patch Changes diff --git a/packages/router-vite-plugin/package.json b/packages/router-vite-plugin/package.json index bd3ef58ac6..aed06c1a13 100644 --- a/packages/router-vite-plugin/package.json +++ b/packages/router-vite-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/router-vite-plugin", - "version": "1.167.18", + "version": "1.167.19", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json index 328e4aeec3..d526266818 100644 --- a/packages/solid-start/package.json +++ b/packages/solid-start/package.json @@ -127,7 +127,7 @@ "pathe": "^2.0.3" }, "devDependencies": { - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@solidjs/web": "2.0.0-beta.15", "@tanstack/router-utils": "workspace:*", "@types/node": ">=20", diff --git a/packages/start-client-core/CHANGELOG.md b/packages/start-client-core/CHANGELOG.md index 7d5b73b16b..29696bad29 100644 --- a/packages/start-client-core/CHANGELOG.md +++ b/packages/start-client-core/CHANGELOG.md @@ -1,5 +1,15 @@ # @tanstack/start-client-core +## 1.170.13 + +### Patch Changes + +- [#7662](https://github.com/TanStack/router/pull/7662) [`ba52d2b`](https://github.com/TanStack/router/commit/ba52d2b8f9e95d6becd1be14c020b35f3231e02c) - perf: zero-copy frame payload extraction in the client frame decoder. When a frame's bytes are fully contained in the first buffered chunk (the common case), return a subarray view instead of allocating a new buffer and copying. This is on the hot path for decoding streamed server-function responses and `RawStream` payloads. + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b)]: + - @tanstack/router-core@1.171.14 + - @tanstack/start-storage-context@1.167.16 + ## 1.170.12 ### Patch Changes diff --git a/packages/start-client-core/package.json b/packages/start-client-core/package.json index edbff306fa..e999c97c80 100644 --- a/packages/start-client-core/package.json +++ b/packages/start-client-core/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/start-client-core", - "version": "1.170.12", + "version": "1.170.13", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/start-client-core/src/client-rpc/frame-decoder.ts b/packages/start-client-core/src/client-rpc/frame-decoder.ts index dbdd605bf2..9834b10a1b 100644 --- a/packages/start-client-core/src/client-rpc/frame-decoder.ts +++ b/packages/start-client-core/src/client-rpc/frame-decoder.ts @@ -201,6 +201,25 @@ export function createFrameDecoder( function extractFlattened(count: number): Uint8Array { if (count === 0) return EMPTY_BUFFER + // Fast path: the requested bytes are fully contained in the first buffered + // chunk (the common case — most frames arrive within a single network + // read). Return a subarray view instead of allocating a new buffer and + // copying `count` bytes. The view shares the chunk's backing ArrayBuffer, + // which is safe because buffered chunks are never mutated in place after + // being read from the network. + const first = bufferList[0] + if (first && first.length >= count) { + const result = first.subarray(0, count) + if (first.length === count) { + bufferList.shift() + } else { + bufferList[0] = first.subarray(count) + } + totalLength -= count + return result + } + + // Slow path: the requested bytes span multiple chunks — flatten by copying. const result = new Uint8Array(count) let offset = 0 let remaining = count diff --git a/packages/start-client-core/tests/frame-decoder.test.ts b/packages/start-client-core/tests/frame-decoder.test.ts index 29b8974dea..3a05611392 100644 --- a/packages/start-client-core/tests/frame-decoder.test.ts +++ b/packages/start-client-core/tests/frame-decoder.test.ts @@ -558,5 +558,51 @@ describe('frame-decoder', () => { const { done: finalDone } = await reader.read() expect(finalDone).toBe(true) }) + + it('should reassemble a chunk payload that spans many small reads', async () => { + // A large binary payload delivered in tiny network reads forces the + // multi-chunk (copy) path; the contiguous fast path must not change the + // reassembled bytes. + const payload = new Uint8Array(300) + for (let i = 0; i < payload.length; i++) payload[i] = i % 256 + + const jsonFrame = encodeJSONFrame('{"ref":11}') + const chunkFrame = encodeChunkFrame(11, payload) + const endFrame = encodeEndFrame(11) + + const combined = new Uint8Array( + jsonFrame.length + chunkFrame.length + endFrame.length, + ) + combined.set(jsonFrame, 0) + combined.set(chunkFrame, jsonFrame.length) + combined.set(endFrame, jsonFrame.length + chunkFrame.length) + + const input = new ReadableStream({ + start(controller) { + // 7-byte reads: smaller than the 9-byte header and the payload, so + // both header and payload span multiple buffered chunks. + for (let i = 0; i < combined.length; i += 7) { + controller.enqueue(combined.subarray(i, i + 7)) + } + controller.close() + }, + }) + + const { getOrCreateStream, jsonChunks } = createFrameDecoder(input) + const stream11 = getOrCreateStream(11) + + const jsonReader = jsonChunks.getReader() + const { value: jsonValue } = await jsonReader.read() + expect(jsonValue).toBe('{"ref":11}') + + const rawReader = stream11.getReader() + const received: Array = [] + while (true) { + const { done, value } = await rawReader.read() + if (done) break + if (value) received.push(...value) + } + expect(received).toEqual(Array.from(payload)) + }) }) }) diff --git a/packages/start-plugin-core/CHANGELOG.md b/packages/start-plugin-core/CHANGELOG.md index 5f78c2ceed..a25d8fc634 100644 --- a/packages/start-plugin-core/CHANGELOG.md +++ b/packages/start-plugin-core/CHANGELOG.md @@ -1,5 +1,25 @@ # @tanstack/start-plugin-core +## 1.171.19 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b)]: + - @tanstack/router-core@1.171.14 + - @tanstack/router-generator@1.167.18 + - @tanstack/router-plugin@1.168.19 + - @tanstack/start-server-core@1.169.16 + +## 1.171.18 + +### Patch Changes + +- [#7372](https://github.com/TanStack/router/pull/7372) [`e499164`](https://github.com/TanStack/router/commit/e499164c722841271af4aa2215483476bdb94c94) - Fix Rsbuild preview support for TanStack Start SSR. Preview always installs the SSR middleware; the `installDevServerMiddleware` option only applies to dev. + +- Updated dependencies [[`96eca43`](https://github.com/TanStack/router/commit/96eca43ab8ee21917a8075521bef6dcf15fc7a43)]: + - @tanstack/start-server-core@1.169.15 + - @tanstack/router-generator@1.167.17 + ## 1.171.17 ### Patch Changes diff --git a/packages/start-plugin-core/package.json b/packages/start-plugin-core/package.json index 8587b9dfca..1b60d95f05 100644 --- a/packages/start-plugin-core/package.json +++ b/packages/start-plugin-core/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/start-plugin-core", - "version": "1.171.17", + "version": "1.171.19", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", @@ -107,7 +107,7 @@ "zod": "^4.4.3" }, "devDependencies": { - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@types/babel__code-frame": "^7.0.6", "@types/babel__core": "^7.20.5", "@types/node": ">=20", diff --git a/packages/start-plugin-core/src/rsbuild/dev-server.ts b/packages/start-plugin-core/src/rsbuild/dev-server.ts deleted file mode 100644 index c83ec71448..0000000000 --- a/packages/start-plugin-core/src/rsbuild/dev-server.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { NodeRequest, sendNodeResponse } from 'srvx/node' -import { RSBUILD_ENVIRONMENT_NAMES } from './planning' -import type { IncomingMessage, ServerResponse } from 'node:http' -import type { RsbuildConfig } from '@rsbuild/core' - -type ServerSetupFn = Extract< - NonNullable['setup']>, - (...args: Array) => any -> -type SSRMiddleware = ( - req: IncomingMessage & { originalUrl?: string }, - res: ServerResponse, - next: () => void, -) => Promise - -/** - * Returns a `server.setup` function for rsbuild v2. - * - * Two middleware positions are used: - * - * 1. **Setup body** (BEFORE built-ins): Intercepts `/_serverFn/` URLs so - * they never reach rsbuild's htmlFallback/htmlCompletion middleware, - * which can swallow long base64 function IDs. - * - * 2. **Returned callback** (AFTER built-ins, BEFORE fallback): Handles - * all remaining SSR requests (page navigations). This position lets - * rsbuild's asset middleware serve compiled JS/CSS first. - * - * See rsbuild source: devMiddlewares.ts `applyDefaultMiddlewares()`. - */ -export function createServerSetup(opts: { - serverFnBasePath: string -}): ServerSetupFn { - return (context) => { - // Only install SSR middleware in dev mode - if (context.action !== 'dev') { - return () => {} - } - - const serverFnBase = opts.serverFnBasePath - - const handleSSR: SSRMiddleware = async (req, res, next) => { - const ssrEnv = - context.server.environments[RSBUILD_ENVIRONMENT_NAMES.server] - - if (!ssrEnv) { - console.error( - `[tanstack-start] SSR environment "${RSBUILD_ENVIRONMENT_NAMES.server}" not found`, - ) - return next() - } - - try { - const serverEntry = await ssrEnv.loadBundle<{ - default: { fetch: (req: Request) => Promise } - }>('index') - - // Restore the original URL (rsbuild may rewrite to /index.html) - if (req.originalUrl) { - req.url = req.originalUrl - } - - const webReq = new NodeRequest({ req, res }) - const webRes = await serverEntry.default.fetch(webReq) - return sendNodeResponse(res, webRes) - } catch (e) { - console.error('[tanstack-start] SSR error:', e) - - const webReq = new NodeRequest({ req, res }) - if (webReq.headers.get('content-type')?.includes('application/json')) { - return sendNodeResponse( - res, - new Response( - JSON.stringify( - { - status: 500, - error: 'Internal Server Error', - message: - 'An unexpected error occurred. Please try again later.', - timestamp: new Date().toISOString(), - }, - null, - 2, - ), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }, - ), - ) - } - - return sendNodeResponse( - res, - new Response( - ` - - Error - -

Internal Server Error

-
${e instanceof Error ? e.message : String(e)}
- -`, - { - status: 500, - headers: { 'Content-Type': 'text/html' }, - }, - ), - ) - } - } - - // Position 1: BEFORE built-ins — intercept server function calls - // early so they are not swallowed by htmlFallback or assetsMiddleware. - context.server.middlewares.use(async (req, res, next) => { - const url = req.url || '/' - if (url.startsWith(serverFnBase)) { - return handleSSR(req, res, next) - } - return next() - }) - - // Position 2: AFTER built-ins, before fallback — SSR catch-all for - // page navigations. Assets are already handled by rsbuild middleware. - return () => { - context.server.middlewares.use(handleSSR) - } - } -} diff --git a/packages/start-plugin-core/src/rsbuild/plugin.ts b/packages/start-plugin-core/src/rsbuild/plugin.ts index f9e48fe7a2..3de4ed8c2f 100644 --- a/packages/start-plugin-core/src/rsbuild/plugin.ts +++ b/packages/start-plugin-core/src/rsbuild/plugin.ts @@ -24,7 +24,7 @@ import { START_MANIFEST_PLACEHOLDER, registerVirtualModules, } from './virtual-modules' -import { createServerSetup } from './dev-server' +import { createServerSetup } from './server-middleware' import { registerClientBuildCapture } from './normalized-client-build' import { registerRouterPlugins } from './start-router-plugin' import { postBuildWithRsbuild } from './post-build' @@ -159,6 +159,7 @@ export function tanStackStartRsbuild( const resolvedEntryPlan = configContext.resolveEntries() const isDev = api.context.action === 'dev' + const isPreview = api.context.action === 'preview' const entryAliases = createRsbuildResolvedEntryAliases({ entryPaths: resolvedEntryPlan.entryPaths, @@ -244,6 +245,10 @@ export function tanStackStartRsbuild( }, }, server: { + ...(rsbuildConfig.server?.printUrls === undefined || + rsbuildConfig.server.printUrls === true + ? { printUrls: ({ urls }: { urls: Array }) => urls } + : {}), // Rsbuild compression currently treats Node's raw header array // writeHead form as an object, which corrupts SSR response headers. compress: false, @@ -252,11 +257,17 @@ export function tanStackStartRsbuild( htmlFallback: false, // server.setup returned callback runs after built-in middleware // but BEFORE fallback middleware — the ideal slot for SSR. - ...(isDev && - startPluginOpts.rsbuild?.installDevServerMiddleware !== false + // Preview always installs the middleware since it is the only SSR + // handler; dev can opt out when a custom server hosts SSR. + ...(isPreview || + (isDev && + startPluginOpts.rsbuild?.installDevServerMiddleware !== false) ? { setup: createServerSetup({ serverFnBasePath: serverFnBase, + serverOutputDirectory: + resolvedStartConfig.outputDirectories.server, + publicBase: resolvedStartConfig.basePaths.publicBase, }), } : {}), diff --git a/packages/start-plugin-core/src/rsbuild/server-middleware.ts b/packages/start-plugin-core/src/rsbuild/server-middleware.ts new file mode 100644 index 0000000000..70e2f234ce --- /dev/null +++ b/packages/start-plugin-core/src/rsbuild/server-middleware.ts @@ -0,0 +1,217 @@ +import { resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { NodeRequest, sendNodeResponse } from 'srvx/node' +import { joinURL } from 'ufo' +import { RSBUILD_ENVIRONMENT_NAMES } from './planning' +import type { IncomingMessage, ServerResponse } from 'node:http' +import type { RsbuildConfig } from '@rsbuild/core' + +type ServerSetupFn = Extract< + NonNullable['setup']>, + (...args: Array) => any +> +type ServerSetupContext = Parameters[0] +type SSRMiddleware = ( + req: IncomingMessage & { originalUrl?: string }, + res: ServerResponse, + next: (error?: unknown) => void, +) => Promise +type FetchHandler = (req: Request) => Promise | Response + +type ServerEntry = + | FetchHandler + | { + fetch?: FetchHandler + } + +function resolveFetchHandler(serverEntry: ServerEntry): FetchHandler { + if (typeof serverEntry === 'function') { + return serverEntry + } + + if (typeof serverEntry.fetch === 'function') { + return serverEntry.fetch.bind(serverEntry) + } + + throw new Error( + 'Unable to resolve a request handler from Rsbuild server bundle', + ) +} + +function getPublicBasePathname(publicBase: string): string { + try { + return new URL(publicBase, 'http://localhost').pathname + } catch { + return publicBase + } +} + +function restorePreviewUrl(opts: { + req: IncomingMessage & { originalUrl?: string } + publicBase: string +}) { + if (opts.req.originalUrl) { + opts.req.url = opts.req.originalUrl + } + + const publicBasePathname = getPublicBasePathname(opts.publicBase) + if (publicBasePathname === '/') { + return + } + + const url = opts.req.url ?? '/' + if (url.startsWith(publicBasePathname)) { + return + } + + opts.req.url = joinURL(publicBasePathname, url) +} + +async function loadDevFetchHandler( + context: ServerSetupContext, +): Promise { + if (context.action !== 'dev') { + throw new Error('Cannot load Rsbuild dev SSR bundle outside dev mode') + } + + const ssrEnv = context.server.environments[RSBUILD_ENVIRONMENT_NAMES.server] + + if (!ssrEnv) { + throw new Error( + `SSR environment "${RSBUILD_ENVIRONMENT_NAMES.server}" not found`, + ) + } + + const serverEntry = await ssrEnv.loadBundle<{ default: ServerEntry }>('index') + return resolveFetchHandler(serverEntry.default) +} + +/** + * Returns a `server.setup` function for rsbuild v2. + * + * Two middleware positions are used: + * + * 1. **Setup body** (BEFORE built-ins): Intercepts `/_serverFn/` URLs so + * they never reach rsbuild's htmlFallback/htmlCompletion middleware, + * which can swallow long base64 function IDs. + * + * 2. **Returned callback** (AFTER built-ins, BEFORE fallback): Handles + * all remaining SSR requests (page navigations). This position lets + * rsbuild's asset middleware serve compiled JS/CSS first. + * + * The middleware choreography is shared by dev and preview. The server entry + * loader differs: dev reads from Rsbuild's in-memory environment so rebuilds + * are reflected immediately, while preview lazy-imports the production server + * bundle from disk. + * + * See rsbuild source: devMiddlewares.ts `applyDefaultMiddlewares()` and + * previewServer.ts `startPreviewServer()`. + */ +export function createServerSetup(opts: { + serverFnBasePath: string + serverOutputDirectory: string + publicBase: string +}): ServerSetupFn { + let previewFetchHandlerPromise: Promise | undefined + + const getPreviewFetchHandler = () => { + if (!previewFetchHandlerPromise) { + previewFetchHandlerPromise = loadPreviewFetchHandler( + opts.serverOutputDirectory, + ) + } + + return previewFetchHandlerPromise + } + + return (context) => { + const serverFnBase = opts.serverFnBasePath + + const handleSSR: SSRMiddleware = async (req, res, next) => { + try { + const fetchHandler = + context.action === 'dev' + ? await loadDevFetchHandler(context) + : await getPreviewFetchHandler() + + if (context.action === 'preview') { + // Rsbuild preview's base middleware strips server.base before the + // returned setup callback runs. Put it back before creating the Web + // Request so Start's router sees the same URL shape as build/custom + // servers. The early server-fn middleware runs before that base + // middleware, so avoid prepending when the URL already has the base. + restorePreviewUrl({ req, publicBase: opts.publicBase }) + } else if (req.originalUrl) { + // Restore the original URL (rsbuild may rewrite to /index.html) + req.url = req.originalUrl + } + + const webReq = new NodeRequest({ req, res }) + const webRes = await fetchHandler(webReq) + return sendNodeResponse(res, webRes) + } catch (e) { + console.error('[tanstack-start] SSR error:', e) + + const webReq = new NodeRequest({ req, res }) + if (webReq.headers.get('content-type')?.includes('application/json')) { + return sendNodeResponse( + res, + new Response( + JSON.stringify( + { + status: 500, + error: 'Internal Server Error', + message: + 'An unexpected error occurred. Please try again later.', + timestamp: new Date().toISOString(), + }, + null, + 2, + ), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }, + ), + ) + } + + return next(e) + } + } + + // Position 1: BEFORE built-ins — intercept server function calls + // early so they are not swallowed by htmlFallback or assetsMiddleware. + context.server.middlewares.use(async (req, res, next) => { + const url = req.url || '/' + if (url.startsWith(serverFnBase)) { + return handleSSR(req, res, next) + } + return next() + }) + + // Position 2: AFTER built-ins, before fallback — SSR catch-all for + // page navigations. Assets are already handled by rsbuild middleware. + return () => { + context.server.middlewares.use(handleSSR) + } + } +} + +async function loadPreviewFetchHandler( + serverOutputDirectory: string, +): Promise { + const serverEntryPath = resolve(serverOutputDirectory, 'index.js') + const imported = (await import( + pathToFileURL(serverEntryPath).toString() + )) as { default: ServerEntry } + + try { + return resolveFetchHandler(imported.default) + } catch (error) { + throw new Error( + `Unable to resolve a request handler from Rsbuild server bundle at ${serverEntryPath}`, + { cause: error }, + ) + } +} diff --git a/packages/start-server-core/CHANGELOG.md b/packages/start-server-core/CHANGELOG.md index 5da4e58b2b..a07a1b30b7 100644 --- a/packages/start-server-core/CHANGELOG.md +++ b/packages/start-server-core/CHANGELOG.md @@ -1,5 +1,20 @@ # @tanstack/start-server-core +## 1.169.16 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b), [`ba52d2b`](https://github.com/TanStack/router/commit/ba52d2b8f9e95d6becd1be14c020b35f3231e02c)]: + - @tanstack/router-core@1.171.14 + - @tanstack/start-client-core@1.170.13 + - @tanstack/start-storage-context@1.167.16 + +## 1.169.15 + +### Patch Changes + +- [#7599](https://github.com/TanStack/router/pull/7599) [`96eca43`](https://github.com/TanStack/router/commit/96eca43ab8ee21917a8075521bef6dcf15fc7a43) - Skip a full router.update for faster createStartHandler + ## 1.169.14 ### Patch Changes diff --git a/packages/start-server-core/package.json b/packages/start-server-core/package.json index 906c25f2fe..1bbb1010a7 100644 --- a/packages/start-server-core/package.json +++ b/packages/start-server-core/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/start-server-core", - "version": "1.169.14", + "version": "1.169.16", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/start-static-server-functions/CHANGELOG.md b/packages/start-static-server-functions/CHANGELOG.md index b157c195d0..1191099aa4 100644 --- a/packages/start-static-server-functions/CHANGELOG.md +++ b/packages/start-static-server-functions/CHANGELOG.md @@ -1,5 +1,14 @@ # @tanstack/start-static-server-functions +## 1.167.18 + +### Patch Changes + +- Updated dependencies [[`ba52d2b`](https://github.com/TanStack/router/commit/ba52d2b8f9e95d6becd1be14c020b35f3231e02c)]: + - @tanstack/start-client-core@1.170.13 + - @tanstack/react-start@1.168.27 + - @tanstack/solid-start@1.168.27 + ## 1.167.17 ### Patch Changes diff --git a/packages/start-static-server-functions/package.json b/packages/start-static-server-functions/package.json index ffcd931ae9..61196bffdf 100644 --- a/packages/start-static-server-functions/package.json +++ b/packages/start-static-server-functions/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/start-static-server-functions", - "version": "1.167.17", + "version": "1.167.18", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/start-storage-context/CHANGELOG.md b/packages/start-storage-context/CHANGELOG.md index 4d5abfdf0d..0d28073b4f 100644 --- a/packages/start-storage-context/CHANGELOG.md +++ b/packages/start-storage-context/CHANGELOG.md @@ -1,5 +1,12 @@ # @tanstack/start-storage-context +## 1.167.16 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b)]: + - @tanstack/router-core@1.171.14 + ## 1.167.15 ### Patch Changes diff --git a/packages/start-storage-context/package.json b/packages/start-storage-context/package.json index 7d0e7e4334..0fa3359847 100644 --- a/packages/start-storage-context/package.json +++ b/packages/start-storage-context/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/start-storage-context", - "version": "1.167.15", + "version": "1.167.16", "description": "Modern and scalable routing for React applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/vue-router/CHANGELOG.md b/packages/vue-router/CHANGELOG.md index 339496707b..6ae02b6762 100644 --- a/packages/vue-router/CHANGELOG.md +++ b/packages/vue-router/CHANGELOG.md @@ -1,5 +1,12 @@ # @tanstack/vue-router +## 1.170.16 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b)]: + - @tanstack/router-core@1.171.14 + ## 1.170.15 ### Patch Changes diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index 9e429bb0b1..c302ba4600 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/vue-router", - "version": "1.170.15", + "version": "1.170.16", "description": "Modern and scalable routing for Vue applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/vue-router/src/ssr/renderRouterToStream.tsx b/packages/vue-router/src/ssr/renderRouterToStream.tsx index 3b122fda6b..efad4fc2eb 100644 --- a/packages/vue-router/src/ssr/renderRouterToStream.tsx +++ b/packages/vue-router/src/ssr/renderRouterToStream.tsx @@ -9,6 +9,11 @@ import { import type { AnyRouter } from '@tanstack/router-core' import type { Component } from 'vue' +const isAbortError = (request: Request, error: unknown) => + (request.signal.aborted && error === request.signal.reason) || + (error instanceof Error && error.name === 'AbortError') || + (error as any)?.code === 'ABORT_ERR' + function prependDoctype( readable: globalThis.ReadableStream, ): NodeReadableStream { @@ -126,25 +131,57 @@ export const renderRouterToStream = async ({ } } const abortVuePipe = (reason?: unknown) => { - if (writerDone) return + if (writerDone) { + return + } + writerDone = true void innerWriter .abort(reason) .catch(() => {}) .finally(releaseWriter) } + const handleWriterError = (err: unknown) => { + if (isAbortError(request, err)) { + return + } + + throw err + } + const handleWriteError = (err: unknown) => { + if (writerDone || isAbortError(request, err)) { + return + } + + throw err + } const vueWritable = new WritableStream({ write(chunk) { - return innerWriter.write(chunk) + if (writerDone) { + return + } + + return innerWriter.write(chunk).catch(handleWriteError) }, close() { + if (writerDone) { + return + } + writerDone = true - return innerWriter.close().finally(releaseWriter) + return innerWriter.close().catch(handleWriterError).finally(releaseWriter) }, abort(reason) { + if (writerDone) { + return + } + writerDone = true - return innerWriter.abort(reason).finally(releaseWriter) + return innerWriter + .abort(reason) + .catch(handleWriterError) + .finally(releaseWriter) }, }) diff --git a/packages/vue-router/tests/renderRouterToStream.test.tsx b/packages/vue-router/tests/renderRouterToStream.test.tsx index 781096185f..527bfa0e03 100644 --- a/packages/vue-router/tests/renderRouterToStream.test.tsx +++ b/packages/vue-router/tests/renderRouterToStream.test.tsx @@ -117,7 +117,7 @@ describe('renderRouterToStream - sync setup failures', () => { } }) - test('request abort aborts Vue writer and terminates response stream', async () => { + test('request abort drops later Vue writes and terminates response stream', async () => { let vueWriter: WritableStreamDefaultWriter | undefined rendererMocks.pipeToWebWritable.mockImplementationOnce( ( @@ -149,7 +149,7 @@ describe('renderRouterToStream - sync setup failures', () => { await expect( vueWriter!.write(new TextEncoder().encode('
')), - ).rejects.toBeTruthy() + ).resolves.toBeUndefined() const terminated = await Promise.race([ drainBody(response), diff --git a/packages/vue-start-client/CHANGELOG.md b/packages/vue-start-client/CHANGELOG.md index ec7755565f..54e1239b1f 100644 --- a/packages/vue-start-client/CHANGELOG.md +++ b/packages/vue-start-client/CHANGELOG.md @@ -1,5 +1,14 @@ # @tanstack/vue-start-client +## 1.167.18 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b), [`ba52d2b`](https://github.com/TanStack/router/commit/ba52d2b8f9e95d6becd1be14c020b35f3231e02c)]: + - @tanstack/router-core@1.171.14 + - @tanstack/start-client-core@1.170.13 + - @tanstack/vue-router@1.170.16 + ## 1.167.17 ### Patch Changes diff --git a/packages/vue-start-client/package.json b/packages/vue-start-client/package.json index f8e2bb2ec1..f191cb60fc 100644 --- a/packages/vue-start-client/package.json +++ b/packages/vue-start-client/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/vue-start-client", - "version": "1.167.17", + "version": "1.167.18", "description": "Modern and scalable routing for Vue applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/vue-start-server/CHANGELOG.md b/packages/vue-start-server/CHANGELOG.md index 1967ab903c..379dfdda9e 100644 --- a/packages/vue-start-server/CHANGELOG.md +++ b/packages/vue-start-server/CHANGELOG.md @@ -1,5 +1,21 @@ # @tanstack/vue-start-server +## 1.167.21 + +### Patch Changes + +- Updated dependencies [[`9809a06`](https://github.com/TanStack/router/commit/9809a0619d4ed3fe8c2a393af5b9eca4b6c7695b)]: + - @tanstack/router-core@1.171.14 + - @tanstack/start-server-core@1.169.16 + - @tanstack/vue-router@1.170.16 + +## 1.167.20 + +### Patch Changes + +- Updated dependencies [[`96eca43`](https://github.com/TanStack/router/commit/96eca43ab8ee21917a8075521bef6dcf15fc7a43)]: + - @tanstack/start-server-core@1.169.15 + ## 1.167.19 ### Patch Changes diff --git a/packages/vue-start-server/package.json b/packages/vue-start-server/package.json index ccc302a05b..bf704e93ea 100644 --- a/packages/vue-start-server/package.json +++ b/packages/vue-start-server/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/vue-start-server", - "version": "1.167.19", + "version": "1.167.21", "description": "Modern and scalable routing for Vue applications", "author": "Tanner Linsley", "license": "MIT", diff --git a/packages/vue-start/CHANGELOG.md b/packages/vue-start/CHANGELOG.md index a7eaf13cf9..1322cce2bf 100644 --- a/packages/vue-start/CHANGELOG.md +++ b/packages/vue-start/CHANGELOG.md @@ -1,5 +1,26 @@ # @tanstack/vue-start +## 1.168.26 + +### Patch Changes + +- Updated dependencies [[`ba52d2b`](https://github.com/TanStack/router/commit/ba52d2b8f9e95d6becd1be14c020b35f3231e02c)]: + - @tanstack/start-client-core@1.170.13 + - @tanstack/start-plugin-core@1.171.19 + - @tanstack/start-server-core@1.169.16 + - @tanstack/vue-router@1.170.16 + - @tanstack/vue-start-client@1.167.18 + - @tanstack/vue-start-server@1.167.21 + +## 1.168.25 + +### Patch Changes + +- Updated dependencies [[`96eca43`](https://github.com/TanStack/router/commit/96eca43ab8ee21917a8075521bef6dcf15fc7a43), [`e499164`](https://github.com/TanStack/router/commit/e499164c722841271af4aa2215483476bdb94c94)]: + - @tanstack/start-server-core@1.169.15 + - @tanstack/start-plugin-core@1.171.18 + - @tanstack/vue-start-server@1.167.20 + ## 1.168.24 ### Patch Changes diff --git a/packages/vue-start/package.json b/packages/vue-start/package.json index b4c35e32d5..78e746056f 100644 --- a/packages/vue-start/package.json +++ b/packages/vue-start/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/vue-start", - "version": "1.168.24", + "version": "1.168.26", "description": "Modern and scalable routing for Vue applications", "author": "Tanner Linsley", "license": "MIT", @@ -121,7 +121,7 @@ "pathe": "^2.0.3" }, "devDependencies": { - "@rsbuild/core": "^2.0.8", + "@rsbuild/core": "^2.0.11", "@tanstack/router-utils": "workspace:*", "@types/node": ">=20", "@vitejs/plugin-vue-jsx": "^4.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a657223340..ed8fe33b3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,8 +66,8 @@ importers: .: devDependencies: '@arethetypeswrong/cli': - specifier: ^0.18.2 - version: 0.18.2 + specifier: ^0.18.4 + version: 0.18.4 '@changesets/changelog-github': specifier: ^0.7.0 version: 0.7.0 @@ -79,7 +79,7 @@ importers: version: 1.26.2(eslint@9.22.0(jiti@2.7.0))(ts-api-utils@2.4.0(typescript@6.0.2))(typescript@6.0.2) '@nx/devkit': specifier: 22.7.5 - version: 22.7.5(nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))) + version: 22.7.5(nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))(debug@4.4.3)) '@playwright/test': specifier: ^1.57.0 version: 1.58.0 @@ -139,7 +139,7 @@ importers: version: 4.0.3 nx: specifier: 22.7.5 - version: 22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23)) + version: 22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))(debug@4.4.3) prettier: specifier: ^3.8.0 version: 3.8.1 @@ -223,11 +223,11 @@ importers: version: 3.5.25(typescript@6.0.2) devDependencies: '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tanstack/router-plugin': specifier: workspace:* version: link:../../packages/router-plugin @@ -288,10 +288,13 @@ importers: devDependencies: '@codspeed/vitest-plugin': specifier: ^5.5.0 - version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) + version: 5.5.0(debug@4.4.3)(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) '@platformatic/flame': specifier: ^1.6.0 version: 1.6.0 + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../../packages/router-plugin '@testing-library/react': specifier: ^16.2.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -318,7 +321,141 @@ importers: version: 3.0.0-next.5(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.15)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + + benchmarks/memory/client: + dependencies: + '@solidjs/web': + specifier: 2.0.0-beta.15 + version: 2.0.0-beta.15(solid-js@2.0.0-beta.15) + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/router-core': + specifier: workspace:* + version: link:../../../packages/router-core + '@tanstack/solid-router': + specifier: workspace:* + version: link:../../../packages/solid-router + '@tanstack/vue-router': + specifier: workspace:* + version: link:../../../packages/vue-router + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + solid-js: + specifier: 2.0.0-beta.15 + version: 2.0.0-beta.15 + vue: + specifier: ^3.5.16 + version: 3.5.25(typescript@6.0.2) + devDependencies: + '@codspeed/vitest-plugin': + specifier: ^5.5.0 + version: 5.5.0(debug@4.4.3)(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) + '@datadog/pprof': + specifier: ^5.13.2 + version: 5.13.2 + '@platformatic/flame': + specifier: ^1.6.0 + version: 1.6.0 + '@testing-library/react': + specifier: ^16.2.0 + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/jsdom': + specifier: 28.0.0 + version: 28.0.0 + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitejs/plugin-vue': + specifier: ^6.0.5 + version: 6.0.5(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vue@3.5.25(typescript@6.0.2)) + '@vitejs/plugin-vue-jsx': + specifier: ^5.1.5 + version: 5.1.5(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vue@3.5.25(typescript@6.0.2)) + jsdom: + specifier: 29.1.1 + version: 29.1.1(@noble/hashes@2.0.1) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + vite-plugin-solid: + specifier: ^3.0.0-next.5 + version: 3.0.0-next.5(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.15)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + vitest: + specifier: ^4.1.4 + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + + benchmarks/memory/server: + dependencies: + '@solidjs/web': + specifier: 2.0.0-beta.15 + version: 2.0.0-beta.15(solid-js@2.0.0-beta.15) + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/react-start': + specifier: workspace:* + version: link:../../../packages/react-start + '@tanstack/solid-router': + specifier: workspace:* + version: link:../../../packages/solid-router + '@tanstack/solid-start': + specifier: workspace:* + version: link:../../../packages/solid-start + '@tanstack/vue-router': + specifier: workspace:* + version: link:../../../packages/vue-router + '@tanstack/vue-start': + specifier: workspace:* + version: link:../../../packages/vue-start + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + solid-js: + specifier: 2.0.0-beta.15 + version: 2.0.0-beta.15 + vue: + specifier: ^3.5.16 + version: 3.5.25(typescript@6.0.2) + devDependencies: + '@codspeed/vitest-plugin': + specifier: ^5.5.0 + version: 5.5.0(debug@4.4.3)(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) + '@datadog/pprof': + specifier: ^5.13.2 + version: 5.13.2 + '@platformatic/flame': + specifier: ^1.6.0 + version: 1.6.0 + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitejs/plugin-vue-jsx': + specifier: ^5.1.5 + version: 5.1.5(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vue@3.5.25(typescript@6.0.2)) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + vite-plugin-solid: + specifier: ^3.0.0-next.5 + version: 3.0.0-next.5(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.15)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + vitest: + specifier: ^4.1.4 + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) benchmarks/ssr: dependencies: @@ -358,7 +495,7 @@ importers: devDependencies: '@codspeed/vitest-plugin': specifier: ^5.5.0 - version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) + version: 5.5.0(debug@4.4.3)(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) @@ -379,7 +516,7 @@ importers: version: 3.0.0-next.5(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.15)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) e2e/e2e-utils: devDependencies: @@ -424,7 +561,7 @@ importers: version: 5.9.2 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) e2e/react-router/basepath-file-based: dependencies: @@ -1126,11 +1263,11 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -1178,11 +1315,11 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -1401,11 +1538,11 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -1804,11 +1941,11 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tanstack/router-e2e-utils': specifier: workspace:^ version: link:../../e2e-utils @@ -1869,7 +2006,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) sass: specifier: ^1.97.2 version: 1.97.2 @@ -1975,11 +2112,11 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -2030,11 +2167,11 @@ importers: version: 19.2.3(react@19.2.3) devDependencies: '@rsbuild/core': - specifier: ^2.0.1 - version: 2.0.1 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.21)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tanstack/router-e2e-utils': specifier: workspace:^ version: link:../../e2e-utils @@ -2098,7 +2235,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) srvx: specifier: ^0.11.9 version: 0.11.12 @@ -2196,7 +2333,7 @@ importers: version: 9.2.1 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) typescript: specifier: ^6.0.2 version: 6.0.2 @@ -2223,11 +2360,11 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -2333,11 +2470,11 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tanstack/router-e2e-utils': specifier: workspace:^ version: link:../../e2e-utils @@ -2492,11 +2629,11 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tanstack/eslint-plugin-start': specifier: workspace:^ version: link:../../../packages/eslint-plugin-start @@ -2661,7 +2798,7 @@ importers: version: 2.0.8 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tanstack/router-e2e-utils': specifier: workspace:^ version: link:../../e2e-utils @@ -2889,11 +3026,11 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -3232,7 +3369,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) typescript: specifier: ^6.0.2 version: 6.0.2 @@ -3253,7 +3390,7 @@ importers: version: link:../../../packages/start-static-server-functions nitro: specifier: ^3.0.1-alpha.2 - version: 3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.5.1)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) react: specifier: ^19.2.3 version: 19.2.3 @@ -4026,14 +4163,14 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-babel': specifier: ^1.1.2 - version: 1.1.2(@rsbuild/core@2.0.8) + version: 1.1.2(@rsbuild/core@2.0.14) '@rsbuild/plugin-solid': specifier: ^2.0.0-beta.0 - version: 2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.8)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15) + version: 2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.14)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -4075,14 +4212,14 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-babel': specifier: ^1.1.2 - version: 1.1.2(@rsbuild/core@2.0.8) + version: 1.1.2(@rsbuild/core@2.0.14) '@rsbuild/plugin-solid': specifier: ^2.0.0-beta.0 - version: 2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.8)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15) + version: 2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.14)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -4280,14 +4417,14 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-babel': specifier: ^1.1.2 - version: 1.1.2(@rsbuild/core@2.0.8) + version: 1.1.2(@rsbuild/core@2.0.14) '@rsbuild/plugin-solid': specifier: ^2.0.0-beta.0 - version: 2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.8)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15) + version: 2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.14)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -4699,14 +4836,14 @@ importers: version: 2.0.0-beta.15 devDependencies: '@rsbuild/core': - specifier: ^2.0.1 - version: 2.0.1 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-babel': specifier: ^1.1.2 - version: 1.1.2(@rsbuild/core@2.0.1) + version: 1.1.2(@rsbuild/core@2.0.14) '@rsbuild/plugin-solid': specifier: ^2.0.0-beta.0 - version: 2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.1)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15) + version: 2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.14)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15) '@tanstack/router-e2e-utils': specifier: workspace:^ version: link:../../e2e-utils @@ -5944,17 +6081,17 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-babel': specifier: ^1.1.2 - version: 1.1.2(@rsbuild/core@2.0.8) + version: 1.1.2(@rsbuild/core@2.0.14) '@rsbuild/plugin-vue': specifier: ^1.2.7 - version: 1.2.7(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23))(vue@3.5.25(typescript@6.0.2)) + version: 1.2.7(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23))(vue@3.5.25(typescript@6.0.2)) '@rsbuild/plugin-vue-jsx': specifier: ^2.0.0 - version: 2.0.0(@babel/core@7.29.0)(@rsbuild/core@2.0.8) + version: 2.0.0(@babel/core@7.29.0)(@rsbuild/core@2.0.14) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -5999,17 +6136,17 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-babel': specifier: ^1.1.2 - version: 1.1.2(@rsbuild/core@2.0.8) + version: 1.1.2(@rsbuild/core@2.0.14) '@rsbuild/plugin-vue': specifier: ^1.2.7 - version: 1.2.7(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23))(vue@3.5.25(typescript@6.0.2)) + version: 1.2.7(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23))(vue@3.5.25(typescript@6.0.2)) '@rsbuild/plugin-vue-jsx': specifier: ^2.0.0 - version: 2.0.0(@babel/core@7.29.0)(@rsbuild/core@2.0.8) + version: 2.0.0(@babel/core@7.29.0)(@rsbuild/core@2.0.14) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -6222,17 +6359,17 @@ importers: specifier: ^1.57.0 version: 1.58.0 '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-babel': specifier: ^1.0.5 - version: 1.0.6(@rsbuild/core@2.0.8) + version: 1.0.6(@rsbuild/core@2.0.14) '@rsbuild/plugin-vue': specifier: ^1.2.2 - version: 1.2.2(@rsbuild/core@2.0.8)(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.27.4)(vue@3.5.25(typescript@6.0.2)) + version: 1.2.2(@rsbuild/core@2.0.14)(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.27.4)(vue@3.5.25(typescript@6.0.2)) '@rsbuild/plugin-vue-jsx': specifier: ^1.1.1 - version: 1.1.1(@babel/core@7.29.0)(@rsbuild/core@2.0.8) + version: 1.1.1(@babel/core@7.29.0)(@rsbuild/core@2.0.14) '@tailwindcss/postcss': specifier: ^4.2.2 version: 4.2.2 @@ -8295,11 +8432,11 @@ importers: version: 4.2.2 devDependencies: '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23)) '@tanstack/router-plugin': specifier: workspace:* version: link:../../../packages/router-plugin @@ -9076,7 +9213,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -9818,7 +9955,7 @@ importers: version: 0.5.20(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: npm:nitro-nightly@latest - version: nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: nitro-nightly@3.0.1-20260702-173353-03122331(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(wrangler@4.75.0) tailwindcss: specifier: ^4.1.18 version: 4.2.2 @@ -11346,14 +11483,14 @@ importers: version: 4.2.2 devDependencies: '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@rsbuild/plugin-babel': specifier: ^1.1.2 - version: 1.1.2(@rsbuild/core@2.0.8) + version: 1.1.2(@rsbuild/core@2.0.14) '@rsbuild/plugin-solid': specifier: ^2.0.0-beta.0 - version: 2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.8)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15) + version: 2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.14)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15) '@tanstack/router-plugin': specifier: workspace:* version: link:../../../packages/router-plugin @@ -11666,7 +11803,7 @@ importers: version: 25.0.9 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -11908,7 +12045,7 @@ importers: version: 25.0.9 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -12103,7 +12240,7 @@ importers: dependencies: '@convex-dev/better-auth': specifier: ^0.9.7 - version: 0.9.7(@standard-schema/spec@1.1.0)(better-auth@1.3.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@2.0.0-beta.15)(vue@3.5.25(typescript@6.0.2)))(convex@1.28.2(@clerk/clerk-react@5.59.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3))(hono@4.7.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + version: 0.9.7(@standard-schema/spec@1.1.0)(better-auth@1.6.19(@prisma/client@7.0.0(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(typescript@6.0.2))(@tanstack/solid-start@packages+solid-start)(mysql2@3.15.3)(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@2.0.0-beta.15)(vitest@4.1.4)(vue@3.5.25(typescript@6.0.2)))(convex@1.28.2(@clerk/clerk-react@5.59.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3))(hono@4.7.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) '@solidjs/web': specifier: 2.0.0-beta.15 version: 2.0.0-beta.15(solid-js@2.0.0-beta.15) @@ -12121,7 +12258,7 @@ importers: version: link:../../../packages/solid-start better-auth: specifier: ^1.3.27 - version: 1.3.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@2.0.0-beta.15)(vue@3.5.25(typescript@6.0.2)) + version: 1.6.19(@prisma/client@7.0.0(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(typescript@6.0.2))(@tanstack/solid-start@packages+solid-start)(mysql2@3.15.3)(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@2.0.0-beta.15)(vitest@4.1.4)(vue@3.5.25(typescript@6.0.2)) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -12919,8 +13056,8 @@ importers: version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) devDependencies: '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@types/node': specifier: 25.0.9 version: 25.0.9 @@ -13423,8 +13560,8 @@ importers: version: 2.0.3 devDependencies: '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@solidjs/web': specifier: 2.0.0-beta.15 version: 2.0.0-beta.15(solid-js@2.0.0-beta.15) @@ -13592,8 +13729,8 @@ importers: version: 4.4.3 devDependencies: '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@types/babel__code-frame': specifier: ^7.0.6 version: 7.0.6 @@ -13629,7 +13766,7 @@ importers: version: 0.1.7 h3-v2: specifier: npm:h3@2.0.1-rc.20 - version: h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)) + version: h3@2.0.1-rc.20(crossws@0.4.9(srvx@0.11.20)) seroval: specifier: ^1.5.4 version: 1.5.4 @@ -13830,8 +13967,8 @@ importers: version: 2.0.3 devDependencies: '@rsbuild/core': - specifier: ^2.0.8 - version: 2.0.8 + specifier: ^2.0.11 + version: 2.0.14 '@tanstack/router-utils': specifier: workspace:* version: link:../router-utils @@ -13931,13 +14068,13 @@ packages: '@andrewbranch/untar.js@1.0.3': resolution: {integrity: sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==} - '@arethetypeswrong/cli@0.18.2': - resolution: {integrity: sha512-PcFM20JNlevEDKBg4Re29Rtv2xvjvQZzg7ENnrWFSS0PHgdP2njibVFw+dRUhNkPgNfac9iUqO0ohAXqQL4hbw==} + '@arethetypeswrong/cli@0.18.4': + resolution: {integrity: sha512-kNWo6LTzGAuLYPpJ7Sgo63whSUeeSuKMlYx6IBgzs4ONEG807gW4hSSENvpeCHzO2H2wIzG5EFl0OKBbqGBAyA==} engines: {node: '>=20'} hasBin: true - '@arethetypeswrong/core@0.18.2': - resolution: {integrity: sha512-GiwTmBFOU1/+UVNqqCGzFJYfBXEytUkiI+iRZ6Qx7KmUVtLm00sYySkfe203C9QtPG11yOz1ZaMek8dT/xnlgg==} + '@arethetypeswrong/core@0.18.4': + resolution: {integrity: sha512-M5F0ePyN6h2Z6XxRiyIPqjGbltotXLjR0CKA0uKspsDu0QmgTNYvRb4RSQPMUs2ZXZHCCYpbaZbFbYOXLxCjUA==} engines: {node: '>=20'} '@ark/schema@0.44.2': @@ -13952,9 +14089,21 @@ packages: '@asamuzakjp/css-color@4.0.5': resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + '@asamuzakjp/dom-selector@6.5.6': resolution: {integrity: sha512-Mj3Hu9ymlsERd7WOsUKNUZnJYL4IZ/I9wVVYgtvOsWYiEFbkQ4G7VRIh2USxTVW4BBDIsLG+gBUgqOqf2Kvqow==} + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} @@ -14277,14 +14426,84 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@better-auth/core@1.3.27': - resolution: {integrity: sha512-3Sfdax6MQyronY+znx7bOsfQHI6m1SThvJWb0RDscFEAhfqLy95k1sl+/PgGyg0cwc2cUXoEiAOSqYdFYrg3vA==} + '@better-auth/core@1.6.19': + resolution: {integrity: sha512-ddE3Y9MoQ8t32QSO5Y8mV7pmnDAv5LdJjX1SPWUH6JUUmuOc7YEy3B5JfXZIJbRaXFdnAitN7pHPVa5u/dYAZA==} + peerDependencies: + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + '@cloudflare/workers-types': '>=4' + '@opentelemetry/api': ^1.9.0 + better-call: 1.3.6 + jose: ^6.1.0 + kysely: ^0.28.5 || ^0.29.0 + nanostores: ^1.0.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + '@opentelemetry/api': + optional: true + + '@better-auth/drizzle-adapter@1.6.19': + resolution: {integrity: sha512-57C9ePorPmIEez6dHuQMz3hCTkYim0lfVRIoRtX7PiVfiRFB2bjXseQwrCJfQmkgMFlkp1s/c9nKgAjc2EvAIg==} + peerDependencies: + '@better-auth/core': ^1.6.19 + '@better-auth/utils': 0.4.2 + drizzle-orm: ^0.45.2 + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.6.19': + resolution: {integrity: sha512-DlmvllEd0nv8JL+plX3JB3WTmqDFnGFOmjmIiUDHo8R3PTAvC0ZaJq3Jk+LQLN5PyVQSUzXZKtvTQYaqRHzBaw==} + peerDependencies: + '@better-auth/core': ^1.6.19 + '@better-auth/utils': 0.4.2 + kysely: ^0.28.17 || ^0.29.0 + peerDependenciesMeta: + kysely: + optional: true + + '@better-auth/memory-adapter@1.6.19': + resolution: {integrity: sha512-cZ8iLRG/T8Oi/CqE9FTHj3z8pIOqRsINi50trWxPNwyY/Eyb7YCljrBi0PuqgIdyVs7BWfrrtEYTpO4ddfuwEw==} + peerDependencies: + '@better-auth/core': ^1.6.19 + '@better-auth/utils': 0.4.2 + + '@better-auth/mongo-adapter@1.6.19': + resolution: {integrity: sha512-8AReXqhMGiGQIPEpGbmAhh+R4g70TsAVvzwdd6Aj4q+LTSwd3tqC89TFJ4eX8KSplxm9PFBZ6g6gsRmDd7urQg==} + peerDependencies: + '@better-auth/core': ^1.6.19 + '@better-auth/utils': 0.4.2 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true + + '@better-auth/prisma-adapter@1.6.19': + resolution: {integrity: sha512-pXZBhR7/bzJb48IUHlGMyz9SM9h1OCO5GIIuHEllJYt8MKgrjtsnXfUkwZh6pAUEIp3WxBEYMMK96bfkqHiWEg==} + peerDependencies: + '@better-auth/core': ^1.6.19 + '@better-auth/utils': 0.4.2 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@prisma/client': + optional: true + prisma: + optional: true + + '@better-auth/telemetry@1.6.19': + resolution: {integrity: sha512-bBaB6SMIsrD3WutDdm5YQ1bQyinANTimHD8RtpLWhNh/jIXvzgwVVCrDFqA256vcGC/ZRCydmtW8ZrUqNMW9Og==} + peerDependencies: + '@better-auth/core': ^1.6.19 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 - '@better-auth/utils@0.3.0': - resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} + '@better-auth/utils@0.4.2': + resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} - '@better-fetch/fetch@1.1.18': - resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + '@better-fetch/fetch@1.3.1': + resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} '@blazediff/core@1.9.1': resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} @@ -14292,6 +14511,10 @@ packages: '@braidai/lang@1.1.2': resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@bundled-es-modules/cookie@2.0.1': resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} @@ -14543,6 +14766,10 @@ packages: resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + '@csstools/css-calc@2.1.1': resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==} engines: {node: '>=18'} @@ -14557,6 +14784,13 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-color-parser@3.0.7': resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==} engines: {node: '>=18'} @@ -14571,6 +14805,13 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-color-parser@4.1.3': + resolution: {integrity: sha512-DOgvIPkikIOixQRlD4YF31VN6fLLUTdrzhfRbis8vm0kMTgIbEPX0Ip/YX9fOeV9iywAS4sUUbTclpan7yYP8Q==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms@3.0.4': resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} engines: {node: '>=18'} @@ -14583,12 +14824,26 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-syntax-patches-for-csstree@1.0.14': resolution: {integrity: sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 + '@csstools/css-syntax-patches-for-csstree@1.1.5': + resolution: {integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + '@csstools/css-tokenizer@3.0.3': resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} engines: {node: '>=18'} @@ -14597,6 +14852,10 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@dabh/diagnostics@2.0.8': resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} @@ -14629,12 +14888,18 @@ packages: '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.4.5': resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.4.5': resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} @@ -14644,6 +14909,9 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -15568,6 +15836,15 @@ packages: resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@fastify/accept-negotiator@2.0.1': resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} @@ -15829,9 +16106,6 @@ packages: engines: {node: '>=6'} hasBin: true - '@hexagon/base64@1.1.28': - resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} - '@hono/node-server@1.14.2': resolution: {integrity: sha512-GHjpOeHYbr9d1vkID2sNUYkl5IxumyhDrUJB7wBp7jvqYwPFt+oNKsAPBRcdSbV7kIrXhouLE199ks1QcK4r7A==} engines: {node: '>=18.14.1'} @@ -16258,9 +16532,6 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} - '@levischuck/tiny-cbor@0.2.11': - resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} - '@libsql/client@0.15.15': resolution: {integrity: sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w==} @@ -16525,6 +16796,12 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} @@ -16647,8 +16924,8 @@ packages: engines: {node: '>=18.14.0'} hasBin: true - '@noble/ciphers@2.0.1': - resolution: {integrity: sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g==} + '@noble/ciphers@2.2.0': + resolution: {integrity: sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==} engines: {node: '>= 20.19.0'} '@noble/hashes@1.4.0': @@ -16761,6 +17038,10 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + '@oxc-minify/binding-android-arm-eabi@0.110.0': resolution: {integrity: sha512-43fMTO8/5bMlqfOiNSZNKUzIqeLIYuB9Hr1Ohyf58B1wU11S2dPGibTXOGNaWsfgHy99eeZ1bSgeIHy/fEYqbw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -16894,6 +17175,9 @@ packages: '@oxc-project/types@0.132.0': resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + '@oxc-project/types@0.138.0': + resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} cpu: [arm] @@ -17226,75 +17510,39 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@peculiar/asn1-android@2.5.0': - resolution: {integrity: sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==} - - '@peculiar/asn1-cms@2.5.0': - resolution: {integrity: sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A==} - '@peculiar/asn1-cms@2.7.0': resolution: {integrity: sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ==} - '@peculiar/asn1-csr@2.5.0': - resolution: {integrity: sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ==} - '@peculiar/asn1-csr@2.7.0': resolution: {integrity: sha512-VVsAyGqErT9D1SY4aEqozThXMVI+ssVRiv2DDeYuvpBKLIgZ3hYs3Ay3u/VSoKq6ESFi9cf6rf3IOOzfwh7oMA==} - '@peculiar/asn1-ecc@2.5.0': - resolution: {integrity: sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==} - '@peculiar/asn1-ecc@2.7.0': resolution: {integrity: sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw==} - '@peculiar/asn1-pfx@2.5.0': - resolution: {integrity: sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug==} - '@peculiar/asn1-pfx@2.7.0': resolution: {integrity: sha512-V/nrlQVmhg7lYAsM7E13UDL5erAwFv6kCIVFqNaMIHSVi7dngcT839JkRTkQBqznMG98l2XjxYk74ZztAohZzA==} - '@peculiar/asn1-pkcs8@2.5.0': - resolution: {integrity: sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw==} - '@peculiar/asn1-pkcs8@2.7.0': resolution: {integrity: sha512-9GTl1nE8Mx1kTZ+7QyYatDyKsm34QcWRBFkY1iPvWC3X4Dona5s/tlLiQsx5WzVdZqiMBZNYT0buyw4/vbhnjw==} - '@peculiar/asn1-pkcs9@2.5.0': - resolution: {integrity: sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A==} - '@peculiar/asn1-pkcs9@2.7.0': resolution: {integrity: sha512-Bh7m+OuIaSEllPQcSd9OSp93F4ROWH7sbITWV8MI+8dwsjE5111/87VxiWVvYFKyww3vp39geLv9ENqhwWHcew==} - '@peculiar/asn1-rsa@2.5.0': - resolution: {integrity: sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==} - '@peculiar/asn1-rsa@2.7.0': resolution: {integrity: sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ==} - '@peculiar/asn1-schema@2.5.0': - resolution: {integrity: sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==} - '@peculiar/asn1-schema@2.7.0': resolution: {integrity: sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==} - '@peculiar/asn1-x509-attr@2.5.0': - resolution: {integrity: sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A==} - '@peculiar/asn1-x509-attr@2.7.0': resolution: {integrity: sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w==} - '@peculiar/asn1-x509@2.5.0': - resolution: {integrity: sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==} - '@peculiar/asn1-x509@2.7.0': resolution: {integrity: sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g==} '@peculiar/utils@2.0.3': resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==} - '@peculiar/x509@1.14.0': - resolution: {integrity: sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==} - '@peculiar/x509@1.14.3': resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} @@ -18097,6 +18345,12 @@ packages: cpu: [arm64] os: [android] + '@rolldown/binding-android-arm64@1.1.4': + resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18109,6 +18363,12 @@ packages: cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-arm64@1.1.4': + resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-rc.9': resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18121,6 +18381,12 @@ packages: cpu: [x64] os: [darwin] + '@rolldown/binding-darwin-x64@1.1.4': + resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18133,6 +18399,12 @@ packages: cpu: [x64] os: [freebsd] + '@rolldown/binding-freebsd-x64@1.1.4': + resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18145,6 +18417,12 @@ packages: cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18159,6 +18437,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-arm64-gnu@1.1.4': + resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18173,6 +18458,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-arm64-musl@1.1.4': + resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18187,6 +18479,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-ppc64-gnu@1.1.4': + resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18201,6 +18500,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.1.4': + resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18215,6 +18521,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.1.4': + resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18229,6 +18542,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-x64-musl@1.1.4': + resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18241,6 +18561,12 @@ packages: cpu: [arm64] os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.1.4': + resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} engines: {node: '>=14.0.0'} @@ -18251,6 +18577,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.1.4': + resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18263,6 +18594,12 @@ packages: cpu: [arm64] os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.1.4': + resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18275,6 +18612,12 @@ packages: cpu: [x64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.1.4': + resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.0': resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==} @@ -18519,6 +18862,16 @@ packages: core-js: optional: true + '@rsbuild/core@2.0.14': + resolution: {integrity: sha512-SSPer6vM+v82CV6JXIOzyyT++A1ECgsVvvUuveD5rfGxX/W58vWGnB+zWcYbMfYOjTntD+9ve6QGGh6kBjPx6A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + core-js: '>= 3.0.0' + peerDependenciesMeta: + core-js: + optional: true + '@rsbuild/core@2.0.8': resolution: {integrity: sha512-V5Bhn3zqljsaB5grw9oMkSk6XZFvMkr6UpIYPZnDmOWsRAT1fNQ6XF6cn8fVUKv/KILblBrKLUZf0DpvDt3exw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18599,6 +18952,11 @@ packages: cpu: [arm64] os: [darwin] + '@rspack/binding-darwin-arm64@2.0.8': + resolution: {integrity: sha512-vCgbgH7B7qom+uID+RCZsTCOYFb9wC4/4+1U6rMfytrXGVJ72eNQs2tbdjOl0lb18CT3N/n+VkWynUiLk84GwA==} + cpu: [arm64] + os: [darwin] + '@rspack/binding-darwin-x64@2.0.0': resolution: {integrity: sha512-YQ96LMmzIzhZt9cZWUDWXSxS9UWWHWoLxJyZ5f42DSaVPVelBg5ThbVORDwOP5QDA2xFXj60rVnmmcZLzg/aDA==} cpu: [x64] @@ -18609,6 +18967,11 @@ packages: cpu: [x64] os: [darwin] + '@rspack/binding-darwin-x64@2.0.8': + resolution: {integrity: sha512-satPm2PD4B7jDTVlVAdvMVdUszwLvWUEnUDzLb77mvVkezKNDZmuhb+e8s+FfKs8hJpNbZ9VAejuA2rr8o985w==} + cpu: [x64] + os: [darwin] + '@rspack/binding-linux-arm64-gnu@2.0.0': resolution: {integrity: sha512-Ufn33gzkIV7JY69k6vJQEdOzRvBqThIgH46pwXksHSMwRZp8IbJhXfyYIAVsRWCk8fXpr9t1nAvCDvJXT2EeyA==} cpu: [arm64] @@ -18621,6 +18984,12 @@ packages: os: [linux] libc: [glibc] + '@rspack/binding-linux-arm64-gnu@2.0.8': + resolution: {integrity: sha512-pSI+npPQE/uDtiboqvcOIRJbEV2+B+H1xffmko/gw50la92oTUW60kVULFwsb6L0+GVCzIcwX3yq60GtYIn+Ug==} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rspack/binding-linux-arm64-musl@2.0.0': resolution: {integrity: sha512-CZbvFKlNY9UC0C+Czz6i8JFCzGpuL9oX8gEqcJA1+84Y6eEEBH50UiTzeCewxKW3dOofkZdvT5vgNMXz6aMUmg==} cpu: [arm64] @@ -18633,6 +19002,12 @@ packages: os: [linux] libc: [musl] + '@rspack/binding-linux-arm64-musl@2.0.8': + resolution: {integrity: sha512-igjJ43yxWQ72GZqjDDZSSHax9/Vg+6rLMmOvFglTJUkQpB4Tyvu/YjW+WRjYj2xRw6blOjLxUSJWASvuSqqlvg==} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rspack/binding-linux-x64-gnu@2.0.0': resolution: {integrity: sha512-dPjFGpoCvZfFpJBsWAUR+PR7mWYxpou6L026qIOpAVkz7WiTzErwKD3P1jVrpP4dM9yLb3fVE+PHHjTglhTJ4g==} cpu: [x64] @@ -18645,6 +19020,12 @@ packages: os: [linux] libc: [glibc] + '@rspack/binding-linux-x64-gnu@2.0.8': + resolution: {integrity: sha512-zrkoEOnqj1hOEBO5T2I/2Ts2HSJsYFh1qXwMpK4dMJFGGNWDfNeUa6/LF5uq3VINF3JUl7RL47AgrucoSZJXPA==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rspack/binding-linux-x64-musl@2.0.0': resolution: {integrity: sha512-4fgDTMWt0mJDiugdia2mdOjTbnm7yM1Drzl1JpPqlUlOr113byOhc+qgN57LURSGypz2yz/h/Zad7/UnVAxYJw==} cpu: [x64] @@ -18657,6 +19038,12 @@ packages: os: [linux] libc: [musl] + '@rspack/binding-linux-x64-musl@2.0.8': + resolution: {integrity: sha512-6CtDaGZjNDvJd9TBp7a9zABbrPORO21W96+3ZcGBn0YNUPUk4ARxIxrTTpeJ/1F41QDM8AYIkGDdqEYMqTYBsA==} + cpu: [x64] + os: [linux] + libc: [musl] + '@rspack/binding-wasm32-wasi@2.0.0': resolution: {integrity: sha512-ANk73ZKtPrZf9gdtyRK2nQUfhi1uXoC5P2KF89pyVAE8+zcoLBnYtZGYpWa/cmNi5BcO5g4Z+v2l1UA3bUPLQQ==} cpu: [wasm32] @@ -18665,6 +19052,10 @@ packages: resolution: {integrity: sha512-duEkRoXrl9SW8uGHv7JURJ5lgKu87qFDQ4Exy6UQPvsUJVXhtRXTfvMHCb/CejVJuW2Bw2D632/axZq3qRSuBQ==} cpu: [wasm32] + '@rspack/binding-wasm32-wasi@2.0.8': + resolution: {integrity: sha512-Yf4SiqTUroT5Ju+te0YAY2xxKOb35tECsO21v7hYyGa705wrgoAK/MmF7enOvs9GR1iZIqgiLD/wxsIxl8GjJw==} + cpu: [wasm32] + '@rspack/binding-win32-arm64-msvc@2.0.0': resolution: {integrity: sha512-IHZFRtJ85ONbM+BCtF4TeYXS2Fu9X0IJS2phX1rPibYq9iEtHGfBt4cNlnsJPhbPAXVvi4Oli/yiLRJ1zxtCIg==} cpu: [arm64] @@ -18675,6 +19066,11 @@ packages: cpu: [arm64] os: [win32] + '@rspack/binding-win32-arm64-msvc@2.0.8': + resolution: {integrity: sha512-8NCuiQsAhXrwRBy57QZoypqrws/zLBkaQVGiB8hksr6v++8hNigNjqpQARLbd0iyMuHsQQ++8+auGk6xlDXmzw==} + cpu: [arm64] + os: [win32] + '@rspack/binding-win32-ia32-msvc@2.0.0': resolution: {integrity: sha512-n4tbIqacq/FhNJflMlgZV50AeQFTLh5hnDS3v4W+rJWa3IW1VfgB0+XppdeW+Dqhw7QcMIsCmro01kwNdlXZDQ==} cpu: [ia32] @@ -18685,6 +19081,11 @@ packages: cpu: [ia32] os: [win32] + '@rspack/binding-win32-ia32-msvc@2.0.8': + resolution: {integrity: sha512-bxiekytbX7V9KFAra+HkwtNWC6pYfHEBBZFpiT0xUs3mCFOmAAFVBsBSQsoCP9AdCEXoMAvNdnrHNw3iov4OZw==} + cpu: [ia32] + os: [win32] + '@rspack/binding-win32-x64-msvc@2.0.0': resolution: {integrity: sha512-cJOgikIW2t3S+42TQZsv+DJriJt2m6lnUk+pUFu/fO93rrMvNrx8gfMxR8W5zDTreBX0cfMx2pw6EVmyi/YzsQ==} cpu: [x64] @@ -18695,12 +19096,20 @@ packages: cpu: [x64] os: [win32] + '@rspack/binding-win32-x64-msvc@2.0.8': + resolution: {integrity: sha512-7zPs8YCe/ZVJTwd+5lpB0CP0tkn2pONf/T1ycmVY76u21Nrwt8mXQGc/2yH2eWP4B7fikYBr3hGr7mpR2fajqQ==} + cpu: [x64] + os: [win32] + '@rspack/binding@2.0.0': resolution: {integrity: sha512-WA2f9eQpejkvf5Vrnf6wNCn1m8RT1p08NjgOZpKhsCzr0uBjWeRvGduawlrFFHZh/jPnWZTVaVdQ08FEAWbwGw==} '@rspack/binding@2.0.5': resolution: {integrity: sha512-Ta1y4WXJA87wM1OstqaMddoPsBGv7Cu779bYToKxEAqR/Yy9DxLkp7bdgBaAx2JH++BwVjV+toWts2V9AaiTFQ==} + '@rspack/binding@2.0.8': + resolution: {integrity: sha512-3uZ+y8aQxq33ty2srMxg2Nu0XuBI6vVrG50rkDaXqwWqOohfgGUSfFuQK7EnSUNy4aFUQlCG6NHialQHJov0wg==} + '@rspack/core@2.0.0': resolution: {integrity: sha512-WD1mJM9LbZ7Z399Rbv9dE3BNEV0+3sE5OzDdzV8hOxUb3mX++ynK5n9kil8w60B6nGdcKeV9ly5aN4PgqiwWUg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18725,6 +19134,18 @@ packages: '@swc/helpers': optional: true + '@rspack/core@2.0.8': + resolution: {integrity: sha512-+NLGJf8gZxihDmMFzjlly3toc2SMjeDmuvz0/Cai9AMdV4F+Pqcnt2BA9V4e3SY2jmhJQtPwgyyLtR1RiJO77g==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 + '@swc/helpers': ^0.5.23 + peerDependenciesMeta: + '@module-federation/runtime-tools': + optional: true + '@swc/helpers': + optional: true + '@rspack/lite-tapable@1.1.0': resolution: {integrity: sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==} @@ -18899,13 +19320,6 @@ packages: pinia: optional: true - '@simplewebauthn/browser@13.2.2': - resolution: {integrity: sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==} - - '@simplewebauthn/server@13.2.2': - resolution: {integrity: sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==} - engines: {node: '>=20.0.0'} - '@sinclair/typebox@0.31.28': resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} @@ -19605,6 +20019,9 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} @@ -20568,10 +20985,6 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: '>=12'} - ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} @@ -20804,24 +21217,55 @@ packages: peerDependencies: ajv: 4.11.8 - 8 - better-auth@1.3.27: - resolution: {integrity: sha512-SwiGAJ7yU6dBhNg0NdV1h5M8T5sa7/AszZVc4vBfMDrLLmvUfbt9JoJ0uRUJUEdKRAAxTyl9yA+F3+GhtAD80w==} + better-auth@1.6.19: + resolution: {integrity: sha512-68eXWKj0sxa0xW4+n4tENd6Co94UCynPKe1fncmO6kIB3XhSXWgwDEpiUouJV2dmLBrHM1FPkoI6Q5597zCGpQ==} peerDependencies: '@lynx-js/react': '*' - '@sveltejs/kit': '*' - next: '*' - react: '*' - react-dom: '*' - solid-js: '*' - svelte: '*' - vue: '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': workspace:* + '@tanstack/solid-start': workspace:* + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: ^0.45.2 + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^19.2.3 + react-dom: ^19.2.3 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': optional: true + '@prisma/client': + optional: true '@sveltejs/kit': optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true next: optional: true + pg: + optional: true + prisma: + optional: true react: optional: true react-dom: @@ -20830,11 +21274,18 @@ packages: optional: true svelte: optional: true + vitest: + optional: true vue: optional: true - better-call@1.0.19: - resolution: {integrity: sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw==} + better-call@1.3.6: + resolution: {integrity: sha512-no1jI+h6Bkxs1NVBo4rONbVIzsPjZ8IUu7IHaJBiFwVX1XEQGN8KpHots5fSWmXe9nNyLuLIcgx6WEUcE6EDaA==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} @@ -20989,10 +21440,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.4.1: - resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -21404,8 +21851,8 @@ packages: srvx: optional: true - crossws@0.4.5: - resolution: {integrity: sha512-wUR89x/Rw7/8t+vn0CmGDYM9TD6VtARGb0LD5jq2wjtMy1vCP4M+sm6N6TigWeTYvnA8MoW29NqqXD0ep0rfBA==} + crossws@0.4.9: + resolution: {integrity: sha512-iWx+1OMSG2aOHpjyf9AESOzkwsVdS49cXM9dVrI2PDhxU5l2RIWE/KG56gk4BbAnsMoycvniJ9OnOxO9LRzHVA==} peerDependencies: srvx: '>=0.11.5' peerDependenciesMeta: @@ -21438,6 +21885,10 @@ packages: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} @@ -21483,6 +21934,10 @@ packages: resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} engines: {node: '>=20'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} @@ -21539,6 +21994,9 @@ packages: decimal.js@10.5.0: resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} peerDependencies: @@ -21881,6 +22339,10 @@ packages: resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -21889,22 +22351,14 @@ packages: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - env-runner@0.1.6: - resolution: {integrity: sha512-fSb7X1zdda8k6611a6/SdSQpDe7a/bqMz2UWdbHjk9YWzpUR4/fn9YtE/hqgGQ2nhvVN0zUtcL1SRMKwIsDbAA==} - hasBin: true - peerDependencies: - miniflare: ^4.0.0 - peerDependenciesMeta: - miniflare: - optional: true - - env-runner@0.1.9: - resolution: {integrity: sha512-W9AiZlPx0uXtghAJiTBkeZOgyQdecVvoln3cHoOEZswPq0cVMi+WBhUQjdUn+JcZFAFgOt+i5fcO7C2zniZoCg==} + env-runner@0.1.16: + resolution: {integrity: sha512-2LRJM4P2KLX6J83QZZrMqvgCDt/D5ea7wPcI3yYiy5cG/9rX5QwdwZFx0D7ktWnjdRyZxYjttGGorb5nFqb1CA==} hasBin: true peerDependencies: '@netlify/runtime': ^4.1.23 - '@vercel/queue': ^0.2.0 + '@vercel/queue': '>=0.2.0' miniflare: ^4.20260515.0 + wrangler: ^4.0.0 peerDependenciesMeta: '@netlify/runtime': optional: true @@ -21912,6 +22366,17 @@ packages: optional: true miniflare: optional: true + wrangler: + optional: true + + env-runner@0.1.6: + resolution: {integrity: sha512-fSb7X1zdda8k6611a6/SdSQpDe7a/bqMz2UWdbHjk9YWzpUR4/fn9YtE/hqgGQ2nhvVN0zUtcL1SRMKwIsDbAA==} + hasBin: true + peerDependencies: + miniflare: ^4.0.0 + peerDependenciesMeta: + miniflare: + optional: true envinfo@7.14.0: resolution: {integrity: sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==} @@ -22283,6 +22748,9 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + exsolve@1.1.0: + resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -22368,6 +22836,9 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -22792,6 +23263,10 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-entities@2.3.3: resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} @@ -22885,8 +23360,8 @@ packages: httpxy@0.3.1: resolution: {integrity: sha512-XjG/CEoofEisMrnFr0D6U6xOZ4mRfnwcYQ9qvvnT4lvnX8BoeA3x3WofB75D+vZwpaobFVkBIHrZzoK40w8XSw==} - httpxy@0.5.3: - resolution: {integrity: sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw==} + httpxy@0.5.4: + resolution: {integrity: sha512-URfeibL0kTH6VuIxxaJDXWQWEk8fKr+9L8MGv6CuAiNy0fGnoVhWbXBvJR1mkdsvCDUxvhX9cW60k2AhtH5s6w==} human-id@4.1.1: resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} @@ -23334,6 +23809,15 @@ packages: canvas: optional: true + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -23431,9 +23915,9 @@ packages: resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==} engines: {node: '>=14.0.0'} - kysely@0.28.8: - resolution: {integrity: sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==} - engines: {node: '>=20.0.0'} + kysely@0.29.2: + resolution: {integrity: sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg==} + engines: {node: '>=22.0.0'} lambda-local@2.2.0: resolution: {integrity: sha512-bPcgpIXbHnVGfI/omZIlgucDqlf4LrsunwoKue5JdZeGybt8L6KyJz2Zu19ffuZwIwLj2NAI2ZyaqNT6/cetcg==} @@ -23658,6 +24142,10 @@ packages: resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} engines: {node: 20 || >=22} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -23727,6 +24215,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -23927,8 +24418,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanostores@1.0.1: - resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==} + nanostores@1.3.0: + resolution: {integrity: sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA==} engines: {node: ^20.0.0 || >=22.0.0} napi-postinstall@0.3.3: @@ -23960,22 +24451,22 @@ packages: nf3@0.3.11: resolution: {integrity: sha512-ObKp/SA3f1g1f/OMeDlRWaZmqGgk7A0NnDIbeO7c/MV4r/quMlpP/BsqMGuTi3lUlXbC1On8YH7ICM2u2bIAOw==} - nf3@0.3.17: - resolution: {integrity: sha512-N9zEWySuJFw+gR0lhS5863YsvNeudOdqRyFvNb+jMXbeTJOdrjDqkCpDginIZfUm0LzT1t1nCRiDeqQm/8kirQ==} + nf3@0.3.19: + resolution: {integrity: sha512-tfOXX/ivQBL+4km/fzxQ0HWMmp1Ewx/YpFLbya080gXVfm26bZy0n4a5K5PPnqTLyuAHu0FobVpXXUadIiIwIQ==} nf3@0.3.6: resolution: {integrity: sha512-/XRUUILTAyuy1XunyVQuqGp8aEmZ2TfRTn8Rji+FA4xqv20qzL4jV7Reqbuey2XucKgPeRVcEYGScmJM0UnB6Q==} - nitro-nightly@3.0.260522-beta: - resolution: {integrity: sha512-Pm0AiQ1nLcreUFZKJcmVI4l9K/ygXoFamIPhc44XJHj9vmt25Rlqjv55frw6sS0L1qHrySpo3xyVVBmR9aTBqA==} + nitro-nightly@3.0.1-20260702-173353-03122331: + resolution: {integrity: sha512-XIpdxZKnC6sFhyfq7Rl/TPgrzEupeIBQKZoQD3tPMTMJfXcF3pMJHy7850vPBq1klIS4ec6pTrE06qh7kGm6/Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@vercel/queue': ^0.2.0 + '@vercel/queue': ^0.3.1 dotenv: '*' giget: '*' - jiti: ^2.6.1 - rollup: ^4.60.3 + jiti: ^2.7.0 + rollup: ^4.62.0 vite: ^8.0.14 xml2js: ^0.6.2 zephyr-agent: ^0.2.0 @@ -24199,8 +24690,8 @@ packages: ocache@0.1.2: resolution: {integrity: sha512-lI34wjM7cahEdrq2I5obbF7MEdE97vULf6vNj6ZCzwEadzyXO1w7QOl2qzzG4IL8cyO7wDtXPj9CqW/aG3mn7g==} - ocache@0.1.4: - resolution: {integrity: sha512-e7geNdWjxSnvsSgvLuPvgKgu7ubM10ZmTPOgpr7mz2BXYtvjMKTiLhjFi/gWU8chkuP6hNkZBsa9LzOusyaqkQ==} + ocache@0.1.5: + resolution: {integrity: sha512-kNNnkkVQup/QDvmTz8Q84wc2ntiyoVHDxa6eHWKt5qdGAmFRBIxy83rxgCYEjW0x06UJ9E3P6VgM2yY4rOBH4w==} ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -24390,6 +24881,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -25098,6 +25592,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rolldown@1.1.4: + resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup-plugin-visualizer@6.0.5: resolution: {integrity: sha512-9+HlNgKCVbJDs8tVtjQ43US12eqaiHyyiLMdBwQ7vSZPiHMysGNo2E88TAp1si5wx8NAoYriI2A5kuKfIakmJg==} engines: {node: '>=18'} @@ -25116,9 +25615,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rou3@0.5.1: - resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} - rou3@0.7.12: resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} @@ -25309,6 +25805,9 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -25514,6 +26013,11 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + srvx@0.11.20: + resolution: {integrity: sha512-gdPvbwpJOJpMyBYcp39q78C4pmv/Aw5WwZKB3+/pfeUVxo3EvNXlOi1fxzoy2ECGvUeBhouXy9rBxstjQQOPog==} + engines: {node: '>=20.16.0'} + hasBin: true + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -25868,6 +26372,10 @@ packages: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -25921,6 +26429,7 @@ packages: tsconfck@3.1.4: resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -26079,6 +26588,10 @@ packages: resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} engines: {node: '>=20.18.1'} + undici@7.27.2: + resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} + engines: {node: '>=20.18.1'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -26785,6 +27298,10 @@ packages: resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} engines: {node: '>=20'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-cli@5.1.4: resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==} engines: {node: '>=14.15.0'} @@ -26879,6 +27396,10 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + whatwg-url@14.1.0: resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} engines: {node: '>=18'} @@ -26887,6 +27408,10 @@ packages: resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} engines: {node: '>=20'} + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -27125,24 +27650,24 @@ snapshots: '@andrewbranch/untar.js@1.0.3': {} - '@arethetypeswrong/cli@0.18.2': + '@arethetypeswrong/cli@0.18.4': dependencies: - '@arethetypeswrong/core': 0.18.2 + '@arethetypeswrong/core': 0.18.4 chalk: 4.1.2 cli-table3: 0.6.5 commander: 10.0.1 marked: 9.1.6 marked-terminal: 7.3.0(marked@9.1.6) - semver: 7.7.3 + semver: 7.8.2 - '@arethetypeswrong/core@0.18.2': + '@arethetypeswrong/core@0.18.4': dependencies: '@andrewbranch/untar.js': 1.0.3 '@loaderkit/resolve': 1.0.4 cjs-module-lexer: 1.4.3 - fflate: 0.8.2 - lru-cache: 11.2.2 - semver: 7.7.3 + fflate: 0.8.3 + lru-cache: 11.5.1 + semver: 7.8.2 typescript: 5.6.1-rc validate-npm-package-name: 5.0.1 @@ -27168,6 +27693,14 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 11.2.2 + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@asamuzakjp/dom-selector@6.5.6': dependencies: '@asamuzakjp/nwsapi': 2.3.9 @@ -27176,6 +27709,16 @@ snapshots: is-potential-custom-element-name: 1.0.1 lru-cache: 11.2.2 + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + '@asamuzakjp/nwsapi@2.3.9': {} '@assemblyscript/loader@0.19.23': {} @@ -27336,7 +27879,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.28.5 + '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -27406,7 +27949,7 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -27415,7 +27958,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -27706,26 +28249,75 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@better-auth/core@1.3.27': + '@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0)': dependencies: - better-call: 1.0.19 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + '@opentelemetry/semantic-conventions': 1.41.1 + '@standard-schema/spec': 1.1.0 + better-call: 1.3.6(zod@4.4.3) + jose: 6.1.3 + kysely: 0.29.2 + nanostores: 1.3.0 zod: 4.4.3 - '@better-auth/utils@0.3.0': {} + '@better-auth/drizzle-adapter@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 - '@better-fetch/fetch@1.1.18': {} + '@better-auth/kysely-adapter@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.29.2)': + dependencies: + '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 + optionalDependencies: + kysely: 0.29.2 + + '@better-auth/memory-adapter@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 + + '@better-auth/mongo-adapter@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': + dependencies: + '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 + + '@better-auth/prisma-adapter@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@prisma/client@7.0.0(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(typescript@6.0.2))(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))': + dependencies: + '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 + optionalDependencies: + '@prisma/client': 7.0.0(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(typescript@6.0.2) + prisma: 7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) + + '@better-auth/telemetry@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': + dependencies: + '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + + '@better-auth/utils@0.4.2': + dependencies: + '@noble/hashes': 2.0.1 + + '@better-fetch/fetch@1.3.1': {} '@blazediff/core@1.9.1': {} '@braidai/lang@1.1.2': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@bundled-es-modules/cookie@2.0.1': dependencies: cookie: 0.7.2 '@bundled-es-modules/statuses@1.0.1': dependencies: - statuses: 2.0.1 + statuses: 2.0.2 '@bundled-es-modules/tough-cookie@0.1.6': dependencies: @@ -28036,9 +28628,9 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260317.1': optional: true - '@codspeed/core@5.5.0': + '@codspeed/core@5.5.0(debug@4.4.3)': dependencies: - axios: 1.17.0 + axios: 1.17.0(debug@4.4.3) find-up: 6.3.0 form-data: 4.0.5 node-gyp-build: 4.8.4 @@ -28047,12 +28639,12 @@ snapshots: - debug - supports-color - '@codspeed/vitest-plugin@5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4)': + '@codspeed/vitest-plugin@5.5.0(debug@4.4.3)(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4)': dependencies: - '@codspeed/core': 5.5.0 + '@codspeed/core': 5.5.0(debug@4.4.3) tinybench: 2.9.0 vite: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) - vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) transitivePeerDependencies: - debug - supports-color @@ -28062,9 +28654,9 @@ snapshots: '@colors/colors@1.6.0': {} - '@convex-dev/better-auth@0.9.7(@standard-schema/spec@1.1.0)(better-auth@1.3.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@2.0.0-beta.15)(vue@3.5.25(typescript@6.0.2)))(convex@1.28.2(@clerk/clerk-react@5.59.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3))(hono@4.7.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': + '@convex-dev/better-auth@0.9.7(@standard-schema/spec@1.1.0)(better-auth@1.6.19(@prisma/client@7.0.0(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(typescript@6.0.2))(@tanstack/solid-start@packages+solid-start)(mysql2@3.15.3)(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@2.0.0-beta.15)(vitest@4.1.4)(vue@3.5.25(typescript@6.0.2)))(convex@1.28.2(@clerk/clerk-react@5.59.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3))(hono@4.7.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)': dependencies: - better-auth: 1.3.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@2.0.0-beta.15)(vue@3.5.25(typescript@6.0.2)) + better-auth: 1.6.19(@prisma/client@7.0.0(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(typescript@6.0.2))(@tanstack/solid-start@packages+solid-start)(mysql2@3.15.3)(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@2.0.0-beta.15)(vitest@4.1.4)(vue@3.5.25(typescript@6.0.2)) common-tags: 1.8.2 convex: 1.28.2(@clerk/clerk-react@5.59.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) convex-helpers: 0.1.104(@standard-schema/spec@1.1.0)(convex@1.28.2(@clerk/clerk-react@5.59.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3))(hono@4.7.10)(react@19.2.3)(typescript@6.0.2)(zod@3.25.76) @@ -28093,6 +28685,8 @@ snapshots: '@csstools/color-helpers@5.1.0': {} + '@csstools/color-helpers@6.0.2': {} + '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) @@ -28103,6 +28697,11 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/color-helpers': 5.0.1 @@ -28117,6 +28716,13 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-color-parser@4.1.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-tokenizer': 3.0.3 @@ -28125,14 +28731,24 @@ snapshots: dependencies: '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-syntax-patches-for-csstree@1.0.14(postcss@8.5.15)': dependencies: postcss: 8.5.15 + '@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + '@csstools/css-tokenizer@3.0.3': {} '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-tokenizer@4.0.0': {} + '@dabh/diagnostics@2.0.8': dependencies: '@so-ric/colorspace': 1.1.6 @@ -28170,6 +28786,12 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.4.5': dependencies: '@emnapi/wasi-threads': 1.0.4 @@ -28180,6 +28802,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.4.5': dependencies: tslib: 2.8.1 @@ -28193,6 +28820,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.27.1 @@ -28819,6 +29451,10 @@ snapshots: '@eslint/core': 0.12.0 levn: 0.4.1 + '@exodus/bytes@1.15.1(@noble/hashes@2.0.1)': + optionalDependencies: + '@noble/hashes': 2.0.1 + '@fastify/accept-negotiator@2.0.1': {} '@fastify/ajv-compiler@4.0.5': @@ -29195,8 +29831,6 @@ snapshots: protobufjs: 7.4.0 yargs: 17.7.2 - '@hexagon/base64@1.1.28': {} - '@hono/node-server@1.14.2(hono@4.7.10)': dependencies: hono: 4.7.10 @@ -29379,7 +30013,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.1 optional: true '@img/sharp-win32-arm64@0.34.4': @@ -29524,8 +30158,6 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} - '@levischuck/tiny-cbor@0.2.11': {} - '@libsql/client@0.15.15': dependencies: '@libsql/core': 0.15.15 @@ -29677,7 +30309,7 @@ snapshots: https-proxy-agent: 7.0.6 node-fetch: 2.7.0 nopt: 8.1.0 - semver: 7.7.3 + semver: 7.8.2 tar: 7.4.3 transitivePeerDependencies: - encoding @@ -29868,6 +30500,20 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@neon-rs/load@0.0.4': {} '@netlify/api@14.0.7': @@ -30172,7 +30818,7 @@ snapshots: '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.56.0)': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 '@babel/types': 7.28.4 '@netlify/binary-info': 1.0.0 '@netlify/serverless-functions-api': 2.7.1 @@ -30211,7 +30857,7 @@ snapshots: - rollup - supports-color - '@noble/ciphers@2.0.1': {} + '@noble/ciphers@2.2.0': {} '@noble/hashes@1.4.0': {} @@ -30231,13 +30877,13 @@ snapshots: '@nothing-but/utils@0.17.0': {} - '@nx/devkit@22.7.5(nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23)))': + '@nx/devkit@22.7.5(nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))(debug@4.4.3))': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 enquirer: 2.3.6 minimatch: 10.2.5 - nx: 22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23)) + nx: 22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))(debug@4.4.3) semver: 7.8.2 tslib: 2.8.1 yargs-parser: 21.1.1 @@ -30300,6 +30946,8 @@ snapshots: '@open-draft/until@2.1.0': {} + '@opentelemetry/semantic-conventions@1.41.1': {} + '@oxc-minify/binding-android-arm-eabi@0.110.0': optional: true @@ -30369,6 +31017,8 @@ snapshots: '@oxc-project/types@0.132.0': {} + '@oxc-project/types@0.138.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -30566,20 +31216,6 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 - '@peculiar/asn1-android@2.5.0': - dependencies: - '@peculiar/asn1-schema': 2.5.0 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-cms@2.5.0': - dependencies: - '@peculiar/asn1-schema': 2.5.0 - '@peculiar/asn1-x509': 2.5.0 - '@peculiar/asn1-x509-attr': 2.5.0 - asn1js: 3.0.6 - tslib: 2.8.1 - '@peculiar/asn1-cms@2.7.0': dependencies: '@peculiar/asn1-schema': 2.7.0 @@ -30588,13 +31224,6 @@ snapshots: asn1js: 3.0.6 tslib: 2.8.1 - '@peculiar/asn1-csr@2.5.0': - dependencies: - '@peculiar/asn1-schema': 2.5.0 - '@peculiar/asn1-x509': 2.5.0 - asn1js: 3.0.6 - tslib: 2.8.1 - '@peculiar/asn1-csr@2.7.0': dependencies: '@peculiar/asn1-schema': 2.7.0 @@ -30602,13 +31231,6 @@ snapshots: asn1js: 3.0.6 tslib: 2.8.1 - '@peculiar/asn1-ecc@2.5.0': - dependencies: - '@peculiar/asn1-schema': 2.5.0 - '@peculiar/asn1-x509': 2.5.0 - asn1js: 3.0.6 - tslib: 2.8.1 - '@peculiar/asn1-ecc@2.7.0': dependencies: '@peculiar/asn1-schema': 2.7.0 @@ -30616,15 +31238,6 @@ snapshots: asn1js: 3.0.6 tslib: 2.8.1 - '@peculiar/asn1-pfx@2.5.0': - dependencies: - '@peculiar/asn1-cms': 2.5.0 - '@peculiar/asn1-pkcs8': 2.5.0 - '@peculiar/asn1-rsa': 2.5.0 - '@peculiar/asn1-schema': 2.5.0 - asn1js: 3.0.6 - tslib: 2.8.1 - '@peculiar/asn1-pfx@2.7.0': dependencies: '@peculiar/asn1-cms': 2.7.0 @@ -30634,13 +31247,6 @@ snapshots: asn1js: 3.0.6 tslib: 2.8.1 - '@peculiar/asn1-pkcs8@2.5.0': - dependencies: - '@peculiar/asn1-schema': 2.5.0 - '@peculiar/asn1-x509': 2.5.0 - asn1js: 3.0.6 - tslib: 2.8.1 - '@peculiar/asn1-pkcs8@2.7.0': dependencies: '@peculiar/asn1-schema': 2.7.0 @@ -30648,17 +31254,6 @@ snapshots: asn1js: 3.0.6 tslib: 2.8.1 - '@peculiar/asn1-pkcs9@2.5.0': - dependencies: - '@peculiar/asn1-cms': 2.5.0 - '@peculiar/asn1-pfx': 2.5.0 - '@peculiar/asn1-pkcs8': 2.5.0 - '@peculiar/asn1-schema': 2.5.0 - '@peculiar/asn1-x509': 2.5.0 - '@peculiar/asn1-x509-attr': 2.5.0 - asn1js: 3.0.6 - tslib: 2.8.1 - '@peculiar/asn1-pkcs9@2.7.0': dependencies: '@peculiar/asn1-cms': 2.7.0 @@ -30670,13 +31265,6 @@ snapshots: asn1js: 3.0.6 tslib: 2.8.1 - '@peculiar/asn1-rsa@2.5.0': - dependencies: - '@peculiar/asn1-schema': 2.5.0 - '@peculiar/asn1-x509': 2.5.0 - asn1js: 3.0.6 - tslib: 2.8.1 - '@peculiar/asn1-rsa@2.7.0': dependencies: '@peculiar/asn1-schema': 2.7.0 @@ -30684,25 +31272,12 @@ snapshots: asn1js: 3.0.6 tslib: 2.8.1 - '@peculiar/asn1-schema@2.5.0': - dependencies: - asn1js: 3.0.6 - pvtsutils: 1.3.6 - tslib: 2.8.1 - '@peculiar/asn1-schema@2.7.0': dependencies: '@peculiar/utils': 2.0.3 asn1js: 3.0.6 tslib: 2.8.1 - '@peculiar/asn1-x509-attr@2.5.0': - dependencies: - '@peculiar/asn1-schema': 2.5.0 - '@peculiar/asn1-x509': 2.5.0 - asn1js: 3.0.6 - tslib: 2.8.1 - '@peculiar/asn1-x509-attr@2.7.0': dependencies: '@peculiar/asn1-schema': 2.7.0 @@ -30710,13 +31285,6 @@ snapshots: asn1js: 3.0.6 tslib: 2.8.1 - '@peculiar/asn1-x509@2.5.0': - dependencies: - '@peculiar/asn1-schema': 2.5.0 - asn1js: 3.0.6 - pvtsutils: 1.3.6 - tslib: 2.8.1 - '@peculiar/asn1-x509@2.7.0': dependencies: '@peculiar/asn1-schema': 2.7.0 @@ -30728,20 +31296,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@peculiar/x509@1.14.0': - dependencies: - '@peculiar/asn1-cms': 2.5.0 - '@peculiar/asn1-csr': 2.5.0 - '@peculiar/asn1-ecc': 2.5.0 - '@peculiar/asn1-pkcs9': 2.5.0 - '@peculiar/asn1-rsa': 2.5.0 - '@peculiar/asn1-schema': 2.5.0 - '@peculiar/asn1-x509': 2.5.0 - pvtsutils: 1.3.6 - reflect-metadata: 0.2.2 - tslib: 2.8.1 - tsyringe: 4.10.0 - '@peculiar/x509@1.14.3': dependencies: '@peculiar/asn1-cms': 2.7.0 @@ -31617,72 +32171,108 @@ snapshots: '@rolldown/binding-android-arm64@1.0.2': optional: true + '@rolldown/binding-android-arm64@1.1.4': + optional: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': optional: true '@rolldown/binding-darwin-arm64@1.0.2': optional: true + '@rolldown/binding-darwin-arm64@1.1.4': + optional: true + '@rolldown/binding-darwin-x64@1.0.0-rc.9': optional: true '@rolldown/binding-darwin-x64@1.0.2': optional: true + '@rolldown/binding-darwin-x64@1.1.4': + optional: true + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': optional: true '@rolldown/binding-freebsd-x64@1.0.2': optional: true + '@rolldown/binding-freebsd-x64@1.1.4': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.2': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.2': optional: true + '@rolldown/binding-linux-arm64-gnu@1.1.4': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': optional: true '@rolldown/binding-linux-arm64-musl@1.0.2': optional: true + '@rolldown/binding-linux-arm64-musl@1.1.4': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': optional: true '@rolldown/binding-linux-ppc64-gnu@1.0.2': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.1.4': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': optional: true '@rolldown/binding-linux-s390x-gnu@1.0.2': optional: true + '@rolldown/binding-linux-s390x-gnu@1.1.4': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': optional: true '@rolldown/binding-linux-x64-gnu@1.0.2': optional: true + '@rolldown/binding-linux-x64-gnu@1.1.4': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': optional: true '@rolldown/binding-linux-x64-musl@1.0.2': optional: true + '@rolldown/binding-linux-x64-musl@1.1.4': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': optional: true '@rolldown/binding-openharmony-arm64@1.0.2': optional: true + '@rolldown/binding-openharmony-arm64@1.1.4': + optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) @@ -31691,6 +32281,14 @@ snapshots: - '@emnapi/runtime' optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + '@rolldown/binding-wasm32-wasi@1.0.2': dependencies: '@emnapi/core': 1.10.0 @@ -31698,18 +32296,31 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true + '@rolldown/binding-wasm32-wasi@1.1.4': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': optional: true '@rolldown/binding-win32-arm64-msvc@1.0.2': optional: true + '@rolldown/binding-win32-arm64-msvc@1.1.4': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': optional: true '@rolldown/binding-win32-x64-msvc@1.0.2': optional: true + '@rolldown/binding-win32-x64-msvc@1.1.4': + optional: true + '@rolldown/pluginutils@1.0.0': {} '@rolldown/pluginutils@1.0.0-beta.19': {} @@ -31871,6 +32482,13 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' + '@rsbuild/core@2.0.14': + dependencies: + '@rspack/core': 2.0.8(@swc/helpers@0.5.23) + '@swc/helpers': 0.5.23 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + '@rsbuild/core@2.0.8': dependencies: '@rspack/core': 2.0.5(@swc/helpers@0.5.23) @@ -31878,13 +32496,13 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/plugin-babel@1.0.6(@rsbuild/core@2.0.8)': + '@rsbuild/plugin-babel@1.0.6(@rsbuild/core@2.0.14)': dependencies: '@babel/core': 7.28.5 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.5) '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.5) '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) - '@rsbuild/core': 2.0.8 + '@rsbuild/core': 2.0.14 '@types/babel__core': 7.20.5 deepmerge: 4.3.1 reduce-configs: 1.1.1 @@ -31892,7 +32510,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@rsbuild/plugin-babel@1.1.2(@rsbuild/core@2.0.1)': + '@rsbuild/plugin-babel@1.1.2(@rsbuild/core@2.0.14)': dependencies: '@babel/core': 7.29.0 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) @@ -31901,92 +32519,66 @@ snapshots: '@types/babel__core': 7.20.5 reduce-configs: 1.1.1 optionalDependencies: - '@rsbuild/core': 2.0.1 + '@rsbuild/core': 2.0.14 transitivePeerDependencies: - supports-color - '@rsbuild/plugin-babel@1.1.2(@rsbuild/core@2.0.8)': + '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23))': dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@types/babel__core': 7.20.5 - reduce-configs: 1.1.1 - optionalDependencies: - '@rsbuild/core': 2.0.8 - transitivePeerDependencies: - - supports-color - - '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.21))': - dependencies: - '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.21))(react-refresh@0.18.0) + '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.8(@swc/helpers@0.5.23))(react-refresh@0.18.0) react-refresh: 0.18.0 optionalDependencies: - '@rsbuild/core': 2.0.1 + '@rsbuild/core': 2.0.14 transitivePeerDependencies: - '@rspack/core' - '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23))': + '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.8)(@rspack/core@2.0.8(@swc/helpers@0.5.23))': dependencies: - '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(react-refresh@0.18.0) + '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.8(@swc/helpers@0.5.23))(react-refresh@0.18.0) react-refresh: 0.18.0 optionalDependencies: '@rsbuild/core': 2.0.8 transitivePeerDependencies: - '@rspack/core' - '@rsbuild/plugin-solid@2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.1)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15)': + '@rsbuild/plugin-solid@2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.14)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15)': dependencies: - '@rsbuild/plugin-babel': 1.1.2(@rsbuild/core@2.0.1) + '@rsbuild/plugin-babel': 1.1.2(@rsbuild/core@2.0.14) '@solidjs/web': 2.0.0-beta.15(solid-js@2.0.0-beta.15) babel-preset-solid: 2.0.0-beta.15(@babel/core@7.29.0)(solid-js@2.0.0-beta.15) solid-js: 2.0.0-beta.15 solid-refresh: 0.8.0-next.7(solid-js@2.0.0-beta.15) optionalDependencies: - '@rsbuild/core': 2.0.1 + '@rsbuild/core': 2.0.14 transitivePeerDependencies: - '@babel/core' - supports-color - '@rsbuild/plugin-solid@2.0.0-beta.0(@babel/core@7.29.0)(@rsbuild/core@2.0.8)(@solidjs/web@2.0.0-beta.15(solid-js@2.0.0-beta.15))(solid-js@2.0.0-beta.15)': + '@rsbuild/plugin-vue-jsx@1.1.1(@babel/core@7.29.0)(@rsbuild/core@2.0.14)': dependencies: - '@rsbuild/plugin-babel': 1.1.2(@rsbuild/core@2.0.8) - '@solidjs/web': 2.0.0-beta.15(solid-js@2.0.0-beta.15) - babel-preset-solid: 2.0.0-beta.15(@babel/core@7.29.0)(solid-js@2.0.0-beta.15) - solid-js: 2.0.0-beta.15 - solid-refresh: 0.8.0-next.7(solid-js@2.0.0-beta.15) - optionalDependencies: - '@rsbuild/core': 2.0.8 - transitivePeerDependencies: - - '@babel/core' - - supports-color - - '@rsbuild/plugin-vue-jsx@1.1.1(@babel/core@7.29.0)(@rsbuild/core@2.0.8)': - dependencies: - '@rsbuild/plugin-babel': 1.1.2(@rsbuild/core@2.0.8) + '@rsbuild/plugin-babel': 1.1.2(@rsbuild/core@2.0.14) '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) babel-plugin-vue-jsx-hmr: 1.0.0 optionalDependencies: - '@rsbuild/core': 2.0.8 + '@rsbuild/core': 2.0.14 transitivePeerDependencies: - '@babel/core' - supports-color - '@rsbuild/plugin-vue-jsx@2.0.0(@babel/core@7.29.0)(@rsbuild/core@2.0.8)': + '@rsbuild/plugin-vue-jsx@2.0.0(@babel/core@7.29.0)(@rsbuild/core@2.0.14)': dependencies: - '@rsbuild/plugin-babel': 1.1.2(@rsbuild/core@2.0.8) + '@rsbuild/plugin-babel': 1.1.2(@rsbuild/core@2.0.14) '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0) babel-plugin-vue-jsx-hmr: 1.0.0 optionalDependencies: - '@rsbuild/core': 2.0.8 + '@rsbuild/core': 2.0.14 transitivePeerDependencies: - '@babel/core' - supports-color - '@rsbuild/plugin-vue@1.2.2(@rsbuild/core@2.0.8)(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.27.4)(vue@3.5.25(typescript@6.0.2))': + '@rsbuild/plugin-vue@1.2.2(@rsbuild/core@2.0.14)(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.27.4)(vue@3.5.25(typescript@6.0.2))': dependencies: - '@rsbuild/core': 2.0.8 + '@rsbuild/core': 2.0.14 rspack-vue-loader: 17.4.4(vue@3.5.25(typescript@6.0.2))(webpack@5.104.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.27.4)) webpack: 5.104.0(@swc/core@1.15.33(@swc/helpers@0.5.23))(esbuild@0.27.4) transitivePeerDependencies: @@ -31997,11 +32589,11 @@ snapshots: - vue - webpack-cli - '@rsbuild/plugin-vue@1.2.7(@rsbuild/core@2.0.8)(@rspack/core@2.0.5(@swc/helpers@0.5.23))(vue@3.5.25(typescript@6.0.2))': + '@rsbuild/plugin-vue@1.2.7(@rsbuild/core@2.0.14)(@rspack/core@2.0.8(@swc/helpers@0.5.23))(vue@3.5.25(typescript@6.0.2))': dependencies: - rspack-vue-loader: 17.5.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(vue@3.5.25(typescript@6.0.2)) + rspack-vue-loader: 17.5.0(@rspack/core@2.0.8(@swc/helpers@0.5.23))(vue@3.5.25(typescript@6.0.2)) optionalDependencies: - '@rsbuild/core': 2.0.8 + '@rsbuild/core': 2.0.14 transitivePeerDependencies: - '@rspack/core' - '@vue/compiler-sfc' @@ -32013,36 +32605,54 @@ snapshots: '@rspack/binding-darwin-arm64@2.0.5': optional: true + '@rspack/binding-darwin-arm64@2.0.8': + optional: true + '@rspack/binding-darwin-x64@2.0.0': optional: true '@rspack/binding-darwin-x64@2.0.5': optional: true + '@rspack/binding-darwin-x64@2.0.8': + optional: true + '@rspack/binding-linux-arm64-gnu@2.0.0': optional: true '@rspack/binding-linux-arm64-gnu@2.0.5': optional: true + '@rspack/binding-linux-arm64-gnu@2.0.8': + optional: true + '@rspack/binding-linux-arm64-musl@2.0.0': optional: true '@rspack/binding-linux-arm64-musl@2.0.5': optional: true + '@rspack/binding-linux-arm64-musl@2.0.8': + optional: true + '@rspack/binding-linux-x64-gnu@2.0.0': optional: true '@rspack/binding-linux-x64-gnu@2.0.5': optional: true + '@rspack/binding-linux-x64-gnu@2.0.8': + optional: true + '@rspack/binding-linux-x64-musl@2.0.0': optional: true '@rspack/binding-linux-x64-musl@2.0.5': optional: true + '@rspack/binding-linux-x64-musl@2.0.8': + optional: true + '@rspack/binding-wasm32-wasi@2.0.0': dependencies: '@emnapi/core': 1.10.0 @@ -32057,24 +32667,40 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true + '@rspack/binding-wasm32-wasi@2.0.8': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + '@rspack/binding-win32-arm64-msvc@2.0.0': optional: true '@rspack/binding-win32-arm64-msvc@2.0.5': optional: true + '@rspack/binding-win32-arm64-msvc@2.0.8': + optional: true + '@rspack/binding-win32-ia32-msvc@2.0.0': optional: true '@rspack/binding-win32-ia32-msvc@2.0.5': optional: true + '@rspack/binding-win32-ia32-msvc@2.0.8': + optional: true + '@rspack/binding-win32-x64-msvc@2.0.0': optional: true '@rspack/binding-win32-x64-msvc@2.0.5': optional: true + '@rspack/binding-win32-x64-msvc@2.0.8': + optional: true + '@rspack/binding@2.0.0': optionalDependencies: '@rspack/binding-darwin-arm64': 2.0.0 @@ -32101,6 +32727,19 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 2.0.5 '@rspack/binding-win32-x64-msvc': 2.0.5 + '@rspack/binding@2.0.8': + optionalDependencies: + '@rspack/binding-darwin-arm64': 2.0.8 + '@rspack/binding-darwin-x64': 2.0.8 + '@rspack/binding-linux-arm64-gnu': 2.0.8 + '@rspack/binding-linux-arm64-musl': 2.0.8 + '@rspack/binding-linux-x64-gnu': 2.0.8 + '@rspack/binding-linux-x64-musl': 2.0.8 + '@rspack/binding-wasm32-wasi': 2.0.8 + '@rspack/binding-win32-arm64-msvc': 2.0.8 + '@rspack/binding-win32-ia32-msvc': 2.0.8 + '@rspack/binding-win32-x64-msvc': 2.0.8 + '@rspack/core@2.0.0(@swc/helpers@0.5.21)': dependencies: '@rspack/binding': 2.0.0 @@ -32113,32 +32752,25 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.23 - '@rspack/core@2.0.5(@swc/helpers@0.5.21)': + '@rspack/core@2.0.5(@swc/helpers@0.5.23)': dependencies: '@rspack/binding': 2.0.5 optionalDependencies: - '@swc/helpers': 0.5.21 - optional: true + '@swc/helpers': 0.5.23 - '@rspack/core@2.0.5(@swc/helpers@0.5.23)': + '@rspack/core@2.0.8(@swc/helpers@0.5.23)': dependencies: - '@rspack/binding': 2.0.5 + '@rspack/binding': 2.0.8 optionalDependencies: '@swc/helpers': 0.5.23 '@rspack/lite-tapable@1.1.0': {} - '@rspack/plugin-react-refresh@2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.21))(react-refresh@0.18.0)': + '@rspack/plugin-react-refresh@2.0.0(@rspack/core@2.0.8(@swc/helpers@0.5.23))(react-refresh@0.18.0)': dependencies: react-refresh: 0.18.0 optionalDependencies: - '@rspack/core': 2.0.5(@swc/helpers@0.5.21) - - '@rspack/plugin-react-refresh@2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(react-refresh@0.18.0)': - dependencies: - react-refresh: 0.18.0 - optionalDependencies: - '@rspack/core': 2.0.5(@swc/helpers@0.5.23) + '@rspack/core': 2.0.8(@swc/helpers@0.5.23) '@rushstack/node-core-library@5.7.0(@types/node@25.0.9)': dependencies: @@ -32314,19 +32946,6 @@ snapshots: optionalDependencies: '@tanstack/vue-router': link:packages/vue-router - '@simplewebauthn/browser@13.2.2': {} - - '@simplewebauthn/server@13.2.2': - dependencies: - '@hexagon/base64': 1.1.28 - '@levischuck/tiny-cbor': 0.2.11 - '@peculiar/asn1-android': 2.5.0 - '@peculiar/asn1-ecc': 2.5.0 - '@peculiar/asn1-rsa': 2.5.0 - '@peculiar/asn1-schema': 2.5.0 - '@peculiar/asn1-x509': 2.5.0 - '@peculiar/x509': 1.14.0 - '@sinclair/typebox@0.31.28': {} '@sindresorhus/is@4.6.0': {} @@ -33101,6 +33720,11 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@tybys/wasm-util@0.9.0': dependencies: tslib: 2.8.1 @@ -33624,7 +34248,7 @@ snapshots: fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.9 - semver: 7.7.2 + semver: 7.8.2 ts-api-utils: 2.4.0(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: @@ -33732,7 +34356,7 @@ snapshots: debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-api-utils: 2.4.0(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: @@ -34231,7 +34855,7 @@ snapshots: '@vue/compiler-core@3.5.25': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 '@vue/shared': 3.5.25 entities: 4.5.0 estree-walker: 2.0.2 @@ -34244,14 +34868,14 @@ snapshots: '@vue/compiler-sfc@3.5.25': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 '@vue/compiler-core': 3.5.25 '@vue/compiler-dom': 3.5.25 '@vue/compiler-ssr': 3.5.25 '@vue/shared': 3.5.25 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.8 + postcss: 8.5.15 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.25': @@ -34643,8 +35267,6 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} - ansi-regex@6.2.2: {} ansi-styles@4.3.0: @@ -34780,7 +35402,7 @@ snapshots: aws-ssl-profiles@1.1.2: {} - axios@1.16.0: + axios@1.16.0(debug@4.4.3): dependencies: follow-redirects: 1.16.0(debug@4.4.3) form-data: 4.0.5 @@ -34788,7 +35410,7 @@ snapshots: transitivePeerDependencies: - debug - axios@1.17.0: + axios@1.17.0(debug@4.4.3): dependencies: follow-redirects: 1.16.0(debug@4.4.3) form-data: 4.0.5 @@ -34824,6 +35446,15 @@ snapshots: html-entities: 2.3.3 parse5: 7.3.0 + babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) + '@babel/types': 7.29.0 + html-entities: 2.3.3 + parse5: 7.3.0 + babel-plugin-jsx-dom-expressions@0.50.0-next.14(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -34871,6 +35502,13 @@ snapshots: optionalDependencies: solid-js: 2.0.0-beta.15 + babel-preset-solid@1.9.10(@babel/core@7.29.0)(solid-js@2.0.0-beta.15): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jsx-dom-expressions: 0.40.3(@babel/core@7.29.0) + optionalDependencies: + solid-js: 2.0.0-beta.15 + babel-preset-solid@2.0.0-beta.15(@babel/core@7.28.5)(solid-js@2.0.0-beta.15): dependencies: '@babel/core': 7.28.5 @@ -34936,34 +35574,47 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - better-auth@1.3.27(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@2.0.0-beta.15)(vue@3.5.25(typescript@6.0.2)): - dependencies: - '@better-auth/core': 1.3.27 - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - '@noble/ciphers': 2.0.1 + better-auth@1.6.19(@prisma/client@7.0.0(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(typescript@6.0.2))(@tanstack/solid-start@packages+solid-start)(mysql2@3.15.3)(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@2.0.0-beta.15)(vitest@4.1.4)(vue@3.5.25(typescript@6.0.2)): + dependencies: + '@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0) + '@better-auth/drizzle-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/kysely-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.29.2) + '@better-auth/memory-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@prisma/client@7.0.0(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(typescript@6.0.2))(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)) + '@better-auth/telemetry': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.3.6(zod@4.4.3))(jose@6.1.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + '@noble/ciphers': 2.2.0 '@noble/hashes': 2.0.1 - '@simplewebauthn/browser': 13.2.2 - '@simplewebauthn/server': 13.2.2 - better-call: 1.0.19 + better-call: 1.3.6(zod@4.4.3) defu: 6.1.4 - jose: 6.1.0 - kysely: 0.28.8 - nanostores: 1.0.1 + jose: 6.1.3 + kysely: 0.29.2 + nanostores: 1.3.0 zod: 4.4.3 optionalDependencies: + '@prisma/client': 7.0.0(prisma@7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(typescript@6.0.2) + '@tanstack/solid-start': link:packages/solid-start + mysql2: 3.15.3 + prisma: 7.0.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) solid-js: 2.0.0-beta.15 + vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) vue: 3.5.25(typescript@6.0.2) + transitivePeerDependencies: + - '@cloudflare/workers-types' + - '@opentelemetry/api' - better-call@1.0.19: + better-call@1.3.6(zod@4.4.3): dependencies: - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - rou3: 0.5.1 - set-cookie-parser: 2.7.2 - uncrypto: 0.1.3 + '@better-auth/utils': 0.4.2 + '@better-fetch/fetch': 1.3.1 + rou3: 0.7.12 + set-cookie-parser: 3.1.0 + optionalDependencies: + zod: 4.4.3 better-path-resolve@1.0.0: dependencies: @@ -35164,8 +35815,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.4.1: {} - chalk@5.6.2: {} char-regex@1.0.2: {} @@ -35560,9 +36209,9 @@ snapshots: optionalDependencies: srvx: 0.11.15 - crossws@0.4.5(srvx@0.11.15): + crossws@0.4.9(srvx@0.11.20): optionalDependencies: - srvx: 0.11.15 + srvx: 0.11.20 css-loader@7.1.2(webpack@5.97.1): dependencies: @@ -35603,6 +36252,11 @@ snapshots: mdn-data: 2.12.2 source-map-js: 1.2.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css-what@6.1.0: {} css.escape@1.5.1: {} @@ -35644,6 +36298,13 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 15.1.0 + data-urls@7.0.0(@noble/hashes@2.0.1): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.0.1) + transitivePeerDependencies: + - '@noble/hashes' + dataloader@1.4.0: {} date-fns@2.30.0: @@ -35672,6 +36333,8 @@ snapshots: decimal.js@10.5.0: {} + decimal.js@10.6.0: {} + dedent@1.5.1(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 @@ -35995,10 +36658,21 @@ snapshots: entities@6.0.0: {} + entities@8.0.0: {} + env-paths@2.2.1: {} env-paths@3.0.0: {} + env-runner@0.1.16(wrangler@4.75.0): + dependencies: + crossws: 0.4.9(srvx@0.11.20) + exsolve: 1.1.0 + httpxy: 0.5.4 + srvx: 0.11.20 + optionalDependencies: + wrangler: 4.75.0 + env-runner@0.1.6(miniflare@4.20260317.0): dependencies: crossws: 0.4.4(srvx@0.11.15) @@ -36007,13 +36681,6 @@ snapshots: optionalDependencies: miniflare: 4.20260317.0 - env-runner@0.1.9: - dependencies: - crossws: 0.4.5(srvx@0.11.15) - exsolve: 1.0.8 - httpxy: 0.5.3 - srvx: 0.11.15 - envinfo@7.14.0: {} environment@1.1.0: {} @@ -36661,6 +37328,8 @@ snapshots: exsolve@1.0.8: {} + exsolve@1.1.0: {} + extendable-error@0.1.7: {} extract-zip@2.0.1: @@ -36765,6 +37434,8 @@ snapshots: fflate@0.8.2: {} + fflate@0.8.3: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -37125,19 +37796,19 @@ snapshots: optionalDependencies: crossws: 0.4.4(srvx@0.11.15) - h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)): + h3@2.0.1-rc.20(crossws@0.4.9(srvx@0.11.20)): dependencies: rou3: 0.8.1 srvx: 0.11.15 optionalDependencies: - crossws: 0.4.5(srvx@0.11.15) + crossws: 0.4.9(srvx@0.11.20) - h3@2.0.1-rc.22(crossws@0.4.5(srvx@0.11.15)): + h3@2.0.1-rc.22(crossws@0.4.9(srvx@0.11.20)): dependencies: rou3: 0.8.1 srvx: 0.11.15 optionalDependencies: - crossws: 0.4.5(srvx@0.11.15) + crossws: 0.4.9(srvx@0.11.20) handle-thing@2.0.1: {} @@ -37210,6 +37881,12 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1) + transitivePeerDependencies: + - '@noble/hashes' + html-entities@2.3.3: {} html-link-extractor@1.0.5: @@ -37339,7 +38016,7 @@ snapshots: httpxy@0.3.1: {} - httpxy@0.5.3: {} + httpxy@0.5.4: {} human-id@4.1.1: {} @@ -37454,7 +38131,7 @@ snapshots: pathe: 2.0.3 sharp: 0.34.4 svgo: 4.0.0 - ufo: 1.6.1 + ufo: 1.6.3 unstorage: 1.17.4(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2) xss: 1.0.15 transitivePeerDependencies: @@ -37786,6 +38463,32 @@ snapshots: - supports-color - utf-8-validate + jsdom@29.1.1(@noble/hashes@2.0.1): + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) + '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1) + css-tree: 3.2.1 + data-urls: 7.0.0(@noble/hashes@2.0.1) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1) + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.27.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.0.1) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -37866,7 +38569,7 @@ snapshots: kysely@0.27.6: {} - kysely@0.28.8: {} + kysely@0.29.2: {} lambda-local@2.2.0: dependencies: @@ -38087,6 +38790,8 @@ snapshots: lru-cache@11.2.2: {} + lru-cache@11.5.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -38129,8 +38834,8 @@ snapshots: marked-terminal@7.3.0(marked@9.1.6): dependencies: ansi-escapes: 7.0.0 - ansi-regex: 6.1.0 - chalk: 5.4.1 + ansi-regex: 6.2.2 + chalk: 5.6.2 cli-highlight: 2.1.11 cli-table3: 0.6.5 marked: 9.1.6 @@ -38147,6 +38852,8 @@ snapshots: mdn-data@2.12.2: {} + mdn-data@2.27.1: {} + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -38368,7 +39075,7 @@ snapshots: nanoid@3.3.12: {} - nanostores@1.0.1: {} + nanostores@1.3.0: {} napi-postinstall@0.3.3: {} @@ -38386,26 +39093,26 @@ snapshots: nf3@0.3.11: {} - nf3@0.3.17: {} + nf3@0.3.19: {} nf3@0.3.6: {} - nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + nitro-nightly@3.0.1-20260702-173353-03122331(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(wrangler@4.75.0): dependencies: consola: 3.4.2 - crossws: 0.4.5(srvx@0.11.15) + crossws: 0.4.9(srvx@0.11.20) db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) - env-runner: 0.1.9 - h3: 2.0.1-rc.22(crossws@0.4.5(srvx@0.11.15)) + env-runner: 0.1.16(wrangler@4.75.0) + h3: 2.0.1-rc.22(crossws@0.4.9(srvx@0.11.20)) hookable: 6.1.1 - nf3: 0.3.17 - ocache: 0.1.4 + nf3: 0.3.19 + ocache: 0.1.5 ofetch: 2.0.0-alpha.3 ohash: 2.0.11 - rolldown: 1.0.2 - srvx: 0.11.15 + rolldown: 1.1.4 + srvx: 0.11.20 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.4.2 giget: 2.0.0 @@ -38441,8 +39148,9 @@ snapshots: - mysql2 - sqlite3 - uploadthing + - wrangler - nitro@3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + nitro@3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.5.1)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.3(srvx@0.10.1) @@ -38457,7 +39165,7 @@ snapshots: srvx: 0.10.1 undici: 7.24.4 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) optionalDependencies: rolldown: 1.0.2 rollup: 4.56.0 @@ -38493,7 +39201,7 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + nitro@3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.4(srvx@0.11.15) @@ -38508,7 +39216,60 @@ snapshots: rolldown: 1.0.0-rc.9(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) srvx: 0.11.15 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) + optionalDependencies: + dotenv: 17.4.2 + giget: 2.0.0 + jiti: 2.7.0 + vite: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@emnapi/core' + - '@emnapi/runtime' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - better-sqlite3 + - chokidar + - drizzle-orm + - idb-keyval + - ioredis + - lru-cache + - miniflare + - mongodb + - mysql2 + - sqlite3 + - uploadthing + + nitro@3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + dependencies: + consola: 3.4.2 + crossws: 0.4.4(srvx@0.11.15) + db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) + env-runner: 0.1.6(miniflare@4.20260317.0) + h3: 2.0.1-rc.16(crossws@0.4.4(srvx@0.11.15)) + hookable: 6.1.0 + nf3: 0.3.11 + ocache: 0.1.2 + ofetch: 2.0.0-alpha.3 + ohash: 2.0.11 + rolldown: 1.0.0-rc.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + srvx: 0.11.15 + unenv: 2.0.0-rc.24 + unstorage: 2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.4.2 giget: 2.0.0 @@ -38690,7 +39451,7 @@ snapshots: node-source-walk@7.0.1: dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 node-stream-zip@1.15.0: {} @@ -38728,7 +39489,7 @@ snapshots: nwsapi@2.2.16: {} - nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23)): + nx@22.7.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.33(@swc/helpers@0.5.23))(debug@4.4.3): dependencies: '@emnapi/core': 1.4.5 '@emnapi/runtime': 1.4.5 @@ -38743,7 +39504,7 @@ snapshots: ansi-styles: 4.3.0 argparse: 2.0.1 asynckit: 0.4.0 - axios: 1.16.0 + axios: 1.16.0(debug@4.4.3) balanced-match: 4.0.3 base64-js: 1.5.1 bl: 4.1.0 @@ -38894,7 +39655,7 @@ snapshots: dependencies: ohash: 2.0.11 - ocache@0.1.4: + ocache@0.1.5: dependencies: ohash: 2.0.11 @@ -39163,6 +39924,10 @@ snapshots: dependencies: entities: 6.0.0 + parse5@8.0.1: + dependencies: + entities: 8.0.0 + parseurl@1.3.3: {} pascal-case@3.1.2: @@ -39926,6 +40691,30 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + rolldown@1.0.0-rc.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): + dependencies: + '@oxc-project/types': 0.115.0 + '@rolldown/pluginutils': 1.0.0-rc.9 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-x64': 1.0.0-rc.9 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + rolldown@1.0.2: dependencies: '@oxc-project/types': 0.132.0 @@ -39947,6 +40736,27 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.2 '@rolldown/binding-win32-x64-msvc': 1.0.2 + rolldown@1.1.4: + dependencies: + '@oxc-project/types': 0.138.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.4 + '@rolldown/binding-darwin-arm64': 1.1.4 + '@rolldown/binding-darwin-x64': 1.1.4 + '@rolldown/binding-freebsd-x64': 1.1.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 + '@rolldown/binding-linux-arm64-gnu': 1.1.4 + '@rolldown/binding-linux-arm64-musl': 1.1.4 + '@rolldown/binding-linux-ppc64-gnu': 1.1.4 + '@rolldown/binding-linux-s390x-gnu': 1.1.4 + '@rolldown/binding-linux-x64-gnu': 1.1.4 + '@rolldown/binding-linux-x64-musl': 1.1.4 + '@rolldown/binding-openharmony-arm64': 1.1.4 + '@rolldown/binding-wasm32-wasi': 1.1.4 + '@rolldown/binding-win32-arm64-msvc': 1.1.4 + '@rolldown/binding-win32-x64-msvc': 1.1.4 + rollup-plugin-visualizer@6.0.5(rolldown@1.0.2)(rollup@4.56.0): dependencies: open: 8.4.2 @@ -39988,8 +40798,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.56.0 fsevents: 2.3.3 - rou3@0.5.1: {} - rou3@0.7.12: {} rou3@0.8.1: {} @@ -40016,12 +40824,12 @@ snapshots: optionalDependencies: vue: 3.5.25(typescript@6.0.2) - rspack-vue-loader@17.5.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(vue@3.5.25(typescript@6.0.2)): + rspack-vue-loader@17.5.0(@rspack/core@2.0.8(@swc/helpers@0.5.23))(vue@3.5.25(typescript@6.0.2)): dependencies: '@rspack/lite-tapable': 1.1.0 chalk: 4.1.2 optionalDependencies: - '@rspack/core': 2.0.5(@swc/helpers@0.5.23) + '@rspack/core': 2.0.8(@swc/helpers@0.5.23) vue: 3.5.25(typescript@6.0.2) run-applescript@7.0.0: {} @@ -40203,6 +41011,8 @@ snapshots: set-cookie-parser@2.7.2: {} + set-cookie-parser@3.1.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -40384,8 +41194,8 @@ snapshots: solid-refresh@0.6.3(solid-js@1.9.12): dependencies: - '@babel/generator': 7.28.5 - '@babel/helper-module-imports': 7.27.1 + '@babel/generator': 7.29.1 + '@babel/helper-module-imports': 7.28.6 '@babel/types': 7.29.0 solid-js: 1.9.12 transitivePeerDependencies: @@ -40393,8 +41203,8 @@ snapshots: solid-refresh@0.6.3(solid-js@2.0.0-beta.15): dependencies: - '@babel/generator': 7.28.5 - '@babel/helper-module-imports': 7.27.1 + '@babel/generator': 7.29.1 + '@babel/helper-module-imports': 7.28.6 '@babel/types': 7.29.0 solid-js: 2.0.0-beta.15 transitivePeerDependencies: @@ -40484,6 +41294,8 @@ snapshots: srvx@0.11.15: {} + srvx@0.11.20: {} + stable-hash-x@0.2.0: {} stack-trace@0.0.10: {} @@ -40824,6 +41636,10 @@ snapshots: dependencies: tldts: 7.0.16 + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.16 + tr46@0.0.3: {} tr46@5.0.0: @@ -40999,6 +41815,8 @@ snapshots: undici@7.24.4: {} + undici@7.27.2: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -41100,27 +41918,29 @@ snapshots: db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) ioredis: 5.9.2 - unstorage@2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): optionalDependencies: '@netlify/blobs': 10.1.0 chokidar: 5.0.0 db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) ioredis: 5.9.2 - lru-cache: 11.2.2 + lru-cache: 11.5.1 ofetch: 2.0.0-alpha.3 - unstorage@2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): optionalDependencies: '@netlify/blobs': 10.1.0 chokidar: 5.0.0 db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) + lru-cache: 11.5.1 ofetch: 2.0.0-alpha.3 - unstorage@2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): optionalDependencies: '@netlify/blobs': 10.1.0 chokidar: 5.0.0 db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) + lru-cache: 11.5.1 ofetch: 2.0.0-alpha.3 untun@0.1.3: @@ -41267,9 +42087,9 @@ snapshots: vite-plugin-solid@2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@2.0.0-beta.15)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 '@types/babel__core': 7.20.5 - babel-preset-solid: 1.9.10(@babel/core@7.28.5)(solid-js@2.0.0-beta.15) + babel-preset-solid: 1.9.10(@babel/core@7.29.0)(solid-js@2.0.0-beta.15) merge-anything: 5.1.7 solid-js: 2.0.0-beta.15 solid-refresh: 0.6.3(solid-js@2.0.0-beta.15) @@ -41367,10 +42187,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -41396,7 +42216,36 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.0.9 + '@vitest/ui': 4.1.4(vitest@4.1.4) + jsdom: 29.1.1(@noble/hashes@2.0.1) + transitivePeerDependencies: + - msw + + vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) @@ -41421,7 +42270,7 @@ snapshots: optionalDependencies: '@types/node': 25.0.9 '@vitest/ui': 4.1.4(vitest@4.1.4) - jsdom: 27.0.0(postcss@8.5.15) + jsdom: 29.1.1(@noble/hashes@2.0.1) transitivePeerDependencies: - msw @@ -41562,6 +42411,8 @@ snapshots: webidl-conversions@8.0.0: {} + webidl-conversions@8.0.1: {} + webpack-cli@5.1.4(webpack-dev-server@5.2.4)(webpack@5.97.1): dependencies: '@discoveryjs/json-ext': 0.5.7 @@ -41753,6 +42604,8 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} + whatwg-url@14.1.0: dependencies: tr46: 5.0.0 @@ -41763,6 +42616,14 @@ snapshots: tr46: 6.0.0 webidl-conversions: 8.0.0 + whatwg-url@16.0.1(@noble/hashes@2.0.1): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b31336ed2c..4cf4e4c0cb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ cleanupUnusedCatalogs: true +minimumReleaseAge: 1440 minimumReleaseAgeExclude: - solid-js - '@solidjs/*' @@ -9,9 +10,18 @@ preferWorkspacePackages: true blockExoticSubdeps: true trustPolicy: 'no-downgrade' +trustPolicyExclude: + - 'chokidar@4.0.3' # known to be safe, installed by sass, prisma, netlify... no planned releases in v4, packages is on v5 now + - 'semver@6.3.1' # installed by babel, ecosystem has been on v7 for 7 years now, but babel is horribly out of date, 6.3.1 is the latest v6 and was released 3 years ago + - 'undici@6.21.2' # installed by cheerio@1.0.0 -> html-link-extractor@1.0.5 -> markdown-link-extractor@4.0.3, is only 1 year old, v6, v7 and v8 are maintained in parallel + - '@netlify/edge-bundler@14.7.0' # existing Netlify dev transitive flagged by pnpm 11.9 trust downgrade checks + - '@netlify/serverless-functions-api@2.7.1' # existing Netlify dev transitive flagged by pnpm 11.9 trust downgrade checks + - '@netlify/zip-it-and-ship-it@14.1.11' # existing Netlify dev transitive flagged by pnpm 11.9 trust downgrade checks + packages: - 'packages/*' - 'benchmarks/*' + - 'benchmarks/memory/*' - 'examples/react/*' - 'examples/solid/*' - 'examples/vue/*' @@ -27,6 +37,20 @@ packages: - 'e2e/vue-start/*' - 'e2e/eslint-plugin-start' +catalog: + react: ^19.2.3 + react-dom: ^19.2.3 + '@types/react': ^19.2.8 + '@types/react-dom': ^19.2.3 + eslint: ^9.22.0 + vite: ^8.0.14 + '@types/node': 25.0.9 + '@playwright/test': ^1.57.0 + '@tanstack/query-core': ^5.99.0 + '@tanstack/react-query': ^5.99.0 + '@tanstack/solid-query': ^6.0.0-beta.5 + '@tanstack/vue-query': ^5.99.0 + overrides: '@types/babel__traverse': '^7.28.0' vite-plugin-dts: 4.2.3 @@ -36,18 +60,18 @@ overrides: # @rsbuild/plugin-solid hard-pins babel-preset-solid to an older beta; align # it with the rest of the Solid v2 toolchain. '@rsbuild/plugin-solid>babel-preset-solid': 2.0.0-beta.15 - react: $react - react-dom: $react-dom - '@types/react': $@types/react - '@types/react-dom': $@types/react-dom - eslint: $eslint - vite: $vite - '@types/node': $@types/node - '@playwright/test': $@playwright/test - '@tanstack/query-core': $@tanstack/query-core - '@tanstack/react-query': $@tanstack/react-query - '@tanstack/solid-query': $@tanstack/solid-query - '@tanstack/vue-query': $@tanstack/vue-query + react: 'catalog:' + react-dom: 'catalog:' + '@types/react': 'catalog:' + '@types/react-dom': 'catalog:' + eslint: 'catalog:' + vite: 'catalog:' + '@types/node': 'catalog:' + '@playwright/test': 'catalog:' + '@tanstack/query-core': 'catalog:' + '@tanstack/react-query': 'catalog:' + '@tanstack/solid-query': 'catalog:' + '@tanstack/vue-query': 'catalog:' '@tanstack/history': 'workspace:*' '@tanstack/router-core': 'workspace:*' '@tanstack/react-router': 'workspace:*'