@tanstack/router-core 1.136.3 → 1.136.5

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.
Files changed (70) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +2 -0
  3. package/dist/cjs/index.cjs +0 -5
  4. package/dist/cjs/index.cjs.map +1 -1
  5. package/dist/cjs/index.d.cts +1 -4
  6. package/dist/cjs/lru-cache.cjs +5 -0
  7. package/dist/cjs/lru-cache.cjs.map +1 -1
  8. package/dist/cjs/lru-cache.d.cts +1 -0
  9. package/dist/cjs/new-process-route-tree.cjs +655 -0
  10. package/dist/cjs/new-process-route-tree.cjs.map +1 -0
  11. package/dist/cjs/new-process-route-tree.d.cts +177 -0
  12. package/dist/cjs/path.cjs +133 -434
  13. package/dist/cjs/path.cjs.map +1 -1
  14. package/dist/cjs/path.d.cts +3 -39
  15. package/dist/cjs/router.cjs +47 -98
  16. package/dist/cjs/router.cjs.map +1 -1
  17. package/dist/cjs/router.d.cts +7 -11
  18. package/dist/cjs/ssr/constants.cjs.map +1 -1
  19. package/dist/cjs/ssr/constants.d.cts +1 -0
  20. package/dist/cjs/ssr/ssr-client.cjs +2 -0
  21. package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
  22. package/dist/cjs/ssr/ssr-client.d.cts +4 -1
  23. package/dist/cjs/ssr/ssr-server.cjs +64 -12
  24. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  25. package/dist/cjs/ssr/tsrScript.cjs +1 -1
  26. package/dist/cjs/ssr/tsrScript.cjs.map +1 -1
  27. package/dist/esm/Matches.d.ts +2 -0
  28. package/dist/esm/Matches.js.map +1 -1
  29. package/dist/esm/index.d.ts +1 -4
  30. package/dist/esm/index.js +1 -6
  31. package/dist/esm/index.js.map +1 -1
  32. package/dist/esm/lru-cache.d.ts +1 -0
  33. package/dist/esm/lru-cache.js +5 -0
  34. package/dist/esm/lru-cache.js.map +1 -1
  35. package/dist/esm/new-process-route-tree.d.ts +177 -0
  36. package/dist/esm/new-process-route-tree.js +655 -0
  37. package/dist/esm/new-process-route-tree.js.map +1 -0
  38. package/dist/esm/path.d.ts +3 -39
  39. package/dist/esm/path.js +133 -434
  40. package/dist/esm/path.js.map +1 -1
  41. package/dist/esm/router.d.ts +7 -11
  42. package/dist/esm/router.js +48 -99
  43. package/dist/esm/router.js.map +1 -1
  44. package/dist/esm/ssr/constants.d.ts +1 -0
  45. package/dist/esm/ssr/constants.js.map +1 -1
  46. package/dist/esm/ssr/ssr-client.d.ts +4 -1
  47. package/dist/esm/ssr/ssr-client.js +2 -0
  48. package/dist/esm/ssr/ssr-client.js.map +1 -1
  49. package/dist/esm/ssr/ssr-server.js +64 -12
  50. package/dist/esm/ssr/ssr-server.js.map +1 -1
  51. package/dist/esm/ssr/tsrScript.js +1 -1
  52. package/dist/esm/ssr/tsrScript.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/Matches.ts +2 -0
  55. package/src/index.ts +0 -6
  56. package/src/lru-cache.ts +6 -0
  57. package/src/new-process-route-tree.ts +1036 -0
  58. package/src/path.ts +168 -639
  59. package/src/router.ts +58 -126
  60. package/src/ssr/constants.ts +1 -0
  61. package/src/ssr/ssr-client.ts +10 -1
  62. package/src/ssr/ssr-server.ts +69 -12
  63. package/src/ssr/tsrScript.ts +4 -0
  64. package/dist/cjs/process-route-tree.cjs +0 -144
  65. package/dist/cjs/process-route-tree.cjs.map +0 -1
  66. package/dist/cjs/process-route-tree.d.cts +0 -18
  67. package/dist/esm/process-route-tree.d.ts +0 -18
  68. package/dist/esm/process-route-tree.js +0 -144
  69. package/dist/esm/process-route-tree.js.map +0 -1
  70. package/src/process-route-tree.ts +0 -241
package/src/path.ts CHANGED
@@ -1,25 +1,12 @@
1
1
  import { last } from './utils'
2
+ import {
3
+ SEGMENT_TYPE_OPTIONAL_PARAM,
4
+ SEGMENT_TYPE_PARAM,
5
+ SEGMENT_TYPE_PATHNAME,
6
+ SEGMENT_TYPE_WILDCARD,
7
+ parseSegment,
8
+ } from './new-process-route-tree'
2
9
  import type { LRUCache } from './lru-cache'
