@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/path.ts CHANGED
@@ -2,11 +2,22 @@ import { last } from './utils'
2
2
  import type { MatchLocation } from './RouterProvider'
3
3
  import type { AnyPathParams } from './route'
4
4
 
5
+ export const SEGMENT_TYPE_PATHNAME = 0
6
+ export const SEGMENT_TYPE_PARAM = 1
7
+ export const SEGMENT_TYPE_WILDCARD = 2
8
+ export const SEGMENT_TYPE_OPTIONAL_PARAM = 3
9
+
5
10
  export interface Segment {
6
- type: 'pathname' | 'param' | 'wildcard' | 'optional-param'
7
- value: string
8
- prefixSegment?: string
9
- suffixSegment?: string
11
+ readonly type:
12
+ | typeof SEGMENT_TYPE_PATHNAME
13
+ | typeof SEGMENT_TYPE_PARAM
14
+ | typeof SEGMENT_TYPE_WILDCARD
15
+ | typeof SEGMENT_TYPE_OPTIONAL_PARAM
16
+ readonly value: string
17
+ readonly prefixSegment?: string
18
+ readonly suffixSegment?: string
19
+ // Indicates if there is a static segment after this required/optional param
20
+ readonly hasStaticAfter?: boolean
10
21
  }
11
22
 
