@tanstack/router-core 1.120.4 → 1.121.0-alpha.1

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 +6 -6
  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 -14
  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 +17 -18
  16. package/dist/cjs/router.cjs +290 -211
  17. package/dist/cjs/router.cjs.map +1 -1
  18. package/dist/cjs/router.d.cts +46 -3
  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 +2 -0
  22. package/dist/esm/fileRoute.d.ts +6 -2
  23. package/dist/esm/index.d.ts +6 -6
  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 -14
  32. package/dist/esm/redirect.js.map +1 -1
  33. package/dist/esm/route.d.ts +17 -18
  34. package/dist/esm/route.js +12 -1
  35. package/dist/esm/route.js.map +1 -1
  36. package/dist/esm/router.d.ts +46 -3
  37. package/dist/esm/router.js +293 -214
  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 +2 -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 -6
  45. package/src/link.ts +97 -11
  46. package/src/path.ts +181 -16
  47. package/src/redirect.ts +37 -22
  48. package/src/route.ts +119 -39
  49. package/src/router.ts +393 -269
  50. package/src/typePrimitives.ts +2 -2
  51. package/src/utils.ts +14 -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 {
@@ -593,8 +601,8 @@ export type ParseLocationFn<TRouteTree extends AnyRoute> = (
593
601
  ) => ParsedLocation<FullSearchSchema<TRouteTree>>
594
602
 
595
603
  export type GetMatchRoutesFn = (
596
- next: ParsedLocation,
597
- dest?: BuildNextOptions,
604
+ pathname: string,
605
+ routePathname: string | undefined,
598
606
  ) => {
599
607
  matchedRoutes: Array<AnyRoute>
600
608
  routeParams: Record<string, string>
@@ -836,6 +844,8 @@ export class RouterCore<
836
844
  // router can be used in a non-react environment if necessary
837
845
  startTransition: StartTransitionFn = (fn) => fn()
838
846
 
847
+ isShell = false
848
+
839
849
  update: UpdateFn<
840
850
  TRouteTree,
841
851
  TTrailingSlashOption,
@@ -882,7 +892,6 @@ export class RouterCore<
882
892
  }
883
893
 
884
894
  if (
885
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
886
895
  !this.history ||
887
896
  (this.options.history && this.options.history !== this.history)
888
897
  ) {
@@ -901,7 +910,6 @@ export class RouterCore<
901
910
  this.buildRouteTree()
902
911
  }
903
912
 
904
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
905
913
  if (!this.__store) {
906
914
  this.__store = new Store(getInitialRouterState(this.latestLocation), {
907
915
  onUpdate: () => {
@@ -920,13 +928,16 @@ export class RouterCore<
920
928
  if (
921
929
  typeof window !== 'undefined' &&
922
930
  'CSS' in window &&
923
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
924
931
  typeof window.CSS?.supports === 'function'
925
932
  ) {
926
933
  this.isViewTransitionTypesSupported = window.CSS.supports(
927
934
  'selector(:active-view-transition-type(a)',
928
935
  )
929
936
  }
937
+
938
+ if ((this.latestLocation.search as any).__TSS_SHELL) {
939
+ this.isShell = true
940
+ }
930
941
  }
931
942
 
932
943
  get state() {
@@ -934,124 +945,29 @@ export class RouterCore<
934
945
  }
935
946
 
936
947
  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({
948
+ const { routesById, routesByPath, flatRoutes } = processRouteTree({
949
+ routeTree: this.routeTree,
950
+ initRoute: (route, i) => {
951
+ route.init({
952
952
  originalIndex: i,
953
953
  defaultSsr: this.options.defaultSsr,
954
954
  })
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 })
955
+ },
1023
956
  })
1024
957
 
1025
- this.flatRoutes = scoredRoutes
1026
- .sort((a, b) => {
1027
- const minLength = Math.min(a.scores.length, b.scores.length)
1028
-
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
- }
958
+ this.routesById = routesById as RoutesById<TRouteTree>
959
+ this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
960
+ this.flatRoutes = flatRoutes as Array<AnyRoute>
1035
961
 
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
- }
962
+ const notFoundRoute = this.options.notFoundRoute
1047
963
 
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
964
+ if (notFoundRoute) {
965
+ notFoundRoute.init({
966
+ originalIndex: 99999999999,
967
+ defaultSsr: this.options.defaultSsr,
1054
968
  })
969
+ this.routesById[notFoundRoute.id] = notFoundRoute
970
+ }
1055
971
  }
1056
972
 
1057
973
  subscribe: SubscribeFn = (eventType, fn) => {
@@ -1165,8 +1081,8 @@ export class RouterCore<
1165
1081
  opts?: MatchRoutesOpts,
1166
1082
  ): Array<AnyRouteMatch> {
1167
1083
  const { foundRoute, matchedRoutes, routeParams } = this.getMatchedRoutes(
1168
- next,
1169
- opts?.dest,
1084
+ next.pathname,
1085
+ opts?.dest?.to as string,
1170
1086
  )
1171
1087
  let isGlobalNotFound = false
1172
1088
 
@@ -1447,72 +1363,24 @@ export class RouterCore<
1447
1363
  ...match.__beforeLoadContext,
1448
1364
  }
1449
1365
  }
1450
-
1451
- // If it's already a success, update headers and head content
1452
- // These may get updated again if the match is refreshed
1453
- // due to being stale
1454
- if (match.status === 'success') {
1455
- match.headers = route.options.headers?.({
1456
- loaderData: match.loaderData,
1457
- })
1458
- const assetContext = {
1459
- matches,
1460
- match,
1461
- params: match.params,
1462
- loaderData: match.loaderData,
1463
- }
1464
- const headFnContent = route.options.head?.(assetContext)
1465
- match.links = headFnContent?.links
1466
- match.headScripts = headFnContent?.scripts
1467
- match.meta = headFnContent?.meta
1468
- match.scripts = route.options.scripts?.(assetContext)
1469
- }
1470
1366
  })
