@tanstack/router-core 1.136.4 → 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.
- package/dist/cjs/Matches.cjs.map +1 -1
- package/dist/cjs/Matches.d.cts +2 -0
- package/dist/cjs/index.cjs +0 -5
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -4
- package/dist/cjs/lru-cache.cjs +5 -0
- package/dist/cjs/lru-cache.cjs.map +1 -1
- package/dist/cjs/lru-cache.d.cts +1 -0
- package/dist/cjs/new-process-route-tree.cjs +655 -0
- package/dist/cjs/new-process-route-tree.cjs.map +1 -0
- package/dist/cjs/new-process-route-tree.d.cts +177 -0
- package/dist/cjs/path.cjs +133 -434
- package/dist/cjs/path.cjs.map +1 -1
- package/dist/cjs/path.d.cts +3 -39
- package/dist/cjs/router.cjs +47 -98
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +6 -11
- package/dist/esm/Matches.d.ts +2 -0
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/index.d.ts +1 -4
- package/dist/esm/index.js +1 -6
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lru-cache.d.ts +1 -0
- package/dist/esm/lru-cache.js +5 -0
- package/dist/esm/lru-cache.js.map +1 -1
- package/dist/esm/new-process-route-tree.d.ts +177 -0
- package/dist/esm/new-process-route-tree.js +655 -0
- package/dist/esm/new-process-route-tree.js.map +1 -0
- package/dist/esm/path.d.ts +3 -39
- package/dist/esm/path.js +133 -434
- package/dist/esm/path.js.map +1 -1
- package/dist/esm/router.d.ts +6 -11
- package/dist/esm/router.js +48 -99
- package/dist/esm/router.js.map +1 -1
- package/package.json +1 -1
- package/src/Matches.ts +2 -0
- package/src/index.ts +0 -6
- package/src/lru-cache.ts +6 -0
- package/src/new-process-route-tree.ts +1036 -0
- package/src/path.ts +168 -639
- package/src/router.ts +57 -126
- package/dist/cjs/process-route-tree.cjs +0 -144
- package/dist/cjs/process-route-tree.cjs.map +0 -1
- package/dist/cjs/process-route-tree.d.cts +0 -18
- package/dist/esm/process-route-tree.d.ts +0 -18
- package/dist/esm/process-route-tree.js +0 -144
- package/dist/esm/process-route-tree.js.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
120
|
+
cache,
|
|
179
121
|
}: ResolvePathOptions) {
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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)
|
|
168
|
+
if (last(baseSegments) === '') {
|
|
211
169
|
if (trailingSlash === 'never') {
|
|
212
170
|
baseSegments.pop()
|
|
213
171
|
}
|
|
214
172
|
} else if (trailingSlash === 'always') {
|
|
215
|
-
baseSegments.push(
|
|
173
|
+
baseSegments.push('')
|
|
216
174
|
}
|
|
217
175
|
}
|
|
218
176
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
257
|
+
if (!path || path === '/')
|
|
258
|
+
return { interpolatedPath: '/', usedParams, isMissingParams }
|
|
259
|
+
if (!path.includes('$'))
|
|
260
|
+
return { interpolatedPath: path, usedParams, isMissingParams }
|
|
433
261
|
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
438
|
-
const segmentSuffix = segment.suffixSegment || ''
|
|
272
|
+
if (start === end) continue
|
|
439
273
|
|
|
440
|
-
|
|
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
|
-
|
|
454
|
-
|
|
276
|
+
if (kind === SEGMENT_TYPE_PATHNAME) {
|
|
277
|
+
joined += '/' + path.substring(start, end)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
455
280
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
462
300
|
|
|
463
|
-
|
|
464
|
-
|
|
301
|
+
const value = encodeParam('_splat', params, decodeCharMap)
|
|
302
|
+
joined += '/' + prefix + value + suffix
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
465
305
|
|
|
466
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
+
usedParams[key] = valueRaw
|
|
486
337
|
|
|
487
|
-
|
|
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
|
-
}
|