Skip to content

fix(router-generator): make buildRouteTree route ordering deterministic#7751

Open
0x80 wants to merge 3 commits into
TanStack:mainfrom
0x80:fix/deterministic-route-tree-sort
Open

fix(router-generator): make buildRouteTree route ordering deterministic#7751
0x80 wants to merge 3 commits into
TanStack:mainfrom
0x80:fix/deterministic-route-tree-sort

Conversation

@0x80

@0x80 0x80 commented Jul 7, 2026

Copy link
Copy Markdown

Fixes #7750.

Problem

buildRouteTree sorts route nodes with multiSortBy, whose final accessor was (d) => d — comparing whole route-node objects:

const sortedRouteNodes = multiSortBy(acc.routeNodes, [
  (d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),
  (d) => (d.routePath === undefined ? undefined : countSlashSeparatedParts(d.routePath)),
  (d) => { /* index-token check */ },
  (d) => d,            // <-- whole object
])

multiSortBy's comparator ends with return ao > bo ? 1 : -1. For two distinct route-node objects, both coerce to "[object Object]", so ao === bo is false and ao > bo is false → it returns -1 for both (a, b) and (b, a). That's an inconsistent comparator, so Array.prototype.sort leaves routes that tie on the earlier keys (same segment-count, same index-token status — e.g. /account/beta-features vs /account/busy-guard) in an order that depends on the engine's sort implementation, the input order, and the array size. The result: routeTree.gen.ts reorders non-deterministically across machines and Node versions, dirtying committed route trees with semantically-identical churn.

Fix

Change the final tiebreaker from (d) => d to (d) => d.routePath, mirroring the pre-sort earlier in the same file (which already ends with (d) => d.routePath). routePath is unique per node, so this gives a total order and makes generation byte-identical regardless of Node version or route-set composition.

A changeset is included (@tanstack/router-generator, patch).

Tests

The buildRouteTree tiebreaker sort is extracted into an exported pure sortRouteNodes(routeNodes, indexTokenSegmentRegex) helper (behavior-identical to the previous inline multiSortBy call) so it can be unit-tested directly. New tests in router-generator/tests/utils.test.ts:

  • Stable ordering — feeds a route set that ties on every key before routePath (e.g. /account/beta-features vs /account/busy-guard, /about vs /posts) through every input permutation and asserts identical output, proving the sort is independent of input order, engine sort implementation, and array size. This test fails against the old (d) => d tiebreaker and passes against (d) => d.routePath.
  • Tiebreaker + precedence — asserts tied siblings order by routePath, and that root / segment-count / index-token precedence still apply ahead of the tiebreaker.

Regenerated snapshots

