@tanstack/router-core 0.0.1-beta.8 → 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 +1679 -228
  141. package/src/routeInfo.ts +224 -217
  142. package/src/router.ts +3073 -1033
  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 -59
  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 -161
  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 -812
  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 -2480
  184. package/build/esm/index.js.map +0 -1
  185. package/build/stats-html.html +0 -4034
  186. package/build/stats-react.json +0 -499
  187. package/build/types/index.d.ts +0 -619
  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,1284 +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, cascadeLoaderData, 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'>>
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
+ }
357
+
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
+ >
374
+
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>
110
402
  }
111
403
 
112
- export interface Action<
113
- TPayload = unknown,
114
- TResponse = unknown,
115
- // TError = unknown,
404
+ export interface RouterState<
405
+ in out TRouteTree extends AnyRoute = AnyRoute,
406
+ in out TRouteMatch = MakeRouteMatchUnion,
116
407
  > {
117
- submit: (submission?: TPayload) => Promise<TResponse>
118
- current?: ActionState<TPayload, TResponse>
119
- latest?: ActionState<TPayload, TResponse>
120
- pending: ActionState<TPayload, TResponse>[]
408
+ status: 'pending' | 'idle'
409
+ loadedAt: number
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
121
419
  }
122
420
 
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
421
+ export interface BuildNextOptions {
422
+ to?: string | number | null
423
+ params?: true | Updater<unknown>
424
+ search?: true | Updater<unknown>
425
+ hash?: true | Updater<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
+ }
435
+ from?: string
436
+ _fromLocation?: ParsedLocation
437
+ href?: string
133
438
  }
134
439
 
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>[]
440
+ type NavigationEventInfo = {
441
+ fromLocation?: ParsedLocation
442
+ toLocation: ParsedLocation
443
+ pathChanged: boolean
444
+ hrefChanged: boolean
445
+ hashChanged: boolean
161
446
  }
162
447
 
163
- export interface LoaderState<
164
- TFullSearchSchema = unknown,
165
- TAllParams = unknown,
166
- > {
167
- loadedAt: number
168
- loaderContext: LoaderContext<TFullSearchSchema, TAllParams>
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
169
471
  }
170
472
 
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
473
+ export type RouterEvent = RouterEvents[keyof RouterEvents]
474
+
475
+ export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void
476
+
477
+ export type RouterListener<TRouterEvent extends RouterEvent> = {
478
+ eventType: TRouterEvent['type']
479
+ fn: ListenerFn<TRouterEvent>
183
480
  }
184
481
 
185
- export interface PendingState {
186
- location: Location
187
- matches: RouteMatch[]
482
+ export interface MatchRoutesOpts {
483
+ preload?: boolean
484
+ throwOnError?: boolean
485
+ _buildLocation?: boolean
486
+ dest?: BuildNextOptions
188
487
  }
189
488
 
190
- type Listener = (router: Router<any, any>) => void
489
+ export type InferRouterContext<TRouteTree extends AnyRoute> =
490
+ TRouteTree['types']['routerContext']
191
491
 
192
- export type ListenerFn = () => void
492
+ export type RouterContextOptions<TRouteTree extends AnyRoute> =
493
+ AnyContext extends InferRouterContext<TRouteTree>
494
+ ? {
495
+ context?: InferRouterContext<TRouteTree>
496
+ }
497
+ : {
498
+ context: InferRouterContext<TRouteTree>
499
+ }
193
500
 
194
- export interface BuildNextOptions {
195
- to?: string | number | null
196
- params?: true | Updater<Record<string, any>>
197
- search?: true | Updater<unknown>
198
- hash?: true | Updater<string>
199
- key?: string
200
- from?: string
201
- fromCurrent?: boolean
202
- __preSearchFilters?: SearchFilter<any>[]
203
- __postSearchFilters?: SearchFilter<any>[]
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
204
522
  }
205
523
 
206
- export type MatchCacheEntry = {
207
- gc: number
208
- match: RouteMatch
524
+ export interface MatchedRoutesResult {
525
+ matchedRoutes: Array<AnyRoute>
526
+ routeParams: Record<string, string>
209
527
  }
210
528
 
211
- export interface MatchLocation {
212
- to?: string | number | null
213
- fuzzy?: boolean
214
- caseSensitive?: boolean
215
- from?: string
216
- fromCurrent?: boolean
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
217
610
  }
218
611
 
219
- export interface MatchRouteOptions {
220
- pending: boolean
221
- caseSensitive?: boolean
612
+ export type EmitFn = (routerEvent: RouterEvent) => void
613
+
614
+ export type LoadFn = (opts?: { sync?: boolean }) => Promise<void>
615
+
616
+ export type CommitLocationFn = ({
617
+ viewTransition,
618
+ ignoreBlocker,
619
+ ...next
620
+ }: ParsedLocation & CommitLocationOptions) => Promise<void>
621
+
622
+ export type StartTransitionFn = (fn: () => void) => void
623
+
624
+ export type SubscribeFn = <TType extends keyof RouterEvents>(
625
+ eventType: TType,
626
+ fn: ListenerFn<RouterEvents[TType]>,
627
+ ) => () => void
628
+
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>
222
641
  }
223
642
 
224
- type LinkCurrentTargetElement = {
225
- preloadTimeout?: null | ReturnType<typeof setTimeout>
643
+ export type GetMatchFn = (matchId: string) => AnyRouteMatch | undefined
644
+
645
+ export type UpdateMatchFn = (
646
+ id: string,
647
+ updater: (match: AnyRouteMatch) => AnyRouteMatch,
648
+ ) => AnyRouteMatch
649
+
650
+ export type LoadRouteChunkFn = (route: AnyRoute) => Promise<Array<void>>
651
+
652
+ export type ResolveRedirect = (err: AnyRedirect) => ResolvedRedirect
653
+
654
+ export type ClearCacheFn<TRouter extends AnyRouter> = (opts?: {
655
+ filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean
656
+ }) => void
657
+
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
226
668
  }
227
669
 
228
- interface DehydratedRouterState
229
- extends Pick<RouterState, 'status' | 'location' | 'lastUpdated'> {
230
- matches: DehydratedRouteMatch[]
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>)
231
690
  }
232
691
 
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>
692
+ export function defaultSerializeError(err: unknown) {
693
+ if (err instanceof Error) {
694
+ const obj = {
695
+ name: err.name,
696
+ message: err.message,
697
+ }
698
+
699
+ if (process.env.NODE_ENV === 'development') {
700
+ ;(obj as any).stack = err.stack
701
+ }
702
+
703
+ return obj
337
704
  }
705
+
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
+ }
717
+
718
+ export interface ExtractedStream extends ExtractedBaseEntry {
719
+ type: 'stream'
720
+ streamState: StreamState
338
721
  }
339
722
 
340
- // Detect if we're in the DOM
341
- const isServer =
342
- typeof window === 'undefined' || !window.document?.createElement
723
+ export interface ExtractedPromise extends ExtractedBaseEntry {
724
+ type: 'promise'
725
+ promiseState: DeferredPromiseState<any>
726
+ }
727
+
728
+ export type ExtractedEntry = ExtractedStream | ExtractedPromise
343
729
 
344
- // This is the default history object if none is defined
345
- const createDefaultHistory = () =>
346
- isServer ? createMemoryHistory() : createBrowserHistory()
730
+ export type StreamState = {
731
+ promises: Array<ControlledPromise<string | null>>
732
+ }
347
733
 
348
- export function createRouter<
349
- TRouteConfig extends AnyRouteConfig = RouteConfig,
350
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
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
+ }
747
+
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>,
351
754
  >(
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,
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
+ }
364
840
  }
