@tanstack/router-core 1.131.38 → 1.131.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,228 @@
1
+ import invariant from 'tiny-invariant'
2
+ import {
3
+ SEGMENT_TYPE_OPTIONAL_PARAM,
4
+ SEGMENT_TYPE_PARAM,
5
+ SEGMENT_TYPE_PATHNAME,
6
+ SEGMENT_TYPE_WILDCARD,
7
+ parsePathname,
8
+ trimPathLeft,
9
+ trimPathRight,
10
+ } from './path'
11
+ import type { Segment } from './path'
12
+ import type { RouteLike } from './route'
13
+
14
+ const REQUIRED_PARAM_BASE_SCORE = 0.5
15
+ const OPTIONAL_PARAM_BASE_SCORE = 0.4
16
+ const WILDCARD_PARAM_BASE_SCORE = 0.25
17
+ const BOTH_PRESENCE_BASE_SCORE = 0.05
18
+ const PREFIX_PRESENCE_BASE_SCORE = 0.02
19
+ const SUFFIX_PRESENCE_BASE_SCORE = 0.01
20
+ const PREFIX_LENGTH_SCORE_MULTIPLIER = 0.0002
21
+ const SUFFIX_LENGTH_SCORE_MULTIPLIER = 0.0001
22
+
23
+ function handleParam(segment: Segment, baseScore: number) {
24
+ if (segment.prefixSegment && segment.suffixSegment) {
25
+ return (
26
+ baseScore +
27
+ BOTH_PRESENCE_BASE_SCORE +
28
+ PREFIX_LENGTH_SCORE_MULTIPLIER * segment.prefixSegment.length +
29
+ SUFFIX_LENGTH_SCORE_MULTIPLIER * segment.suffixSegment.length
30
+ )
31
+ }
32
+
33
+ if (segment.prefixSegment) {
34
+ return (
35
+ baseScore +
36
+ PREFIX_PRESENCE_BASE_SCORE +
37
+ PREFIX_LENGTH_SCORE_MULTIPLIER * segment.prefixSegment.length
38
+ )
39
+ }
40
+
41
+ if (segment.suffixSegment) {
42
+ return (
43
+ baseScore +
44
+ SUFFIX_PRESENCE_BASE_SCORE +
45
+ SUFFIX_LENGTH_SCORE_MULTIPLIER * segment.suffixSegment.length
46
+ )
47
+ }
48
+
49
+ return baseScore
50
+ }
51
+
52
+ function sortRoutes<TRouteLike extends RouteLike>(
53
+ routes: ReadonlyArray<TRouteLike>,
54
+ ): Array<TRouteLike> {
55
+ const scoredRoutes: Array<{
56
+ child: TRouteLike
57
+ trimmed: string
58
+ parsed: ReadonlyArray<Segment>
59
+ index: number
60
+ scores: Array<number>
61
+ hasStaticAfter: boolean
62
+ optionalParamCount: number
63
+ }> = []
64
+
65
+ routes.forEach((d, i) => {
66
+ if (d.isRoot || !d.path) {
67
+ return
68
+ }
69
+
70
+ const trimmed = trimPathLeft(d.fullPath)
71
+ let parsed = parsePathname(trimmed)
72
+
73
+ // Removes the leading slash if it is not the only remaining segment
74
+ let skip = 0
75
+ while (parsed.length > skip + 1 && parsed[skip]?.value === '/') {
76
+ skip++
77
+ }
78
+ if (skip > 0) parsed = parsed.slice(skip)
79
+
80
+ let optionalParamCount = 0
81
+ let hasStaticAfter = false
82
+ const scores = parsed.map((segment, index) => {
83
+ if (segment.value === '/') {
84
+ return 0.75
85
+ }
86
+
87
+ let baseScore: number | undefined = undefined
88
+ if (segment.type === SEGMENT_TYPE_PARAM) {
89
+ baseScore = REQUIRED_PARAM_BASE_SCORE
90
+ } else if (segment.type === SEGMENT_TYPE_OPTIONAL_PARAM) {
91
+ baseScore = OPTIONAL_PARAM_BASE_SCORE
92
+ optionalParamCount++
93
+ } else if (segment.type === SEGMENT_TYPE_WILDCARD) {
94
+ baseScore = WILDCARD_PARAM_BASE_SCORE
95
+ }
96
+
97
+ if (baseScore) {
98
+ // if there is any static segment (that is not an index) after a required / optional param,
99
+ // we will boost this param so it ranks higher than a required/optional param without a static segment after it
100
+ // JUST FOR SORTING, NOT FOR MATCHING
101
+ for (let i = index + 1; i < parsed.length; i++) {
102
+ const nextSegment = parsed[i]!
103
+ if (
104
+ nextSegment.type === SEGMENT_TYPE_PATHNAME &&
105
+ nextSegment.value !== '/'
106
+ ) {
107
+ hasStaticAfter = true
108
+ return handleParam(segment, baseScore + 0.2)
109
+ }
110
+ }
111
+
112
+ return handleParam(segment, baseScore)
113
+ }
114
+
115
+ return 1
116
+ })
117
+
118
+ scoredRoutes.push({
119
+ child: d,
120
+ trimmed,
121
+ parsed,
122
+ index: i,
123
+ scores,
124
+ optionalParamCount,
125
+ hasStaticAfter,
126
+ })
127
+ })
128
+
129
+ const flatRoutes = scoredRoutes
130
+ .sort((a, b) => {
131
+ const minLength = Math.min(a.scores.length, b.scores.length)
132
+
133
+ // Sort by segment-by-segment score comparison ONLY for the common prefix
134
+ for (let i = 0; i < minLength; i++) {
135
+ if (a.scores[i] !== b.scores[i]) {
136
+ return b.scores[i]! - a.scores[i]!
137
+ }
138
+ }
139
+
140
+ // If all common segments have equal scores, then consider length and specificity
141
+ if (a.scores.length !== b.scores.length) {
142
+ // If different number of optional parameters, fewer optional parameters wins (more specific)
143
+ // only if both or none of the routes has static segments after the params
144
+ if (a.optionalParamCount !== b.optionalParamCount) {
145
+ if (a.hasStaticAfter === b.hasStaticAfter) {
146
+ return a.optionalParamCount - b.optionalParamCount
147
+ } else if (a.hasStaticAfter && !b.hasStaticAfter) {
148
+ return -1
149
+ } else if (!a.hasStaticAfter && b.hasStaticAfter) {
150
+ return 1
151
+ }
152
+ }
153
+
154
+ // If same number of optional parameters, longer path wins (for static segments)
155
+ return b.scores.length - a.scores.length
156
+ }
157
+
158
+ // Sort by min available parsed value for alphabetical ordering
159
+ for (let i = 0; i < minLength; i++) {
160
+ if (a.parsed[i]!.value !== b.parsed[i]!.value) {
161
+ return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
162
+ }
163
+ }
164
+
165
+ // Sort by original index
166
+ return a.index - b.index
167
+ })
168
+ .map((d, i) => {
169
+ d.child.rank = i
170
+ return d.child
171
+ })
172
+
173
+ return flatRoutes
174
+ }
175
+
176
+ export type ProcessRouteTreeResult<TRouteLike extends RouteLike> = {
177
+ routesById: Record<string, TRouteLike>
178
+ routesByPath: Record<string, TRouteLike>
179
+ flatRoutes: Array<TRouteLike>
180
+ }
181
+
182
+ export function processRouteTree<TRouteLike extends RouteLike>({
183
+ routeTree,
184
+ initRoute,
185
+ }: {
186
+ routeTree: TRouteLike
187
+ initRoute?: (route: TRouteLike, index: number) => void
188
+ }): ProcessRouteTreeResult<TRouteLike> {
189
+ const routesById = {} as Record<string, TRouteLike>
190
+ const routesByPath = {} as Record<string, TRouteLike>
191
+
192
+ const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
193
+ childRoutes.forEach((childRoute, i) => {
194
+ initRoute?.(childRoute, i)
195
+
196
+ const existingRoute = routesById[childRoute.id]
197
+
198
+ invariant(
199
+ !existingRoute,
200
+ `Duplicate routes found with id: ${String(childRoute.id)}`,
201
+ )
202
+
203
+ routesById[childRoute.id] = childRoute
204
+
205
+ if (!childRoute.isRoot && childRoute.path) {
206
+ const trimmedFullPath = trimPathRight(childRoute.fullPath)
207
+ if (
208
+ !routesByPath[trimmedFullPath] ||
209
+ childRoute.fullPath.endsWith('/')
210
+ ) {
211
+ routesByPath[trimmedFullPath] = childRoute
212
+ }
213
+ }
214
+
215
+ const children = childRoute.children as Array<TRouteLike>
216
+
217
+ if (children?.length) {
218
+ recurseRoutes(children)
219
+ }
220
+ })
221
+ }
222
+
223
+ recurseRoutes([routeTree])
224
+
225
+ const flatRoutes = sortRoutes(Object.values(routesById))
226
+
227
+ return { routesById, routesByPath, flatRoutes }
228
+ }
package/src/route.ts CHANGED
@@ -1749,3 +1749,16 @@ export class BaseRootRoute<
1749
1749
  }