The deterministic tiebreaker changes the order in which route nodes are emitted, so the 38 committed routeTree.snapshot.* files under tests/generator/ are regenerated. This is expected: the previous snapshots captured the arbitrary order the inconsistent comparator happened to produce on the machine that generated them. The buggy sort was stable run-to-run for a fixed input order and engine, so CI kept passing — the non-determinism only appeared across machines/Node versions (issue #7750).

The snapshot diffs are pure emission-order reordering: every route's id, path, and parentRoute is unchanged, and the route-tree structure (built from acc.routeTree, not the sorted list) is untouched — only the order of imports and route declarations shifts. In other words, the regenerated snapshots are exactly the semantically-identical churn this fix eliminates going forward, applied once.

Summary by CodeRabbit

  • Bug Fixes
    • Improved route tree generation to use a stable, deterministic ordering for routes that previously could tie, preventing environment-dependent output differences.
    • Reduces unexpected churn in generated route files and related diffs.
  • New Features
    • Added and exposed a public helper to deterministically sort route nodes.
  • Tests
    • Added coverage to verify deterministic sorting behavior across many input permutations.
    • Updated generated route snapshots to reflect the new stable ordering.

The final sort tiebreaker in buildRouteTree compared whole route-node
objects ((d) => d). Two objects both coerce to "[object Object]", so the
comparator returns -1 for both (a, b) and (b, a) — an inconsistent
comparator. Routes tying on segment-count and index-token were then left
in an engine- and input-order-dependent order, producing
non-deterministic routeTree.gen.ts diffs across machines and Node
versions. Compare routePath instead, mirroring the pre-sort's final
accessor, for a stable total order.
Copilot AI review requested due to automatic review settings July 7, 2026 20:05
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f98ea490-d4f9-4333-9f67-7e4b7a55cf20

📥 Commits

Reviewing files that changed from the base of the PR and between 85b7e95 and e83e34b.

📒 Files selected for processing (1)
  • packages/router-generator/tests/utils.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router-generator/tests/utils.test.ts

📝 Walkthrough

Walkthrough

The route-tree generator now uses a shared sort helper with a routePath tiebreaker. The package export surface and tests were updated, and many generated route-tree snapshots were reordered to match the new deterministic output.

Changes

Deterministic Route Tree Sort

Layer / File(s) Summary
Helper-based route ordering
packages/router-generator/src/utils.ts, packages/router-generator/src/generator.ts, packages/router-generator/src/index.ts, .changeset/deterministic-route-tree-sort.md
sortRouteNodes is added and exported, buildRouteTree delegates sorting to it, and the changeset documents the deterministic ordering fix.
Determinism coverage and regenerated snapshots
packages/router-generator/tests/utils.test.ts, packages/router-generator/tests/generator/*/routeTree.snapshot.ts
sortRouteNodes gets permutation and ordering tests, and the generated route-tree snapshots are reordered to match the stable sort output.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Suggested labels: package: router-generator

Suggested reviewers: schiller-manuel, Sheraff

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: making router-generator route ordering deterministic in buildRouteTree.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes non-deterministic route ordering during route-tree generation in @tanstack/router-generator by ensuring buildRouteTree uses a consistent, total ordering when sorting route nodes. This prevents semantically-identical routeTree.gen.ts churn across different Node/V8 versions and machines.

Changes:

  • Replace the final multiSortBy tiebreaker in buildRouteTree from comparing whole route-node objects to comparing routePath.
  • Ensure sibling routes that tie on earlier sort keys still sort deterministically by a stable string key.
  • Add a patch changeset for @tanstack/router-generator documenting the determinism fix.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
packages/router-generator/src/generator.ts Fixes buildRouteTree sorting by using routePath as the final tiebreaker for deterministic generation.
.changeset/deterministic-route-tree-sort.md Adds a patch changeset describing the deterministic route ordering fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@schiller-manuel

Copy link
Copy Markdown
Collaborator

can you please add unit tests that prove stable sorting?

…apshots

Extract the buildRouteTree tiebreaker sort into an exported pure
`sortRouteNodes` helper and cover it with unit tests. The key test feeds a
route set that ties on every key before `routePath` through all input
permutations and asserts identical output, proving the sort is order- and
engine-independent.

Regenerate the 38 generator snapshots whose ordering changes under the
deterministic tiebreaker. The diffs are pure emission-order reordering
(every route's id/path/parentRoute is unchanged) -- the same
semantically-identical churn the fix eliminates going forward.
@0x80

0x80 commented Jul 8, 2026

Copy link
Copy Markdown
Author

@schiller-manuel added — pushed in 85b7e95.

To make the sort testable I pulled the buildRouteTree tiebreaker out into an exported pure sortRouteNodes(routeNodes, indexTokenSegmentRegex) (behavior-identical to the old inline multiSortBy call) and covered it in router-generator/tests/utils.test.ts. The stable-sort test builds a route set that ties on every key before routePath (/account/beta-features vs /account/busy-guard, /about vs /posts, …), runs it through every input permutation, and asserts identical output — so it's independent of input order, engine sort implementation, and array size. It fails against the old (d) => d tiebreaker and passes against (d) => d.routePath. A second test pins the routePath tiebreak plus the root / segment-count / index-token precedence ahead of it.

Heads-up: the deterministic order also changes route emission order, so I had to regenerate the 38 routeTree.snapshot.* fixtures under tests/generator/. The diffs are pure reordering — every route's id/path/parentRoute and the tree structure are unchanged. The old snapshots captured the arbitrary order the inconsistent comparator produced on whatever machine generated them (stable run-to-run for a fixed input, which is why CI stayed green — the non-determinism only showed up across machines, #7750). More detail in the updated PR description.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/router-generator/tests/utils.test.ts`:
- Around line 982-984: The one-line `if` in `permutations<T>` violates the
project’s brace-required control-flow style. Update the `if (arr.length <= 1)`
branch in `permutations` to use curly braces around its body, keeping the same
return behavior while matching the coding guidelines for all `if`/`else`
statements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ceac5fba-5751-4ad3-8b46-5c6144b193f8

📥 Commits

Reviewing files that changed from the base of the PR and between 67049ac and 85b7e95.

📒 Files selected for processing (42)
  • packages/router-generator/src/generator.ts
  • packages/router-generator/src/index.ts
  • packages/router-generator/src/utils.ts
  • packages/router-generator/tests/generator/add-extensions-custom/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/custom-scaffolding/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/custom-tokens/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/dot-escaped/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/escaped-custom-tokens/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/escaped-special-strings/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/export-variations/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/file-modification/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/flat-route-group/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/flat/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/invalid-param-names/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/nested-layouts/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/nested-route-groups-with-layouts-before-physical/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/nested/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/numbers-in-path/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/physical-pathless-layout-escaped-underscore/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/prefix-suffix/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/regex-tokens-inline/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/regex-tokens-json/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/regex-tokens-plus-json/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/root-export-specifier/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/route-groups/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/routeFileIgnore/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/routeFilePrefix/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/single-level/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/types-disabled/routeTree.snapshot.js
  • packages/router-generator/tests/generator/virtual-config-file-default-export/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-config-file-named-export/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-config-file-tsconfig-paths/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-inside-nested/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-nested-layouts-with-virtual-route/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-pathless-layout-escaped-underscore/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-physical-layout-and-index/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-physical-no-prefix/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-with-escaped-underscore/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual/routeTree.snapshot.ts
  • packages/router-generator/tests/utils.test.ts
✅ Files skipped from review due to trivial changes (34)
  • packages/router-generator/tests/generator/escaped-custom-tokens/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/single-level/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/regex-tokens-inline/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/add-extensions-custom/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-with-escaped-underscore/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/types-disabled/routeTree.snapshot.js
  • packages/router-generator/tests/generator/export-variations/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/regex-tokens-json/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/custom-scaffolding/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/invalid-param-names/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/regex-tokens-plus-json/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-physical-layout-and-index/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-nested-layouts-with-virtual-route/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/routeFileIgnore/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/numbers-in-path/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/file-modification/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/flat-route-group/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/root-export-specifier/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/nested-route-groups-with-layouts-before-physical/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-config-file-default-export/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/escaped-special-strings/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-config-file-named-export/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/dot-escaped/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/flat/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-pathless-layout-escaped-underscore/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/virtual-config-file-tsconfig-paths/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/custom-tokens/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/nested/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/nested-layouts/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/physical-pathless-layout-escaped-underscore/routeTree.snapshot.ts
  • packages/router-generator/tests/generator/route-groups/routeTree.snapshot.ts

Comment thread packages/router-generator/tests/utils.test.ts Outdated
Follow the AGENTS.md control-statement convention (no one-line if bodies).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

buildRouteTree sorts sibling routes with an inconsistent comparator ((d) => d), producing non-deterministic routeTree.gen.ts ordering

3 participants