3
- import type { MatchLocation } from './RouterProvider'
4
- import type { AnyPathParams } from './route'
5
-
6
- export const SEGMENT_TYPE_PATHNAME = 0
7
- export const SEGMENT_TYPE_PARAM = 1
8
- export const SEGMENT_TYPE_WILDCARD = 2
9
- export const SEGMENT_TYPE_OPTIONAL_PARAM = 3
10
-
11
- export interface Segment {
12
- readonly type:
13
- | typeof SEGMENT_TYPE_PATHNAME
14
- | typeof SEGMENT_TYPE_PARAM
15
- | typeof SEGMENT_TYPE_WILDCARD
16
- | typeof SEGMENT_TYPE_OPTIONAL_PARAM
17
- readonly value: string
18
- readonly prefixSegment?: string
19
- readonly suffixSegment?: string
20
- // Indicates if there is a static segment after this required/optional param
21
- readonly hasStaticAfter?: boolean
22
- }
23
10
 
24
11
  /** Join path segments, cleaning duplicate slashes between parts. */
25
12
  /** Join path segments, cleaning duplicate slashes between parts. */
@@ -119,52 +106,7 @@ interface ResolvePathOptions {
119
106
  base: string
120
107
  to: string
121
108
  trailingSlash?: 'always' | 'never' | 'preserve'
122
- parseCache?: ParsePathnameCache
123
- }
124
-
125
- function segmentToString(segment: Segment): string {
126
- const { type, value } = segment
127
- if (type === SEGMENT_TYPE_PATHNAME) {
128
- return value
129
- }
130
-
131
- const { prefixSegment, suffixSegment } = segment
132
-
133
- if (type === SEGMENT_TYPE_PARAM) {
134
- const param = value.substring(1)
135
- if (prefixSegment && suffixSegment) {
136
- return `${prefixSegment}{$${param}}${suffixSegment}`
137
- } else if (prefixSegment) {
138
- return `${prefixSegment}{$${param}}`
139
- } else if (suffixSegment) {
140
- return `{$${param}}${suffixSegment}`
141
- }
142
- }
143
-
144
- if (type === SEGMENT_TYPE_OPTIONAL_PARAM) {
145
- const param = value.substring(1)
146
- if (prefixSegment && suffixSegment) {
147
- return `${prefixSegment}{-$${param}}${suffixSegment}`
148
- } else if (prefixSegment) {
149
- return `${prefixSegment}{-$${param}}`
150
- } else if (suffixSegment) {
151
- return `{-$${param}}${suffixSegment}`
152
- }
153
- return `{-$${param}}`
154
- }
155
-
156
- if (type === SEGMENT_TYPE_WILDCARD) {
157
- if (prefixSegment && suffixSegment) {
158
- return `${prefixSegment}{$}${suffixSegment}`
159
- } else if (prefixSegment) {
160
- return `${prefixSegment}{$}`
161
- } else if (suffixSegment) {
162
- return `{$}${suffixSegment}`
163
- }
164
- }
165
-
166
- // This case should never happen, should we throw instead?
167
- return value
109
+ cache?: LRUCache<string, string>
168
110
  }
169
111
 
