@benjavicente/router-core 1.168.9 → 1.169.2

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 (169) hide show
  1. package/dist/cjs/Matches.cjs +1 -1
  2. package/dist/cjs/Matches.cjs.map +1 -1
  3. package/dist/cjs/config.cjs +1 -1
  4. package/dist/cjs/config.cjs.map +1 -1
  5. package/dist/cjs/defer.cjs +1 -1
  6. package/dist/cjs/defer.cjs.map +1 -1
  7. package/dist/cjs/hash-scroll.cjs +1 -1
  8. package/dist/cjs/hash-scroll.cjs.map +1 -1
  9. package/dist/cjs/index.cjs +4 -0
  10. package/dist/cjs/index.d.cts +3 -3
  11. package/dist/cjs/isServer/client.cjs +1 -1
  12. package/dist/cjs/isServer/client.cjs.map +1 -1
  13. package/dist/cjs/isServer/development.cjs +1 -1
  14. package/dist/cjs/isServer/development.cjs.map +1 -1
  15. package/dist/cjs/isServer/server.cjs +1 -1
  16. package/dist/cjs/isServer/server.cjs.map +1 -1
  17. package/dist/cjs/link.cjs +1 -1
  18. package/dist/cjs/link.cjs.map +1 -1
  19. package/dist/cjs/load-matches.cjs +26 -26
  20. package/dist/cjs/load-matches.cjs.map +1 -1
  21. package/dist/cjs/manifest.cjs +31 -0
  22. package/dist/cjs/manifest.cjs.map +1 -1
  23. package/dist/cjs/manifest.d.cts +8 -0
  24. package/dist/cjs/new-process-route-tree.cjs +55 -67
  25. package/dist/cjs/new-process-route-tree.cjs.map +1 -1
  26. package/dist/cjs/new-process-route-tree.d.cts +4 -26
  27. package/dist/cjs/not-found.cjs +1 -1
  28. package/dist/cjs/not-found.cjs.map +1 -1
  29. package/dist/cjs/path.cjs +1 -22
  30. package/dist/cjs/path.cjs.map +1 -1
  31. package/dist/cjs/root.cjs +1 -1
  32. package/dist/cjs/root.cjs.map +1 -1
  33. package/dist/cjs/route.cjs.map +1 -1
  34. package/dist/cjs/route.d.cts +6 -34
  35. package/dist/cjs/router.cjs +94 -80
  36. package/dist/cjs/router.cjs.map +1 -1
  37. package/dist/cjs/router.d.cts +6 -5
  38. package/dist/cjs/scroll-restoration-script/server.cjs +1 -1
  39. package/dist/cjs/scroll-restoration-script/server.cjs.map +1 -1
  40. package/dist/cjs/scroll-restoration.cjs +7 -7
  41. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  42. package/dist/cjs/searchParams.cjs +2 -2
  43. package/dist/cjs/searchParams.cjs.map +1 -1
  44. package/dist/cjs/ssr/constants.cjs +2 -2
  45. package/dist/cjs/ssr/constants.cjs.map +1 -1
  46. package/dist/cjs/ssr/createRequestHandler.cjs +2 -2
  47. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  48. package/dist/cjs/ssr/serializer/RawStream.cjs +50 -41
  49. package/dist/cjs/ssr/serializer/RawStream.cjs.map +1 -1
  50. package/dist/cjs/ssr/serializer/RawStream.d.cts +12 -4
  51. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs +1 -1
  52. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs.map +1 -1
  53. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.d.cts +2 -2
  54. package/dist/cjs/ssr/serializer/seroval-plugins.cjs +1 -1
  55. package/dist/cjs/ssr/serializer/seroval-plugins.cjs.map +1 -1
  56. package/dist/cjs/ssr/serializer/seroval-plugins.d.cts +2 -1
  57. package/dist/cjs/ssr/serializer/transformer.cjs +16 -14
  58. package/dist/cjs/ssr/serializer/transformer.cjs.map +1 -1
  59. package/dist/cjs/ssr/serializer/transformer.d.cts +24 -23
  60. package/dist/cjs/ssr/ssr-client.cjs +9 -9
  61. package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
  62. package/dist/cjs/ssr/ssr-server.cjs +110 -22
  63. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  64. package/dist/cjs/ssr/ssr-server.d.cts +4 -2
  65. package/dist/cjs/ssr/transformStreamWithRouter.cjs +10 -7
  66. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
  67. package/dist/cjs/stores.cjs +57 -57
  68. package/dist/cjs/stores.cjs.map +1 -1
  69. package/dist/cjs/stores.d.cts +16 -16
  70. package/dist/cjs/utils.cjs +12 -7
  71. package/dist/cjs/utils.cjs.map +1 -1
  72. package/dist/cjs/utils.d.cts +1 -0
  73. package/dist/esm/Matches.js +1 -1
  74. package/dist/esm/Matches.js.map +1 -1
  75. package/dist/esm/config.js +1 -1
  76. package/dist/esm/config.js.map +1 -1
  77. package/dist/esm/defer.js +1 -1
  78. package/dist/esm/defer.js.map +1 -1
  79. package/dist/esm/hash-scroll.js +1 -1
  80. package/dist/esm/hash-scroll.js.map +1 -1
  81. package/dist/esm/index.d.ts +3 -3
  82. package/dist/esm/index.js +3 -3
  83. package/dist/esm/isServer/client.js +1 -1
  84. package/dist/esm/isServer/client.js.map +1 -1
  85. package/dist/esm/isServer/development.js +1 -1
  86. package/dist/esm/isServer/development.js.map +1 -1
  87. package/dist/esm/isServer/server.js +1 -1
  88. package/dist/esm/isServer/server.js.map +1 -1
  89. package/dist/esm/link.js +1 -1
  90. package/dist/esm/link.js.map +1 -1
  91. package/dist/esm/load-matches.js +26 -26
  92. package/dist/esm/load-matches.js.map +1 -1
  93. package/dist/esm/manifest.d.ts +8 -0
  94. package/dist/esm/manifest.js +28 -1
  95. package/dist/esm/manifest.js.map +1 -1
  96. package/dist/esm/new-process-route-tree.d.ts +4 -26
  97. package/dist/esm/new-process-route-tree.js +55 -68
  98. package/dist/esm/new-process-route-tree.js.map +1 -1
  99. package/dist/esm/not-found.js +1 -1
  100. package/dist/esm/not-found.js.map +1 -1
  101. package/dist/esm/path.js +1 -22
  102. package/dist/esm/path.js.map +1 -1
  103. package/dist/esm/root.js +1 -1
  104. package/dist/esm/root.js.map +1 -1
  105. package/dist/esm/route.d.ts +6 -34
  106. package/dist/esm/route.js.map +1 -1
  107. package/dist/esm/router.d.ts +6 -5
  108. package/dist/esm/router.js +96 -82
  109. package/dist/esm/router.js.map +1 -1
  110. package/dist/esm/scroll-restoration-script/server.js +1 -1
  111. package/dist/esm/scroll-restoration-script/server.js.map +1 -1
  112. package/dist/esm/scroll-restoration.js +7 -7
  113. package/dist/esm/scroll-restoration.js.map +1 -1
  114. package/dist/esm/searchParams.js +2 -2
  115. package/dist/esm/searchParams.js.map +1 -1
  116. package/dist/esm/ssr/constants.js +2 -2
  117. package/dist/esm/ssr/constants.js.map +1 -1
  118. package/dist/esm/ssr/createRequestHandler.js +2 -2
  119. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  120. package/dist/esm/ssr/serializer/RawStream.d.ts +12 -4
  121. package/dist/esm/ssr/serializer/RawStream.js +50 -41
  122. package/dist/esm/ssr/serializer/RawStream.js.map +1 -1
  123. package/dist/esm/ssr/serializer/ShallowErrorPlugin.d.ts +2 -2
  124. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js +1 -1
  125. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js.map +1 -1
  126. package/dist/esm/ssr/serializer/seroval-plugins.d.ts +2 -1
  127. package/dist/esm/ssr/serializer/seroval-plugins.js +1 -1
  128. package/dist/esm/ssr/serializer/seroval-plugins.js.map +1 -1
  129. package/dist/esm/ssr/serializer/transformer.d.ts +24 -23
  130. package/dist/esm/ssr/serializer/transformer.js +16 -14
  131. package/dist/esm/ssr/serializer/transformer.js.map +1 -1
  132. package/dist/esm/ssr/ssr-client.js +9 -9
  133. package/dist/esm/ssr/ssr-client.js.map +1 -1
  134. package/dist/esm/ssr/ssr-server.d.ts +4 -2
  135. package/dist/esm/ssr/ssr-server.js +110 -22
  136. package/dist/esm/ssr/ssr-server.js.map +1 -1
  137. package/dist/esm/ssr/transformStreamWithRouter.js +10 -7
  138. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
  139. package/dist/esm/stores.d.ts +16 -16
  140. package/dist/esm/stores.js +58 -58
  141. package/dist/esm/stores.js.map +1 -1
  142. package/dist/esm/utils.d.ts +1 -0
  143. package/dist/esm/utils.js +12 -8
  144. package/dist/esm/utils.js.map +1 -1
  145. package/package.json +4 -8
  146. package/skills/router-core/auth-and-guards/SKILL.md +41 -0
  147. package/skills/router-core/ssr/SKILL.md +59 -5
  148. package/skills/router-core/type-safety/SKILL.md +37 -42
  149. package/src/hash-scroll.ts +1 -1
  150. package/src/index.ts +9 -2
  151. package/src/load-matches.ts +14 -15
  152. package/src/manifest.ts +46 -0
  153. package/src/new-process-route-tree.ts +93 -106
  154. package/src/not-found.ts +1 -1
  155. package/src/path.ts +1 -27
  156. package/src/route.ts +10 -36
  157. package/src/router.ts +151 -140
  158. package/src/scroll-restoration.ts +1 -1
  159. package/src/ssr/createRequestHandler.ts +4 -5
  160. package/src/ssr/serializer/RawStream.ts +65 -56
  161. package/src/ssr/serializer/ShallowErrorPlugin.ts +2 -2
  162. package/src/ssr/serializer/seroval-plugins.ts +2 -1
  163. package/src/ssr/serializer/transformer.ts +71 -76
  164. package/src/ssr/ssr-client.ts +8 -12
  165. package/src/ssr/ssr-server.ts +170 -14
  166. package/src/ssr/transformStreamWithRouter.ts +3 -0
  167. package/src/stores.ts +86 -86
  168. package/src/utils.ts +7 -0
  169. package/bin/intent.js +0 -25
