@tanstack/router-core 0.0.1-beta.9 → 1.20.3-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 (194) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/cjs/Matches.cjs +13 -0
  4. package/dist/cjs/Matches.cjs.map +1 -0
  5. package/dist/cjs/Matches.d.cts +109 -0
  6. package/dist/cjs/RouterProvider.d.cts +26 -0
  7. package/dist/cjs/defer.cjs +25 -0
  8. package/dist/cjs/defer.cjs.map +1 -0
  9. package/dist/cjs/defer.d.cts +20 -0
  10. package/dist/cjs/fileRoute.d.cts +23 -0
  11. package/dist/cjs/history.d.cts +8 -0
  12. package/dist/cjs/index.cjs +80 -0
  13. package/dist/cjs/index.cjs.map +1 -0
  14. package/dist/cjs/index.d.cts +41 -0
  15. package/dist/cjs/link.cjs +5 -0
  16. package/dist/cjs/link.cjs.map +1 -0
  17. package/dist/cjs/link.d.cts +200 -0
  18. package/dist/cjs/location.d.cts +12 -0
  19. package/dist/cjs/manifest.d.cts +24 -0
  20. package/dist/cjs/not-found.cjs +13 -0
  21. package/dist/cjs/not-found.cjs.map +1 -0
  22. package/dist/cjs/not-found.d.cts +20 -0
  23. package/dist/cjs/path.cjs +412 -0
  24. package/dist/cjs/path.cjs.map +1 -0
  25. package/dist/cjs/path.d.cts +56 -0
  26. package/dist/cjs/qss.cjs +38 -0
  27. package/dist/cjs/qss.cjs.map +1 -0
  28. package/dist/cjs/qss.d.cts +22 -0
  29. package/dist/cjs/redirect.cjs +34 -0
  30. package/dist/cjs/redirect.cjs.map +1 -0
  31. package/dist/cjs/redirect.d.cts +38 -0
  32. package/dist/cjs/root.cjs +5 -0
  33. package/dist/cjs/root.cjs.map +1 -0
  34. package/dist/cjs/root.d.cts +2 -0
  35. package/dist/cjs/route.cjs +119 -0
  36. package/dist/cjs/route.cjs.map +1 -0
  37. package/dist/cjs/route.d.cts +422 -0
  38. package/dist/cjs/routeInfo.d.cts +54 -0
  39. package/dist/cjs/router.cjs +1800 -0
  40. package/dist/cjs/router.cjs.map +1 -0
  41. package/dist/cjs/router.d.cts +630 -0
  42. package/dist/cjs/scroll-restoration.cjs +196 -0
  43. package/dist/cjs/scroll-restoration.cjs.map +1 -0
  44. package/dist/cjs/scroll-restoration.d.cts +38 -0
  45. package/dist/cjs/searchMiddleware.cjs +42 -0
  46. package/dist/cjs/searchMiddleware.cjs.map +1 -0
  47. package/dist/cjs/searchMiddleware.d.cts +5 -0
  48. package/dist/cjs/searchParams.cjs +61 -0
  49. package/dist/cjs/searchParams.cjs.map +1 -0
  50. package/dist/cjs/searchParams.d.cts +7 -0
  51. package/dist/cjs/serializer.d.cts +22 -0
  52. package/dist/cjs/structuralSharing.d.cts +4 -0
  53. package/dist/cjs/typePrimitives.d.cts +65 -0
  54. package/dist/cjs/useLoaderData.d.cts +5 -0
  55. package/dist/cjs/useLoaderDeps.d.cts +5 -0
  56. package/dist/cjs/useNavigate.d.cts +3 -0
  57. package/dist/cjs/useParams.d.cts +5 -0
  58. package/dist/cjs/useRouteContext.d.cts +9 -0
  59. package/dist/cjs/useSearch.d.cts +5 -0
  60. package/dist/cjs/utils.cjs +160 -0
  61. package/dist/cjs/utils.cjs.map +1 -0
  62. package/dist/cjs/utils.d.cts +105 -0
  63. package/dist/cjs/validators.d.cts +51 -0
  64. package/dist/esm/Matches.d.ts +109 -0
  65. package/dist/esm/Matches.js +13 -0
  66. package/dist/esm/Matches.js.map +1 -0
  67. package/dist/esm/RouterProvider.d.ts +26 -0
  68. package/dist/esm/defer.d.ts +20 -0
  69. package/dist/esm/defer.js +25 -0
  70. package/dist/esm/defer.js.map +1 -0
  71. package/dist/esm/fileRoute.d.ts +23 -0
  72. package/dist/esm/history.d.ts +8 -0
  73. package/dist/esm/index.d.ts +41 -0
  74. package/dist/esm/index.js +80 -0
  75. package/dist/esm/index.js.map +1 -0
  76. package/dist/esm/link.d.ts +200 -0
  77. package/dist/esm/link.js +5 -0
  78. package/dist/esm/link.js.map +1 -0
  79. package/dist/esm/location.d.ts +12 -0
  80. package/dist/esm/manifest.d.ts +24 -0
  81. package/dist/esm/not-found.d.ts +20 -0
  82. package/dist/esm/not-found.js +13 -0
  83. package/dist/esm/not-found.js.map +1 -0
  84. package/dist/esm/path.d.ts +56 -0
  85. package/dist/esm/path.js +412 -0
  86. package/dist/esm/path.js.map +1 -0
  87. package/dist/esm/qss.d.ts +22 -0
  88. package/dist/esm/qss.js +38 -0
  89. package/dist/esm/qss.js.map +1 -0
  90. package/dist/esm/redirect.d.ts +38 -0
  91. package/dist/esm/redirect.js +34 -0
  92. package/dist/esm/redirect.js.map +1 -0
  93. package/dist/esm/root.d.ts +2 -0
  94. package/dist/esm/root.js +5 -0
  95. package/dist/esm/root.js.map +1 -0
  96. package/dist/esm/route.d.ts +422 -0
  97. package/dist/esm/route.js +119 -0
  98. package/dist/esm/route.js.map +1 -0
  99. package/dist/esm/routeInfo.d.ts +54 -0
  100. package/dist/esm/router.d.ts +630 -0
  101. package/dist/esm/router.js +1800 -0
  102. package/dist/esm/router.js.map +1 -0
  103. package/dist/esm/scroll-restoration.d.ts +38 -0
  104. package/dist/esm/scroll-restoration.js +196 -0
  105. package/dist/esm/scroll-restoration.js.map +1 -0
  106. package/dist/esm/searchMiddleware.d.ts +5 -0
  107. package/dist/esm/searchMiddleware.js +42 -0
  108. package/dist/esm/searchMiddleware.js.map +1 -0
  109. package/dist/esm/searchParams.d.ts +7 -0
  110. package/dist/esm/searchParams.js +61 -0
  111. package/dist/esm/searchParams.js.map +1 -0
  112. package/dist/esm/serializer.d.ts +22 -0
  113. package/dist/esm/structuralSharing.d.ts +4 -0
  114. package/dist/esm/typePrimitives.d.ts +65 -0
  115. package/dist/esm/useLoaderData.d.ts +5 -0
  116. package/dist/esm/useLoaderDeps.d.ts +5 -0
  117. package/dist/esm/useNavigate.d.ts +3 -0
  118. package/dist/esm/useParams.d.ts +5 -0
  119. package/dist/esm/useRouteContext.d.ts +9 -0
  120. package/dist/esm/useSearch.d.ts +5 -0
  121. package/dist/esm/utils.d.ts +105 -0
  122. package/dist/esm/utils.js +160 -0
  123. package/dist/esm/utils.js.map +1 -0
  124. package/dist/esm/validators.d.ts +51 -0
  125. package/package.json +36 -32
  126. package/src/Matches.ts +239 -0
  127. package/src/RouterProvider.ts +50 -0
  128. package/src/defer.ts +52 -0
  129. package/src/fileRoute.ts +140 -0
  130. package/src/history.ts +9 -0
  131. package/src/index.ts +421 -19
  132. package/src/link.ts +580 -286
  133. package/src/location.ts +13 -0
  134. package/src/manifest.ts +32 -0
  135. package/src/not-found.ts +29 -0
  136. package/src/path.ts +425 -49
  137. package/src/qss.ts +70 -41
  138. package/src/redirect.ts +100 -0
  139. package/src/root.ts +2 -0
  140. package/src/route.ts +1682 -218
  141. package/src/routeInfo.ts +224 -217
  142. package/src/router.ts +3100 -1073
  143. package/src/scroll-restoration.ts +340 -0
  144. package/src/searchMiddleware.ts +54 -0
  145. package/src/searchParams.ts +43 -20
  146. package/src/serializer.ts +32 -0
  147. package/src/structuralSharing.ts +7 -0
  148. package/src/typePrimitives.ts +181 -0
  149. package/src/useLoaderData.ts +20 -0
  150. package/src/useLoaderDeps.ts +13 -0
  151. package/src/useNavigate.ts +13 -0
  152. package/src/useParams.ts +20 -0
  153. package/src/useRouteContext.ts +39 -0
  154. package/src/useSearch.ts +20 -0
  155. package/src/utils.ts +369 -75
  156. package/src/validators.ts +121 -0
  157. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +0 -33
  158. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +0 -1
  159. package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js +0 -33
  160. package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js.map +0 -1
  161. package/build/cjs/node_modules/history/index.js +0 -815
  162. package/build/cjs/node_modules/history/index.js.map +0 -1
  163. package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js +0 -30
  164. package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js.map +0 -1
  165. package/build/cjs/packages/router-core/src/index.js +0 -58
  166. package/build/cjs/packages/router-core/src/index.js.map +0 -1
  167. package/build/cjs/packages/router-core/src/path.js +0 -222
  168. package/build/cjs/packages/router-core/src/path.js.map +0 -1
  169. package/build/cjs/packages/router-core/src/qss.js +0 -71
  170. package/build/cjs/packages/router-core/src/qss.js.map +0 -1
  171. package/build/cjs/packages/router-core/src/route.js +0 -150
  172. package/build/cjs/packages/router-core/src/route.js.map +0 -1
  173. package/build/cjs/packages/router-core/src/routeConfig.js +0 -69
  174. package/build/cjs/packages/router-core/src/routeConfig.js.map +0 -1
  175. package/build/cjs/packages/router-core/src/routeMatch.js +0 -266
  176. package/build/cjs/packages/router-core/src/routeMatch.js.map +0 -1
  177. package/build/cjs/packages/router-core/src/router.js +0 -822
  178. package/build/cjs/packages/router-core/src/router.js.map +0 -1
  179. package/build/cjs/packages/router-core/src/searchParams.js +0 -70
  180. package/build/cjs/packages/router-core/src/searchParams.js.map +0 -1
  181. package/build/cjs/packages/router-core/src/utils.js +0 -125
  182. package/build/cjs/packages/router-core/src/utils.js.map +0 -1
  183. package/build/esm/index.js +0 -2481
  184. package/build/esm/index.js.map +0 -1
  185. package/build/stats-html.html +0 -4034
  186. package/build/stats-react.json +0 -493
  187. package/build/types/index.d.ts +0 -618
  188. package/build/umd/index.development.js +0 -2514
  189. package/build/umd/index.development.js.map +0 -1
  190. package/build/umd/index.production.js +0 -12
  191. package/build/umd/index.production.js.map +0 -1
  192. package/src/frameworks.ts +0 -12
  193. package/src/routeConfig.ts +0 -495
  194. package/src/routeMatch.ts +0 -374
package/src/router.ts CHANGED
@@ -1,1297 +1,3324 @@
1
+ import { Store, batch } from '@tanstack/store'
1
2
  import {
2
- BrowserHistory,
3
3
  createBrowserHistory,
4
4
  createMemoryHistory,
5
- HashHistory,
6
- History,
7
- MemoryHistory,
8
- } from 'history'
5
+ parseHref,
6
+ } from '@tanstack/history'
9
7
  import invariant from 'tiny-invariant'
10
- import { GetFrameworkGeneric } from './frameworks'
11
-
12
8
  import {
13
- LinkInfo,
14
- LinkOptions,
15
- NavigateOptionsAbsolute,
16
- ToOptions,
17
- ValidFromPath,
18
- } from './link'
9
+ createControlledPromise,
10
+ deepEqual,
11
+ functionalUpdate,
12
+ last,
13
+ pick,
14
+ replaceEqualDeep,
15
+ } from './utils'
19
16
  import {
20
17
  cleanPath,
21
18
  interpolatePath,
22
19
  joinPaths,
23
20
  matchPathname,
21
+ parsePathname,
24
22
  resolvePath,
23
+ trimPath,
24
+ trimPathLeft,
25
+ trimPathRight,
25
26
  } from './path'
