@tanstack/router-core 1.120.7 → 1.121.0-alpha.11

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 (51) hide show
  1. package/dist/cjs/fileRoute.d.cts +6 -2
  2. package/dist/cjs/index.cjs +3 -0
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/cjs/index.d.cts +7 -7
  5. package/dist/cjs/link.cjs.map +1 -1
  6. package/dist/cjs/link.d.cts +18 -1
  7. package/dist/cjs/path.cjs +130 -16
  8. package/dist/cjs/path.cjs.map +1 -1
  9. package/dist/cjs/path.d.cts +17 -0
  10. package/dist/cjs/redirect.cjs +17 -7
  11. package/dist/cjs/redirect.cjs.map +1 -1
  12. package/dist/cjs/redirect.d.cts +13 -7
  13. package/dist/cjs/route.cjs +12 -1
  14. package/dist/cjs/route.cjs.map +1 -1
  15. package/dist/cjs/route.d.cts +18 -27
  16. package/dist/cjs/router.cjs +395 -335
  17. package/dist/cjs/router.cjs.map +1 -1
  18. package/dist/cjs/router.d.cts +48 -8
  19. package/dist/cjs/typePrimitives.d.cts +2 -2
  20. package/dist/cjs/utils.cjs.map +1 -1
  21. package/dist/cjs/utils.d.cts +3 -0
  22. package/dist/esm/fileRoute.d.ts +6 -2
  23. package/dist/esm/index.d.ts +7 -7
  24. package/dist/esm/index.js +5 -2
  25. package/dist/esm/link.d.ts +18 -1
  26. package/dist/esm/link.js.map +1 -1
  27. package/dist/esm/path.d.ts +17 -0
  28. package/dist/esm/path.js +130 -16
  29. package/dist/esm/path.js.map +1 -1
  30. package/dist/esm/redirect.d.ts +13 -7
  31. package/dist/esm/redirect.js +17 -7
  32. package/dist/esm/redirect.js.map +1 -1
  33. package/dist/esm/route.d.ts +18 -27
  34. package/dist/esm/route.js +12 -1
  35. package/dist/esm/route.js.map +1 -1
  36. package/dist/esm/router.d.ts +48 -8
  37. package/dist/esm/router.js +398 -338
  38. package/dist/esm/router.js.map +1 -1
  39. package/dist/esm/typePrimitives.d.ts +2 -2
  40. package/dist/esm/utils.d.ts +3 -0
  41. package/dist/esm/utils.js.map +1 -1
  42. package/package.json +2 -2
  43. package/src/fileRoute.ts +90 -1
  44. package/src/index.ts +14 -8
  45. package/src/link.ts +97 -11
  46. package/src/path.ts +181 -16
  47. package/src/redirect.ts +39 -16
  48. package/src/route.ts +91 -64
  49. package/src/router.ts +569 -434
  50. package/src/typePrimitives.ts +2 -2
  51. package/src/utils.ts +15 -0
package/src/router.ts CHANGED
@@ -28,7 +28,7 @@ import { isNotFound } from './not-found'
28
28
  import { setupScrollRestoration } from './scroll-restoration'
29
29
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
30
30
  import { rootRouteId } from './root'
31
- import { isRedirect, isResolvedRedirect } from './redirect'
31
+ import { isRedirect } from './redirect'
32
32
  import type { SearchParser, SearchSerializer } from './searchParams'
33
33
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
34
34
  import type {
@@ -165,6 +165,14 @@ export interface RouterOptions<
165
165
  * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-delay)
166
166
  */
167
167
  defaultPreloadDelay?: number
