@tanstack/router-core 1.120.5 → 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 +15 -14
  16. package/dist/cjs/router.cjs +231 -155
  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 +15 -14
  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 +234 -158
  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 +104 -35
  49. package/src/router.ts +332 -209
  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)
958
+ this.routesById = routesById as RoutesById<TRouteTree>
959
+ this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
960
+ this.flatRoutes = flatRoutes as Array<AnyRoute>
1028
961
 
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
- }
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
 
@@ -1452,47 +1368,19 @@ export class RouterCore<
1452
1368
  return matches
1453
1369
  }
1454
1370
 
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 }
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
+ })
1496
1384
  }
1497
1385
 
1498
1386
  cancelMatch = (id: string) => {
@@ -1792,11 +1680,17 @@ export class RouterCore<
1792
1680
  }
1793
1681
  }
1794
1682
 
1795
- const nextMatches = this.getMatchedRoutes(next, dest)
1683
+ const nextMatches = this.getMatchedRoutes(
1684
+ next.pathname,
1685
+ dest.to as string,
1686
+ )
1796
1687
  const final = build(dest, nextMatches)
1797
1688
 
1798
1689
  if (maskedNext) {
1799
- const maskedMatches = this.getMatchedRoutes(maskedNext, maskedDest)
1690
+ const maskedMatches = this.getMatchedRoutes(
1691
+ maskedNext.pathname,
1692
+ maskedDest?.to as string,
1693
+ )
1800
1694
  const maskedFinal = build(maskedDest, maskedMatches)
1801
1695
  final.maskedLocation = maskedFinal
1802
1696
  }
@@ -1938,6 +1832,13 @@ export class RouterCore<
1938
1832
  }
1939
1833
 
1940
1834
  navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
1835
+ if (!reloadDocument && href) {
1836
+ try {
1837
+ new URL(`${href}`)
1838
+ reloadDocument = true
1839
+ } catch {}
1840
+ }
1841
+
1941
1842
  if (reloadDocument) {
1942
1843
  if (!href) {
1943
1844
  const location = this.buildLocation({ to, ...rest } as any)
@@ -1960,10 +1861,30 @@ export class RouterCore<
1960
1861
 
1961
1862
  latestLoadPromise: undefined | Promise<void>
1962
1863
 
1963
- load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
1864
+ beforeLoad = () => {
1865
+ // Cancel any pending matches
1866
+ this.cancelMatches()
1964
1867
  this.latestLocation = this.parseLocation(this.latestLocation)
1965
1868
 
1966
- 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
1967
1888
  let notFound: NotFoundError | undefined
1968
1889
 
1969
1890
  let loadPromise: Promise<void>
@@ -1972,36 +1893,10 @@ export class RouterCore<
1972
1893
  loadPromise = new Promise<void>((resolve) => {
1973
1894
  this.startTransition(async () => {
1974
1895
  try {
1896
+ this.beforeLoad()
1975
1897
  const next = this.latestLocation
1976
1898
  const prevLocation = this.state.resolvedLocation
1977
1899
 
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
1900
  if (!this.state.redirect) {
2006
1901
  this.emit({
2007
1902
  type: 'onBeforeNavigate',
@@ -2022,7 +1917,7 @@ export class RouterCore<
2022
1917
 
2023
1918
  await this.loadMatches({
2024
1919
  sync: opts?.sync,
2025
- matches: pendingMatches,
1920
+ matches: this.state.pendingMatches as Array<AnyRouteMatch>,
2026
1921
  location: next,
2027
1922
  // eslint-disable-next-line @typescript-eslint/require-await
2028
1923
  onReady: async () => {
@@ -2083,11 +1978,11 @@ export class RouterCore<
2083
1978
  },
2084
1979
  })
2085
1980
  } catch (err) {
2086
- if (isResolvedRedirect(err)) {
1981
+ if (isRedirect(err)) {
2087
1982
  redirect = err
2088
1983
  if (!this.isServer) {
2089
1984
  this.navigate({
2090
- ...redirect,
1985
+ ...redirect.options,
2091
1986
  replace: true,
2092
1987
  ignoreBlocker: true,
2093
1988
  })
@@ -2099,7 +1994,7 @@ export class RouterCore<
2099
1994
  this.__store.setState((s) => ({
2100
1995
  ...s,
2101
1996
  statusCode: redirect
2102
- ? redirect.statusCode
1997
+ ? redirect.status
2103
1998
  : notFound
2104
1999
  ? 404
2105
2000
  : s.matches.some((d) => d.status === 'error')
@@ -2255,13 +2150,15 @@ export class RouterCore<
2255
2150
  }
2256
2151
 
2257
2152
  const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
2258
- if (isResolvedRedirect(err)) {
2259
- if (!err.reloadDocument) {
2260
- 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
+ }
2261
2160
  }
2262
- }
2263
2161
 
2264
- if (isRedirect(err) || isNotFound(err)) {
2265
2162
  updateMatch(match.id, (prev) => ({
2266
2163
  ...prev,
2267
2164
  status: isRedirect(err)
@@ -2285,7 +2182,9 @@ export class RouterCore<
2285
2182
 
2286
2183
  if (isRedirect(err)) {
2287
2184
  rendered = true
2288
- err = this.resolveRedirect({ ...err, _fromLocation: location })
2185
+ err.options._fromLocation = location
2186
+ err.redirectHandled = true
2187
+ err = this.resolveRedirect(err)
2289
2188
  throw err
2290
2189
  } else if (isNotFound(err)) {
2291
2190
  this._handleNotFound(matches, err, {
@@ -2738,8 +2637,8 @@ export class RouterCore<
2738
2637
  loaderPromise: undefined,
2739
2638
  }))
2740
2639
  } catch (err) {
2741
- if (isResolvedRedirect(err)) {
2742
- await this.navigate(err)
2640
+ if (isRedirect(err)) {
2641
+ await this.navigate(err.options)
2743
2642
  }
2744
2643
  }
2745
2644
  })()
@@ -2829,11 +2728,14 @@ export class RouterCore<
2829
2728
  return this.load({ sync: opts?.sync })
2830
2729
  }
2831
2730
 
2832
- resolveRedirect = (err: AnyRedirect): ResolvedRedirect => {
2833
- 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
+ }
2834
2736
 
2835
- if (!redirect.href) {
2836
- redirect.href = this.buildLocation(redirect as any).href
2737
+ if (!redirect.headers.get('Location')) {
2738
+ redirect.headers.set('Location', redirect.options.href)
2837
2739
  }
2838
2740
 
2839
2741
  return redirect
@@ -2968,11 +2870,11 @@ export class RouterCore<
2968
2870
  return matches
2969
2871
  } catch (err) {
2970
2872
  if (isRedirect(err)) {
2971
- if (err.reloadDocument) {
2873
+ if (err.options.reloadDocument) {
2972
2874
  return undefined
2973
2875
  }
2974
2876
  return await this.preloadRoute({
2975
- ...(err as any),
2877
+ ...err.options,
2976
2878
  _fromLocation: next,
2977
2879
  })
2978
2880
  }
@@ -3206,3 +3108,224 @@ function routeNeedsPreload(route: AnyRoute) {
3206
3108
  }
3207
3109
  return false
3208
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
+ }