@tanstack/router-core 1.120.4-alpha.19 → 1.120.5

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 (58) hide show
  1. package/dist/cjs/fileRoute.d.cts +2 -6
  2. package/dist/cjs/index.cjs +0 -3
  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 +1 -18
  7. package/dist/cjs/path.cjs +16 -130
  8. package/dist/cjs/path.cjs.map +1 -1
  9. package/dist/cjs/path.d.cts +0 -17
  10. package/dist/cjs/redirect.cjs +14 -17
  11. package/dist/cjs/redirect.cjs.map +1 -1
  12. package/dist/cjs/redirect.d.cts +7 -13
  13. package/dist/cjs/route.cjs +1 -12
  14. package/dist/cjs/route.cjs.map +1 -1
  15. package/dist/cjs/route.d.cts +16 -19
  16. package/dist/cjs/router.cjs +214 -287
  17. package/dist/cjs/router.cjs.map +1 -1
  18. package/dist/cjs/router.d.cts +3 -46
  19. package/dist/cjs/scroll-restoration.cjs +23 -12
  20. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  21. package/dist/cjs/scroll-restoration.d.cts +1 -1
  22. package/dist/cjs/typePrimitives.d.cts +2 -2
  23. package/dist/cjs/utils.cjs.map +1 -1
  24. package/dist/cjs/utils.d.cts +0 -2
  25. package/dist/esm/fileRoute.d.ts +2 -6
  26. package/dist/esm/index.d.ts +6 -6
  27. package/dist/esm/index.js +2 -5
  28. package/dist/esm/link.d.ts +1 -18
  29. package/dist/esm/link.js.map +1 -1
  30. package/dist/esm/path.d.ts +0 -17
  31. package/dist/esm/path.js +16 -130
  32. package/dist/esm/path.js.map +1 -1
  33. package/dist/esm/redirect.d.ts +7 -13
  34. package/dist/esm/redirect.js +14 -17
  35. package/dist/esm/redirect.js.map +1 -1
  36. package/dist/esm/route.d.ts +16 -19
  37. package/dist/esm/route.js +1 -12
  38. package/dist/esm/route.js.map +1 -1
  39. package/dist/esm/router.d.ts +3 -46
  40. package/dist/esm/router.js +217 -290
  41. package/dist/esm/router.js.map +1 -1
  42. package/dist/esm/scroll-restoration.d.ts +1 -1
  43. package/dist/esm/scroll-restoration.js +23 -12
  44. package/dist/esm/scroll-restoration.js.map +1 -1
  45. package/dist/esm/typePrimitives.d.ts +2 -2
  46. package/dist/esm/utils.d.ts +0 -2
  47. package/dist/esm/utils.js.map +1 -1
  48. package/package.json +2 -2
  49. package/src/fileRoute.ts +1 -90
  50. package/src/index.ts +6 -14
  51. package/src/link.ts +11 -97
  52. package/src/path.ts +16 -181
  53. package/src/redirect.ts +22 -37
  54. package/src/route.ts +50 -108
  55. package/src/router.ts +270 -392
  56. package/src/scroll-restoration.ts +44 -27
  57. package/src/typePrimitives.ts +2 -2
  58. package/src/utils.ts +0 -14
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 } from './redirect'
31
+ import { isRedirect, isResolvedRedirect } from './redirect'
32
32
  import type { SearchParser, SearchSerializer } from './searchParams'