168
+ /**
169
+ * The default `preloadIntentProximity` a route should use if no preloadIntentProximity is provided.
170
+ *
171
+ * @default 0
172
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadintentproximity-property)
173
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-intent-proximity)
174
+ */
175
+ defaultPreloadIntentProximity?: number
168
176
  /**
169
177
  * The default `pendingMs` a route should use if no pendingMs is provided.
170
178
  *
@@ -407,7 +415,7 @@ export interface RouterState<
407
415
  location: ParsedLocation<FullSearchSchema<TRouteTree>>
408
416
  resolvedLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
409
417
  statusCode: number
410
- redirect?: ResolvedRedirect
418
+ redirect?: AnyRedirect
411
419
  }
412
420
 
413
421
  export interface BuildNextOptions {
@@ -425,8 +433,9 @@ export interface BuildNextOptions {
425
433
  unmaskOnReload?: boolean
426
434
  }
427
435
  from?: string
428
- _fromLocation?: ParsedLocation
429
436
  href?: string
437
+ _fromLocation?: ParsedLocation
438
+ unsafeRelative?: 'path'
430
439
  }
431
440
 
432
441
  type NavigationEventInfo = {
@@ -513,11 +522,6 @@ export interface RouterErrorSerializer<TSerializedError> {
513
522
  deserialize: (err: TSerializedError) => unknown
514
523
  }
515
524
 
516
- export interface MatchedRoutesResult {
517
- matchedRoutes: Array<AnyRoute>
518
- routeParams: Record<string, string>
519
- }
520
-
521
525
  export type PreloadRouteFn<
522
526
  TRouteTree extends AnyRoute,
523
527
  TTrailingSlashOption extends TrailingSlashOption,
@@ -593,8 +597,8 @@ export type ParseLocationFn<TRouteTree extends AnyRoute> = (
593
597
  ) => ParsedLocation<FullSearchSchema<TRouteTree>>
594
598
 
595
599
  export type GetMatchRoutesFn = (
596
- next: ParsedLocation,
597
- dest?: BuildNextOptions,
600
+ pathname: string,
601
+ routePathname: string | undefined,
598
602
  ) => {
599
603
  matchedRoutes: Array<AnyRoute>
600
604
  routeParams: Record<string, string>
@@ -836,6 +840,8 @@ export class RouterCore<
836
840
  // router can be used in a non-react environment if necessary
837
841
  startTransition: StartTransitionFn = (fn) => fn()
838
842
 
843
+ isShell = false
844
+
839
845
  update: UpdateFn<
840
846
  TRouteTree,
841
847
  TTrailingSlashOption,
@@ -882,7 +888,6 @@ export class RouterCore<
882
888
  }
883
889
 
884
890
  if (
885
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
886
891
  !this.history ||
887
892
  (this.options.history && this.options.history !== this.history)
888
893
  ) {
@@ -901,7 +906,6 @@ export class RouterCore<
901
906
  this.buildRouteTree()
902
907
  }
903
908
 
904
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
905
909
  if (!this.__store) {
906
910
  this.__store = new Store(getInitialRouterState(this.latestLocation), {
907
911
  onUpdate: () => {
@@ -920,13 +924,16 @@ export class RouterCore<
920
924
  if (
921
925
  typeof window !== 'undefined' &&
922
926
  'CSS' in window &&
923
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
924
927
  typeof window.CSS?.supports === 'function'
925
928
  ) {
926
929
  this.isViewTransitionTypesSupported = window.CSS.supports(
927
930
  'selector(:active-view-transition-type(a)',
928
931
  )
929
932
  }
933
+
934
+ if ((this.latestLocation.search as any).__TSS_SHELL) {
935
+ this.isShell = true
936
+ }
930
937
  }
931
938
 
932
939
  get state() {
@@ -934,124 +941,29 @@ export class RouterCore<
934
941
  }
935
942
 
936
943
  buildRouteTree = () => {
937
- this.routesById = {} as RoutesById<TRouteTree>
938
- this.routesByPath = {} as RoutesByPath<TRouteTree>
939
-
940
- const notFoundRoute = this.options.notFoundRoute
941
- if (notFoundRoute) {
942
- notFoundRoute.init({
943
- originalIndex: 99999999999,
944
- defaultSsr: this.options.defaultSsr,
945
- })
946
- ;(this.routesById as any)[notFoundRoute.id] = notFoundRoute
947
- }
948
-
949
- const recurseRoutes = (childRoutes: Array<AnyRoute>) => {
950
- childRoutes.forEach((childRoute, i) => {
951
- childRoute.init({
944
+ const { routesById, routesByPath, flatRoutes } = processRouteTree({
945
+ routeTree: this.routeTree,
946
+ initRoute: (route, i) => {
947
+ route.init({
952
948
  originalIndex: i,
953
949
  defaultSsr: this.options.defaultSsr,
954
950
  })
955
-
956
- const existingRoute = (this.routesById as any)[childRoute.id]
957
-
958
- invariant(
959
- !existingRoute,
960
- `Duplicate routes found with id: ${String(childRoute.id)}`,
961
- )
962
- ;(this.routesById as any)[childRoute.id] = childRoute
963
-
964
- if (!childRoute.isRoot && childRoute.path) {
965
- const trimmedFullPath = trimPathRight(childRoute.fullPath)
966
- if (
967
- !(this.routesByPath as any)[trimmedFullPath] ||
968
- childRoute.fullPath.endsWith('/')
969
- ) {
970
- ;(this.routesByPath as any)[trimmedFullPath] = childRoute
971
- }
972
- }
973
-
974
- const children = childRoute.children
975
-
976
- if (children?.length) {
977
- recurseRoutes(children)
978
- }
979
- })
980
- }
981
-
982
- recurseRoutes([this.routeTree])
983
-
984
- const scoredRoutes: Array<{
985
- child: AnyRoute
986
- trimmed: string
987
- parsed: ReturnType<typeof parsePathname>
988
- index: number
989
- scores: Array<number>
990
- }> = []
991
-
992
- const routes: Array<AnyRoute> = Object.values(this.routesById)
993
-
994
- routes.forEach((d, i) => {
995
- if (d.isRoot || !d.path) {
996
- return
997
- }
998
-
999
- const trimmed = trimPathLeft(d.fullPath)
1000
- const parsed = parsePathname(trimmed)
1001
-
1002
- while (parsed.length > 1 && parsed[0]?.value === '/') {
1003
- parsed.shift()
1004
- }
1005
-
1006
- const scores = parsed.map((segment) => {
1007
- if (segment.value === '/') {
1008
- return 0.75
1009
- }
1010
-
1011
- if (segment.type === 'param') {
1012
- return 0.5
1013
- }
1014
-
1015
- if (segment.type === 'wildcard') {
1016
- return 0.25
1017
- }
1018
-
1019
- return 1
1020
- })
1021
-
1022
- scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
951
+ },
1023
952
  })
1024
953
 
1025
- this.flatRoutes = scoredRoutes
1026
- .sort((a, b) => {
1027
- const minLength = Math.min(a.scores.length, b.scores.length)
954
+ this.routesById = routesById as RoutesById<TRouteTree>
955
+ this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
956
+ this.flatRoutes = flatRoutes as Array<AnyRoute>
1028
957
 
1029
- // Sort by min available score
1030
- for (let i = 0; i < minLength; i++) {
1031
- if (a.scores[i] !== b.scores[i]) {
1032
- return b.scores[i]! - a.scores[i]!
1033
- }
1034
- }
1035
-
1036
- // Sort by length of score
1037
- if (a.scores.length !== b.scores.length) {
1038
- return b.scores.length - a.scores.length
1039
- }
1040
-
1041
- // Sort by min available parsed value
1042
- for (let i = 0; i < minLength; i++) {
1043
- if (a.parsed[i]!.value !== b.parsed[i]!.value) {
1044
- return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
1045
- }
1046
- }
958
+ const notFoundRoute = this.options.notFoundRoute
1047
959
 
1048
- // Sort by original index
1049
- return a.index - b.index
1050
- })
1051
- .map((d, i) => {
1052
- d.child.rank = i
1053
- return d.child
960
+ if (notFoundRoute) {
961
+ notFoundRoute.init({
962
+ originalIndex: 99999999999,
963
+ defaultSsr: this.options.defaultSsr,
1054
964
  })
965
+ this.routesById[notFoundRoute.id] = notFoundRoute
966
+ }
1055
967
  }
1056
968
 
1057
969
  subscribe: SubscribeFn = (eventType, fn) => {
@@ -1155,9 +1067,9 @@ export class RouterCore<
1155
1067
  } as ParsedLocation,
1156
1068
  opts,
1157
1069
  )
1158
- } else {
1159
- return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
1160
1070
  }
1071
+
1072
+ return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
1161
1073
  }
1162
1074
 
1163
1075
  private matchRoutesInternal(
@@ -1165,8 +1077,8 @@ export class RouterCore<
1165
1077
  opts?: MatchRoutesOpts,
1166
1078
  ): Array<AnyRouteMatch> {
1167
1079
  const { foundRoute, matchedRoutes, routeParams } = this.getMatchedRoutes(
1168
- next,
1169
- opts?.dest,
1080
+ next.pathname,
1081
+ opts?.dest?.to as string,
1170
1082
  )
1171
1083
  let isGlobalNotFound = false
1172
1084
 
@@ -1418,7 +1330,8 @@ export class RouterCore<
1418
1330
  const route = this.looseRoutesById[match.routeId]!
1419
1331
  const existingMatch = this.getMatch(match.id)
1420
1332
 
1421
- // only execute `context` if we are not just building a location
1333
+ // only execute `context` if we are not calling from router.buildLocation
1334
+
1422
1335
  if (!existingMatch && opts?._buildLocation !== true) {
1423
1336
  const parentMatch = matches[index - 1]
1424
1337
  const parentContext = getParentContext(parentMatch)
@@ -1452,47 +1365,19 @@ export class RouterCore<
1452
1365
  return matches
1453
1366
  }
1454
1367
 
1455
- getMatchedRoutes: GetMatchRoutesFn = (next, dest) => {
1456
- let routeParams: Record<string, string> = {}
1457
- const trimmedPath = trimPathRight(next.pathname)
1458
- const getMatchedParams = (route: AnyRoute) => {
1459
- const result = matchPathname(this.basepath, trimmedPath, {
1460
- to: route.fullPath,
1461
- caseSensitive:
1462
- route.options.caseSensitive ?? this.options.caseSensitive,
1463
- fuzzy: true,
1464
- })
1465
- return result
1466
- }
1467
-
1468
- let foundRoute: AnyRoute | undefined =
1469
- dest?.to !== undefined ? this.routesByPath[dest.to!] : undefined
1470
- if (foundRoute) {
1471
- routeParams = getMatchedParams(foundRoute)!
1472
- } else {
1473
- foundRoute = this.flatRoutes.find((route) => {
1474
- const matchedParams = getMatchedParams(route)
1475
-
1476
- if (matchedParams) {
1477
- routeParams = matchedParams
1478
- return true
1479
- }
1480
-
1481
- return false
1482
- })
1483
- }
1484
-
1485
- let routeCursor: AnyRoute =
1486
- foundRoute || (this.routesById as any)[rootRouteId]
1487
-
1488
- const matchedRoutes: Array<AnyRoute> = [routeCursor]
1489
-
1490
- while (routeCursor.parentRoute) {
1491
- routeCursor = routeCursor.parentRoute
1492
- matchedRoutes.unshift(routeCursor)
1493
- }
1494
-
1495
- return { matchedRoutes, routeParams, foundRoute }
1368
+ getMatchedRoutes: GetMatchRoutesFn = (
1369
+ pathname: string,
1370
+ routePathname: string | undefined,
1371
+ ) => {
1372
+ return getMatchedRoutes({
1373
+ pathname,
1374
+ routePathname,
1375
+ basepath: this.basepath,
1376
+ caseSensitive: this.options.caseSensitive,
1377
+ routesByPath: this.routesByPath,
1378
+ routesById: this.routesById,
1379
+ flatRoutes: this.flatRoutes,
1380
+ })
1496
1381
  }
1497
1382
 
1498
1383
  cancelMatch = (id: string) => {
@@ -1515,75 +1400,67 @@ export class RouterCore<
1515
1400
  dest: BuildNextOptions & {
1516
1401
  unmaskOnReload?: boolean
1517
1402
  } = {},
1518
- matchedRoutesResult?: MatchedRoutesResult,
1519
1403
  ): ParsedLocation => {
1520
- const fromMatches = dest._fromLocation
1521
- ? this.matchRoutes(dest._fromLocation, { _buildLocation: true })
1522
- : this.state.matches
1523
-
1524
- const fromMatch =
1525
- dest.from != null
1526
- ? fromMatches.find((d) =>
1527
- matchPathname(this.basepath, trimPathRight(d.pathname), {
1528
- to: dest.from,
1529
- caseSensitive: false,
1530
- fuzzy: false,
1531
- }),
1532
- )
1533
- : undefined
1534
-
1535
- const fromPath = fromMatch?.pathname || this.latestLocation.pathname
1404
+ // We allow the caller to override the current location
1405
+ const currentLocation = dest._fromLocation || this.latestLocation
1536
1406
 
1537
- invariant(
1538
- dest.from == null || fromMatch != null,
1539
- 'Could not find match for from: ' + dest.from,
1540
- )
1407
+ const allFromMatches = this.matchRoutes(currentLocation, {
1408
+ _buildLocation: true,
1409
+ })
1541
1410
 
1542
- const fromSearch = this.state.pendingMatches?.length
1543
- ? last(this.state.pendingMatches)?.search
1544
- : last(fromMatches)?.search || this.latestLocation.search
1411
+ const lastMatch = last(allFromMatches)!
1412
+
1413
+ // First let's find the starting pathname
1414
+ // By default, start with the current location
1415
+ let fromPath = lastMatch.fullPath
1416
+
1417
+ // If there is a to, it means we are changing the path in some way
1418
+ // So we need to find the relative fromPath
1419
+ if (dest.unsafeRelative === 'path') {
1420
+ fromPath = currentLocation.pathname
1421
+ } else if (dest.to && dest.from) {
1422
+ fromPath = dest.from
1423
+ const existingFrom = [...allFromMatches].reverse().find((d) => {
1424
+ return (
1425
+ d.fullPath === fromPath || d.fullPath === joinPaths([fromPath, '/'])
1426
+ )
1427
+ })
1545
1428
 
1546
- const stayingMatches = matchedRoutesResult?.matchedRoutes.filter((d) =>
1547
- fromMatches.find((e) => e.routeId === d.id),
1548
- )
1549
- let pathname: string
1550
- if (dest.to) {
1551
- const resolvePathTo =
1552
- fromMatch?.fullPath ||
1553
- last(fromMatches)?.fullPath ||
1554
- this.latestLocation.pathname
1555
- pathname = this.resolvePathWithBase(resolvePathTo, `${dest.to}`)
1556
- } else {
1557
- const fromRouteByFromPathRouteId =
1558
- this.routesById[
1559
- stayingMatches?.find((route) => {
1560
- const interpolatedPath = interpolatePath({
1561
- path: route.fullPath,
1562
- params: matchedRoutesResult?.routeParams ?? {},
1563
- decodeCharMap: this.pathParamsDecodeCharMap,
1564
- }).interpolatedPath
1565
- const pathname = joinPaths([this.basepath, interpolatedPath])
1566
- return pathname === fromPath
1567
- })?.id as keyof this['routesById']
1568
- ]
1569
- pathname = this.resolvePathWithBase(
1570
- fromPath,
1571
- fromRouteByFromPathRouteId?.to ?? fromPath,
1572
- )
1429
+ if (!existingFrom) {
1430
+ console.warn(`Could not find match for from: ${dest.from}`)
1431
+ }
1573
1432
  }
1574
1433
 
1575
- const prevParams = { ...last(fromMatches)?.params }
1434
+ // From search should always use the current location
1435
+ const fromSearch = lastMatch.search
1436
+ // Same with params. It can't hurt to provide as many as possible
1437
+ const fromParams = { ...lastMatch.params }
1438
+
1439
+ // Resolve the next to
1440
+ const nextTo = dest.to
1441
+ ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1442
+ : fromPath
1576
1443
 
1444
+ // Resolve the next params
1577
1445
  let nextParams =
1578
1446
  (dest.params ?? true) === true
1579
- ? prevParams
1447
+ ? fromParams
1580
1448
  : {
1581
- ...prevParams,
1582
- ...functionalUpdate(dest.params as any, prevParams),
1449
+ ...fromParams,
1450
+ ...functionalUpdate(dest.params as any, fromParams),
1583
1451
  }
1584
1452
 
1453
+ const destRoutes = this.matchRoutes(
1454
+ nextTo,
1455
+ {},
1456
+ {
1457
+ _buildLocation: true,
1458
+ },
1459
+ ).map((d) => this.looseRoutesById[d.routeId]!)
1460
+
1461
+ // If there are any params, we need to stringify them
1585
1462
  if (Object.keys(nextParams).length > 0) {
1586
- matchedRoutesResult?.matchedRoutes
1463
+ destRoutes
1587
1464
  .map((route) => {
1588
1465
  return (
1589
1466
  route.options.params?.stringify ?? route.options.stringifyParams
@@ -1595,25 +1472,27 @@ export class RouterCore<
1595
1472
  })
1596
1473
  }
1597
1474
 
1598
- pathname = interpolatePath({
1599
- path: pathname,
1475
+ // Interpolate the next to into the next pathname
1476
+ const nextPathname = interpolatePath({
1477
+ path: nextTo,
1600
1478
  params: nextParams ?? {},
1601
1479
  leaveWildcards: false,
1602
1480
  leaveParams: opts.leaveParams,
1603
1481
  decodeCharMap: this.pathParamsDecodeCharMap,
1604
1482
  }).interpolatedPath
1605
1483
 
1606
- let search = fromSearch
1484
+ // Resolve the next search
1485
+ let nextSearch = fromSearch
1607
1486
  if (opts._includeValidateSearch && this.options.search?.strict) {
1608
1487
  let validatedSearch = {}
1609
- matchedRoutesResult?.matchedRoutes.forEach((route) => {
1488
+ destRoutes.forEach((route) => {
1610
1489
  try {
1611
1490
  if (route.options.validateSearch) {
1612
1491
  validatedSearch = {
1613
1492
  ...validatedSearch,
1614
1493
  ...(validateSearch(route.options.validateSearch, {
1615
1494
  ...validatedSearch,
1616
- ...search,
1495
+ ...nextSearch,
1617
1496
  }) ?? {}),
1618
1497
  }
1619
1498
  }
@@ -1621,137 +1500,52 @@ export class RouterCore<
1621
1500
  // ignore errors here because they are already handled in matchRoutes
1622
1501
  }
1623
1502
  })
1624
- search = validatedSearch
1503
+ nextSearch = validatedSearch
1625
1504
  }
1626
1505
 
1627
- const applyMiddlewares = (search: any) => {
1628
- const allMiddlewares =
1629
- matchedRoutesResult?.matchedRoutes.reduce(
1630
- (acc, route) => {
1631
- const middlewares: Array<SearchMiddleware<any>> = []
1632
- if ('search' in route.options) {
1633
- if (route.options.search?.middlewares) {
1634
- middlewares.push(...route.options.search.middlewares)
1635
- }
1636
- }
1637
- // TODO remove preSearchFilters and postSearchFilters in v2
1638
- else if (
1639
- route.options.preSearchFilters ||
1640
- route.options.postSearchFilters
1641
- ) {
1642
- const legacyMiddleware: SearchMiddleware<any> = ({
1643
- search,
1644
- next,
1645
- }) => {
1646
- let nextSearch = search
1647
- if (
1648
- 'preSearchFilters' in route.options &&
1649
- route.options.preSearchFilters
1650
- ) {
1651
- nextSearch = route.options.preSearchFilters.reduce(
1652
- (prev, next) => next(prev),
1653
- search,
1654
- )
1655
- }
1656
- const result = next(nextSearch)
1657
- if (
1658
- 'postSearchFilters' in route.options &&
1659
- route.options.postSearchFilters
1660
- ) {
1661
- return route.options.postSearchFilters.reduce(
1662
- (prev, next) => next(prev),
1663
- result,
1664
- )
1665
- }
1666
- return result
1667
- }
1668
- middlewares.push(legacyMiddleware)
1669
- }
1670
- if (opts._includeValidateSearch && route.options.validateSearch) {
1671
- const validate: SearchMiddleware<any> = ({ search, next }) => {
1672
- const result = next(search)
1673
- try {
1674
- const validatedSearch = {
1675
- ...result,
1676
- ...(validateSearch(
1677
- route.options.validateSearch,
1678
- result,
1679
- ) ?? {}),
1680
- }
1681
- return validatedSearch
1682
- } catch {
1683
- // ignore errors here because they are already handled in matchRoutes
1684
- return result
1685
- }
1686
- }
1687
- middlewares.push(validate)
1688
- }
1689
- return acc.concat(middlewares)
1690
- },
1691
- [] as Array<SearchMiddleware<any>>,
1692
- ) ?? []
1693
-
1694
- // the chain ends here since `next` is not called
1695
- const final: SearchMiddleware<any> = ({ search }) => {
1696
- if (!dest.search) {
1697
- return {}
1698
- }
1699
- if (dest.search === true) {
1700
- return search
1701
- }
1702
- return functionalUpdate(dest.search, search)
1703
- }
1704
- allMiddlewares.push(final)
1705
-
1706
- const applyNext = (index: number, currentSearch: any): any => {
1707
- // no more middlewares left, return the current search
1708
- if (index >= allMiddlewares.length) {
1709
- return currentSearch
1710
- }
1711
-
1712
- const middleware = allMiddlewares[index]!
1713
-
1714
- const next = (newSearch: any): any => {
1715
- return applyNext(index + 1, newSearch)
1716
- }
1717
-
1718
- return middleware({ search: currentSearch, next })
1719
- }
1720
-
1721
- // Start applying middlewares
1722
- return applyNext(0, search)
1723
- }
1506
+ nextSearch = applySearchMiddleware({
1507
+ search: nextSearch,
1508
+ dest,
1509
+ destRoutes,
1510
+ _includeValidateSearch: opts._includeValidateSearch,
1511
+ })
1724
1512
 
1725
- search = applyMiddlewares(search)
1513
+ // Replace the equal deep
1514
+ nextSearch = replaceEqualDeep(fromSearch, nextSearch)
1726
1515
 
1727
- search = replaceEqualDeep(fromSearch, search)
1728
- const searchStr = this.options.stringifySearch(search)
1516
+ // Stringify the next search
1517
+ const searchStr = this.options.stringifySearch(nextSearch)
1729
1518
 
1519
+ // Resolve the next hash
1730
1520
  const hash =
1731
1521
  dest.hash === true
1732
- ? this.latestLocation.hash
1522
+ ? currentLocation.hash
1733
1523
  : dest.hash
1734
- ? functionalUpdate(dest.hash, this.latestLocation.hash)
1524
+ ? functionalUpdate(dest.hash, currentLocation.hash)
1735
1525
  : undefined
1736
1526
 
1527
+ // Resolve the next hash string
1737
1528
  const hashStr = hash ? `#${hash}` : ''
1738
1529
 
1530
+ // Resolve the next state
1739
1531
  let nextState =
1740
1532
  dest.state === true
1741
- ? this.latestLocation.state
1533
+ ? currentLocation.state
1742
1534
  : dest.state
1743
- ? functionalUpdate(dest.state, this.latestLocation.state)
1535
+ ? functionalUpdate(dest.state, currentLocation.state)
1744
1536
  : {}
1745
1537
 
1746
- nextState = replaceEqualDeep(this.latestLocation.state, nextState)
1538
+ // Replace the equal deep
1539
+ nextState = replaceEqualDeep(currentLocation.state, nextState)
1747
1540
 
1541
+ // Return the next location
1748
1542
  return {
1749
- pathname,
1750
- search,
1543
+ pathname: nextPathname,
1544
+ search: nextSearch,
1751
1545
  searchStr,
1752
1546
  state: nextState as any,
1753
1547
  hash: hash ?? '',
1754
- href: `${pathname}${searchStr}${hashStr}`,
1548
+ href: `${nextPathname}${searchStr}${hashStr}`,
1755
1549
  unmaskOnReload: dest.unmaskOnReload,
1756
1550
  }
1757
1551
  }
@@ -1761,6 +1555,7 @@ export class RouterCore<
1761
1555
  maskedDest?: BuildNextOptions,
1762
1556
  ) => {
1763
1557
  const next = build(dest)
1558
+
1764
1559
  let maskedNext = maskedDest ? build(maskedDest) : undefined
1765
1560
 
1766
1561
  if (!maskedNext) {
@@ -1792,16 +1587,12 @@ export class RouterCore<
1792
1587
  }
1793
1588
  }
1794
1589
 
1795
- const nextMatches = this.getMatchedRoutes(next, dest)
1796
- const final = build(dest, nextMatches)
1797
-
1798
1590
  if (maskedNext) {
1799
- const maskedMatches = this.getMatchedRoutes(maskedNext, maskedDest)
1800
- const maskedFinal = build(maskedDest, maskedMatches)
1801
- final.maskedLocation = maskedFinal
1591
+ const maskedFinal = build(maskedDest)
1592
+ next.maskedLocation = maskedFinal
1802
1593
  }
1803
1594
 
1804
- return final
1595
+ return next
1805
1596
  }
1806
1597
 
1807
1598
  if (opts.mask) {
@@ -1938,6 +1729,13 @@ export class RouterCore<
1938
1729
  }
1939
1730
 
1940
1731
  navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
1732
+ if (!reloadDocument && href) {
1733
+ try {
1734
+ new URL(`${href}`)
1735
+ reloadDocument = true
1736
+ } catch {}
1737
+ }
1738
+
1941
1739
  if (reloadDocument) {
1942
1740
  if (!href) {
1943
1741
  const location = this.buildLocation({ to, ...rest } as any)
@@ -1960,10 +1758,30 @@ export class RouterCore<
1960
1758
 
1961
1759
  latestLoadPromise: undefined | Promise<void>
1962
1760
 
1963
- load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
1761
+ beforeLoad = () => {
1762
+ // Cancel any pending matches
1763
+ this.cancelMatches()
1964
1764
  this.latestLocation = this.parseLocation(this.latestLocation)
1965
1765
 
1966
- let redirect: ResolvedRedirect | undefined
1766
+ // Match the routes
1767
+ const pendingMatches = this.matchRoutes(this.latestLocation)
1768
+
1769
+ // Ingest the new matches
1770
+ this.__store.setState((s) => ({
1771
+ ...s,
1772
+ status: 'pending',
1773
+ isLoading: true,
1774
+ location: this.latestLocation,
1775
+ pendingMatches,
1776
+ // If a cached moved to pendingMatches, remove it from cachedMatches
1777
+ cachedMatches: s.cachedMatches.filter((d) => {
1778
+ return !pendingMatches.find((e) => e.id === d.id)
1779
+ }),
1780
+ }))
1781
+ }
1782
+
1783
+ load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
1784
+ let redirect: AnyRedirect | undefined
1967
1785
  let notFound: NotFoundError | undefined
1968
1786
 
1969
1787
  let loadPromise: Promise<void>
@@ -1972,36 +1790,10 @@ export class RouterCore<
1972
1790
  loadPromise = new Promise<void>((resolve) => {
1973
1791
  this.startTransition(async () => {
1974
1792
  try {
1793
+ this.beforeLoad()
1975
1794
  const next = this.latestLocation
1976
1795
  const prevLocation = this.state.resolvedLocation
1977
1796
 
1978
- // Cancel any pending matches
1979
- this.cancelMatches()
1980
-
1981
- let pendingMatches!: Array<AnyRouteMatch>
1982
-
1983
- batch(() => {
1984
- // this call breaks a route context of destination route after a redirect
1985
- // we should be fine not eagerly calling this since we call it later
1986
- // this.clearExpiredCache()
1987
-
1988
- // Match the routes
1989
- pendingMatches = this.matchRoutes(next)
1990
-
1991
- // Ingest the new matches
1992
- this.__store.setState((s) => ({
1993
- ...s,
1994
- status: 'pending',
1995
- isLoading: true,
1996
- location: next,
1997
- pendingMatches,
1998
- // If a cached moved to pendingMatches, remove it from cachedMatches
1999
- cachedMatches: s.cachedMatches.filter((d) => {
2000
- return !pendingMatches.find((e) => e.id === d.id)
2001
- }),
2002
- }))
2003
- })
2004
-
2005
1797
  if (!this.state.redirect) {
2006
1798
  this.emit({
2007
1799
  type: 'onBeforeNavigate',
@@ -2022,7 +1814,7 @@ export class RouterCore<
2022
1814
 
2023
1815
  await this.loadMatches({
2024
1816
  sync: opts?.sync,
2025
- matches: pendingMatches,
1817
+ matches: this.state.pendingMatches as Array<AnyRouteMatch>,
2026
1818
  location: next,
2027
1819
  // eslint-disable-next-line @typescript-eslint/require-await
2028
1820
  onReady: async () => {
@@ -2083,11 +1875,11 @@ export class RouterCore<
2083
1875
  },
2084
1876
  })
2085
1877
  } catch (err) {
2086
- if (isResolvedRedirect(err)) {
1878
+ if (isRedirect(err)) {
2087
1879
  redirect = err
2088
1880
  if (!this.isServer) {
2089
1881
  this.navigate({
2090
- ...redirect,
1882
+ ...redirect.options,
2091
1883
  replace: true,
2092
1884
  ignoreBlocker: true,
2093
1885
  })
@@ -2099,7 +1891,7 @@ export class RouterCore<
2099
1891
  this.__store.setState((s) => ({
2100
1892
  ...s,
2101
1893
  statusCode: redirect
2102
- ? redirect.statusCode
1894
+ ? redirect.status
2103
1895
  : notFound
2104
1896
  ? 404
2105
1897
  : s.matches.some((d) => d.status === 'error')
@@ -2255,13 +2047,15 @@ export class RouterCore<
2255
2047
  }
2256
2048
 
2257
2049
  const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
2258
- if (isResolvedRedirect(err)) {
2259
- if (!err.reloadDocument) {
2260
- throw err
2050
+ if (isRedirect(err) || isNotFound(err)) {
2051
+ if (isRedirect(err)) {
2052
+ if (err.redirectHandled) {
2053
+ if (!err.options.reloadDocument) {
2054
+ throw err
2055
+ }
2056
+ }
2261
2057
  }
2262
- }
2263
2058
 
2264
- if (isRedirect(err) || isNotFound(err)) {
2265
2059
  updateMatch(match.id, (prev) => ({
2266
2060
  ...prev,
2267
2061
  status: isRedirect(err)
@@ -2285,7 +2079,9 @@ export class RouterCore<
2285
2079
 
2286
2080
  if (isRedirect(err)) {
2287
2081
  rendered = true
2288
- err = this.resolveRedirect({ ...err, _fromLocation: location })
2082
+ err.options._fromLocation = location
2083
+ err.redirectHandled = true
2084
+ err = this.resolveRedirect(err)
2289
2085
  throw err
2290
2086
  } else if (isNotFound(err)) {
2291
2087
  this._handleNotFound(matches, err, {
@@ -2589,7 +2385,7 @@ export class RouterCore<
2589
2385
  !this.state.matches.find((d) => d.id === matchId),
2590
2386
  }))
2591
2387
 
2592
- const executeHead = () => {
2388
+ const executeHead = async () => {
2593
2389
  const match = this.getMatch(matchId)
2594
2390
  // in case of a redirecting match during preload, the match does not exist
2595
2391
  if (!match) {
@@ -2601,21 +2397,17 @@ export class RouterCore<
2601
2397
  params: match.params,
2602
2398
  loaderData: match.loaderData,
2603
2399
  }
2604
- const headFnContent = route.options.head?.(assetContext)
2400
+ const headFnContent =
2401
+ await route.options.head?.(assetContext)
2605
2402
  const meta = headFnContent?.meta
2606
2403
  const links = headFnContent?.links
2607
2404
  const headScripts = headFnContent?.scripts
2608
2405
 
2609
- const scripts = route.options.scripts?.(assetContext)
2610
- const headers = route.options.headers?.(assetContext)
2611
- updateMatch(matchId, (prev) => ({
2612
- ...prev,
2613
- meta,
2614
- links,
2615
- headScripts,
2616
- headers,
2617
- scripts,
2618
- }))
2406
+ const scripts =
2407
+ await route.options.scripts?.(assetContext)
2408
+ const headers =
2409
+ await route.options.headers?.(assetContext)
2410
+ return { meta, links, headScripts, headers, scripts }
2619
2411
  }
2620
2412
 
2621
2413
  const runLoader = async () => {
@@ -2662,17 +2454,19 @@ export class RouterCore<
2662
2454
  // to be preloaded before we resolve the match
2663
2455
  await route._componentsPromise
2664
2456
 
2665
- batch(() => {
2666
- updateMatch(matchId, (prev) => ({
2667
- ...prev,
2668
- error: undefined,
2669
- status: 'success',
2670
- isFetching: false,
2671
- updatedAt: Date.now(),
2672
- loaderData,
2673
- }))
2674
- executeHead()
2675
- })
2457
+ updateMatch(matchId, (prev) => ({
2458
+ ...prev,
2459
+ error: undefined,
2460
+ status: 'success',
2461
+ isFetching: false,
2462
+ updatedAt: Date.now(),
2463
+ loaderData,
2464
+ }))
2465
+ const head = await executeHead()
2466
+ updateMatch(matchId, (prev) => ({
2467
+ ...prev,
2468
+ ...head,
2469
+ }))
2676
2470
  } catch (e) {
2677
2471
  let error = e
2678
2472
 
@@ -2689,16 +2483,14 @@ export class RouterCore<
2689
2483
  onErrorError,
2690
2484
  )
2691
2485
  }
2692
-
2693
- batch(() => {
2694
- updateMatch(matchId, (prev) => ({
2695
- ...prev,
2696
- error,
2697
- status: 'error',
2698
- isFetching: false,
2699
- }))
2700
- executeHead()
2701
- })
2486
+ const head = await executeHead()
2487
+ updateMatch(matchId, (prev) => ({
2488
+ ...prev,
2489
+ error,
2490
+ status: 'error',
2491
+ isFetching: false,
2492
+ ...head,
2493
+ }))
2702
2494
  }
2703
2495
 
2704
2496
  this.serverSsr?.onMatchSettled({
@@ -2706,13 +2498,13 @@ export class RouterCore<
2706
2498
  match: this.getMatch(matchId)!,
2707
2499
  })
2708
2500
  } catch (err) {
2709
- batch(() => {
2710
- updateMatch(matchId, (prev) => ({
2711
- ...prev,
2712
- loaderPromise: undefined,
2713
- }))
2714
- executeHead()
2715
- })
2501
+ const head = await executeHead()
2502
+
2503
+ updateMatch(matchId, (prev) => ({
2504
+ ...prev,
2505
+ loaderPromise: undefined,
2506
+ ...head,
2507
+ }))
2716
2508
  handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2717
2509
  }
2718
2510
  }
@@ -2738,8 +2530,8 @@ export class RouterCore<
2738
2530
  loaderPromise: undefined,
2739
2531
  }))
2740
2532
  } catch (err) {
2741
- if (isResolvedRedirect(err)) {
2742
- await this.navigate(err)
2533
+ if (isRedirect(err)) {
2534
+ await this.navigate(err.options)
2743
2535
  }
2744
2536
  }
2745
2537
  })()
@@ -2752,7 +2544,11 @@ export class RouterCore<
2752
2544
  // if the loader did not run, still update head.
2753
2545
  // reason: parent's beforeLoad may have changed the route context
2754
2546
  // and only now do we know the route context (and that the loader would not run)
2755
- executeHead()
2547
+ const head = await executeHead()
2548
+ updateMatch(matchId, (prev) => ({
2549
+ ...prev,
2550
+ ...head,
2551
+ }))
2756
2552
  }
2757
2553
  }
2758
2554
  if (!loaderIsRunningAsync) {
@@ -2829,11 +2625,14 @@ export class RouterCore<
2829
2625
  return this.load({ sync: opts?.sync })
2830
2626
  }
2831
2627
 
2832
- resolveRedirect = (err: AnyRedirect): ResolvedRedirect => {
2833
- const redirect = err as ResolvedRedirect
2628
+ resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
2629
+ if (!redirect.options.href) {
2630
+ redirect.options.href = this.buildLocation(redirect.options).href
2631
+ redirect.headers.set('Location', redirect.options.href)
2632
+ }
2834
2633
 
2835
- if (!redirect.href) {
2836
- redirect.href = this.buildLocation(redirect as any).href
2634
+ if (!redirect.headers.get('Location')) {
2635
+ redirect.headers.set('Location', redirect.options.href)
2837
2636
  }
2838
2637
 
2839
2638
  return redirect
@@ -2968,11 +2767,12 @@ export class RouterCore<
2968
2767
  return matches
2969
2768
  } catch (err) {
2970
2769
  if (isRedirect(err)) {
2971
- if (err.reloadDocument) {
2770
+ if (err.options.reloadDocument) {
2972
2771
  return undefined
2973
2772
  }
2773
+
2974
2774
  return await this.preloadRoute({
2975
- ...(err as any),
2775
+ ...err.options,
2976
2776
  _fromLocation: next,
2977
2777
  })
2978
2778
  }
@@ -3206,3 +3006,338 @@ function routeNeedsPreload(route: AnyRoute) {
3206
3006
  }
3207
3007
  return false
3208
3008
  }
3009
+
3010
+ interface RouteLike {
3011
+ id: string
3012
+ isRoot?: boolean
3013
+ path?: string
3014
+ fullPath: string
3015
+ rank?: number
3016
+ parentRoute?: RouteLike
3017
+ children?: Array<RouteLike>
3018
+ options?: {
3019
+ caseSensitive?: boolean
3020
+ }
3021
+ }
3022
+
3023
+ export function processRouteTree<TRouteLike extends RouteLike>({
3024
+ routeTree,
3025
+ initRoute,
3026
+ }: {
3027
+ routeTree: TRouteLike
3028
+ initRoute?: (route: TRouteLike, index: number) => void
3029
+ }) {
3030
+ const routesById = {} as Record<string, TRouteLike>
3031
+ const routesByPath = {} as Record<string, TRouteLike>
3032
+
3033
+ const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
3034
+ childRoutes.forEach((childRoute, i) => {
3035
+ initRoute?.(childRoute, i)
3036
+
3037
+ const existingRoute = routesById[childRoute.id]
3038
+
3039
+ invariant(
3040
+ !existingRoute,
3041
+ `Duplicate routes found with id: ${String(childRoute.id)}`,
3042
+ )
3043
+
3044
+ routesById[childRoute.id] = childRoute
3045
+
3046
+ if (!childRoute.isRoot && childRoute.path) {
3047
+ const trimmedFullPath = trimPathRight(childRoute.fullPath)
3048
+ if (
3049
+ !routesByPath[trimmedFullPath] ||
3050
+ childRoute.fullPath.endsWith('/')
3051
+ ) {
3052
+ routesByPath[trimmedFullPath] = childRoute
3053
+ }
3054
+ }
3055
+
3056
+ const children = childRoute.children as Array<TRouteLike>
3057
+
3058
+ if (children?.length) {
3059
+ recurseRoutes(children)
3060
+ }
3061
+ })
3062
+ }
3063
+
3064
+ recurseRoutes([routeTree])
3065
+
3066
+ const scoredRoutes: Array<{
3067
+ child: TRouteLike
3068
+ trimmed: string
3069
+ parsed: ReturnType<typeof parsePathname>
3070
+ index: number
3071
+ scores: Array<number>
3072
+ }> = []
3073
+
3074
+ const routes: Array<TRouteLike> = Object.values(routesById)
3075
+
3076
+ routes.forEach((d, i) => {
3077
+ if (d.isRoot || !d.path) {
3078
+ return
3079
+ }
3080
+
3081
+ const trimmed = trimPathLeft(d.fullPath)
3082
+ const parsed = parsePathname(trimmed)
3083
+
3084
+ // Removes the leading slash if it is not the only remaining segment
3085
+ while (parsed.length > 1 && parsed[0]?.value === '/') {
3086
+ parsed.shift()
3087
+ }
3088
+
3089
+ const scores = parsed.map((segment) => {
3090
+ if (segment.value === '/') {
3091
+ return 0.75
3092
+ }
3093
+
3094
+ if (
3095
+ segment.type === 'param' &&
3096
+ segment.prefixSegment &&
3097
+ segment.suffixSegment
3098
+ ) {
3099
+ return 0.55
3100
+ }
3101
+
3102
+ if (segment.type === 'param' && segment.prefixSegment) {
3103
+ return 0.52
3104
+ }
3105
+
3106
+ if (segment.type === 'param' && segment.suffixSegment) {
3107
+ return 0.51
3108
+ }
3109
+
3110
+ if (segment.type === 'param') {
3111
+ return 0.5
3112
+ }
3113
+
3114
+ if (
3115
+ segment.type === 'wildcard' &&
3116
+ segment.prefixSegment &&
3117
+ segment.suffixSegment
3118
+ ) {
3119
+ return 0.3
3120
+ }
3121
+
3122
+ if (segment.type === 'wildcard' && segment.prefixSegment) {
3123
+ return 0.27
3124
+ }
3125
+
3126
+ if (segment.type === 'wildcard' && segment.suffixSegment) {
3127
+ return 0.26
3128
+ }
3129
+
3130
+ if (segment.type === 'wildcard') {
3131
+ return 0.25
3132
+ }
3133
+
3134
+ return 1
3135
+ })
3136
+
3137
+ scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
3138
+ })
3139
+
3140
+ const flatRoutes = scoredRoutes
3141
+ .sort((a, b) => {
3142
+ const minLength = Math.min(a.scores.length, b.scores.length)
3143
+
3144
+ // Sort by min available score
3145
+ for (let i = 0; i < minLength; i++) {
3146
+ if (a.scores[i] !== b.scores[i]) {
3147
+ return b.scores[i]! - a.scores[i]!
3148
+ }
3149
+ }
3150
+
3151
+ // Sort by length of score
3152
+ if (a.scores.length !== b.scores.length) {
3153
+ return b.scores.length - a.scores.length
3154
+ }
3155
+
3156
+ // Sort by min available parsed value
3157
+ for (let i = 0; i < minLength; i++) {
3158
+ if (a.parsed[i]!.value !== b.parsed[i]!.value) {
3159
+ return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
3160
+ }
3161
+ }
3162
+
3163
+ // Sort by original index
3164
+ return a.index - b.index
3165
+ })
3166
+ .map((d, i) => {
3167
+ d.child.rank = i
3168
+ return d.child
3169
+ })
3170
+
3171
+ return { routesById, routesByPath, flatRoutes }
3172
+ }
3173
+
3174
+ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3175
+ pathname,
3176
+ routePathname,
3177
+ basepath,
3178
+ caseSensitive,
3179
+ routesByPath,
3180
+ routesById,
3181
+ flatRoutes,
3182
+ }: {
3183
+ pathname: string
3184
+ routePathname?: string
3185
+ basepath: string
3186
+ caseSensitive?: boolean
3187
+ routesByPath: Record<string, TRouteLike>
3188
+ routesById: Record<string, TRouteLike>
3189
+ flatRoutes: Array<TRouteLike>
3190
+ }) {
3191
+ let routeParams: Record<string, string> = {}
3192
+ const trimmedPath = trimPathRight(pathname)
3193
+ const getMatchedParams = (route: TRouteLike) => {
3194
+ const result = matchPathname(basepath, trimmedPath, {
3195
+ to: route.fullPath,
3196
+ caseSensitive: route.options?.caseSensitive ?? caseSensitive,
3197
+ fuzzy: true,
3198
+ })
3199
+ return result
3200
+ }
3201
+
3202
+ let foundRoute: TRouteLike | undefined =
3203
+ routePathname !== undefined ? routesByPath[routePathname] : undefined
3204
+ if (foundRoute) {
3205
+ routeParams = getMatchedParams(foundRoute)!
3206
+ } else {
3207
+ foundRoute = flatRoutes.find((route) => {
3208
+ const matchedParams = getMatchedParams(route)
3209
+
3210
+ if (matchedParams) {
3211
+ routeParams = matchedParams
3212
+ return true
3213
+ }
3214
+
3215
+ return false
3216
+ })
3217
+ }
3218
+
3219
+ let routeCursor: TRouteLike = foundRoute || routesById[rootRouteId]!
3220
+
3221
+ const matchedRoutes: Array<TRouteLike> = [routeCursor]
3222
+
3223
+ while (routeCursor.parentRoute) {
3224
+ routeCursor = routeCursor.parentRoute as TRouteLike
3225
+ matchedRoutes.unshift(routeCursor)
3226
+ }
3227
+
3228
+ return { matchedRoutes, routeParams, foundRoute }
3229
+ }
3230
+
3231
+ function applySearchMiddleware({
3232
+ search,
3233
+ dest,
3234
+ destRoutes,
3235
+ _includeValidateSearch,
3236
+ }: {
3237
+ search: any
3238
+ dest: BuildNextOptions
3239
+ destRoutes: Array<AnyRoute>
3240
+ _includeValidateSearch: boolean | undefined
3241
+ }) {
3242
+ const allMiddlewares =
3243
+ destRoutes.reduce(
3244
+ (acc, route) => {
3245
+ const middlewares: Array<SearchMiddleware<any>> = []
3246
+
3247
+ if ('search' in route.options) {
3248
+ if (route.options.search?.middlewares) {
3249
+ middlewares.push(...route.options.search.middlewares)
3250
+ }
3251
+ }
3252
+ // TODO remove preSearchFilters and postSearchFilters in v2
3253
+ else if (
3254
+ route.options.preSearchFilters ||
3255
+ route.options.postSearchFilters
3256
+ ) {
3257
+ const legacyMiddleware: SearchMiddleware<any> = ({
3258
+ search,
3259
+ next,
3260
+ }) => {
3261
+ let nextSearch = search
3262
+
3263
+ if (
3264
+ 'preSearchFilters' in route.options &&
3265
+ route.options.preSearchFilters
3266
+ ) {
3267
+ nextSearch = route.options.preSearchFilters.reduce(
3268
+ (prev, next) => next(prev),
3269
+ search,
3270
+ )
3271
+ }
3272
+
3273
+ const result = next(nextSearch)
3274
+
3275
+ if (
3276
+ 'postSearchFilters' in route.options &&
3277
+ route.options.postSearchFilters
3278
+ ) {
3279
+ return route.options.postSearchFilters.reduce(
3280
+ (prev, next) => next(prev),
3281
+ result,
3282
+ )
3283
+ }
3284
+
3285
+ return result
3286
+ }
3287
+ middlewares.push(legacyMiddleware)
3288
+ }
3289
+
3290
+ if (_includeValidateSearch && route.options.validateSearch) {
3291
+ const validate: SearchMiddleware<any> = ({ search, next }) => {
3292
+ const result = next(search)
3293
+ try {
3294
+ const validatedSearch = {
3295
+ ...result,
3296
+ ...(validateSearch(route.options.validateSearch, result) ?? {}),
3297
+ }
3298
+ return validatedSearch
3299
+ } catch {
3300
+ // ignore errors here because they are already handled in matchRoutes
3301
+ return result
3302
+ }
3303
+ }
3304
+
3305
+ middlewares.push(validate)
3306
+ }
3307
+
3308
+ return acc.concat(middlewares)
3309
+ },
3310
+ [] as Array<SearchMiddleware<any>>,
3311
+ ) ?? []
3312
+
3313
+ // the chain ends here since `next` is not called
3314
+ const final: SearchMiddleware<any> = ({ search }) => {
3315
+ if (!dest.search) {
3316
+ return {}
3317
+ }
3318
+ if (dest.search === true) {
3319
+ return search
3320
+ }
3321
+ return functionalUpdate(dest.search, search)
3322
+ }
3323
+
3324
+ allMiddlewares.push(final)
3325
+
3326
+ const applyNext = (index: number, currentSearch: any): any => {
3327
+ // no more middlewares left, return the current search
3328
+ if (index >= allMiddlewares.length) {
3329
+ return currentSearch
3330
+ }
3331
+
3332
+ const middleware = allMiddlewares[index]!
3333
+
3334
+ const next = (newSearch: any): any => {
3335
+ return applyNext(index + 1, newSearch)
3336
+ }
3337
+
3338
+ return middleware({ search: currentSearch, next })
3339
+ }
3340
+
3341
+ // Start applying middlewares
3342
+ return applyNext(0, search)
3343
+ }