170
112
  /**
@@ -175,203 +117,92 @@ export function resolvePath({
175
117
  base,
176
118
  to,
177
119
  trailingSlash = 'never',
178
- parseCache,
120
+ cache,
179
121
  }: ResolvePathOptions) {
180
- let baseSegments = parsePathname(base, parseCache).slice()
181
- const toSegments = parsePathname(to, parseCache)
182
-
183
- if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
184
- baseSegments.pop()
122
+ const isAbsolute = to.startsWith('/')
123
+ const isBase = !isAbsolute && to === '.'
124
+
125
+ let key
126
+ if (cache) {
127
+ // `trailingSlash` is static per router, so it doesn't need to be part of the cache key
128
+ key = isAbsolute ? to : isBase ? base : base + '\0' + to
129
+ const cached = cache.get(key)
130
+ if (cached) return cached
185
131
  }
186
132
 
187
- for (let index = 0, length = toSegments.length; index < length; index++) {
188
- const toSegment = toSegments[index]!
189
- const value = toSegment.value
190
- if (value === '/') {
191
- if (!index) {
192
- // Leading slash
193
- baseSegments = [toSegment]
194
- } else if (index === length - 1) {
195
- // Trailing Slash
196
- baseSegments.push(toSegment)
133
+ let baseSegments: Array<string>
134
+ if (isBase) {
135
+ baseSegments = base.split('/')
136
+ } else if (isAbsolute) {
137
+ baseSegments = to.split('/')
138
+ } else {
139
+ baseSegments = base.split('/')
140
+ while (baseSegments.length > 1 && last(baseSegments) === '') {
141
+ baseSegments.pop()
142
+ }
143
+
144
+ const toSegments = to.split('/')
145
+ for (let index = 0, length = toSegments.length; index < length; index++) {
146
+ const value = toSegments[index]!
147
+ if (value === '') {
148
+ if (!index) {
149
+ // Leading slash
150
+ baseSegments = [value]
151
+ } else if (index === length - 1) {
152
+ // Trailing Slash
153
+ baseSegments.push(value)
154
+ } else {
155
+ // ignore inter-slashes
156
+ }
157
+ } else if (value === '..') {
158
+ baseSegments.pop()
159
+ } else if (value === '.') {
160
+ // ignore
197
161
  } else {
198
- // ignore inter-slashes
162
+ baseSegments.push(value)
199
163
  }
200
- } else if (value === '..') {
201
- baseSegments.pop()
202
- } else if (value === '.') {
203
- // ignore
204
- } else {
205
- baseSegments.push(toSegment)
206
164
  }
207
165
  }
208
166
 
209
167
  if (baseSegments.length > 1) {
210
- if (last(baseSegments)!.value === '/') {
168
+ if (last(baseSegments) === '') {
211
169
  if (trailingSlash === 'never') {
212
170
  baseSegments.pop()
213
171
  }
214
172
  } else if (trailingSlash === 'always') {
215
- baseSegments.push({ type: SEGMENT_TYPE_PATHNAME, value: '/' })
173
+ baseSegments.push('')
216
174
  }
217
175
  }
218
176
 
219
- const segmentValues = baseSegments.map(segmentToString)
220
- // const joined = joinPaths([basepath, ...segmentValues])
221
- const joined = joinPaths(segmentValues)
222
- return joined
223
- }
224
-
225
- export type ParsePathnameCache = LRUCache<string, ReadonlyArray<Segment>>
226
-
227
- /**
228
- * Parse a pathname into an array of typed segments used by the router's
229
- * matcher. Results are optionally cached via an LRU cache.
230
- */
231
- /**
232
- * Parse a pathname into an array of typed segments used by the router's
233
- * matcher. Results are optionally cached via an LRU cache.
234
- */
235
- export const parsePathname = (
236
- pathname?: string,
237
- cache?: ParsePathnameCache,
238
- ): ReadonlyArray<Segment> => {
239
- if (!pathname) return []
240
- const cached = cache?.get(pathname)
241
- if (cached) return cached
242
- const parsed = baseParsePathname(pathname)
243
- cache?.set(pathname, parsed)
244
- return parsed
245
- }
246
-
247
- const PARAM_RE = /^\$.{1,}$/ // $paramName
248
- const PARAM_W_CURLY_BRACES_RE = /^(.*?)\{(\$[a-zA-Z_$][a-zA-Z0-9_$]*)\}(.*)$/ // prefix{$paramName}suffix
249
- const OPTIONAL_PARAM_W_CURLY_BRACES_RE =
250
- /^(.*?)\{-(\$[a-zA-Z_$][a-zA-Z0-9_$]*)\}(.*)$/ // prefix{-$paramName}suffix
251
-
252
- const WILDCARD_RE = /^\$$/ // $
253
- const WILDCARD_W_CURLY_BRACES_RE = /^(.*?)\{\$\}(.*)$/ // prefix{$}suffix
254
-
255
- /**
256
- * Required: `/foo/$bar` ✅
257
- * Prefix and Suffix: `/foo/prefix${bar}suffix` ✅
258
- * Wildcard: `/foo/$` ✅
259
- * Wildcard with Prefix and Suffix: `/foo/prefix{$}suffix` ✅
260
- *
261
- * Optional param: `/foo/{-$bar}`
262
- * Optional param with Prefix and Suffix: `/foo/prefix{-$bar}suffix`
263
-
264
- * Future:
265
- * Optional named segment: `/foo/{bar}`
266
- * Optional named segment with Prefix and Suffix: `/foo/prefix{-bar}suffix`
267
- * Escape special characters:
268
- * - `/foo/[$]` - Static route
269
- * - `/foo/[$]{$foo} - Dynamic route with a static prefix of `$`
270
- * - `/foo/{$foo}[$]` - Dynamic route with a static suffix of `$`
271
- */
272
- function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
273
- pathname = cleanPath(pathname)
274
-
275
- const segments: Array<Segment> = []
276
-
277
- if (pathname.slice(0, 1) === '/') {
278
- pathname = pathname.substring(1)
279
- segments.push({
280
- type: SEGMENT_TYPE_PATHNAME,
281
- value: '/',
282
- })
283
- }
284
-
285
- if (!pathname) {
286
- return segments
287
- }
288
-
289
- // Remove empty segments and '.' segments
290
- const split = pathname.split('/').filter(Boolean)
291
-
292
- segments.push(
293
- ...split.map((part): Segment => {
294
- // Check for wildcard with curly braces: prefix{$}suffix
295
- const wildcardBracesMatch = part.match(WILDCARD_W_CURLY_BRACES_RE)
296
- if (wildcardBracesMatch) {
297
- const prefix = wildcardBracesMatch[1]
298
- const suffix = wildcardBracesMatch[2]
299
- return {
300
- type: SEGMENT_TYPE_WILDCARD,
301
- value: '$',
302
- prefixSegment: prefix || undefined,
303
- suffixSegment: suffix || undefined,
304
- }
305
- }
306
-
307
- // Check for optional parameter format: prefix{-$paramName}suffix
308
- const optionalParamBracesMatch = part.match(
309
- OPTIONAL_PARAM_W_CURLY_BRACES_RE,
310
- )
311
- if (optionalParamBracesMatch) {
312
- const prefix = optionalParamBracesMatch[1]
313
- const paramName = optionalParamBracesMatch[2]!
314
- const suffix = optionalParamBracesMatch[3]
315
- return {
316
- type: SEGMENT_TYPE_OPTIONAL_PARAM,
317
- value: paramName, // Now just $paramName (no prefix)
318
- prefixSegment: prefix || undefined,
319
- suffixSegment: suffix || undefined,
320
- }
321
- }
322
-
323
- // Check for the new parameter format: prefix{$paramName}suffix
324
- const paramBracesMatch = part.match(PARAM_W_CURLY_BRACES_RE)
325
- if (paramBracesMatch) {
326
- const prefix = paramBracesMatch[1]
327
- const paramName = paramBracesMatch[2]
328
- const suffix = paramBracesMatch[3]
329
- return {
330
- type: SEGMENT_TYPE_PARAM,
331
- value: '' + paramName,
332
- prefixSegment: prefix || undefined,
333
- suffixSegment: suffix || undefined,
334
- }
335
- }
336
-
337
- // Check for bare parameter format: $paramName (without curly braces)
338
- if (PARAM_RE.test(part)) {
339
- const paramName = part.substring(1)
340
- return {
341
- type: SEGMENT_TYPE_PARAM,
342
- value: '$' + paramName,
343
- prefixSegment: undefined,
344
- suffixSegment: undefined,
345
- }
346
- }
347
-
348
- // Check for bare wildcard: $ (without curly braces)
349
- if (WILDCARD_RE.test(part)) {
350
- return {
351
- type: SEGMENT_TYPE_WILDCARD,
352
- value: '$',
353
- prefixSegment: undefined,
354
- suffixSegment: undefined,
355
- }
356
- }
357
-
358
- // Handle regular pathname segment
359
- return {
360
- type: SEGMENT_TYPE_PATHNAME,
361
- value: part,
362
- }
363
- }),
364
- )
365
-
366
- if (pathname.slice(-1) === '/') {
367
- pathname = pathname.substring(1)
368
- segments.push({
369
- type: SEGMENT_TYPE_PATHNAME,
370
- value: '/',
371
- })
177
+ let segment
178
+ let joined = ''
179
+ for (let i = 0; i < baseSegments.length; i++) {
180
+ if (i > 0) joined += '/'
181
+ const part = baseSegments[i]!
182
+ if (!part) continue
183
+ segment = parseSegment(part, 0, segment)
184
+ const kind = segment[0]
185
+ if (kind === SEGMENT_TYPE_PATHNAME) {
186
+ joined += part
187
+ continue
188
+ }
189
+ const end = segment[5]
190
+ const prefix = part.substring(0, segment[1])
191
+ const suffix = part.substring(segment[4], end)
192
+ const value = part.substring(segment[2], segment[3])
193
+ if (kind === SEGMENT_TYPE_PARAM) {
194
+ joined += prefix || suffix ? `${prefix}{$${value}}${suffix}` : `$${value}`
195
+ } else if (kind === SEGMENT_TYPE_WILDCARD) {
196
+ joined += prefix || suffix ? `${prefix}{$}${suffix}` : '$'
197
+ } else {
198
+ // SEGMENT_TYPE_OPTIONAL_PARAM
199
+ joined += `${prefix}{-$${value}}${suffix}`
200
+ }
372
201
  }
373
-
374
- return segments
202
+ joined = cleanPath(joined)
203
+ const result = joined || '/'
204
+ if (key && cache) cache.set(key, result)
205
+ return result
375
206
  }