@@ -207,10 +207,8 @@ function parseSegments<TRouteLike extends RouteLike>(
207
207
  const path = route.fullPath ?? route.from
208
208
  const length = path.length
209
209
  const caseSensitive = route.options?.caseSensitive ?? defaultCaseSensitive
210
- const skipOnParamError = !!(
211
- route.options?.params?.parse &&
212
- route.options?.skipRouteOnParseError?.params
213
- )
210
+ const parseParams =
211
+ route.options?.params?.parse ?? route.options?.parseParams
214
212
  while (cursor < length) {
215
213
  const segment = parseSegment(path, cursor, data)
216
214
  let nextNode: AnySegmentNode<TRouteLike>
@@ -270,10 +268,10 @@ function parseSegments<TRouteLike extends RouteLike>(
270
268
  ? suffix_raw
271
269
  : suffix_raw.toLowerCase()
272
270
  const existingNode =
273
- !skipOnParamError &&
271
+ !parseParams &&
274
272
  node.dynamic?.find(
275
273
  (s) =>
276
- !s.skipOnParamError &&
274
+ !s.parse &&
277
275
  s.caseSensitive === actuallyCaseSensitive &&
278
276
  s.prefix === prefix &&
279
277
  s.suffix === suffix,
@@ -312,10 +310,10 @@ function parseSegments<TRouteLike extends RouteLike>(
312
310
  ? suffix_raw
313
311
  : suffix_raw.toLowerCase()
314
312
  const existingNode =
315
- !skipOnParamError &&
313
+ !parseParams &&
316
314
  node.optional?.find(
317
315
  (s) =>
318
- !s.skipOnParamError &&
316
+ !s.parse &&
319
317
  s.caseSensitive === actuallyCaseSensitive &&
320
318
  s.prefix === prefix &&
321
319
  s.suffix === suffix,
@@ -372,7 +370,7 @@ function parseSegments<TRouteLike extends RouteLike>(
372
370
 
373
371
  // create pathless node
374
372
  if (
375
- skipOnParamError &&
373
+ parseParams &&
376
374
  route.children &&
377
375
  !route.isRoot &&
378
376
  route.id &&
@@ -404,9 +402,7 @@ function parseSegments<TRouteLike extends RouteLike>(
404
402
  node = indexNode
405
403
  }
406
404
 
407
- node.parse = route.options?.params?.parse ?? null
408
- node.skipOnParamError = skipOnParamError
409
- node.parsingPriority = route.options?.skipRouteOnParseError?.priority ?? 0
405
+ node.parse = parseParams ?? null
410
406
 
411
407
  // make node "matchable"
412
408
  if (isLeaf && !node.route) {
@@ -433,25 +429,17 @@ function sortDynamic(
433
429
  prefix?: string
434
430
  suffix?: string
435
431
  caseSensitive: boolean
436
- skipOnParamError: boolean
437
- parsingPriority: number
432
+ parse: null | ((params: Record<string, string>) => unknown)
438
433
  },
439
434
  b: {
440
435
  prefix?: string
441
436
  suffix?: string
442
437
  caseSensitive: boolean
443
- skipOnParamError: boolean
444
- parsingPriority: number
438
+ parse: null | ((params: Record<string, string>) => unknown)
445
439
  },
446
440
  ) {
447
- if (a.skipOnParamError && !b.skipOnParamError) return -1
448
- if (!a.skipOnParamError && b.skipOnParamError) return 1
449
- if (
450
- a.skipOnParamError &&
451
- b.skipOnParamError &&
452
- (a.parsingPriority || b.parsingPriority)
453
- )
454
- return b.parsingPriority - a.parsingPriority
441
+ if (a.parse && !b.parse) return -1
442
+ if (!a.parse && b.parse) return 1
455
443
  if (a.prefix && b.prefix && a.prefix !== b.prefix) {
456
444
  if (a.prefix.startsWith(b.prefix)) return -1
457
445
  if (b.prefix.startsWith(a.prefix)) return 1
@@ -467,8 +455,7 @@ function sortDynamic(
467
455
  if (a.caseSensitive && !b.caseSensitive) return -1
468
456
  if (!a.caseSensitive && b.caseSensitive) return 1
469
457
 
470
- // we don't need a tiebreaker here
471
- // at this point the 2 nodes cannot conflict during matching
458
+ // Equal specificity preserves route declaration order through stable sort.
472
459
  return 0
473
460
  }
474
461
 
@@ -525,8 +512,6 @@ function createStaticNode<T extends RouteLike>(
525
512
  fullPath,
526
513
  parent: null,
527
514
  parse: null,
528
- skipOnParamError: false,
529
- parsingPriority: 0,
530
515
  }
531
516
  }
532
517
 
@@ -558,8 +543,6 @@ function createDynamicNode<T extends RouteLike>(
558
543
  fullPath,
559
544
  parent: null,
560
545
  parse: null,
561
- skipOnParamError: false,
562
- parsingPriority: 0,
563
546
  caseSensitive,
564
547
  prefix,
565
548
  suffix,
@@ -621,13 +604,7 @@ type SegmentNode<T extends RouteLike> = {
621
604
  depth: number
622
605
 
623
606
  /** route.options.params.parse function, set on the last node of the route */
624
- parse: null | ((params: Record<string, string>) => any)
625
-
626
- /** options.skipRouteOnParseError.params ?? false */
627
- skipOnParamError: boolean
628
-
629
- /** options.skipRouteOnParseError.priority ?? 0 */
630
- parsingPriority: number
607
+ parse: null | ((params: Record<string, string>) => unknown)
631
608
  }
632
609
 
633
610
  type RouteLike = {
@@ -637,13 +614,10 @@ type RouteLike = {
637
614
  parentRoute?: RouteLike // parent route,
638
615
  isRoot?: boolean
639
616
  options?: {
640
- skipRouteOnParseError?: {
641
- params?: boolean
642
- priority?: number
643
- }
644
617
  caseSensitive?: boolean
618
+ parseParams?: (params: Record<string, string>) => unknown
645
619
  params?: {
646
- parse?: (params: Record<string, string>) => any
620
+ parse?: (params: Record<string, string>) => unknown
647
621
  }
648
622
  }
649
623
  } &
@@ -734,7 +708,6 @@ export function findSingleMatch(
734
708
  type RouteMatch<T extends Extract<RouteLike, { fullPath: string }>> = {
735
709
  route: T
736
710
  rawParams: Record<string, string>
737
- parsedParams?: Record<string, unknown>
738
711
  branch: ReadonlyArray<T>
739
712
  }
740
713
 
@@ -858,11 +831,6 @@ function findMatch<T extends RouteLike>(
858
831
  * This will be the exhaustive list of all params defined in the route's path.
859
832
  */
860
833
  rawParams: Record<string, string>
861
- /**
862
- * The accumlulated parsed params of each route in the branch that had `skipRouteOnParseError` enabled.
863
- * 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.
864
- */
865
- parsedParams?: Record<string, unknown>
866
834
  } | null {
867
835
  const parts = path.split('/')
868
836
  const leaf = getNodeMatch(path, parts, segmentTree, fuzzy)
@@ -871,7 +839,6 @@ function findMatch<T extends RouteLike>(
871
839
  return {
872
840
  route: leaf.node.route!,
873
841
  rawParams,
874
- parsedParams: leaf.parsedParams,
875
842
  }
876
843
  }
877
844
 
@@ -991,7 +958,7 @@ function extractParams<T extends RouteLike>(
991
958
  ]
992
959
  }
993
960
 
994
- function buildRouteBranch<T extends RouteLike>(route: T) {
961
+ export function buildRouteBranch<T extends RouteLike>(route: T) {
995
962
  const list = [route]
996
963
  while (route.parentRoute) {
997
964
  route = route.parentRoute as T
@@ -1023,6 +990,7 @@ type MatchStackFrame<T extends RouteLike> = {
1023
990
  * If we really really need to support more than 32 segments we can switch to using a `BigInt` here. It's about 2x slower in worst case scenarios.
1024
991
  */
1025
992
  skipped: number
993
+ /** Positional bitmasks tracking which consumed URL segments matched each segment kind. */
1026
994
  statics: number
1027
995
  dynamics: number
1028
996
  optionals: number
@@ -1030,7 +998,6 @@ type MatchStackFrame<T extends RouteLike> = {
1030
998
  extract?: ParamExtractionState
1031
999
  /** intermediary params from param extraction */
1032
1000
  rawParams?: Record<string, string>
1033
- parsedParams?: Record<string, unknown>
1034
1001
  }
1035
1002
 
1036
1003
  function getNodeMatch<T extends RouteLike>(
@@ -1044,7 +1011,7 @@ function getNodeMatch<T extends RouteLike>(
1044
1011
  if (path === '/' && segmentTree.index)
1045
1012
  return { node: segmentTree.index, skipped: 0 } as Pick<
1046
1013
  Frame,
1047
- 'node' | 'skipped' | 'parsedParams'
1014
+ 'node' | 'skipped'
1048
1015
  >
1049
1016
 
1050
1017
  const trailingSlash = !last(parts)
@@ -1066,27 +1033,37 @@ function getNodeMatch<T extends RouteLike>(
1066
1033
  index: 1,
1067
1034
  skipped: 0,
1068
1035
  depth: 1,
1069
- statics: 1,
1036
+ statics: 0,
1070
1037
  dynamics: 0,
1071
1038
  optionals: 0,
1072
1039
  },
1073
1040
  ]
1074
1041
 
1075
- let wildcardMatch: Frame | null = null
1076
1042
  let bestFuzzy: Frame | null = null
1077
1043
  let bestMatch: Frame | null = null
1078
1044
 
1079
1045
  while (stack.length) {
1080
1046
  const frame = stack.pop()!
1081
1047
  const { node, index, skipped, depth, statics, dynamics, optionals } = frame
1082
- let { extract, rawParams, parsedParams } = frame
1048
+ let { extract, rawParams } = frame
1083
1049
 
1084
- if (node.skipOnParamError) {
1085
- const result = validateMatchParams(path, parts, frame)
1050
+ // Wildcard candidates are pushed speculatively as fallbacks in case a
1051
+ // higher-priority wildcard later fails params.parse. If a better wildcard
1052
+ // has already validated and become bestMatch, lower-priority wildcard
1053
+ // fallbacks cannot win anymore and should not run params.parse.
1054
+ if (
1055
+ node.kind === SEGMENT_TYPE_WILDCARD &&
1056
+ node.route &&
1057
+ !isFrameMoreSpecific(bestMatch, frame)
1058
+ ) {
1059
+ continue
1060
+ }
1061
+
1062
+ if (node.parse) {
1063
+ const result = validateParseParams(path, parts, frame)
1086
1064
  if (!result) continue
1087
1065
  rawParams = frame.rawParams
1088
1066
  extract = frame.extract
1089
- parsedParams = frame.parsedParams
1090
1067
  }
1091
1068
 
1092
1069
  // In fuzzy mode, track the best partial match we've found so far
@@ -1101,7 +1078,13 @@ function getNodeMatch<T extends RouteLike>(
1101
1078
 
1102
1079
  const isBeyondPath = index === partsLength
1103
1080
  if (isBeyondPath) {
1104
- if (node.route && !pathIsIndex && isFrameMoreSpecific(bestMatch, frame)) {
1081
+ if (
1082
+ node.route &&
1083
+ (!pathIsIndex ||
1084
+ node.kind === SEGMENT_TYPE_INDEX ||
1085
+ node.kind === SEGMENT_TYPE_WILDCARD) &&
1086
+ isFrameMoreSpecific(bestMatch, frame)
1087
+ ) {
1105
1088
  bestMatch = frame
1106
1089
  }
1107
1090
  // beyond the length of the path parts, only some segment types can match
@@ -1124,17 +1107,21 @@ function getNodeMatch<T extends RouteLike>(
1124
1107
  optionals,
1125
1108
  extract,
1126
1109
  rawParams,
1127
- parsedParams,
1128
1110
  }
1129
1111
  let indexValid = true
1130
- if (node.index.skipOnParamError) {
1131
- const result = validateMatchParams(path, parts, indexFrame)
1112
+ if (node.index.parse) {
1113
+ const result = validateParseParams(path, parts, indexFrame)
1132
1114
  if (!result) indexValid = false
1133
1115
  }
1134
1116
  if (indexValid) {
1135
1117
  // perfect match, no need to continue
1136
1118
  // this is an optimization, algorithm should work correctly without this block
1137
- if (statics === partsLength && !dynamics && !optionals && !skipped) {
1119
+ if (
1120
+ !dynamics &&
1121
+ !optionals &&
1122
+ !skipped &&
1123
+ isPerfectStaticMatch(statics, partsLength)
1124
+ ) {
1138
1125
  return indexFrame
1139
1126
  }
1140
1127
  if (isFrameMoreSpecific(bestMatch, indexFrame)) {
@@ -1145,8 +1132,9 @@ function getNodeMatch<T extends RouteLike>(
1145
1132
  }
1146
1133
 
1147
1134
  // 5. Try wildcard match
1148
- if (node.wildcard && isFrameMoreSpecific(wildcardMatch, frame)) {
1149
- for (const segment of node.wildcard) {
1135
+ if (node.wildcard) {
1136
+ for (let i = node.wildcard.length - 1; i >= 0; i--) {
1137
+ const segment = node.wildcard[i]!
1150
1138
  const { prefix, suffix } = segment
1151
1139
  if (prefix) {
1152
1140
  if (isBeyondPath) continue
@@ -1161,26 +1149,18 @@ function getNodeMatch<T extends RouteLike>(
1161
1149
  const casePart = segment.caseSensitive ? end : end.toLowerCase()
1162
1150
  if (casePart !== suffix) continue
1163
1151
  }
1164
- // the first wildcard match is the highest priority one
1165
- // wildcard matches skip the stack because they cannot have children
1166
- const frame = {
1152
+ // wildcard matches consume the rest of the URL and cannot have children
1153
+ stack.push({
1167
1154
  node: segment,
1168
1155
  index: partsLength,
1169
1156
  skipped,
1170
- depth,
1157
+ depth: depth + 1,
1171
1158
  statics,
1172
1159
  dynamics,
1173
1160
  optionals,
1174
1161
  extract,
1175
1162
  rawParams,
1176
- parsedParams,
1177
- }
1178
- if (segment.skipOnParamError) {
1179
- const result = validateMatchParams(path, parts, frame)
1180
- if (!result) continue
1181
- }
1182
- wildcardMatch = frame
1183
- break
1163
+ })
1184
1164
  }
1185
1165
  }
1186
1166
 
@@ -1201,7 +1181,6 @@ function getNodeMatch<T extends RouteLike>(
1201
1181
  optionals,
1202
1182
  extract,
1203
1183
  rawParams,
1204
- parsedParams,
1205
1184
  }) // enqueue skipping the optional
1206
1185
  }
1207
1186
  if (!isBeyondPath) {
@@ -1222,10 +1201,9 @@ function getNodeMatch<T extends RouteLike>(
1222
1201
  depth: nextDepth,
1223
1202
  statics,
1224
1203
  dynamics,
1225
- optionals: optionals + 1,
1204
+ optionals: optionals + segmentScore(partsLength, index),
1226
1205
  extract,
1227
1206
  rawParams,
1228
- parsedParams,
1229
1207
  })
1230
1208
  }
1231
1209
  }
@@ -1249,11 +1227,10 @@ function getNodeMatch<T extends RouteLike>(
1249
1227
  skipped,
1250
1228
  depth: depth + 1,
1251
1229
  statics,
1252
- dynamics: dynamics + 1,
1230
+ dynamics: dynamics + segmentScore(partsLength, index),
1253
1231
  optionals,
1254
1232
  extract,
1255
1233
  rawParams,
1256
- parsedParams,
1257
1234
  })
1258
1235
  }
1259
1236
  }
@@ -1269,12 +1246,11 @@ function getNodeMatch<T extends RouteLike>(
1269
1246
  index: index + 1,
1270
1247
  skipped,
1271
1248
  depth: depth + 1,
1272
- statics: statics + 1,
1249
+ statics: statics + segmentScore(partsLength, index),
1273
1250
  dynamics,
1274
1251
  optionals,
1275
1252
  extract,
1276
1253
  rawParams,
1277
- parsedParams,
1278
1254
  })
1279
1255
  }
1280
1256
  }
@@ -1288,12 +1264,11 @@ function getNodeMatch<T extends RouteLike>(
1288
1264
  index: index + 1,
1289
1265
  skipped,
1290
1266
  depth: depth + 1,
1291
- statics: statics + 1,
1267
+ statics: statics + segmentScore(partsLength, index),
1292
1268
  dynamics,
1293
1269
  optionals,
1294
1270
  extract,
1295
1271
  rawParams,
1296
- parsedParams,
1297
1272
  })
1298
1273
  }
1299
1274
  }
@@ -1313,22 +1288,13 @@ function getNodeMatch<T extends RouteLike>(
1313
1288
  optionals,
1314
1289
  extract,
1315
1290
  rawParams,
1316
- parsedParams,
1317
1291
  })
1318
1292
  }
1319
1293
  }
1320
1294
  }
1321
1295
 
1322
- if (bestMatch && wildcardMatch) {
1323
- return isFrameMoreSpecific(wildcardMatch, bestMatch)
1324
- ? bestMatch
1325
- : wildcardMatch
1326
- }
1327
-
1328
1296
  if (bestMatch) return bestMatch
1329
1297
 
1330
- if (wildcardMatch) return wildcardMatch
1331
-
1332
1298
  if (fuzzy && bestFuzzy) {
1333
1299
  let sliceIndex = bestFuzzy.index
1334
1300
  for (let i = 0; i < bestFuzzy.index; i++) {
@@ -1343,25 +1309,46 @@ function getNodeMatch<T extends RouteLike>(
1343
1309
  return null
1344
1310
  }
1345
1311
 
1346
- function validateMatchParams<T extends RouteLike>(
1312
+ function segmentScore(partsLength: number, index: number): number {
1313
+ // The specificity scores are bitmasks over consumed URL segments. Earlier
1314
+ // URL segments should dominate later ones when comparing scores, so the
1315
+ // first real segment gets the highest bit and the last gets bit 0. Since
1316
+ // `parts[0]` is the empty string before the leading slash, real URL segments
1317
+ // are [1, partsLength), making this segment's bit `partsLength - index - 1`.
1318
+ return 2 ** (partsLength - index - 1)
1319
+ }
1320
+
1321
+ function isPerfectStaticMatch(statics: number, partsLength: number): boolean {
1322
+ return statics === 2 ** (partsLength - 1) - 1
1323
+ }
1324
+
1325
+ function validateParseParams<T extends RouteLike>(
1347
1326
  path: string,
1348
1327
  parts: Array<string>,
1349
1328
  frame: MatchStackFrame<T>,
1350
1329
  ) {
1330
+ let rawParams: Record<string, string>
1331
+ let state: ParamExtractionState
1332
+
1351
1333
  try {
1352
- const [rawParams, state] = extractParams(path, parts, frame)
1353
- frame.rawParams = rawParams
1354
- frame.extract = state
1355
- const parsed = frame.node.parse!(rawParams)
1356
- frame.parsedParams = Object.assign(
1357
- Object.create(null),
1358
- frame.parsedParams,
1359
- parsed,
1360
- )
1361
- return true
1334
+ ;[rawParams, state] = extractParams(path, parts, frame)
1362
1335
  } catch {
1363
1336
  return null
1364
1337
  }
1338
+
1339
+ frame.rawParams = rawParams
1340
+ frame.extract = state
1341
+
1342
+ if (!frame.node.parse) return true
1343
+
1344
+ try {
1345
+ if (frame.node.parse(rawParams) === false) return null
1346
+ } catch {
1347
+ // Thrown parse errors should be surfaced on the selected match by
1348
+ // extractStrictParams, not used as fallback route selection.
1349
+ }
1350
+
1351
+ return true
1365
1352
  }
1366
1353
 
1367
1354
  function isFrameMoreSpecific(
package/src/not-found.ts CHANGED
@@ -37,5 +37,5 @@ export function notFound(options: NotFoundError = {}) {
37
37
 
38
38
  /** Determine if a value is a TanStack Router not-found error. */
39
39
  export function isNotFound(obj: any): obj is NotFoundError {
40
- return !!obj?.isNotFound
40
+ return obj?.isNotFound === true
41
41
  }
package/src/path.ts CHANGED
@@ -167,33 +167,7 @@ export function resolvePath({
167
167
  }
168
168
  }
169
169
 
170
- let segment
171
- let joined = ''
172
- for (let i = 0; i < baseSegments.length; i++) {
173
- if (i > 0) joined += '/'
174
- const part = baseSegments[i]!
175
- if (!part) continue
176
- segment = parseSegment(part, 0, segment)
177
- const kind = segment[0]
178
- if (kind === SEGMENT_TYPE_PATHNAME) {
179
- joined += part
180
- continue
181
- }
182
- const end = segment[5]
183
- const prefix = part.substring(0, segment[1])
184
- const suffix = part.substring(segment[4], end)
185
- const value = part.substring(segment[2], segment[3])
186
- if (kind === SEGMENT_TYPE_PARAM) {
187
- joined += prefix || suffix ? `${prefix}{$${value}}${suffix}` : `$${value}`
188
- } else if (kind === SEGMENT_TYPE_WILDCARD) {
189
- joined += prefix || suffix ? `${prefix}{$}${suffix}` : '$'
190
- } else {
191
- // SEGMENT_TYPE_OPTIONAL_PARAM
192
- joined += `${prefix}{-$${value}}${suffix}`
193
- }
194
- }
195
- joined = cleanPath(joined)
196
- const result = joined || '/'
170
+ const result = cleanPath(baseSegments.join('/')) || '/'
197
171
  if (key && cache) cache.set(key, result)
198
172
  return result
199
173
  }
package/src/route.ts CHANGED
@@ -173,9 +173,13 @@ export type ResolveParams<
173
173
 
174
174
  export type ParseParamsFn<in out TPath extends string, in out TParams> = (
175
175
  rawParams: Expand<ResolveParams<TPath>>,
176
- ) => TParams extends ResolveParams<TPath, any>
177
- ? TParams
178
- : ResolveParams<TPath, any>
176
+ ) => TParams | false
177
+
178
+ type ValidateParsedParams<TPath extends string, TParams> = [TParams] extends [
179
+ ResolveParams<TPath, any>,
180
+ ]
181
+ ? unknown
182
+ : never
179
183
 
180
184
  export type StringifyParamsFn<in out TPath extends string, in out TParams> = (
181
185
  params: TParams,
@@ -183,14 +187,15 @@ export type StringifyParamsFn<in out TPath extends string, in out TParams> = (
183
187
 
184
188
  export type ParamsOptions<in out TPath extends string, in out TParams> = {
185
189
  params?: {
186
- parse?: ParseParamsFn<TPath, TParams>
190
+ parse?: ParseParamsFn<TPath, TParams> & ValidateParsedParams<TPath, TParams>
187
191
  stringify?: StringifyParamsFn<TPath, TParams>
188
192
  }
189
193
 
190
194
  /**
191
195
  @deprecated Use params.parse instead
192
196
  */
193
- parseParams?: ParseParamsFn<TPath, TParams>
197
+ parseParams?: ParseParamsFn<TPath, TParams> &
198
+ ValidateParsedParams<TPath, TParams>
194
199
 
195
200
  /**
196
201
  @deprecated Use params.stringify instead
@@ -1293,37 +1298,6 @@ export interface UpdatableRouteOptions<
1293
1298
  in out TBeforeLoadFn,
1294
1299
  >
1295
1300
  extends UpdatableStaticRouteOption, UpdatableRouteOptionsExtensions {
1296
- /**
1297
- * Options to control route matching behavior with runtime code.
1298
- *
1299
- * @experimental 🚧 this feature is subject to change
1300
- *
1301
- * @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouteOptionsType
1302
- */
1303
- skipRouteOnParseError?: {
1304
- /**
1305
- * If `true`, skip this route during matching if `params.parse` fails.
1306
- *
1307
- * Without this option, a `/$param` route could match *any* value for `param`,
1308
- * and only later during the route lifecycle would `params.parse` run and potentially
1309
- * show the `errorComponent` if validation failed.
1310
- *
1311
- * With this option enabled, the route will only match if `params.parse` succeeds.
1312
- * If it fails, the router will continue trying to match other routes, potentially
1313
- * finding a different route that works, or ultimately showing the `notFoundComponent`.
1314
- *
1315
- * @default false
1316
- */
1317
- params?: boolean
1318
- /**
1319
- * In cases where multiple routes would need to run `params.parse` during matching
1320
- * to determine which route to pick, this priority number can be used as a tie-breaker
1321
- * for which route to try first. Higher number = higher priority.
1322
- *
1323
- * @default 0
1324
- */
1325
- priority?: number
1326
- }
1327
1301
  /**
1328
1302
  * If true, this route will be matched as case-sensitive
1329
1303
  *