1471
1367
 
1472
1368
  return matches
1473
1369
  }
1474
1370
 
1475
- getMatchedRoutes: GetMatchRoutesFn = (next, dest) => {
1476
- let routeParams: Record<string, string> = {}
1477
- const trimmedPath = trimPathRight(next.pathname)
1478
- const getMatchedParams = (route: AnyRoute) => {
1479
- const result = matchPathname(this.basepath, trimmedPath, {
1480
- to: route.fullPath,
1481
- caseSensitive:
1482
- route.options.caseSensitive ?? this.options.caseSensitive,
1483
- fuzzy: true,
1484
- })
1485
- return result
1486
- }
1487
-
1488
- let foundRoute: AnyRoute | undefined =
1489
- dest?.to !== undefined ? this.routesByPath[dest.to!] : undefined
1490
- if (foundRoute) {
1491
- routeParams = getMatchedParams(foundRoute)!
1492
- } else {
1493
- foundRoute = this.flatRoutes.find((route) => {
1494
- const matchedParams = getMatchedParams(route)
1495
-
1496
- if (matchedParams) {
1497
- routeParams = matchedParams
1498
- return true
1499
- }
1500
-
1501
- return false
1502
- })
1503
- }
1504
-
1505
- let routeCursor: AnyRoute =
1506
- foundRoute || (this.routesById as any)[rootRouteId]
1507
-
1508
- const matchedRoutes: Array<AnyRoute> = [routeCursor]
1509
-
1510
- while (routeCursor.parentRoute) {
1511
- routeCursor = routeCursor.parentRoute
1512
- matchedRoutes.unshift(routeCursor)
1513
- }
1514
-
1515
- return { matchedRoutes, routeParams, foundRoute }
1371
+ getMatchedRoutes: GetMatchRoutesFn = (
1372
+ pathname: string,
1373
+ routePathname: string | undefined,
1374
+ ) => {
1375
+ return getMatchedRoutes({
1376
+ pathname,
1377
+ routePathname,
1378
+ basepath: this.basepath,
1379
+ caseSensitive: this.options.caseSensitive,
1380
+ routesByPath: this.routesByPath,
1381
+ routesById: this.routesById,
1382
+ flatRoutes: this.flatRoutes,
1383
+ })
1516
1384
  }
1517
1385
 
1518
1386
  cancelMatch = (id: string) => {
@@ -1812,11 +1680,17 @@ export class RouterCore<
1812
1680
  }
1813
1681
  }
