@tanstack/router-core 1.128.3 → 1.128.6

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.
package/src/router.ts CHANGED
@@ -14,6 +14,10 @@ import {
14
14
  replaceEqualDeep,
15
15
  } from './utils'
16
16
  import {
17
+ SEGMENT_TYPE_OPTIONAL_PARAM,
18
+ SEGMENT_TYPE_PARAM,
19
+ SEGMENT_TYPE_PATHNAME,
20
+ SEGMENT_TYPE_WILDCARD,
17
21
  cleanPath,
18
22
  interpolatePath,
19
23
  joinPaths,
@@ -29,6 +33,7 @@ import { setupScrollRestoration } from './scroll-restoration'
29
33
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
30
34
  import { rootRouteId } from './root'
31
35
  import { isRedirect, redirect } from './redirect'
36
+ import type { Segment } from './path'
32
37
  import type { SearchParser, SearchSerializer } from './searchParams'
33
38
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
34
39
  import type {
@@ -3178,6 +3183,27 @@ export type ProcessRouteTreeResult<TRouteLike extends RouteLike> = {
3178
3183
  routesByPath: Record<string, TRouteLike>
3179
3184
  flatRoutes: Array<TRouteLike>
3180
3185
  }
3186
+
3187
+ const REQUIRED_PARAM_BASE_SCORE = 0.5
3188
+ const OPTIONAL_PARAM_BASE_SCORE = 0.4
3189
+ const WILDCARD_PARAM_BASE_SCORE = 0.25
3190
+
3191
+ function handleParam(segment: Segment, baseScore: number) {
3192
+ if (segment.prefixSegment && segment.suffixSegment) {
3193
+ return baseScore + 0.05
3194
+ }
3195
+
3196
+ if (segment.prefixSegment) {
3197
+ return baseScore + 0.02
3198
+ }
3199
+
3200
+ if (segment.suffixSegment) {
3201
+ return baseScore + 0.01
3202
+ }
3203
+
3204
+ return baseScore
3205
+ }
3206
+
3181
3207
  export function processRouteTree<TRouteLike extends RouteLike>({
3182
3208
  routeTree,
3183
3209
  initRoute,
@@ -3224,9 +3250,11 @@ export function processRouteTree<TRouteLike extends RouteLike>({
3224
3250
  const scoredRoutes: Array<{
3225
3251
  child: TRouteLike
3226
3252
  trimmed: string
3227
- parsed: ReturnType<typeof parsePathname>
3253
+ parsed: ReadonlyArray<Segment>
3228
3254
  index: number
3229
3255
  scores: Array<number>
3256
+ hasStaticAfter: boolean
3257
+ optionalParamCount: number
3230
3258
  }> = []
3231
3259
 
3232
3260
  const routes: Array<TRouteLike> = Object.values(routesById)
@@ -3237,70 +3265,62 @@ export function processRouteTree<TRouteLike extends RouteLike>({
3237
3265
  }
3238
3266
 
3239
3267
  const trimmed = trimPathLeft(d.fullPath)
3240
- const parsed = parsePathname(trimmed)
3268
+ let parsed = parsePathname(trimmed)
3241
3269
 
3242
3270
  // Removes the leading slash if it is not the only remaining segment
3243
- while (parsed.length > 1 && parsed[0]?.value === '/') {
3244
- parsed.shift()
3271
+ let skip = 0
3272
+ while (parsed.length > skip + 1 && parsed[skip]?.value === '/') {
3273
+ skip++
3245
3274
  }
3275
+ if (skip > 0) parsed = parsed.slice(skip)
3246
3276
 
3247
- const scores = parsed.map((segment) => {
3277
+ let optionalParamCount = 0
3278
+ let hasStaticAfter = false
3279
+ const scores = parsed.map((segment, index) => {
3248
3280
  if (segment.value === '/') {
3249
3281
  return 0.75
3250
3282
  }
3251
3283
 
3252
- if (segment.type === 'param') {
3253
- if (segment.prefixSegment && segment.suffixSegment) {
3254
- return 0.55
3255
- }
3256
-
3257
- if (segment.prefixSegment) {
3258
- return 0.52
3259
- }
3260
-
3261
- if (segment.suffixSegment) {
3262
- return 0.51
3263
- }
3264
-
3265
- return 0.5
3266
- }
3267
-
3268
- if (segment.type === 'optional-param') {
3269
- if (segment.prefixSegment && segment.suffixSegment) {
3270
- return 0.45
3271
- }
3272
-
3273
- if (segment.prefixSegment) {
3274
- return 0.42
3275
- }
3276
-
3277
- if (segment.suffixSegment) {
3278
- return 0.41
3279
- }
3280
-
3281
- return 0.4
3284
+ let baseScore: number | undefined = undefined
3285
+ if (segment.type === SEGMENT_TYPE_PARAM) {
3286
+ baseScore = REQUIRED_PARAM_BASE_SCORE
3287
+ } else if (segment.type === SEGMENT_TYPE_OPTIONAL_PARAM) {
3288
+ baseScore = OPTIONAL_PARAM_BASE_SCORE
3289
+ optionalParamCount++
3290
+ } else if (segment.type === SEGMENT_TYPE_WILDCARD) {
3291
+ baseScore = WILDCARD_PARAM_BASE_SCORE
3282
3292
  }
3283
3293
 
3284
- if (segment.type === 'wildcard') {
3285
- if (segment.prefixSegment && segment.suffixSegment) {
3286
- return 0.3
3287
- }
3288
-
3289
- if (segment.prefixSegment) {
3290
- return 0.27
3291
- }
3292
-
3293
- if (segment.suffixSegment) {
3294
- return 0.26
3294
+ if (baseScore) {
3295
+ // if there is any static segment (that is not an index) after a required / optional param,
3296
+ // we will boost this param so it ranks higher than a required/optional param without a static segment after it
3297
+ // JUST FOR SORTING, NOT FOR MATCHING
3298
+ for (let i = index + 1; i < parsed.length; i++) {
3299
+ const nextSegment = parsed[i]!
3300
+ if (
3301
+ nextSegment.type === SEGMENT_TYPE_PATHNAME &&
3302
+ nextSegment.value !== '/'
3303
+ ) {
3304
+ hasStaticAfter = true
3305
+ return handleParam(segment, baseScore + 0.2)
3306
+ }
3295
3307
  }
3296
3308
 
3297
- return 0.25
3309
+ return handleParam(segment, baseScore)
3298
3310
  }
3299
3311
 
3300
3312
  return 1
3301
3313
  })
3302
3314
 
3303
- scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
3315
+ scoredRoutes.push({
3316
+ child: d,
3317
+ trimmed,
3318
+ parsed,
3319
+ index: i,
3320
+ scores,
3321
+ optionalParamCount,
3322
+ hasStaticAfter,
3323
+ })
3304
3324
  })
3305
3325
 
3306
3326
  const flatRoutes = scoredRoutes
@@ -3316,17 +3336,16 @@ export function processRouteTree<TRouteLike extends RouteLike>({
3316
3336
 
3317
3337
  // If all common segments have equal scores, then consider length and specificity
3318
3338
  if (a.scores.length !== b.scores.length) {
3319
- // Count optional parameters in each route
3320
- const aOptionalCount = a.parsed.filter(
3321
- (seg) => seg.type === 'optional-param',
3322
- ).length
3323
- const bOptionalCount = b.parsed.filter(
3324
- (seg) => seg.type === 'optional-param',
3325
- ).length
3326
-
3327
3339
  // If different number of optional parameters, fewer optional parameters wins (more specific)
3328
- if (aOptionalCount !== bOptionalCount) {
3329
- return aOptionalCount - bOptionalCount
3340
+ // only if both or none of the routes has static segments after the params
3341
+ if (a.optionalParamCount !== b.optionalParamCount) {
3342
+ if (a.hasStaticAfter === b.hasStaticAfter) {
3343
+ return a.optionalParamCount - b.optionalParamCount
3344
+ } else if (a.hasStaticAfter && !b.hasStaticAfter) {
3345
+ return -1
3346
+ } else if (!a.hasStaticAfter && b.hasStaticAfter) {
3347
+ return 1
3348
+ }
3330
3349
  }
3331
3350
 
3332
3351
  // If same number of optional parameters, longer path wins (for static segments)
@@ -3402,8 +3421,9 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3402
3421
 
3403
3422
  while (routeCursor.parentRoute) {
3404
3423
  routeCursor = routeCursor.parentRoute as TRouteLike
3405
- matchedRoutes.unshift(routeCursor)
3424
+ matchedRoutes.push(routeCursor)
3406
3425
  }
3426
+ matchedRoutes.reverse()
3407
3427
 
3408
3428
  return { matchedRoutes, routeParams, foundRoute }
3409
3429
  }