@tanstack/router-core 1.128.4 → 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,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,217 @@ 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)
551
+ const baseSegments = parsePathname(from.startsWith('/') ? from : `/${from}`)
552
+ const routeSegments = parsePathname(to.startsWith('/') ? to : `/${to}`)
523
553
 
524
- if (!from.startsWith('/')) {
525
- baseSegments.unshift({
526
- type: 'pathname',
527
- value: '/',
528
- })
529
- }
554
+ const params: Record<string, string> = {}
530
555
 
531
- if (!to.startsWith('/')) {
532
- routeSegments.unshift({
533
- type: 'pathname',
534
- value: '/',
535
- })
536
- }
556
+ const result = isMatch(
557
+ baseSegments,
558
+ routeSegments,
559
+ params,
560
+ fuzzy,
561
+ caseSensitive,
562
+ )
537
563
 
538
- const params: Record<string, string> = {}
564
+ return result ? params : undefined
565
+ }
539
566
 
540
- const isMatch = (() => {
541
- let baseIndex = 0
542
- 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
543
576
 
544
- while (
545
- baseIndex < baseSegments.length ||
546
- routeIndex < routeSegments.length
547
- ) {
548
- const baseSegment = baseSegments[baseIndex]
549
- const routeSegment = routeSegments[routeIndex]
577
+ while (baseIndex < baseSegments.length || routeIndex < routeSegments.length) {
578
+ const baseSegment = baseSegments[baseIndex]
579
+ const routeSegment = routeSegments[routeIndex]
550
580
 
551
- const isLastBaseSegment = baseIndex >= baseSegments.length - 1
552
- const isLastRouteSegment = routeIndex >= routeSegments.length - 1
581
+ const isLastBaseSegment = baseIndex >= baseSegments.length - 1
582
+ const isLastRouteSegment = routeIndex >= routeSegments.length - 1
553
583
 
554
- if (routeSegment) {
555
- if (routeSegment.type === 'wildcard') {
556
- // Capture all remaining segments for a wildcard
557
- 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)
558
588
 
559
- let _splat: string
589
+ let _splat: string
560
590
 
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
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
564
594
 
565
- const prefix = routeSegment.prefixSegment || ''
566
- const suffix = routeSegment.suffixSegment || ''
595
+ const prefix = routeSegment.prefixSegment || ''
596
+ const suffix = routeSegment.suffixSegment || ''
567
597
 
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
- }
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
574
603
  }
575
- if ('suffixSegment' in routeSegment) {
576
- if (
577
- !baseSegments[baseSegments.length - 1]?.value.endsWith(suffix)
578
- ) {
579
- return false
580
- }
604
+ }
605
+ if ('suffixSegment' in routeSegment) {
606
+ if (
607
+ !baseSegments[baseSegments.length - 1]?.value.endsWith(suffix)
608
+ ) {
609
+ return false
581
610
  }
611
+ }
582
612
 
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
- }
613
+ let rejoinedSplat = decodeURI(
614
+ joinPaths(remainingBaseSegments.map((d) => d.value)),
615
+ )
591
616
 
592
- if (suffix && rejoinedSplat.endsWith(suffix)) {
593
- rejoinedSplat = rejoinedSplat.slice(
594
- 0,
595
- rejoinedSplat.length - suffix.length,
596
- )
597
- }
617
+ // Remove the prefix and suffix from the rejoined splat
618
+ if (prefix && rejoinedSplat.startsWith(prefix)) {
619
+ rejoinedSplat = rejoinedSplat.slice(prefix.length)
620
+ }
598
621
 
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)),
622
+ if (suffix && rejoinedSplat.endsWith(suffix)) {
623
+ rejoinedSplat = rejoinedSplat.slice(
624
+ 0,
625
+ rejoinedSplat.length - suffix.length,
604
626
  )
605
627
  }
606
628
 
607
- // TODO: Deprecate *
608
- params['*'] = _splat
609
- params['_splat'] = _splat
610
- 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
+ )
611
635
  }
612
636
 
613
- if (routeSegment.type === 'pathname') {
614
- if (routeSegment.value === '/' && !baseSegment?.value) {
615
- routeIndex++
616
- continue
617
- }
637
+ // TODO: Deprecate *
638
+ params['*'] = _splat
639
+ params['_splat'] = _splat
640
+ return true
641
+ }
618
642
 
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
- ) {
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) {
628
652
  return false
629
653
  }
