@tanstack/router-core 1.145.11 → 1.146.0
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/new-process-route-tree.cjs +137 -33
- package/dist/cjs/new-process-route-tree.cjs.map +1 -1
- package/dist/cjs/new-process-route-tree.d.cts +50 -6
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +39 -0
- package/dist/cjs/router.cjs +33 -23
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +5 -0
- package/dist/esm/new-process-route-tree.d.ts +50 -6
- package/dist/esm/new-process-route-tree.js +137 -33
- package/dist/esm/new-process-route-tree.js.map +1 -1
- package/dist/esm/route.d.ts +39 -0
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +5 -0
- package/dist/esm/router.js +33 -23
- package/dist/esm/router.js.map +1 -1
- package/package.json +1 -1
- package/src/new-process-route-tree.ts +250 -49
- package/src/route.ts +39 -2
- package/src/router.ts +39 -25
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ export const SEGMENT_TYPE_PARAM = 1
|
|
|
8
8
|
export const SEGMENT_TYPE_WILDCARD = 2
|
|
9
9
|
export const SEGMENT_TYPE_OPTIONAL_PARAM = 3
|
|
10
10
|
const SEGMENT_TYPE_INDEX = 4
|
|
11
|
+
const SEGMENT_TYPE_PATHLESS = 5 // only used in matching to represent pathless routes that need to carry more information
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* All the kinds of segments that can be present in a route path.
|
|
@@ -21,7 +22,10 @@ export type SegmentKind =
|
|
|
21
22
|
/**
|
|
22
23
|
* All the kinds of segments that can be present in the segment tree.
|
|
23
24
|
*/
|
|
24
|
-
type ExtendedSegmentKind =
|
|
25
|
+
type ExtendedSegmentKind =
|
|
26
|
+
| SegmentKind
|
|
27
|
+
| typeof SEGMENT_TYPE_INDEX
|
|
28
|
+
| typeof SEGMENT_TYPE_PATHLESS
|
|
25
29
|
|
|
26
30
|
const PARAM_W_CURLY_BRACES_RE =
|
|
27
31
|
/^([^{]*)\{\$([a-zA-Z_$][a-zA-Z0-9_$]*)\}([^}]*)$/ // prefix{$paramName}suffix
|
|
@@ -183,6 +187,10 @@ function parseSegments<TRouteLike extends RouteLike>(
|
|
|
183
187
|
const path = route.fullPath ?? route.from
|
|
184
188
|
const length = path.length
|
|
185
189
|
const caseSensitive = route.options?.caseSensitive ?? defaultCaseSensitive
|
|
190
|
+
const skipOnParamError = !!(
|
|
191
|
+
route.options?.params?.parse &&
|
|
192
|
+
route.options?.skipRouteOnParseError?.params
|
|
193
|
+
)
|
|
186
194
|
while (cursor < length) {
|
|
187
195
|
const segment = parseSegment(path, cursor, data)
|
|
188
196
|
let nextNode: AnySegmentNode<TRouteLike>
|
|
@@ -241,12 +249,15 @@ function parseSegments<TRouteLike extends RouteLike>(
|
|
|
241
249
|
: actuallyCaseSensitive
|
|
242
250
|
? suffix_raw
|
|
243
251
|
: suffix_raw.toLowerCase()
|
|
244
|
-
const existingNode =
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
s
|
|
248
|
-
|
|
249
|
-
|
|
252
|
+
const existingNode =
|
|
253
|
+
!skipOnParamError &&
|
|
254
|
+
node.dynamic?.find(
|
|
255
|
+
(s) =>
|
|
256
|
+
!s.skipOnParamError &&
|
|
257
|
+
s.caseSensitive === actuallyCaseSensitive &&
|
|
258
|
+
s.prefix === prefix &&
|
|
259
|
+
s.suffix === suffix,
|
|
260
|
+
)
|
|
250
261
|
if (existingNode) {
|
|
251
262
|
nextNode = existingNode
|
|
252
263
|
} else {
|
|
@@ -280,12 +291,15 @@ function parseSegments<TRouteLike extends RouteLike>(
|
|
|
280
291
|
: actuallyCaseSensitive
|
|
281
292
|
? suffix_raw
|
|
282
293
|
: suffix_raw.toLowerCase()
|
|
283
|
-
const existingNode =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
s
|
|
287
|
-
|
|
288
|
-
|
|
294
|
+
const existingNode =
|
|
295
|
+
!skipOnParamError &&
|
|
296
|
+
node.optional?.find(
|
|
297
|
+
(s) =>
|
|
298
|
+
!s.skipOnParamError &&
|
|
299
|
+
s.caseSensitive === actuallyCaseSensitive &&
|
|
300
|
+
s.prefix === prefix &&
|
|
301
|
+
s.suffix === suffix,
|
|
302
|
+
)
|
|
289
303
|
if (existingNode) {
|
|
290
304
|
nextNode = existingNode
|
|
291
305
|
} else {
|
|
@@ -336,8 +350,27 @@ function parseSegments<TRouteLike extends RouteLike>(
|
|
|
336
350
|
node = nextNode
|
|
337
351
|
}
|
|
338
352
|
|
|
339
|
-
|
|
353
|
+
// create pathless node
|
|
354
|
+
if (
|
|
355
|
+
skipOnParamError &&
|
|
356
|
+
route.children &&
|
|
357
|
+
!route.isRoot &&
|
|
358
|
+
route.id &&
|
|
359
|
+
route.id.charCodeAt(route.id.lastIndexOf('/') + 1) === 95 /* '_' */
|
|
360
|
+
) {
|
|
361
|
+
const pathlessNode = createStaticNode<TRouteLike>(
|
|
362
|
+
route.fullPath ?? route.from,
|
|
363
|
+
)
|
|
364
|
+
pathlessNode.kind = SEGMENT_TYPE_PATHLESS
|
|
365
|
+
pathlessNode.parent = node
|
|
366
|
+
depth++
|
|
367
|
+
pathlessNode.depth = depth
|
|
368
|
+
node.pathless ??= []
|
|
369
|
+
node.pathless.push(pathlessNode)
|
|
370
|
+
node = pathlessNode
|
|
371
|
+
}
|
|
340
372
|
|
|
373
|
+
const isLeaf = (route.path || !route.children) && !route.isRoot
|
|
341
374
|
// create index node
|
|
342
375
|
if (isLeaf && path.endsWith('/')) {
|
|
343
376
|
const indexNode = createStaticNode<TRouteLike>(
|
|
@@ -351,6 +384,10 @@ function parseSegments<TRouteLike extends RouteLike>(
|
|
|
351
384
|
node = indexNode
|
|
352
385
|
}
|
|
353
386
|
|
|
387
|
+
node.parse = route.options?.params?.parse ?? null
|
|
388
|
+
node.skipOnParamError = skipOnParamError
|
|
389
|
+
node.parsingPriority = route.options?.skipRouteOnParseError?.priority ?? 0
|
|
390
|
+
|
|
354
391
|
// make node "matchable"
|
|
355
392
|
if (isLeaf && !node.route) {
|
|
356
393
|
node.route = route
|
|
@@ -372,9 +409,29 @@ function parseSegments<TRouteLike extends RouteLike>(
|
|
|
372
409
|
}
|
|
373
410
|
|
|
374
411
|
function sortDynamic(
|
|
375
|
-
a: {
|
|
376
|
-
|
|
412
|
+
a: {
|
|
413
|
+
prefix?: string
|
|
414
|
+
suffix?: string
|
|
415
|
+
caseSensitive: boolean
|
|
416
|
+
skipOnParamError: boolean
|
|
417
|
+
parsingPriority: number
|
|
418
|
+
},
|
|
419
|
+
b: {
|
|
420
|
+
prefix?: string
|
|
421
|
+
suffix?: string
|
|
422
|
+
caseSensitive: boolean
|
|
423
|
+
skipOnParamError: boolean
|
|
424
|
+
parsingPriority: number
|
|
425
|
+
},
|
|
377
426
|
) {
|
|
427
|
+
if (a.skipOnParamError && !b.skipOnParamError) return -1
|
|
428
|
+
if (!a.skipOnParamError && b.skipOnParamError) return 1
|
|
429
|
+
if (
|
|
430
|
+
a.skipOnParamError &&
|
|
431
|
+
b.skipOnParamError &&
|
|
432
|
+
(a.parsingPriority || b.parsingPriority)
|
|
433
|
+
)
|
|
434
|
+
return b.parsingPriority - a.parsingPriority
|
|
378
435
|
if (a.prefix && b.prefix && a.prefix !== b.prefix) {
|
|
379
436
|
if (a.prefix.startsWith(b.prefix)) return -1
|
|
380
437
|
if (b.prefix.startsWith(a.prefix)) return 1
|
|
@@ -396,6 +453,11 @@ function sortDynamic(
|
|
|
396
453
|
}
|
|
397
454
|
|
|
398
455
|
function sortTreeNodes(node: SegmentNode<RouteLike>) {
|
|
456
|
+
if (node.pathless) {
|
|
457
|
+
for (const child of node.pathless) {
|
|
458
|
+
sortTreeNodes(child)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
399
461
|
if (node.static) {
|
|
400
462
|
for (const child of node.static.values()) {
|
|
401
463
|
sortTreeNodes(child)
|
|
@@ -432,6 +494,7 @@ function createStaticNode<T extends RouteLike>(
|
|
|
432
494
|
return {
|
|
433
495
|
kind: SEGMENT_TYPE_PATHNAME,
|
|
434
496
|
depth: 0,
|
|
497
|
+
pathless: null,
|
|
435
498
|
index: null,
|
|
436
499
|
static: null,
|
|
437
500
|
staticInsensitive: null,
|
|
@@ -441,6 +504,9 @@ function createStaticNode<T extends RouteLike>(
|
|
|
441
504
|
route: null,
|
|
442
505
|
fullPath,
|
|
443
506
|
parent: null,
|
|
507
|
+
parse: null,
|
|
508
|
+
skipOnParamError: false,
|
|
509
|
+
parsingPriority: 0,
|
|
444
510
|
}
|
|
445
511
|
}
|
|
446
512
|
|
|
@@ -461,6 +527,7 @@ function createDynamicNode<T extends RouteLike>(
|
|
|
461
527
|
return {
|
|
462
528
|
kind,
|
|
463
529
|
depth: 0,
|
|
530
|
+
pathless: null,
|
|
464
531
|
index: null,
|
|
465
532
|
static: null,
|
|
466
533
|
staticInsensitive: null,
|
|
@@ -470,6 +537,9 @@ function createDynamicNode<T extends RouteLike>(
|
|
|
470
537
|
route: null,
|
|
471
538
|
fullPath,
|
|
472
539
|
parent: null,
|
|
540
|
+
parse: null,
|
|
541
|
+
skipOnParamError: false,
|
|
542
|
+
parsingPriority: 0,
|
|
473
543
|
caseSensitive,
|
|
474
544
|
prefix,
|
|
475
545
|
suffix,
|
|
@@ -477,7 +547,10 @@ function createDynamicNode<T extends RouteLike>(
|
|
|
477
547
|
}
|
|
478
548
|
|
|
479
549
|
type StaticSegmentNode<T extends RouteLike> = SegmentNode<T> & {
|
|
480
|
-
kind:
|
|
550
|
+
kind:
|
|
551
|
+
| typeof SEGMENT_TYPE_PATHNAME
|
|
552
|
+
| typeof SEGMENT_TYPE_PATHLESS
|
|
553
|
+
| typeof SEGMENT_TYPE_INDEX
|
|
481
554
|
}
|
|
482
555
|
|
|
483
556
|
type DynamicSegmentNode<T extends RouteLike> = SegmentNode<T> & {
|
|
@@ -497,6 +570,8 @@ type AnySegmentNode<T extends RouteLike> =
|
|
|
497
570
|
type SegmentNode<T extends RouteLike> = {
|
|
498
571
|
kind: ExtendedSegmentKind
|
|
499
572
|
|
|
573
|
+
pathless: Array<StaticSegmentNode<T>> | null
|
|
574
|
+
|
|
500
575
|
/** Exact index segment (highest priority) */
|
|
501
576
|
index: StaticSegmentNode<T> | null
|
|
502
577
|
|
|
@@ -524,15 +599,32 @@ type SegmentNode<T extends RouteLike> = {
|
|
|
524
599
|
parent: AnySegmentNode<T> | null
|
|
525
600
|
|
|
526
601
|
depth: number
|
|
602
|
+
|
|
603
|
+
/** route.options.params.parse function, set on the last node of the route */
|
|
604
|
+
parse: null | ((params: Record<string, string>) => any)
|
|
605
|
+
|
|
606
|
+
/** options.skipRouteOnParseError.params ?? false */
|
|
607
|
+
skipOnParamError: boolean
|
|
608
|
+
|
|
609
|
+
/** options.skipRouteOnParseError.priority ?? 0 */
|
|
610
|
+
parsingPriority: number
|
|
527
611
|
}
|
|
528
612
|
|
|
529
613
|
type RouteLike = {
|
|
614
|
+
id?: string
|
|
530
615
|
path?: string // relative path from the parent,
|
|
531
616
|
children?: Array<RouteLike> // child routes,
|
|
532
617
|
parentRoute?: RouteLike // parent route,
|
|
533
618
|
isRoot?: boolean
|
|
534
619
|
options?: {
|
|
620
|
+
skipRouteOnParseError?: {
|
|
621
|
+
params?: boolean
|
|
622
|
+
priority?: number
|
|
623
|
+
}
|
|
535
624
|
caseSensitive?: boolean
|
|
625
|
+
params?: {
|
|
626
|
+
parse?: (params: Record<string, string>) => any
|
|
627
|
+
}
|
|
536
628
|
}
|
|
537
629
|
} &
|
|
538
630
|
// router tree
|
|
@@ -621,7 +713,8 @@ export function findSingleMatch(
|
|
|
621
713
|
|
|
622
714
|
type RouteMatch<T extends Extract<RouteLike, { fullPath: string }>> = {
|
|
623
715
|
route: T
|
|
624
|
-
|
|
716
|
+
rawParams: Record<string, string>
|
|
717
|
+
parsedParams?: Record<string, unknown>
|
|
625
718
|
branch: ReadonlyArray<T>
|
|
626
719
|
}
|
|
627
720
|
|
|
@@ -718,32 +811,57 @@ function findMatch<T extends RouteLike>(
|
|
|
718
811
|
path: string,
|
|
719
812
|
segmentTree: AnySegmentNode<T>,
|
|
720
813
|
fuzzy = false,
|
|
721
|
-
): {
|
|
814
|
+
): {
|
|
815
|
+
route: T
|
|
816
|
+
/**
|
|
817
|
+
* The raw (unparsed) params extracted from the path.
|
|
818
|
+
* This will be the exhaustive list of all params defined in the route's path.
|
|
819
|
+
*/
|
|
820
|
+
rawParams: Record<string, string>
|
|
821
|
+
/**
|
|
822
|
+
* The accumlulated parsed params of each route in the branch that had `skipRouteOnParseError` enabled.
|
|
823
|
+
* Will not contain all params defined in the route's path. Those w/ a `params.parse` but no `skipRouteOnParseError` will need to be parsed separately.
|
|
824
|
+
*/
|
|
825
|
+
parsedParams?: Record<string, unknown>
|
|
826
|
+
} | null {
|
|
722
827
|
const parts = path.split('/')
|
|
723
828
|
const leaf = getNodeMatch(path, parts, segmentTree, fuzzy)
|
|
724
829
|
if (!leaf) return null
|
|
725
|
-
const
|
|
726
|
-
if ('**' in leaf) params['**'] = leaf['**']!
|
|
727
|
-
const route = leaf.node.route!
|
|
830
|
+
const [rawParams] = extractParams(path, parts, leaf)
|
|
728
831
|
return {
|
|
729
|
-
route
|
|
730
|
-
|
|
832
|
+
route: leaf.node.route!,
|
|
833
|
+
rawParams,
|
|
834
|
+
parsedParams: leaf.parsedParams,
|
|
731
835
|
}
|
|
732
836
|
}
|
|
733
837
|
|
|
838
|
+
/**
|
|
839
|
+
* This function is "resumable":
|
|
840
|
+
* - the `leaf` input can contain `extract` and `rawParams` properties from a previous `extractParams` call
|
|
841
|
+
* - the returned `state` can be passed back as `extract` in a future call to continue extracting params from where we left off
|
|
842
|
+
*
|
|
843
|
+
* Inputs are *not* mutated.
|
|
844
|
+
*/
|
|
734
845
|
function extractParams<T extends RouteLike>(
|
|
735
846
|
path: string,
|
|
736
847
|
parts: Array<string>,
|
|
737
|
-
leaf: {
|
|
738
|
-
|
|
848
|
+
leaf: {
|
|
849
|
+
node: AnySegmentNode<T>
|
|
850
|
+
skipped: number
|
|
851
|
+
extract?: { part: number; node: number; path: number }
|
|
852
|
+
rawParams?: Record<string, string>
|
|
853
|
+
},
|
|
854
|
+
): [
|
|
855
|
+
rawParams: Record<string, string>,
|
|
856
|
+
state: { part: number; node: number; path: number },
|
|
857
|
+
] {
|
|
739
858
|
const list = buildBranch(leaf.node)
|
|
740
859
|
let nodeParts: Array<string> | null = null
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
) {
|
|
860
|
+
const rawParams: Record<string, string> = {}
|
|
861
|
+
let partIndex = leaf.extract?.part ?? 0
|
|
862
|
+
let nodeIndex = leaf.extract?.node ?? 0
|
|
863
|
+
let pathIndex = leaf.extract?.path ?? 0
|
|
864
|
+
for (; nodeIndex < list.length; partIndex++, nodeIndex++, pathIndex++) {
|
|
747
865
|
const node = list[nodeIndex]!
|
|
748
866
|
const part = parts[partIndex]
|
|
749
867
|
const currentPathIndex = pathIndex
|
|
@@ -762,10 +880,10 @@ function extractParams<T extends RouteLike>(
|
|
|
762
880
|
nodePart.length - sufLength - 1,
|
|
763
881
|
)
|
|
764
882
|
const value = part!.substring(preLength, part!.length - sufLength)
|
|
765
|
-
|
|
883
|
+
rawParams[name] = decodeURIComponent(value)
|
|
766
884
|
} else {
|
|
767
885
|
const name = nodePart.substring(1)
|
|
768
|
-
|
|
886
|
+
rawParams[name] = decodeURIComponent(part!)
|
|
769
887
|
}
|
|
770
888
|
} else if (node.kind === SEGMENT_TYPE_OPTIONAL_PARAM) {
|
|
771
889
|
if (leaf.skipped & (1 << nodeIndex)) {
|
|
@@ -784,7 +902,7 @@ function extractParams<T extends RouteLike>(
|
|
|
784
902
|
node.suffix || node.prefix
|
|
785
903
|
? part!.substring(preLength, part!.length - sufLength)
|
|
786
904
|
: part
|
|
787
|
-
if (value)
|
|
905
|
+
if (value) rawParams[name] = decodeURIComponent(value)
|
|
788
906
|
} else if (node.kind === SEGMENT_TYPE_WILDCARD) {
|
|
789
907
|
const n = node
|
|
790
908
|
const value = path.substring(
|
|
@@ -793,12 +911,13 @@ function extractParams<T extends RouteLike>(
|
|
|
793
911
|
)
|
|
794
912
|
const splat = decodeURIComponent(value)
|
|
795
913
|
// TODO: Deprecate *
|
|
796
|
-
|
|
797
|
-
|
|
914
|
+
rawParams['*'] = splat
|
|
915
|
+
rawParams._splat = splat
|
|
798
916
|
break
|
|
799
917
|
}
|
|
800
918
|
}
|
|
801
|
-
|
|
919
|
+
if (leaf.rawParams) Object.assign(rawParams, leaf.rawParams)
|
|
920
|
+
return [rawParams, { part: partIndex, node: nodeIndex, path: pathIndex }]
|
|
802
921
|
}
|
|
803
922
|
|
|
804
923
|
function buildRouteBranch<T extends RouteLike>(route: T) {
|
|
@@ -836,6 +955,11 @@ type MatchStackFrame<T extends RouteLike> = {
|
|
|
836
955
|
statics: number
|
|
837
956
|
dynamics: number
|
|
838
957
|
optionals: number
|
|
958
|
+
/** intermediary state for param extraction */
|
|
959
|
+
extract?: { part: number; node: number; path: number }
|
|
960
|
+
/** intermediary params from param extraction */
|
|
961
|
+
rawParams?: Record<string, string>
|
|
962
|
+
parsedParams?: Record<string, unknown>
|
|
839
963
|
}
|
|
840
964
|
|
|
841
965
|
function getNodeMatch<T extends RouteLike>(
|
|
@@ -847,7 +971,10 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
847
971
|
// quick check for root index
|
|
848
972
|
// this is an optimization, algorithm should work correctly without this block
|
|
849
973
|
if (path === '/' && segmentTree.index)
|
|
850
|
-
return { node: segmentTree.index, skipped: 0 }
|
|
974
|
+
return { node: segmentTree.index, skipped: 0 } as Pick<
|
|
975
|
+
Frame,
|
|
976
|
+
'node' | 'skipped' | 'parsedParams'
|
|
977
|
+
>
|
|
851
978
|
|
|
852
979
|
const trailingSlash = !last(parts)
|
|
853
980
|
const pathIsIndex = trailingSlash && path !== '/'
|
|
@@ -880,8 +1007,16 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
880
1007
|
|
|
881
1008
|
while (stack.length) {
|
|
882
1009
|
const frame = stack.pop()!
|
|
883
|
-
|
|
884
|
-
let {
|
|
1010
|
+
const { node, index, skipped, depth, statics, dynamics, optionals } = frame
|
|
1011
|
+
let { extract, rawParams, parsedParams } = frame
|
|
1012
|
+
|
|
1013
|
+
if (node.skipOnParamError) {
|
|
1014
|
+
const result = validateMatchParams(path, parts, frame)
|
|
1015
|
+
if (!result) continue
|
|
1016
|
+
rawParams = frame.rawParams
|
|
1017
|
+
extract = frame.extract
|
|
1018
|
+
parsedParams = frame.parsedParams
|
|
1019
|
+
}
|
|
885
1020
|
|
|
886
1021
|
// In fuzzy mode, track the best partial match we've found so far
|
|
887
1022
|
if (
|
|
@@ -898,8 +1033,9 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
898
1033
|
if (node.route && !pathIsIndex && isFrameMoreSpecific(bestMatch, frame)) {
|
|
899
1034
|
bestMatch = frame
|
|
900
1035
|
}
|
|
901
|
-
// beyond the length of the path parts, only
|
|
902
|
-
if (!node.optional && !node.wildcard && !node.index)
|
|
1036
|
+
// beyond the length of the path parts, only some segment types can match
|
|
1037
|
+
if (!node.optional && !node.wildcard && !node.index && !node.pathless)
|
|
1038
|
+
continue
|
|
903
1039
|
}
|
|
904
1040
|
|
|
905
1041
|
const part = isBeyondPath ? undefined : parts[index]!
|
|
@@ -915,6 +1051,13 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
915
1051
|
statics,
|
|
916
1052
|
dynamics,
|
|
917
1053
|
optionals,
|
|
1054
|
+
extract,
|
|
1055
|
+
rawParams,
|
|
1056
|
+
parsedParams,
|
|
1057
|
+
}
|
|
1058
|
+
if (node.index.skipOnParamError) {
|
|
1059
|
+
const result = validateMatchParams(path, parts, indexFrame)
|
|
1060
|
+
if (!result) continue
|
|
918
1061
|
}
|
|
919
1062
|
// perfect match, no need to continue
|
|
920
1063
|
// this is an optimization, algorithm should work correctly without this block
|
|
@@ -946,7 +1089,7 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
946
1089
|
}
|
|
947
1090
|
// the first wildcard match is the highest priority one
|
|
948
1091
|
// wildcard matches skip the stack because they cannot have children
|
|
949
|
-
|
|
1092
|
+
const frame = {
|
|
950
1093
|
node: segment,
|
|
951
1094
|
index: partsLength,
|
|
952
1095
|
skipped,
|
|
@@ -954,7 +1097,15 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
954
1097
|
statics,
|
|
955
1098
|
dynamics,
|
|
956
1099
|
optionals,
|
|
1100
|
+
extract,
|
|
1101
|
+
rawParams,
|
|
1102
|
+
parsedParams,
|
|
957
1103
|
}
|
|
1104
|
+
if (segment.skipOnParamError) {
|
|
1105
|
+
const result = validateMatchParams(path, parts, frame)
|
|
1106
|
+
if (!result) continue
|
|
1107
|
+
}
|
|
1108
|
+
wildcardMatch = frame
|
|
958
1109
|
break
|
|
959
1110
|
}
|
|
960
1111
|
}
|
|
@@ -974,6 +1125,9 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
974
1125
|
statics,
|
|
975
1126
|
dynamics,
|
|
976
1127
|
optionals,
|
|
1128
|
+
extract,
|
|
1129
|
+
rawParams,
|
|
1130
|
+
parsedParams,
|
|
977
1131
|
}) // enqueue skipping the optional
|
|
978
1132
|
}
|
|
979
1133
|
if (!isBeyondPath) {
|
|
@@ -995,6 +1149,9 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
995
1149
|
statics,
|
|
996
1150
|
dynamics,
|
|
997
1151
|
optionals: optionals + 1,
|
|
1152
|
+
extract,
|
|
1153
|
+
rawParams,
|
|
1154
|
+
parsedParams,
|
|
998
1155
|
})
|
|
999
1156
|
}
|
|
1000
1157
|
}
|
|
@@ -1020,6 +1177,9 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
1020
1177
|
statics,
|
|
1021
1178
|
dynamics: dynamics + 1,
|
|
1022
1179
|
optionals,
|
|
1180
|
+
extract,
|
|
1181
|
+
rawParams,
|
|
1182
|
+
parsedParams,
|
|
1023
1183
|
})
|
|
1024
1184
|
}
|
|
1025
1185
|
}
|
|
@@ -1038,6 +1198,9 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
1038
1198
|
statics: statics + 1,
|
|
1039
1199
|
dynamics,
|
|
1040
1200
|
optionals,
|
|
1201
|
+
extract,
|
|
1202
|
+
rawParams,
|
|
1203
|
+
parsedParams,
|
|
1041
1204
|
})
|
|
1042
1205
|
}
|
|
1043
1206
|
}
|
|
@@ -1054,6 +1217,29 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
1054
1217
|
statics: statics + 1,
|
|
1055
1218
|
dynamics,
|
|
1056
1219
|
optionals,
|
|
1220
|
+
extract,
|
|
1221
|
+
rawParams,
|
|
1222
|
+
parsedParams,
|
|
1223
|
+
})
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// 0. Try pathless match
|
|
1228
|
+
if (node.pathless) {
|
|
1229
|
+
const nextDepth = depth + 1
|
|
1230
|
+
for (let i = node.pathless.length - 1; i >= 0; i--) {
|
|
1231
|
+
const segment = node.pathless[i]!
|
|
1232
|
+
stack.push({
|
|
1233
|
+
node: segment,
|
|
1234
|
+
index,
|
|
1235
|
+
skipped,
|
|
1236
|
+
depth: nextDepth,
|
|
1237
|
+
statics,
|
|
1238
|
+
dynamics,
|
|
1239
|
+
optionals,
|
|
1240
|
+
extract,
|
|
1241
|
+
rawParams,
|
|
1242
|
+
parsedParams,
|
|
1057
1243
|
})
|
|
1058
1244
|
}
|
|
1059
1245
|
}
|
|
@@ -1075,16 +1261,31 @@ function getNodeMatch<T extends RouteLike>(
|
|
|
1075
1261
|
sliceIndex += parts[i]!.length
|
|
1076
1262
|
}
|
|
1077
1263
|
const splat = sliceIndex === path.length ? '/' : path.slice(sliceIndex)
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
'**': decodeURIComponent(splat),
|
|
1082
|
-
}
|
|
1264
|
+
bestFuzzy.rawParams ??= {}
|
|
1265
|
+
bestFuzzy.rawParams['**'] = decodeURIComponent(splat)
|
|
1266
|
+
return bestFuzzy
|
|
1083
1267
|
}
|
|
1084
1268
|
|
|
1085
1269
|
return null
|
|
1086
1270
|
}
|
|
1087
1271
|
|
|
1272
|
+
function validateMatchParams<T extends RouteLike>(
|
|
1273
|
+
path: string,
|
|
1274
|
+
parts: Array<string>,
|
|
1275
|
+
frame: MatchStackFrame<T>,
|
|
1276
|
+
) {
|
|
1277
|
+
try {
|
|
1278
|
+
const [rawParams, state] = extractParams(path, parts, frame)
|
|
1279
|
+
frame.rawParams = rawParams
|
|
1280
|
+
frame.extract = state
|
|
1281
|
+
const parsed = frame.node.parse!(rawParams)
|
|
1282
|
+
frame.parsedParams = Object.assign({}, frame.parsedParams, parsed)
|
|
1283
|
+
return true
|
|
1284
|
+
} catch {
|
|
1285
|
+
return null
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1088
1289
|
function isFrameMoreSpecific(
|
|
1089
1290
|
// the stack frame previously saved as "best match"
|
|
1090
1291
|
prev: MatchStackFrame<any> | null,
|
package/src/route.ts
CHANGED
|
@@ -1188,9 +1188,46 @@ export interface UpdatableRouteOptions<
|
|
|
1188
1188
|
in out TBeforeLoadFn,
|
|
1189
1189
|
> extends UpdatableStaticRouteOption,
|
|
1190
1190
|
UpdatableRouteOptionsExtensions {
|
|
1191
|
-
|
|
1191
|
+
/**
|
|
1192
|
+
* Options to control route matching behavior with runtime code.
|
|
1193
|
+
*
|
|
1194
|
+
* @experimental 🚧 this feature is subject to change
|
|
1195
|
+
*
|
|
1196
|
+
* @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouteOptionsType
|
|
1197
|
+
*/
|
|
1198
|
+
skipRouteOnParseError?: {
|
|
1199
|
+
/**
|
|
1200
|
+
* If `true`, skip this route during matching if `params.parse` fails.
|
|
1201
|
+
*
|
|
1202
|
+
* Without this option, a `/$param` route could match *any* value for `param`,
|
|
1203
|
+
* and only later during the route lifecycle would `params.parse` run and potentially
|
|
1204
|
+
* show the `errorComponent` if validation failed.
|
|
1205
|
+
*
|
|
1206
|
+
* With this option enabled, the route will only match if `params.parse` succeeds.
|
|
1207
|
+
* If it fails, the router will continue trying to match other routes, potentially
|
|
1208
|
+
* finding a different route that works, or ultimately showing the `notFoundComponent`.
|
|
1209
|
+
*
|
|
1210
|
+
* @default false
|
|
1211
|
+
*/
|
|
1212
|
+
params?: boolean
|
|
1213
|
+
/**
|
|
1214
|
+
* In cases where multiple routes would need to run `params.parse` during matching
|
|
1215
|
+
* to determine which route to pick, this priority number can be used as a tie-breaker
|
|
1216
|
+
* for which route to try first. Higher number = higher priority.
|
|
1217
|
+
*
|
|
1218
|
+
* @default 0
|
|
1219
|
+
*/
|
|
1220
|
+
priority?: number
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* If true, this route will be matched as case-sensitive
|
|
1224
|
+
*
|
|
1225
|
+
* @default false
|
|
1226
|
+
*/
|
|
1192
1227
|
caseSensitive?: boolean
|
|
1193
|
-
|
|
1228
|
+
/**
|
|
1229
|
+
* If true, this route will be forcefully wrapped in a suspense boundary
|
|
1230
|
+
*/
|
|
1194
1231
|
wrapInSuspense?: boolean
|
|
1195
1232
|
// The content to be rendered when the route is matched. If no component is provided, defaults to `<Outlet />`
|
|
1196
1233
|
|