1814
1682
 
1815
- const nextMatches = this.getMatchedRoutes(next, dest)
1683
+ const nextMatches = this.getMatchedRoutes(
1684
+ next.pathname,
1685
+ dest.to as string,
1686
+ )
1816
1687
  const final = build(dest, nextMatches)
1817
1688
 
1818
1689
  if (maskedNext) {
1819
- const maskedMatches = this.getMatchedRoutes(maskedNext, maskedDest)
1690
+ const maskedMatches = this.getMatchedRoutes(
1691
+ maskedNext.pathname,
1692
+ maskedDest?.to as string,
1693
+ )
1820
1694
  const maskedFinal = build(maskedDest, maskedMatches)
1821
1695
  final.maskedLocation = maskedFinal
1822
1696
  }
@@ -1958,6 +1832,13 @@ export class RouterCore<
1958
1832
  }
1959
1833
 
1960
1834
  navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
1835
+ if (!reloadDocument && href) {
1836
+ try {
1837
+ new URL(`${href}`)
1838
+ reloadDocument = true
1839
+ } catch {}
1840
+ }
1841
+
1961
1842
  if (reloadDocument) {
1962
1843
  if (!href) {
1963
1844
  const location = this.buildLocation({ to, ...rest } as any)
@@ -1980,10 +1861,30 @@ export class RouterCore<
1980
1861
 
1981
1862
  latestLoadPromise: undefined | Promise<void>
1982
1863
 
1983
- load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
1864
+ beforeLoad = () => {
1865
+ // Cancel any pending matches
1866
+ this.cancelMatches()
1984
1867
  this.latestLocation = this.parseLocation(this.latestLocation)
1985
1868
 
1986
- let redirect: ResolvedRedirect | undefined
1869
+ // Match the routes
1870
+ const pendingMatches = this.matchRoutes(this.latestLocation)
1871
+
1872
+ // Ingest the new matches
1873
+ this.__store.setState((s) => ({
1874
+ ...s,
1875
+ status: 'pending',
1876
+ isLoading: true,
1877
+ location: this.latestLocation,
1878
+ pendingMatches,
1879
+ // If a cached moved to pendingMatches, remove it from cachedMatches
1880
+ cachedMatches: s.cachedMatches.filter((d) => {
1881
+ return !pendingMatches.find((e) => e.id === d.id)
1882
+ }),
1883
+ }))
1884
+ }
1885
+
1886
+ load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
1887
+ let redirect: AnyRedirect | undefined
1987
1888
  let notFound: NotFoundError | undefined
1988
1889
 
1989
1890
  let loadPromise: Promise<void>
@@ -1992,36 +1893,10 @@ export class RouterCore<
1992
1893
  loadPromise = new Promise<void>((resolve) => {
1993
1894
  this.startTransition(async () => {
1994
1895
  try {
1896
+ this.beforeLoad()
1995
1897
  const next = this.latestLocation
1996
1898
  const prevLocation = this.state.resolvedLocation
1997
1899
 
1998
- // Cancel any pending matches
1999
- this.cancelMatches()
2000
-
2001
- let pendingMatches!: Array<AnyRouteMatch>
2002
-
2003
- batch(() => {
2004
- // this call breaks a route context of destination route after a redirect
2005
- // we should be fine not eagerly calling this since we call it later
2006
- // this.clearExpiredCache()
2007
-
2008
- // Match the routes
2009
- pendingMatches = this.matchRoutes(next)
2010
-
2011
- // Ingest the new matches
2012
- this.__store.setState((s) => ({
2013
- ...s,
2014
- status: 'pending',
2015
- isLoading: true,
2016
- location: next,
2017
- pendingMatches,
2018
- // If a cached moved to pendingMatches, remove it from cachedMatches
2019
- cachedMatches: s.cachedMatches.filter((d) => {
2020
- return !pendingMatches.find((e) => e.id === d.id)
2021
- }),
2022
- }))
2023
- })
2024
-
2025
1900
  if (!this.state.redirect) {
2026
1901
  this.emit({
2027
1902
  type: 'onBeforeNavigate',
@@ -2042,7 +1917,7 @@ export class RouterCore<
2042
1917
 
2043
1918
  await this.loadMatches({
2044
1919
  sync: opts?.sync,
2045
- matches: pendingMatches,
1920
+ matches: this.state.pendingMatches as Array<AnyRouteMatch>,
2046
1921
  location: next,
2047
1922
  // eslint-disable-next-line @typescript-eslint/require-await
2048
1923
  onReady: async () => {
@@ -2103,11 +1978,11 @@ export class RouterCore<
2103
1978
  },
2104
1979
  })
2105
1980
  } catch (err) {
2106
- if (isResolvedRedirect(err)) {
1981
+ if (isRedirect(err)) {
2107
1982
  redirect = err
2108
1983
  if (!this.isServer) {
2109
1984
  this.navigate({
2110
- ...redirect,
1985
+ ...redirect.options,
2111
1986
  replace: true,
2112
1987
  ignoreBlocker: true,
2113
1988
  })
@@ -2119,7 +1994,7 @@ export class RouterCore<
2119
1994
  this.__store.setState((s) => ({
2120
1995
  ...s,
2121
1996
  statusCode: redirect
2122
- ? redirect.statusCode
1997
+ ? redirect.status
2123
1998
  : notFound
2124
1999
  ? 404
2125
2000
  : s.matches.some((d) => d.status === 'error')
@@ -2275,13 +2150,15 @@ export class RouterCore<
2275
2150
  }
2276
2151
 
2277
2152
  const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
2278
- if (isResolvedRedirect(err)) {
2279
- if (!err.reloadDocument) {
2280
- throw err
2153
+ if (isRedirect(err) || isNotFound(err)) {
2154
+ if (isRedirect(err)) {
2155
+ if (err.redirectHandled) {
2156
+ if (!err.options.reloadDocument) {
2157
+ throw err
2158
+ }
2159
+ }
2281
2160
  }
2282
- }
2283
2161
 
2284
- if (isRedirect(err) || isNotFound(err)) {
2285
2162
  updateMatch(match.id, (prev) => ({
2286
2163
  ...prev,
2287
2164
  status: isRedirect(err)
@@ -2305,7 +2182,9 @@ export class RouterCore<
2305
2182
 
2306
2183
  if (isRedirect(err)) {
2307
2184
  rendered = true
2308
- err = this.resolveRedirect({ ...err, _fromLocation: location })
2185
+ err.options._fromLocation = location
2186
+ err.redirectHandled = true
2187
+ err = this.resolveRedirect(err)
2309
2188
  throw err
2310
2189
  } else if (isNotFound(err)) {
2311
2190
  this._handleNotFound(matches, err, {
@@ -2609,6 +2488,35 @@ export class RouterCore<
2609
2488
  !this.state.matches.find((d) => d.id === matchId),
2610
2489
  }))
2611
2490
 
2491
+ const executeHead = () => {
2492
+ const match = this.getMatch(matchId)
2493
+ // in case of a redirecting match during preload, the match does not exist
2494
+ if (!match) {
2495
+ return
2496
+ }
2497
+ const assetContext = {
2498
+ matches,
2499
+ match,
2500
+ params: match.params,
2501
+ loaderData: match.loaderData,
2502
+ }
2503
+ const headFnContent = route.options.head?.(assetContext)
2504
+ const meta = headFnContent?.meta
2505
+ const links = headFnContent?.links
2506
+ const headScripts = headFnContent?.scripts
2507
+
2508
+ const scripts = route.options.scripts?.(assetContext)
2509
+ const headers = route.options.headers?.(assetContext)
2510
+ updateMatch(matchId, (prev) => ({
2511
+ ...prev,
2512
+ meta,
2513
+ links,
2514
+ headScripts,
2515
+ headers,
2516
+ scripts,
2517
+ }))
2518
+ }
2519
+
2612
2520
  const runLoader = async () => {
2613
2521
  try {
2614
2522
  // If the Matches component rendered
@@ -2649,40 +2557,21 @@ export class RouterCore<
2649
2557
 
2650
2558
  await potentialPendingMinPromise()
2651
2559
 
2652
- const assetContext = {
2653
- matches,
2654
- match: this.getMatch(matchId)!,
2655
- params: this.getMatch(matchId)!.params,
2656
- loaderData,
2657
- }
2658
- const headFnContent =
2659
- route.options.head?.(assetContext)
2660
- const meta = headFnContent?.meta
2661
- const links = headFnContent?.links
2662
- const headScripts = headFnContent?.scripts
2663
-
2664
- const scripts = route.options.scripts?.(assetContext)
2665
- const headers = route.options.headers?.({
2666
- loaderData,
2667
- })
2668
-
2669
2560
  // Last but not least, wait for the the components
2670
2561
  // to be preloaded before we resolve the match
2671
2562
  await route._componentsPromise
2672
2563
 
2673
- updateMatch(matchId, (prev) => ({
2674
- ...prev,
2675
- error: undefined,
2676
- status: 'success',
2677
- isFetching: false,
2678
- updatedAt: Date.now(),
2679
- loaderData,
2680
- meta,
2681
- links,
2682
- headScripts,
2683
- headers,
2684
- scripts,
2685
- }))
2564
+ batch(() => {
2565
+ updateMatch(matchId, (prev) => ({
2566
+ ...prev,
2567
+ error: undefined,
2568
+ status: 'success',
2569
+ isFetching: false,
2570
+ updatedAt: Date.now(),
2571
+ loaderData,
2572
+ }))
2573
+ executeHead()
2574
+ })
2686
2575
  } catch (e) {
2687
2576
  let error = e
2688
2577
 
@@ -2700,12 +2589,15 @@ export class RouterCore<
2700
2589
  )
2701
2590
  }
2702
2591
 
2703
- updateMatch(matchId, (prev) => ({
2704
- ...prev,
2705
- error,
2706
- status: 'error',
2707
- isFetching: false,
2708
- }))
2592
+ batch(() => {
2593
+ updateMatch(matchId, (prev) => ({
2594
+ ...prev,
2595
+ error,
2596
+ status: 'error',
2597
+ isFetching: false,
2598
+ }))
2599
+ executeHead()
2600
+ })
2709
2601
  }
2710
2602
 
2711
2603
  this.serverSsr?.onMatchSettled({
@@ -2713,10 +2605,13 @@ export class RouterCore<
2713
2605
  match: this.getMatch(matchId)!,
2714
2606
  })
2715
2607
  } catch (err) {
2716
- updateMatch(matchId, (prev) => ({
2717
- ...prev,
2718
- loaderPromise: undefined,
2719
- }))
2608
+ batch(() => {
2609
+ updateMatch(matchId, (prev) => ({
2610
+ ...prev,
2611
+ loaderPromise: undefined,
2612
+ }))
2613
+ executeHead()
2614
+ })
2720
2615
  handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2721
2616
  }
2722
2617
  }
@@ -2742,8 +2637,8 @@ export class RouterCore<
2742
2637
  loaderPromise: undefined,
2743
2638
  }))
2744
2639
  } catch (err) {
2745
- if (isResolvedRedirect(err)) {
2746
- await this.navigate(err)
2640
+ if (isRedirect(err)) {
2641
+ await this.navigate(err.options)
2747
2642
  }
2748
2643
  }
2749
2644
  })()
@@ -2752,6 +2647,11 @@ export class RouterCore<
2752
2647
  (loaderShouldRunAsync && sync)
2753
2648
  ) {
2754
2649
  await runLoader()
2650
+ } else {
2651
+ // if the loader did not run, still update head.
2652
+ // reason: parent's beforeLoad may have changed the route context
2653
+ // and only now do we know the route context (and that the loader would not run)
2654
+ executeHead()
2755
2655
  }
2756
2656
  }
2757
2657
  if (!loaderIsRunningAsync) {
@@ -2828,11 +2728,14 @@ export class RouterCore<
2828
2728
  return this.load({ sync: opts?.sync })
2829
2729
  }
2830
2730
 
2831
- resolveRedirect = (err: AnyRedirect): ResolvedRedirect => {
2832
- const redirect = err as ResolvedRedirect
2731
+ resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
2732
+ if (!redirect.options.href) {
2733
+ redirect.options.href = this.buildLocation(redirect.options).href
2734
+ redirect.headers.set('Location', redirect.options.href)
2735
+ }
2833
2736
 
2834
- if (!redirect.href) {
2835
- redirect.href = this.buildLocation(redirect as any).href
2737
+ if (!redirect.headers.get('Location')) {
2738
+ redirect.headers.set('Location', redirect.options.href)
2836
2739
  }
2837
2740
 
2838
2741
  return redirect
@@ -2967,11 +2870,11 @@ export class RouterCore<
2967
2870
  return matches
2968
2871
  } catch (err) {
2969
2872
  if (isRedirect(err)) {
2970
- if (err.reloadDocument) {
2873
+ if (err.options.reloadDocument) {
2971
2874
  return undefined
2972
2875
  }
2973
2876
  return await this.preloadRoute({
2974
- ...(err as any),
2877
+ ...err.options,
2975
2878
  _fromLocation: next,
2976
2879
  })
2977
2880
  }
@@ -3205,3 +3108,224 @@ function routeNeedsPreload(route: AnyRoute) {
3205
3108
  }
3206
3109
  return false
3207
3110
  }
3111
+
3112
+ interface RouteLike {
3113
+ id: string
3114
+ isRoot?: boolean
3115
+ path?: string
3116
+ fullPath: string
3117
+ rank?: number
3118
+ parentRoute?: RouteLike
3119
+ children?: Array<RouteLike>
3120
+ options?: {
3121
+ caseSensitive?: boolean
3122
+ }
3123
+ }
3124
+
3125
+ export function processRouteTree<TRouteLike extends RouteLike>({
3126
+ routeTree,
3127
+ initRoute,
3128
+ }: {
3129
+ routeTree: TRouteLike
3130
+ initRoute?: (route: TRouteLike, index: number) => void
3131
+ }) {
3132
+ const routesById = {} as Record<string, TRouteLike>
3133
+ const routesByPath = {} as Record<string, TRouteLike>
3134
+
3135
+ const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
3136
+ childRoutes.forEach((childRoute, i) => {
3137
+ initRoute?.(childRoute, i)
3138
+
3139
+ const existingRoute = routesById[childRoute.id]
3140
+
3141
+ invariant(
3142
+ !existingRoute,
3143
+ `Duplicate routes found with id: ${String(childRoute.id)}`,
3144
+ )
3145
+
3146
+ routesById[childRoute.id] = childRoute
3147
+
3148
+ if (!childRoute.isRoot && childRoute.path) {
3149
+ const trimmedFullPath = trimPathRight(childRoute.fullPath)
3150
+ if (
3151
+ !routesByPath[trimmedFullPath] ||
3152
+ childRoute.fullPath.endsWith('/')
3153
+ ) {
3154
+ routesByPath[trimmedFullPath] = childRoute
3155
+ }
3156
+ }
3157
+
3158
+ const children = childRoute.children as Array<TRouteLike>
3159
+
3160
+ if (children?.length) {
3161
+ recurseRoutes(children)
3162
+ }
3163
+ })
3164
+ }
3165
+
3166
+ recurseRoutes([routeTree])
3167
+
3168
+ const scoredRoutes: Array<{
3169
+ child: TRouteLike
3170
+ trimmed: string
3171
+ parsed: ReturnType<typeof parsePathname>
3172
+ index: number
3173
+ scores: Array<number>
3174
+ }> = []
3175
+
3176
+ const routes: Array<TRouteLike> = Object.values(routesById)
3177
+
3178
+ routes.forEach((d, i) => {
3179
+ if (d.isRoot || !d.path) {
3180
+ return
3181
+ }
3182
+
3183
+ const trimmed = trimPathLeft(d.fullPath)
3184
+ const parsed = parsePathname(trimmed)
3185
+
3186
+ // Removes the leading slash if it is not the only remaining segment
3187
+ while (parsed.length > 1 && parsed[0]?.value === '/') {
3188
+ parsed.shift()
3189
+ }
3190
+
3191
+ const scores = parsed.map((segment) => {
3192
+ if (segment.value === '/') {
3193
+ return 0.75
3194
+ }
3195
+
3196
+ if (
3197
+ segment.type === 'param' &&
3198
+ segment.prefixSegment &&
3199
+ segment.suffixSegment
3200
+ ) {
3201
+ return 0.55
3202
+ }
3203
+
3204
+ if (segment.type === 'param' && segment.prefixSegment) {
3205
+ return 0.52
3206
+ }
3207
+
3208
+ if (segment.type === 'param' && segment.suffixSegment) {
3209
+ return 0.51
3210
+ }
3211
+
3212
+ if (segment.type === 'param') {
3213
+ return 0.5
3214
+ }
3215
+
3216
+ if (
3217
+ segment.type === 'wildcard' &&
3218
+ segment.prefixSegment &&
3219
+ segment.suffixSegment
3220
+ ) {
3221
+ return 0.3
3222
+ }
3223
+
3224
+ if (segment.type === 'wildcard' && segment.prefixSegment) {
3225
+ return 0.27
3226
+ }
3227
+
3228
+ if (segment.type === 'wildcard' && segment.suffixSegment) {
3229
+ return 0.26
3230
+ }
3231
+
3232
+ if (segment.type === 'wildcard') {
3233
+ return 0.25
3234
+ }
3235
+
3236
+ return 1
3237
+ })
3238
+
3239
+ scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
3240
+ })
3241
+
3242
+ const flatRoutes = scoredRoutes
3243
+ .sort((a, b) => {
3244
+ const minLength = Math.min(a.scores.length, b.scores.length)
3245
+
3246
+ // Sort by min available score
3247
+ for (let i = 0; i < minLength; i++) {
3248
+ if (a.scores[i] !== b.scores[i]) {
3249
+ return b.scores[i]! - a.scores[i]!
3250
+ }
3251
+ }
3252
+
3253
+ // Sort by length of score
3254
+ if (a.scores.length !== b.scores.length) {
3255
+ return b.scores.length - a.scores.length
3256
+ }
3257
+
3258
+ // Sort by min available parsed value
3259
+ for (let i = 0; i < minLength; i++) {
3260
+ if (a.parsed[i]!.value !== b.parsed[i]!.value) {
3261
+ return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
3262
+ }
3263
+ }
3264
+
3265
+ // Sort by original index
3266
+ return a.index - b.index
3267
+ })
3268
+ .map((d, i) => {
3269
+ d.child.rank = i
3270
+ return d.child
3271
+ })
3272
+
3273
+ return { routesById, routesByPath, flatRoutes }
3274
+ }
3275
+
3276
+ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3277
+ pathname,
3278
+ routePathname,
3279
+ basepath,
3280
+ caseSensitive,
3281
+ routesByPath,
3282
+ routesById,
3283
+ flatRoutes,
3284
+ }: {
3285
+ pathname: string
3286
+ routePathname?: string
3287
+ basepath: string
3288
+ caseSensitive?: boolean
3289
+ routesByPath: Record<string, TRouteLike>
3290
+ routesById: Record<string, TRouteLike>
3291
+ flatRoutes: Array<TRouteLike>
3292
+ }) {
3293
+ let routeParams: Record<string, string> = {}
3294
+ const trimmedPath = trimPathRight(pathname)
3295
+ const getMatchedParams = (route: TRouteLike) => {
3296
+ const result = matchPathname(basepath, trimmedPath, {
3297
+ to: route.fullPath,
3298
+ caseSensitive: route.options?.caseSensitive ?? caseSensitive,
3299
+ fuzzy: true,
3300
+ })
3301
+ return result
3302
+ }
3303
+
3304
+ let foundRoute: TRouteLike | undefined =
3305
+ routePathname !== undefined ? routesByPath[routePathname] : undefined
3306
+ if (foundRoute) {
3307
+ routeParams = getMatchedParams(foundRoute)!
3308
+ } else {
3309
+ foundRoute = flatRoutes.find((route) => {
3310
+ const matchedParams = getMatchedParams(route)
3311
+
3312
+ if (matchedParams) {
3313
+ routeParams = matchedParams
3314
+ return true
3315
+ }
3316
+
3317
+ return false
3318
+ })
3319
+ }
3320
+
3321
+ let routeCursor: TRouteLike = foundRoute || routesById[rootRouteId]!
3322
+
3323
+ const matchedRoutes: Array<TRouteLike> = [routeCursor]
3324
+
3325
+ while (routeCursor.parentRoute) {
3326
+ routeCursor = routeCursor.parentRoute as TRouteLike
3327
+ matchedRoutes.unshift(routeCursor)
3328
+ }
3329
+
3330
+ return { matchedRoutes, routeParams, foundRoute }
3331
+ }