12
23
  export function joinPaths(paths: Array<string | undefined>) {
@@ -92,6 +103,51 @@ interface ResolvePathOptions {
92
103
  caseSensitive?: boolean
93
104
  }
94
105
 
106
+ function segmentToString(segment: Segment): string {
107
+ const { type, value } = segment
108
+ if (type === SEGMENT_TYPE_PATHNAME) {
109
+ return value
110
+ }
111
+
112
+ const { prefixSegment, suffixSegment } = segment
113
+
114
+ if (type === SEGMENT_TYPE_PARAM) {
115
+ const param = value.substring(1)
116
+ if (prefixSegment && suffixSegment) {
117
+ return `${prefixSegment}{$${param}}${suffixSegment}`
118
+ } else if (prefixSegment) {
119
+ return `${prefixSegment}{$${param}}`
120
+ } else if (suffixSegment) {
121
+ return `{$${param}}${suffixSegment}`
122
+ }
123
+ }
124
+
125
+ if (type === SEGMENT_TYPE_OPTIONAL_PARAM) {
126
+ const param = value.substring(1)
127
+ if (prefixSegment && suffixSegment) {
128
+ return `${prefixSegment}{-$${param}}${suffixSegment}`
129
+ } else if (prefixSegment) {
130
+ return `${prefixSegment}{-$${param}}`
131
+ } else if (suffixSegment) {
132
+ return `{-$${param}}${suffixSegment}`
133
+ }
134
+ return `{-$${param}}`
135
+ }
136
+
137
+ if (type === SEGMENT_TYPE_WILDCARD) {
138
+ if (prefixSegment && suffixSegment) {
139
+ return `${prefixSegment}{$}${suffixSegment}`
140
+ } else if (prefixSegment) {
141
+ return `${prefixSegment}{$}`
142
+ } else if (suffixSegment) {
143
+ return `{$}${suffixSegment}`
144
+ }
145
+ }
146
+
147
+ // This case should never happen, should we throw instead?
148
+ return value
149
+ }
150
+
95
151
  export function resolvePath({
96
152
  basepath,
97
153
  base,
@@ -102,79 +158,48 @@ export function resolvePath({
102
158
  base = removeBasepath(basepath, base, caseSensitive)
103
159
  to = removeBasepath(basepath, to, caseSensitive)
104
160
 
105
- let baseSegments = parsePathname(base)
161
+ let baseSegments = parsePathname(base).slice()
106
162
  const toSegments = parsePathname(to)
107
163
 
108
164
  if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
109
165
  baseSegments.pop()
110
166
  }
111
167
 
112
- toSegments.forEach((toSegment, index) => {
113
- if (toSegment.value === '/') {
168
+ for (let index = 0, length = toSegments.length; index < length; index++) {
169
+ const toSegment = toSegments[index]!
170
+ const value = toSegment.value
171
+ if (value === '/') {
114
172
  if (!index) {
115
173
  // Leading slash
116
174
  baseSegments = [toSegment]
117
- } else if (index === toSegments.length - 1) {
175
+ } else if (index === length - 1) {
118
176
  // Trailing Slash
119
177
  baseSegments.push(toSegment)
120
178
  } else {
121
179
  // ignore inter-slashes
122
180
  }
123
- } else if (toSegment.value === '..') {
181
+ } else if (value === '..') {
124
182
  baseSegments.pop()
125
- } else if (toSegment.value === '.') {
183
+ } else if (value === '.') {
126
184
  // ignore
127
185
  } else {
128
186
  baseSegments.push(toSegment)
129
187
  }
130
- })
188
+ }
131
189
 
132
190
  if (baseSegments.length > 1) {
133
- if (last(baseSegments)?.value === '/') {
191
+ if (last(baseSegments)!.value === '/') {
134
192
  if (trailingSlash === 'never') {
135
193
  baseSegments.pop()
136
194
  }
137
195
  } else if (trailingSlash === 'always') {
138
- baseSegments.push({ type: 'pathname', value: '/' })
196
+ baseSegments.push({ type: SEGMENT_TYPE_PATHNAME, value: '/' })
139
197
  }
140
198
  }
141
199
 
142
- const segmentValues = baseSegments.map((segment) => {
143
- if (segment.type === 'param') {
144
- const param = segment.value.substring(1)
145
- if (segment.prefixSegment && segment.suffixSegment) {
146
- return `${segment.prefixSegment}{$${param}}${segment.suffixSegment}`
147
- } else if (segment.prefixSegment) {
148
- return `${segment.prefixSegment}{$${param}}`
149
- } else if (segment.suffixSegment) {
150
- return `{$${param}}${segment.suffixSegment}`
151
- }
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
-
165
- if (segment.type === 'wildcard') {
166
- if (segment.prefixSegment && segment.suffixSegment) {
167
- return `${segment.prefixSegment}{$}${segment.suffixSegment}`
168
- } else if (segment.prefixSegment) {
169
- return `${segment.prefixSegment}{$}`
170
- } else if (segment.suffixSegment) {
171
- return `{$}${segment.suffixSegment}`
172
- }
173
- }
174
- return segment.value
175
- })
200
+ const segmentValues = baseSegments.map(segmentToString)
176
201
  const joined = joinPaths([basepath, ...segmentValues])
177
- return cleanPath(joined)
202
+ return joined
178
203
  }
179
204
 
180
205
  const PARAM_RE = /^\$.{1,}$/ // $paramName
@@ -202,7 +227,7 @@ const WILDCARD_W_CURLY_BRACES_RE = /^(.*?)\{\$\}(.*)$/ // prefix{$}suffix
202
227
  * - `/foo/[$]{$foo} - Dynamic route with a static prefix of `$`
203
228
  * - `/foo/{$foo}[$]` - Dynamic route with a static suffix of `$`
204
229
  */
205
- export function parsePathname(pathname?: string): Array<Segment> {
230
+ export function parsePathname(pathname?: string): ReadonlyArray<Segment> {
206
231
  if (!pathname) {
207
232
  return []
208
233
  }
@@ -214,7 +239,7 @@ export function parsePathname(pathname?: string): Array<Segment> {
214
239
  if (pathname.slice(0, 1) === '/') {
215
240
  pathname = pathname.substring(1)
216
241
  segments.push({
217
- type: 'pathname',
242
+ type: SEGMENT_TYPE_PATHNAME,
218
243
  value: '/',
219
244
  })
220
245
  }
@@ -234,7 +259,7 @@ export function parsePathname(pathname?: string): Array<Segment> {
234
259
  const prefix = wildcardBracesMatch[1]
235
260
  const suffix = wildcardBracesMatch[2]
236
261
  return {
237
- type: 'wildcard',
262
+ type: SEGMENT_TYPE_WILDCARD,
238
263
  value: '$',
239
264
  prefixSegment: prefix || undefined,
240
265
  suffixSegment: suffix || undefined,
@@ -250,7 +275,7 @@ export function parsePathname(pathname?: string): Array<Segment> {
250
275
  const paramName = optionalParamBracesMatch[2]!
251
276
  const suffix = optionalParamBracesMatch[3]
252
277
  return {
253
- type: 'optional-param',
278
+ type: SEGMENT_TYPE_OPTIONAL_PARAM,
254
279
  value: paramName, // Now just $paramName (no prefix)
255
280
  prefixSegment: prefix || undefined,
256
281
  suffixSegment: suffix || undefined,
@@ -264,7 +289,7 @@ export function parsePathname(pathname?: string): Array<Segment> {
264
289
  const paramName = paramBracesMatch[2]
265
290
  const suffix = paramBracesMatch[3]
266
291
  return {
267
- type: 'param',
292
+ type: SEGMENT_TYPE_PARAM,
268
293
  value: '' + paramName,
269
294
  prefixSegment: prefix || undefined,
270
295
  suffixSegment: suffix || undefined,
@@ -275,7 +300,7 @@ export function parsePathname(pathname?: string): Array<Segment> {
275
300
  if (PARAM_RE.test(part)) {
276
301
  const paramName = part.substring(1)
277
302
  return {
278
- type: 'param',
303
+ type: SEGMENT_TYPE_PARAM,
279
304
  value: '$' + paramName,
280
305
  prefixSegment: undefined,
281
306
  suffixSegment: undefined,
@@ -285,7 +310,7 @@ export function parsePathname(pathname?: string): Array<Segment> {
285
310
  // Check for bare wildcard: $ (without curly braces)
286
311
  if (WILDCARD_RE.test(part)) {
287
312
  return {
288
- type: 'wildcard',
313
+ type: SEGMENT_TYPE_WILDCARD,
289
314
  value: '$',
290
315
  prefixSegment: undefined,
291
316
  suffixSegment: undefined,
@@ -294,7 +319,7 @@ export function parsePathname(pathname?: string): Array<Segment> {
294
319
 
295
320
  // Handle regular pathname segment
296
321
  return {
297
- type: 'pathname',
322
+ type: SEGMENT_TYPE_PATHNAME,
298
323
  value: part.includes('%25')
299
324
  ? part
300
325
  .split('%25')
@@ -308,7 +333,7 @@ export function parsePathname(pathname?: string): Array<Segment> {
308
333
  if (pathname.slice(-1) === '/') {
309
334
  pathname = pathname.substring(1)
310
335
  segments.push({
311
- type: 'pathname',
336
+ type: SEGMENT_TYPE_PATHNAME,
312
337
  value: '/',
313
338
  })
314
339
  }
@@ -343,7 +368,7 @@ export function interpolatePath({
343
368
  const value = params[key]
344
369
  const isValueString = typeof value === 'string'
345
370
 
346
- if (['*', '_splat'].includes(key)) {
371
+ if (key === '*' || key === '_splat') {
347
372
  // the splat/catch-all routes shouldn't have the '/' encoded out
348
373
  return isValueString ? encodeURI(value) : value
349
374
  } else {
@@ -358,7 +383,11 @@ export function interpolatePath({
358
383
  const usedParams: Record<string, unknown> = {}
359
384
  const interpolatedPath = joinPaths(
360
385
  interpolatedPathSegments.map((segment) => {
361
- if (segment.type === 'wildcard') {
386
+ if (segment.type === SEGMENT_TYPE_PATHNAME) {
387
+ return segment.value
388
+ }
389
+
390
+ if (segment.type === SEGMENT_TYPE_WILDCARD) {
362
391
  usedParams._splat = params._splat
363
392
  const segmentPrefix = segment.prefixSegment || ''
364
393
  const segmentSuffix = segment.suffixSegment || ''
@@ -384,7 +413,7 @@ export function interpolatePath({
384
413
  return `${segmentPrefix}${value}${segmentSuffix}`
385
414
  }
386
415
 
387
- if (segment.type === 'param') {
416
+ if (segment.type === SEGMENT_TYPE_PARAM) {
388
417
  const key = segment.value.substring(1)
389
418
  if (!isMissingParams && !(key in params)) {
390
419
  isMissingParams = true
@@ -400,7 +429,7 @@ export function interpolatePath({
400
429
  return `${segmentPrefix}${encodeParam(key) ?? 'undefined'}${segmentSuffix}`
401
430
  }
402
431
 
403
- if (segment.type === 'optional-param') {
432
+ if (segment.type === SEGMENT_TYPE_OPTIONAL_PARAM) {
404
433
  const key = segment.value.substring(1)
405
434
 
406
435
  const segmentPrefix = segment.prefixSegment || ''
@@ -408,6 +437,9 @@ export function interpolatePath({
408
437
 
409
438
  // Check if optional parameter is missing or undefined
410
439
  if (!(key in params) || params[key] == null) {
440
+ if (leaveWildcards) {
441
+ return `${segmentPrefix}${key}${segmentSuffix}`
442
+ }
411
443
  // For optional params with prefix/suffix, keep the prefix/suffix but omit the param
412
444
  if (segmentPrefix || segmentSuffix) {
413
445
  return `${segmentPrefix}${segmentSuffix}`
@@ -500,165 +532,217 @@ export function removeBasepath(
500
532
  export function matchByPath(
501
533
  basepath: string,
502
534
  from: string,
503
- matchLocation: Pick<MatchLocation, 'to' | 'caseSensitive' | 'fuzzy'>,
535
+ {
536
+ to,
537
+ fuzzy,
538
+ caseSensitive,
539
+ }: Pick<MatchLocation, 'to' | 'caseSensitive' | 'fuzzy'>,
504
540
  ): Record<string, string> | undefined {
505
541
  // check basepath first
506
542
  if (basepath !== '/' && !from.startsWith(basepath)) {
507
543
  return undefined
508
544
  }
509
545
  // Remove the base path from the pathname
510
- from = removeBasepath(basepath, from, matchLocation.caseSensitive)
546
+ from = removeBasepath(basepath, from, caseSensitive)
511
547
  // Default to to $ (wildcard)
512
- const to = removeBasepath(
513
- basepath,
514
- `${matchLocation.to ?? '$'}`,
515
- matchLocation.caseSensitive,
516
- )
548
+ to = removeBasepath(basepath, `${to ?? '$'}`, caseSensitive)
517
549
 
518
550
  // Parse the from and to
519
- const baseSegments = parsePathname(from)
520
- const routeSegments = parsePathname(to)
551
+ const baseSegments = parsePathname(from.startsWith('/') ? from : `/${from}`)
552
+ const routeSegments = parsePathname(to.startsWith('/') ? to : `/${to}`)
521
553
 
522
- if (!from.startsWith('/')) {
523
- baseSegments.unshift({
524
- type: 'pathname',
525
- value: '/',
526
- })
527
- }
554
+ const params: Record<string, string> = {}
528
555
 
529
- if (!to.startsWith('/')) {
530
- routeSegments.unshift({
531
- type: 'pathname',
532
- value: '/',
533
- })
534
- }
556
+ const result = isMatch(
557
+ baseSegments,
558
+ routeSegments,
559
+ params,
560
+ fuzzy,
561
+ caseSensitive,
562
+ )
535
563
 
536
- const params: Record<string, string> = {}
564
+ return result ? params : undefined
565
+ }
537
566
 
538
- const isMatch = (() => {
539
- let baseIndex = 0
540
- let routeIndex = 0
567
+ function isMatch(
568
+ baseSegments: ReadonlyArray<Segment>,
569
+ routeSegments: ReadonlyArray<Segment>,
570
+ params: Record<string, string>,
571
+ fuzzy?: boolean,
572
+ caseSensitive?: boolean,
573
+ ): boolean {
574
+ let baseIndex = 0
575
+ let routeIndex = 0
541
576
 
542
- while (
543
- baseIndex < baseSegments.length ||
544
- routeIndex < routeSegments.length
545
- ) {
546
- const baseSegment = baseSegments[baseIndex]
547
- const routeSegment = routeSegments[routeIndex]
577
+ while (baseIndex < baseSegments.length || routeIndex < routeSegments.length) {
578
+ const baseSegment = baseSegments[baseIndex]
579
+ const routeSegment = routeSegments[routeIndex]
548
580
 
549
- const isLastBaseSegment = baseIndex >= baseSegments.length - 1
550
- const isLastRouteSegment = routeIndex >= routeSegments.length - 1
581
+ const isLastBaseSegment = baseIndex >= baseSegments.length - 1
582
+ const isLastRouteSegment = routeIndex >= routeSegments.length - 1
551
583
 
552
- if (routeSegment) {
553
- if (routeSegment.type === 'wildcard') {
554
- // Capture all remaining segments for a wildcard
555
- const remainingBaseSegments = baseSegments.slice(baseIndex)
584
+ if (routeSegment) {
585
+ if (routeSegment.type === SEGMENT_TYPE_WILDCARD) {
586
+ // Capture all remaining segments for a wildcard
587
+ const remainingBaseSegments = baseSegments.slice(baseIndex)
556
588
 
557
- let _splat: string
589
+ let _splat: string
558
590
 
559
- // If this is a wildcard with prefix/suffix, we need to handle the first segment specially
560
- if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
561
- if (!baseSegment) return false
591
+ // If this is a wildcard with prefix/suffix, we need to handle the first segment specially
592
+ if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
593
+ if (!baseSegment) return false
562
594
 
563
- const prefix = routeSegment.prefixSegment || ''
564
- const suffix = routeSegment.suffixSegment || ''
595
+ const prefix = routeSegment.prefixSegment || ''
596
+ const suffix = routeSegment.suffixSegment || ''
565
597
 
566
- // Check if the base segment starts with prefix and ends with suffix
567
- const baseValue = baseSegment.value
568
- if ('prefixSegment' in routeSegment) {
569
- if (!baseValue.startsWith(prefix)) {
570
- return false
571
- }
598
+ // Check if the base segment starts with prefix and ends with suffix
599
+ const baseValue = baseSegment.value
600
+ if ('prefixSegment' in routeSegment) {
601
+ if (!baseValue.startsWith(prefix)) {
602
+ return false
572
603
  }
573
- if ('suffixSegment' in routeSegment) {
574
- if (
575
- !baseSegments[baseSegments.length - 1]?.value.endsWith(suffix)
576
- ) {
577
- return false
578
- }
604
+ }
605
+ if ('suffixSegment' in routeSegment) {
606
+ if (
607
+ !baseSegments[baseSegments.length - 1]?.value.endsWith(suffix)
608
+ ) {
609
+ return false
579
610
  }
611
+ }
580
612
 
581
- let rejoinedSplat = decodeURI(
582
- joinPaths(remainingBaseSegments.map((d) => d.value)),
583
- )
584
-
585
- // Remove the prefix and suffix from the rejoined splat
586
- if (prefix && rejoinedSplat.startsWith(prefix)) {
587
- rejoinedSplat = rejoinedSplat.slice(prefix.length)
588
- }
613
+ let rejoinedSplat = decodeURI(
614
+ joinPaths(remainingBaseSegments.map((d) => d.value)),
615
+ )
589
616
 
590
- if (suffix && rejoinedSplat.endsWith(suffix)) {
591
- rejoinedSplat = rejoinedSplat.slice(
592
- 0,
593
- rejoinedSplat.length - suffix.length,
594
- )
595
- }
617
+ // Remove the prefix and suffix from the rejoined splat
618
+ if (prefix && rejoinedSplat.startsWith(prefix)) {
619
+ rejoinedSplat = rejoinedSplat.slice(prefix.length)
620
+ }
596
621
 
597
- _splat = rejoinedSplat
598
- } else {
599
- // If no prefix/suffix, just rejoin the remaining segments
600
- _splat = decodeURI(
601
- joinPaths(remainingBaseSegments.map((d) => d.value)),
622
+ if (suffix && rejoinedSplat.endsWith(suffix)) {
623
+ rejoinedSplat = rejoinedSplat.slice(
624
+ 0,
625
+ rejoinedSplat.length - suffix.length,
602
626
  )
603
627
  }
604
628
 
605
- // TODO: Deprecate *
606
- params['*'] = _splat
607
- params['_splat'] = _splat
608
- return true
629
+ _splat = rejoinedSplat
630
+ } else {
631
+ // If no prefix/suffix, just rejoin the remaining segments
632
+ _splat = decodeURI(
633
+ joinPaths(remainingBaseSegments.map((d) => d.value)),
634
+ )
609
635
  }
610
636
 
611
- if (routeSegment.type === 'pathname') {
612
- if (routeSegment.value === '/' && !baseSegment?.value) {
613
- routeIndex++
614
- continue
615
- }
637
+ // TODO: Deprecate *
638
+ params['*'] = _splat
639
+ params['_splat'] = _splat
640
+ return true
641
+ }
616
642
 
617
- if (baseSegment) {
618
- if (matchLocation.caseSensitive) {
619
- if (routeSegment.value !== baseSegment.value) {
620
- return false
621
- }
622
- } else if (
623
- routeSegment.value.toLowerCase() !==
624
- baseSegment.value.toLowerCase()
625
- ) {
643
+ if (routeSegment.type === SEGMENT_TYPE_PATHNAME) {
644
+ if (routeSegment.value === '/' && !baseSegment?.value) {
645
+ routeIndex++
646
+ continue
647
+ }
648
+
649
+ if (baseSegment) {
650
+ if (caseSensitive) {
651
+ if (routeSegment.value !== baseSegment.value) {
626
652
  return false
627
653
  }
628
- baseIndex++
629
- routeIndex++
630
- continue
631
- } else {
654
+ } else if (
655
+ routeSegment.value.toLowerCase() !== baseSegment.value.toLowerCase()
656
+ ) {
632
657
  return false
633
658
  }
659
+ baseIndex++
660
+ routeIndex++
661
+ continue
662
+ } else {
663
+ return false
634
664
  }
665
+ }
635
666
 
636
- if (routeSegment.type === 'param') {
637
- if (!baseSegment) {
667
+ if (routeSegment.type === SEGMENT_TYPE_PARAM) {
668
+ if (!baseSegment) {
669
+ return false
670
+ }
671
+
672
+ if (baseSegment.value === '/') {
673
+ return false
674
+ }
675
+
676
+ let _paramValue = ''
677
+ let matched = false
678
+
679
+ // If this param has prefix/suffix, we need to extract the actual parameter value
680
+ if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
681
+ const prefix = routeSegment.prefixSegment || ''
682
+ const suffix = routeSegment.suffixSegment || ''
683
+
684
+ // Check if the base segment starts with prefix and ends with suffix
685
+ const baseValue = baseSegment.value
686
+ if (prefix && !baseValue.startsWith(prefix)) {
638
687
  return false
639
688
  }
640
-
641
- if (baseSegment.value === '/') {
689
+ if (suffix && !baseValue.endsWith(suffix)) {
642
690
  return false
643
691
  }
644
692
 
645
- let _paramValue = ''
646
- let matched = false
693
+ let paramValue = baseValue
694
+ if (prefix && paramValue.startsWith(prefix)) {
695
+ paramValue = paramValue.slice(prefix.length)
696
+ }
697
+ if (suffix && paramValue.endsWith(suffix)) {
698
+ paramValue = paramValue.slice(0, paramValue.length - suffix.length)
699
+ }
700
+
701
+ _paramValue = decodeURIComponent(paramValue)
702
+ matched = true
703
+ } else {
704
+ // If no prefix/suffix, just decode the base segment value
705
+ _paramValue = decodeURIComponent(baseSegment.value)
706
+ matched = true
707
+ }
647
708
 
648
- // If this param has prefix/suffix, we need to extract the actual parameter value
649
- if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
650
- const prefix = routeSegment.prefixSegment || ''
651
- const suffix = routeSegment.suffixSegment || ''
709
+ if (matched) {
710
+ params[routeSegment.value.substring(1)] = _paramValue
711
+ baseIndex++
712
+ }
652
713
 
653
- // Check if the base segment starts with prefix and ends with suffix
654
- const baseValue = baseSegment.value
655
- if (prefix && !baseValue.startsWith(prefix)) {
656
- return false
657
- }
658
- if (suffix && !baseValue.endsWith(suffix)) {
659
- return false
660
- }
714
+ routeIndex++
715
+ continue
716
+ }
661
717
 
718
+ if (routeSegment.type === SEGMENT_TYPE_OPTIONAL_PARAM) {
719
+ // Optional parameters can be missing - don't fail the match
720
+ if (!baseSegment) {
721
+ // No base segment for optional param - skip this route segment
722
+ routeIndex++
723
+ continue
724
+ }
725
+
726
+ if (baseSegment.value === '/') {
727
+ // Skip slash segments for optional params
728
+ routeIndex++
729
+ continue
730
+ }
731
+
732
+ let _paramValue = ''
733
+ let matched = false
734
+
735
+ // If this optional param has prefix/suffix, we need to extract the actual parameter value
736
+ if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
737
+ const prefix = routeSegment.prefixSegment || ''
738
+ const suffix = routeSegment.suffixSegment || ''
739
+
740
+ // Check if the base segment starts with prefix and ends with suffix
741
+ const baseValue = baseSegment.value
742
+ if (
743
+ (!prefix || baseValue.startsWith(prefix)) &&
744
+ (!suffix || baseValue.endsWith(suffix))
745
+ ) {
662
746
  let paramValue = baseValue
663
747
  if (prefix && paramValue.startsWith(prefix)) {
664
748
  paramValue = paramValue.slice(prefix.length)
@@ -672,146 +756,81 @@ export function matchByPath(
672
756
 
673
757
  _paramValue = decodeURIComponent(paramValue)
674
758
  matched = true
675
- } else {
676
- // If no prefix/suffix, just decode the base segment value
677
- _paramValue = decodeURIComponent(baseSegment.value)
678
- matched = true
679
- }
680
-
681
- if (matched) {
682
- params[routeSegment.value.substring(1)] = _paramValue
683
- baseIndex++
684
759
  }
685
-
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
760
+ } else {
761
+ // For optional params without prefix/suffix, we need to check if the current
762
+ // base segment should match this optional param or a later route segment
763
+
764
+ // Look ahead to see if there's a later route segment that matches the current base segment
765
+ let shouldMatchOptional = true
766
+ for (
767
+ let lookAhead = routeIndex + 1;
768
+ lookAhead < routeSegments.length;
769
+ lookAhead++
770
+ ) {
771
+ const futureRouteSegment = routeSegments[lookAhead]
714
772
  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++
773
+ futureRouteSegment?.type === SEGMENT_TYPE_PATHNAME &&
774
+ futureRouteSegment.value === baseSegment.value
742
775
  ) {
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
- }
776
+ // The current base segment matches a future pathname segment,
777
+ // so we should skip this optional parameter
778
+ shouldMatchOptional = false
779
+ break
761
780
  }
762
781
 
763
- if (shouldMatchOptional) {
764
- // If no prefix/suffix, just decode the base segment value
765
- _paramValue = decodeURIComponent(baseSegment.value)
766
- matched = true
782
+ // If we encounter a required param or wildcard, stop looking ahead
783
+ if (
784
+ futureRouteSegment?.type === SEGMENT_TYPE_PARAM ||
785
+ futureRouteSegment?.type === SEGMENT_TYPE_WILDCARD
786
+ ) {
787
+ break
767
788
  }
768
789
  }
769
790
 
770
- if (matched) {
771
- params[routeSegment.value.substring(1)] = _paramValue
772
- baseIndex++
791
+ if (shouldMatchOptional) {
792
+ // If no prefix/suffix, just decode the base segment value
793
+ _paramValue = decodeURIComponent(baseSegment.value)
794
+ matched = true
773
795
  }
796
+ }
774
797
 
775
- routeIndex++
776
- continue
798
+ if (matched) {
799
+ params[routeSegment.value.substring(1)] = _paramValue
800
+ baseIndex++
777
801
  }
778
- }
779
802
 
780
- if (!isLastBaseSegment && isLastRouteSegment) {
781
- params['**'] = joinPaths(
782
- baseSegments.slice(baseIndex + 1).map((d) => d.value),
783
- )
784
- return !!matchLocation.fuzzy && routeSegment?.value !== '/'
803
+ routeIndex++
804
+ continue
785
805
  }
806
+ }
786
807
 
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
- }
808
+ if (!isLastBaseSegment && isLastRouteSegment) {
809
+ params['**'] = joinPaths(
810
+ baseSegments.slice(baseIndex + 1).map((d) => d.value),
811
+ )
812
+ return !!fuzzy && routeSegment?.value !== '/'
813
+ }
794
814
 
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
- }
815
+ // If we have base segments left but no route segments, it's not a match
816
+ if (baseIndex < baseSegments.length && routeIndex >= routeSegments.length) {
817
+ return false
818
+ }
819
+
820
+ // If we have route segments left but no base segments, check if remaining are optional
821
+ if (routeIndex < routeSegments.length && baseIndex >= baseSegments.length) {
822
+ // Check if all remaining route segments are optional
823
+ for (let i = routeIndex; i < routeSegments.length; i++) {
824
+ if (routeSegments[i]?.type !== SEGMENT_TYPE_OPTIONAL_PARAM) {
825
+ return false
805
826
  }
806
- // All remaining are optional, so we can finish
807
- break
808
827
  }
809
-
828
+ // All remaining are optional, so we can finish
810
829
  break
811
830
  }
812
831
 
813
- return true
814
- })()
832
+ break
833
+ }
815
834
 
816
- return isMatch ? params : undefined
835
+ return true
817
836
  }