26
- import { AnyRoute, createRoute, Route } from './route'
27
- import {
28
- AnyLoaderData,
29
- AnyPathParams,
30
- AnyRouteConfig,
31
- AnySearchSchema,
32
- LoaderContext,
33
- RouteConfig,
34
- SearchFilter,
35
- } from './routeConfig'
36
- import {
37
- AllRouteInfo,
38
- AnyAllRouteInfo,
39
- AnyRouteInfo,
40
- RouteInfo,
41
- RoutesById,
42
- } from './routeInfo'
43
- import { createRouteMatch, RouteMatch } from './routeMatch'
27
+ import { isNotFound } from './not-found'
28
+ import { setupScrollRestoration } from './scroll-restoration'
44
29
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
45
- import {
46
- functionalUpdate,
47
- last,
48
- pick,
30
+ import { rootRouteId } from './root'
31
+ import { isRedirect } from './redirect'
32
+ import type { SearchParser, SearchSerializer } from './searchParams'
33
+ import type { AnyRedirect, ResolvedRedirect } from './redirect'
34
+ import type {
35
+ HistoryLocation,
36
+ HistoryState,
37
+ ParsedHistoryState,
38
+ RouterHistory,
39
+ } from '@tanstack/history'
40
+ import type {
41
+ ControlledPromise,
42
+ NoInfer,
43
+ NonNullableUpdater,
49
44
  PickAsRequired,
50
- PickRequired,
51
- replaceEqualDeep,
52
- Timeout,
53
45
  Updater,
54
46
  } from './utils'
47
+ import type { ParsedLocation } from './location'
48
+ import type { DeferredPromiseState } from './defer'
49
+ import type {
50
+ AnyContext,
51
+ AnyRoute,
52
+ AnyRouteWithContext,
53
+ BeforeLoadContextOptions,
54
+ LoaderFnContext,
55
+ MakeRemountDepsOptionsUnion,
56
+ RouteContextOptions,
57
+ RouteMask,
58
+ SearchMiddleware,
59
+ } from './route'
60
+ import type {
61
+ FullSearchSchema,
62
+ RouteById,
63
+ RoutePaths,
64
+ RoutesById,
65
+ RoutesByPath,
66
+ } from './routeInfo'
67
+ import type {
68
+ AnyRouteMatch,
69
+ MakeRouteMatch,
70
+ MakeRouteMatchUnion,
71
+ MatchRouteOptions,
72
+ } from './Matches'
73
+ import type {
74
+ BuildLocationFn,
75
+ CommitLocationOptions,
76
+ NavigateFn,
77
+ } from './RouterProvider'
78
+ import type { Manifest } from './manifest'
79
+ import type { StartSerializer } from './serializer'
80
+ import type { AnySchema, AnyValidator } from './validators'
81
+ import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
82
+ import type { NotFoundError } from './not-found'
83
+
84
+ declare global {
85
+ interface Window {
86
+ __TSR_ROUTER__?: AnyRouter
87
+ }
88
+ }
55
89
 
56
- export interface LocationState {}
57
-
58
- export interface Location<
59
- TSearchObj extends AnySearchSchema = {},
60
- TState extends LocationState = LocationState,
61
- > {
62
- href: string
63
- pathname: string
64
- search: TSearchObj
65
- searchStr: string
66
- state: TState
67
- hash: string
68
- key?: string
90
+ export type ControllablePromise<T = any> = Promise<T> & {
91
+ resolve: (value: T) => void
92
+ reject: (value?: any) => void
69
93
  }
70
94
 
71
- export interface FromLocation {
72
- pathname: string
73
- search?: unknown
74
- key?: string
75
- hash?: string
95
+ export type InjectedHtmlEntry = Promise<string>
96
+
97
+ export interface DefaultRegister {
98
+ router: AnyRouter
76
99
  }
77
100
 
78
- export type SearchSerializer = (searchObj: Record<string, any>) => string
79
- export type SearchParser = (searchStr: string) => Record<string, any>
80
- export type FilterRoutesFn = <TRoute extends Route<any, RouteInfo>>(
81
- routeConfigs: TRoute[],
82
- ) => TRoute[]
101
+ export interface Register extends DefaultRegister {
102
+ // router: Router
103
+ }
83
104
 
84
- export interface RouterOptions<TRouteConfig extends AnyRouteConfig> {
85
- history?: BrowserHistory | MemoryHistory | HashHistory
105
+ export type RegisteredRouter = Register['router']
106
+
107
+ export type DefaultRemountDepsFn<TRouteTree extends AnyRoute> = (
108
+ opts: MakeRemountDepsOptionsUnion<TRouteTree>,
109
+ ) => any
110
+
111
+ export interface DefaultRouterOptionsExtensions {}
112
+
113
+ export interface RouterOptionsExtensions
114
+ extends DefaultRouterOptionsExtensions {}
115
+
116
+ export interface RouterOptions<
117
+ TRouteTree extends AnyRoute,
118
+ TTrailingSlashOption extends TrailingSlashOption,
119
+ TDefaultStructuralSharingOption extends boolean = false,
120
+ TRouterHistory extends RouterHistory = RouterHistory,
121
+ TDehydrated extends Record<string, any> = Record<string, any>,
122
+ > extends RouterOptionsExtensions {
123
+ /**
124
+ * The history object that will be used to manage the browser history.
125
+ *
126
+ * If not provided, a new createBrowserHistory instance will be created and used.
127
+ *
128
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#history-property)
129
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/history-types)
130
+ */
131
+ history?: TRouterHistory
132
+ /**
133
+ * A function that will be used to stringify search params when generating links.
134
+ *
135
+ * @default defaultStringifySearch
136
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#stringifysearch-method)
137
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization)
138
+ */
86
139
  stringifySearch?: SearchSerializer
140
+ /**
141
+ * A function that will be used to parse search params when parsing the current location.
142
+ *
143
+ * @default defaultParseSearch
144
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#parsesearch-method)
145
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization)
146
+ */
87
147
  parseSearch?: SearchParser
88
- filterRoutes?: FilterRoutesFn
89
- defaultPreload?: false | 'intent'
90
- defaultPreloadMaxAge?: number
91
- defaultPreloadGcMaxAge?: number
148
+ /**
149
+ * If `false`, routes will not be preloaded by default in any way.
150
+ *
151
+ * If `'intent'`, routes will be preloaded by default when the user hovers over a link or a `touchstart` event is detected on a `<Link>`.
152
+ *
153
+ * If `'viewport'`, routes will be preloaded by default when they are within the viewport.
154
+ *
155
+ * @default false
156
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreload-property)
157
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading)
158
+ */
159
+ defaultPreload?: false | 'intent' | 'viewport' | 'render'
160
+ /**
161
+ * The delay in milliseconds that a route must be hovered over or touched before it is preloaded.
162
+ *
163
+ * @default 50
164
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloaddelay-property)
165
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-delay)
166
+ */
92
167
  defaultPreloadDelay?: number
93
- useErrorBoundary?: boolean
94
- defaultElement?: GetFrameworkGeneric<'Element'>
95
- defaultErrorElement?: GetFrameworkGeneric<'Element'>
96
- defaultCatchElement?: GetFrameworkGeneric<'Element'>
97
- defaultPendingElement?: GetFrameworkGeneric<'Element'>
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
+ /**
177
+ * The default `pendingMs` a route should use if no pendingMs is provided.
178
+ *
179
+ * @default 1000
180
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpendingms-property)
181
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#avoiding-pending-component-flash)
182
+ */
98
183
  defaultPendingMs?: number
184
+ /**
185
+ * The default `pendingMinMs` a route should use if no pendingMinMs is provided.
186
+ *
187
+ * @default 500
188
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpendingminms-property)
189
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#avoiding-pending-component-flash)
190
+ */
99
191
  defaultPendingMinMs?: number
100
- defaultLoaderMaxAge?: number
101
- defaultLoaderGcMaxAge?: number
192
+ /**
193
+ * The default `staleTime` a route should use if no staleTime is provided. This is the time in milliseconds that a route will be considered fresh.
194
+ *
195
+ * @default 0
196
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultstaletime-property)
197
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#key-options)
198
+ */
199
+ defaultStaleTime?: number
200
+ /**
201
+ * The default `preloadStaleTime` a route should use if no preloadStaleTime is provided.
202
+ *
203
+ * @default 30_000 `(30 seconds)`
204
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadstaletime-property)
205
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading)
206
+ */
207
+ defaultPreloadStaleTime?: number
208
+ /**
209
+ * The default `defaultPreloadGcTime` a route should use if no preloadGcTime is provided.
210
+ *
211
+ * @default 1_800_000 `(30 minutes)`
212
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadgctime-property)
213
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading)
214
+ */
215
+ defaultPreloadGcTime?: number
216
+ /**
217
+ * If `true`, route navigations will called using `document.startViewTransition()`.
218
+ *
219
+ * If the browser does not support this api, this option will be ignored.
220
+ *
221
+ * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) for more information on how this function works.
222
+ *
223
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultviewtransition-property)
224
+ */
225
+ defaultViewTransition?: boolean | ViewTransitionOptions
226
+ /**
227
+ * The default `hashScrollIntoView` a route should use if no hashScrollIntoView is provided while navigating
228
+ *
229
+ * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) for more information on `ScrollIntoViewOptions`.
230
+ *
231
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaulthashscrollintoview-property)
232
+ */
233
+ defaultHashScrollIntoView?: boolean | ScrollIntoViewOptions
234
+ /**
235
+ * @default 'fuzzy'
236
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#notfoundmode-property)
237
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/not-found-errors#the-notfoundmode-option)
238
+ */
239
+ notFoundMode?: 'root' | 'fuzzy'
240
+ /**
241
+ * The default `gcTime` a route should use if no gcTime is provided.
242
+ *
243
+ * @default 1_800_000 `(30 minutes)`
244
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultgctime-property)
245
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#key-options)
246
+ */
247
+ defaultGcTime?: number
248
+ /**
249
+ * If `true`, all routes will be matched as case-sensitive.
250
+ *
251
+ * @default false
252
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#casesensitive-property)
253
+ */
102
254
  caseSensitive?: boolean
103
- routeConfig?: TRouteConfig
255
+ /**
256
+ *
257
+ * The route tree that will be used to configure the router instance.
258
+ *
259
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#routetree-property)
260
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/routing/route-trees)
261
+ */
262
+ routeTree?: TRouteTree
263
+ /**
264
+ * The basepath for then entire router. This is useful for mounting a router instance at a subpath.
265
+ *
266
+ * @default '/'
267
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#basepath-property)
268
+ */
104
269
  basepath?: string
105
- createRouter?: (router: Router<any, any>) => void
106
- createRoute?: (opts: { route: AnyRoute; router: Router<any, any> }) => void
107
- createElement?: (
108
- element: GetFrameworkGeneric<'SyncOrAsyncElement'>,
109
- ) => Promise<GetFrameworkGeneric<'Element'>>
110
- }
111
-
112
- export interface Action<
113
- TPayload = unknown,
114
- TResponse = unknown,
115
- // TError = unknown,
116
- > {
117
- submit: (submission?: TPayload) => Promise<TResponse>
118
- current?: ActionState<TPayload, TResponse>
119
- latest?: ActionState<TPayload, TResponse>
120
- pending: ActionState<TPayload, TResponse>[]
121
- }
270
+ /**
271
+ * The root context that will be provided to all routes in the route tree.
272
+ *
273
+ * This can be used to provide a context to all routes in the tree without having to provide it to each route individually.
274
+ *
275
+ * Optional or required if the root route was created with [`createRootRouteWithContext()`](https://tanstack.com/router/latest/docs/framework/react/api/router/createRootRouteWithContextFunction).
276
+ *
277
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#context-property)
278
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/router-context)
279
+ */
280
+ context?: InferRouterContext<TRouteTree>
281
+ /**
282
+ * A function that will be called when the router is dehydrated.
283
+ *
284
+ * The return value of this function will be serialized and stored in the router's dehydrated state.
285
+ *
286
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#dehydrate-method)
287
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration)
288
+ */
289
+ dehydrate?: () => TDehydrated
290
+ /**
291
+ * A function that will be called when the router is hydrated.
292
+ *
293
+ * The return value of this function will be serialized and stored in the router's dehydrated state.
294
+ *
295
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#hydrate-method)
296
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration)
297
+ */
298
+ hydrate?: (dehydrated: TDehydrated) => void
299
+ /**
300
+ * An array of route masks that will be used to mask routes in the route tree.
301
+ *
302
+ * Route masking is when you display a route at a different path than the one it is configured to match, like a modal popup that when shared will unmask to the modal's content instead of the modal's context.
303
+ *
304
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#routemasks-property)
305
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/route-masking)
306
+ */
307
+ routeMasks?: Array<RouteMask<TRouteTree>>
308
+ /**
309
+ * If `true`, route masks will, by default, be removed when the page is reloaded.
310
+ *
311
+ * This can be overridden on a per-mask basis by setting the `unmaskOnReload` option on the mask, or on a per-navigation basis by setting the `unmaskOnReload` option in the `Navigate` options.
312
+ *
313
+ * @default false
314
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#unmaskonreload-property)
315
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/route-masking#unmasking-on-page-reload)
316
+ */
317
+ unmaskOnReload?: boolean
318
+
319
+ /**
320
+ * Use `notFoundComponent` instead.
321
+ *
322
+ * @deprecated
323
+ * See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info.
324
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#notfoundroute-property)
325
+ */
326
+ notFoundRoute?: AnyRoute
327
+ /**
328
+ * Configures how trailing slashes are treated.
329
+ *
330
+ * - `'always'` will add a trailing slash if not present
331
+ * - `'never'` will remove the trailing slash if present
332
+ * - `'preserve'` will not modify the trailing slash.
333
+ *
334
+ * @default 'never'
335
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#trailingslash-property)
336
+ */
337
+ trailingSlash?: TTrailingSlashOption
338
+ /**
339
+ * While usually automatic, sometimes it can be useful to force the router into a server-side state, e.g. when using the router in a non-browser environment that has access to a global.document object.
340
+ *
341
+ * @default typeof document !== 'undefined'
342
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#isserver-property)
343
+ */
344
+ isServer?: boolean
345
+
346
+ defaultSsr?: boolean
347
+
348
+ search?: {
349
+ /**
350
+ * Configures how unknown search params (= not returned by any `validateSearch`) are treated.
351
+ *
352
+ * @default false
353
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#search.strict-property)
354
+ */
355
+ strict?: boolean
356
+ }
122
357
 
123
- export interface ActionState<
124
- TPayload = unknown,
125
- TResponse = unknown,
126
- // TError = unknown,
127
- > {
128
- submittedAt: number
129
- status: 'idle' | 'pending' | 'success' | 'error'
130
- submission: TPayload
131
- data?: TResponse
132
- error?: unknown
133
- }
358
+ /**
359
+ * Configures whether structural sharing is enabled by default for fine-grained selectors.
360
+ *
361
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultstructuralsharing-property)
362
+ */
363
+ defaultStructuralSharing?: TDefaultStructuralSharingOption
364
+
365
+ /**
366
+ * Configures which URI characters are allowed in path params that would ordinarily be escaped by encodeURIComponent.
367
+ *
368
+ * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#pathparamsallowedcharacters-property)
369
+ * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/path-params#allowed-characters)
370
+ */
371
+ pathParamsAllowedCharacters?: Array<
372
+ ';' | ':' | '@' | '&' | '=' | '+' | '$' | ','
373
+ >
134
374
 
135
- export interface Loader<
136
- TFullSearchSchema extends AnySearchSchema = {},
137
- TAllParams extends AnyPathParams = {},
138
- TRouteLoaderData = AnyLoaderData,
139
- > {
140
- fetch: keyof PickRequired<TFullSearchSchema> extends never
141
- ? keyof TAllParams extends never
142
- ? (loaderContext: { signal?: AbortSignal }) => Promise<TRouteLoaderData>
143
- : (loaderContext: {
144
- params: TAllParams
145
- search?: TFullSearchSchema
146
- signal?: AbortSignal
147
- }) => Promise<TRouteLoaderData>
148
- : keyof TAllParams extends never
149
- ? (loaderContext: {
150
- search: TFullSearchSchema
151
- params: TAllParams
152
- signal?: AbortSignal
153
- }) => Promise<TRouteLoaderData>
154
- : (loaderContext: {
155
- search: TFullSearchSchema
156
- signal?: AbortSignal
157
- }) => Promise<TRouteLoaderData>
158
- current?: LoaderState<TFullSearchSchema, TAllParams>
159
- latest?: LoaderState<TFullSearchSchema, TAllParams>
160
- pending: LoaderState<TFullSearchSchema, TAllParams>[]
375
+ defaultRemountDeps?: DefaultRemountDepsFn<TRouteTree>
376
+
377
+ /**
378
+ * If `true`, scroll restoration will be enabled
379
+ *
380
+ * @default false
381
+ */
382
+ scrollRestoration?: boolean
383
+
384
+ /**
385
+ * A function that will be called to get the key for the scroll restoration cache.
386
+ *
387
+ * @default (location) => location.href
388
+ */
389
+ getScrollRestorationKey?: (location: ParsedLocation) => string
390
+ /**
391
+ * The default behavior for scroll restoration.
392
+ *
393
+ * @default 'auto'
394
+ */
395
+ scrollRestorationBehavior?: ScrollBehavior
396
+ /**
397
+ * An array of selectors that will be used to scroll to the top of the page in addition to `window`
398
+ *
399
+ * @default ['window']
400
+ */
401
+ scrollToTopSelectors?: Array<string>
161
402
  }
162
403
 
163
- export interface LoaderState<
164
- TFullSearchSchema = unknown,
165
- TAllParams = unknown,
404
+ export interface RouterState<
405
+ in out TRouteTree extends AnyRoute = AnyRoute,
406
+ in out TRouteMatch = MakeRouteMatchUnion,
166
407
  > {
408
+ status: 'pending' | 'idle'
167
409
  loadedAt: number
168
- loaderContext: LoaderContext<TFullSearchSchema, TAllParams>
169
- }
170
-
171
- export interface RouterState {
172
- status: 'idle' | 'loading'
173
- location: Location
174
- matches: RouteMatch[]
175
- lastUpdated: number
176
- currentAction?: ActionState
177
- latestAction?: ActionState
178
- actions: Record<string, Action>
179
- loaders: Record<string, Loader>
180
- pending?: PendingState
181
- isFetching: boolean
182
- isPreloading: boolean
410
+ isLoading: boolean
411
+ isTransitioning: boolean
412
+ matches: Array<TRouteMatch>
413
+ pendingMatches?: Array<TRouteMatch>
414
+ cachedMatches: Array<TRouteMatch>
415
+ location: ParsedLocation<FullSearchSchema<TRouteTree>>
416
+ resolvedLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
417
+ statusCode: number
418
+ redirect?: AnyRedirect
183
419
  }
184
420
 
185
- export interface PendingState {
186
- location: Location
187
- matches: RouteMatch[]
188
- }
189
-
190
- type Listener = (router: Router<any, any>) => void
191
-
192
- export type ListenerFn = () => void
193
-
194
421
  export interface BuildNextOptions {
195
422
  to?: string | number | null
196
- params?: true | Updater<Record<string, any>>
423
+ params?: true | Updater<unknown>
197
424
  search?: true | Updater<unknown>
198
425
  hash?: true | Updater<string>
199
- key?: string
426
+ state?: true | NonNullableUpdater<ParsedHistoryState, HistoryState>
427
+ mask?: {
428
+ to?: string | number | null
429
+ params?: true | Updater<unknown>
430
+ search?: true | Updater<unknown>
431
+ hash?: true | Updater<string>
432
+ state?: true | NonNullableUpdater<ParsedHistoryState, HistoryState>
433
+ unmaskOnReload?: boolean
434
+ }
200
435
  from?: string
201
- fromCurrent?: boolean
202
- __preSearchFilters?: SearchFilter<any>[]
203
- __postSearchFilters?: SearchFilter<any>[]
436
+ _fromLocation?: ParsedLocation
437
+ href?: string
204
438
  }
205
439
 
206
- export type MatchCacheEntry = {
207
- gc: number
208
- match: RouteMatch
440
+ type NavigationEventInfo = {
441
+ fromLocation?: ParsedLocation
442
+ toLocation: ParsedLocation
443
+ pathChanged: boolean
444
+ hrefChanged: boolean
445
+ hashChanged: boolean
209
446
  }
210
447
 
211
- export interface MatchLocation {
212
- to?: string | number | null
213
- fuzzy?: boolean
214
- caseSensitive?: boolean
215
- from?: string
216
- fromCurrent?: boolean
448
+ export type RouterEvents = {
449
+ onBeforeNavigate: {
450
+ type: 'onBeforeNavigate'
451
+ } & NavigationEventInfo
452
+ onBeforeLoad: {
453
+ type: 'onBeforeLoad'
454
+ } & NavigationEventInfo
455
+ onLoad: {
456
+ type: 'onLoad'
457
+ } & NavigationEventInfo
458
+ onResolved: {
459
+ type: 'onResolved'
460
+ } & NavigationEventInfo
461
+ onBeforeRouteMount: {
462
+ type: 'onBeforeRouteMount'
463
+ } & NavigationEventInfo
464
+ onInjectedHtml: {
465
+ type: 'onInjectedHtml'
466
+ promise: Promise<string>
467
+ }
468
+ onRendered: {
469
+ type: 'onRendered'
470
+ } & NavigationEventInfo
217
471
  }
218
472
 
219
- export interface MatchRouteOptions {
220
- pending: boolean
221
- caseSensitive?: boolean
222
- }
473
+ export type RouterEvent = RouterEvents[keyof RouterEvents]
223
474
 
224
- type LinkCurrentTargetElement = {
225
- preloadTimeout?: null | ReturnType<typeof setTimeout>
226
- }
475
+ export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void
227
476
 
228
- interface DehydratedRouterState
229
- extends Pick<RouterState, 'status' | 'location' | 'lastUpdated'> {
230
- matches: DehydratedRouteMatch[]
477
+ export type RouterListener<TRouterEvent extends RouterEvent> = {
478
+ eventType: TRouterEvent['type']
479
+ fn: ListenerFn<TRouterEvent>
231
480
  }
232
481
 
233
- interface DehydratedRouteMatch
234
- extends Pick<
235
- RouteMatch<any, any>,
236
- | 'matchId'
237
- | 'status'
238
- | 'routeLoaderData'
239
- | 'loaderData'
240
- | 'isInvalid'
241
- | 'invalidAt'
242
- > {}
243
-
244
- export interface Router<
245
- TRouteConfig extends AnyRouteConfig = RouteConfig,
246
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
247
- > {
248
- history: BrowserHistory | MemoryHistory | HashHistory
249
- options: PickAsRequired<
250
- RouterOptions<TRouteConfig>,
251
- 'stringifySearch' | 'parseSearch'
252
- >
253
- // Computed in this.update()
254
- basepath: string
255
- // Internal:
256
- allRouteInfo: TAllRouteInfo
257
- listeners: Listener[]
258
- location: Location
259
- navigateTimeout?: Timeout
260
- nextAction?: 'push' | 'replace'
261
- state: RouterState
262
- routeTree: Route<TAllRouteInfo, RouteInfo>
263
- routesById: RoutesById<TAllRouteInfo>
264
- navigationPromise: Promise<void>
265
- removeActionQueue: { action: Action; actionState: ActionState }[]
266
- startedLoadingAt: number
267
- resolveNavigation: () => void
268
- subscribe: (listener: Listener) => () => void
269
- notify: () => void
270
- mount: () => () => void
271
- onFocus: () => void
272
- update: <TRouteConfig extends RouteConfig = RouteConfig>(
273
- opts?: RouterOptions<TRouteConfig>,
274
- ) => Router<TRouteConfig>
275
-
276
- buildNext: (opts: BuildNextOptions) => Location
277
- cancelMatches: () => void
278
- loadLocation: (next?: Location) => Promise<void>
279
- matchCache: Record<string, MatchCacheEntry>
280
- cleanMatchCache: () => void
281
- getRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
282
- id: TId,
283
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
284
- loadRoute: (navigateOpts: BuildNextOptions) => Promise<RouteMatch[]>
285
- preloadRoute: (
286
- navigateOpts: BuildNextOptions,
287
- loaderOpts: { maxAge?: number; gcMaxAge?: number },
288
- ) => Promise<RouteMatch[]>
289
- matchRoutes: (
290
- pathname: string,
291
- opts?: { strictParseParams?: boolean },
292
- ) => RouteMatch[]
293
- loadMatches: (
294
- resolvedMatches: RouteMatch[],
295
- loaderOpts?: { withPending?: boolean } & (
296
- | { preload: true; maxAge: number; gcMaxAge: number }
297
- | { preload?: false; maxAge?: never; gcMaxAge?: never }
298
- ),
299
- ) => Promise<void>
300
- invalidateRoute: (opts: MatchLocation) => void
301
- reload: () => Promise<void>
302
- resolvePath: (from: string, path: string) => string
303
- navigate: <
304
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
305
- TTo extends string = '.',
306
- >(
307
- opts: NavigateOptionsAbsolute<TAllRouteInfo, TFrom, TTo>,
308
- ) => Promise<void>
309
- matchRoute: <
310
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
311
- TTo extends string = '.',
312
- >(
313
- matchLocation: ToOptions<TAllRouteInfo, TFrom, TTo>,
314
- opts?: MatchRouteOptions,
315
- ) => boolean
316
- buildLink: <
317
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
318
- TTo extends string = '.',
319
- >(
320
- opts: LinkOptions<TAllRouteInfo, TFrom, TTo>,
321
- ) => LinkInfo
322
- dehydrateState: () => DehydratedRouterState
323
- hydrateState: (state: DehydratedRouterState) => void
324
- __: {
325
- buildRouteTree: (
326
- routeConfig: RouteConfig,
327
- ) => Route<TAllRouteInfo, AnyRouteInfo>
328
- parseLocation: (
329
- location: History['location'],
330
- previousLocation?: Location,
331
- ) => Location
332
- buildLocation: (dest: BuildNextOptions) => Location
333
- commitLocation: (next: Location, replace?: boolean) => Promise<void>
334
- navigate: (
335
- location: BuildNextOptions & { replace?: boolean },
336
- ) => Promise<void>
337
- }
482
+ export interface MatchRoutesOpts {
483
+ preload?: boolean
484
+ throwOnError?: boolean
485
+ _buildLocation?: boolean
486
+ dest?: BuildNextOptions
338
487
  }
339
488
 
340
- // Detect if we're in the DOM
341
- const isServer =
342
- typeof window === 'undefined' || !window.document?.createElement
489
+ export type InferRouterContext<TRouteTree extends AnyRoute> =
490
+ TRouteTree['types']['routerContext']
343
491
 
344
- // This is the default history object if none is defined
345
- const createDefaultHistory = () =>
346
- isServer ? createMemoryHistory() : createBrowserHistory()
347
-
348
- export function createRouter<
349
- TRouteConfig extends AnyRouteConfig = RouteConfig,
350
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
351
- >(
352
- userOptions?: RouterOptions<TRouteConfig>,
353
- ): Router<TRouteConfig, TAllRouteInfo> {
354
- const history = userOptions?.history || createDefaultHistory()
355
-
356
- const originalOptions = {
357
- defaultLoaderGcMaxAge: 5 * 60 * 1000,
358
- defaultLoaderMaxAge: 0,
359
- defaultPreloadMaxAge: 2000,
360
- defaultPreloadDelay: 50,
361
- ...userOptions,
362
- stringifySearch: userOptions?.stringifySearch ?? defaultStringifySearch,
363
- parseSearch: userOptions?.parseSearch ?? defaultParseSearch,
364
- }
365
-
366
- let router: Router<TRouteConfig, TAllRouteInfo> = {
367
- history,
368
- options: originalOptions,
369
- listeners: [],
370
- removeActionQueue: [],
371
- // Resolved after construction
372
- basepath: '',
373
- routeTree: undefined!,
374
- routesById: {} as any,
375
- location: undefined!,
376
- allRouteInfo: undefined!,
377
- //
378
- navigationPromise: Promise.resolve(),
379
- resolveNavigation: () => {},
380
- matchCache: {},
381
- state: {
382
- status: 'idle',
383
- location: null!,
384
- matches: [],
385
- actions: {},
386
- loaders: {},
387
- lastUpdated: Date.now(),
388
- isFetching: false,
389
- isPreloading: false,
390
- },
391
- startedLoadingAt: Date.now(),
392
- subscribe: (listener: Listener): (() => void) => {
393
- router.listeners.push(listener as Listener)
394
- return () => {
395
- router.listeners = router.listeners.filter((x) => x !== listener)
396
- }
397
- },
398
- getRoute: (id) => {
399
- return router.routesById[id]
400
- },
401
- notify: (): void => {
402
- router.state = {
403
- ...router.state,
404
- isFetching:
405
- router.state.status === 'loading' ||
406
- router.state.matches.some((d) => d.isFetching),
407
- isPreloading: Object.values(router.matchCache).some(
408
- (d) =>
409
- d.match.isFetching &&
410
- !router.state.matches.find((dd) => dd.matchId === d.match.matchId),
411
- ),
492
+ export type RouterContextOptions<TRouteTree extends AnyRoute> =
493
+ AnyContext extends InferRouterContext<TRouteTree>
494
+ ? {
495
+ context?: InferRouterContext<TRouteTree>
412
496
  }
413
-
414
- cascadeLoaderData(router.state.matches)
415
- router.listeners.forEach((listener) => listener(router))
416
- },
417
-
418
- dehydrateState: () => {
419
- return {
420
- ...pick(router.state, ['status', 'location', 'lastUpdated']),
421
- matches: router.state.matches.map((match) =>
422
- pick(match, [
423
- 'matchId',
424
- 'status',
425
- 'routeLoaderData',
426
- 'loaderData',
427
- 'isInvalid',
428
- 'invalidAt',
429
- ]),
430
- ),
497
+ : {
498
+ context: InferRouterContext<TRouteTree>
431
499
  }
432
- },
433
500
 
434
- hydrateState: (dehydratedState) => {
435
- // Match the routes
436
- const matches = router.matchRoutes(router.location.pathname, {
437
- strictParseParams: true,
438
- })
501
+ export type RouterConstructorOptions<
502
+ TRouteTree extends AnyRoute,
503
+ TTrailingSlashOption extends TrailingSlashOption,
504
+ TDefaultStructuralSharingOption extends boolean,
505
+ TRouterHistory extends RouterHistory,
506
+ TDehydrated extends Record<string, any>,
507
+ > = Omit<
508
+ RouterOptions<
509
+ TRouteTree,
510
+ TTrailingSlashOption,
511
+ TDefaultStructuralSharingOption,
512
+ TRouterHistory,
513
+ TDehydrated
514
+ >,
515
+ 'context'
516
+ > &
517
+ RouterContextOptions<TRouteTree>
518
+
519
+ export interface RouterErrorSerializer<TSerializedError> {
520
+ serialize: (err: unknown) => TSerializedError
521
+ deserialize: (err: TSerializedError) => unknown
522
+ }
439
523
 
440
- router.state = {
441
- ...router.state,
442
- ...dehydratedState,
443
- matches: matches.map((match) => {
444
- const dehydratedMatch = dehydratedState.matches.find(
445
- (d: any) => d.matchId === match.matchId,
446
- )
447
- invariant(
448
- dehydratedMatch,
449
- 'Oh no! Dehydrated route matches did not match the active state of the router 😬',
450
- )
451
- Object.assign(match, dehydratedMatch)
452
- return match
453
- }),
454
- }
455
- },
524
+ export interface MatchedRoutesResult {
525
+ matchedRoutes: Array<AnyRoute>
526
+ routeParams: Record<string, string>
527
+ }
456
528
 
457
- mount: () => {
458
- const next = router.__.buildLocation({
459
- to: '.',
460
- search: true,
461
- hash: true,
462
- })
529
+ export type PreloadRouteFn<
530
+ TRouteTree extends AnyRoute,
531
+ TTrailingSlashOption extends TrailingSlashOption,
532
+ TDefaultStructuralSharingOption extends boolean,
533
+ TRouterHistory extends RouterHistory,
534
+ > = <
535
+ TFrom extends RoutePaths<TRouteTree> | string = string,
536
+ TTo extends string | undefined = undefined,
537
+ TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
538
+ TMaskTo extends string = '',
539
+ >(
540
+ opts: NavigateOptions<
541
+ RouterCore<
542
+ TRouteTree,
543
+ TTrailingSlashOption,
544
+ TDefaultStructuralSharingOption,
545
+ TRouterHistory
546
+ >,
547
+ TFrom,
548
+ TTo,
549
+ TMaskFrom,
550
+ TMaskTo
551
+ >,
552
+ ) => Promise<Array<AnyRouteMatch> | undefined>
553
+
554
+ export type MatchRouteFn<
555
+ TRouteTree extends AnyRoute,
556
+ TTrailingSlashOption extends TrailingSlashOption,
557
+ TDefaultStructuralSharingOption extends boolean,
558
+ TRouterHistory extends RouterHistory,
559
+ > = <
560
+ TFrom extends RoutePaths<TRouteTree> = '/',
561
+ TTo extends string | undefined = undefined,
562
+ TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
563
+ >(
564
+ location: ToOptions<
565
+ RouterCore<
566
+ TRouteTree,
567
+ TTrailingSlashOption,
568
+ TDefaultStructuralSharingOption,
569
+ TRouterHistory
570
+ >,
571
+ TFrom,
572
+ TTo
573
+ >,
574
+ opts?: MatchRouteOptions,
575
+ ) => false | RouteById<TRouteTree, TResolved>['types']['allParams']
576
+
577
+ export type UpdateFn<
578
+ TRouteTree extends AnyRoute,
579
+ TTrailingSlashOption extends TrailingSlashOption,
580
+ TDefaultStructuralSharingOption extends boolean,
581
+ TRouterHistory extends RouterHistory,
582
+ TDehydrated extends Record<string, any>,
583
+ > = (
584
+ newOptions: RouterConstructorOptions<
585
+ TRouteTree,
586
+ TTrailingSlashOption,
587
+ TDefaultStructuralSharingOption,
588
+ TRouterHistory,
589
+ TDehydrated
590
+ >,
591
+ ) => void
592
+
593
+ export type InvalidateFn<TRouter extends AnyRouter> = (opts?: {
594
+ filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean
595
+ sync?: boolean
596
+ }) => Promise<void>
597
+
598
+ export type ParseLocationFn<TRouteTree extends AnyRoute> = (
599
+ previousLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>,
600
+ locationToParse?: HistoryLocation,
601
+ ) => ParsedLocation<FullSearchSchema<TRouteTree>>
602
+
603
+ export type GetMatchRoutesFn = (
604
+ pathname: string,
605
+ routePathname: string | undefined,
606
+ ) => {
607
+ matchedRoutes: Array<AnyRoute>
608
+ routeParams: Record<string, string>
609
+ foundRoute: AnyRoute | undefined
610
+ }
463
611
 
464
- // If the current location isn't updated, trigger a navigation
465
- // to the current location. Otherwise, load the current location.
466
- if (next.href !== router.location.href) {
467
- router.__.commitLocation(next, true)
468
- }
612
+ export type EmitFn = (routerEvent: RouterEvent) => void
469
613
 
470
- router.loadLocation()
614
+ export type LoadFn = (opts?: { sync?: boolean }) => Promise<void>
471
615
 
472
- const unsub = router.history.listen((event) => {
473
- console.log(event.location)
474
- router.loadLocation(
475
- router.__.parseLocation(event.location, router.location),
476
- )
477
- })
616
+ export type CommitLocationFn = ({
617
+ viewTransition,
618
+ ignoreBlocker,
619
+ ...next
620
+ }: ParsedLocation & CommitLocationOptions) => Promise<void>
478
621
 
479
- // addEventListener does not exist in React Native, but window does
480
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
481
- if (!isServer && window.addEventListener) {
482
- // Listen to visibillitychange and focus
483
- window.addEventListener('visibilitychange', router.onFocus, false)
484
- window.addEventListener('focus', router.onFocus, false)
485
- }
622
+ export type StartTransitionFn = (fn: () => void) => void
486
623
 
487
- return () => {
488
- unsub()
489
- // Be sure to unsubscribe if a new handler is set
490
- window.removeEventListener('visibilitychange', router.onFocus)
491
- window.removeEventListener('focus', router.onFocus)
492
- }
493
- },
624
+ export type SubscribeFn = <TType extends keyof RouterEvents>(
625
+ eventType: TType,
626
+ fn: ListenerFn<RouterEvents[TType]>,
627
+ ) => () => void
494
628
 
495
- onFocus: () => {
496
- router.loadLocation()
497
- },
629
+ export interface MatchRoutesFn {
630
+ (
631
+ pathname: string,
632
+ locationSearch: AnySchema,
633
+ opts?: MatchRoutesOpts,
634
+ ): Array<AnyRouteMatch>
635
+ (next: ParsedLocation, opts?: MatchRoutesOpts): Array<AnyRouteMatch>
636
+ (
637
+ pathnameOrNext: string | ParsedLocation,
638
+ locationSearchOrOpts?: AnySchema | MatchRoutesOpts,
639
+ opts?: MatchRoutesOpts,
640
+ ): Array<AnyRouteMatch>
641
+ }
498
642
 
499
- update: (opts) => {
500
- const newHistory = opts?.history !== router.history
501
- if (!router.location || newHistory) {
502
- if (opts?.history) {
503
- router.history = opts.history
504
- }
505
- router.location = router.__.parseLocation(router.history.location)
506
- router.state.location = router.location
507
- }
643
+ export type GetMatchFn = (matchId: string) => AnyRouteMatch | undefined
508
644
 
509
- Object.assign(router.options, opts)
645
+ export type UpdateMatchFn = (
646
+ id: string,
647
+ updater: (match: AnyRouteMatch) => AnyRouteMatch,
648
+ ) => AnyRouteMatch
510
649
 
511
- const { basepath, routeConfig } = router.options
650
+ export type LoadRouteChunkFn = (route: AnyRoute) => Promise<Array<void>>
512
651
 
513
- router.basepath = cleanPath(`/${basepath ?? ''}`)
652
+ export type ResolveRedirect = (err: AnyRedirect) => ResolvedRedirect
514
653
 
515
- if (routeConfig) {
516
- router.routesById = {} as any
517
- router.routeTree = router.__.buildRouteTree(routeConfig)
518
- }
654
+ export type ClearCacheFn<TRouter extends AnyRouter> = (opts?: {
655
+ filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean
656
+ }) => void
519
657
 
520
- return router as any
521
- },
658
+ export interface ServerSrr {
659
+ injectedHtml: Array<InjectedHtmlEntry>
660
+ injectHtml: (getHtml: () => string | Promise<string>) => Promise<void>
661
+ injectScript: (
662
+ getScript: () => string | Promise<string>,
663
+ opts?: { logScript?: boolean },
664
+ ) => Promise<void>
665
+ streamValue: (key: string, value: any) => void
666
+ streamedKeys: Set<string>
667
+ onMatchSettled: (opts: { router: AnyRouter; match: AnyRouteMatch }) => any
668
+ }
522
669
 
523
- cancelMatches: () => {
524
- ;[
525
- ...router.state.matches,
526
- ...(router.state.pending?.matches ?? []),
527
- ].forEach((match) => {
528
- match.cancel()
529
- })
530
- },
670
+ export type AnyRouterWithContext<TContext> = RouterCore<
671
+ AnyRouteWithContext<TContext>,
672
+ any,
673
+ any,
674
+ any,
675
+ any
676
+ >
677
+
678
+ export type AnyRouter = RouterCore<any, any, any, any, any>
679
+
680
+ export interface ViewTransitionOptions {
681
+ types:
682
+ | Array<string>
683
+ | ((locationChangeInfo: {
684
+ fromLocation?: ParsedLocation
685
+ toLocation: ParsedLocation
686
+ pathChanged: boolean
687
+ hrefChanged: boolean
688
+ hashChanged: boolean
689
+ }) => Array<string>)
690
+ }
531
691
 
532
- loadLocation: async (next?: Location) => {
533
- const id = Math.random()
534
- router.startedLoadingAt = id
692
+ export function defaultSerializeError(err: unknown) {
693
+ if (err instanceof Error) {
694
+ const obj = {
695
+ name: err.name,
696
+ message: err.message,
697
+ }
535
698
 
536
- if (next) {
537
- // Ingest the new location
538
- router.location = next
539
- }
699
+ if (process.env.NODE_ENV === 'development') {
700
+ ;(obj as any).stack = err.stack
701
+ }
540
702
 
541
- // Clear out old actions
542
- router.removeActionQueue.forEach(({ action, actionState }) => {
543
- if (router.state.currentAction === actionState) {
544
- router.state.currentAction = undefined
545
- }
546
- if (action.current === actionState) {
547
- action.current = undefined
548
- }
549
- })
550
- router.removeActionQueue = []
703
+ return obj
704
+ }
551
705
 
552
- // Cancel any pending matches
553
- router.cancelMatches()
706
+ return {
707
+ data: err,
708
+ }
709
+ }
710
+ export interface ExtractedBaseEntry {
711
+ dataType: '__beforeLoadContext' | 'loaderData'
712
+ type: string
713
+ path: Array<string>
714
+ id: number
715
+ matchIndex: number
716
+ }
554
717
 
555
- // Match the routes
556
- const matches = router.matchRoutes(router.location.pathname, {
557
- strictParseParams: true,
558
- })
718
+ export interface ExtractedStream extends ExtractedBaseEntry {
719
+ type: 'stream'
720
+ streamState: StreamState
721
+ }
559
722
 
560
- router.state = {
561
- ...router.state,
562
- pending: {
563
- matches: matches,
564
- location: router.location,
565
- },
566
- status: 'loading',
567
- }
723
+ export interface ExtractedPromise extends ExtractedBaseEntry {
724
+ type: 'promise'
725
+ promiseState: DeferredPromiseState<any>
726
+ }
568
727
 
569
- router.notify()
728
+ export type ExtractedEntry = ExtractedStream | ExtractedPromise
570
729
 
571
- // Load the matches
572
- await router.loadMatches(matches, {
573
- withPending: true,
574
- })
730
+ export type StreamState = {
731
+ promises: Array<ControlledPromise<string | null>>
732
+ }
575
733
 
576
- if (router.startedLoadingAt !== id) {
577
- // Ignore side-effects of match loading
578
- return router.navigationPromise
579
- }
734
+ export type TrailingSlashOption = 'always' | 'never' | 'preserve'
735
+
736
+ export function getLocationChangeInfo(routerState: {
737
+ resolvedLocation?: ParsedLocation
738
+ location: ParsedLocation
739
+ }) {
740
+ const fromLocation = routerState.resolvedLocation
741
+ const toLocation = routerState.location
742
+ const pathChanged = fromLocation?.pathname !== toLocation.pathname
743
+ const hrefChanged = fromLocation?.href !== toLocation.href
744
+ const hashChanged = fromLocation?.hash !== toLocation.hash
745
+ return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
746
+ }
580
747
 
581
- const previousMatches = router.state.matches
748
+ export type CreateRouterFn = <
749
+ TRouteTree extends AnyRoute,
750
+ TTrailingSlashOption extends TrailingSlashOption = 'never',
751
+ TDefaultStructuralSharingOption extends boolean = false,
752
+ TRouterHistory extends RouterHistory = RouterHistory,
753
+ TDehydrated extends Record<string, any> = Record<string, any>,
754
+ >(
755
+ options: undefined extends number
756
+ ? 'strictNullChecks must be enabled in tsconfig.json'
757
+ : RouterConstructorOptions<
758
+ TRouteTree,
759
+ TTrailingSlashOption,
760
+ TDefaultStructuralSharingOption,
761
+ TRouterHistory,
762
+ TDehydrated
763
+ >,
764
+ ) => RouterCore<
765
+ TRouteTree,
766
+ TTrailingSlashOption,
767
+ TDefaultStructuralSharingOption,
768
+ TRouterHistory,
769
+ TDehydrated
770
+ >
771
+
772
+ export class RouterCore<
773
+ in out TRouteTree extends AnyRoute,
774
+ in out TTrailingSlashOption extends TrailingSlashOption,
775
+ in out TDefaultStructuralSharingOption extends boolean,
776
+ in out TRouterHistory extends RouterHistory = RouterHistory,
777
+ in out TDehydrated extends Record<string, any> = Record<string, any>,
778
+ > {
779
+ // Option-independent properties
780
+ tempLocationKey: string | undefined = `${Math.round(
781
+ Math.random() * 10000000,
782
+ )}`
783
+ resetNextScroll = true
784
+ shouldViewTransition?: boolean | ViewTransitionOptions = undefined
785
+ isViewTransitionTypesSupported?: boolean = undefined
786
+ subscribers = new Set<RouterListener<RouterEvent>>()
787
+ viewTransitionPromise?: ControlledPromise<true>
788
+ isScrollRestoring = false
789
+ isScrollRestorationSetup = false
790
+
791
+ // Must build in constructor
792
+ __store!: Store<RouterState<TRouteTree>>
793
+ options!: PickAsRequired<
794
+ RouterOptions<
795
+ TRouteTree,
796
+ TTrailingSlashOption,
797
+ TDefaultStructuralSharingOption,
798
+ TRouterHistory,
799
+ TDehydrated
800
+ >,
801
+ 'stringifySearch' | 'parseSearch' | 'context'
802
+ >
803
+ history!: TRouterHistory
804
+ latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
805
+ basepath!: string
806
+ routeTree!: TRouteTree
807
+ routesById!: RoutesById<TRouteTree>
808
+ routesByPath!: RoutesByPath<TRouteTree>
809
+ flatRoutes!: Array<AnyRoute>
810
+ isServer!: boolean
811
+ pathParamsDecodeCharMap?: Map<string, string>
812
+
813
+ /**
814
+ * @deprecated Use the `createRouter` function instead
815
+ */
816
+ constructor(
817
+ options: RouterConstructorOptions<
818
+ TRouteTree,
819
+ TTrailingSlashOption,
820
+ TDefaultStructuralSharingOption,
821
+ TRouterHistory,
822
+ TDehydrated
823
+ >,
824
+ ) {
825
+ this.update({
826
+ defaultPreloadDelay: 50,
827
+ defaultPendingMs: 1000,
828
+ defaultPendingMinMs: 500,
829
+ context: undefined!,
830
+ ...options,
831
+ caseSensitive: options.caseSensitive ?? false,
832
+ notFoundMode: options.notFoundMode ?? 'fuzzy',
833
+ stringifySearch: options.stringifySearch ?? defaultStringifySearch,
834
+ parseSearch: options.parseSearch ?? defaultParseSearch,
835
+ })
836
+
837
+ if (typeof document !== 'undefined') {
838
+ ;(window as any).__TSR_ROUTER__ = this
839
+ }
840
+ }
582
841
 
583
- const exiting: RouteMatch[] = [],
584
- staying: RouteMatch[] = []
842
+ // These are default implementations that can optionally be overridden
843
+ // by the router provider once rendered. We provide these so that the
844
+ // router can be used in a non-react environment if necessary
845
+ startTransition: StartTransitionFn = (fn) => fn()
846
+
847
+ update: UpdateFn<
848
+ TRouteTree,
849
+ TTrailingSlashOption,
850
+ TDefaultStructuralSharingOption,
851
+ TRouterHistory,
852
+ TDehydrated
853
+ > = (newOptions) => {
854
+ if (newOptions.notFoundRoute) {
855
+ console.warn(
856
+ 'The notFoundRoute API is deprecated and will be removed in the next major version. See https://tanstack.com/router/v1/docs/framework/react/guide/not-found-errors#migrating-from-notfoundroute for more info.',
857
+ )
858
+ }
585
859
 
586
- previousMatches.forEach((d) => {
587
- if (matches.find((dd) => dd.matchId === d.matchId)) {
588
- staying.push(d)
589
- } else {
590
- exiting.push(d)
591
- }
592
- })
860
+ const previousOptions = this.options
861
+ this.options = {
862
+ ...this.options,
863
+ ...newOptions,
864
+ }
593
865
 
594
- const now = Date.now()
866
+ this.isServer = this.options.isServer ?? typeof document === 'undefined'
595
867
 
596
- exiting.forEach((d) => {
597
- d.__.onExit?.({
598
- params: d.params,
599
- search: d.routeSearch,
600
- })
601
- // Clear idle error states when match leaves
602
- if (d.status === 'error' && !d.isFetching) {
603
- d.status = 'idle'
604
- d.error = undefined
605
- }
606
- const gc = Math.max(
607
- d.options.loaderGcMaxAge ?? router.options.defaultLoaderGcMaxAge ?? 0,
608
- d.options.loaderMaxAge ?? router.options.defaultLoaderMaxAge ?? 0,
868
+ this.pathParamsDecodeCharMap = this.options.pathParamsAllowedCharacters
869
+ ? new Map(
870
+ this.options.pathParamsAllowedCharacters.map((char) => [
871
+ encodeURIComponent(char),
872
+ char,
873
+ ]),
609
874
  )
610
- if (gc > 0) {
611
- router.matchCache[d.matchId] = {
612
- gc: gc == Infinity ? Number.MAX_SAFE_INTEGER : now + gc,
613
- match: d,
614
- }
615
- }
616
- })
875
+ : undefined
876
+
877
+ if (
878
+ !this.basepath ||
879
+ (newOptions.basepath && newOptions.basepath !== previousOptions.basepath)
880
+ ) {
881
+ if (
882
+ newOptions.basepath === undefined ||
883
+ newOptions.basepath === '' ||
884
+ newOptions.basepath === '/'
885
+ ) {
886
+ this.basepath = '/'
887
+ } else {
888
+ this.basepath = `/${trimPath(newOptions.basepath)}`
889
+ }
890
+ }
617
891
 
618
- staying.forEach((d) => {
619
- d.options.onTransition?.({
620
- params: d.params,
621
- search: d.routeSearch,
622
- })
623
- })
892
+ if (
893
+ !this.history ||
894
+ (this.options.history && this.options.history !== this.history)
895
+ ) {
896
+ this.history =
897
+ this.options.history ??
898
+ ((this.isServer
899
+ ? createMemoryHistory({
900
+ initialEntries: [this.basepath || '/'],
901
+ })
902
+ : createBrowserHistory()) as TRouterHistory)
903
+ this.latestLocation = this.parseLocation()
904
+ }
624
905
 
625
- const entering = matches.filter((d) => {
626
- return !previousMatches.find((dd) => dd.matchId === d.matchId)
627
- })
906
+ if (this.options.routeTree !== this.routeTree) {
907
+ this.routeTree = this.options.routeTree as TRouteTree
908
+ this.buildRouteTree()
909
+ }
628
910
 
629
- entering.forEach((d) => {
630
- d.__.onExit = d.options.onMatch?.({
631
- params: d.params,
632
- search: d.search,
633
- })
634
- delete router.matchCache[d.matchId]
911
+ if (!this.__store) {
912
+ this.__store = new Store(getInitialRouterState(this.latestLocation), {
913
+ onUpdate: () => {
914
+ this.__store.state = {
915
+ ...this.state,
916
+ cachedMatches: this.state.cachedMatches.filter(
917
+ (d) => !['redirected'].includes(d.status),
918
+ ),
919
+ }
920
+ },
635
921
  })
636
922
 
637
- if (matches.some((d) => d.status === 'loading')) {
638
- router.notify()
639
- await Promise.all(
640
- matches.map((d) => d.__.loaderPromise || Promise.resolve()),
641
- )
642
- }
643
- if (router.startedLoadingAt !== id) {
644
- // Ignore side-effects of match loading
645
- return
646
- }
647
-
648
- router.state = {
649
- ...router.state,
650
- location: router.location,
651
- matches,
652
- pending: undefined,
653
- status: 'idle',
654
- }
655
-
656
- router.notify()
657
- router.resolveNavigation()
658
- },
923
+ setupScrollRestoration(this)
924
+ }
659
925
 
660
- cleanMatchCache: () => {
661
- const now = Date.now()
926
+ if (
927
+ typeof window !== 'undefined' &&
928
+ 'CSS' in window &&
929
+ typeof window.CSS?.supports === 'function'
930
+ ) {
931
+ this.isViewTransitionTypesSupported = window.CSS.supports(
932
+ 'selector(:active-view-transition-type(a)',
933
+ )
934
+ }
935
+ }
662
936
 
663
- Object.keys(router.matchCache).forEach((matchId) => {
664
- const entry = router.matchCache[matchId]!
937
+ get state() {
938
+ return this.__store.state
939
+ }
665
940
 
666
- // Don't remove loading matches
667
- if (entry.match.status === 'loading') {
668
- return
669
- }
941
+ buildRouteTree = () => {
942
+ const { routesById, routesByPath, flatRoutes } = processRouteTree({
943
+ routeTree: this.routeTree,
944
+ initRoute: (route, i) => {
945
+ route.init({
946
+ originalIndex: i,
947
+ defaultSsr: this.options.defaultSsr,
948
+ })
949
+ },
950
+ })
670
951
 
671
- // Do not remove successful matches that are still valid
672
- if (entry.gc > 0 && entry.gc > now) {
673
- return
674
- }
952
+ this.routesById = routesById as RoutesById<TRouteTree>
953
+ this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
954
+ this.flatRoutes = flatRoutes as Array<AnyRoute>
675
955
 
676
- // Everything else gets removed
677
- delete router.matchCache[matchId]
678
- })
679
- },
956
+ const notFoundRoute = this.options.notFoundRoute
680
957
 
681
- loadRoute: async (navigateOpts = router.location) => {
682
- const next = router.buildNext(navigateOpts)
683
- const matches = router.matchRoutes(next.pathname, {
684
- strictParseParams: true,
958
+ if (notFoundRoute) {
959
+ notFoundRoute.init({
960
+ originalIndex: 99999999999,
961
+ defaultSsr: this.options.defaultSsr,
685
962
  })
686
- await router.loadMatches(matches)
687
- return matches
688
- },
963
+ this.routesById[notFoundRoute.id] = notFoundRoute
964
+ }
965
+ }
689
966
 
690
- preloadRoute: async (navigateOpts = router.location, loaderOpts) => {
691
- const next = router.buildNext(navigateOpts)
692
- const matches = router.matchRoutes(next.pathname, {
693
- strictParseParams: true,
694
- })
695
- await router.loadMatches(matches, {
696
- preload: true,
697
- maxAge:
698
- loaderOpts.maxAge ??
699
- router.options.defaultPreloadMaxAge ??
700
- router.options.defaultLoaderMaxAge ??
701
- 0,
702
- gcMaxAge:
703
- loaderOpts.gcMaxAge ??
704
- router.options.defaultPreloadGcMaxAge ??
705
- router.options.defaultLoaderGcMaxAge ??
706
- 0,
707
- })
708
- return matches
709
- },
967
+ subscribe: SubscribeFn = (eventType, fn) => {
968
+ const listener: RouterListener<any> = {
969
+ eventType,
970
+ fn,
971
+ }
710
972
 
711
- matchRoutes: (pathname, opts) => {
712
- router.cleanMatchCache()
973
+ this.subscribers.add(listener)
713
974
 
714
- const matches: RouteMatch[] = []
975
+ return () => {
976
+ this.subscribers.delete(listener)
977
+ }
978
+ }
715
979
 
716
- if (!router.routeTree) {
717
- return matches
980
+ emit: EmitFn = (routerEvent) => {
981
+ this.subscribers.forEach((listener) => {
982
+ if (listener.eventType === routerEvent.type) {
983
+ listener.fn(routerEvent)
718
984
  }
985
+ })
986
+ }
719
987
 
720
- const existingMatches = [
721
- ...router.state.matches,
722
- ...(router.state.pending?.matches ?? []),
723
- ]
988
+ parseLocation: ParseLocationFn<TRouteTree> = (
989
+ previousLocation,
990
+ locationToParse,
991
+ ) => {
992
+ const parse = ({
993
+ pathname,
994
+ search,
995
+ hash,
996
+ state,
997
+ }: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
998
+ const parsedSearch = this.options.parseSearch(search)
999
+ const searchStr = this.options.stringifySearch(parsedSearch)
724
1000
 
725
- const recurse = async (routes: Route<any, any>[]): Promise<void> => {
726
- const parentMatch = last(matches)
727
- let params = parentMatch?.params ?? {}
1001
+ return {
1002
+ pathname,
1003
+ searchStr,
1004
+ search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
1005
+ hash: hash.split('#').reverse()[0] ?? '',
1006
+ href: `${pathname}${searchStr}${hash}`,
1007
+ state: replaceEqualDeep(previousLocation?.state, state),
1008
+ }
1009
+ }
728
1010
 
729
- const filteredRoutes = router.options.filterRoutes?.(routes) ?? routes
1011
+ const location = parse(locationToParse ?? this.history.location)
730
1012
 
731
- let foundRoutes: Route[] = []
1013
+ const { __tempLocation, __tempKey } = location.state
732
1014
 
733
- const findMatchInRoutes = (parentRoutes: Route[], routes: Route[]) => {
734
- routes.some((route) => {
735
- if (!route.routePath && route.childRoutes?.length) {
736
- return findMatchInRoutes(
737
- [...foundRoutes, route],
738
- route.childRoutes,
739
- )
740
- }
1015
+ if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
1016
+ // Sync up the location keys
1017
+ const parsedTempLocation = parse(__tempLocation) as any
1018
+ parsedTempLocation.state.key = location.state.key
741
1019
 
742
- const fuzzy = !!(
743
- route.routePath !== '/' || route.childRoutes?.length
744
- )
1020
+ delete parsedTempLocation.state.__tempLocation
745
1021
 
746
- const matchParams = matchPathname(pathname, {
747
- to: route.fullPath,
748
- fuzzy,
749
- caseSensitive:
750
- route.options.caseSensitive ?? router.options.caseSensitive,
751
- })
1022
+ return {
1023
+ ...parsedTempLocation,
1024
+ maskedLocation: location,
1025
+ }
1026
+ }
752
1027
 
753
- if (matchParams) {
754
- let parsedParams
1028
+ return location
1029
+ }
755
1030
 
756
- try {
757
- parsedParams =
758
- route.options.parseParams?.(matchParams!) ?? matchParams
759
- } catch (err) {
760
- if (opts?.strictParseParams) {
761
- throw err
762
- }
763
- }
1031
+ resolvePathWithBase = (from: string, path: string) => {
1032
+ const resolvedPath = resolvePath({
1033
+ basepath: this.basepath,
1034
+ base: from,
1035
+ to: cleanPath(path),
1036
+ trailingSlash: this.options.trailingSlash,
1037
+ caseSensitive: this.options.caseSensitive,
1038
+ })
1039
+ return resolvedPath
1040
+ }
764
1041
 
765
- params = {
766
- ...params,
767
- ...parsedParams,
768
- }
769
- }
1042
+ get looseRoutesById() {
1043
+ return this.routesById as Record<string, AnyRoute>
1044
+ }
770
1045
 
771
- if (!!matchParams) {
772
- foundRoutes = [...parentRoutes, route]
773
- }
1046
+ /**
1047
+ @deprecated use the following signature instead
1048
+ ```ts
1049
+ matchRoutes (
1050
+ next: ParsedLocation,
1051
+ opts?: { preload?: boolean; throwOnError?: boolean },
1052
+ ): Array<AnyRouteMatch>;
1053
+ ```
1054
+ */
1055
+ matchRoutes: MatchRoutesFn = (
1056
+ pathnameOrNext: string | ParsedLocation,
1057
+ locationSearchOrOpts?: AnySchema | MatchRoutesOpts,
1058
+ opts?: MatchRoutesOpts,
1059
+ ) => {
1060
+ if (typeof pathnameOrNext === 'string') {
1061
+ return this.matchRoutesInternal(
1062
+ {
1063
+ pathname: pathnameOrNext,
1064
+ search: locationSearchOrOpts,
1065
+ } as ParsedLocation,
1066
+ opts,
1067
+ )
1068
+ } else {
1069
+ return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
1070
+ }
1071
+ }
774
1072
 
775
- return !!foundRoutes.length
776
- })
1073
+ private matchRoutesInternal(
1074
+ next: ParsedLocation,
1075
+ opts?: MatchRoutesOpts,
1076
+ ): Array<AnyRouteMatch> {
1077
+ const { foundRoute, matchedRoutes, routeParams } = this.getMatchedRoutes(
1078
+ next.pathname,
1079
+ opts?.dest?.to as string,
1080
+ )
1081
+ let isGlobalNotFound = false
1082
+
1083
+ // Check to see if the route needs a 404 entry
1084
+ if (
1085
+ // If we found a route, and it's not an index route and we have left over path
1086
+ foundRoute
1087
+ ? foundRoute.path !== '/' && routeParams['**']
1088
+ : // Or if we didn't find a route and we have left over path
1089
+ trimPathRight(next.pathname)
1090
+ ) {
1091
+ // If the user has defined an (old) 404 route, use it
1092
+ if (this.options.notFoundRoute) {
1093
+ matchedRoutes.push(this.options.notFoundRoute)
1094
+ } else {
1095
+ // If there is no routes found during path matching
1096
+ isGlobalNotFound = true
1097
+ }
1098
+ }
1099
+
1100
+ const globalNotFoundRouteId = (() => {
1101
+ if (!isGlobalNotFound) {
1102
+ return undefined
1103
+ }
777
1104
 
778
- return !!foundRoutes.length
1105
+ if (this.options.notFoundMode !== 'root') {
1106
+ for (let i = matchedRoutes.length - 1; i >= 0; i--) {
1107
+ const route = matchedRoutes[i]!
1108
+ if (route.children) {
1109
+ return route.id
1110
+ }
779
1111
  }
1112
+ }
780
1113
 
781
- findMatchInRoutes([], filteredRoutes)
1114
+ return rootRouteId
1115
+ })()
782
1116
 
783
- if (!foundRoutes.length) {
784
- return
785
- }
1117
+ const parseErrors = matchedRoutes.map((route) => {
1118
+ let parsedParamsError
786
1119
 
787
- foundRoutes.forEach((foundRoute) => {
788
- const interpolatedPath = interpolatePath(foundRoute.routePath, params)
789
- const matchId = interpolatePath(foundRoute.routeId, params, true)
790
-
791
- const match =
792
- existingMatches.find((d) => d.matchId === matchId) ||
793
- router.matchCache[matchId]?.match ||
794
- createRouteMatch(router, foundRoute, {
795
- matchId,
796
- params,
797
- pathname: joinPaths([pathname, interpolatedPath]),
798
- })
1120
+ const parseParams =
1121
+ route.options.params?.parse ?? route.options.parseParams
799
1122
 
800
- matches.push(match)
801
- })
1123
+ if (parseParams) {
1124
+ try {
1125
+ const parsedParams = parseParams(routeParams)
1126
+ // Add the parsed params to the accumulated params bag
1127
+ Object.assign(routeParams, parsedParams)
1128
+ } catch (err: any) {
1129
+ parsedParamsError = new PathParamError(err.message, {
1130
+ cause: err,
1131
+ })
802
1132
 
803
- const foundRoute = last(foundRoutes)!
1133
+ if (opts?.throwOnError) {
1134
+ throw parsedParamsError
1135
+ }
804
1136
 
805
- if (foundRoute.childRoutes?.length) {
806
- recurse(foundRoute.childRoutes)
1137
+ return parsedParamsError
807
1138
  }
808
1139
  }
809
1140
 
810
- recurse([router.routeTree])
811
-
812
- cascadeLoaderData(matches)
813
-
814
- return matches
815
- },
1141
+ return
1142
+ })
816
1143
 
817
- loadMatches: async (resolvedMatches, loaderOpts) => {
818
- const matchPromises = resolvedMatches.map(async (match) => {
819
- // Validate the match (loads search params etc)
820
- match.__.validate()
821
- match.load(loaderOpts)
1144
+ const matches: Array<AnyRouteMatch> = []
822
1145
 
823
- if (match.status === 'loading') {
824
- // If requested, start the pending timers
825
- if (loaderOpts?.withPending) match.__.startPending()
1146
+ const getParentContext = (parentMatch?: AnyRouteMatch) => {
1147
+ const parentMatchId = parentMatch?.id
826
1148
 
827
- // Wait for the first sign of activity from the match
828
- // This might be completion, error, or a pending state
829
- await match.__.loadPromise
830
- }
831
- })
1149
+ const parentContext = !parentMatchId
1150
+ ? ((this.options.context as any) ?? {})
1151
+ : (parentMatch.context ?? this.options.context ?? {})
832
1152
 
833
- router.notify()
834
-
835
- await Promise.all(matchPromises)
836
- },
837
-
838
- invalidateRoute: (opts: MatchLocation) => {
839
- const next = router.buildNext(opts)
840
- const unloadedMatchIds = router
841
- .matchRoutes(next.pathname)
842
- .map((d) => d.matchId)
843
- ;[
844
- ...router.state.matches,
845
- ...(router.state.pending?.matches ?? []),
846
- ].forEach((match) => {
847
- if (unloadedMatchIds.includes(match.matchId)) {
848
- match.invalidate()
849
- }
850
- })
851
- },
1153
+ return parentContext
1154
+ }
852
1155
 
853
- reload: () =>
854
- router.__.navigate({
855
- fromCurrent: true,
856
- replace: true,
857
- search: true,
858
- }),
1156
+ matchedRoutes.forEach((route, index) => {
1157
+ // Take each matched route and resolve + validate its search params
1158
+ // This has to happen serially because each route's search params
1159
+ // can depend on the parent route's search params
1160
+ // It must also happen before we create the match so that we can
1161
+ // pass the search params to the route's potential key function
1162
+ // which is used to uniquely identify the route match in state
1163
+
1164
+ const parentMatch = matches[index - 1]
1165
+
1166
+ const [preMatchSearch, strictMatchSearch, searchError]: [
1167
+ Record<string, any>,
1168
+ Record<string, any>,
1169
+ any,
1170
+ ] = (() => {
1171
+ // Validate the search params and stabilize them
1172
+ const parentSearch = parentMatch?.search ?? next.search
1173
+ const parentStrictSearch = parentMatch?._strictSearch ?? {}
1174
+
1175
+ try {
1176
+ const strictSearch =
1177
+ validateSearch(route.options.validateSearch, { ...parentSearch }) ??
1178
+ {}
1179
+
1180
+ return [
1181
+ {
1182
+ ...parentSearch,
1183
+ ...strictSearch,
1184
+ },
1185
+ { ...parentStrictSearch, ...strictSearch },
1186
+ undefined,
1187
+ ]
1188
+ } catch (err: any) {
1189
+ let searchParamError = err
1190
+ if (!(err instanceof SearchParamError)) {
1191
+ searchParamError = new SearchParamError(err.message, {
1192
+ cause: err,
1193
+ })
1194
+ }
859
1195
 
860
- resolvePath: (from: string, path: string) => {
861
- return resolvePath(router.basepath!, from, cleanPath(path))
862
- },
1196
+ if (opts?.throwOnError) {
1197
+ throw searchParamError
1198
+ }
863
1199
 
864
- matchRoute: (location, opts) => {
865
- // const location = router.buildNext(opts)
1200
+ return [parentSearch, {}, searchParamError]
1201
+ }
1202
+ })()
866
1203
 
867
- location = {
868
- ...location,
869
- to: location.to
870
- ? router.resolvePath(location.from ?? '', location.to)
871
- : undefined,
872
- }
1204
+ // This is where we need to call route.options.loaderDeps() to get any additional
1205
+ // deps that the route's loader function might need to run. We need to do this
1206
+ // before we create the match so that we can pass the deps to the route's
1207
+ // potential key function which is used to uniquely identify the route match in state
873
1208
 
874
- const next = router.buildNext(location)
1209
+ const loaderDeps =
1210
+ route.options.loaderDeps?.({
1211
+ search: preMatchSearch,
1212
+ }) ?? ''
875
1213
 
876
- if (opts?.pending) {
877
- if (!router.state.pending?.location) {
878
- return false
879
- }
880
- return !!matchPathname(router.state.pending.location.pathname, {
881
- ...opts,
882
- to: next.pathname,
883
- })
884
- }
1214
+ const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : ''
885
1215
 
886
- return !!matchPathname(router.state.location.pathname, {
887
- ...opts,
888
- to: next.pathname,
1216
+ const { usedParams, interpolatedPath } = interpolatePath({
1217
+ path: route.fullPath,
1218
+ params: routeParams,
1219
+ decodeCharMap: this.pathParamsDecodeCharMap,
889
1220
  })
890
- },
891
1221
 
892
- navigate: async ({ from, to = '.', search, hash, replace, params }) => {
893
- // If this link simply reloads the current route,
894
- // make sure it has a new key so it will trigger a data refresh
1222
+ const matchId =
1223
+ interpolatePath({
1224
+ path: route.id,
1225
+ params: routeParams,
1226
+ leaveWildcards: true,
1227
+ decodeCharMap: this.pathParamsDecodeCharMap,
1228
+ }).interpolatedPath + loaderDepsHash
895
1229
 
896
- // If this `to` is a valid external URL, return
897
- // null for LinkUtils
898
- const toString = String(to)
899
- const fromString = String(from)
1230
+ // Waste not, want not. If we already have a match for this route,
1231
+ // reuse it. This is important for layout routes, which might stick
1232
+ // around between navigation actions that only change leaf routes.
900
1233
 
901
- let isExternal
902
-
903
- try {
904
- new URL(`${toString}`)
905
- isExternal = true
906
- } catch (e) {}
1234
+ // Existing matches are matches that are already loaded along with
1235
+ // pending matches that are still loading
1236
+ const existingMatch = this.getMatch(matchId)
907
1237
 
908
- invariant(
909
- !isExternal,
910
- 'Attempting to navigate to external url with router.navigate!',
1238
+ const previousMatch = this.state.matches.find(
1239
+ (d) => d.routeId === route.id,
911
1240
  )
912
1241
 
913
- return router.__.navigate({
914
- from: fromString,
915
- to: toString,
916
- search,
917
- hash,
918
- replace,
919
- params,
920
- })
921
- },
922
-
923
- buildLink: ({
924
- from,
925
- to = '.',
926
- search,
927
- params,
928
- hash,
929
- target,
930
- replace,
931
- activeOptions,
932
- preload,
933
- preloadMaxAge: userPreloadMaxAge,
934
- preloadGcMaxAge: userPreloadGcMaxAge,
935
- preloadDelay: userPreloadDelay,
936
- disabled,
937
- }) => {
938
- // If this link simply reloads the current route,
939
- // make sure it has a new key so it will trigger a data refresh
940
-
941
- // If this `to` is a valid external URL, return
942
- // null for LinkUtils
943
-
944
- try {
945
- new URL(`${to}`)
946
- return {
947
- type: 'external',
948
- href: to,
1242
+ const cause = previousMatch ? 'stay' : 'enter'
1243
+
1244
+ let match: AnyRouteMatch
1245
+
1246
+ if (existingMatch) {
1247
+ match = {
1248
+ ...existingMatch,
1249
+ cause,
1250
+ params: previousMatch
1251
+ ? replaceEqualDeep(previousMatch.params, routeParams)
1252
+ : routeParams,
1253
+ _strictParams: usedParams,
1254
+ search: previousMatch
1255
+ ? replaceEqualDeep(previousMatch.search, preMatchSearch)
1256
+ : replaceEqualDeep(existingMatch.search, preMatchSearch),
1257
+ _strictSearch: strictMatchSearch,
949
1258
  }
950
- } catch (e) {}
1259
+ } else {
1260
+ const status =
1261
+ route.options.loader ||
1262
+ route.options.beforeLoad ||
1263
+ route.lazyFn ||
1264
+ routeNeedsPreload(route)
1265
+ ? 'pending'
1266
+ : 'success'
1267
+
1268
+ match = {
1269
+ id: matchId,
1270
+ index,
1271
+ routeId: route.id,
1272
+ params: previousMatch
1273
+ ? replaceEqualDeep(previousMatch.params, routeParams)
1274
+ : routeParams,
1275
+ _strictParams: usedParams,
1276
+ pathname: joinPaths([this.basepath, interpolatedPath]),
1277
+ updatedAt: Date.now(),
1278
+ search: previousMatch
1279
+ ? replaceEqualDeep(previousMatch.search, preMatchSearch)
1280
+ : preMatchSearch,
1281
+ _strictSearch: strictMatchSearch,
1282
+ searchError: undefined,
1283
+ status,
1284
+ isFetching: false,
1285
+ error: undefined,
1286
+ paramsError: parseErrors[index],
1287
+ __routeContext: {},
1288
+ __beforeLoadContext: {},
1289
+ context: {},
1290
+ abortController: new AbortController(),
1291
+ fetchCount: 0,
1292
+ cause,
1293
+ loaderDeps: previousMatch
1294
+ ? replaceEqualDeep(previousMatch.loaderDeps, loaderDeps)
1295
+ : loaderDeps,
1296
+ invalid: false,
1297
+ preload: false,
1298
+ links: undefined,
1299
+ scripts: undefined,
1300
+ headScripts: undefined,
1301
+ meta: undefined,
1302
+ staticData: route.options.staticData || {},
1303
+ loadPromise: createControlledPromise(),
1304
+ fullPath: route.fullPath,
1305
+ }
1306
+ }
951
1307
 
952
- const nextOpts = {
953
- from,
954
- to,
955
- search,
956
- params,
957
- hash,
958
- replace,
1308
+ if (!opts?.preload) {
1309
+ // If we have a global not found, mark the right match as global not found
1310
+ match.globalNotFound = globalNotFoundRouteId === route.id
959
1311
  }
960
1312
 
961
- const next = router.buildNext(nextOpts)
1313
+ // update the searchError if there is one
1314
+ match.searchError = searchError
962
1315
 
963
- preload = preload ?? router.options.defaultPreload
964
- const preloadDelay =
965
- userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0
1316
+ const parentContext = getParentContext(parentMatch)
966
1317
 
967
- // Compare path/hash for matches
968
- const pathIsEqual = router.state.location.pathname === next.pathname
969
- const currentPathSplit = router.state.location.pathname.split('/')
970
- const nextPathSplit = next.pathname.split('/')
971
- const pathIsFuzzyEqual = nextPathSplit.every(
972
- (d, i) => d === currentPathSplit[i],
973
- )
974
- const hashIsEqual = router.state.location.hash === next.hash
975
- // Combine the matches based on user options
976
- const pathTest = activeOptions?.exact ? pathIsEqual : pathIsFuzzyEqual
977
- const hashTest = activeOptions?.includeHash ? hashIsEqual : true
1318
+ match.context = {
1319
+ ...parentContext,
1320
+ ...match.__routeContext,
1321
+ ...match.__beforeLoadContext,
1322
+ }
978
1323
 
979
- // The final "active" test
980
- const isActive = pathTest && hashTest
1324
+ matches.push(match)
1325
+ })
1326
+
1327
+ matches.forEach((match, index) => {
1328
+ const route = this.looseRoutesById[match.routeId]!
1329
+ const existingMatch = this.getMatch(match.id)
1330
+
1331
+ // only execute `context` if we are not just building a location
1332
+ if (!existingMatch && opts?._buildLocation !== true) {
1333
+ const parentMatch = matches[index - 1]
1334
+ const parentContext = getParentContext(parentMatch)
1335
+
1336
+ // Update the match's context
1337
+ const contextFnContext: RouteContextOptions<any, any, any, any> = {
1338
+ deps: match.loaderDeps,
1339
+ params: match.params,
1340
+ context: parentContext,
1341
+ location: next,
1342
+ navigate: (opts: any) =>
1343
+ this.navigate({ ...opts, _fromLocation: next }),
1344
+ buildLocation: this.buildLocation,
1345
+ cause: match.cause,
1346
+ abortController: match.abortController,
1347
+ preload: !!match.preload,
1348
+ matches,
1349
+ }
981
1350
 
982
- // The click handler
983
- const handleClick = (e: MouseEvent) => {
984
- if (
985
- !disabled &&
986
- !isCtrlEvent(e) &&
987
- !e.defaultPrevented &&
988
- (!target || target === '_self') &&
989
- e.button === 0
990
- ) {
991
- e.preventDefault()
992
- if (pathIsEqual && !search && !hash) {
993
- router.invalidateRoute(nextOpts)
994
- }
1351
+ // Get the route context
1352
+ match.__routeContext = route.options.context?.(contextFnContext) ?? {}
995
1353
 
996
- // All is well? Navigate!)
997
- router.__.navigate(nextOpts)
1354
+ match.context = {
1355
+ ...parentContext,
1356
+ ...match.__routeContext,
1357
+ ...match.__beforeLoadContext,
998
1358
  }
999
1359
  }
1000
1360
 
1001
- // The click handler
1002
- const handleFocus = (e: MouseEvent) => {
1003
- if (preload) {
1004
- router.preloadRoute(nextOpts, {
1005
- maxAge: userPreloadMaxAge,
1006
- gcMaxAge: userPreloadGcMaxAge,
1007
- })
1361
+ // If it's already a success, update headers and head content
1362
+ // These may get updated again if the match is refreshed
1363
+ // due to being stale
1364
+ if (match.status === 'success') {
1365
+ match.headers = route.options.headers?.({
1366
+ loaderData: match.loaderData,
1367
+ })
1368
+ const assetContext = {
1369
+ matches,
1370
+ match,
1371
+ params: match.params,
1372
+ loaderData: match.loaderData,
1008
1373
  }
1374
+ const headFnContent = route.options.head?.(assetContext)
1375
+ match.links = headFnContent?.links
1376
+ match.headScripts = headFnContent?.scripts
1377
+ match.meta = headFnContent?.meta
1378
+ match.scripts = route.options.scripts?.(assetContext)
1009
1379
  }
1380
+ })
1010
1381
 
1011
- const handleEnter = (e: MouseEvent) => {
1012
- const target = (e.target || {}) as LinkCurrentTargetElement
1382
+ return matches
1383
+ }
1013
1384
 
1014
- if (preload) {
1015
- if (target.preloadTimeout) {
1016
- return
1017
- }
1385
+ getMatchedRoutes: GetMatchRoutesFn = (
1386
+ pathname: string,
1387
+ routePathname: string | undefined,
1388
+ ) => {
1389
+ return getMatchedRoutes({
1390
+ pathname,
1391
+ routePathname,
1392
+ basepath: this.basepath,
1393
+ caseSensitive: this.options.caseSensitive,
1394
+ routesByPath: this.routesByPath,
1395
+ routesById: this.routesById,
1396
+ flatRoutes: this.flatRoutes,
1397
+ })
1398
+ }
1018
1399
 
1019
- target.preloadTimeout = setTimeout(() => {
1020
- target.preloadTimeout = null
1021
- router.preloadRoute(nextOpts, {
1022
- maxAge: userPreloadMaxAge,
1023
- gcMaxAge: userPreloadGcMaxAge,
1024
- })
1025
- }, preloadDelay)
1026
- }
1027
- }
1400
+ cancelMatch = (id: string) => {
1401
+ const match = this.getMatch(id)
1028
1402
 
1029
- const handleLeave = (e: MouseEvent) => {
1030
- const target = (e.target || {}) as LinkCurrentTargetElement
1403
+ if (!match) return
1031
1404
 
1032
- if (target.preloadTimeout) {
1033
- clearTimeout(target.preloadTimeout)
1034
- target.preloadTimeout = null
1035
- }
1036
- }
1405
+ match.abortController.abort()
1406
+ clearTimeout(match.pendingTimeout)
1407
+ }
1037
1408
 
1038
- return {
1039
- type: 'internal',
1040
- next,
1041
- handleFocus,
1042
- handleClick,
1043
- handleEnter,
1044
- handleLeave,
1045
- isActive,
1046
- disabled,
1409
+ cancelMatches = () => {
1410
+ this.state.pendingMatches?.forEach((match) => {
1411
+ this.cancelMatch(match.id)
1412
+ })
1413
+ }
1414
+
1415
+ buildLocation: BuildLocationFn = (opts) => {
1416
+ const build = (
1417
+ dest: BuildNextOptions & {
1418
+ unmaskOnReload?: boolean
1419
+ } = {},
1420
+ matchedRoutesResult?: MatchedRoutesResult,
1421
+ ): ParsedLocation => {
1422
+ const fromMatches = dest._fromLocation
1423
+ ? this.matchRoutes(dest._fromLocation, { _buildLocation: true })
1424
+ : this.state.matches
1425
+
1426
+ const fromMatch =
1427
+ dest.from != null
1428
+ ? fromMatches.find((d) =>
1429
+ matchPathname(this.basepath, trimPathRight(d.pathname), {
1430
+ to: dest.from,
1431
+ caseSensitive: false,
1432
+ fuzzy: false,
1433
+ }),
1434
+ )
1435
+ : undefined
1436
+
1437
+ const fromPath = fromMatch?.pathname || this.latestLocation.pathname
1438
+
1439
+ invariant(
1440
+ dest.from == null || fromMatch != null,
1441
+ 'Could not find match for from: ' + dest.from,
1442
+ )
1443
+
1444
+ const fromSearch = this.state.pendingMatches?.length
1445
+ ? last(this.state.pendingMatches)?.search
1446
+ : last(fromMatches)?.search || this.latestLocation.search
1447
+
1448
+ const stayingMatches = matchedRoutesResult?.matchedRoutes.filter((d) =>
1449
+ fromMatches.find((e) => e.routeId === d.id),
1450
+ )
1451
+ let pathname: string
1452
+ if (dest.to) {
1453
+ const resolvePathTo =
1454
+ fromMatch?.fullPath ||
1455
+ last(fromMatches)?.fullPath ||
1456
+ this.latestLocation.pathname
1457
+ pathname = this.resolvePathWithBase(resolvePathTo, `${dest.to}`)
1458
+ } else {
1459
+ const fromRouteByFromPathRouteId =
1460
+ this.routesById[
1461
+ stayingMatches?.find((route) => {
1462
+ const interpolatedPath = interpolatePath({
1463
+ path: route.fullPath,
1464
+ params: matchedRoutesResult?.routeParams ?? {},
1465
+ decodeCharMap: this.pathParamsDecodeCharMap,
1466
+ }).interpolatedPath
1467
+ const pathname = joinPaths([this.basepath, interpolatedPath])
1468
+ return pathname === fromPath
1469
+ })?.id as keyof this['routesById']
1470
+ ]
1471
+ pathname = this.resolvePathWithBase(
1472
+ fromPath,
1473
+ fromRouteByFromPathRouteId?.to ?? fromPath,
1474
+ )
1047
1475
  }
1048
- },
1049
- buildNext: (opts: BuildNextOptions) => {
1050
- const next = router.__.buildLocation(opts)
1051
-
1052
- const matches = router.matchRoutes(next.pathname)
1053
-
1054
- const __preSearchFilters = matches
1055
- .map((match) => match.options.preSearchFilters ?? [])
1056
- .flat()
1057
- .filter(Boolean)
1058
-
1059
- const __postSearchFilters = matches
1060
- .map((match) => match.options.postSearchFilters ?? [])
1061
- .flat()
1062
- .filter(Boolean)
1063
-
1064
- return router.__.buildLocation({
1065
- ...opts,
1066
- __preSearchFilters,
1067
- __postSearchFilters,
1068
- })
1069
- },
1070
-
1071
- __: {
1072
- buildRouteTree: (rootRouteConfig: RouteConfig) => {
1073
- const recurseRoutes = (
1074
- routeConfigs: RouteConfig[],
1075
- parent?: Route<TAllRouteInfo, any>,
1076
- ): Route<TAllRouteInfo, any>[] => {
1077
- return routeConfigs.map((routeConfig) => {
1078
- const routeOptions = routeConfig.options
1079
- const route = createRoute(routeConfig, routeOptions, parent, router)
1080
- const existingRoute = (router.routesById as any)[route.routeId]
1081
-
1082
- if (existingRoute) {
1083
- if (process.env.NODE_ENV !== 'production') {
1084
- console.warn(
1085
- `Duplicate routes found with id: ${String(route.routeId)}`,
1086
- router.routesById,
1087
- route,
1088
- )
1476
+
1477
+ const prevParams = { ...last(fromMatches)?.params }
1478
+
1479
+ let nextParams =
1480
+ (dest.params ?? true) === true
1481
+ ? prevParams
1482
+ : {
1483
+ ...prevParams,
1484
+ ...functionalUpdate(dest.params as any, prevParams),
1485
+ }
1486
+
1487
+ if (Object.keys(nextParams).length > 0) {
1488
+ matchedRoutesResult?.matchedRoutes
1489
+ .map((route) => {
1490
+ return (
1491
+ route.options.params?.stringify ?? route.options.stringifyParams
1492
+ )
1493
+ })
1494
+ .filter(Boolean)
1495
+ .forEach((fn) => {
1496
+ nextParams = { ...nextParams!, ...fn!(nextParams) }
1497
+ })
1498
+ }
1499
+
1500
+ pathname = interpolatePath({
1501
+ path: pathname,
1502
+ params: nextParams ?? {},
1503
+ leaveWildcards: false,
1504
+ leaveParams: opts.leaveParams,
1505
+ decodeCharMap: this.pathParamsDecodeCharMap,
1506
+ }).interpolatedPath
1507
+
1508
+ let search = fromSearch
1509
+ if (opts._includeValidateSearch && this.options.search?.strict) {
1510
+ let validatedSearch = {}
1511
+ matchedRoutesResult?.matchedRoutes.forEach((route) => {
1512
+ try {
1513
+ if (route.options.validateSearch) {
1514
+ validatedSearch = {
1515
+ ...validatedSearch,
1516
+ ...(validateSearch(route.options.validateSearch, {
1517
+ ...validatedSearch,
1518
+ ...search,
1519
+ }) ?? {}),
1089
1520
  }
1090
- throw new Error()
1091
1521
  }
1522
+ } catch {
1523
+ // ignore errors here because they are already handled in matchRoutes
1524
+ }
1525
+ })
1526
+ search = validatedSearch
1527
+ }
1092
1528
 
1093
- ;(router.routesById as any)[route.routeId] = route
1529
+ const applyMiddlewares = (search: any) => {
1530
+ const allMiddlewares =
1531
+ matchedRoutesResult?.matchedRoutes.reduce(
1532
+ (acc, route) => {
1533
+ const middlewares: Array<SearchMiddleware<any>> = []
1534
+ if ('search' in route.options) {
1535
+ if (route.options.search?.middlewares) {
1536
+ middlewares.push(...route.options.search.middlewares)
1537
+ }
1538
+ }
1539
+ // TODO remove preSearchFilters and postSearchFilters in v2
1540
+ else if (
1541
+ route.options.preSearchFilters ||
1542
+ route.options.postSearchFilters
1543
+ ) {
1544
+ const legacyMiddleware: SearchMiddleware<any> = ({
1545
+ search,
1546
+ next,
1547
+ }) => {
1548
+ let nextSearch = search
1549
+ if (
1550
+ 'preSearchFilters' in route.options &&
1551
+ route.options.preSearchFilters
1552
+ ) {
1553
+ nextSearch = route.options.preSearchFilters.reduce(
1554
+ (prev, next) => next(prev),
1555
+ search,
1556
+ )
1557
+ }
1558
+ const result = next(nextSearch)
1559
+ if (
1560
+ 'postSearchFilters' in route.options &&
1561
+ route.options.postSearchFilters
1562
+ ) {
1563
+ return route.options.postSearchFilters.reduce(
1564
+ (prev, next) => next(prev),
1565
+ result,
1566
+ )
1567
+ }
1568
+ return result
1569
+ }
1570
+ middlewares.push(legacyMiddleware)
1571
+ }
1572
+ if (opts._includeValidateSearch && route.options.validateSearch) {
1573
+ const validate: SearchMiddleware<any> = ({ search, next }) => {
1574
+ const result = next(search)
1575
+ try {
1576
+ const validatedSearch = {
1577
+ ...result,
1578
+ ...(validateSearch(
1579
+ route.options.validateSearch,
1580
+ result,
1581
+ ) ?? {}),
1582
+ }
1583
+ return validatedSearch
1584
+ } catch {
1585
+ // ignore errors here because they are already handled in matchRoutes
1586
+ return result
1587
+ }
1588
+ }
1589
+ middlewares.push(validate)
1590
+ }
1591
+ return acc.concat(middlewares)
1592
+ },
1593
+ [] as Array<SearchMiddleware<any>>,
1594
+ ) ?? []
1094
1595
 
1095
- const children = routeConfig.children as RouteConfig[]
1596
+ // the chain ends here since `next` is not called
1597
+ const final: SearchMiddleware<any> = ({ search }) => {
1598
+ if (!dest.search) {
1599
+ return {}
1600
+ }
1601
+ if (dest.search === true) {
1602
+ return search
1603
+ }
1604
+ return functionalUpdate(dest.search, search)
1605
+ }
1606
+ allMiddlewares.push(final)
1096
1607
 
1097
- route.childRoutes = children?.length
1098
- ? recurseRoutes(children, route)
1099
- : undefined
1608
+ const applyNext = (index: number, currentSearch: any): any => {
1609
+ // no more middlewares left, return the current search
1610
+ if (index >= allMiddlewares.length) {
1611
+ return currentSearch
1612
+ }
1100
1613
 
1101
- return route
1102
- })
1614
+ const middleware = allMiddlewares[index]!
1615
+
1616
+ const next = (newSearch: any): any => {
1617
+ return applyNext(index + 1, newSearch)
1618
+ }
1619
+
1620
+ return middleware({ search: currentSearch, next })
1103
1621
  }
1104
1622
 
1105
- const routes = recurseRoutes([rootRouteConfig])
1623
+ // Start applying middlewares
1624
+ return applyNext(0, search)
1625
+ }
1106
1626
 
1107
- return routes[0]!
1108
- },
1627
+ search = applyMiddlewares(search)
1109
1628
 
1110
- parseLocation: (
1111
- location: History['location'],
1112
- previousLocation?: Location,
1113
- ): Location => {
1114
- const parsedSearch = router.options.parseSearch(location.search)
1629
+ search = replaceEqualDeep(fromSearch, search)
1630
+ const searchStr = this.options.stringifySearch(search)
1115
1631
 
1116
- return {
1117
- pathname: location.pathname,
1118
- searchStr: location.search,
1119
- search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1120
- hash: location.hash.split('#').reverse()[0] ?? '',
1121
- href: `${location.pathname}${location.search}${location.hash}`,
1122
- state: location.state as LocationState,
1123
- key: location.key,
1632
+ const hash =
1633
+ dest.hash === true
1634
+ ? this.latestLocation.hash
1635
+ : dest.hash
1636
+ ? functionalUpdate(dest.hash, this.latestLocation.hash)
1637
+ : undefined
1638
+
1639
+ const hashStr = hash ? `#${hash}` : ''
1640
+
1641
+ let nextState =
1642
+ dest.state === true
1643
+ ? this.latestLocation.state
1644
+ : dest.state
1645
+ ? functionalUpdate(dest.state, this.latestLocation.state)
1646
+ : {}
1647
+
1648
+ nextState = replaceEqualDeep(this.latestLocation.state, nextState)
1649
+
1650
+ return {
1651
+ pathname,
1652
+ search,
1653
+ searchStr,
1654
+ state: nextState as any,
1655
+ hash: hash ?? '',
1656
+ href: `${pathname}${searchStr}${hashStr}`,
1657
+ unmaskOnReload: dest.unmaskOnReload,
1658
+ }
1659
+ }
1660
+
1661
+ const buildWithMatches = (
1662
+ dest: BuildNextOptions = {},
1663
+ maskedDest?: BuildNextOptions,
1664
+ ) => {
1665
+ const next = build(dest)
1666
+ let maskedNext = maskedDest ? build(maskedDest) : undefined
1667
+
1668
+ if (!maskedNext) {
1669
+ let params = {}
1670
+
1671
+ const foundMask = this.options.routeMasks?.find((d) => {
1672
+ const match = matchPathname(this.basepath, next.pathname, {
1673
+ to: d.from,
1674
+ caseSensitive: false,
1675
+ fuzzy: false,
1676
+ })
1677
+
1678
+ if (match) {
1679
+ params = match
1680
+ return true
1681
+ }
1682
+
1683
+ return false
1684
+ })
1685
+
1686
+ if (foundMask) {
1687
+ const { from: _from, ...maskProps } = foundMask
1688
+ maskedDest = {
1689
+ ...pick(opts, ['from']),
1690
+ ...maskProps,
1691
+ params,
1692
+ }
1693
+ maskedNext = build(maskedDest)
1124
1694
  }
1125
- },
1695
+ }
1126
1696
 
1127
- navigate: (location: BuildNextOptions & { replace?: boolean }) => {
1128
- const next = router.buildNext(location)
1129
- return router.__.commitLocation(next, location.replace)
1130
- },
1697
+ const nextMatches = this.getMatchedRoutes(
1698
+ next.pathname,
1699
+ dest.to as string,
1700
+ )
1701
+ const final = build(dest, nextMatches)
1131
1702
 
1132
- buildLocation: (dest: BuildNextOptions = {}): Location => {
1133
- // const resolvedFrom: Location = {
1134
- // ...router.location,
1135
- const fromPathname = dest.fromCurrent
1136
- ? router.location.pathname
1137
- : dest.from ?? router.location.pathname
1138
-
1139
- let pathname = resolvePath(
1140
- router.basepath ?? '/',
1141
- fromPathname,
1142
- `${dest.to ?? '.'}`,
1703
+ if (maskedNext) {
1704
+ const maskedMatches = this.getMatchedRoutes(
1705
+ maskedNext.pathname,
1706
+ maskedDest?.to as string,
1143
1707
  )
1708
+ const maskedFinal = build(maskedDest, maskedMatches)
1709
+ final.maskedLocation = maskedFinal
1710
+ }
1144
1711
 
1145
- const fromMatches = router.matchRoutes(router.location.pathname, {
1146
- strictParseParams: true,
1147
- })
1712
+ return final
1713
+ }
1714
+
1715
+ if (opts.mask) {
1716
+ return buildWithMatches(opts, {
1717
+ ...pick(opts, ['from']),
1718
+ ...opts.mask,
1719
+ })
1720
+ }
1721
+
1722
+ return buildWithMatches(opts)
1723
+ }
1148
1724
 
1149
- const toMatches = router.matchRoutes(pathname)
1725
+ commitLocationPromise: undefined | ControlledPromise<void>
1726
+
1727
+ commitLocation: CommitLocationFn = ({
1728
+ viewTransition,
1729
+ ignoreBlocker,
1730
+ ...next
1731
+ }) => {
1732
+ const isSameState = () => {
1733
+ // the following props are ignored but may still be provided when navigating,
1734
+ // temporarily add the previous values to the next state so they don't affect
1735
+ // the comparison
1736
+ const ignoredProps = [
1737
+ 'key',
1738
+ '__TSR_index',
1739
+ '__hashScrollIntoViewOptions',
1740
+ ] as const
1741
+ ignoredProps.forEach((prop) => {
1742
+ ;(next.state as any)[prop] = this.latestLocation.state[prop]
1743
+ })
1744
+ const isEqual = deepEqual(next.state, this.latestLocation.state)
1745
+ ignoredProps.forEach((prop) => {
1746
+ delete next.state[prop]
1747
+ })
1748
+ return isEqual
1749
+ }
1150
1750
 
1151
- const prevParams = { ...last(fromMatches)?.params }
1751
+ const isSameUrl = this.latestLocation.href === next.href
1752
+
1753
+ const previousCommitPromise = this.commitLocationPromise
1754
+ this.commitLocationPromise = createControlledPromise<void>(() => {
1755
+ previousCommitPromise?.resolve()
1756
+ })
1757
+
1758
+ // Don't commit to history if nothing changed
1759
+ if (isSameUrl && isSameState()) {
1760
+ this.load()
1761
+ } else {
1762
+ // eslint-disable-next-line prefer-const
1763
+ let { maskedLocation, hashScrollIntoView, ...nextHistory } = next
1764
+
1765
+ if (maskedLocation) {
1766
+ nextHistory = {
1767
+ ...maskedLocation,
1768
+ state: {
1769
+ ...maskedLocation.state,
1770
+ __tempKey: undefined,
1771
+ __tempLocation: {
1772
+ ...nextHistory,
1773
+ search: nextHistory.searchStr,
1774
+ state: {
1775
+ ...nextHistory.state,
1776
+ __tempKey: undefined!,
1777
+ __tempLocation: undefined!,
1778
+ key: undefined!,
1779
+ },
1780
+ },
1781
+ },
1782
+ }
1152
1783
 
1153
- let nextParams =
1154
- (dest.params ?? true) === true
1155
- ? prevParams
1156
- : functionalUpdate(dest.params!, prevParams)
1784
+ if (
1785
+ nextHistory.unmaskOnReload ??
1786
+ this.options.unmaskOnReload ??
1787
+ false
1788
+ ) {
1789
+ nextHistory.state.__tempKey = this.tempLocationKey
1790
+ }
1791
+ }
1157
1792
 
1158
- if (nextParams) {
1159
- toMatches
1160
- .map((d) => d.options.stringifyParams)
1161
- .filter(Boolean)
1162
- .forEach((fn) => {
1163
- Object.assign({}, nextParams!, fn!(nextParams!))
1793
+ nextHistory.state.__hashScrollIntoViewOptions =
1794
+ hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true
1795
+
1796
+ this.shouldViewTransition = viewTransition
1797
+
1798
+ this.history[next.replace ? 'replace' : 'push'](
1799
+ nextHistory.href,
1800
+ nextHistory.state,
1801
+ { ignoreBlocker },
1802
+ )
1803
+ }
1804
+
1805
+ this.resetNextScroll = next.resetScroll ?? true
1806
+
1807
+ if (!this.history.subscribers.size) {
1808
+ this.load()
1809
+ }
1810
+
1811
+ return this.commitLocationPromise
1812
+ }
1813
+
1814
+ buildAndCommitLocation = ({
1815
+ replace,
1816
+ resetScroll,
1817
+ hashScrollIntoView,
1818
+ viewTransition,
1819
+ ignoreBlocker,
1820
+ href,
1821
+ ...rest
1822
+ }: BuildNextOptions & CommitLocationOptions = {}) => {
1823
+ if (href) {
1824
+ const currentIndex = this.history.location.state.__TSR_index
1825
+ const parsed = parseHref(href, {
1826
+ __TSR_index: replace ? currentIndex : currentIndex + 1,
1827
+ })
1828
+ rest.to = parsed.pathname
1829
+ rest.search = this.options.parseSearch(parsed.search)
1830
+ // remove the leading `#` from the hash
1831
+ rest.hash = parsed.hash.slice(1)
1832
+ }
1833
+
1834
+ const location = this.buildLocation({
1835
+ ...(rest as any),
1836
+ _includeValidateSearch: true,
1837
+ })
1838
+ return this.commitLocation({
1839
+ ...location,
1840
+ viewTransition,
1841
+ replace,
1842
+ resetScroll,
1843
+ hashScrollIntoView,
1844
+ ignoreBlocker,
1845
+ })
1846
+ }
1847
+
1848
+ navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
1849
+ if (!reloadDocument && href) {
1850
+ try {
1851
+ new URL(`${href}`)
1852
+ reloadDocument = true
1853
+ } catch {}
1854
+ }
1855
+
1856
+ if (reloadDocument) {
1857
+ if (!href) {
1858
+ const location = this.buildLocation({ to, ...rest } as any)
1859
+ href = this.history.createHref(location.href)
1860
+ }
1861
+ if (rest.replace) {
1862
+ window.location.replace(href)
1863
+ } else {
1864
+ window.location.href = href
1865
+ }
1866
+ return
1867
+ }
1868
+
1869
+ return this.buildAndCommitLocation({
1870
+ ...rest,
1871
+ href,
1872
+ to: to as string,
1873
+ })
1874
+ }
1875
+
1876
+ latestLoadPromise: undefined | Promise<void>
1877
+
1878
+ beforeLoad = () => {
1879
+ // Cancel any pending matches
1880
+ this.cancelMatches()
1881
+ this.latestLocation = this.parseLocation(this.latestLocation)
1882
+
1883
+ // Match the routes
1884
+ const pendingMatches = this.matchRoutes(this.latestLocation)
1885
+
1886
+ // Ingest the new matches
1887
+ this.__store.setState((s) => ({
1888
+ ...s,
1889
+ status: 'pending',
1890
+ isLoading: true,
1891
+ location: this.latestLocation,
1892
+ pendingMatches,
1893
+ // If a cached moved to pendingMatches, remove it from cachedMatches
1894
+ cachedMatches: s.cachedMatches.filter((d) => {
1895
+ return !pendingMatches.find((e) => e.id === d.id)
1896
+ }),
1897
+ }))
1898
+ }
1899
+
1900
+ load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
1901
+ let redirect: AnyRedirect | undefined
1902
+ let notFound: NotFoundError | undefined
1903
+
1904
+ let loadPromise: Promise<void>
1905
+
1906
+ // eslint-disable-next-line prefer-const
1907
+ loadPromise = new Promise<void>((resolve) => {
1908
+ this.startTransition(async () => {
1909
+ try {
1910
+ this.beforeLoad()
1911
+ const next = this.latestLocation
1912
+ const prevLocation = this.state.resolvedLocation
1913
+
1914
+ if (!this.state.redirect) {
1915
+ this.emit({
1916
+ type: 'onBeforeNavigate',
1917
+ ...getLocationChangeInfo({
1918
+ resolvedLocation: prevLocation,
1919
+ location: next,
1920
+ }),
1164
1921
  })
1922
+ }
1923
+
1924
+ this.emit({
1925
+ type: 'onBeforeLoad',
1926
+ ...getLocationChangeInfo({
1927
+ resolvedLocation: prevLocation,
1928
+ location: next,
1929
+ }),
1930
+ })
1931
+
1932
+ await this.loadMatches({
1933
+ sync: opts?.sync,
1934
+ matches: this.state.pendingMatches as Array<AnyRouteMatch>,
1935
+ location: next,
1936
+ // eslint-disable-next-line @typescript-eslint/require-await
1937
+ onReady: async () => {
1938
+ // eslint-disable-next-line @typescript-eslint/require-await
1939
+ this.startViewTransition(async () => {
1940
+ // this.viewTransitionPromise = createControlledPromise<true>()
1941
+
1942
+ // Commit the pending matches. If a previous match was
1943
+ // removed, place it in the cachedMatches
1944
+ let exitingMatches!: Array<AnyRouteMatch>
1945
+ let enteringMatches!: Array<AnyRouteMatch>
1946
+ let stayingMatches!: Array<AnyRouteMatch>
1947
+
1948
+ batch(() => {
1949
+ this.__store.setState((s) => {
1950
+ const previousMatches = s.matches
1951
+ const newMatches = s.pendingMatches || s.matches
1952
+
1953
+ exitingMatches = previousMatches.filter(
1954
+ (match) => !newMatches.find((d) => d.id === match.id),
1955
+ )
1956
+ enteringMatches = newMatches.filter(
1957
+ (match) =>
1958
+ !previousMatches.find((d) => d.id === match.id),
1959
+ )
1960
+ stayingMatches = previousMatches.filter((match) =>
1961
+ newMatches.find((d) => d.id === match.id),
1962
+ )
1963
+
1964
+ return {
1965
+ ...s,
1966
+ isLoading: false,
1967
+ loadedAt: Date.now(),
1968
+ matches: newMatches,
1969
+ pendingMatches: undefined,
1970
+ cachedMatches: [
1971
+ ...s.cachedMatches,
1972
+ ...exitingMatches.filter((d) => d.status !== 'error'),
1973
+ ],
1974
+ }
1975
+ })
1976
+ this.clearExpiredCache()
1977
+ })
1978
+
1979
+ //
1980
+ ;(
1981
+ [
1982
+ [exitingMatches, 'onLeave'],
1983
+ [enteringMatches, 'onEnter'],
1984
+ [stayingMatches, 'onStay'],
1985
+ ] as const
1986
+ ).forEach(([matches, hook]) => {
1987
+ matches.forEach((match) => {
1988
+ this.looseRoutesById[match.routeId]!.options[hook]?.(match)
1989
+ })
1990
+ })
1991
+ })
1992
+ },
1993
+ })
1994
+ } catch (err) {
1995
+ if (isRedirect(err)) {
1996
+ redirect = err
1997
+ if (!this.isServer) {
1998
+ this.navigate({
1999
+ ...redirect.options,
2000
+ replace: true,
2001
+ ignoreBlocker: true,
2002
+ })
2003
+ }
2004
+ } else if (isNotFound(err)) {
2005
+ notFound = err
2006
+ }
2007
+
2008
+ this.__store.setState((s) => ({
2009
+ ...s,
2010
+ statusCode: redirect
2011
+ ? redirect.status
2012
+ : notFound
2013
+ ? 404
2014
+ : s.matches.some((d) => d.status === 'error')
2015
+ ? 500
2016
+ : 200,
2017
+ redirect,
2018
+ }))
1165
2019
  }
1166
2020
 
1167
- pathname = interpolatePath(pathname, nextParams ?? {})
2021
+ if (this.latestLoadPromise === loadPromise) {
2022
+ this.commitLocationPromise?.resolve()
2023
+ this.latestLoadPromise = undefined
2024
+ this.commitLocationPromise = undefined
2025
+ }
2026
+ resolve()
2027
+ })
2028
+ })
1168
2029
 
1169
- // Pre filters first
1170
- const preFilteredSearch = dest.__preSearchFilters?.length
1171
- ? dest.__preSearchFilters.reduce(
1172
- (prev, next) => next(prev),
1173
- router.location.search,
1174
- )
1175
- : router.location.search
1176
-
1177
- // Then the link/navigate function
1178
- const destSearch =
1179
- dest.search === true
1180
- ? preFilteredSearch // Preserve resolvedFrom true
1181
- : dest.search
1182
- ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1183
- : dest.__preSearchFilters?.length
1184
- ? preFilteredSearch // Preserve resolvedFrom filters
1185
- : {}
2030
+ this.latestLoadPromise = loadPromise
1186
2031
 
1187
- // Then post filters
1188
- const postFilteredSearch = dest.__postSearchFilters?.length
1189
- ? dest.__postSearchFilters.reduce(
1190
- (prev, next) => next(prev),
1191
- destSearch,
1192
- )
1193
- : destSearch
2032
+ await loadPromise
1194
2033
 
1195
- const search = replaceEqualDeep(
1196
- router.location.search,
1197
- postFilteredSearch,
1198
- )
2034
+ while (
2035
+ (this.latestLoadPromise as any) &&
2036
+ loadPromise !== this.latestLoadPromise
2037
+ ) {
2038
+ await this.latestLoadPromise
2039
+ }
1199
2040
 
1200
- const searchStr = router.options.stringifySearch(search)
1201
- let hash =
1202
- dest.hash === true
1203
- ? router.location.hash
1204
- : functionalUpdate(dest.hash!, router.location.hash)
1205
- hash = hash ? `#${hash}` : ''
2041
+ if (this.hasNotFoundMatch()) {
2042
+ this.__store.setState((s) => ({
2043
+ ...s,
2044
+ statusCode: 404,
2045
+ }))
2046
+ }
2047
+ }
1206
2048
 
1207
- return {
1208
- pathname,
1209
- search,
1210
- searchStr,
1211
- state: router.location.state,
1212
- hash,
1213
- href: `${pathname}${searchStr}${hash}`,
1214
- key: dest.key,
2049
+ startViewTransition = (fn: () => Promise<void>) => {
2050
+ // Determine if we should start a view transition from the navigation
2051
+ // or from the router default
2052
+ const shouldViewTransition =
2053
+ this.shouldViewTransition ?? this.options.defaultViewTransition
2054
+
2055
+ // Reset the view transition flag
2056
+ delete this.shouldViewTransition
2057
+ // Attempt to start a view transition (or just apply the changes if we can't)
2058
+ if (
2059
+ shouldViewTransition &&
2060
+ typeof document !== 'undefined' &&
2061
+ 'startViewTransition' in document &&
2062
+ typeof document.startViewTransition === 'function'
2063
+ ) {
2064
+ // lib.dom.ts doesn't support viewTransition types variant yet.
2065
+ // TODO: Fix this when dom types are updated
2066
+ let startViewTransitionParams: any
2067
+
2068
+ if (
2069
+ typeof shouldViewTransition === 'object' &&
2070
+ this.isViewTransitionTypesSupported
2071
+ ) {
2072
+ const next = this.latestLocation
2073
+ const prevLocation = this.state.resolvedLocation
2074
+
2075
+ const resolvedViewTransitionTypes =
2076
+ typeof shouldViewTransition.types === 'function'
2077
+ ? shouldViewTransition.types(
2078
+ getLocationChangeInfo({
2079
+ resolvedLocation: prevLocation,
2080
+ location: next,
2081
+ }),
2082
+ )
2083
+ : shouldViewTransition.types
2084
+
2085
+ startViewTransitionParams = {
2086
+ update: fn,
2087
+ types: resolvedViewTransitionTypes,
1215
2088
  }
1216
- },
2089
+ } else {
2090
+ startViewTransitionParams = fn
2091
+ }
1217
2092
 
1218
- commitLocation: (next: Location, replace?: boolean): Promise<void> => {
1219
- const id = '' + Date.now() + Math.random()
2093
+ document.startViewTransition(startViewTransitionParams)
2094
+ } else {
2095
+ fn()
2096
+ }
2097
+ }
1220
2098
 
1221
- if (router.navigateTimeout) clearTimeout(router.navigateTimeout)
2099
+ updateMatch: UpdateMatchFn = (id, updater) => {
2100
+ let updated!: AnyRouteMatch
2101
+ const isPending = this.state.pendingMatches?.find((d) => d.id === id)
2102
+ const isMatched = this.state.matches.find((d) => d.id === id)
2103
+ const isCached = this.state.cachedMatches.find((d) => d.id === id)
2104
+
2105
+ const matchesKey = isPending
2106
+ ? 'pendingMatches'
2107
+ : isMatched
2108
+ ? 'matches'
2109
+ : isCached
2110
+ ? 'cachedMatches'
2111
+ : ''
2112
+
2113
+ if (matchesKey) {
2114
+ this.__store.setState((s) => ({
2115
+ ...s,
2116
+ [matchesKey]: s[matchesKey]?.map((d) =>
2117
+ d.id === id ? (updated = updater(d)) : d,
2118
+ ),
2119
+ }))
2120
+ }
1222
2121
 
1223
- let nextAction: 'push' | 'replace' = 'replace'
2122
+ return updated
2123
+ }
1224
2124
 
1225
- if (!replace) {
1226
- nextAction = 'push'
2125
+ getMatch: GetMatchFn = (matchId: string) => {
2126
+ return [
2127
+ ...this.state.cachedMatches,
2128
+ ...(this.state.pendingMatches ?? []),
2129
+ ...this.state.matches,
2130
+ ].find((d) => d.id === matchId)
2131
+ }
2132
+
2133
+ loadMatches = async ({
2134
+ location,
2135
+ matches,
2136
+ preload: allPreload,
2137
+ onReady,
2138
+ updateMatch = this.updateMatch,
2139
+ sync,
2140
+ }: {
2141
+ location: ParsedLocation
2142
+ matches: Array<AnyRouteMatch>
2143
+ preload?: boolean
2144
+ onReady?: () => Promise<void>
2145
+ updateMatch?: (
2146
+ id: string,
2147
+ updater: (match: AnyRouteMatch) => AnyRouteMatch,
2148
+ ) => void
2149
+ getMatch?: (matchId: string) => AnyRouteMatch | undefined
2150
+ sync?: boolean
2151
+ }): Promise<Array<MakeRouteMatch>> => {
2152
+ let firstBadMatchIndex: number | undefined
2153
+ let rendered = false
2154
+
2155
+ const triggerOnReady = async () => {
2156
+ if (!rendered) {
2157
+ rendered = true
2158
+ await onReady?.()
2159
+ }
2160
+ }
2161
+
2162
+ const resolvePreload = (matchId: string) => {
2163
+ return !!(allPreload && !this.state.matches.find((d) => d.id === matchId))
2164
+ }
2165
+
2166
+ const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
2167
+ if (isRedirect(err) || isNotFound(err)) {
2168
+ if (isRedirect(err)) {
2169
+ if (err.redirectHandled) {
2170
+ if (!err.options.reloadDocument) {
2171
+ throw err
2172
+ }
2173
+ }
1227
2174
  }
1228
2175
 
1229
- const isSameUrl =
1230
- router.__.parseLocation(history.location).href === next.href
2176
+ updateMatch(match.id, (prev) => ({
2177
+ ...prev,
2178
+ status: isRedirect(err)
2179
+ ? 'redirected'
2180
+ : isNotFound(err)
2181
+ ? 'notFound'
2182
+ : 'error',
2183
+ isFetching: false,
2184
+ error: err,
2185
+ beforeLoadPromise: undefined,
2186
+ loaderPromise: undefined,
2187
+ }))
2188
+
2189
+ if (!(err as any).routeId) {
2190
+ ;(err as any).routeId = match.routeId
2191
+ }
1231
2192
 
1232
- if (isSameUrl && !next.key) {
1233
- nextAction = 'replace'
2193
+ match.beforeLoadPromise?.resolve()
2194
+ match.loaderPromise?.resolve()
2195
+ match.loadPromise?.resolve()
2196
+
2197
+ if (isRedirect(err)) {
2198
+ rendered = true
2199
+ err.options._fromLocation = location
2200
+ err.redirectHandled = true
2201
+ err = this.resolveRedirect(err)
2202
+ throw err
2203
+ } else if (isNotFound(err)) {
2204
+ this._handleNotFound(matches, err, {
2205
+ updateMatch,
2206
+ })
2207
+ this.serverSsr?.onMatchSettled({
2208
+ router: this,
2209
+ match: this.getMatch(match.id)!,
2210
+ })
2211
+ throw err
1234
2212
  }
2213
+ }
2214
+ }
1235
2215
 
1236
- if (nextAction === 'replace') {
1237
- history.replace(
1238
- {
1239
- pathname: next.pathname,
1240
- hash: next.hash,
1241
- search: next.searchStr,
1242
- },
1243
- {
1244
- id,
1245
- },
1246
- )
1247
- } else {
1248
- history.push(
1249
- {
1250
- pathname: next.pathname,
1251
- hash: next.hash,
1252
- search: next.searchStr,
1253
- },
1254
- {
1255
- id,
1256
- },
1257
- )
2216
+ try {
2217
+ await new Promise<void>((resolveAll, rejectAll) => {
2218
+ ;(async () => {
2219
+ try {
2220
+ const handleSerialError = (
2221
+ index: number,
2222
+ err: any,
2223
+ routerCode: string,
2224
+ ) => {
2225
+ const { id: matchId, routeId } = matches[index]!
2226
+ const route = this.looseRoutesById[routeId]!
2227
+
2228
+ // Much like suspense, we use a promise here to know if
2229
+ // we've been outdated by a new loadMatches call and
2230
+ // should abort the current async operation
2231
+ if (err instanceof Promise) {
2232
+ throw err
2233
+ }
2234
+
2235
+ err.routerCode = routerCode
2236
+ firstBadMatchIndex = firstBadMatchIndex ?? index
2237
+ handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2238
+
2239
+ try {
2240
+ route.options.onError?.(err)
2241
+ } catch (errorHandlerErr) {
2242
+ err = errorHandlerErr
2243
+ handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2244
+ }
2245
+
2246
+ updateMatch(matchId, (prev) => {
2247
+ prev.beforeLoadPromise?.resolve()
2248
+ prev.loadPromise?.resolve()
2249
+
2250
+ return {
2251
+ ...prev,
2252
+ error: err,
2253
+ status: 'error',
2254
+ isFetching: false,
2255
+ updatedAt: Date.now(),
2256
+ abortController: new AbortController(),
2257
+ beforeLoadPromise: undefined,
2258
+ }
2259
+ })
2260
+ }
2261
+
2262
+ for (const [index, { id: matchId, routeId }] of matches.entries()) {
2263
+ const existingMatch = this.getMatch(matchId)!
2264
+ const parentMatchId = matches[index - 1]?.id
2265
+
2266
+ const route = this.looseRoutesById[routeId]!
2267
+
2268
+ const pendingMs =
2269
+ route.options.pendingMs ?? this.options.defaultPendingMs
2270
+
2271
+ const shouldPending = !!(
2272
+ onReady &&
2273
+ !this.isServer &&
2274
+ !resolvePreload(matchId) &&
2275
+ (route.options.loader ||
2276
+ route.options.beforeLoad ||
2277
+ routeNeedsPreload(route)) &&
2278
+ typeof pendingMs === 'number' &&
2279
+ pendingMs !== Infinity &&
2280
+ (route.options.pendingComponent ??
2281
+ (this.options as any)?.defaultPendingComponent)
2282
+ )
2283
+
2284
+ let executeBeforeLoad = true
2285
+ if (
2286
+ // If we are in the middle of a load, either of these will be present
2287
+ // (not to be confused with `loadPromise`, which is always defined)
2288
+ existingMatch.beforeLoadPromise ||
2289
+ existingMatch.loaderPromise
2290
+ ) {
2291
+ if (shouldPending) {
2292
+ setTimeout(() => {
2293
+ try {
2294
+ // Update the match and prematurely resolve the loadMatches promise so that
2295
+ // the pending component can start rendering
2296
+ triggerOnReady()
2297
+ } catch {}
2298
+ }, pendingMs)
2299
+ }
2300
+
2301
+ // Wait for the beforeLoad to resolve before we continue
2302
+ await existingMatch.beforeLoadPromise
2303
+ executeBeforeLoad = this.getMatch(matchId)!.status !== 'success'
2304
+ }
2305
+ if (executeBeforeLoad) {
2306
+ // If we are not in the middle of a load OR the previous load failed, start it
2307
+ try {
2308
+ updateMatch(matchId, (prev) => {
2309
+ // explicitly capture the previous loadPromise
2310
+ const prevLoadPromise = prev.loadPromise
2311
+ return {
2312
+ ...prev,
2313
+ loadPromise: createControlledPromise<void>(() => {
2314
+ prevLoadPromise?.resolve()
2315
+ }),
2316
+ beforeLoadPromise: createControlledPromise<void>(),
2317
+ }
2318
+ })
2319
+ const abortController = new AbortController()
2320
+
2321
+ let pendingTimeout: ReturnType<typeof setTimeout>
2322
+
2323
+ if (shouldPending) {
2324
+ // If we might show a pending component, we need to wait for the
2325
+ // pending promise to resolve before we start showing that state
2326
+ pendingTimeout = setTimeout(() => {
2327
+ try {
2328
+ // Update the match and prematurely resolve the loadMatches promise so that
2329
+ // the pending component can start rendering
2330
+ triggerOnReady()
2331
+ } catch {}
2332
+ }, pendingMs)
2333
+ }
2334
+
2335
+ const { paramsError, searchError } = this.getMatch(matchId)!
2336
+
2337
+ if (paramsError) {
2338
+ handleSerialError(index, paramsError, 'PARSE_PARAMS')
2339
+ }
2340
+
2341
+ if (searchError) {
2342
+ handleSerialError(index, searchError, 'VALIDATE_SEARCH')
2343
+ }
2344
+
2345
+ const getParentMatchContext = () =>
2346
+ parentMatchId
2347
+ ? this.getMatch(parentMatchId)!.context
2348
+ : (this.options.context ?? {})
2349
+
2350
+ updateMatch(matchId, (prev) => ({
2351
+ ...prev,
2352
+ isFetching: 'beforeLoad',
2353
+ fetchCount: prev.fetchCount + 1,
2354
+ abortController,
2355
+ pendingTimeout,
2356
+ context: {
2357
+ ...getParentMatchContext(),
2358
+ ...prev.__routeContext,
2359
+ },
2360
+ }))
2361
+
2362
+ const { search, params, context, cause } =
2363
+ this.getMatch(matchId)!
2364
+
2365
+ const preload = resolvePreload(matchId)
2366
+
2367
+ const beforeLoadFnContext: BeforeLoadContextOptions<
2368
+ any,
2369
+ any,
2370
+ any,
2371
+ any,
2372
+ any
2373
+ > = {
2374
+ search,
2375
+ abortController,
2376
+ params,
2377
+ preload,
2378
+ context,
2379
+ location,
2380
+ navigate: (opts: any) =>
2381
+ this.navigate({ ...opts, _fromLocation: location }),
2382
+ buildLocation: this.buildLocation,
2383
+ cause: preload ? 'preload' : cause,
2384
+ matches,
2385
+ }
2386
+
2387
+ const beforeLoadContext =
2388
+ (await route.options.beforeLoad?.(beforeLoadFnContext)) ??
2389
+ {}
2390
+
2391
+ if (
2392
+ isRedirect(beforeLoadContext) ||
2393
+ isNotFound(beforeLoadContext)
2394
+ ) {
2395
+ handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
2396
+ }
2397
+
2398
+ updateMatch(matchId, (prev) => {
2399
+ return {
2400
+ ...prev,
2401
+ __beforeLoadContext: beforeLoadContext,
2402
+ context: {
2403
+ ...getParentMatchContext(),
2404
+ ...prev.__routeContext,
2405
+ ...beforeLoadContext,
2406
+ },
2407
+ abortController,
2408
+ }
2409
+ })
2410
+ } catch (err) {
2411
+ handleSerialError(index, err, 'BEFORE_LOAD')
2412
+ }
2413
+
2414
+ updateMatch(matchId, (prev) => {
2415
+ prev.beforeLoadPromise?.resolve()
2416
+
2417
+ return {
2418
+ ...prev,
2419
+ beforeLoadPromise: undefined,
2420
+ isFetching: false,
2421
+ }
2422
+ })
2423
+ }
2424
+ }
2425
+
2426
+ const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
2427
+ const matchPromises: Array<Promise<AnyRouteMatch>> = []
2428
+
2429
+ validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
2430
+ matchPromises.push(
2431
+ (async () => {
2432
+ const { loaderPromise: prevLoaderPromise } =
2433
+ this.getMatch(matchId)!
2434
+
2435
+ let loaderShouldRunAsync = false
2436
+ let loaderIsRunningAsync = false
2437
+
2438
+ if (prevLoaderPromise) {
2439
+ await prevLoaderPromise
2440
+ const match = this.getMatch(matchId)!
2441
+ if (match.error) {
2442
+ handleRedirectAndNotFound(match, match.error)
2443
+ }
2444
+ } else {
2445
+ const parentMatchPromise = matchPromises[index - 1] as any
2446
+ const route = this.looseRoutesById[routeId]!
2447
+
2448
+ const getLoaderContext = (): LoaderFnContext => {
2449
+ const {
2450
+ params,
2451
+ loaderDeps,
2452
+ abortController,
2453
+ context,
2454
+ cause,
2455
+ } = this.getMatch(matchId)!
2456
+
2457
+ const preload = resolvePreload(matchId)
2458
+
2459
+ return {
2460
+ params,
2461
+ deps: loaderDeps,
2462
+ preload: !!preload,
2463
+ parentMatchPromise,
2464
+ abortController: abortController,
2465
+ context,
2466
+ location,
2467
+ navigate: (opts) =>
2468
+ this.navigate({ ...opts, _fromLocation: location }),
2469
+ cause: preload ? 'preload' : cause,
2470
+ route,
2471
+ }
2472
+ }
2473
+
2474
+ // This is where all of the stale-while-revalidate magic happens
2475
+ const age = Date.now() - this.getMatch(matchId)!.updatedAt
2476
+
2477
+ const preload = resolvePreload(matchId)
2478
+
2479
+ const staleAge = preload
2480
+ ? (route.options.preloadStaleTime ??
2481
+ this.options.defaultPreloadStaleTime ??
2482
+ 30_000) // 30 seconds for preloads by default
2483
+ : (route.options.staleTime ??
2484
+ this.options.defaultStaleTime ??
2485
+ 0)
2486
+
2487
+ const shouldReloadOption = route.options.shouldReload
2488
+
2489
+ // Default to reloading the route all the time
2490
+ // Allow shouldReload to get the last say,
2491
+ // if provided.
2492
+ const shouldReload =
2493
+ typeof shouldReloadOption === 'function'
2494
+ ? shouldReloadOption(getLoaderContext())
2495
+ : shouldReloadOption
2496
+
2497
+ updateMatch(matchId, (prev) => ({
2498
+ ...prev,
2499
+ loaderPromise: createControlledPromise<void>(),
2500
+ preload:
2501
+ !!preload &&
2502
+ !this.state.matches.find((d) => d.id === matchId),
2503
+ }))
2504
+
2505
+ const runLoader = async () => {
2506
+ try {
2507
+ // If the Matches component rendered
2508
+ // the pending component and needs to show it for
2509
+ // a minimum duration, we''ll wait for it to resolve
2510
+ // before committing to the match and resolving
2511
+ // the loadPromise
2512
+ const potentialPendingMinPromise = async () => {
2513
+ const latestMatch = this.getMatch(matchId)!
2514
+
2515
+ if (latestMatch.minPendingPromise) {
2516
+ await latestMatch.minPendingPromise
2517
+ }
2518
+ }
2519
+
2520
+ // Actually run the loader and handle the result
2521
+ try {
2522
+ this.loadRouteChunk(route)
2523
+
2524
+ updateMatch(matchId, (prev) => ({
2525
+ ...prev,
2526
+ isFetching: 'loader',
2527
+ }))
2528
+
2529
+ // Kick off the loader!
2530
+ const loaderData =
2531
+ await route.options.loader?.(getLoaderContext())
2532
+
2533
+ handleRedirectAndNotFound(
2534
+ this.getMatch(matchId)!,
2535
+ loaderData,
2536
+ )
2537
+
2538
+ // Lazy option can modify the route options,
2539
+ // so we need to wait for it to resolve before
2540
+ // we can use the options
2541
+ await route._lazyPromise
2542
+
2543
+ await potentialPendingMinPromise()
2544
+
2545
+ const assetContext = {
2546
+ matches,
2547
+ match: this.getMatch(matchId)!,
2548
+ params: this.getMatch(matchId)!.params,
2549
+ loaderData,
2550
+ }
2551
+ const headFnContent =
2552
+ route.options.head?.(assetContext)
2553
+ const meta = headFnContent?.meta
2554
+ const links = headFnContent?.links
2555
+ const headScripts = headFnContent?.scripts
2556
+
2557
+ const scripts = route.options.scripts?.(assetContext)
2558
+ const headers = route.options.headers?.({
2559
+ loaderData,
2560
+ })
2561
+
2562
+ // Last but not least, wait for the the components
2563
+ // to be preloaded before we resolve the match
2564
+ await route._componentsPromise
2565
+
2566
+ updateMatch(matchId, (prev) => ({
2567
+ ...prev,
2568
+ error: undefined,
2569
+ status: 'success',
2570
+ isFetching: false,
2571
+ updatedAt: Date.now(),
2572
+ loaderData,
2573
+ meta,
2574
+ links,
2575
+ headScripts,
2576
+ headers,
2577
+ scripts,
2578
+ }))
2579
+ } catch (e) {
2580
+ let error = e
2581
+
2582
+ await potentialPendingMinPromise()
2583
+
2584
+ handleRedirectAndNotFound(this.getMatch(matchId)!, e)
2585
+
2586
+ try {
2587
+ route.options.onError?.(e)
2588
+ } catch (onErrorError) {
2589
+ error = onErrorError
2590
+ handleRedirectAndNotFound(
2591
+ this.getMatch(matchId)!,
2592
+ onErrorError,
2593
+ )
2594
+ }
2595
+
2596
+ updateMatch(matchId, (prev) => ({
2597
+ ...prev,
2598
+ error,
2599
+ status: 'error',
2600
+ isFetching: false,
2601
+ }))
2602
+ }
2603
+
2604
+ this.serverSsr?.onMatchSettled({
2605
+ router: this,
2606
+ match: this.getMatch(matchId)!,
2607
+ })
2608
+ } catch (err) {
2609
+ updateMatch(matchId, (prev) => ({
2610
+ ...prev,
2611
+ loaderPromise: undefined,
2612
+ }))
2613
+ handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2614
+ }
2615
+ }
2616
+
2617
+ // If the route is successful and still fresh, just resolve
2618
+ const { status, invalid } = this.getMatch(matchId)!
2619
+ loaderShouldRunAsync =
2620
+ status === 'success' &&
2621
+ (invalid || (shouldReload ?? age > staleAge))
2622
+ if (preload && route.options.preload === false) {
2623
+ // Do nothing
2624
+ } else if (loaderShouldRunAsync && !sync) {
2625
+ loaderIsRunningAsync = true
2626
+ ;(async () => {
2627
+ try {
2628
+ await runLoader()
2629
+ const { loaderPromise, loadPromise } =
2630
+ this.getMatch(matchId)!
2631
+ loaderPromise?.resolve()
2632
+ loadPromise?.resolve()
2633
+ updateMatch(matchId, (prev) => ({
2634
+ ...prev,
2635
+ loaderPromise: undefined,
2636
+ }))
2637
+ } catch (err) {
2638
+ if (isRedirect(err)) {
2639
+ await this.navigate(err.options)
2640
+ }
2641
+ }
2642
+ })()
2643
+ } else if (
2644
+ status !== 'success' ||
2645
+ (loaderShouldRunAsync && sync)
2646
+ ) {
2647
+ await runLoader()
2648
+ }
2649
+ }
2650
+ if (!loaderIsRunningAsync) {
2651
+ const { loaderPromise, loadPromise } =
2652
+ this.getMatch(matchId)!
2653
+ loaderPromise?.resolve()
2654
+ loadPromise?.resolve()
2655
+ }
2656
+
2657
+ updateMatch(matchId, (prev) => ({
2658
+ ...prev,
2659
+ isFetching: loaderIsRunningAsync ? prev.isFetching : false,
2660
+ loaderPromise: loaderIsRunningAsync
2661
+ ? prev.loaderPromise
2662
+ : undefined,
2663
+ invalid: false,
2664
+ }))
2665
+ return this.getMatch(matchId)!
2666
+ })(),
2667
+ )
2668
+ })
2669
+
2670
+ await Promise.all(matchPromises)
2671
+
2672
+ resolveAll()
2673
+ } catch (err) {
2674
+ rejectAll(err)
2675
+ }
2676
+ })()
2677
+ })
2678
+ await triggerOnReady()
2679
+ } catch (err) {
2680
+ if (isRedirect(err) || isNotFound(err)) {
2681
+ if (isNotFound(err) && !allPreload) {
2682
+ await triggerOnReady()
2683
+ }
2684
+
2685
+ throw err
2686
+ }
2687
+ }
2688
+
2689
+ return matches
2690
+ }
2691
+
2692
+ invalidate: InvalidateFn<
2693
+ RouterCore<
2694
+ TRouteTree,
2695
+ TTrailingSlashOption,
2696
+ TDefaultStructuralSharingOption,
2697
+ TRouterHistory,
2698
+ TDehydrated
2699
+ >
2700
+ > = (opts) => {
2701
+ const invalidate = (d: MakeRouteMatch<TRouteTree>) => {
2702
+ if (opts?.filter?.(d as MakeRouteMatchUnion<this>) ?? true) {
2703
+ return {
2704
+ ...d,
2705
+ invalid: true,
2706
+ ...(d.status === 'error'
2707
+ ? ({ status: 'pending', error: undefined } as const)
2708
+ : {}),
2709
+ }
2710
+ }
2711
+ return d
2712
+ }
2713
+
2714
+ this.__store.setState((s) => ({
2715
+ ...s,
2716
+ matches: s.matches.map(invalidate),
2717
+ cachedMatches: s.cachedMatches.map(invalidate),
2718
+ pendingMatches: s.pendingMatches?.map(invalidate),
2719
+ }))
2720
+
2721
+ return this.load({ sync: opts?.sync })
2722
+ }
2723
+
2724
+ resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
2725
+ if (!redirect.options.href) {
2726
+ redirect.options.href = this.buildLocation(redirect.options).href
2727
+ redirect.headers.set('Location', redirect.options.href)
2728
+ }
2729
+
2730
+ if (!redirect.headers.get('Location')) {
2731
+ redirect.headers.set('Location', redirect.options.href)
2732
+ }
2733
+
2734
+ return redirect
2735
+ }
2736
+
2737
+ clearCache: ClearCacheFn<this> = (opts) => {
2738
+ const filter = opts?.filter
2739
+ if (filter !== undefined) {
2740
+ this.__store.setState((s) => {
2741
+ return {
2742
+ ...s,
2743
+ cachedMatches: s.cachedMatches.filter(
2744
+ (m) => !filter(m as MakeRouteMatchUnion<this>),
2745
+ ),
2746
+ }
2747
+ })
2748
+ } else {
2749
+ this.__store.setState((s) => {
2750
+ return {
2751
+ ...s,
2752
+ cachedMatches: [],
1258
2753
  }
2754
+ })
2755
+ }
2756
+ }
2757
+
2758
+ clearExpiredCache = () => {
2759
+ // This is where all of the garbage collection magic happens
2760
+ const filter = (d: MakeRouteMatch<TRouteTree>) => {
2761
+ const route = this.looseRoutesById[d.routeId]!
2762
+
2763
+ if (!route.options.loader) {
2764
+ return true
2765
+ }
2766
+
2767
+ // If the route was preloaded, use the preloadGcTime
2768
+ // otherwise, use the gcTime
2769
+ const gcTime =
2770
+ (d.preload
2771
+ ? (route.options.preloadGcTime ?? this.options.defaultPreloadGcTime)
2772
+ : (route.options.gcTime ?? this.options.defaultGcTime)) ??
2773
+ 5 * 60 * 1000
2774
+
2775
+ return !(d.status !== 'error' && Date.now() - d.updatedAt < gcTime)
2776
+ }
2777
+ this.clearCache({ filter })
2778
+ }
2779
+
2780
+ loadRouteChunk = (route: AnyRoute) => {
2781
+ if (route._lazyPromise === undefined) {
2782
+ if (route.lazyFn) {
2783
+ route._lazyPromise = route.lazyFn().then((lazyRoute) => {
2784
+ // explicitly don't copy over the lazy route's id
2785
+ const { id: _id, ...options } = lazyRoute.options
2786
+ Object.assign(route.options, options)
2787
+ })
2788
+ } else {
2789
+ route._lazyPromise = Promise.resolve()
2790
+ }
2791
+ }
2792
+
2793
+ // If for some reason lazy resolves more lazy components...
2794
+ // We'll wait for that before pre attempt to preload any
2795
+ // components themselves.
2796
+ if (route._componentsPromise === undefined) {
2797
+ route._componentsPromise = route._lazyPromise.then(() =>
2798
+ Promise.all(
2799
+ componentTypes.map(async (type) => {
2800
+ const component = route.options[type]
2801
+ if ((component as any)?.preload) {
2802
+ await (component as any).preload()
2803
+ }
2804
+ }),
2805
+ ),
2806
+ )
2807
+ }
2808
+ return route._componentsPromise
2809
+ }
1259
2810
 
1260
- router.navigationPromise = new Promise((resolve) => {
1261
- const previousNavigationResolve = router.resolveNavigation
2811
+ preloadRoute: PreloadRouteFn<
2812
+ TRouteTree,
2813
+ TTrailingSlashOption,
2814
+ TDefaultStructuralSharingOption,
2815
+ TRouterHistory
2816
+ > = async (opts) => {
2817
+ const next = this.buildLocation(opts as any)
2818
+
2819
+ let matches = this.matchRoutes(next, {
2820
+ throwOnError: true,
2821
+ preload: true,
2822
+ dest: opts,
2823
+ })
2824
+
2825
+ const activeMatchIds = new Set(
2826
+ [...this.state.matches, ...(this.state.pendingMatches ?? [])].map(
2827
+ (d) => d.id,
2828
+ ),
2829
+ )
2830
+
2831
+ const loadedMatchIds = new Set([
2832
+ ...activeMatchIds,
2833
+ ...this.state.cachedMatches.map((d) => d.id),
2834
+ ])
2835
+
2836
+ // If the matches are already loaded, we need to add them to the cachedMatches
2837
+ batch(() => {
2838
+ matches.forEach((match) => {
2839
+ if (!loadedMatchIds.has(match.id)) {
2840
+ this.__store.setState((s) => ({
2841
+ ...s,
2842
+ cachedMatches: [...(s.cachedMatches as any), match],
2843
+ }))
2844
+ }
2845
+ })
2846
+ })
1262
2847
 
1263
- router.resolveNavigation = () => {
1264
- previousNavigationResolve()
1265
- resolve()
2848
+ try {
2849
+ matches = await this.loadMatches({
2850
+ matches,
2851
+ location: next,
2852
+ preload: true,
2853
+ updateMatch: (id, updater) => {
2854
+ // Don't update the match if it's currently loaded
2855
+ if (activeMatchIds.has(id)) {
2856
+ matches = matches.map((d) => (d.id === id ? updater(d) : d))
2857
+ } else {
2858
+ this.updateMatch(id, updater)
1266
2859
  }
2860
+ },
2861
+ })
2862
+
2863
+ return matches
2864
+ } catch (err) {
2865
+ if (isRedirect(err)) {
2866
+ if (err.options.reloadDocument) {
2867
+ return undefined
2868
+ }
2869
+ return await this.preloadRoute({
2870
+ ...err.options,
2871
+ _fromLocation: next,
1267
2872
  })
2873
+ }
2874
+ if (!isNotFound(err)) {
2875
+ // Preload errors are not fatal, but we should still log them
2876
+ console.error(err)
2877
+ }
2878
+ return undefined
2879
+ }
2880
+ }
1268
2881
 
1269
- return router.navigationPromise
1270
- },
1271
- },
2882
+ matchRoute: MatchRouteFn<
2883
+ TRouteTree,
2884
+ TTrailingSlashOption,
2885
+ TDefaultStructuralSharingOption,
2886
+ TRouterHistory
2887
+ > = (location, opts) => {
2888
+ const matchLocation = {
2889
+ ...location,
2890
+ to: location.to
2891
+ ? this.resolvePathWithBase(
2892
+ (location.from || '') as string,
2893
+ location.to as string,
2894
+ )
2895
+ : undefined,
2896
+ params: location.params || {},
2897
+ leaveParams: true,
2898
+ }
2899
+ const next = this.buildLocation(matchLocation as any)
2900
+
2901
+ if (opts?.pending && this.state.status !== 'pending') {
2902
+ return false
2903
+ }
2904
+
2905
+ const pending =
2906
+ opts?.pending === undefined ? !this.state.isLoading : opts.pending
2907
+
2908
+ const baseLocation = pending
2909
+ ? this.latestLocation
2910
+ : this.state.resolvedLocation || this.state.location
2911
+
2912
+ const match = matchPathname(this.basepath, baseLocation.pathname, {
2913
+ ...opts,
2914
+ to: next.pathname,
2915
+ }) as any
2916
+
2917
+ if (!match) {
2918
+ return false
2919
+ }
2920
+ if (location.params) {
2921
+ if (!deepEqual(match, location.params, { partial: true })) {
2922
+ return false
2923
+ }
2924
+ }
2925
+
2926
+ if (match && (opts?.includeSearch ?? true)) {
2927
+ return deepEqual(baseLocation.search, next.search, { partial: true })
2928
+ ? match
2929
+ : false
2930
+ }
2931
+
2932
+ return match
2933
+ }
2934
+
2935
+ ssr?: {
2936
+ manifest: Manifest | undefined
2937
+ serializer: StartSerializer
1272
2938
  }
1273
2939
 
1274
- router.update(userOptions)
2940
+ serverSsr?: {
2941
+ injectedHtml: Array<InjectedHtmlEntry>
2942
+ injectHtml: (getHtml: () => string | Promise<string>) => Promise<void>
2943
+ injectScript: (
2944
+ getScript: () => string | Promise<string>,
2945
+ opts?: { logScript?: boolean },
2946
+ ) => Promise<void>
2947
+ streamValue: (key: string, value: any) => void
2948
+ streamedKeys: Set<string>
2949
+ onMatchSettled: (opts: { router: AnyRouter; match: AnyRouteMatch }) => any
2950
+ }
1275
2951
 
1276
- // Allow frameworks to hook into the router creation
1277
- router.options.createRouter?.(router)
2952
+ clientSsr?: {
2953
+ getStreamedValue: <T>(key: string) => T | undefined
2954
+ }
2955
+
2956
+ _handleNotFound = (
2957
+ matches: Array<AnyRouteMatch>,
2958
+ err: NotFoundError,
2959
+ {
2960
+ updateMatch = this.updateMatch,
2961
+ }: {
2962
+ updateMatch?: (
2963
+ id: string,
2964
+ updater: (match: AnyRouteMatch) => AnyRouteMatch,
2965
+ ) => void
2966
+ } = {},
2967
+ ) => {
2968
+ // Find the route that should handle the not found error
2969
+ // First check if a specific route is requested to show the error
2970
+ const routeCursor = this.routesById[err.routeId ?? ''] ?? this.routeTree
2971
+ const matchesByRouteId: Record<string, AnyRouteMatch> = {}
2972
+
2973
+ // Setup routesByRouteId object for quick access
2974
+ for (const match of matches) {
2975
+ matchesByRouteId[match.routeId] = match
2976
+ }
2977
+
2978
+ // Ensure a NotFoundComponent exists on the route
2979
+ if (
2980
+ !routeCursor.options.notFoundComponent &&
2981
+ (this.options as any)?.defaultNotFoundComponent
2982
+ ) {
2983
+ routeCursor.options.notFoundComponent = (
2984
+ this.options as any
2985
+ ).defaultNotFoundComponent
2986
+ }
2987
+
2988
+ // Ensure we have a notFoundComponent
2989
+ invariant(
2990
+ routeCursor.options.notFoundComponent,
2991
+ 'No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router.',
2992
+ )
2993
+
2994
+ // Find the match for this route
2995
+ const matchForRoute = matchesByRouteId[routeCursor.id]
2996
+
2997
+ invariant(
2998
+ matchForRoute,
2999
+ 'Could not find match for route: ' + routeCursor.id,
3000
+ )
3001
+
3002
+ // Assign the error to the match - using non-null assertion since we've checked with invariant
3003
+ updateMatch(matchForRoute.id, (prev) => ({
3004
+ ...prev,
3005
+ status: 'notFound',
3006
+ error: err,
3007
+ isFetching: false,
3008
+ }))
3009
+
3010
+ if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
3011
+ err.routeId = routeCursor.parentRoute.id
3012
+ this._handleNotFound(matches, err, {
3013
+ updateMatch,
3014
+ })
3015
+ }
3016
+ }
1278
3017
 
1279
- return router
3018
+ hasNotFoundMatch = () => {
3019
+ return this.__store.state.matches.some(
3020
+ (d) => d.status === 'notFound' || d.globalNotFound,
3021
+ )
3022
+ }
3023
+ }
3024
+
3025
+ export class SearchParamError extends Error {}
3026
+
3027
+ export class PathParamError extends Error {}
3028
+
3029
+ // A function that takes an import() argument which is a function and returns a new function that will
3030
+ // proxy arguments from the caller to the imported function, retaining all type
3031
+ // information along the way
3032
+ export function lazyFn<
3033
+ T extends Record<string, (...args: Array<any>) => any>,
3034
+ TKey extends keyof T = 'default',
3035
+ >(fn: () => Promise<T>, key?: TKey) {
3036
+ return async (
3037
+ ...args: Parameters<T[TKey]>
3038
+ ): Promise<Awaited<ReturnType<T[TKey]>>> => {
3039
+ const imported = await fn()
3040
+ return imported[key || 'default'](...args)
3041
+ }
1280
3042
  }
1281
3043
 
1282
- function isCtrlEvent(e: MouseEvent) {
1283
- return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
3044
+ export function getInitialRouterState(
3045
+ location: ParsedLocation,
3046
+ ): RouterState<any> {
3047
+ return {
3048
+ loadedAt: 0,
3049
+ isLoading: false,
3050
+ isTransitioning: false,
3051
+ status: 'idle',
3052
+ resolvedLocation: undefined,
3053
+ location,
3054
+ matches: [],
3055
+ pendingMatches: [],
3056
+ cachedMatches: [],
3057
+ statusCode: 200,
3058
+ }
1284
3059
  }
1285
3060
 
1286
- function cascadeLoaderData(matches: RouteMatch<any, any>[]) {
1287
- matches.forEach((match, index) => {
1288
- const parent = matches[index - 1]
3061
+ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
3062
+ if (validateSearch == null) return {}
3063
+
3064
+ if ('~standard' in validateSearch) {
3065
+ const result = validateSearch['~standard'].validate(input)
1289
3066
 
1290
- if (parent) {
1291
- match.loaderData = replaceEqualDeep(match.loaderData, {
1292
- ...parent.loaderData,
1293
- ...match.routeLoaderData,
3067
+ if (result instanceof Promise)
3068
+ throw new SearchParamError('Async validation not supported')
3069
+
3070
+ if (result.issues)
3071
+ throw new SearchParamError(JSON.stringify(result.issues, undefined, 2), {
3072
+ cause: result,
1294
3073
  })
3074
+
3075
+ return result.value
3076
+ }
3077
+
3078
+ if ('parse' in validateSearch) {
3079
+ return validateSearch.parse(input)
3080
+ }
3081
+
3082
+ if (typeof validateSearch === 'function') {
3083
+ return validateSearch(input)
3084
+ }
3085
+
3086
+ return {}
3087
+ }
3088
+
3089
+ export const componentTypes = [
3090
+ 'component',
3091
+ 'errorComponent',
3092
+ 'pendingComponent',
3093
+ 'notFoundComponent',
3094
+ ] as const
3095
+
3096
+ function routeNeedsPreload(route: AnyRoute) {
3097
+ for (const componentType of componentTypes) {
3098
+ if ((route.options[componentType] as any)?.preload) {
3099
+ return true
3100
+ }
3101
+ }
3102
+ return false
3103
+ }
3104
+
3105
+ interface RouteLike {
3106
+ id: string
3107
+ isRoot?: boolean
3108
+ path?: string
3109
+ fullPath: string
3110
+ rank?: number
3111
+ parentRoute?: RouteLike
3112
+ children?: Array<RouteLike>
3113
+ options?: {
3114
+ caseSensitive?: boolean
3115
+ }
3116
+ }
3117
+
3118
+ export function processRouteTree<TRouteLike extends RouteLike>({
3119
+ routeTree,
3120
+ initRoute,
3121
+ }: {
3122
+ routeTree: TRouteLike
3123
+ initRoute?: (route: TRouteLike, index: number) => void
3124
+ }) {
3125
+ const routesById = {} as Record<string, TRouteLike>
3126
+ const routesByPath = {} as Record<string, TRouteLike>
3127
+
3128
+ const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
3129
+ childRoutes.forEach((childRoute, i) => {
3130
+ initRoute?.(childRoute, i)
3131
+
3132
+ const existingRoute = routesById[childRoute.id]
3133
+
3134
+ invariant(
3135
+ !existingRoute,
3136
+ `Duplicate routes found with id: ${String(childRoute.id)}`,
3137
+ )
3138
+
3139
+ routesById[childRoute.id] = childRoute
3140
+
3141
+ if (!childRoute.isRoot && childRoute.path) {
3142
+ const trimmedFullPath = trimPathRight(childRoute.fullPath)
3143
+ if (
3144
+ !routesByPath[trimmedFullPath] ||
3145
+ childRoute.fullPath.endsWith('/')
3146
+ ) {
3147
+ routesByPath[trimmedFullPath] = childRoute
3148
+ }
3149
+ }
3150
+
3151
+ const children = childRoute.children as Array<TRouteLike>
3152
+
3153
+ if (children?.length) {
3154
+ recurseRoutes(children)
3155
+ }
3156
+ })
3157
+ }
3158
+
3159
+ recurseRoutes([routeTree])
3160
+
3161
+ const scoredRoutes: Array<{
3162
+ child: TRouteLike
3163
+ trimmed: string
3164
+ parsed: ReturnType<typeof parsePathname>
3165
+ index: number
3166
+ scores: Array<number>
3167
+ }> = []
3168
+
3169
+ const routes: Array<TRouteLike> = Object.values(routesById)
3170
+
3171
+ routes.forEach((d, i) => {
3172
+ if (d.isRoot || !d.path) {
3173
+ return
1295
3174
  }
3175
+
3176
+ const trimmed = trimPathLeft(d.fullPath)
3177
+ const parsed = parsePathname(trimmed)
3178
+
3179
+ // Removes the leading slash if it is not the only remaining segment
3180
+ while (parsed.length > 1 && parsed[0]?.value === '/') {
3181
+ parsed.shift()
3182
+ }
3183
+
3184
+ const scores = parsed.map((segment) => {
3185
+ if (segment.value === '/') {
3186
+ return 0.75
3187
+ }
3188
+
3189
+ if (
3190
+ segment.type === 'param' &&
3191
+ segment.prefixSegment &&
3192
+ segment.suffixSegment
3193
+ ) {
3194
+ return 0.55
3195
+ }
3196
+
3197
+ if (segment.type === 'param' && segment.prefixSegment) {
3198
+ return 0.52
3199
+ }
3200
+
3201
+ if (segment.type === 'param' && segment.suffixSegment) {
3202
+ return 0.51
3203
+ }
3204
+
3205
+ if (segment.type === 'param') {
3206
+ return 0.5
3207
+ }
3208
+
3209
+ if (
3210
+ segment.type === 'wildcard' &&
3211
+ segment.prefixSegment &&
3212
+ segment.suffixSegment
3213
+ ) {
3214
+ return 0.3
3215
+ }
3216
+
3217
+ if (segment.type === 'wildcard' && segment.prefixSegment) {
3218
+ return 0.27
3219
+ }
3220
+
3221
+ if (segment.type === 'wildcard' && segment.suffixSegment) {
3222
+ return 0.26
3223
+ }
3224
+
3225
+ if (segment.type === 'wildcard') {
3226
+ return 0.25
3227
+ }
3228
+
3229
+ return 1
3230
+ })
3231
+
3232
+ scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
1296
3233
  })
3234
+
3235
+ const flatRoutes = scoredRoutes
3236
+ .sort((a, b) => {
3237
+ const minLength = Math.min(a.scores.length, b.scores.length)
3238
+
3239
+ // Sort by min available score
3240
+ for (let i = 0; i < minLength; i++) {
3241
+ if (a.scores[i] !== b.scores[i]) {
3242
+ return b.scores[i]! - a.scores[i]!
3243
+ }
3244
+ }
3245
+
3246
+ // Sort by length of score
3247
+ if (a.scores.length !== b.scores.length) {
3248
+ return b.scores.length - a.scores.length
3249
+ }
3250
+
3251
+ // Sort by min available parsed value
3252
+ for (let i = 0; i < minLength; i++) {
3253
+ if (a.parsed[i]!.value !== b.parsed[i]!.value) {
3254
+ return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
3255
+ }
3256
+ }
3257
+
3258
+ // Sort by original index
3259
+ return a.index - b.index
3260
+ })
3261
+ .map((d, i) => {
3262
+ d.child.rank = i
3263
+ return d.child
3264
+ })
3265
+
3266
+ return { routesById, routesByPath, flatRoutes }
3267
+ }
3268
+
3269
+ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3270
+ pathname,
3271
+ routePathname,
3272
+ basepath,
3273
+ caseSensitive,
3274
+ routesByPath,
3275
+ routesById,
3276
+ flatRoutes,
3277
+ }: {
3278
+ pathname: string
3279
+ routePathname?: string
3280
+ basepath: string
3281
+ caseSensitive?: boolean
3282
+ routesByPath: Record<string, TRouteLike>
3283
+ routesById: Record<string, TRouteLike>
3284
+ flatRoutes: Array<TRouteLike>
3285
+ }) {
3286
+ let routeParams: Record<string, string> = {}
3287
+ const trimmedPath = trimPathRight(pathname)
3288
+ const getMatchedParams = (route: TRouteLike) => {
3289
+ const result = matchPathname(basepath, trimmedPath, {
3290
+ to: route.fullPath,
3291
+ caseSensitive: route.options?.caseSensitive ?? caseSensitive,
3292
+ fuzzy: true,
3293
+ })
3294
+ return result
3295
+ }
3296
+
3297
+ let foundRoute: TRouteLike | undefined =
3298
+ routePathname !== undefined ? routesByPath[routePathname] : undefined
3299
+ if (foundRoute) {
3300
+ routeParams = getMatchedParams(foundRoute)!
3301
+ } else {
3302
+ foundRoute = flatRoutes.find((route) => {
3303
+ const matchedParams = getMatchedParams(route)
3304
+
3305
+ if (matchedParams) {
3306
+ routeParams = matchedParams
3307
+ return true
3308
+ }
3309
+
3310
+ return false
3311
+ })
3312
+ }
3313
+
3314
+ let routeCursor: TRouteLike = foundRoute || routesById[rootRouteId]!
3315
+
3316
+ const matchedRoutes: Array<TRouteLike> = [routeCursor]
3317
+
3318
+ while (routeCursor.parentRoute) {
3319
+ routeCursor = routeCursor.parentRoute as TRouteLike
3320
+ matchedRoutes.unshift(routeCursor)
3321
+ }
3322
+
3323
+ return { matchedRoutes, routeParams, foundRoute }
1297
3324
  }