@tanstack/router-core 1.131.37 → 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'
@@ -1111,33 +1106,6 @@ export class RouterCore<
1111
1106
  return rootRouteId
1112
1107
  })()
1113
1108
 
1114
- const parseErrors = matchedRoutes.map((route) => {
1115
- let parsedParamsError
1116
-
1117
- const parseParams =
1118
- route.options.params?.parse ?? route.options.parseParams
1119
-
1120
- if (parseParams) {
1121
- try {
1122
- const parsedParams = parseParams(routeParams)
1123
- // Add the parsed params to the accumulated params bag
1124
- Object.assign(routeParams, parsedParams)
1125
- } catch (err: any) {
1126
- parsedParamsError = new PathParamError(err.message, {
1127
- cause: err,
1128
- })
1129
-
1130
- if (opts?.throwOnError) {
1131
- throw parsedParamsError
1132
- }
1133
-
1134
- return parsedParamsError
1135
- }
1136
- }
1137
-
1138
- return
1139
- })
1140
-
1141
1109
  const matches: Array<AnyRouteMatch> = []
1142
1110
 
1143
1111
  const getParentContext = (parentMatch?: AnyRouteMatch) => {
@@ -1224,20 +1192,36 @@ export class RouterCore<
1224
1192
  parseCache: this.parsePathnameCache,
1225
1193
  })
1226
1194
 
1227
- const strictParams = interpolatePathResult.usedParams
1195
+ // Waste not, want not. If we already have a match for this route,
1196
+ // reuse it. This is important for layout routes, which might stick
1197
+ // around between navigation actions that only change leaf routes.
1198
+
1199
+ // Existing matches are matches that are already loaded along with
1200
+ // pending matches that are still loading
1201
+ const matchId = interpolatePathResult.interpolatedPath + loaderDepsHash
1228
1202
 
1229
- let paramsError = parseErrors[index]
1203
+ const existingMatch = this.getMatch(matchId)
1230
1204
 
1231
- const strictParseParams =
1232
- route.options.params?.parse ?? route.options.parseParams
1205
+ const previousMatch = this.state.matches.find(
1206
+ (d) => d.routeId === route.id,
1207
+ )
1233
1208
 
