@tanstack/router-core 1.127.8 → 1.128.3
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/dist/cjs/path.cjs +141 -14
- package/dist/cjs/path.cjs.map +1 -1
- package/dist/cjs/path.d.cts +4 -2
- package/dist/cjs/router.cjs +56 -21
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/esm/path.d.ts +4 -2
- package/dist/esm/path.js +141 -14
- package/dist/esm/path.js.map +1 -1
- package/dist/esm/router.js +56 -21
- package/dist/esm/router.js.map +1 -1
- package/package.json +1 -1
- package/src/path.ts +210 -20
- package/src/router.ts +85 -39
package/package.json
CHANGED
package/src/path.ts
CHANGED
|
@@ -3,9 +3,8 @@ import type { MatchLocation } from './RouterProvider'
|
|
|
3
3
|
import type { AnyPathParams } from './route'
|
|
4
4
|
|
|
5
5
|
export interface Segment {
|
|
6
|
-
type: 'pathname' | 'param' | 'wildcard'
|
|
6
|
+
type: 'pathname' | 'param' | 'wildcard' | 'optional-param'
|
|
7
7
|
value: string
|
|
8
|
-
// Add a new property to store the static segment if present
|
|
9
8
|
prefixSegment?: string
|
|
10
9
|
suffixSegment?: string
|
|
11
10
|
}
|
|
@@ -151,6 +150,18 @@ export function resolvePath({
|
|
|
151
150
|
return `{$${param}}${segment.suffixSegment}`
|
|
152
151
|
}
|
|
153
152
|
}
|
|
153
|
+
if (segment.type === 'optional-param') {
|
|
154
|
+
const param = segment.value.substring(1)
|
|
155
|
+
if (segment.prefixSegment && segment.suffixSegment) {
|
|
156
|
+
return `${segment.prefixSegment}{-$${param}}${segment.suffixSegment}`
|
|
157
|
+
} else if (segment.prefixSegment) {
|
|
158
|
+
return `${segment.prefixSegment}{-$${param}}`
|
|
159
|
+
} else if (segment.suffixSegment) {
|
|
160
|
+
return `{-$${param}}${segment.suffixSegment}`
|
|
161
|
+
}
|
|
162
|
+
return `{-$${param}}`
|
|
163
|
+
}
|
|
164
|
+
|
|
154
165
|
if (segment.type === 'wildcard') {
|
|
155
166
|
if (segment.prefixSegment && segment.suffixSegment) {
|
|
156
167
|
return `${segment.prefixSegment}{$}${segment.suffixSegment}`
|
|
@@ -168,6 +179,9 @@ export function resolvePath({
|
|
|
168
179
|
|
|
169
180
|
const PARAM_RE = /^\$.{1,}$/ // $paramName
|
|
170
181
|
const PARAM_W_CURLY_BRACES_RE = /^(.*?)\{(\$[a-zA-Z_$][a-zA-Z0-9_$]*)\}(.*)$/ // prefix{$paramName}suffix
|
|
182
|
+
const OPTIONAL_PARAM_W_CURLY_BRACES_RE =
|
|
183
|
+
/^(.*?)\{-(\$[a-zA-Z_$][a-zA-Z0-9_$]*)\}(.*)$/ // prefix{-$paramName}suffix
|
|
184
|
+
|
|
171
185
|
const WILDCARD_RE = /^\$$/ // $
|
|
172
186
|
const WILDCARD_W_CURLY_BRACES_RE = /^(.*?)\{\$\}(.*)$/ // prefix{$}suffix
|
|
173
187
|
|
|
@@ -177,8 +191,10 @@ const WILDCARD_W_CURLY_BRACES_RE = /^(.*?)\{\$\}(.*)$/ // prefix{$}suffix
|
|
|
177
191
|
* Wildcard: `/foo/$` ✅
|
|
178
192
|
* Wildcard with Prefix and Suffix: `/foo/prefix{$}suffix` ✅
|
|
179
193
|
*
|
|
194
|
+
* Optional param: `/foo/{-$bar}`
|
|
195
|
+
* Optional param with Prefix and Suffix: `/foo/prefix{-$bar}suffix`
|
|
196
|
+
|
|
180
197
|
* Future:
|
|
181
|
-
* Optional: `/foo/{-bar}`
|
|
182
198
|
* Optional named segment: `/foo/{bar}`
|
|
183
199
|
* Optional named segment with Prefix and Suffix: `/foo/prefix{-bar}suffix`
|
|
184
200
|
* Escape special characters:
|
|
@@ -225,6 +241,22 @@ export function parsePathname(pathname?: string): Array<Segment> {
|
|
|
225
241
|
}
|
|
226
242
|
}
|
|
227
243
|
|
|
244
|
+
// Check for optional parameter format: prefix{-$paramName}suffix
|
|
245
|
+
const optionalParamBracesMatch = part.match(
|
|
246
|
+
OPTIONAL_PARAM_W_CURLY_BRACES_RE,
|
|
247
|
+
)
|
|
248
|
+
if (optionalParamBracesMatch) {
|
|
249
|
+
const prefix = optionalParamBracesMatch[1]
|
|
250
|
+
const paramName = optionalParamBracesMatch[2]!
|
|
251
|
+
const suffix = optionalParamBracesMatch[3]
|
|
252
|
+
return {
|
|
253
|
+
type: 'optional-param',
|
|
254
|
+
value: paramName, // Now just $paramName (no prefix)
|
|
255
|
+
prefixSegment: prefix || undefined,
|
|
256
|
+
suffixSegment: suffix || undefined,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
228
260
|
// Check for the new parameter format: prefix{$paramName}suffix
|
|
229
261
|
const paramBracesMatch = part.match(PARAM_W_CURLY_BRACES_RE)
|
|
230
262
|
if (paramBracesMatch) {
|
|
@@ -368,6 +400,31 @@ export function interpolatePath({
|
|
|
368
400
|
return `${segmentPrefix}${encodeParam(key) ?? 'undefined'}${segmentSuffix}`
|
|
369
401
|
}
|
|
370
402
|
|
|
403
|
+
if (segment.type === 'optional-param') {
|
|
404
|
+
const key = segment.value.substring(1)
|
|
405
|
+
|
|
406
|
+
const segmentPrefix = segment.prefixSegment || ''
|
|
407
|
+
const segmentSuffix = segment.suffixSegment || ''
|
|
408
|
+
|
|
409
|
+
// Check if optional parameter is missing or undefined
|
|
410
|
+
if (!(key in params) || params[key] == null) {
|
|
411
|
+
// For optional params with prefix/suffix, keep the prefix/suffix but omit the param
|
|
412
|
+
if (segmentPrefix || segmentSuffix) {
|
|
413
|
+
return `${segmentPrefix}${segmentSuffix}`
|
|
414
|
+
}
|
|
415
|
+
// If no prefix/suffix, omit the entire segment
|
|
416
|
+
return undefined
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
usedParams[key] = params[key]
|
|
420
|
+
|
|
421
|
+
if (leaveParams) {
|
|
422
|
+
const value = encodeParam(segment.value)
|
|
423
|
+
return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
|
|
424
|
+
}
|
|
425
|
+
return `${segmentPrefix}${encodeParam(key) ?? ''}${segmentSuffix}`
|
|
426
|
+
}
|
|
427
|
+
|
|
371
428
|
return segment.value
|
|
372
429
|
}),
|
|
373
430
|
)
|
|
@@ -479,21 +536,23 @@ export function matchByPath(
|
|
|
479
536
|
const params: Record<string, string> = {}
|
|
480
537
|
|
|
481
538
|
const isMatch = (() => {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
539
|
+
let baseIndex = 0
|
|
540
|
+
let routeIndex = 0
|
|
541
|
+
|
|
542
|
+
while (
|
|
543
|
+
baseIndex < baseSegments.length ||
|
|
544
|
+
routeIndex < routeSegments.length
|
|
486
545
|
) {
|
|
487
|
-
const baseSegment = baseSegments[
|
|
488
|
-
const routeSegment = routeSegments[
|
|
546
|
+
const baseSegment = baseSegments[baseIndex]
|
|
547
|
+
const routeSegment = routeSegments[routeIndex]
|
|
489
548
|
|
|
490
|
-
const isLastBaseSegment =
|
|
491
|
-
const isLastRouteSegment =
|
|
549
|
+
const isLastBaseSegment = baseIndex >= baseSegments.length - 1
|
|
550
|
+
const isLastRouteSegment = routeIndex >= routeSegments.length - 1
|
|
492
551
|
|
|
493
552
|
if (routeSegment) {
|
|
494
553
|
if (routeSegment.type === 'wildcard') {
|
|
495
554
|
// Capture all remaining segments for a wildcard
|
|
496
|
-
const remainingBaseSegments = baseSegments.slice(
|
|
555
|
+
const remainingBaseSegments = baseSegments.slice(baseIndex)
|
|
497
556
|
|
|
498
557
|
let _splat: string
|
|
499
558
|
|
|
@@ -551,7 +610,8 @@ export function matchByPath(
|
|
|
551
610
|
|
|
552
611
|
if (routeSegment.type === 'pathname') {
|
|
553
612
|
if (routeSegment.value === '/' && !baseSegment?.value) {
|
|
554
|
-
|
|
613
|
+
routeIndex++
|
|
614
|
+
continue
|
|
555
615
|
}
|
|
556
616
|
|
|
557
617
|
if (baseSegment) {
|
|
@@ -565,19 +625,25 @@ export function matchByPath(
|
|
|
565
625
|
) {
|
|
566
626
|
return false
|
|
567
627
|
}
|
|
628
|
+
baseIndex++
|
|
629
|
+
routeIndex++
|
|
630
|
+
continue
|
|
631
|
+
} else {
|
|
632
|
+
return false
|
|
568
633
|
}
|
|
569
634
|
}
|
|
570
635
|
|
|
571
|
-
if (!baseSegment) {
|
|
572
|
-
return false
|
|
573
|
-
}
|
|
574
|
-
|
|
575
636
|
if (routeSegment.type === 'param') {
|
|
637
|
+
if (!baseSegment) {
|
|
638
|
+
return false
|
|
639
|
+
}
|
|
640
|
+
|
|
576
641
|
if (baseSegment.value === '/') {
|
|
577
642
|
return false
|
|
578
643
|
}
|
|
579
644
|
|
|
580
|
-
let _paramValue
|
|
645
|
+
let _paramValue = ''
|
|
646
|
+
let matched = false
|
|
581
647
|
|
|
582
648
|
// If this param has prefix/suffix, we need to extract the actual parameter value
|
|
583
649
|
if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
|
|
@@ -605,19 +671,143 @@ export function matchByPath(
|
|
|
605
671
|
}
|
|
606
672
|
|
|
607
673
|
_paramValue = decodeURIComponent(paramValue)
|
|
674
|
+
matched = true
|
|
608
675
|
} else {
|
|
609
676
|
// If no prefix/suffix, just decode the base segment value
|
|
610
677
|
_paramValue = decodeURIComponent(baseSegment.value)
|
|
678
|
+
matched = true
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (matched) {
|
|
682
|
+
params[routeSegment.value.substring(1)] = _paramValue
|
|
683
|
+
baseIndex++
|
|
611
684
|
}
|
|
612
685
|
|
|
613
|
-
|
|
686
|
+
routeIndex++
|
|
687
|
+
continue
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (routeSegment.type === 'optional-param') {
|
|
691
|
+
// Optional parameters can be missing - don't fail the match
|
|
692
|
+
if (!baseSegment) {
|
|
693
|
+
// No base segment for optional param - skip this route segment
|
|
694
|
+
routeIndex++
|
|
695
|
+
continue
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (baseSegment.value === '/') {
|
|
699
|
+
// Skip slash segments for optional params
|
|
700
|
+
routeIndex++
|
|
701
|
+
continue
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
let _paramValue = ''
|
|
705
|
+
let matched = false
|
|
706
|
+
|
|
707
|
+
// If this optional param has prefix/suffix, we need to extract the actual parameter value
|
|
708
|
+
if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
|
|
709
|
+
const prefix = routeSegment.prefixSegment || ''
|
|
710
|
+
const suffix = routeSegment.suffixSegment || ''
|
|
711
|
+
|
|
712
|
+
// Check if the base segment starts with prefix and ends with suffix
|
|
713
|
+
const baseValue = baseSegment.value
|
|
714
|
+
if (
|
|
715
|
+
(!prefix || baseValue.startsWith(prefix)) &&
|
|
716
|
+
(!suffix || baseValue.endsWith(suffix))
|
|
717
|
+
) {
|
|
718
|
+
let paramValue = baseValue
|
|
719
|
+
if (prefix && paramValue.startsWith(prefix)) {
|
|
720
|
+
paramValue = paramValue.slice(prefix.length)
|
|
721
|
+
}
|
|
722
|
+
if (suffix && paramValue.endsWith(suffix)) {
|
|
723
|
+
paramValue = paramValue.slice(
|
|
724
|
+
0,
|
|
725
|
+
paramValue.length - suffix.length,
|
|
726
|
+
)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
_paramValue = decodeURIComponent(paramValue)
|
|
730
|
+
matched = true
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
733
|
+
// For optional params without prefix/suffix, we need to check if the current
|
|
734
|
+
// base segment should match this optional param or a later route segment
|
|
735
|
+
|
|
736
|
+
// Look ahead to see if there's a later route segment that matches the current base segment
|
|
737
|
+
let shouldMatchOptional = true
|
|
738
|
+
for (
|
|
739
|
+
let lookAhead = routeIndex + 1;
|
|
740
|
+
lookAhead < routeSegments.length;
|
|
741
|
+
lookAhead++
|
|
742
|
+
) {
|
|
743
|
+
const futureRouteSegment = routeSegments[lookAhead]
|
|
744
|
+
if (
|
|
745
|
+
futureRouteSegment?.type === 'pathname' &&
|
|
746
|
+
futureRouteSegment.value === baseSegment.value
|
|
747
|
+
) {
|
|
748
|
+
// The current base segment matches a future pathname segment,
|
|
749
|
+
// so we should skip this optional parameter
|
|
750
|
+
shouldMatchOptional = false
|
|
751
|
+
break
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// If we encounter a required param or wildcard, stop looking ahead
|
|
755
|
+
if (
|
|
756
|
+
futureRouteSegment?.type === 'param' ||
|
|
757
|
+
futureRouteSegment?.type === 'wildcard'
|
|
758
|
+
) {
|
|
759
|
+
break
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (shouldMatchOptional) {
|
|
764
|
+
// If no prefix/suffix, just decode the base segment value
|
|
765
|
+
_paramValue = decodeURIComponent(baseSegment.value)
|
|
766
|
+
matched = true
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (matched) {
|
|
771
|
+
params[routeSegment.value.substring(1)] = _paramValue
|
|
772
|
+
baseIndex++
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
routeIndex++
|
|
776
|
+
continue
|
|
614
777
|
}
|
|
615
778
|
}
|
|
616
779
|
|
|
617
780
|
if (!isLastBaseSegment && isLastRouteSegment) {
|
|
618
|
-
params['**'] = joinPaths(
|
|
781
|
+
params['**'] = joinPaths(
|
|
782
|
+
baseSegments.slice(baseIndex + 1).map((d) => d.value),
|
|
783
|
+
)
|
|
619
784
|
return !!matchLocation.fuzzy && routeSegment?.value !== '/'
|
|
620
785
|
}
|
|
786
|
+
|
|
787
|
+
// If we have base segments left but no route segments, it's not a match
|
|
788
|
+
if (
|
|
789
|
+
baseIndex < baseSegments.length &&
|
|
790
|
+
routeIndex >= routeSegments.length
|
|
791
|
+
) {
|
|
792
|
+
return false
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// If we have route segments left but no base segments, check if remaining are optional
|
|
796
|
+
if (
|
|
797
|
+
routeIndex < routeSegments.length &&
|
|
798
|
+
baseIndex >= baseSegments.length
|
|
799
|
+
) {
|
|
800
|
+
// Check if all remaining route segments are optional
|
|
801
|
+
for (let i = routeIndex; i < routeSegments.length; i++) {
|
|
802
|
+
if (routeSegments[i]?.type !== 'optional-param') {
|
|
803
|
+
return false
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
// All remaining are optional, so we can finish
|
|
807
|
+
break
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
break
|
|
621
811
|
}
|
|
622
812
|
|
|
623
813
|
return true
|
package/src/router.ts
CHANGED
|
@@ -1450,15 +1450,23 @@ export class RouterCore<
|
|
|
1450
1450
|
|
|
1451
1451
|
// Resolve the next params
|
|
1452
1452
|
let nextParams =
|
|
1453
|
-
|
|
1454
|
-
?
|
|
1455
|
-
:
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1453
|
+
dest.params === false || dest.params === null
|
|
1454
|
+
? {}
|
|
1455
|
+
: (dest.params ?? true) === true
|
|
1456
|
+
? fromParams
|
|
1457
|
+
: {
|
|
1458
|
+
...fromParams,
|
|
1459
|
+
...functionalUpdate(dest.params as any, fromParams),
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Interpolate the path first to get the actual resolved path, then match against that
|
|
1463
|
+
const interpolatedNextTo = interpolatePath({
|
|
1464
|
+
path: nextTo,
|
|
1465
|
+
params: nextParams ?? {},
|
|
1466
|
+
}).interpolatedPath
|
|
1459
1467
|
|
|
1460
1468
|
const destRoutes = this.matchRoutes(
|
|
1461
|
-
|
|
1469
|
+
interpolatedNextTo,
|
|
1462
1470
|
{},
|
|
1463
1471
|
{
|
|
1464
1472
|
_buildLocation: true,
|
|
@@ -1479,8 +1487,9 @@ export class RouterCore<
|
|
|
1479
1487
|
})
|
|
1480
1488
|
}
|
|
1481
1489
|
|
|
1482
|
-
// Interpolate the next to into the next pathname
|
|
1483
1490
|
const nextPathname = interpolatePath({
|
|
1491
|
+
// Use the original template path for interpolation
|
|
1492
|
+
// This preserves the original parameter syntax including optional parameters
|
|
1484
1493
|
path: nextTo,
|
|
1485
1494
|
params: nextParams ?? {},
|
|
1486
1495
|
leaveWildcards: false,
|
|
@@ -1785,7 +1794,21 @@ export class RouterCore<
|
|
|
1785
1794
|
state: true,
|
|
1786
1795
|
_includeValidateSearch: true,
|
|
1787
1796
|
})
|
|
1788
|
-
|
|
1797
|
+
|
|
1798
|
+
// Normalize URLs for comparison to handle encoding differences
|
|
1799
|
+
// Browser history always stores encoded URLs while buildLocation may produce decoded URLs
|
|
1800
|
+
const normalizeUrl = (url: string) => {
|
|
1801
|
+
try {
|
|
1802
|
+
return encodeURI(decodeURI(url))
|
|
1803
|
+
} catch {
|
|
1804
|
+
return url
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
if (
|
|
1809
|
+
trimPath(normalizeUrl(this.latestLocation.href)) !==
|
|
1810
|
+
trimPath(normalizeUrl(nextLocation.href))
|
|
1811
|
+
) {
|
|
1789
1812
|
throw redirect({ href: nextLocation.href })
|
|
1790
1813
|
}
|
|
1791
1814
|
}
|
|
@@ -1796,6 +1819,7 @@ export class RouterCore<
|
|
|
1796
1819
|
this.__store.setState((s) => ({
|
|
1797
1820
|
...s,
|
|
1798
1821
|
status: 'pending',
|
|
1822
|
+
statusCode: 200,
|
|
1799
1823
|
isLoading: true,
|
|
1800
1824
|
location: this.latestLocation,
|
|
1801
1825
|
pendingMatches,
|
|
@@ -3225,43 +3249,51 @@ export function processRouteTree<TRouteLike extends RouteLike>({
|
|
|
3225
3249
|
return 0.75
|
|
3226
3250
|
}
|
|
3227
3251
|
|
|
3228
|
-
if (
|
|
3229
|
-
segment.
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
) {
|
|
3233
|
-
return 0.55
|
|
3234
|
-
}
|
|
3252
|
+
if (segment.type === 'param') {
|
|
3253
|
+
if (segment.prefixSegment && segment.suffixSegment) {
|
|
3254
|
+
return 0.55
|
|
3255
|
+
}
|
|
3235
3256
|
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3257
|
+
if (segment.prefixSegment) {
|
|
3258
|
+
return 0.52
|
|
3259
|
+
}
|
|
3239
3260
|
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3261
|
+
if (segment.suffixSegment) {
|
|
3262
|
+
return 0.51
|
|
3263
|
+
}
|
|
3243
3264
|
|
|
3244
|
-
if (segment.type === 'param') {
|
|
3245
3265
|
return 0.5
|
|
3246
3266
|
}
|
|
3247
3267
|
|
|
3248
|
-
if (
|
|
3249
|
-
segment.
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
) {
|
|
3253
|
-
return 0.3
|
|
3254
|
-
}
|
|
3268
|
+
if (segment.type === 'optional-param') {
|
|
3269
|
+
if (segment.prefixSegment && segment.suffixSegment) {
|
|
3270
|
+
return 0.45
|
|
3271
|
+
}
|
|
3255
3272
|
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3273
|
+
if (segment.prefixSegment) {
|
|
3274
|
+
return 0.42
|
|
3275
|
+
}
|
|
3259
3276
|
|
|
3260
|
-
|
|
3261
|
-
|
|
3277
|
+
if (segment.suffixSegment) {
|
|
3278
|
+
return 0.41
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
return 0.4
|
|
3262
3282
|
}
|
|
3263
3283
|
|
|
3264
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
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3265
3297
|
return 0.25
|
|
3266
3298
|
}
|
|
3267
3299
|
|
|
@@ -3275,19 +3307,33 @@ export function processRouteTree<TRouteLike extends RouteLike>({
|
|
|
3275
3307
|
.sort((a, b) => {
|
|
3276
3308
|
const minLength = Math.min(a.scores.length, b.scores.length)
|
|
3277
3309
|
|
|
3278
|
-
// Sort by
|
|
3310
|
+
// Sort by segment-by-segment score comparison ONLY for the common prefix
|
|
3279
3311
|
for (let i = 0; i < minLength; i++) {
|
|
3280
3312
|
if (a.scores[i] !== b.scores[i]) {
|
|
3281
3313
|
return b.scores[i]! - a.scores[i]!
|
|
3282
3314
|
}
|
|
3283
3315
|
}
|
|
3284
3316
|
|
|
3285
|
-
//
|
|
3317
|
+
// If all common segments have equal scores, then consider length and specificity
|
|
3286
3318
|
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
|
+
// If different number of optional parameters, fewer optional parameters wins (more specific)
|
|
3328
|
+
if (aOptionalCount !== bOptionalCount) {
|
|
3329
|
+
return aOptionalCount - bOptionalCount
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
// If same number of optional parameters, longer path wins (for static segments)
|
|
3287
3333
|
return b.scores.length - a.scores.length
|
|
3288
3334
|
}
|
|
3289
3335
|
|
|
3290
|
-
// Sort by min available parsed value
|
|
3336
|
+
// Sort by min available parsed value for alphabetical ordering
|
|
3291
3337
|
for (let i = 0; i < minLength; i++) {
|
|
3292
3338
|
if (a.parsed[i]!.value !== b.parsed[i]!.value) {
|
|
3293
3339
|
return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
|
|
@@ -3328,7 +3374,7 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
|
|
|
3328
3374
|
const result = matchPathname(basepath, trimmedPath, {
|
|
3329
3375
|
to: route.fullPath,
|
|
3330
3376
|
caseSensitive: route.options?.caseSensitive ?? caseSensitive,
|
|
3331
|
-
fuzzy:
|
|
3377
|
+
fuzzy: false,
|
|
3332
3378
|
})
|
|
3333
3379
|
return result
|
|
3334
3380
|
}
|