@tanstack/router-core 1.171.16 → 1.171.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.171.16",
3
+ "version": "1.171.18",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -168,9 +168,9 @@
168
168
  },
169
169
  "dependencies": {
170
170
  "cookie-es": "^3.0.0",
171
- "seroval": "^1.5.4",
172
- "seroval-plugins": "^1.5.4",
173
- "@tanstack/history": "1.162.0"
171
+ "seroval": "^1.6.2",
172
+ "seroval-plugins": "^1.6.2",
173
+ "@tanstack/history": "1.162.1"
174
174
  },
175
175
  "devDependencies": {
176
176
  "@tanstack/store": "^0.9.3",
@@ -199,16 +199,18 @@ function parseSegments<TRouteLike extends RouteLike>(
199
199
  start: number,
200
200
  node: AnySegmentNode<TRouteLike>,
201
201
  depth: number,
202
+ /** Each dynamic sibling list is recorded once, when it first needs sorting. */
203
+ dynamicListsToSort?: Array<Array<DynamicSegmentNode<TRouteLike>>>,
202
204
  onRoute?: (route: TRouteLike) => void,
203
205
  ) {
204
206
  onRoute?.(route)
205
207
  let cursor = start
206
208
  {
207
209
  const path = route.fullPath ?? route.from
210
+ const options = route.options
208
211
  const length = path.length
209
- const caseSensitive = route.options?.caseSensitive ?? defaultCaseSensitive
210
- const parseParams =
211
- route.options?.params?.parse ?? route.options?.parseParams
212
+ const caseSensitive = options?.caseSensitive ?? defaultCaseSensitive
213
+ const parseParams = options?.params?.parse ?? options?.parseParams
212
214
  while (cursor < length) {
213
215
  const segment = parseSegment(path, cursor, data)
214
216
  let nextNode: AnySegmentNode<TRouteLike>
@@ -220,81 +222,29 @@ function parseSegments<TRouteLike extends RouteLike>(
220
222
  switch (kind) {
221
223
  case SEGMENT_TYPE_PATHNAME: {
222
224
  const value = path.substring(segment[2], segment[3])
225
+ let name = value
226
+ let staticChildren: Map<string, StaticSegmentNode<TRouteLike>>
223
227
  if (caseSensitive) {
224
- const existingNode = node.static?.get(value)
225
- if (existingNode) {
226
- nextNode = existingNode
227
- } else {
228
- node.static ??= new Map()
229
- const next = createStaticNode<TRouteLike>(
230
- route.fullPath ?? route.from,
231
- )
232
- next.parent = node
233
- next.depth = depth
234
- nextNode = next
235
- node.static.set(value, next)
236
- }
228
+ staticChildren = node.static ??= new Map()
237
229
  } else {
238
- const name = value.toLowerCase()
239
- const existingNode = node.staticInsensitive?.get(name)
240
- if (existingNode) {
241
- nextNode = existingNode
242
- } else {
243
- node.staticInsensitive ??= new Map()
244
- const next = createStaticNode<TRouteLike>(
245
- route.fullPath ?? route.from,
246
- )
247
- next.parent = node
248
- next.depth = depth
249
- nextNode = next
250
- node.staticInsensitive.set(name, next)
251
- }
230
+ name = value.toLowerCase()
231
+ staticChildren = node.staticInsensitive ??= new Map()
252
232
  }
253
- break
254
- }
255
- case SEGMENT_TYPE_PARAM: {
256
- const prefix_raw = path.substring(start, segment[1])
257
- const suffix_raw = path.substring(segment[4], end)
258
- const actuallyCaseSensitive =
259
- caseSensitive && !!(prefix_raw || suffix_raw)
260
- const prefix = !prefix_raw
261
- ? undefined
262
- : actuallyCaseSensitive
263
- ? prefix_raw
264
- : prefix_raw.toLowerCase()
265
- const suffix = !suffix_raw
266
- ? undefined
267
- : actuallyCaseSensitive
268
- ? suffix_raw
269
- : suffix_raw.toLowerCase()
270
- const existingNode =
271
- !parseParams &&
272
- node.dynamic?.find(
273
- (s) =>
274
- !s.parse &&
275
- s.caseSensitive === actuallyCaseSensitive &&
276
- s.prefix === prefix &&
277
- s.suffix === suffix,
278
- )
233
+ const existingNode = staticChildren.get(name)
279
234
  if (existingNode) {
280
235
  nextNode = existingNode
281
236
  } else {
282
- const next = createDynamicNode<TRouteLike>(
283
- SEGMENT_TYPE_PARAM,
284
- route.fullPath ?? route.from,
285
- actuallyCaseSensitive,
286
- prefix,
287
- suffix,
288
- )
289
- nextNode = next
290
- next.depth = depth
237
+ const next = createStaticNode<TRouteLike>(path)
291
238
  next.parent = node
292
- node.dynamic ??= []
293
- node.dynamic.push(next)
239
+ next.depth = depth
240
+ nextNode = next
241
+ staticChildren.set(name, next)
294
242
  }
295
243
  break
296
244
  }
297
- case SEGMENT_TYPE_OPTIONAL_PARAM: {
245
+ case SEGMENT_TYPE_PARAM:
246
+ case SEGMENT_TYPE_OPTIONAL_PARAM:
247
+ case SEGMENT_TYPE_WILDCARD: {
298
248
  const prefix_raw = path.substring(start, segment[1])
299
249
  const suffix_raw = path.substring(segment[4], end)
300
250
  const actuallyCaseSensitive =
@@ -309,9 +259,18 @@ function parseSegments<TRouteLike extends RouteLike>(
309
259
  : actuallyCaseSensitive
310
260
  ? suffix_raw
311
261
  : suffix_raw.toLowerCase()
262
+ const siblings =
263
+ kind === SEGMENT_TYPE_PARAM
264
+ ? node.dynamic
265
+ : kind === SEGMENT_TYPE_OPTIONAL_PARAM
266
+ ? node.optional
267
+ : node.wildcard
312
268
  const existingNode =
269
+ // Keep wildcard aliases as separate match candidates, even when
270
+ // they have the same shape and no parser.
271
+ kind !== SEGMENT_TYPE_WILDCARD &&
313
272
  !parseParams &&
314
- node.optional?.find(
273
+ siblings?.find(
315
274
  (s) =>
316
275
  !s.parse &&
317
276
  s.caseSensitive === actuallyCaseSensitive &&
@@ -322,8 +281,8 @@ function parseSegments<TRouteLike extends RouteLike>(
322
281
  nextNode = existingNode
323
282
  } else {
324
283
  const next = createDynamicNode<TRouteLike>(
325
- SEGMENT_TYPE_OPTIONAL_PARAM,
326
- route.fullPath ?? route.from,
284
+ kind,
285
+ path,
327
286
  actuallyCaseSensitive,
328
287
  prefix,
329
288
  suffix,
@@ -331,39 +290,21 @@ function parseSegments<TRouteLike extends RouteLike>(
331
290
  nextNode = next
332
291
  next.parent = node
333
292
  next.depth = depth
334
- node.optional ??= []
335
- node.optional.push(next)
293
+ let nodes: Array<DynamicSegmentNode<TRouteLike>>
294
+ if (kind === SEGMENT_TYPE_PARAM) {
295
+ nodes = node.dynamic ??= []
296
+ } else if (kind === SEGMENT_TYPE_OPTIONAL_PARAM) {
297
+ nodes = node.optional ??= []
298
+ } else {
299
+ nodes = node.wildcard ??= []
300
+ }
301
+ nodes.push(next)
302
+ if (nodes.length === 2) {
303
+ dynamicListsToSort?.push(nodes)
304
+ }
336
305
  }
337
306
  break
338
307
  }
339
- case SEGMENT_TYPE_WILDCARD: {
340
- const prefix_raw = path.substring(start, segment[1])
341
- const suffix_raw = path.substring(segment[4], end)
342
- const actuallyCaseSensitive =
343
- caseSensitive && !!(prefix_raw || suffix_raw)
344
- const prefix = !prefix_raw
345
- ? undefined
346
- : actuallyCaseSensitive
347
- ? prefix_raw
348
- : prefix_raw.toLowerCase()
349
- const suffix = !suffix_raw
350
- ? undefined
351
- : actuallyCaseSensitive
352
- ? suffix_raw
353
- : suffix_raw.toLowerCase()
354
- const next = createDynamicNode<TRouteLike>(
355
- SEGMENT_TYPE_WILDCARD,
356
- route.fullPath ?? route.from,
357
- actuallyCaseSensitive,
358
- prefix,
359
- suffix,
360
- )
361
- nextNode = next
362
- next.parent = node
363
- next.depth = depth
364
- node.wildcard ??= []
365
- node.wildcard.push(next)
366
- }
367
308
  }
368
309
  node = nextNode
369
310
  }
@@ -376,9 +317,7 @@ function parseSegments<TRouteLike extends RouteLike>(
376
317
  route.id &&
377
318
  route.id.charCodeAt(route.id.lastIndexOf('/') + 1) === 95 /* '_' */
378
319
  ) {
379
- const pathlessNode = createStaticNode<TRouteLike>(
380
- route.fullPath ?? route.from,
381
- )
320
+ const pathlessNode = createStaticNode<TRouteLike>(path)
382
321
  pathlessNode.kind = SEGMENT_TYPE_PATHLESS
383
322
  pathlessNode.parent = node
384
323
  depth++
@@ -391,9 +330,7 @@ function parseSegments<TRouteLike extends RouteLike>(
391
330
  const isLeaf = (route.path || !route.children) && !route.isRoot
392
331
  // create index node
393
332
  if (isLeaf && path.endsWith('/')) {
394
- const indexNode = createStaticNode<TRouteLike>(
395
- route.fullPath ?? route.from,
396
- )
333
+ const indexNode = createStaticNode<TRouteLike>(path)
397
334
  indexNode.kind = SEGMENT_TYPE_INDEX
398
335
  indexNode.parent = node
399
336
  depth++
@@ -403,12 +340,12 @@ function parseSegments<TRouteLike extends RouteLike>(
403
340
  }
404
341
 
405
342
  node.parse = parseParams ?? null
406
- node.priority = route.options?.params?.priority ?? 0
343
+ node.priority = options?.params?.priority ?? 0
407
344
 
408
345
  // make node "matchable"
409
346
  if (isLeaf && !node.route) {
410
347
  node.route = route
411
- node.fullPath = route.fullPath ?? route.from
348
+ node.fullPath = path
412
349
  }
413
350
  }
414
351
  if (route.children)
@@ -420,6 +357,7 @@ function parseSegments<TRouteLike extends RouteLike>(
420
357
  cursor,
421
358
  node,
422
359
  depth,
360
+ dynamicListsToSort,
423
361
  onRoute,
424
362
  )
425
363
  }
@@ -464,42 +402,6 @@ function sortDynamic(
464
402
  return 0
465
403
  }
466
404
 
467
- function sortTreeNodes(node: SegmentNode<RouteLike>) {
468
- if (node.pathless) {
469
- for (const child of node.pathless) {
470
- sortTreeNodes(child)
471
- }
472
- }
473
- if (node.static) {
474
- for (const child of node.static.values()) {
475
- sortTreeNodes(child)
476
- }
477
- }
478
- if (node.staticInsensitive) {
479
- for (const child of node.staticInsensitive.values()) {
480
- sortTreeNodes(child)
481
- }
482
- }
483
- if (node.dynamic?.length) {
484
- node.dynamic.sort(sortDynamic)
485
- for (const child of node.dynamic) {
486
- sortTreeNodes(child)
487
- }
488
- }
489
- if (node.optional?.length) {
490
- node.optional.sort(sortDynamic)
491
- for (const child of node.optional) {
492
- sortTreeNodes(child)
493
- }
494
- }
495
- if (node.wildcard?.length) {
496
- node.wildcard.sort(sortDynamic)
497
- for (const child of node.wildcard) {
498
- sortTreeNodes(child)
499
- }
500
- }
501
- }
502
-
503
405
  function createStaticNode<T extends RouteLike>(
504
406
  fullPath: string,
505
407
  ): StaticSegmentNode<T> {
@@ -663,10 +565,13 @@ export function processRouteMasks<
663
565
  ) {
664
566
  const segmentTree = createStaticNode<TRouteLike>('/')
665
567
  const data = new Uint16Array(6)
568
+ const dynamicListsToSort: Array<Array<DynamicSegmentNode<TRouteLike>>> = []
666
569
  for (const route of routeList) {
667
- parseSegments(false, data, route, 1, segmentTree, 0)
570
+ parseSegments(false, data, route, 1, segmentTree, 0, dynamicListsToSort)
571
+ }
572
+ for (const nodes of dynamicListsToSort) {
573
+ nodes.sort(sortDynamic)
668
574
  }
669
- sortTreeNodes(segmentTree)
670
575
  processedTree.masksTree = segmentTree
671
576
  processedTree.flatCache = createLRUCache<
672
577
  string,
@@ -789,34 +694,46 @@ export function processRouteTree<
789
694
  ): ProcessRouteTreeResult<TRouteLike> {
790
695
  const segmentTree = createStaticNode<TRouteLike>(routeTree.fullPath)
791
696
  const data = new Uint16Array(6)
697
+ const dynamicListsToSort: Array<Array<DynamicSegmentNode<TRouteLike>>> = []
792
698
  const routesById = {} as Record<string, TRouteLike>
793
699
  const routesByPath = {} as Record<string, TRouteLike>
794
700
  let index = 0
795
- parseSegments(caseSensitive, data, routeTree, 1, segmentTree, 0, (route) => {
796
- initRoute?.(route, index)
701
+ parseSegments(
702
+ caseSensitive,
703
+ data,
704
+ routeTree,
705
+ 1,
706
+ segmentTree,
707
+ 0,
708
+ dynamicListsToSort,
709
+ (route) => {
710
+ initRoute?.(route, index)
711
+
712
+ if (route.id in routesById) {
713
+ if (process.env.NODE_ENV !== 'production') {
714
+ throw new Error(
715
+ `Invariant failed: Duplicate routes found with id: ${String(route.id)}`,
716
+ )
717
+ }
797
718
 
798
- if (route.id in routesById) {
799
- if (process.env.NODE_ENV !== 'production') {
800
- throw new Error(
801
- `Invariant failed: Duplicate routes found with id: ${String(route.id)}`,
802
- )
719
+ invariant()
803
720
  }
804
721
 
805
- invariant()
806
- }
807
-
808
- routesById[route.id] = route
722
+ routesById[route.id] = route
809
723
 
810
- if (index !== 0 && route.path) {
811
- const trimmedFullPath = trimPathRight(route.fullPath)
812
- if (!routesByPath[trimmedFullPath] || route.fullPath.endsWith('/')) {
813
- routesByPath[trimmedFullPath] = route
724
+ if (index !== 0 && route.path) {
725
+ const trimmedFullPath = trimPathRight(route.fullPath)
726
+ if (!routesByPath[trimmedFullPath] || route.fullPath.endsWith('/')) {
727
+ routesByPath[trimmedFullPath] = route
728
+ }
814
729
  }
815
- }
816
730
 
817
- index++
818
- })
819
- sortTreeNodes(segmentTree)
731
+ index++
732
+ },
733
+ )
734
+ for (const nodes of dynamicListsToSort) {
735
+ nodes.sort(sortDynamic)
736
+ }
820
737
  const processedTree: ProcessedTree<TRouteLike, any, any> = {
821
738
  segmentTree,
822
739
  singleCache: createLRUCache<string, AnySegmentNode<any>>(1000),
@@ -992,8 +909,6 @@ type MatchStackFrame<T extends RouteLike> = {
992
909
  node: AnySegmentNode<T>
993
910
  /** index of the segment of path */
994
911
  index: number
995
- /** how many nodes between `node` and the root of the segment tree */
996
- depth: number
997
912
  /**
998
913
  * Bitmask of skipped optional segments.
999
914
  *
@@ -1043,7 +958,6 @@ function getNodeMatch<T extends RouteLike>(
1043
958
  node: segmentTree,
1044
959
  index: 1,
1045
960
  skipped: 0,
1046
- depth: 1,
1047
961
  statics: 0,
1048
962
  dynamics: 0,
1049
963
  optionals: 0,
@@ -1055,7 +969,7 @@ function getNodeMatch<T extends RouteLike>(
1055
969
 
1056
970
  while (stack.length) {
1057
971
  const frame = stack.pop()!
1058
- const { node, index, skipped, depth, statics, dynamics, optionals } = frame
972
+ const { node, index, skipped, statics, dynamics, optionals } = frame
1059
973
  let { extract, rawParams } = frame
1060
974
 
1061
975
  // Wildcard candidates are pushed speculatively as fallbacks in case a
@@ -1112,7 +1026,6 @@ function getNodeMatch<T extends RouteLike>(
1112
1026
  node: node.index,
1113
1027
  index,
1114
1028
  skipped,
1115
- depth: depth + 1,
1116
1029
  statics,
1117
1030
  dynamics,
1118
1031
  optionals,
@@ -1165,7 +1078,6 @@ function getNodeMatch<T extends RouteLike>(
1165
1078
  node: segment,
1166
1079
  index: partsLength,
1167
1080
  skipped,
1168
- depth: depth + 1,
1169
1081
  statics,
1170
1082
  dynamics,
1171
1083
  optionals,
@@ -1177,16 +1089,15 @@ function getNodeMatch<T extends RouteLike>(
1177
1089
 
1178
1090
  // 4. Try optional match
1179
1091
  if (node.optional) {
1180
- const nextSkipped = skipped | (1 << depth)
1181
- const nextDepth = depth + 1
1092
+ // A skipped optional is keyed by the child node's trie depth.
1093
+ const nextSkipped = skipped | (1 << (node.depth + 1))
1182
1094
  for (let i = node.optional.length - 1; i >= 0; i--) {
1183
1095
  const segment = node.optional[i]!
1184
- // when skipping, node and depth advance by 1, but index doesn't
1096
+ // when skipping, the node advances by 1, but the index doesn't
1185
1097
  stack.push({
1186
1098
  node: segment,
1187
1099
  index,
1188
1100
  skipped: nextSkipped,
1189
- depth: nextDepth,
1190
1101
  statics,
1191
1102
  dynamics,
1192
1103
  optionals,
@@ -1209,7 +1120,6 @@ function getNodeMatch<T extends RouteLike>(
1209
1120
  node: segment,
1210
1121
  index: index + 1,
1211
1122
  skipped,
1212
- depth: nextDepth,
1213
1123
  statics,
1214
1124
  dynamics,
1215
1125
  optionals: optionals + segmentScore(partsLength, index),
@@ -1236,7 +1146,6 @@ function getNodeMatch<T extends RouteLike>(
1236
1146
  node: segment,
1237
1147
  index: index + 1,
1238
1148
  skipped,
1239
- depth: depth + 1,
1240
1149
  statics,
1241
1150
  dynamics: dynamics + segmentScore(partsLength, index),
1242
1151
  optionals,
@@ -1256,7 +1165,6 @@ function getNodeMatch<T extends RouteLike>(
1256
1165
  node: match,
1257
1166
  index: index + 1,
1258
1167
  skipped,
1259
- depth: depth + 1,
1260
1168
  statics: statics + segmentScore(partsLength, index),
1261
1169
  dynamics,
1262
1170
  optionals,
@@ -1274,7 +1182,6 @@ function getNodeMatch<T extends RouteLike>(
1274
1182
  node: match,
1275
1183
  index: index + 1,
1276
1184
  skipped,
1277
- depth: depth + 1,
1278
1185
  statics: statics + segmentScore(partsLength, index),
1279
1186
  dynamics,
1280
1187
  optionals,
@@ -1286,14 +1193,12 @@ function getNodeMatch<T extends RouteLike>(
1286
1193
 
1287
1194
  // 0. Try pathless match
1288
1195
  if (node.pathless) {
1289
- const nextDepth = depth + 1
1290
1196
  for (let i = node.pathless.length - 1; i >= 0; i--) {
1291
1197
  const segment = node.pathless[i]!
1292
1198
  stack.push({
1293
1199
  node: segment,
1294
1200
  index,
1295
1201
  skipped,
1296
- depth: nextDepth,
1297
1202
  statics,
1298
1203
  dynamics,
1299
1204
  optionals,
@@ -1380,6 +1285,6 @@ function isFrameMoreSpecific(
1380
1285
  (prev.node.kind === SEGMENT_TYPE_INDEX) ||
1381
1286
  ((next.node.kind === SEGMENT_TYPE_INDEX) ===
1382
1287
  (prev.node.kind === SEGMENT_TYPE_INDEX) &&
1383
- next.depth > prev.depth)))))))
1288
+ next.node.depth > prev.node.depth)))))))
1384
1289
  )
1385
1290
  }