365
841
 
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
- ),
412
- }
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
+ }
413
859
 
414
- cascadeLoaderData(router.state.matches)
415
- router.listeners.forEach((listener) => listener(router))
416
- },
860
+ const previousOptions = this.options
861
+ this.options = {
862
+ ...this.options,
863
+ ...newOptions,
864
+ }
417
865
 
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',
866
+ this.isServer = this.options.isServer ?? typeof document === 'undefined'
867
+
868
+ this.pathParamsDecodeCharMap = this.options.pathParamsAllowedCharacters
869
+ ? new Map(
870
+ this.options.pathParamsAllowedCharacters.map((char) => [
871
+ encodeURIComponent(char),
872
+ char,
429
873
  ]),
430
- ),
874
+ )
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)}`
431
889
  }
432
- },
890
+ }
891
+
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
+ }
905
+
906
+ if (this.options.routeTree !== this.routeTree) {
907
+ this.routeTree = this.options.routeTree as TRouteTree
908
+ this.buildRouteTree()
909
+ }
910
+
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
+ },
921
+ })
922
+
923
+ setupScrollRestoration(this)
924
+ }
925
+
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
+ }
936
+
937
+ get state() {
938
+ return this.__store.state
939
+ }
433
940
 
434
- hydrateState: (dehydratedState) => {
435
- // Match the routes
436
- const matches = router.matchRoutes(router.location.pathname, {
437
- strictParseParams: true,
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
+ })
951
+
952
+ this.routesById = routesById as RoutesById<TRouteTree>
953
+ this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
954
+ this.flatRoutes = flatRoutes as Array<AnyRoute>
955
+
956
+ const notFoundRoute = this.options.notFoundRoute
957
+
958
+ if (notFoundRoute) {
959
+ notFoundRoute.init({
960
+ originalIndex: 99999999999,
961
+ defaultSsr: this.options.defaultSsr,
438
962
  })
963
+ this.routesById[notFoundRoute.id] = notFoundRoute
964
+ }
965
+ }
439
966
 
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
- }),
967
+ subscribe: SubscribeFn = (eventType, fn) => {
968
+ const listener: RouterListener<any> = {
969
+ eventType,
970
+ fn,
971
+ }
972
+
973
+ this.subscribers.add(listener)
974
+
975
+ return () => {
976
+ this.subscribers.delete(listener)
977
+ }
978
+ }
979
+
980
+ emit: EmitFn = (routerEvent) => {
981
+ this.subscribers.forEach((listener) => {
982
+ if (listener.eventType === routerEvent.type) {
983
+ listener.fn(routerEvent)
454
984
  }
455
- },
985
+ })
986
+ }
456
987
 
457
- mount: () => {
458
- const next = router.__.buildLocation({
459
- to: '.',
460
- search: true,
461
- hash: true,
462
- })
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)
463
1000
 
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)
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),
468
1008
  }
1009
+ }
469
1010
 
470
- router.loadLocation()
1011
+ const location = parse(locationToParse ?? this.history.location)
471
1012
 
472
- const unsub = router.history.listen((event) => {
473
- console.log(event.location)
474
- router.loadLocation(
475
- router.__.parseLocation(event.location, router.location),
476
- )
477
- })
1013
+ const { __tempLocation, __tempKey } = location.state
478
1014
 
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)
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
1019
+
1020
+ delete parsedTempLocation.state.__tempLocation
1021
+
1022
+ return {
1023
+ ...parsedTempLocation,
1024
+ maskedLocation: location,
485
1025
  }
1026
+ }
1027
+
1028
+ return location
1029
+ }
1030
+
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
+ }
1041
+
1042
+ get looseRoutesById() {
1043
+ return this.routesById as Record<string, AnyRoute>
1044
+ }
1045
+
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
+ }
486
1072
 
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)
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
492
1097
  }
493
- },
1098
+ }
494
1099
 
495
- onFocus: () => {
496
- router.loadLocation()
497
- },
1100
+ const globalNotFoundRouteId = (() => {
1101
+ if (!isGlobalNotFound) {
1102
+ return undefined
1103
+ }
498
1104
 
499
- update: (opts) => {
500
- const newHistory = opts?.history !== router.history
501
- if (!router.location || newHistory) {
502
- if (opts?.history) {
503
- router.history = opts.history
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
+ }
504
1111
  }
505
- router.location = router.__.parseLocation(router.history.location)
506
- router.state.location = router.location
507
1112
  }
508
1113
 
509
- Object.assign(router.options, opts)
1114
+ return rootRouteId
1115
+ })()
1116
+
1117
+ const parseErrors = matchedRoutes.map((route) => {
1118
+ let parsedParamsError
1119
+
1120
+ const parseParams =
1121
+ route.options.params?.parse ?? route.options.parseParams
510
1122
 
511
- const { basepath, routeConfig } = router.options
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
+ })
512
1132
 
513
- router.basepath = cleanPath(`/${basepath ?? ''}`)
1133
+ if (opts?.throwOnError) {
1134
+ throw parsedParamsError
1135
+ }
514
1136
 
515
- if (routeConfig) {
516
- router.routesById = {} as any
517
- router.routeTree = router.__.buildRouteTree(routeConfig)
1137
+ return parsedParamsError
1138
+ }
518
1139
  }
519
1140
 
520
- return router as any
521
- },
1141
+ return
1142
+ })
522
1143
 
523
- cancelMatches: () => {
524
- ;[
525
- ...router.state.matches,
526
- ...(router.state.pending?.matches ?? []),
527
- ].forEach((match) => {
528
- match.cancel()
529
- })
530
- },
1144
+ const matches: Array<AnyRouteMatch> = []
531
1145
 
532
- loadLocation: async (next?: Location) => {
533
- const id = Math.random()
534
- router.startedLoadingAt = id
1146
+ const getParentContext = (parentMatch?: AnyRouteMatch) => {
1147
+ const parentMatchId = parentMatch?.id
535
1148
 
536
- if (next) {
537
- // Ingest the new location
538
- router.location = next
539
- }
1149
+ const parentContext = !parentMatchId
1150
+ ? ((this.options.context as any) ?? {})
1151
+ : (parentMatch.context ?? this.options.context ?? {})
540
1152
 
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
1153
+ return parentContext
1154
+ }
1155
+
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
+ }
1195
+
1196
+ if (opts?.throwOnError) {
1197
+ throw searchParamError
1198
+ }
1199
+
1200
+ return [parentSearch, {}, searchParamError]
548
1201
  }
549
- })
550
- router.removeActionQueue = []
1202
+ })()
551
1203
 
552
- // Cancel any pending matches
553
- router.cancelMatches()
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
554
1208
 
555
- // Match the routes
556
- const matches = router.matchRoutes(router.location.pathname, {
557
- strictParseParams: true,
1209
+ const loaderDeps =
1210
+ route.options.loaderDeps?.({
1211
+ search: preMatchSearch,
1212
+ }) ?? ''
1213
+
1214
+ const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : ''
1215
+
1216
+ const { usedParams, interpolatedPath } = interpolatePath({
1217
+ path: route.fullPath,
1218
+ params: routeParams,
1219
+ decodeCharMap: this.pathParamsDecodeCharMap,
558
1220
  })
559
1221
 
560
- router.state = {
561
- ...router.state,
562
- pending: {
563
- matches: matches,
564
- location: router.location,
565
- },
566
- status: 'loading',
567
- }
1222
+ const matchId =
1223
+ interpolatePath({
1224
+ path: route.id,
1225
+ params: routeParams,
1226
+ leaveWildcards: true,
1227
+ decodeCharMap: this.pathParamsDecodeCharMap,
1228
+ }).interpolatedPath + loaderDepsHash
568
1229
 
569
- router.notify()
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.
570
1233
 
571
- // Load the matches
572
- await router.loadMatches(matches, {
573
- withPending: true,
574
- })
1234
+ // Existing matches are matches that are already loaded along with
1235
+ // pending matches that are still loading
1236
+ const existingMatch = this.getMatch(matchId)
1237
+
1238
+ const previousMatch = this.state.matches.find(
1239
+ (d) => d.routeId === route.id,
1240
+ )
575
1241
 
576
- if (router.startedLoadingAt !== id) {
577
- // Ignore side-effects of match loading
578
- return router.navigationPromise
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,
1258
+ }
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
+ }
1307
+
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
579
1311
  }
580
1312
 
581
- const previousMatches = router.state.matches
1313
+ // update the searchError if there is one
1314
+ match.searchError = searchError
1315
+
1316
+ const parentContext = getParentContext(parentMatch)
582
1317
 
583
- const exiting: RouteMatch[] = [],
584
- staying: RouteMatch[] = []
1318
+ match.context = {
1319
+ ...parentContext,
1320
+ ...match.__routeContext,
1321
+ ...match.__beforeLoadContext,
1322
+ }
585
1323
 
586
- previousMatches.forEach((d) => {
587
- if (matches.find((dd) => dd.matchId === d.matchId)) {
588
- staying.push(d)
589
- } else {
590
- exiting.push(d)
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,
591
1349
  }
592
- })
593
1350
 
594
- const now = Date.now()
1351
+ // Get the route context
1352
+ match.__routeContext = route.options.context?.(contextFnContext) ?? {}
595
1353
 
596
- exiting.forEach((d) => {
597
- d.__.onExit?.({
598
- params: d.params,
599
- search: d.routeSearch,
1354
+ match.context = {
1355
+ ...parentContext,
1356
+ ...match.__routeContext,
1357
+ ...match.__beforeLoadContext,
1358
+ }
1359
+ }
1360
+
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,
600
1367
  })
601
- // Clear idle error states when match leaves
602
- if (d.status === 'error' && !d.isFetching) {
603
- d.status = 'idle'
604
- d.error = undefined
1368
+ const assetContext = {
1369
+ matches,
1370
+ match,
1371
+ params: match.params,
1372
+ loaderData: match.loaderData,
605
1373
  }
606
- const gc = Math.max(
607
- d.options.loaderGcMaxAge ?? router.options.defaultLoaderGcMaxAge ?? 0,
608
- d.options.loaderMaxAge ?? router.options.defaultLoaderMaxAge ?? 0,
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)
1379
+ }
1380
+ })
1381
+
1382
+ return matches
1383
+ }
1384
+
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
+ }
1399
+
1400
+ cancelMatch = (id: string) => {
1401
+ const match = this.getMatch(id)
1402
+
1403
+ if (!match) return
1404
+
1405
+ match.abortController.abort()
1406
+ clearTimeout(match.pendingTimeout)
1407
+ }
1408
+
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,
609
1474
  )
610
- if (gc > 0) {
611
- router.matchCache[d.matchId] = {
612
- gc: gc == Infinity ? Number.MAX_SAFE_INTEGER : now + gc,
613
- match: d,
1475
+ }
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
+ }) ?? {}),
1520
+ }
1521
+ }
1522
+ } catch {
1523
+ // ignore errors here because they are already handled in matchRoutes
614
1524
  }
1525
+ })
1526
+ search = validatedSearch
1527
+ }
1528
+
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
+ ) ?? []
1595
+
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)
615
1605
  }
616
- })
1606
+ allMiddlewares.push(final)
1607
+
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
+ }
1613
+
1614
+ const middleware = allMiddlewares[index]!
1615
+
1616
+ const next = (newSearch: any): any => {
1617
+ return applyNext(index + 1, newSearch)
1618
+ }
617
1619
 
618
- staying.forEach((d) => {
619
- d.options.onTransition?.({
620
- params: d.params,
621
- search: d.routeSearch,
1620
+ return middleware({ search: currentSearch, next })
1621
+ }
1622
+
1623
+ // Start applying middlewares
1624
+ return applyNext(0, search)
1625
+ }
1626
+
1627
+ search = applyMiddlewares(search)
1628
+
1629
+ search = replaceEqualDeep(fromSearch, search)
1630
+ const searchStr = this.options.stringifySearch(search)
1631
+
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
622
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)
1694
+ }
1695
+ }
1696
+
1697
+ const nextMatches = this.getMatchedRoutes(
1698
+ next.pathname,
1699
+ dest.to as string,
1700
+ )
1701
+ const final = build(dest, nextMatches)
1702
+
1703
+ if (maskedNext) {
1704
+ const maskedMatches = this.getMatchedRoutes(
1705
+ maskedNext.pathname,
1706
+ maskedDest?.to as string,
1707
+ )
1708
+ const maskedFinal = build(maskedDest, maskedMatches)
1709
+ final.maskedLocation = maskedFinal
1710
+ }
1711
+
1712
+ return final
1713
+ }
1714
+
1715
+ if (opts.mask) {
1716
+ return buildWithMatches(opts, {
1717
+ ...pick(opts, ['from']),
1718
+ ...opts.mask,
623
1719
  })
1720
+ }
624
1721
 
625
- const entering = matches.filter((d) => {
626
- return !previousMatches.find((dd) => dd.matchId === d.matchId)
1722
+ return buildWithMatches(opts)
1723
+ }
1724
+
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]
627
1747
  })
1748
+ return isEqual
1749
+ }
1750
+
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
+ }
628
1783
 
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]
1784
+ if (
1785
+ nextHistory.unmaskOnReload ??
1786
+ this.options.unmaskOnReload ??
1787
+ false
1788
+ ) {
1789
+ nextHistory.state.__tempKey = this.tempLocationKey
1790
+ }
1791
+ }
1792
+
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,
635
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
+ }
636
1847
 
637
- if (matches.some((d) => d.status === 'loading')) {
638
- router.notify()
639
- await Promise.all(
640
- matches.map((d) => d.__.loaderPromise || Promise.resolve()),
641
- )
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)
642
1860
  }
643
- if (router.startedLoadingAt !== id) {
644
- // Ignore side-effects of match loading
645
- return
1861
+ if (rest.replace) {
1862
+ window.location.replace(href)
1863
+ } else {
1864
+ window.location.href = href
646
1865
  }
1866
+ return
1867
+ }
1868
+
1869
+ return this.buildAndCommitLocation({
1870
+ ...rest,
1871
+ href,
1872
+ to: to as string,
1873
+ })
1874
+ }
647
1875
 
648
- router.state = {
649
- ...router.state,
650
- location: router.location,
651
- matches,
652
- pending: undefined,
653
- status: 'idle',
654
- }
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
+ }
655
1899
 
656
- router.notify()
657
- router.resolveNavigation()
658
- },
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
+ }),
1921
+ })
1922
+ }
659
1923
 
660
- cleanMatchCache: () => {
661
- const now = Date.now()
1924
+ this.emit({
1925
+ type: 'onBeforeLoad',
1926
+ ...getLocationChangeInfo({
1927
+ resolvedLocation: prevLocation,
1928
+ location: next,
1929
+ }),
1930
+ })
662
1931
 
663
- Object.keys(router.matchCache).forEach((matchId) => {
664
- const entry = router.matchCache[matchId]!
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
+ }
665
2007
 
666
- // Don't remove loading matches
667
- if (entry.match.status === 'loading') {
668
- return
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
+ }))
669
2019
  }
670
2020
 
671
- // Do not remove successful matches that are still valid
672
- if (entry.gc > 0 && entry.gc > now) {
673
- return
2021
+ if (this.latestLoadPromise === loadPromise) {
2022
+ this.commitLocationPromise?.resolve()
2023
+ this.latestLoadPromise = undefined
2024
+ this.commitLocationPromise = undefined
674
2025
  }
675
-
676
- // Everything else gets removed
677
- delete router.matchCache[matchId]
2026
+ resolve()
678
2027
  })
679
- },
2028
+ })
680
2029
 
681
- loadRoute: async (navigateOpts = router.location) => {
682
- const next = router.buildNext(navigateOpts)
683
- const matches = router.matchRoutes(next.pathname, {
684
- strictParseParams: true,
685
- })
686
- await router.loadMatches(matches)
687
- return matches
688
- },
2030
+ this.latestLoadPromise = loadPromise
689
2031
 
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
- },
2032
+ await loadPromise
710
2033
 
711
- matchRoutes: (pathname, opts) => {
712
- router.cleanMatchCache()
2034
+ while (
2035
+ (this.latestLoadPromise as any) &&
2036
+ loadPromise !== this.latestLoadPromise
2037
+ ) {
2038
+ await this.latestLoadPromise
2039
+ }
713
2040
 
714
- const matches: RouteMatch[] = []
2041
+ if (this.hasNotFoundMatch()) {
2042
+ this.__store.setState((s) => ({
2043
+ ...s,
2044
+ statusCode: 404,
2045
+ }))
2046
+ }
2047
+ }
715
2048
 
716
- if (!router.routeTree) {
717
- return matches
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,
2088
+ }
2089
+ } else {
2090
+ startViewTransitionParams = fn
718
2091
  }
719
2092
 
720
- const existingMatches = [
721
- ...router.state.matches,
722
- ...(router.state.pending?.matches ?? []),
723
- ]
2093
+ document.startViewTransition(startViewTransitionParams)
2094
+ } else {
2095
+ fn()
2096
+ }
2097
+ }
724
2098
 
725
- const recurse = async (routes: Route<any, any>[]): Promise<void> => {
726
- const parentMatch = last(matches)
727
- let params = parentMatch?.params ?? {}
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
+ }
728
2121
 
729
- const filteredRoutes = router.options.filterRoutes?.(routes) ?? routes
2122
+ return updated
2123
+ }
730
2124
 
731
- let foundRoutes: Route[] = []
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
+ }
732
2132
 
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
- )
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
740
2172
  }
2173
+ }
2174
+ }
741
2175
 
742
- const fuzzy = !!(
743
- route.routePath !== '/' || route.childRoutes?.length
744
- )
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
+ }
745
2192
 
746
- const matchParams = matchPathname(pathname, {
747
- to: route.fullPath,
748
- fuzzy,
749
- caseSensitive:
750
- route.options.caseSensitive ?? router.options.caseSensitive,
751
- })
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
2212
+ }
2213
+ }
2214
+ }
2215
+
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
+ }
752
2234
 
753
- if (matchParams) {
754
- let parsedParams
2235
+ err.routerCode = routerCode
2236
+ firstBadMatchIndex = firstBadMatchIndex ?? index
2237
+ handleRedirectAndNotFound(this.getMatch(matchId)!, err)
755
2238
 
756
2239
  try {
757
- parsedParams =
758
- route.options.parseParams?.(matchParams!) ?? matchParams
759
- } catch (err) {
760
- if (opts?.strictParseParams) {
761
- throw err
762
- }
2240
+ route.options.onError?.(err)
2241
+ } catch (errorHandlerErr) {
2242
+ err = errorHandlerErr
2243
+ handleRedirectAndNotFound(this.getMatch(matchId)!, err)
763
2244
  }
764
2245
 
765
- params = {
766
- ...params,
767
- ...parsedParams,
768
- }
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
+ })
769
2260
  }
770
2261
 
771
- if (!!matchParams) {
772
- foundRoutes = [...parentRoutes, route]
773
- }
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
+ )
774
2283
 
775
- return !!foundRoutes.length
776
- })
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
+ }
777
2300
 
778
- return !!foundRoutes.length
779
- }
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
+ }
780
2413
 
781
- findMatchInRoutes([], filteredRoutes)
2414
+ updateMatch(matchId, (prev) => {
2415
+ prev.beforeLoadPromise?.resolve()
782
2416
 
783
- if (!foundRoutes.length) {
784
- return
785
- }
2417
+ return {
2418
+ ...prev,
2419
+ beforeLoadPromise: undefined,
2420
+ isFetching: false,
2421
+ }
2422
+ })
2423
+ }
2424
+ }
786
2425
 
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]),
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
+ )
798
2668
  })
799
2669
 
800
- matches.push(match)
801
- })
2670
+ await Promise.all(matchPromises)
802
2671
 
803
- const foundRoute = last(foundRoutes)!
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
+ }
804
2688
 
805
- if (foundRoute.childRoutes?.length) {
806
- recurse(foundRoute.childRoutes)
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
+ : {}),
807
2709
  }
808
2710
  }
2711
+ return d
2712
+ }
809
2713
 
810
- recurse([router.routeTree])
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
+ }))
811
2720
 
812
- cascadeLoaderData(matches)
2721
+ return this.load({ sync: opts?.sync })
2722
+ }
813
2723
 
814
- return matches
815
- },
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
+ }
816
2729
 
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)
2730
+ if (!redirect.headers.get('Location')) {
2731
+ redirect.headers.set('Location', redirect.options.href)
2732
+ }
822
2733
 
823
- if (match.status === 'loading') {
824
- // If requested, start the pending timers
825
- if (loaderOpts?.withPending) match.__.startPending()
2734
+ return redirect
2735
+ }
826
2736
 
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
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
+ ),
830
2746
  }
831
2747
  })
832
-
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()
2748
+ } else {
2749
+ this.__store.setState((s) => {
2750
+ return {
2751
+ ...s,
2752
+ cachedMatches: [],
849
2753
  }
850
2754
  })
851
- },
2755
+ }
2756
+ }
852
2757
 
853
- reload: () =>
854
- router.__.navigate({
855
- fromCurrent: true,
856
- replace: true,
857
- search: true,
858
- }),
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]!
859
2762
 
860
- resolvePath: (from: string, path: string) => {
861
- return resolvePath(router.basepath!, from, cleanPath(path))
862
- },
2763
+ if (!route.options.loader) {
2764
+ return true
2765
+ }
863
2766
 
864
- matchRoute: (location, opts) => {
865
- // const location = router.buildNext(opts)
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
+ }
866
2779
 
867
- location = {
868
- ...location,
869
- to: location.to
870
- ? router.resolvePath(location.from ?? '', location.to)
871
- : undefined,
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()
872
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
+ }
873
2810
 
874
- const next = router.buildNext(location)
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
+ })
875
2847
 
876
- if (opts?.pending) {
877
- if (!router.state.pending?.location) {
878
- return false
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)
2859
+ }
2860
+ },
2861
+ })
2862
+
2863
+ return matches
2864
+ } catch (err) {
2865
+ if (isRedirect(err)) {
2866
+ if (err.options.reloadDocument) {
2867
+ return undefined
879
2868
  }
880
- return !!matchPathname(router.state.pending.location.pathname, {
881
- ...opts,
882
- to: next.pathname,
2869
+ return await this.preloadRoute({
2870
+ ...err.options,
2871
+ _fromLocation: next,
883
2872
  })
884
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
+ }
885
2881
 
886
- return !!matchPathname(router.state.location.pathname, {
887
- ...opts,
888
- to: next.pathname,
889
- })
890
- },
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
+ }
891
2925
 
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
2926
+ if (match && (opts?.includeSearch ?? true)) {
2927
+ return deepEqual(baseLocation.search, next.search, { partial: true })
2928
+ ? match
2929
+ : false
2930
+ }
895
2931
 
896
- // If this `to` is a valid external URL, return
897
- // null for LinkUtils
898
- const toString = String(to)
899
- const fromString = String(from)
2932
+ return match
2933
+ }
900
2934
 
901
- let isExternal
2935
+ ssr?: {
2936
+ manifest: Manifest | undefined
2937
+ serializer: StartSerializer
2938
+ }
902
2939
 
903
- try {
904
- new URL(`${toString}`)
905
- isExternal = true
906
- } catch (e) {}
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
+ }
907
2951
 
908
- invariant(
909
- !isExternal,
910
- 'Attempting to navigate to external url with router.navigate!',
911
- )
2952
+ clientSsr?: {
2953
+ getStreamedValue: <T>(key: string) => T | undefined
2954
+ }
912
2955
 
913
- return router.__.navigate({
914
- from: fromString,
915
- to: toString,
916
- search,
917
- hash,
918
- replace,
919
- params,
920
- })
921
- },
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
+ }))
922
3009
 
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
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
+ }
943
3017
 
944
- try {
945
- new URL(`${to}`)
946
- return {
947
- type: 'external',
948
- href: to,
949
- }
950
- } catch (e) {}
3018
+ hasNotFoundMatch = () => {
3019
+ return this.__store.state.matches.some(
3020
+ (d) => d.status === 'notFound' || d.globalNotFound,
3021
+ )
3022
+ }
3023
+ }
951
3024
 
952
- const nextOpts = {
953
- from,
954
- to,
955
- search,
956
- params,
957
- hash,
958
- replace,
959
- }
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
+ }
3042
+ }
960
3043
 
961
- const next = router.buildNext(nextOpts)
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
+ }
3059
+ }
962
3060
 
963
- preload = preload ?? router.options.defaultPreload
964
- const preloadDelay =
965
- userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0
3061
+ function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
3062
+ if (validateSearch == null) return {}
966
3063
 
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
3064
+ if ('~standard' in validateSearch) {
3065
+ const result = validateSearch['~standard'].validate(input)
978
3066
 
979
- // The final "active" test
980
- const isActive = pathTest && hashTest
3067
+ if (result instanceof Promise)
3068
+ throw new SearchParamError('Async validation not supported')
981
3069
 
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
- }
3070
+ if (result.issues)
3071
+ throw new SearchParamError(JSON.stringify(result.issues, undefined, 2), {
3072
+ cause: result,
3073
+ })
995
3074
 
996
- // All is well? Navigate!)
997
- router.__.navigate(nextOpts)
998
- }
999
- }
3075
+ return result.value
3076
+ }
1000
3077
 
1001
- // The click handler
1002
- const handleFocus = (e: MouseEvent) => {
1003
- if (preload) {
1004
- router.preloadRoute(nextOpts, {
1005
- maxAge: userPreloadMaxAge,
1006
- gcMaxAge: userPreloadGcMaxAge,
1007
- })
1008
- }
1009
- }
3078
+ if ('parse' in validateSearch) {
3079
+ return validateSearch.parse(input)
3080
+ }
1010
3081
 
1011
- const handleEnter = (e: MouseEvent) => {
1012
- const target = (e.target || {}) as LinkCurrentTargetElement
3082
+ if (typeof validateSearch === 'function') {
3083
+ return validateSearch(input)
3084
+ }
1013
3085
 
1014
- if (preload) {
1015
- if (target.preloadTimeout) {
1016
- return
1017
- }
3086
+ return {}
3087
+ }
1018
3088
 
1019
- target.preloadTimeout = setTimeout(() => {
1020
- target.preloadTimeout = null
1021
- router.preloadRoute(nextOpts, {
1022
- maxAge: userPreloadMaxAge,
1023
- gcMaxAge: userPreloadGcMaxAge,
1024
- })
1025
- }, preloadDelay)
1026
- }
1027
- }
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
+ }
1028
3104
 
1029
- const handleLeave = (e: MouseEvent) => {
1030
- const target = (e.target || {}) as LinkCurrentTargetElement
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
+ }
1031
3117
 
1032
- if (target.preloadTimeout) {
1033
- clearTimeout(target.preloadTimeout)
1034
- target.preloadTimeout = null
1035
- }
1036
- }
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>
1037
3127
 
1038
- return {
1039
- type: 'internal',
1040
- next,
1041
- handleFocus,
1042
- handleClick,
1043
- handleEnter,
1044
- handleLeave,
1045
- isActive,
1046
- disabled,
1047
- }
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
- )
1089
- }
1090
- throw new Error()
1091
- }
3128
+ const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
3129
+ childRoutes.forEach((childRoute, i) => {
3130
+ initRoute?.(childRoute, i)
1092
3131
 
1093
- ;(router.routesById as any)[route.routeId] = route
3132
+ const existingRoute = routesById[childRoute.id]
1094
3133
 
1095
- const children = routeConfig.children as RouteConfig[]
3134
+ invariant(
3135
+ !existingRoute,
3136
+ `Duplicate routes found with id: ${String(childRoute.id)}`,
3137
+ )
1096
3138
 
1097
- route.childRoutes = children?.length
1098
- ? recurseRoutes(children, route)
1099
- : undefined
3139
+ routesById[childRoute.id] = childRoute
1100
3140
 
1101
- return route
1102
- })
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
1103
3148
  }
3149
+ }
1104
3150
 
1105
- const routes = recurseRoutes([rootRouteConfig])
3151
+ const children = childRoute.children as Array<TRouteLike>
1106
3152
 
1107
- return routes[0]!
1108
- },
3153
+ if (children?.length) {
3154
+ recurseRoutes(children)
3155
+ }
3156
+ })
3157
+ }
1109
3158
 
1110
- parseLocation: (
1111
- location: History['location'],
1112
- previousLocation?: Location,
1113
- ): Location => {
1114
- const parsedSearch = router.options.parseSearch(location.search)
3159
+ recurseRoutes([routeTree])
1115
3160
 
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,
1124
- }
1125
- },
3161
+ const scoredRoutes: Array<{
3162
+ child: TRouteLike
3163
+ trimmed: string
3164
+ parsed: ReturnType<typeof parsePathname>
3165
+ index: number
3166
+ scores: Array<number>
3167
+ }> = []
1126
3168
 
1127
- navigate: (location: BuildNextOptions & { replace?: boolean }) => {
1128
- const next = router.buildNext(location)
1129
- return router.__.commitLocation(next, location.replace)
1130
- },
3169
+ const routes: Array<TRouteLike> = Object.values(routesById)
1131
3170
 
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 ?? '.'}`,
1143
- )
3171
+ routes.forEach((d, i) => {
3172
+ if (d.isRoot || !d.path) {
3173
+ return
3174
+ }
1144
3175
 
1145
- const fromMatches = router.matchRoutes(router.location.pathname, {
1146
- strictParseParams: true,
1147
- })
3176
+ const trimmed = trimPathLeft(d.fullPath)
3177
+ const parsed = parsePathname(trimmed)
1148
3178
 
1149
- const toMatches = router.matchRoutes(pathname)
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
+ }
1150
3183
 
1151
- const prevParams = { ...last(fromMatches)?.params }
3184
+ const scores = parsed.map((segment) => {
3185
+ if (segment.value === '/') {
3186
+ return 0.75
3187
+ }
1152
3188
 
1153
- let nextParams =
1154
- (dest.params ?? true) === true
1155
- ? prevParams
1156
- : functionalUpdate(dest.params!, prevParams)
3189
+ if (
3190
+ segment.type === 'param' &&
3191
+ segment.prefixSegment &&
3192
+ segment.suffixSegment
3193
+ ) {
3194
+ return 0.55
3195
+ }
1157
3196
 
1158
- if (nextParams) {
1159
- toMatches
1160
- .map((d) => d.options.stringifyParams)
1161
- .filter(Boolean)
1162
- .forEach((fn) => {
1163
- Object.assign({}, nextParams!, fn!(nextParams!))
1164
- })
1165
- }
3197
+ if (segment.type === 'param' && segment.prefixSegment) {
3198
+ return 0.52
3199
+ }
1166
3200
 
1167
- pathname = interpolatePath(pathname, nextParams ?? {})
3201
+ if (segment.type === 'param' && segment.suffixSegment) {
3202
+ return 0.51
3203
+ }
1168
3204
 
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
- : {}
3205
+ if (segment.type === 'param') {
3206
+ return 0.5
3207
+ }
1186
3208
 
1187
- // Then post filters
1188
- const postFilteredSearch = dest.__postSearchFilters?.length
1189
- ? dest.__postSearchFilters.reduce(
1190
- (prev, next) => next(prev),
1191
- destSearch,
1192
- )
1193
- : destSearch
3209
+ if (
3210
+ segment.type === 'wildcard' &&
3211
+ segment.prefixSegment &&
3212
+ segment.suffixSegment
3213
+ ) {
3214
+ return 0.3
3215
+ }
1194
3216
 
1195
- const search = replaceEqualDeep(
1196
- router.location.search,
1197
- postFilteredSearch,
1198
- )
3217
+ if (segment.type === 'wildcard' && segment.prefixSegment) {
3218
+ return 0.27
3219
+ }
1199
3220
 
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}` : ''
3221
+ if (segment.type === 'wildcard' && segment.suffixSegment) {
3222
+ return 0.26
3223
+ }
1206
3224
 
1207
- return {
1208
- pathname,
1209
- search,
1210
- searchStr,
1211
- state: router.location.state,
1212
- hash,
1213
- href: `${pathname}${searchStr}${hash}`,
1214
- key: dest.key,
1215
- }
1216
- },
3225
+ if (segment.type === 'wildcard') {
3226
+ return 0.25
3227
+ }
1217
3228
 
1218
- commitLocation: (next: Location, replace?: boolean): Promise<void> => {
1219
- const id = '' + Date.now() + Math.random()
3229
+ return 1
3230
+ })
1220
3231
 
1221
- if (router.navigateTimeout) clearTimeout(router.navigateTimeout)
3232
+ scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
3233
+ })
1222
3234
 
1223
- let nextAction: 'push' | 'replace' = 'replace'
3235
+ const flatRoutes = scoredRoutes
3236
+ .sort((a, b) => {
3237
+ const minLength = Math.min(a.scores.length, b.scores.length)
1224
3238
 
1225
- if (!replace) {
1226
- nextAction = 'push'
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]!
1227
3243
  }
3244
+ }
1228
3245
 
1229
- const isSameUrl =
1230
- router.__.parseLocation(history.location).href === next.href
3246
+ // Sort by length of score
3247
+ if (a.scores.length !== b.scores.length) {
3248
+ return b.scores.length - a.scores.length
3249
+ }
1231
3250
 
1232
- if (isSameUrl && !next.key) {
1233
- nextAction = 'replace'
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
1234
3255
  }
3256
+ }
1235
3257
 
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
- )
1258
- }
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
+ })
1259
3265
 
1260
- router.navigationPromise = new Promise((resolve) => {
1261
- const previousNavigationResolve = router.resolveNavigation
3266
+ return { routesById, routesByPath, flatRoutes }
3267
+ }
1262
3268
 
1263
- router.resolveNavigation = () => {
1264
- previousNavigationResolve()
1265
- resolve()
1266
- }
1267
- })
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
+ }
1268
3296
 
1269
- return router.navigationPromise
1270
- },
1271
- },
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
+ })
1272
3312
  }
1273
3313
 
1274
- router.update(userOptions)
3314
+ let routeCursor: TRouteLike = foundRoute || routesById[rootRouteId]!
1275
3315
 
1276
- // Allow frameworks to hook into the router creation
1277
- router.options.createRouter?.(router)
3316
+ const matchedRoutes: Array<TRouteLike> = [routeCursor]
1278
3317
 
1279
- return router
1280
- }
3318
+ while (routeCursor.parentRoute) {
3319
+ routeCursor = routeCursor.parentRoute as TRouteLike
3320
+ matchedRoutes.unshift(routeCursor)
3321
+ }
1281
3322
 
1282
- function isCtrlEvent(e: MouseEvent) {
1283
- return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
3323
+ return { matchedRoutes, routeParams, foundRoute }
1284
3324
  }