@tanstack/router-core 1.128.4 → 1.128.7

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