33
33
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
34
34
  import type {
@@ -165,14 +165,6 @@ 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
176
168
  /**
177
169
  * The default `pendingMs` a route should use if no pendingMs is provided.
178
170
  *
@@ -415,7 +407,7 @@ export interface RouterState<
415
407
  location: ParsedLocation<FullSearchSchema<TRouteTree>>
416
408
  resolvedLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
417
409
  statusCode: number
418
- redirect?: AnyRedirect
410
+ redirect?: ResolvedRedirect
419
411
  }
420
412
 
421
413
  export interface BuildNextOptions {
@@ -601,8 +593,8 @@ export type ParseLocationFn<TRouteTree extends AnyRoute> = (
601
593
  ) => ParsedLocation<FullSearchSchema<TRouteTree>>
602
594
 
603
595
  export type GetMatchRoutesFn = (
604
- pathname: string,
605
- routePathname: string | undefined,
596
+ next: ParsedLocation,
597
+ dest?: BuildNextOptions,
606
598
  ) => {
607
599
  matchedRoutes: Array<AnyRoute>
608
600
  routeParams: Record<string, string>
@@ -844,8 +836,6 @@ export class RouterCore<
844
836
  // router can be used in a non-react environment if necessary
845
837
  startTransition: StartTransitionFn = (fn) => fn()
846
838
 
847
- isShell = false
848
-
849
839
  update: UpdateFn<
850
840
  TRouteTree,
851
841
  TTrailingSlashOption,
@@ -892,6 +882,7 @@ export class RouterCore<
892
882
  }
893
883
 
894
884
  if (
885
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
895
886
  !this.history ||
896
887
  (this.options.history && this.options.history !== this.history)
897
888
  ) {
@@ -910,6 +901,7 @@ export class RouterCore<
910
901
  this.buildRouteTree()
911
902
  }
912
903
 
904
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
913
905
  if (!this.__store) {
914
906
  this.__store = new Store(getInitialRouterState(this.latestLocation), {
915
907
  onUpdate: () => {
@@ -928,16 +920,13 @@ export class RouterCore<
928
920
  if (
929
921
  typeof window !== 'undefined' &&
930
922
  'CSS' in window &&
923
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
931
924
  typeof window.CSS?.supports === 'function'
932
925
  ) {
933
926
  this.isViewTransitionTypesSupported = window.CSS.supports(
934
927
  'selector(:active-view-transition-type(a)',
935
928
  )
936
929
  }
937
-
938
- if ((this.latestLocation.search as any).__TSS_SHELL) {
939
- this.isShell = true
940
- }
941
930
  }
942
931
 
943
932
  get state() {
@@ -945,29 +934,124 @@ export class RouterCore<
945
934
  }
946
935
 
947
936
  buildRouteTree = () => {
948
- const { routesById, routesByPath, flatRoutes } = processRouteTree({
949
- routeTree: this.routeTree,
950
- initRoute: (route, i) => {
951
- route.init({
952
- originalIndex: i,
953
- defaultSsr: this.options.defaultSsr,
954
- })
955
- },
956
- })
957
-
958
- this.routesById = routesById as RoutesById<TRouteTree>
959
- this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
960
- this.flatRoutes = flatRoutes as Array<AnyRoute>
937
+ this.routesById = {} as RoutesById<TRouteTree>
938
+ this.routesByPath = {} as RoutesByPath<TRouteTree>
961
939
 
962
940
  const notFoundRoute = this.options.notFoundRoute
963
-
964
941
  if (notFoundRoute) {
965
942
  notFoundRoute.init({
966
943
  originalIndex: 99999999999,
967
944
  defaultSsr: this.options.defaultSsr,
968
945
  })
969
- this.routesById[notFoundRoute.id] = notFoundRoute
946
+ ;(this.routesById as any)[notFoundRoute.id] = notFoundRoute
947
+ }
948
+
949
+ const recurseRoutes = (childRoutes: Array<AnyRoute>) => {
950
+ childRoutes.forEach((childRoute, i) => {
951
+ childRoute.init({
952
+ originalIndex: i,
953
+ defaultSsr: this.options.defaultSsr,
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
+ })
970
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 })
1023
+ })
1024
+
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
+ }
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
+ }
1047
+
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
1054
+ })
971
1055
  }
972
1056
 
973
1057
  subscribe: SubscribeFn = (eventType, fn) => {
@@ -1081,8 +1165,8 @@ export class RouterCore<
1081
1165
  opts?: MatchRoutesOpts,
1082
1166
  ): Array<AnyRouteMatch> {
1083
1167
  const { foundRoute, matchedRoutes, routeParams } = this.getMatchedRoutes(
1084
- next.pathname,
1085
- opts?.dest?.to as string,
1168
+ next,
1169
+ opts?.dest,
1086
1170
  )
1087
1171
  let isGlobalNotFound = false
1088
1172
 
@@ -1363,44 +1447,52 @@ export class RouterCore<
1363
1447
  ...match.__beforeLoadContext,
1364
1448
  }
1365
1449
  }
1366
-
1367
- // If it's already a success, update headers and head content
1368
- // These may get updated again if the match is refreshed
1369
- // due to being stale
1370
- if (match.status === 'success') {
1371
- match.headers = route.options.headers?.({
1372
- loaderData: match.loaderData,
1373
- })
1374
- const assetContext = {
1375
- matches,
1376
- match,
1377
- params: match.params,
1378
- loaderData: match.loaderData,
1379
- }
1380
- const headFnContent = route.options.head?.(assetContext)
1381
- match.links = headFnContent?.links
1382
- match.headScripts = headFnContent?.scripts
1383
- match.meta = headFnContent?.meta
1384
- match.scripts = route.options.scripts?.(assetContext)
1385
- }
1386
1450
  })
1387
1451
 
1388
1452
  return matches
1389
1453
  }
1390
1454
 
1391
- getMatchedRoutes: GetMatchRoutesFn = (
1392
- pathname: string,
1393
- routePathname: string | undefined,
1394
- ) => {
1395
- return getMatchedRoutes({
1396
- pathname,
1397
- routePathname,
1398
- basepath: this.basepath,
1399
- caseSensitive: this.options.caseSensitive,
1400
- routesByPath: this.routesByPath,
1401
- routesById: this.routesById,
1402
- flatRoutes: this.flatRoutes,
1403
- })
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 }
1404
1496
  }
1405
1497
 
1406
1498
  cancelMatch = (id: string) => {
@@ -1700,17 +1792,11 @@ export class RouterCore<
1700
1792
  }
1701
1793
  }
1702
1794
 
1703
- const nextMatches = this.getMatchedRoutes(
1704
- next.pathname,
1705
- dest.to as string,
1706
- )
1795
+ const nextMatches = this.getMatchedRoutes(next, dest)
1707
1796
  const final = build(dest, nextMatches)
1708
1797
 
1709
1798
  if (maskedNext) {
1710
- const maskedMatches = this.getMatchedRoutes(
1711
- maskedNext.pathname,
1712
- maskedDest?.to as string,
1713
- )
1799
+ const maskedMatches = this.getMatchedRoutes(maskedNext, maskedDest)
1714
1800
  const maskedFinal = build(maskedDest, maskedMatches)
1715
1801
  final.maskedLocation = maskedFinal
1716
1802
  }
@@ -1852,13 +1938,6 @@ export class RouterCore<
1852
1938
  }
1853
1939
 
1854
1940
  navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
1855
- if (!reloadDocument && href) {
1856
- try {
1857
- new URL(`${href}`)
1858
- reloadDocument = true
1859
- } catch {}
1860
- }
1861
-
1862
1941
  if (reloadDocument) {
1863
1942
  if (!href) {
1864
1943
  const location = this.buildLocation({ to, ...rest } as any)
@@ -1881,30 +1960,10 @@ export class RouterCore<
1881
1960
 
1882
1961
  latestLoadPromise: undefined | Promise<void>
1883
1962
 
1884
- beforeLoad = () => {
1885
- // Cancel any pending matches
1886
- this.cancelMatches()
1963
+ load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
1887
1964
  this.latestLocation = this.parseLocation(this.latestLocation)
1888
1965
 
1889
- // Match the routes
1890
- const pendingMatches = this.matchRoutes(this.latestLocation)
1891
-
1892
- // Ingest the new matches
1893
- this.__store.setState((s) => ({
1894
- ...s,
1895
- status: 'pending',
1896
- isLoading: true,
1897
- location: this.latestLocation,
1898
- pendingMatches,
1899
- // If a cached moved to pendingMatches, remove it from cachedMatches
1900
- cachedMatches: s.cachedMatches.filter((d) => {
1901
- return !pendingMatches.find((e) => e.id === d.id)
1902
- }),
1903
- }))
1904
- }
1905
-
1906
- load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
1907
- let redirect: AnyRedirect | undefined
1966
+ let redirect: ResolvedRedirect | undefined
1908
1967
  let notFound: NotFoundError | undefined
1909
1968
 
1910
1969
  let loadPromise: Promise<void>
@@ -1913,10 +1972,36 @@ export class RouterCore<
1913
1972
  loadPromise = new Promise<void>((resolve) => {
1914
1973
  this.startTransition(async () => {
1915
1974
  try {
1916
- this.beforeLoad()
1917
1975
  const next = this.latestLocation
1918
1976
  const prevLocation = this.state.resolvedLocation
1919
1977
 
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
+
1920
2005
  if (!this.state.redirect) {
1921
2006
  this.emit({
1922
2007
  type: 'onBeforeNavigate',
@@ -1937,7 +2022,7 @@ export class RouterCore<
1937
2022
 
1938
2023
  await this.loadMatches({
1939
2024
  sync: opts?.sync,
1940
- matches: this.state.pendingMatches as Array<AnyRouteMatch>,
2025
+ matches: pendingMatches,
1941
2026
  location: next,
1942
2027
  // eslint-disable-next-line @typescript-eslint/require-await
1943
2028
  onReady: async () => {
@@ -1998,11 +2083,11 @@ export class RouterCore<
1998
2083
  },
1999
2084
  })
2000
2085
  } catch (err) {
2001
- if (isRedirect(err)) {
2086
+ if (isResolvedRedirect(err)) {
2002
2087
  redirect = err
2003
2088
  if (!this.isServer) {
2004
2089
  this.navigate({
2005
- ...redirect.options,
2090
+ ...redirect,
2006
2091
  replace: true,
2007
2092
  ignoreBlocker: true,
2008
2093
  })
@@ -2014,7 +2099,7 @@ export class RouterCore<
2014
2099
  this.__store.setState((s) => ({
2015
2100
  ...s,
2016
2101
  statusCode: redirect
2017
- ? redirect.status
2102
+ ? redirect.statusCode
2018
2103
  : notFound
2019
2104
  ? 404
2020
2105
  : s.matches.some((d) => d.status === 'error')
@@ -2170,15 +2255,13 @@ export class RouterCore<
2170
2255
  }
2171
2256
 
2172
2257
  const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
2173
- if (isRedirect(err) || isNotFound(err)) {
2174
- if (isRedirect(err)) {
2175
- if (err.redirectHandled) {
2176
- if (!err.options.reloadDocument) {
2177
- throw err
2178
- }
2179
- }
2258
+ if (isResolvedRedirect(err)) {
2259
+ if (!err.reloadDocument) {
2260
+ throw err
2180
2261
  }
2262
+ }
2181
2263
 
2264
+ if (isRedirect(err) || isNotFound(err)) {
2182
2265
  updateMatch(match.id, (prev) => ({
2183
2266
  ...prev,
2184
2267
  status: isRedirect(err)
@@ -2202,9 +2285,7 @@ export class RouterCore<
2202
2285
 
2203
2286
  if (isRedirect(err)) {
2204
2287
  rendered = true
2205
- err.options._fromLocation = location
2206
- err.redirectHandled = true
2207
- err = this.resolveRedirect(err)
2288
+ err = this.resolveRedirect({ ...err, _fromLocation: location })
2208
2289
  throw err
2209
2290
  } else if (isNotFound(err)) {
2210
2291
  this._handleNotFound(matches, err, {
@@ -2508,6 +2589,35 @@ export class RouterCore<
2508
2589
  !this.state.matches.find((d) => d.id === matchId),
2509
2590
  }))
2510
2591
 
2592
+ const executeHead = () => {
2593
+ const match = this.getMatch(matchId)
2594
+ // in case of a redirecting match during preload, the match does not exist
2595
+ if (!match) {
2596
+ return
2597
+ }
2598
+ const assetContext = {
2599
+ matches,
2600
+ match,
2601
+ params: match.params,
2602
+ loaderData: match.loaderData,
2603
+ }
2604
+ const headFnContent = route.options.head?.(assetContext)
2605
+ const meta = headFnContent?.meta
2606
+ const links = headFnContent?.links
2607
+ const headScripts = headFnContent?.scripts
2608
+
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
+ }))
2619
+ }
2620
+
2511
2621
  const runLoader = async () => {
2512
2622
  try {
2513
2623
  // If the Matches component rendered
@@ -2548,40 +2658,21 @@ export class RouterCore<
2548
2658
 
2549
2659
  await potentialPendingMinPromise()
2550
2660
 
2551
- const assetContext = {
2552
- matches,
2553
- match: this.getMatch(matchId)!,
2554
- params: this.getMatch(matchId)!.params,
2555
- loaderData,
2556
- }
2557
- const headFnContent =
2558
- route.options.head?.(assetContext)
2559
- const meta = headFnContent?.meta
2560
- const links = headFnContent?.links
2561
- const headScripts = headFnContent?.scripts
2562
-
2563
- const scripts = route.options.scripts?.(assetContext)
2564
- const headers = route.options.headers?.({
2565
- loaderData,
2566
- })
2567
-
2568
2661
  // Last but not least, wait for the the components
2569
2662
  // to be preloaded before we resolve the match
2570
2663
  await route._componentsPromise
2571
2664
 
2572
- updateMatch(matchId, (prev) => ({
2573
- ...prev,
2574
- error: undefined,
2575
- status: 'success',
2576
- isFetching: false,
2577
- updatedAt: Date.now(),
2578
- loaderData,
2579
- meta,
2580
- links,
2581
- headScripts,
2582
- headers,
2583
- scripts,
2584
- }))
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
+ })
2585
2676
  } catch (e) {
2586
2677
  let error = e
2587
2678
 
@@ -2599,12 +2690,15 @@ export class RouterCore<
2599
2690
  )
2600
2691
  }
2601
2692
 
2602
- updateMatch(matchId, (prev) => ({
2603
- ...prev,
2604
- error,
2605
- status: 'error',
2606
- isFetching: false,
2607
- }))
2693
+ batch(() => {
2694
+ updateMatch(matchId, (prev) => ({
2695
+ ...prev,
2696
+ error,
2697
+ status: 'error',
2698
+ isFetching: false,
2699
+ }))
2700
+ executeHead()
2701
+ })
2608
2702
  }
2609
2703
 
2610
2704
  this.serverSsr?.onMatchSettled({
@@ -2612,10 +2706,13 @@ export class RouterCore<
2612
2706
  match: this.getMatch(matchId)!,
2613
2707
  })
2614
2708
  } catch (err) {
2615
- updateMatch(matchId, (prev) => ({
2616
- ...prev,
2617
- loaderPromise: undefined,
2618
- }))
2709
+ batch(() => {
2710
+ updateMatch(matchId, (prev) => ({
2711
+ ...prev,
2712
+ loaderPromise: undefined,
2713
+ }))
2714
+ executeHead()
2715
+ })
2619
2716
  handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2620
2717
  }
2621
2718
  }
@@ -2641,8 +2738,8 @@ export class RouterCore<
2641
2738
  loaderPromise: undefined,
2642
2739
  }))
2643
2740
  } catch (err) {
2644
- if (isRedirect(err)) {
2645
- await this.navigate(err.options)
2741
+ if (isResolvedRedirect(err)) {
2742
+ await this.navigate(err)
2646
2743
  }
2647
2744
  }
2648
2745
  })()
@@ -2651,6 +2748,11 @@ export class RouterCore<
2651
2748
  (loaderShouldRunAsync && sync)
2652
2749
  ) {
2653
2750
  await runLoader()
2751
+ } else {
2752
+ // if the loader did not run, still update head.
2753
+ // reason: parent's beforeLoad may have changed the route context
2754
+ // and only now do we know the route context (and that the loader would not run)
2755
+ executeHead()
2654
2756
  }
2655
2757
  }
2656
2758
  if (!loaderIsRunningAsync) {
@@ -2727,14 +2829,11 @@ export class RouterCore<
2727
2829
  return this.load({ sync: opts?.sync })
2728
2830
  }
2729
2831
 
2730
- resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
2731
- if (!redirect.options.href) {
2732
- redirect.options.href = this.buildLocation(redirect.options).href
2733
- redirect.headers.set('Location', redirect.options.href)
2734
- }
2832
+ resolveRedirect = (err: AnyRedirect): ResolvedRedirect => {
2833
+ const redirect = err as ResolvedRedirect
2735
2834
 
2736
- if (!redirect.headers.get('Location')) {
2737
- redirect.headers.set('Location', redirect.options.href)
2835
+ if (!redirect.href) {
2836
+ redirect.href = this.buildLocation(redirect as any).href
2738
2837
  }
2739
2838
 
2740
2839
  return redirect
@@ -2869,11 +2968,11 @@ export class RouterCore<
2869
2968
  return matches
2870
2969
  } catch (err) {
2871
2970
  if (isRedirect(err)) {
2872
- if (err.options.reloadDocument) {
2971
+ if (err.reloadDocument) {
2873
2972
  return undefined
2874
2973
  }
2875
2974
  return await this.preloadRoute({
2876
- ...err.options,
2975
+ ...(err as any),
2877
2976
  _fromLocation: next,
2878
2977
  })
2879
2978
  }
@@ -3107,224 +3206,3 @@ function routeNeedsPreload(route: AnyRoute) {
3107
3206
  }
3108
3207
  return false
3109
3208
  }
3110
-
3111
- interface RouteLike {
3112
- id: string
3113
- isRoot?: boolean
3114
- path?: string
3115
- fullPath: string
3116
- rank?: number
3117
- parentRoute?: RouteLike
3118
- children?: Array<RouteLike>
3119
- options?: {
3120
- caseSensitive?: boolean
3121
- }
3122
- }
3123
-
3124
- export function processRouteTree<TRouteLike extends RouteLike>({
3125
- routeTree,
3126
- initRoute,
3127
- }: {
3128
- routeTree: TRouteLike
3129
- initRoute?: (route: TRouteLike, index: number) => void
3130
- }) {
3131
- const routesById = {} as Record<string, TRouteLike>
3132
- const routesByPath = {} as Record<string, TRouteLike>
3133
-
3134
- const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
3135
- childRoutes.forEach((childRoute, i) => {
3136
- initRoute?.(childRoute, i)
3137
-
3138
- const existingRoute = routesById[childRoute.id]
3139
-
3140
- invariant(
3141
- !existingRoute,
3142
- `Duplicate routes found with id: ${String(childRoute.id)}`,
3143
- )
3144
-
3145
- routesById[childRoute.id] = childRoute
3146
-
3147
- if (!childRoute.isRoot && childRoute.path) {
3148
- const trimmedFullPath = trimPathRight(childRoute.fullPath)
3149
- if (
3150
- !routesByPath[trimmedFullPath] ||
3151
- childRoute.fullPath.endsWith('/')
3152
- ) {
3153
- routesByPath[trimmedFullPath] = childRoute
3154
- }
3155
- }
3156
-
3157
- const children = childRoute.children as Array<TRouteLike>
3158
-
3159
- if (children?.length) {
3160
- recurseRoutes(children)
3161
- }
3162
- })
3163
- }
3164
-
3165
- recurseRoutes([routeTree])
3166
-
3167
- const scoredRoutes: Array<{
3168
- child: TRouteLike
3169
- trimmed: string
3170
- parsed: ReturnType<typeof parsePathname>
3171
- index: number
3172
- scores: Array<number>
3173
- }> = []
3174
-
3175
- const routes: Array<TRouteLike> = Object.values(routesById)
3176
-
3177
- routes.forEach((d, i) => {
3178
- if (d.isRoot || !d.path) {
3179
- return
3180
- }
3181
-
3182
- const trimmed = trimPathLeft(d.fullPath)
3183
- const parsed = parsePathname(trimmed)
3184
-
3185
- // Removes the leading slash if it is not the only remaining segment
3186
- while (parsed.length > 1 && parsed[0]?.value === '/') {
3187
- parsed.shift()
3188
- }
3189
-
3190
- const scores = parsed.map((segment) => {
3191
- if (segment.value === '/') {
3192
- return 0.75
3193
- }
3194
-
3195
- if (
3196
- segment.type === 'param' &&
3197
- segment.prefixSegment &&
3198
- segment.suffixSegment
3199
- ) {
3200
- return 0.55
3201
- }
3202
-
3203
- if (segment.type === 'param' && segment.prefixSegment) {
3204
- return 0.52
3205
- }
3206
-
3207
- if (segment.type === 'param' && segment.suffixSegment) {
3208
- return 0.51
3209
- }
3210
-
3211
- if (segment.type === 'param') {
3212
- return 0.5
3213
- }
3214
-
3215
- if (
3216
- segment.type === 'wildcard' &&
3217
- segment.prefixSegment &&
3218
- segment.suffixSegment
3219
- ) {
3220
- return 0.3
3221
- }
3222
-
3223
- if (segment.type === 'wildcard' && segment.prefixSegment) {
3224
- return 0.27
3225
- }
3226
-
3227
- if (segment.type === 'wildcard' && segment.suffixSegment) {
3228
- return 0.26
3229
- }
3230
-
3231
- if (segment.type === 'wildcard') {
3232
- return 0.25
3233
- }
3234
-
3235
- return 1
3236
- })
3237
-
3238
- scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
3239
- })
3240
-
3241
- const flatRoutes = scoredRoutes
3242
- .sort((a, b) => {
3243
- const minLength = Math.min(a.scores.length, b.scores.length)
3244
-
3245
- // Sort by min available score
3246
- for (let i = 0; i < minLength; i++) {
3247
- if (a.scores[i] !== b.scores[i]) {
3248
- return b.scores[i]! - a.scores[i]!
3249
- }
3250
- }
3251
-
3252
- // Sort by length of score
3253
- if (a.scores.length !== b.scores.length) {
3254
- return b.scores.length - a.scores.length
3255
- }
3256
-
3257
- // Sort by min available parsed value
3258
- for (let i = 0; i < minLength; i++) {
3259
- if (a.parsed[i]!.value !== b.parsed[i]!.value) {
3260
- return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
3261
- }
3262
- }
3263
-
3264
- // Sort by original index
3265
- return a.index - b.index
3266
- })
3267
- .map((d, i) => {
3268
- d.child.rank = i
3269
- return d.child
3270
- })
3271
-
3272
- return { routesById, routesByPath, flatRoutes }
3273
- }
3274
-
3275
- export function getMatchedRoutes<TRouteLike extends RouteLike>({
3276
- pathname,
3277
- routePathname,
3278
- basepath,
3279
- caseSensitive,
3280
- routesByPath,
3281
- routesById,
3282
- flatRoutes,
3283
- }: {
3284
- pathname: string
3285
- routePathname?: string
3286
- basepath: string
3287
- caseSensitive?: boolean
3288
- routesByPath: Record<string, TRouteLike>
3289
- routesById: Record<string, TRouteLike>
3290
- flatRoutes: Array<TRouteLike>
3291
- }) {
3292
- let routeParams: Record<string, string> = {}
3293
- const trimmedPath = trimPathRight(pathname)
3294
- const getMatchedParams = (route: TRouteLike) => {
3295
- const result = matchPathname(basepath, trimmedPath, {
3296
- to: route.fullPath,
3297
- caseSensitive: route.options?.caseSensitive ?? caseSensitive,
3298
- fuzzy: true,
3299
- })
3300
- return result
3301
- }
3302
-
3303
- let foundRoute: TRouteLike | undefined =
3304
- routePathname !== undefined ? routesByPath[routePathname] : undefined
3305
- if (foundRoute) {
3306
- routeParams = getMatchedParams(foundRoute)!
3307
- } else {
3308
- foundRoute = flatRoutes.find((route) => {
3309
- const matchedParams = getMatchedParams(route)
3310
-
3311
- if (matchedParams) {
3312
- routeParams = matchedParams
3313
- return true
3314
- }
3315
-
3316
- return false
3317
- })
3318
- }
3319
-
3320
- let routeCursor: TRouteLike = foundRoute || routesById[rootRouteId]!
3321
-
3322
- const matchedRoutes: Array<TRouteLike> = [routeCursor]
3323
-
3324
- while (routeCursor.parentRoute) {
3325
- routeCursor = routeCursor.parentRoute as TRouteLike
3326
- matchedRoutes.unshift(routeCursor)
3327
- }
3328
-
3329
- return { matchedRoutes, routeParams, foundRoute }
3330
- }