376
207
 
377
208
  interface InterpolatePathOptions {
@@ -379,7 +210,6 @@ interface InterpolatePathOptions {
379
210
  params: Record<string, unknown>
380
211
  // Map of encoded chars to decoded chars (e.g. '%40' -> '@') that should remain decoded in path params
381
212
  decodeCharMap?: Map<string, string>
382
- parseCache?: ParsePathnameCache
383
213
  }
384
214
 
385
215
  type InterPolatePathResult = {
@@ -387,6 +217,23 @@ type InterPolatePathResult = {
387
217
  usedParams: Record<string, unknown>
388
218
  isMissingParams: boolean // true if any params were not available when being looked up in the params object
389
219
  }
220
+
221
+ function encodeParam(
222
+ key: string,
223
+ params: InterpolatePathOptions['params'],
224
+ decodeCharMap: InterpolatePathOptions['decodeCharMap'],
225
+ ): any {
226
+ const value = params[key]
227
+ if (typeof value !== 'string') return value
228
+
229
+ if (key === '_splat') {
230
+ // the splat/catch-all routes shouldn't have the '/' encoded out
231
+ return encodeURI(value)
232
+ } else {
233
+ return encodePathParam(value, decodeCharMap)
234
+ }
235
+ }
236
+
390
237
  /**
391
238
  * Interpolate params and wildcards into a route path template.
392
239
  *
@@ -401,95 +248,103 @@ export function interpolatePath({
401
248
  path,
402
249
  params,
403
250
  decodeCharMap,
404
- parseCache,
405
251
  }: InterpolatePathOptions): InterPolatePathResult {
406
- const interpolatedPathSegments = parsePathname(path, parseCache)
407
-
408
- function encodeParam(key: string): any {
409
- const value = params[key]
410
- const isValueString = typeof value === 'string'
411
-
412
- if (key === '*' || key === '_splat') {
413
- // the splat/catch-all routes shouldn't have the '/' encoded out
414
- return isValueString ? encodeURI(value) : value
415
- } else {
416
- return isValueString ? encodePathParam(value, decodeCharMap) : value
417
- }
418
- }
419
-
420
252
  // Tracking if any params are missing in the `params` object
421
253
  // when interpolating the path
422
254
  let isMissingParams = false
423
-
424
255
  const usedParams: Record<string, unknown> = {}
425
- const interpolatedPath = joinPaths(
426
- interpolatedPathSegments.map((segment) => {
427
- if (segment.type === SEGMENT_TYPE_PATHNAME) {
428
- return segment.value
429
- }
430
256
 
431
- if (segment.type === SEGMENT_TYPE_WILDCARD) {
432
- usedParams._splat = params._splat
257
+ if (!path || path === '/')
258
+ return { interpolatedPath: '/', usedParams, isMissingParams }
259
+ if (!path.includes('$'))
260
+ return { interpolatedPath: path, usedParams, isMissingParams }
433
261
 
434
- // TODO: Deprecate *
435
- usedParams['*'] = params._splat
262
+ const length = path.length
263
+ let cursor = 0
264
+ let segment
265
+ let joined = ''
266
+ while (cursor < length) {
267
+ const start = cursor
268
+ segment = parseSegment(path, start, segment)
269
+ const end = segment[5]
270
+ cursor = end + 1
436
271
 
437
- const segmentPrefix = segment.prefixSegment || ''
438
- const segmentSuffix = segment.suffixSegment || ''
272
+ if (start === end) continue
439
273
 
440
- // Check if _splat parameter is missing. _splat could be missing if undefined or an empty string or some other falsy value.
441
- if (!params._splat) {
442
- isMissingParams = true
443
- // For missing splat parameters, just return the prefix and suffix without the wildcard
444
- // If there is a prefix or suffix, return them joined, otherwise omit the segment
445
- if (segmentPrefix || segmentSuffix) {
446
- return `${segmentPrefix}${segmentSuffix}`
447
- }
448
- return undefined
449
- }
450
-
451
- const value = encodeParam('_splat')
274
+ const kind = segment[0]
452
275
 
453
- return `${segmentPrefix}${value}${segmentSuffix}`
454
- }
276
+ if (kind === SEGMENT_TYPE_PATHNAME) {
277
+ joined += '/' + path.substring(start, end)
278
+ continue
279
+ }
455
280
 
456
- if (segment.type === SEGMENT_TYPE_PARAM) {
457
- const key = segment.value.substring(1)
458
- if (!isMissingParams && !(key in params)) {
459
- isMissingParams = true
281
+ if (kind === SEGMENT_TYPE_WILDCARD) {
282
+ const splat = params._splat
283
+ usedParams._splat = splat
284
+ // TODO: Deprecate *
285
+ usedParams['*'] = splat
286
+
287
+ const prefix = path.substring(start, segment[1])
288
+ const suffix = path.substring(segment[4], end)
289
+
290
+ // Check if _splat parameter is missing. _splat could be missing if undefined or an empty string or some other falsy value.
291
+ if (!splat) {
292
+ isMissingParams = true
293
+ // For missing splat parameters, just return the prefix and suffix without the wildcard
294
+ // If there is a prefix or suffix, return them joined, otherwise omit the segment
295
+ if (prefix || suffix) {
296
+ joined += '/' + prefix + suffix
460
297
  }
461
- usedParams[key] = params[key]
298
+ continue
299
+ }
462
300
 
463
- const segmentPrefix = segment.prefixSegment || ''
464
- const segmentSuffix = segment.suffixSegment || ''
301
+ const value = encodeParam('_splat', params, decodeCharMap)
302
+ joined += '/' + prefix + value + suffix
303
+ continue
304
+ }
465
305
 
466
- return `${segmentPrefix}${encodeParam(key) ?? 'undefined'}${segmentSuffix}`
306
+ if (kind === SEGMENT_TYPE_PARAM) {
307
+ const key = path.substring(segment[2], segment[3])
308
+ if (!isMissingParams && !(key in params)) {
309
+ isMissingParams = true
467
310
  }
311
+ usedParams[key] = params[key]
468
312
 
469
- if (segment.type === SEGMENT_TYPE_OPTIONAL_PARAM) {
470
- const key = segment.value.substring(1)
313
+ const prefix = path.substring(start, segment[1])
314
+ const suffix = path.substring(segment[4], end)
315
+ const value = encodeParam(key, params, decodeCharMap) ?? 'undefined'
316
+ joined += '/' + prefix + value + suffix
317
+ continue
318
+ }
471
319
 
472
- const segmentPrefix = segment.prefixSegment || ''
473
- const segmentSuffix = segment.suffixSegment || ''
320
+ if (kind === SEGMENT_TYPE_OPTIONAL_PARAM) {
321
+ const key = path.substring(segment[2], segment[3])
322
+ const prefix = path.substring(start, segment[1])
323
+ const suffix = path.substring(segment[4], end)
324
+ const valueRaw = params[key]
474
325
 
475
- // Check if optional parameter is missing or undefined
476
- if (!(key in params) || params[key] == null) {
326
+ // Check if optional parameter is missing or undefined
327
+ if (valueRaw == null) {
328
+ if (prefix || suffix) {
477
329
  // For optional params with prefix/suffix, keep the prefix/suffix but omit the param
478
- if (segmentPrefix || segmentSuffix) {
479
- return `${segmentPrefix}${segmentSuffix}`
480
- }
481
- // If no prefix/suffix, omit the entire segment
482
- return undefined
330
+ joined += '/' + prefix + suffix
483
331
  }
332
+ // If no prefix/suffix, omit the entire segment
333
+ continue
334
+ }
484
335
 
485
- usedParams[key] = params[key]
336
+ usedParams[key] = valueRaw
486
337
 
487
- return `${segmentPrefix}${encodeParam(key) ?? ''}${segmentSuffix}`
488
- }
338
+ const value = encodeParam(key, params, decodeCharMap) ?? ''
339
+ joined += '/' + prefix + value + suffix
340
+ continue
341
+ }
342
+ }
343
+
344
+ if (path.endsWith('/')) joined += '/'
345
+
346
+ const interpolatedPath = joined || '/'
489
347
 
490
- return segment.value
491
- }),
492
- )
493
348
  return { usedParams, interpolatedPath, isMissingParams }
494
349
  }
495
350
 
@@ -502,329 +357,3 @@ function encodePathParam(value: string, decodeCharMap?: Map<string, string>) {
502
357
  }
503
358
  return encoded
504
359
  }
505
-
506
- /**
507
- * Match a pathname against a route destination and return extracted params
508
- * or `undefined`. Uses the same parsing as the router for consistency.
509
- */
510
- /**
511
- * Match a pathname against a route destination and return extracted params
512
- * or `undefined`. Uses the same parsing as the router for consistency.
513
- */
514
- export function matchPathname(
515
- currentPathname: string,
516
- matchLocation: Pick<MatchLocation, 'to' | 'fuzzy' | 'caseSensitive'>,
517
- parseCache?: ParsePathnameCache,
518
- ): AnyPathParams | undefined {
519
- const pathParams = matchByPath(currentPathname, matchLocation, parseCache)
520
- // const searchMatched = matchBySearch(location.search, matchLocation)
521
-
522
- if (matchLocation.to && !pathParams) {
523
- return
524
- }
525
-
526
- return pathParams ?? {}
527
- }
528
-
529
- /** Low-level matcher that compares two path strings and extracts params. */
530
- /** Low-level matcher that compares two path strings and extracts params. */
531
- export function matchByPath(
532
- from: string,
533
- {
534
- to,
535
- fuzzy,
536
- caseSensitive,
537
- }: Pick<MatchLocation, 'to' | 'caseSensitive' | 'fuzzy'>,
538
- parseCache?: ParsePathnameCache,
539
- ): Record<string, string> | undefined {
540
- const stringTo = to as string
541
-
542
- // Parse the from and to
543
- const baseSegments = parsePathname(
544
- from.startsWith('/') ? from : `/${from}`,
545
- parseCache,
546
- )
547
- const routeSegments = parsePathname(
548
- stringTo.startsWith('/') ? stringTo : `/${stringTo}`,
549
- parseCache,
550
- )
551
-
552
- const params: Record<string, string> = {}
553
-
554
- const result = isMatch(
555
- baseSegments,
556
- routeSegments,
557
- params,
558
- fuzzy,
559
- caseSensitive,
560
- )
561
-
562
- return result ? params : undefined
563
- }
564
-
565
- function isMatch(
566
- baseSegments: ReadonlyArray<Segment>,
567
- routeSegments: ReadonlyArray<Segment>,
568
- params: Record<string, string>,
569
- fuzzy?: boolean,
570
- caseSensitive?: boolean,
571
- ): boolean {
572
- let baseIndex = 0
573
- let routeIndex = 0
574
-
575
- while (baseIndex < baseSegments.length || routeIndex < routeSegments.length) {
576
- const baseSegment = baseSegments[baseIndex]
577
- const routeSegment = routeSegments[routeIndex]
578
-
579
- if (routeSegment) {
580
- if (routeSegment.type === SEGMENT_TYPE_WILDCARD) {
581
- // Capture all remaining segments for a wildcard
582
- const remainingBaseSegments = baseSegments.slice(baseIndex)
583
-
584
- let _splat: string
585
-
586
- // If this is a wildcard with prefix/suffix, we need to handle the first segment specially
587
- if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
588
- if (!baseSegment) return false
589
-
590
- const prefix = routeSegment.prefixSegment || ''
591
- const suffix = routeSegment.suffixSegment || ''
592
-
593
- // Check if the base segment starts with prefix and ends with suffix
594
- const baseValue = baseSegment.value
595
- if ('prefixSegment' in routeSegment) {
596
- if (!baseValue.startsWith(prefix)) {
597
- return false
598
- }
599
- }
600
- if ('suffixSegment' in routeSegment) {
601
- if (
602
- !baseSegments[baseSegments.length - 1]?.value.endsWith(suffix)
603
- ) {
604
- return false
605
- }
606
- }
607
-
608
- let rejoinedSplat = decodeURI(
609
- joinPaths(remainingBaseSegments.map((d) => d.value)),
610
- )
611
-
612
- // Remove the prefix and suffix from the rejoined splat
613
- if (prefix && rejoinedSplat.startsWith(prefix)) {
614
- rejoinedSplat = rejoinedSplat.slice(prefix.length)
615
- }
616
-
617
- if (suffix && rejoinedSplat.endsWith(suffix)) {
618
- rejoinedSplat = rejoinedSplat.slice(
619
- 0,
620
- rejoinedSplat.length - suffix.length,
621
- )
622
- }
623
-
624
- _splat = rejoinedSplat
625
- } else {
626
- // If no prefix/suffix, just rejoin the remaining segments
627
- _splat = decodeURI(
628
- joinPaths(remainingBaseSegments.map((d) => d.value)),
629
- )
630
- }
631
-
632
- // TODO: Deprecate *
633
- params['*'] = _splat
634
- params['_splat'] = _splat
635
- return true
636
- }
637
-
638
- if (routeSegment.type === SEGMENT_TYPE_PATHNAME) {
639
- if (routeSegment.value === '/' && !baseSegment?.value) {
640
- routeIndex++
641
- continue
642
- }
643
-
644
- if (baseSegment) {
645
- if (caseSensitive) {
646
- if (routeSegment.value !== baseSegment.value) {
647
- return false
648
- }
649
- } else if (
650
- routeSegment.value.toLowerCase() !== baseSegment.value.toLowerCase()
651
- ) {
652
- return false
653
- }
654
- baseIndex++
655
- routeIndex++
656
- continue
657
- } else {
658
- return false
659
- }
660
- }
661
-
662
- if (routeSegment.type === SEGMENT_TYPE_PARAM) {
663
- if (!baseSegment) {
664
- return false
665
- }
666
-
667
- if (baseSegment.value === '/') {
668
- return false
669
- }
670
-
671
- let _paramValue = ''
672
- let matched = false
673
-
674
- // If this param has prefix/suffix, we need to extract the actual parameter value
675
- if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
676
- const prefix = routeSegment.prefixSegment || ''
677
- const suffix = routeSegment.suffixSegment || ''
678
-
679
- // Check if the base segment starts with prefix and ends with suffix
680
- const baseValue = baseSegment.value
681
- if (prefix && !baseValue.startsWith(prefix)) {
682
- return false
683
- }
684
- if (suffix && !baseValue.endsWith(suffix)) {
685
- return false
686
- }
687
-
688
- let paramValue = baseValue
689
- if (prefix && paramValue.startsWith(prefix)) {
690
- paramValue = paramValue.slice(prefix.length)
691
- }
692
- if (suffix && paramValue.endsWith(suffix)) {
693
- paramValue = paramValue.slice(0, paramValue.length - suffix.length)
694
- }
695
-
696
- _paramValue = decodeURIComponent(paramValue)
697
- matched = true
698
- } else {
699
- // If no prefix/suffix, just decode the base segment value
700
- _paramValue = decodeURIComponent(baseSegment.value)
701
- matched = true
702
- }
703
-
704
- if (matched) {
705
- params[routeSegment.value.substring(1)] = _paramValue
706
- baseIndex++
707
- }
708
-
709
- routeIndex++
710
- continue
711
- }
712
-
713
- if (routeSegment.type === SEGMENT_TYPE_OPTIONAL_PARAM) {
714
- // Optional parameters can be missing - don't fail the match
715
- if (!baseSegment) {
716
- // No base segment for optional param - skip this route segment
717
- routeIndex++
718
- continue
719
- }
720
-
721
- if (baseSegment.value === '/') {
722
- // Skip slash segments for optional params
723
- routeIndex++
724
- continue
725
- }
726
-
727
- let _paramValue = ''
728
- let matched = false
729
-
730
- // If this optional param has prefix/suffix, we need to extract the actual parameter value
731
- if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
732
- const prefix = routeSegment.prefixSegment || ''
733
- const suffix = routeSegment.suffixSegment || ''
734
-
735
- // Check if the base segment starts with prefix and ends with suffix
736
- const baseValue = baseSegment.value
737
- if (
738
- (!prefix || baseValue.startsWith(prefix)) &&
739
- (!suffix || baseValue.endsWith(suffix))
740
- ) {
741
- let paramValue = baseValue
742
- if (prefix && paramValue.startsWith(prefix)) {
743
- paramValue = paramValue.slice(prefix.length)
744
- }
745
- if (suffix && paramValue.endsWith(suffix)) {
746
- paramValue = paramValue.slice(
747
- 0,
748
- paramValue.length - suffix.length,
749
- )
750
- }
751
-
752
- _paramValue = decodeURIComponent(paramValue)
753
- matched = true
754
- }
755
- } else {
756
- // For optional params without prefix/suffix, we need to check if the current
757
- // base segment should match this optional param or a later route segment
758
-
759
- // Look ahead to see if there's a later route segment that matches the current base segment
760
- let shouldMatchOptional = true
761
- for (
762
- let lookAhead = routeIndex + 1;
763
- lookAhead < routeSegments.length;
764
- lookAhead++
765
- ) {
766
- const futureRouteSegment = routeSegments[lookAhead]
767
- if (
768
- futureRouteSegment?.type === SEGMENT_TYPE_PATHNAME &&
769
- futureRouteSegment.value === baseSegment.value
770
- ) {
771
- // The current base segment matches a future pathname segment,
772
- // so we should skip this optional parameter
773
- shouldMatchOptional = false
774
- break
775
- }
776
-
777
- // If we encounter a required param or wildcard, stop looking ahead
778
- if (
779
- futureRouteSegment?.type === SEGMENT_TYPE_PARAM ||
780
- futureRouteSegment?.type === SEGMENT_TYPE_WILDCARD
781
- ) {
782
- if (baseSegments.length < routeSegments.length) {
783
- shouldMatchOptional = false
784
- }
785
- break
786
- }
787
- }
788
-
789
- if (shouldMatchOptional) {
790
- // If no prefix/suffix, just decode the base segment value
791
- _paramValue = decodeURIComponent(baseSegment.value)
792
- matched = true
793
- }
794
- }
795
-
796
- if (matched) {
797
- params[routeSegment.value.substring(1)] = _paramValue
798
- baseIndex++
799
- }
800
-
801
- routeIndex++
802
- continue
803
- }
804
- }
805
-
806
- // If we have base segments left but no route segments, it's a fuzzy match
807
- if (baseIndex < baseSegments.length && routeIndex >= routeSegments.length) {
808
- params['**'] = joinPaths(
809
- baseSegments.slice(baseIndex).map((d) => d.value),
810
- )
811
- return !!fuzzy && routeSegments[routeSegments.length - 1]?.value !== '/'
812
- }
813
-
814
- // If we have route segments left but no base segments, check if remaining are optional
815
- if (routeIndex < routeSegments.length && baseIndex >= baseSegments.length) {
816
- // Check if all remaining route segments are optional
817
- for (let i = routeIndex; i < routeSegments.length; i++) {
818
- if (routeSegments[i]?.type !== SEGMENT_TYPE_OPTIONAL_PARAM) {
819
- return false
820
- }
821
- }
822
- // All remaining are optional, so we can finish
823
- break
824
- }
825
-
826
- break
827
- }
828
-
829
- return true
830
- }