1750
1750
 
1751
1751
  //
1752
+
1753
+ export interface RouteLike {
1754
+ id: string
1755
+ isRoot?: boolean
1756
+ path?: string
1757
+ fullPath: string
1758
+ rank?: number
1759
+ parentRoute?: RouteLike
1760
+ children?: Array<RouteLike>
1761
+ options?: {
1762
+ caseSensitive?: boolean
1763
+ }
1764
+ }
package/src/router.ts CHANGED
@@ -4,7 +4,6 @@ import {
4
4
  createMemoryHistory,
5
5
  parseHref,
6
6
  } from '@tanstack/history'
7
- import invariant from 'tiny-invariant'
8
7
  import {
9
8
  createControlledPromise,
10
9
  deepEqual,
@@ -13,19 +12,14 @@ import {
13
12
  last,
14
13
  replaceEqualDeep,
15
14
  } from './utils'
15
+ import { processRouteTree } from './process-route-tree'
16
16
  import {
17
- SEGMENT_TYPE_OPTIONAL_PARAM,
18
- SEGMENT_TYPE_PARAM,
19
- SEGMENT_TYPE_PATHNAME,
20
- SEGMENT_TYPE_WILDCARD,
21
17
  cleanPath,
22
18
  interpolatePath,
23
19
  joinPaths,
24
20
  matchPathname,
25
- parsePathname,
26
21
  resolvePath,
27
22
  trimPath,
28
- trimPathLeft,
29
23
  trimPathRight,
30
24
  } from './path'
31
25
  import { isNotFound } from './not-found'
@@ -35,7 +29,7 @@ import { rootRouteId } from './root'
35
29
  import { isRedirect, redirect } from './redirect'
36
30
  import { createLRUCache } from './lru-cache'
37
31
  import { loadMatches, loadRouteChunk, routeNeedsPreload } from './load-matches'
38
- import type { ParsePathnameCache, Segment } from './path'
32
+ import type { ParsePathnameCache } from './path'
39
33
  import type { SearchParser, SearchSerializer } from './searchParams'
40
34
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
41
35
  import type {
@@ -59,6 +53,7 @@ import type {
59
53
  AnyRouteWithContext,
60
54
  MakeRemountDepsOptionsUnion,
61
55
  RouteContextOptions,
56
+ RouteLike,
62
57
  RouteMask,
63
58
  SearchMiddleware,
64
59
  } from './route'
@@ -2383,229 +2378,6 @@ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
2383
2378
  return {}
2384
2379
  }
2385
2380
 
2386
- interface RouteLike {
2387
- id: string
2388
- isRoot?: boolean
2389
- path?: string
2390
- fullPath: string
2391
- rank?: number
2392
- parentRoute?: RouteLike
2393
- children?: Array<RouteLike>
2394
- options?: {
2395
- caseSensitive?: boolean
2396
- }
2397
- }
2398
-
2399
- export type ProcessRouteTreeResult<TRouteLike extends RouteLike> = {
2400
- routesById: Record<string, TRouteLike>
2401
- routesByPath: Record<string, TRouteLike>
2402
- flatRoutes: Array<TRouteLike>
2403
- }
2404
-
2405
- const REQUIRED_PARAM_BASE_SCORE = 0.5
2406
- const OPTIONAL_PARAM_BASE_SCORE = 0.4
2407
- const WILDCARD_PARAM_BASE_SCORE = 0.25
2408
- const BOTH_PRESENCE_BASE_SCORE = 0.05
2409
- const PREFIX_PRESENCE_BASE_SCORE = 0.02
2410
- const SUFFIX_PRESENCE_BASE_SCORE = 0.01
2411
- const PREFIX_LENGTH_SCORE_MULTIPLIER = 0.0002
2412
- const SUFFIX_LENGTH_SCORE_MULTIPLIER = 0.0001
2413
-
2414
- function handleParam(segment: Segment, baseScore: number) {
2415
- if (segment.prefixSegment && segment.suffixSegment) {
2416
- return (
2417
- baseScore +
2418
- BOTH_PRESENCE_BASE_SCORE +
2419
- PREFIX_LENGTH_SCORE_MULTIPLIER * segment.prefixSegment.length +
2420
- SUFFIX_LENGTH_SCORE_MULTIPLIER * segment.suffixSegment.length
2421
- )
2422
- }
2423
-
2424
- if (segment.prefixSegment) {
2425
- return (
2426
- baseScore +
2427
- PREFIX_PRESENCE_BASE_SCORE +
2428
- PREFIX_LENGTH_SCORE_MULTIPLIER * segment.prefixSegment.length
2429
- )
2430
- }
2431
-
2432
- if (segment.suffixSegment) {
2433
- return (
2434
- baseScore +
2435
- SUFFIX_PRESENCE_BASE_SCORE +
2436
- SUFFIX_LENGTH_SCORE_MULTIPLIER * segment.suffixSegment.length
2437
- )
2438
- }
2439
-
2440
- return baseScore
2441
- }
2442
-
2443
- export function processRouteTree<TRouteLike extends RouteLike>({
2444
- routeTree,
2445
- initRoute,
2446
- }: {
2447
- routeTree: TRouteLike
2448
- initRoute?: (route: TRouteLike, index: number) => void
2449
- }): ProcessRouteTreeResult<TRouteLike> {
2450
- const routesById = {} as Record<string, TRouteLike>
2451
- const routesByPath = {} as Record<string, TRouteLike>
2452
-
2453
- const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
2454
- childRoutes.forEach((childRoute, i) => {
2455
- initRoute?.(childRoute, i)
2456
-
2457
- const existingRoute = routesById[childRoute.id]
2458
-
2459
- invariant(
2460
- !existingRoute,
2461
- `Duplicate routes found with id: ${String(childRoute.id)}`,
2462
- )
2463
-
2464
- routesById[childRoute.id] = childRoute
2465
-
2466
- if (!childRoute.isRoot && childRoute.path) {
2467
- const trimmedFullPath = trimPathRight(childRoute.fullPath)
2468
- if (
2469
- !routesByPath[trimmedFullPath] ||
2470
- childRoute.fullPath.endsWith('/')
2471
- ) {
2472
- routesByPath[trimmedFullPath] = childRoute
2473
- }
2474
- }
2475
-
2476
- const children = childRoute.children as Array<TRouteLike>
2477
-
2478
- if (children?.length) {
2479
- recurseRoutes(children)
2480
- }
2481
- })
2482
- }
2483
-
2484
- recurseRoutes([routeTree])
2485
-
2486
- const scoredRoutes: Array<{
2487
- child: TRouteLike
2488
- trimmed: string
2489
- parsed: ReadonlyArray<Segment>
2490
- index: number
2491
- scores: Array<number>
2492
- hasStaticAfter: boolean
2493
- optionalParamCount: number
2494
- }> = []
2495
-
2496
- const routes: Array<TRouteLike> = Object.values(routesById)
2497
-
2498
- routes.forEach((d, i) => {
2499
- if (d.isRoot || !d.path) {
2500
- return
2501
- }
2502
-
2503
- const trimmed = trimPathLeft(d.fullPath)
2504
- let parsed = parsePathname(trimmed)
2505
-
2506
- // Removes the leading slash if it is not the only remaining segment
2507
- let skip = 0
2508
- while (parsed.length > skip + 1 && parsed[skip]?.value === '/') {
2509
- skip++
2510
- }
2511
- if (skip > 0) parsed = parsed.slice(skip)
2512
-
2513
- let optionalParamCount = 0
2514
- let hasStaticAfter = false
2515
- const scores = parsed.map((segment, index) => {
2516
- if (segment.value === '/') {
2517
- return 0.75
2518
- }
2519
-
2520
- let baseScore: number | undefined = undefined
2521
- if (segment.type === SEGMENT_TYPE_PARAM) {
2522
- baseScore = REQUIRED_PARAM_BASE_SCORE
2523
- } else if (segment.type === SEGMENT_TYPE_OPTIONAL_PARAM) {
2524
- baseScore = OPTIONAL_PARAM_BASE_SCORE
2525
- optionalParamCount++
2526
- } else if (segment.type === SEGMENT_TYPE_WILDCARD) {
2527
- baseScore = WILDCARD_PARAM_BASE_SCORE
2528
- }
2529
-
2530
- if (baseScore) {
2531
- // if there is any static segment (that is not an index) after a required / optional param,
2532
- // we will boost this param so it ranks higher than a required/optional param without a static segment after it
2533
- // JUST FOR SORTING, NOT FOR MATCHING
2534
- for (let i = index + 1; i < parsed.length; i++) {
2535
- const nextSegment = parsed[i]!
2536
- if (
2537
- nextSegment.type === SEGMENT_TYPE_PATHNAME &&
2538
- nextSegment.value !== '/'
2539
- ) {
2540
- hasStaticAfter = true
2541
- return handleParam(segment, baseScore + 0.2)
2542
- }
2543
- }
2544
-
2545
- return handleParam(segment, baseScore)
2546
- }
2547
-
2548
- return 1
2549
- })
2550
-
2551
- scoredRoutes.push({
2552
- child: d,
2553
- trimmed,
2554
- parsed,
2555
- index: i,
2556
- scores,
2557
- optionalParamCount,
2558
- hasStaticAfter,
2559
- })
2560
- })
2561
-
2562
- const flatRoutes = scoredRoutes
2563
- .sort((a, b) => {
2564
- const minLength = Math.min(a.scores.length, b.scores.length)
2565
-
2566
- // Sort by segment-by-segment score comparison ONLY for the common prefix
2567
- for (let i = 0; i < minLength; i++) {
2568
- if (a.scores[i] !== b.scores[i]) {
2569
- return b.scores[i]! - a.scores[i]!
2570
- }
2571
- }
2572
-
2573
- // If all common segments have equal scores, then consider length and specificity
2574
- if (a.scores.length !== b.scores.length) {
2575
- // If different number of optional parameters, fewer optional parameters wins (more specific)
2576
- // only if both or none of the routes has static segments after the params
2577
- if (a.optionalParamCount !== b.optionalParamCount) {
2578
- if (a.hasStaticAfter === b.hasStaticAfter) {
2579
- return a.optionalParamCount - b.optionalParamCount
2580
- } else if (a.hasStaticAfter && !b.hasStaticAfter) {
2581
- return -1
2582
- } else if (!a.hasStaticAfter && b.hasStaticAfter) {
2583
- return 1
2584
- }
2585
- }
2586
-
2587
- // If same number of optional parameters, longer path wins (for static segments)
2588
- return b.scores.length - a.scores.length
2589
- }
2590
-
2591
- // Sort by min available parsed value for alphabetical ordering
2592
- for (let i = 0; i < minLength; i++) {
2593
- if (a.parsed[i]!.value !== b.parsed[i]!.value) {
2594
- return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
2595
- }
2596
- }
2597
-
2598
- // Sort by original index
2599
- return a.index - b.index
2600
- })
2601
- .map((d, i) => {
2602
- d.child.rank = i
2603
- return d.child
2604
- })
2605
-
2606
- return { routesById, routesByPath, flatRoutes }
2607
- }
2608
-
2609
2381
  export function getMatchedRoutes<TRouteLike extends RouteLike>({
2610
2382
  pathname,
2611
2383
  routePathname,