@tanstack/router-core 1.121.0-alpha.22 → 1.121.0-alpha.28

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