1234
- if (strictParseParams) {
1235
- try {
1236
- Object.assign(strictParams, strictParseParams(strictParams as any))
1237
- } catch (err: any) {
1238
- // any param errors should already have been dealt with above, if this
1239
- // somehow differs, let's report this in the same manner
1240
- if (!paramsError) {
1209
+ const strictParams =
1210
+ existingMatch?._strictParams ?? interpolatePathResult.usedParams
1211
+
1212
+ let paramsError: PathParamError | undefined = undefined
1213
+
1214
+ if (!existingMatch) {
1215
+ const strictParseParams =
1216
+ route.options.params?.parse ?? route.options.parseParams
1217
+
1218
+ if (strictParseParams) {
1219
+ try {
1220
+ Object.assign(
1221
+ strictParams,
1222
+ strictParseParams(strictParams as Record<string, string>),
1223
+ )
1224
+ } catch (err: any) {
1241
1225
  paramsError = new PathParamError(err.message, {
1242
1226
  cause: err,
1243
1227
  })
@@ -1249,19 +1233,7 @@ export class RouterCore<
1249
1233
  }
1250
1234
  }
1251
1235
 
1252
- // Waste not, want not. If we already have a match for this route,
1253
- // reuse it. This is important for layout routes, which might stick
1254
- // around between navigation actions that only change leaf routes.
1255
-
1256
- // Existing matches are matches that are already loaded along with
1257
- // pending matches that are still loading
1258
- const matchId = interpolatePathResult.interpolatedPath + loaderDepsHash
1259
-
1260
- const existingMatch = this.getMatch(matchId)
1261
-
1262
- const previousMatch = this.state.matches.find(
1263
- (d) => d.routeId === route.id,
1264
- )
1236
+ Object.assign(routeParams, strictParams)
1265
1237
 
1266
1238
  const cause = previousMatch ? 'stay' : 'enter'
1267
1239
 
@@ -2406,229 +2378,6 @@ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
2406
2378
  return {}
2407
2379
  }
2408
2380
 
2409
- interface RouteLike {
2410
- id: string
2411
- isRoot?: boolean
2412
- path?: string
2413
- fullPath: string
2414
- rank?: number
2415
- parentRoute?: RouteLike
2416
- children?: Array<RouteLike>
2417
- options?: {
2418
- caseSensitive?: boolean
2419
- }
2420
- }
2421
-
2422
- export type ProcessRouteTreeResult<TRouteLike extends RouteLike> = {
2423
- routesById: Record<string, TRouteLike>
2424
- routesByPath: Record<string, TRouteLike>
2425
- flatRoutes: Array<TRouteLike>
2426
- }
2427
-
2428
- const REQUIRED_PARAM_BASE_SCORE = 0.5
2429
- const OPTIONAL_PARAM_BASE_SCORE = 0.4
2430
- const WILDCARD_PARAM_BASE_SCORE = 0.25
2431
- const BOTH_PRESENCE_BASE_SCORE = 0.05
2432
- const PREFIX_PRESENCE_BASE_SCORE = 0.02
2433
- const SUFFIX_PRESENCE_BASE_SCORE = 0.01
2434
- const PREFIX_LENGTH_SCORE_MULTIPLIER = 0.0002
2435
- const SUFFIX_LENGTH_SCORE_MULTIPLIER = 0.0001
2436
-
2437
- function handleParam(segment: Segment, baseScore: number) {
2438
- if (segment.prefixSegment && segment.suffixSegment) {
2439
- return (
2440
- baseScore +
2441
- BOTH_PRESENCE_BASE_SCORE +
2442
- PREFIX_LENGTH_SCORE_MULTIPLIER * segment.prefixSegment.length +
2443
- SUFFIX_LENGTH_SCORE_MULTIPLIER * segment.suffixSegment.length
2444
- )
2445
- }
2446
-
2447
- if (segment.prefixSegment) {
2448
- return (
2449
- baseScore +
2450
- PREFIX_PRESENCE_BASE_SCORE +
2451
- PREFIX_LENGTH_SCORE_MULTIPLIER * segment.prefixSegment.length
2452
- )
2453
- }
2454
-
2455
- if (segment.suffixSegment) {
2456
- return (
2457
- baseScore +
2458
- SUFFIX_PRESENCE_BASE_SCORE +
2459
- SUFFIX_LENGTH_SCORE_MULTIPLIER * segment.suffixSegment.length
2460
- )
2461
- }
2462
-
2463
- return baseScore
2464
- }
2465
-
2466
- export function processRouteTree<TRouteLike extends RouteLike>({
2467
- routeTree,
2468
- initRoute,
2469
- }: {
2470
- routeTree: TRouteLike
2471
- initRoute?: (route: TRouteLike, index: number) => void
2472
- }): ProcessRouteTreeResult<TRouteLike> {
2473
- const routesById = {} as Record<string, TRouteLike>
2474
- const routesByPath = {} as Record<string, TRouteLike>
2475
-
2476
- const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
2477
- childRoutes.forEach((childRoute, i) => {
2478
- initRoute?.(childRoute, i)
2479
-
2480
- const existingRoute = routesById[childRoute.id]
2481
-
2482
- invariant(
2483
- !existingRoute,
2484
- `Duplicate routes found with id: ${String(childRoute.id)}`,
2485
- )
2486
-
2487
- routesById[childRoute.id] = childRoute
2488
-
2489
- if (!childRoute.isRoot && childRoute.path) {
2490
- const trimmedFullPath = trimPathRight(childRoute.fullPath)
2491
- if (
2492
- !routesByPath[trimmedFullPath] ||
2493
- childRoute.fullPath.endsWith('/')
2494
- ) {
2495
- routesByPath[trimmedFullPath] = childRoute
2496
- }
2497
- }
2498
-
2499
- const children = childRoute.children as Array<TRouteLike>
2500
-
2501
- if (children?.length) {
2502
- recurseRoutes(children)
2503
- }
2504
- })
2505
- }
2506
-
2507
- recurseRoutes([routeTree])
2508
-
2509
- const scoredRoutes: Array<{
2510
- child: TRouteLike
2511
- trimmed: string
2512
- parsed: ReadonlyArray<Segment>
2513
- index: number
2514
- scores: Array<number>
2515
- hasStaticAfter: boolean
2516
- optionalParamCount: number
2517
- }> = []
2518
-
2519
- const routes: Array<TRouteLike> = Object.values(routesById)
2520
-
2521
- routes.forEach((d, i) => {
2522
- if (d.isRoot || !d.path) {
2523
- return
2524
- }
2525
-
2526
- const trimmed = trimPathLeft(d.fullPath)
2527
- let parsed = parsePathname(trimmed)
2528
-
2529
- // Removes the leading slash if it is not the only remaining segment
2530
- let skip = 0
2531
- while (parsed.length > skip + 1 && parsed[skip]?.value === '/') {
2532
- skip++
2533
- }
2534
- if (skip > 0) parsed = parsed.slice(skip)
2535
-
2536
- let optionalParamCount = 0
2537
- let hasStaticAfter = false
2538
- const scores = parsed.map((segment, index) => {
2539
- if (segment.value === '/') {
2540
- return 0.75
2541
- }
2542
-
2543
- let baseScore: number | undefined = undefined
2544
- if (segment.type === SEGMENT_TYPE_PARAM) {
2545
- baseScore = REQUIRED_PARAM_BASE_SCORE
2546
- } else if (segment.type === SEGMENT_TYPE_OPTIONAL_PARAM) {
2547
- baseScore = OPTIONAL_PARAM_BASE_SCORE
2548
- optionalParamCount++
2549
- } else if (segment.type === SEGMENT_TYPE_WILDCARD) {
2550
- baseScore = WILDCARD_PARAM_BASE_SCORE
2551
- }
2552
-
2553
- if (baseScore) {
2554
- // if there is any static segment (that is not an index) after a required / optional param,
2555
- // we will boost this param so it ranks higher than a required/optional param without a static segment after it
2556
- // JUST FOR SORTING, NOT FOR MATCHING
2557
- for (let i = index + 1; i < parsed.length; i++) {
2558
- const nextSegment = parsed[i]!
2559
- if (
2560
- nextSegment.type === SEGMENT_TYPE_PATHNAME &&
2561
- nextSegment.value !== '/'
2562
- ) {
2563
- hasStaticAfter = true
2564
- return handleParam(segment, baseScore + 0.2)
2565
- }
2566
- }
2567
-
2568
- return handleParam(segment, baseScore)
2569
- }
2570
-
2571
- return 1
2572
- })
2573
-
2574
- scoredRoutes.push({
2575
- child: d,
2576
- trimmed,
2577
- parsed,
2578
- index: i,
2579
- scores,
2580
- optionalParamCount,
2581
- hasStaticAfter,
2582
- })
2583
- })
2584
-
2585
- const flatRoutes = scoredRoutes
2586
- .sort((a, b) => {
2587
- const minLength = Math.min(a.scores.length, b.scores.length)
2588
-
2589
- // Sort by segment-by-segment score comparison ONLY for the common prefix
2590
- for (let i = 0; i < minLength; i++) {
2591
- if (a.scores[i] !== b.scores[i]) {
2592
- return b.scores[i]! - a.scores[i]!
2593
- }
2594
- }
2595
-
2596
- // If all common segments have equal scores, then consider length and specificity
2597
- if (a.scores.length !== b.scores.length) {
2598
- // If different number of optional parameters, fewer optional parameters wins (more specific)
2599
- // only if both or none of the routes has static segments after the params
2600
- if (a.optionalParamCount !== b.optionalParamCount) {
2601
- if (a.hasStaticAfter === b.hasStaticAfter) {
2602
- return a.optionalParamCount - b.optionalParamCount
2603
- } else if (a.hasStaticAfter && !b.hasStaticAfter) {
2604
- return -1
2605
- } else if (!a.hasStaticAfter && b.hasStaticAfter) {
2606
- return 1
2607
- }
2608
- }
2609
-
2610
- // If same number of optional parameters, longer path wins (for static segments)
2611
- return b.scores.length - a.scores.length
2612
- }
2613
-
2614
- // Sort by min available parsed value for alphabetical ordering
2615
- for (let i = 0; i < minLength; i++) {
2616
- if (a.parsed[i]!.value !== b.parsed[i]!.value) {
2617
- return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
2618
- }
2619
- }
2620
-
2621
- // Sort by original index
2622
- return a.index - b.index
2623
- })
2624
- .map((d, i) => {
2625
- d.child.rank = i
2626
- return d.child
2627
- })
2628
-
2629
- return { routesById, routesByPath, flatRoutes }
2630
- }
2631
-
2632
2381
  export function getMatchedRoutes<TRouteLike extends RouteLike>({
2633
2382
  pathname,
2634
2383
  routePathname,