630
- baseIndex++
631
- routeIndex++
632
- continue
633
- } else {
654
+ } else if (
655
+ routeSegment.value.toLowerCase() !== baseSegment.value.toLowerCase()
656
+ ) {
634
657
  return false
635
658
  }
659
+ baseIndex++
660
+ routeIndex++
661
+ continue
662
+ } else {
663
+ return false
636
664
  }
665
+ }
637
666
 
638
- if (routeSegment.type === 'param') {
639
- 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)) {
640
687
  return false
641
688
  }
642
-
643
- if (baseSegment.value === '/') {
689
+ if (suffix && !baseValue.endsWith(suffix)) {
644
690
  return false
645
691
  }
646
692
 
647
- let _paramValue = ''
648
- 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
+ }
649
708
 
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 || ''
709
+ if (matched) {
710
+ params[routeSegment.value.substring(1)] = _paramValue
711
+ baseIndex++
712
+ }
654
713
 
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
- }
714
+ routeIndex++
715
+ continue
716
+ }
663
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
+ ) {
664
746
  let paramValue = baseValue
665
747
  if (prefix && paramValue.startsWith(prefix)) {
666
748
  paramValue = paramValue.slice(prefix.length)
@@ -674,146 +756,81 @@ export function matchByPath(
674
756
 
675
757
  _paramValue = decodeURIComponent(paramValue)
676
758
  matched = true
677
- } else {
678
- // If no prefix/suffix, just decode the base segment value
679
- _paramValue = decodeURIComponent(baseSegment.value)
680
- matched = true
681
- }
682
-
683
- if (matched) {
684
- params[routeSegment.value.substring(1)] = _paramValue
685
- baseIndex++
686
759
  }
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
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]
716
772
  if (
717
- (!prefix || baseValue.startsWith(prefix)) &&
718
- (!suffix || baseValue.endsWith(suffix))
719
- ) {
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++
773
+ futureRouteSegment?.type === SEGMENT_TYPE_PATHNAME &&
774
+ futureRouteSegment.value === baseSegment.value
744
775
  ) {
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
- }
776
+ // The current base segment matches a future pathname segment,
777
+ // so we should skip this optional parameter
778
+ shouldMatchOptional = false
779
+ break
763
780
  }
764
781
 
765
- if (shouldMatchOptional) {
766
- // If no prefix/suffix, just decode the base segment value
767
- _paramValue = decodeURIComponent(baseSegment.value)
768
- 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
769
788
  }
770
789
  }
771
790
 
772
- if (matched) {
773
- params[routeSegment.value.substring(1)] = _paramValue
774
- baseIndex++
791
+ if (shouldMatchOptional) {
792
+ // If no prefix/suffix, just decode the base segment value
793
+ _paramValue = decodeURIComponent(baseSegment.value)
794
+ matched = true
775
795
  }
796
+ }
776
797
 
777
- routeIndex++
778
- continue
798
+ if (matched) {
799
+ params[routeSegment.value.substring(1)] = _paramValue
800
+ baseIndex++
779
801
  }
780
- }
781
802
 
782
- if (!isLastBaseSegment && isLastRouteSegment) {
783
- params['**'] = joinPaths(
784
- baseSegments.slice(baseIndex + 1).map((d) => d.value),
785
- )
786
- return !!matchLocation.fuzzy && routeSegment?.value !== '/'
803
+ routeIndex++
804
+ continue
787
805
  }
806
+ }
788
807
 
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
- }
808
+ if (!isLastBaseSegment && isLastRouteSegment) {
809
+ params['**'] = joinPaths(
810
+ baseSegments.slice(baseIndex + 1).map((d) => d.value),
811
+ )
812
+ return !!fuzzy && routeSegment?.value !== '/'
813
+ }
796
814
 
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
- }
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
807
826
  }
808
- // All remaining are optional, so we can finish
809
- break
810
827
  }
811
-
828
+ // All remaining are optional, so we can finish
812
829
  break
813
830
  }
814
831
 
815
- return true
816
- })()
832
+ break
833
+ }
817
834
 
818
- return isMatch ? params : undefined
835
+ return true
819
836
  }