@tanstack/router-core 0.0.1-beta.19 → 0.0.1-beta.191

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/build/cjs/defer.js +39 -0
  3. package/build/cjs/defer.js.map +1 -0
  4. package/build/cjs/fileRoute.js +29 -0
  5. package/build/cjs/fileRoute.js.map +1 -0
  6. package/build/cjs/history.js +228 -0
  7. package/build/cjs/history.js.map +1 -0
  8. package/build/cjs/index.js +86 -0
  9. package/build/cjs/{packages/router-core/src/index.js.map → index.js.map} +1 -1
  10. package/build/cjs/{packages/router-core/src/path.js → path.js} +45 -56
  11. package/build/cjs/path.js.map +1 -0
  12. package/build/cjs/{packages/router-core/src/qss.js → qss.js} +10 -16
  13. package/build/cjs/qss.js.map +1 -0
  14. package/build/cjs/route.js +114 -0
  15. package/build/cjs/route.js.map +1 -0
  16. package/build/cjs/router.js +1267 -0
  17. package/build/cjs/router.js.map +1 -0
  18. package/build/cjs/scroll-restoration.js +139 -0
  19. package/build/cjs/scroll-restoration.js.map +1 -0
  20. package/build/cjs/{packages/router-core/src/searchParams.js → searchParams.js} +32 -19
  21. package/build/cjs/searchParams.js.map +1 -0
  22. package/build/cjs/{packages/router-core/src/utils.js → utils.js} +69 -64
  23. package/build/cjs/utils.js.map +1 -0
  24. package/build/esm/index.js +1746 -2121
  25. package/build/esm/index.js.map +1 -1
  26. package/build/stats-html.html +59 -49
  27. package/build/stats-react.json +197 -211
  28. package/build/types/defer.d.ts +19 -0
  29. package/build/types/fileRoute.d.ts +35 -0
  30. package/build/types/history.d.ts +36 -0
  31. package/build/types/index.d.ts +13 -609
  32. package/build/types/link.d.ts +96 -0
  33. package/build/types/path.d.ts +16 -0
  34. package/build/types/qss.d.ts +2 -0
  35. package/build/types/route.d.ts +251 -0
  36. package/build/types/routeInfo.d.ts +22 -0
  37. package/build/types/router.d.ts +260 -0
  38. package/build/types/scroll-restoration.d.ts +6 -0
  39. package/build/types/searchParams.d.ts +5 -0
  40. package/build/types/utils.d.ts +44 -0
  41. package/build/umd/index.development.js +1978 -2243
  42. package/build/umd/index.development.js.map +1 -1
  43. package/build/umd/index.production.js +13 -2
  44. package/build/umd/index.production.js.map +1 -1
  45. package/package.json +11 -7
  46. package/src/defer.ts +55 -0
  47. package/src/fileRoute.ts +161 -0
  48. package/src/history.ts +300 -0
  49. package/src/index.ts +5 -10
  50. package/src/link.ts +136 -125
  51. package/src/path.ts +37 -17
  52. package/src/qss.ts +1 -2
  53. package/src/route.ts +948 -218
  54. package/src/routeInfo.ts +45 -211
  55. package/src/router.ts +1778 -1075
  56. package/src/scroll-restoration.ts +179 -0
  57. package/src/searchParams.ts +31 -9
  58. package/src/utils.ts +84 -49
  59. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +0 -33
  60. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +0 -1
  61. package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js +0 -33
  62. package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js.map +0 -1
  63. package/build/cjs/node_modules/history/index.js +0 -815
  64. package/build/cjs/node_modules/history/index.js.map +0 -1
  65. package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js +0 -30
  66. package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js.map +0 -1
  67. package/build/cjs/packages/router-core/src/index.js +0 -58
  68. package/build/cjs/packages/router-core/src/path.js.map +0 -1
  69. package/build/cjs/packages/router-core/src/qss.js.map +0 -1
  70. package/build/cjs/packages/router-core/src/route.js +0 -147
  71. package/build/cjs/packages/router-core/src/route.js.map +0 -1
  72. package/build/cjs/packages/router-core/src/routeConfig.js +0 -69
  73. package/build/cjs/packages/router-core/src/routeConfig.js.map +0 -1
  74. package/build/cjs/packages/router-core/src/routeMatch.js +0 -220
  75. package/build/cjs/packages/router-core/src/routeMatch.js.map +0 -1
  76. package/build/cjs/packages/router-core/src/router.js +0 -870
  77. package/build/cjs/packages/router-core/src/router.js.map +0 -1
  78. package/build/cjs/packages/router-core/src/searchParams.js.map +0 -1
  79. package/build/cjs/packages/router-core/src/utils.js.map +0 -1
  80. package/src/frameworks.ts +0 -11
  81. package/src/routeConfig.ts +0 -511
  82. package/src/routeMatch.ts +0 -312
package/src/router.ts CHANGED
@@ -1,214 +1,234 @@
1
- import {
2
- BrowserHistory,
3
- createBrowserHistory,
4
- createMemoryHistory,
5
- HashHistory,
6
- History,
7
- MemoryHistory,
8
- } from 'history'
1
+ import { Store } from '@tanstack/store'
9
2
  import invariant from 'tiny-invariant'
10
- import { GetFrameworkGeneric } from './frameworks'
3
+
4
+ //
11
5
 
12
6
  import {
13
7
  LinkInfo,
14
8
  LinkOptions,
15
- NavigateOptionsAbsolute,
9
+ NavigateOptions,
16
10
  ToOptions,
17
- ValidFromPath,
11
+ ResolveRelativePath,
18
12
  } from './link'
19
13
  import {
20
14
  cleanPath,
21
15
  interpolatePath,
22
16
  joinPaths,
23
17
  matchPathname,
18
+ parsePathname,
24
19
  resolvePath,
20
+ trimPath,
21
+ trimPathRight,
25
22
  } from './path'
26
- import { AnyRoute, createRoute, Route } from './route'
27
23
  import {
28
- AnyLoaderData,
29
- AnyPathParams,
30
- AnyRouteConfig,
24
+ Route,
31
25
  AnySearchSchema,
32
- LoaderContext,
33
- RouteConfig,
34
- SearchFilter,
35
- } from './routeConfig'
26
+ AnyRoute,
27
+ AnyContext,
28
+ AnyPathParams,
29
+ RegisteredRouteComponent,
30
+ RegisteredErrorRouteComponent,
31
+ RegisteredPendingRouteComponent,
32
+ RouteMask,
33
+ } from './route'
36
34
  import {
37
- AllRouteInfo,
38
- AnyAllRouteInfo,
39
- AnyRouteInfo,
40
- RouteInfo,
41
35
  RoutesById,
36
+ RoutesByPath,
37
+ ParseRoute,
38
+ FullSearchSchema,
39
+ RouteById,
40
+ RoutePaths,
41
+ RouteIds,
42
42
  } from './routeInfo'
43
- import { createRouteMatch, RouteMatch } from './routeMatch'
44
43
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
45
44
  import {
46
45
  functionalUpdate,
47
46
  last,
47
+ NoInfer,
48
48
  pick,
49
49
  PickAsRequired,
50
- PickRequired,
51
- replaceEqualDeep,
52
50
  Timeout,
53
51
  Updater,
52
+ replaceEqualDeep,
53
+ partialDeepEqual,
54
+ NonNullableUpdater,
54
55
  } from './utils'
56
+ import {
57
+ createBrowserHistory,
58
+ createMemoryHistory,
59
+ HistoryLocation,
60
+ HistoryState,
61
+ RouterHistory,
62
+ } from './history'
63
+
64
+ //
65
+
66
+ declare global {
67
+ interface Window {
68
+ __TSR_DEHYDRATED__?: HydrationCtx
69
+ }
70
+ }
71
+
72
+ export interface Register {
73
+ // router: Router
74
+ }
55
75
 
56
76
  export interface LocationState {}
57
77
 
58
- export interface Location<
59
- TSearchObj extends AnySearchSchema = {},
60
- TState extends LocationState = LocationState,
61
- > {
78
+ export type AnyRouter = Router<any, any>
79
+
80
+ export type RegisteredRouter = Register extends {
81
+ router: infer TRouter extends AnyRouter
82
+ }
83
+ ? TRouter
84
+ : AnyRouter
85
+
86
+ export interface ParsedLocation<TSearchObj extends AnySearchSchema = {}> {
62
87
  href: string
63
88
  pathname: string
64
89
  search: TSearchObj
65
90
  searchStr: string
66
- state: TState
91
+ state: HistoryState
67
92
  hash: string
68
- key?: string
93
+ maskedLocation?: ParsedLocation<TSearchObj>
94
+ unmaskOnReload?: boolean
69
95
  }
70
96
 
71
97
  export interface FromLocation {
72
98
  pathname: string
73
99
  search?: unknown
74
- key?: string
75
100
  hash?: string
76
101
  }
77
102
 
78
103
  export type SearchSerializer = (searchObj: Record<string, any>) => string
79
104
  export type SearchParser = (searchStr: string) => Record<string, any>
80
- export type FilterRoutesFn = <TRoute extends Route<any, RouteInfo>>(
81
- routeConfigs: TRoute[],
82
- ) => TRoute[]
83
105
 
84
- export interface RouterOptions<TRouteConfig extends AnyRouteConfig> {
85
- history?: BrowserHistory | MemoryHistory | HashHistory
86
- stringifySearch?: SearchSerializer
87
- parseSearch?: SearchParser
88
- filterRoutes?: FilterRoutesFn
89
- defaultPreload?: false | 'intent'
90
- defaultPreloadMaxAge?: number
91
- defaultPreloadGcMaxAge?: number
92
- defaultPreloadDelay?: number
93
- defaultComponent?: GetFrameworkGeneric<'Component'>
94
- defaultErrorComponent?: GetFrameworkGeneric<'Component'>
95
- defaultPendingComponent?: GetFrameworkGeneric<'Component'>
96
- defaultLoaderMaxAge?: number
97
- defaultLoaderGcMaxAge?: number
98
- caseSensitive?: boolean
99
- routeConfig?: TRouteConfig
100
- basepath?: string
101
- useServerData?: boolean
102
- createRouter?: (router: Router<any, any>) => void
103
- createRoute?: (opts: { route: AnyRoute; router: Router<any, any> }) => void
104
- loadComponent?: (
105
- component: GetFrameworkGeneric<'Component'>,
106
- ) => Promise<GetFrameworkGeneric<'Component'>>
107
- // renderComponent?: (
108
- // component: GetFrameworkGeneric<'Component'>,
109
- // ) => GetFrameworkGeneric<'Element'>
106
+ export type HydrationCtx = {
107
+ router: DehydratedRouter
108
+ payload: Record<string, any>
110
109
  }
111
110
 
112
- export interface Action<
113
- TPayload = unknown,
114
- TResponse = unknown,
115
- // TError = unknown,
111
+ export interface RouteMatch<
112
+ TRouteTree extends AnyRoute = AnyRoute,
113
+ TRouteId extends RouteIds<TRouteTree> = ParseRoute<TRouteTree>['id'],
116
114
  > {
117
- submit: (
118
- submission?: TPayload,
119
- actionOpts?: { invalidate?: boolean; multi?: boolean },
120
- ) => Promise<TResponse>
121
- current?: ActionState<TPayload, TResponse>
122
- latest?: ActionState<TPayload, TResponse>
123
- submissions: ActionState<TPayload, TResponse>[]
115
+ id: string
116
+ key?: string
117
+ routeId: TRouteId
118
+ pathname: string
119
+ params: RouteById<TRouteTree, TRouteId>['types']['allParams']
120
+ status: 'pending' | 'success' | 'error'
121
+ isFetching: boolean
122
+ invalid: boolean
123
+ error: unknown
124
+ paramsError: unknown
125
+ searchError: unknown
126
+ updatedAt: number
127
+ maxAge: number
128
+ preloadMaxAge: number
129
+ loaderData: RouteById<TRouteTree, TRouteId>['types']['loader']
130
+ loadPromise?: Promise<void>
131
+ __resolveLoadPromise?: () => void
132
+ routeContext: RouteById<TRouteTree, TRouteId>['types']['routeContext']
133
+ context: RouteById<TRouteTree, TRouteId>['types']['context']
134
+ routeSearch: RouteById<TRouteTree, TRouteId>['types']['searchSchema']
135
+ search: FullSearchSchema<TRouteTree> &
136
+ RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema']
137
+ fetchedAt: number
138
+ abortController: AbortController
124
139
  }
125
140
 
126
- export interface ActionState<
127
- TPayload = unknown,
128
- TResponse = unknown,
129
- // TError = unknown,
130
- > {
131
- submittedAt: number
132
- status: 'idle' | 'pending' | 'success' | 'error'
133
- submission: TPayload
134
- isMulti: boolean
135
- data?: TResponse
136
- error?: unknown
137
- }
141
+ export type AnyRouteMatch = RouteMatch<any>
138
142
 
139
- export interface Loader<
140
- TFullSearchSchema extends AnySearchSchema = {},
141
- TAllParams extends AnyPathParams = {},
142
- TRouteLoaderData = AnyLoaderData,
143
- > {
144
- fetch: keyof PickRequired<TFullSearchSchema> extends never
145
- ? keyof TAllParams extends never
146
- ? (loaderContext: { signal?: AbortSignal }) => Promise<TRouteLoaderData>
147
- : (loaderContext: {
148
- params: TAllParams
149
- search?: TFullSearchSchema
150
- signal?: AbortSignal
151
- }) => Promise<TRouteLoaderData>
152
- : keyof TAllParams extends never
153
- ? (loaderContext: {
154
- search: TFullSearchSchema
155
- params: TAllParams
156
- signal?: AbortSignal
157
- }) => Promise<TRouteLoaderData>
158
- : (loaderContext: {
159
- search: TFullSearchSchema
160
- signal?: AbortSignal
161
- }) => Promise<TRouteLoaderData>
162
- current?: LoaderState<TFullSearchSchema, TAllParams>
163
- latest?: LoaderState<TFullSearchSchema, TAllParams>
164
- pending: LoaderState<TFullSearchSchema, TAllParams>[]
165
- }
143
+ export type RouterContextOptions<TRouteTree extends AnyRoute> =
144
+ AnyContext extends TRouteTree['types']['routerContext']
145
+ ? {
146
+ context?: TRouteTree['types']['routerContext']
147
+ }
148
+ : {
149
+ context: TRouteTree['types']['routerContext']
150
+ }
166
151
 
167
- export interface LoaderState<
168
- TFullSearchSchema extends AnySearchSchema = {},
169
- TAllParams extends AnyPathParams = {},
152
+ export interface RouterOptions<
153
+ TRouteTree extends AnyRoute,
154
+ TDehydrated extends Record<string, any>,
170
155
  > {
171
- loadedAt: number
172
- loaderContext: LoaderContext<AnyLoaderData, TFullSearchSchema, TAllParams>
156
+ history?: RouterHistory
157
+ stringifySearch?: SearchSerializer
158
+ parseSearch?: SearchParser
159
+ defaultPreload?: false | 'intent'
160
+ defaultPreloadDelay?: number
161
+ reloadOnWindowFocus?: boolean
162
+ defaultComponent?: RegisteredRouteComponent<
163
+ unknown,
164
+ AnySearchSchema,
165
+ AnyPathParams,
166
+ AnyContext,
167
+ AnyContext
168
+ >
169
+ defaultErrorComponent?: RegisteredErrorRouteComponent<
170
+ AnySearchSchema,
171
+ AnyPathParams,
172
+ AnyContext,
173
+ AnyContext
174
+ >
175
+ defaultPendingComponent?: RegisteredPendingRouteComponent<
176
+ AnySearchSchema,
177
+ AnyPathParams,
178
+ AnyContext,
179
+ AnyContext
180
+ >
181
+ defaultMaxAge?: number
182
+ defaultGcMaxAge?: number
183
+ defaultPreloadMaxAge?: number
184
+ caseSensitive?: boolean
185
+ routeTree?: TRouteTree
186
+ basepath?: string
187
+ createRoute?: (opts: { route: AnyRoute; router: AnyRouter }) => void
188
+ context?: TRouteTree['types']['routerContext']
189
+ dehydrate?: () => TDehydrated
190
+ hydrate?: (dehydrated: TDehydrated) => void
191
+ routeMasks?: RouteMask<TRouteTree>[]
192
+ unmaskOnReload?: boolean
173
193
  }
174
194
 
175
- export interface RouterState {
176
- status: 'idle' | 'loading'
177
- location: Location
178
- matches: RouteMatch[]
179
- lastUpdated: number
180
- actions: Record<string, Action>
181
- loaders: Record<string, Loader>
182
- pending?: PendingState
195
+ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
196
+ status: 'idle' | 'pending'
183
197
  isFetching: boolean
184
- isPreloading: boolean
185
- }
186
-
187
- export interface PendingState {
188
- location: Location
189
- matches: RouteMatch[]
198
+ matchesById: Record<string, RouteMatch<TRouteTree>>
199
+ matchIds: string[]
200
+ pendingMatchIds: string[]
201
+ matches: RouteMatch<TRouteTree>[]
202
+ pendingMatches: RouteMatch<TRouteTree>[]
203
+ renderedMatchIds: string[]
204
+ renderedMatches: RouteMatch<TRouteTree>[]
205
+ location: ParsedLocation<FullSearchSchema<TRouteTree>>
206
+ resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
207
+ lastUpdated: number
190
208
  }
191
209
 
192
- type Listener = (router: Router<any, any>) => void
193
-
194
- export type ListenerFn = () => void
210
+ export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void
195
211
 
196
212
  export interface BuildNextOptions {
197
213
  to?: string | number | null
198
- params?: true | Updater<Record<string, any>>
214
+ params?: true | Updater<unknown>
199
215
  search?: true | Updater<unknown>
200
216
  hash?: true | Updater<string>
201
- state?: LocationState
202
- key?: string
217
+ state?: true | NonNullableUpdater<LocationState>
218
+ mask?: {
219
+ to?: string | number | null
220
+ params?: true | Updater<unknown>
221
+ search?: true | Updater<unknown>
222
+ hash?: true | Updater<string>
223
+ state?: true | NonNullableUpdater<LocationState>
224
+ unmaskOnReload?: boolean
225
+ }
203
226
  from?: string
204
- fromCurrent?: boolean
205
- __preSearchFilters?: SearchFilter<any>[]
206
- __postSearchFilters?: SearchFilter<any>[]
207
227
  }
208
228
 
209
- export type MatchCacheEntry = {
210
- gc: number
211
- match: RouteMatch
229
+ export interface CommitLocationOptions {
230
+ replace?: boolean
231
+ resetScroll?: boolean
212
232
  }
213
233
 
214
234
  export interface MatchLocation {
@@ -216,1150 +236,1833 @@ export interface MatchLocation {
216
236
  fuzzy?: boolean
217
237
  caseSensitive?: boolean
218
238
  from?: string
219
- fromCurrent?: boolean
220
239
  }
221
240
 
222
241
  export interface MatchRouteOptions {
223
- pending: boolean
242
+ pending?: boolean
224
243
  caseSensitive?: boolean
244
+ includeSearch?: boolean
245
+ fuzzy?: boolean
225
246
  }
226
247
 
227
248
  type LinkCurrentTargetElement = {
228
249
  preloadTimeout?: null | ReturnType<typeof setTimeout>
229
250
  }
230
251
 
231
- interface DehydratedRouterState
232
- extends Pick<RouterState, 'status' | 'location' | 'lastUpdated'> {
233
- matches: DehydratedRouteMatch[]
252
+ export interface DehydratedRouterState {
253
+ matchIds: string[]
254
+ dehydratedMatches: DehydratedRouteMatch[]
255
+ }
256
+
257
+ export type DehydratedRouteMatch = Pick<
258
+ RouteMatch,
259
+ | 'fetchedAt'
260
+ | 'invalid'
261
+ | 'maxAge'
262
+ | 'preloadMaxAge'
263
+ | 'id'
264
+ | 'loaderData'
265
+ | 'status'
266
+ | 'updatedAt'
267
+ >
268
+
269
+ export interface DehydratedRouter {
270
+ state: DehydratedRouterState
271
+ }
272
+
273
+ export type RouterConstructorOptions<
274
+ TRouteTree extends AnyRoute,
275
+ TDehydrated extends Record<string, any>,
276
+ > = Omit<RouterOptions<TRouteTree, TDehydrated>, 'context'> &
277
+ RouterContextOptions<TRouteTree>
278
+
279
+ export const componentTypes = [
280
+ 'component',
281
+ 'errorComponent',
282
+ 'pendingComponent',
283
+ ] as const
284
+
285
+ export type RouterEvents = {
286
+ onBeforeLoad: {
287
+ type: 'onBeforeLoad'
288
+ from: ParsedLocation
289
+ to: ParsedLocation
290
+ pathChanged: boolean
291
+ }
292
+ onLoad: {
293
+ type: 'onLoad'
294
+ from: ParsedLocation
295
+ to: ParsedLocation
296
+ pathChanged: boolean
297
+ }
298
+ }
299
+
300
+ export type RouterEvent = RouterEvents[keyof RouterEvents]
301
+
302
+ export type RouterListener<TRouterEvent extends RouterEvent> = {
303
+ eventType: TRouterEvent['type']
304
+ fn: ListenerFn<TRouterEvent>
234
305
  }
235
306
 
236
- interface DehydratedRouteMatch
237
- extends Pick<
238
- RouteMatch<any, any>,
239
- | 'matchId'
240
- | 'status'
241
- | 'routeLoaderData'
242
- | 'loaderData'
243
- | 'isInvalid'
244
- | 'invalidAt'
245
- > {}
246
-
247
- export interface Router<
248
- TRouteConfig extends AnyRouteConfig = RouteConfig,
249
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
307
+ const visibilityChangeEvent = 'visibilitychange'
308
+ const focusEvent = 'focus'
309
+ const preloadWarning = 'Error preloading route! ☝️'
310
+
311
+ export class Router<
312
+ TRouteTree extends AnyRoute = AnyRoute,
313
+ TDehydrated extends Record<string, any> = Record<string, any>,
250
314
  > {
251
- types: {
252
- // Super secret internal stuff
253
- RouteConfig: TRouteConfig
254
- AllRouteInfo: TAllRouteInfo
315
+ types!: {
316
+ RootRoute: TRouteTree
255
317
  }
256
318
 
257
- // Public API
258
- history: BrowserHistory | MemoryHistory | HashHistory
259
319
  options: PickAsRequired<
260
- RouterOptions<TRouteConfig>,
261
- 'stringifySearch' | 'parseSearch'
320
+ RouterOptions<TRouteTree, TDehydrated>,
321
+ 'stringifySearch' | 'parseSearch' | 'context'
262
322
  >
263
- // Computed in this.update()
264
- basepath: string
265
- // Internal:
266
- listeners: Listener[]
267
- location: Location
268
- navigateTimeout?: Timeout
269
- nextAction?: 'push' | 'replace'
270
- state: RouterState
271
- routeTree: Route<TAllRouteInfo, RouteInfo>
272
- routesById: RoutesById<TAllRouteInfo>
273
- navigationPromise: Promise<void>
274
- startedLoadingAt: number
275
- resolveNavigation: () => void
276
- subscribe: (listener: Listener) => () => void
277
- reset: () => void
278
- notify: () => void
279
- mount: () => () => void
280
- onFocus: () => void
281
- update: <TRouteConfig extends RouteConfig = RouteConfig>(
282
- opts?: RouterOptions<TRouteConfig>,
283
- ) => Router<TRouteConfig>
284
-
285
- buildNext: (opts: BuildNextOptions) => Location
286
- cancelMatches: () => void
287
- load: (next?: Location) => Promise<void>
288
- matchCache: Record<string, MatchCacheEntry>
289
- cleanMatchCache: () => void
290
- getRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
291
- id: TId,
292
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
293
- loadRoute: (navigateOpts: BuildNextOptions) => Promise<RouteMatch[]>
294
- preloadRoute: (
295
- navigateOpts: BuildNextOptions,
296
- loaderOpts: { maxAge?: number; gcMaxAge?: number },
297
- ) => Promise<RouteMatch[]>
298
- matchRoutes: (
299
- pathname: string,
300
- opts?: { strictParseParams?: boolean },
301
- ) => RouteMatch[]
302
- loadMatches: (
303
- resolvedMatches: RouteMatch[],
304
- loaderOpts?:
305
- | { preload: true; maxAge: number; gcMaxAge: number }
306
- | { preload?: false; maxAge?: never; gcMaxAge?: never },
307
- ) => Promise<void>
308
- loadMatchData: (
309
- routeMatch: RouteMatch<any, any>,
310
- ) => Promise<Record<string, unknown>>
311
- invalidateRoute: (opts: MatchLocation) => void
312
- reload: () => Promise<void>
313
- resolvePath: (from: string, path: string) => string
314
- navigate: <
315
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
316
- TTo extends string = '.',
317
- >(
318
- opts: NavigateOptionsAbsolute<TAllRouteInfo, TFrom, TTo>,
319
- ) => Promise<void>
320
- matchRoute: <
321
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
322
- TTo extends string = '.',
323
- >(
324
- matchLocation: ToOptions<TAllRouteInfo, TFrom, TTo>,
325
- opts?: MatchRouteOptions,
326
- ) => boolean
327
- buildLink: <
328
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
329
- TTo extends string = '.',
330
- >(
331
- opts: LinkOptions<TAllRouteInfo, TFrom, TTo>,
332
- ) => LinkInfo
333
- dehydrateState: () => DehydratedRouterState
334
- hydrateState: (state: DehydratedRouterState) => void
335
- __: {
336
- buildRouteTree: (
337
- routeConfig: RouteConfig,
338
- ) => Route<TAllRouteInfo, AnyRouteInfo>
339
- parseLocation: (
340
- location: History['location'],
341
- previousLocation?: Location,
342
- ) => Location
343
- buildLocation: (dest: BuildNextOptions) => Location
344
- commitLocation: (next: Location, replace?: boolean) => Promise<void>
345
- navigate: (
346
- location: BuildNextOptions & { replace?: boolean },
347
- ) => Promise<void>
348
- }
349
- }
323
+ history!: RouterHistory
324
+ #unsubHistory?: () => void
325
+ basepath!: string
326
+ routeTree!: TRouteTree
327
+ routesById!: RoutesById<TRouteTree>
328
+ routesByPath!: RoutesByPath<TRouteTree>
329
+ flatRoutes!: ParseRoute<TRouteTree>[]
330
+ navigateTimeout: undefined | Timeout
331
+ nextAction: undefined | 'push' | 'replace'
332
+ navigationPromise: undefined | Promise<void>
333
+
334
+ __store: Store<RouterState<TRouteTree>>
335
+ state: RouterState<TRouteTree>
336
+ dehydratedData?: TDehydrated
337
+ resetNextScroll = false
338
+ tempLocationKey = `${Math.round(Math.random() * 10000000)}`
339
+ // nextTemporaryLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
340
+
341
+ constructor(options: RouterConstructorOptions<TRouteTree, TDehydrated>) {
342
+ this.options = {
343
+ defaultPreloadDelay: 50,
344
+ context: undefined!,
345
+ ...options,
346
+ stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
347
+ parseSearch: options?.parseSearch ?? defaultParseSearch,
348
+ // fetchServerDataFn: options?.fetchServerDataFn ?? defaultFetchServerDataFn,
349
+ }
350
350
 
351
- // Detect if we're in the DOM
352
- const isServer =
353
- typeof window === 'undefined' || !window.document?.createElement
351
+ this.__store = new Store<RouterState<TRouteTree>>(getInitialRouterState(), {
352
+ onUpdate: () => {
353
+ const prev = this.state
354
354
 
355
- // This is the default history object if none is defined
356
- const createDefaultHistory = () =>
357
- isServer ? createMemoryHistory() : createBrowserHistory()
355
+ const next = this.__store.state
358
356
 
359
- function getInitialRouterState(): RouterState {
360
- return {
361
- status: 'idle',
362
- location: null!,
363
- matches: [],
364
- actions: {},
365
- loaders: {},
366
- lastUpdated: Date.now(),
367
- isFetching: false,
368
- isPreloading: false,
369
- }
370
- }
357
+ const matchesByIdChanged = prev.matchesById !== next.matchesById
358
+ let matchesChanged
359
+ let pendingMatchesChanged
371
360
 
372
- export function createRouter<
373
- TRouteConfig extends AnyRouteConfig = RouteConfig,
374
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
375
- >(
376
- userOptions?: RouterOptions<TRouteConfig>,
377
- ): Router<TRouteConfig, TAllRouteInfo> {
378
- const history = userOptions?.history || createDefaultHistory()
379
-
380
- const originalOptions = {
381
- defaultLoaderGcMaxAge: 5 * 60 * 1000,
382
- defaultLoaderMaxAge: 0,
383
- defaultPreloadMaxAge: 2000,
384
- defaultPreloadDelay: 50,
385
- ...userOptions,
386
- stringifySearch: userOptions?.stringifySearch ?? defaultStringifySearch,
387
- parseSearch: userOptions?.parseSearch ?? defaultParseSearch,
388
- }
361
+ if (!matchesByIdChanged) {
362
+ matchesChanged =
363
+ prev.matchIds.length !== next.matchIds.length ||
364
+ prev.matchIds.some((d, i) => d !== next.matchIds[i])
389
365
 
390
- let router: Router<TRouteConfig, TAllRouteInfo> = {
391
- types: undefined!,
392
-
393
- // public api
394
- history,
395
- options: originalOptions,
396
- listeners: [],
397
- // Resolved after construction
398
- basepath: '',
399
- routeTree: undefined!,
400
- routesById: {} as any,
401
- location: undefined!,
402
- //
403
- navigationPromise: Promise.resolve(),
404
- resolveNavigation: () => {},
405
- matchCache: {},
406
- state: getInitialRouterState(),
407
- reset: () => {
408
- router.state = getInitialRouterState()
409
- router.notify()
410
- },
411
- startedLoadingAt: Date.now(),
412
- subscribe: (listener: Listener): (() => void) => {
413
- router.listeners.push(listener as Listener)
414
- return () => {
415
- router.listeners = router.listeners.filter((x) => x !== listener)
416
- }
417
- },
418
- getRoute: (id) => {
419
- return router.routesById[id]
420
- },
421
- notify: (): void => {
422
- const isFetching =
423
- router.state.status === 'loading' ||
424
- router.state.matches.some((d) => d.isFetching)
425
-
426
- const isPreloading = Object.values(router.matchCache).some(
427
- (d) =>
428
- d.match.isFetching &&
429
- !router.state.matches.find((dd) => dd.matchId === d.match.matchId),
430
- )
366
+ pendingMatchesChanged =
367
+ prev.pendingMatchIds.length !== next.pendingMatchIds.length ||
368
+ prev.pendingMatchIds.some((d, i) => d !== next.pendingMatchIds[i])
369
+ }
431
370
 
432
- if (
433
- router.state.isFetching !== isFetching ||
434
- router.state.isPreloading !== isPreloading
435
- ) {
436
- router.state = {
437
- ...router.state,
438
- isFetching,
439
- isPreloading,
371
+ if (matchesByIdChanged || matchesChanged) {
372
+ next.matches = next.matchIds.map((id) => {
373
+ return next.matchesById[id] as any
374
+ })
440
375
  }
441
- }
442
376
 
443
- cascadeLoaderData(router.state.matches)
444
- router.listeners.forEach((listener) => listener(router))
445
- },
377
+ if (matchesByIdChanged || pendingMatchesChanged) {
378
+ next.pendingMatches = next.pendingMatchIds.map((id) => {
379
+ return next.matchesById[id] as any
380
+ })
381
+ }
446
382
 
447
- dehydrateState: () => {
448
- return {
449
- ...pick(router.state, ['status', 'location', 'lastUpdated']),
450
- matches: router.state.matches.map((match) =>
451
- pick(match, [
452
- 'matchId',
453
- 'status',
454
- 'routeLoaderData',
455
- 'loaderData',
456
- 'isInvalid',
457
- 'invalidAt',
458
- ]),
459
- ),
460
- }
461
- },
383
+ if (matchesByIdChanged || matchesChanged || pendingMatchesChanged) {
384
+ const hasPendingComponent = next.pendingMatches.some((d) => {
385
+ const route = this.getRoute(d.routeId as any)
386
+ return !!route?.options.pendingComponent
387
+ })
462
388
 
463
- hydrateState: (dehydratedState) => {
464
- // Match the routes
465
- const matches = router.matchRoutes(router.location.pathname, {
466
- strictParseParams: true,
467
- })
389
+ next.renderedMatchIds = hasPendingComponent
390
+ ? next.pendingMatchIds
391
+ : next.matchIds
468
392
 
469
- matches.forEach((match, index) => {
470
- const dehydratedMatch = dehydratedState.matches[index]
471
- invariant(
472
- dehydratedMatch,
473
- 'Oh no! Dehydrated route matches did not match the active state of the router 😬',
393
+ next.renderedMatches = next.renderedMatchIds.map((id) => {
394
+ return next.matchesById[id] as any
395
+ })
396
+ }
397
+
398
+ next.isFetching = [...next.matches, ...next.pendingMatches].some(
399
+ (d) => d.isFetching,
474
400
  )
475
- Object.assign(match, dehydratedMatch)
476
- })
477
401
 
478
- matches.forEach((match) => match.__.validate())
402
+ this.state = next
403
+ },
404
+ defaultPriority: 'low',
405
+ })
479
406
 
480
- router.state = {
481
- ...router.state,
482
- ...dehydratedState,
483
- matches,
484
- }
485
- },
407
+ this.state = this.__store.state
486
408
 
487
- mount: () => {
488
- const next = router.__.buildLocation({
489
- to: '.',
490
- search: true,
491
- hash: true,
492
- })
409
+ this.update(options)
493
410
 
494
- // If the current location isn't updated, trigger a navigation
495
- // to the current location. Otherwise, load the current location.
496
- if (next.href !== router.location.href) {
497
- router.__.commitLocation(next, true)
498
- }
411
+ const nextLocation = this.buildLocation({
412
+ search: true,
413
+ params: true,
414
+ hash: true,
415
+ state: true,
416
+ })
499
417
 
500
- if (!router.state.matches.length) {
501
- router.load()
502
- }
418
+ if (this.state.location.href !== nextLocation.href) {
419
+ this.#commitLocation({ ...nextLocation, replace: true })
420
+ }
421
+ }
503
422
 
504
- const unsub = router.history.listen((event) => {
505
- router.load(router.__.parseLocation(event.location, router.location))
506
- })
423
+ subscribers = new Set<RouterListener<RouterEvent>>()
507
424
 
508
- // addEventListener does not exist in React Native, but window does
509
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
510
- if (!isServer && window.addEventListener) {
511
- // Listen to visibillitychange and focus
512
- window.addEventListener('visibilitychange', router.onFocus, false)
513
- window.addEventListener('focus', router.onFocus, false)
514
- }
425
+ subscribe = <TType extends keyof RouterEvents>(
426
+ eventType: TType,
427
+ fn: ListenerFn<RouterEvents[TType]>,
428
+ ) => {
429
+ const listener: RouterListener<any> = {
430
+ eventType,
431
+ fn,
432
+ }
515
433
 
516
- return () => {
517
- unsub()
518
- if (!isServer && window.removeEventListener) {
519
- // Be sure to unsubscribe if a new handler is set
520
- window.removeEventListener('visibilitychange', router.onFocus)
521
- window.removeEventListener('focus', router.onFocus)
522
- }
523
- }
524
- },
434
+ this.subscribers.add(listener)
525
435
 
526
- onFocus: () => {
527
- router.load()
528
- },
436
+ return () => {
437
+ this.subscribers.delete(listener)
438
+ }
439
+ }
529
440
 
530
- update: (opts) => {
531
- const newHistory = opts?.history !== router.history
532
- if (!router.location || newHistory) {
533
- if (opts?.history) {
534
- router.history = opts.history
535
- }
536
- router.location = router.__.parseLocation(router.history.location)
537
- router.state.location = router.location
441
+ #emit = (routerEvent: RouterEvent) => {
442
+ this.subscribers.forEach((listener) => {
443
+ if (listener.eventType === routerEvent.type) {
444
+ listener.fn(routerEvent)
538
445
  }
446
+ })
447
+ }
539
448
 
540
- Object.assign(router.options, opts)
449
+ reset = () => {
450
+ this.__store.setState((s) => Object.assign(s, getInitialRouterState()))
451
+ }
541
452
 
542
- const { basepath, routeConfig } = router.options
453
+ mount = () => {
454
+ // addEventListener does not exist in React Native, but window does
455
+ // In the future, we might need to invert control here for more adapters
456
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
457
+ if (typeof window !== 'undefined' && window.addEventListener) {
458
+ window.addEventListener(visibilityChangeEvent, this.#onFocus, false)
459
+ window.addEventListener(focusEvent, this.#onFocus, false)
460
+ }
543
461
 
544
- router.basepath = cleanPath(`/${basepath ?? ''}`)
462
+ this.safeLoad()
545
463
 
546
- if (routeConfig) {
547
- router.routesById = {} as any
548
- router.routeTree = router.__.buildRouteTree(routeConfig)
464
+ return () => {
465
+ if (typeof window !== 'undefined' && window.removeEventListener) {
466
+ window.removeEventListener(visibilityChangeEvent, this.#onFocus)
467
+ window.removeEventListener(focusEvent, this.#onFocus)
549
468
  }
469
+ }
470
+ }
550
471
 
551
- return router as any
552
- },
553
-
554
- cancelMatches: () => {
555
- ;[
556
- ...router.state.matches,
557
- ...(router.state.pending?.matches ?? []),
558
- ].forEach((match) => {
559
- match.cancel()
472
+ #onFocus = () => {
473
+ if (this.options.reloadOnWindowFocus ?? true) {
474
+ this.invalidate({
475
+ __fromFocus: true,
560
476
  })
561
- },
477
+ }
478
+ }
562
479
 
563
- load: async (next?: Location) => {
564
- const id = Math.random()
565
- router.startedLoadingAt = id
480
+ update = (opts?: RouterOptions<any, any>): this => {
481
+ this.options = {
482
+ ...this.options,
483
+ ...opts,
484
+ context: {
485
+ ...this.options.context,
486
+ ...opts?.context,
487
+ },
488
+ }
566
489
 
567
- if (next) {
568
- // Ingest the new location
569
- router.location = next
490
+ if (
491
+ !this.history ||
492
+ (this.options.history && this.options.history !== this.history)
493
+ ) {
494
+ if (this.#unsubHistory) {
495
+ this.#unsubHistory()
570
496
  }
571
497
 
572
- // Cancel any pending matches
573
- router.cancelMatches()
498
+ this.history =
499
+ this.options.history ??
500
+ (isServer ? createMemoryHistory() : createBrowserHistory()!)
574
501
 
575
- // Match the routes
576
- const matches = router.matchRoutes(router.location.pathname, {
577
- strictParseParams: true,
502
+ const parsedLocation = this.#parseLocation()
503
+
504
+ this.__store.setState((s) => ({
505
+ ...s,
506
+ resolvedLocation: parsedLocation as any,
507
+ location: parsedLocation as any,
508
+ }))
509
+
510
+ this.#unsubHistory = this.history.subscribe(() => {
511
+ this.safeLoad({
512
+ next: this.#parseLocation(this.state.location),
513
+ })
578
514
  })
515
+ }
579
516
 
580
- if (typeof document !== 'undefined') {
581
- router.state = {
582
- ...router.state,
583
- pending: {
584
- matches: matches,
585
- location: router.location,
586
- },
587
- status: 'loading',
588
- }
589
- } else {
590
- router.state = {
591
- ...router.state,
592
- matches: matches,
593
- location: router.location,
594
- status: 'loading',
595
- }
596
- }
517
+ const { basepath, routeTree } = this.options
597
518
 
598
- router.notify()
519
+ this.basepath = `/${trimPath(basepath ?? '') ?? ''}`
599
520
 
600
- // Load the matches
601
- await router.loadMatches(matches)
521
+ if (routeTree && routeTree !== this.routeTree) {
522
+ this.#processRoutes(routeTree)
523
+ }
602
524
 
603
- if (router.startedLoadingAt !== id) {
604
- // Ignore side-effects of match loading
605
- return router.navigationPromise
606
- }
525
+ return this
526
+ }
607
527
 
608
- const previousMatches = router.state.matches
528
+ cancelMatches = () => {
529
+ this.state.matches.forEach((match) => {
530
+ this.cancelMatch(match.id)
531
+ })
532
+ }
609
533
 
610
- const exiting: RouteMatch[] = [],
611
- staying: RouteMatch[] = []
534
+ cancelMatch = (id: string) => {
535
+ this.getRouteMatch(id)?.abortController?.abort()
536
+ }
612
537
 
613
- previousMatches.forEach((d) => {
614
- if (matches.find((dd) => dd.matchId === d.matchId)) {
615
- staying.push(d)
616
- } else {
617
- exiting.push(d)
618
- }
619
- })
538
+ safeLoad = async (opts?: { next?: ParsedLocation }) => {
539
+ try {
540
+ return this.load(opts)
541
+ } catch (err) {
542
+ // Don't do anything
543
+ }
544
+ }
620
545
 
621
- const entering = matches.filter((d) => {
622
- return !previousMatches.find((dd) => dd.matchId === d.matchId)
623
- })
546
+ latestLoadPromise: Promise<void> = Promise.resolve()
547
+
548
+ load = async (opts?: {
549
+ next?: ParsedLocation
550
+ throwOnError?: boolean
551
+ __dehydratedMatches?: DehydratedRouteMatch[]
552
+ }) => {
553
+ const promise = new Promise<void>(async (resolve, reject) => {
554
+ const prevLocation = this.state.resolvedLocation
555
+ const pathDidChange = !!(
556
+ opts?.next && prevLocation!.href !== opts.next.href
557
+ )
624
558
 
625
- const now = Date.now()
559
+ let latestPromise: Promise<void> | undefined | null
626
560
 
627
- exiting.forEach((d) => {
628
- d.__.onExit?.({
629
- params: d.params,
630
- search: d.routeSearch,
631
- })
561
+ const checkLatest = (): undefined | Promise<void> | null => {
562
+ return this.latestLoadPromise !== promise
563
+ ? this.latestLoadPromise
564
+ : undefined
565
+ }
632
566
 
633
- // Clear idle error states when match leaves
634
- if (d.status === 'error' && !d.isFetching) {
635
- d.status = 'idle'
636
- d.error = undefined
637
- }
638
- const gc = Math.max(
639
- d.options.loaderGcMaxAge ?? router.options.defaultLoaderGcMaxAge ?? 0,
640
- d.options.loaderMaxAge ?? router.options.defaultLoaderMaxAge ?? 0,
641
- )
642
- if (gc > 0) {
643
- router.matchCache[d.matchId] = {
644
- gc: gc == Infinity ? Number.MAX_SAFE_INTEGER : now + gc,
645
- match: d,
646
- }
647
- }
648
- })
567
+ // Cancel any pending matches
649
568
 
650
- staying.forEach((d) => {
651
- d.options.onTransition?.({
652
- params: d.params,
653
- search: d.routeSearch,
654
- })
569
+ let pendingMatches!: RouteMatch<any, any>[]
570
+
571
+ this.#emit({
572
+ type: 'onBeforeLoad',
573
+ from: prevLocation,
574
+ to: opts?.next ?? this.state.location,
575
+ pathChanged: pathDidChange,
655
576
  })
656
577
 
657
- entering.forEach((d) => {
658
- d.__.onExit = d.options.onMatch?.({
659
- params: d.params,
660
- search: d.search,
661
- })
662
- delete router.matchCache[d.matchId]
578
+ this.__store.batch(() => {
579
+ if (opts?.next) {
580
+ // Ingest the new location
581
+ this.__store.setState((s) => ({
582
+ ...s,
583
+ location: opts.next! as any,
584
+ }))
585
+ }
586
+
587
+ // Match the routes
588
+ pendingMatches = this.matchRoutes(
589
+ this.state.location.pathname,
590
+ this.state.location.search,
591
+ {
592
+ throwOnError: opts?.throwOnError,
593
+ debug: true,
594
+ },
595
+ )
596
+
597
+ this.__store.setState((s) => ({
598
+ ...s,
599
+ status: 'pending',
600
+ pendingMatchIds: pendingMatches.map((d) => d.id),
601
+ matchesById: this.#mergeMatches(s.matchesById, pendingMatches),
602
+ }))
663
603
  })
664
604
 
665
- if (router.startedLoadingAt !== id) {
666
- // Ignore side-effects of match loading
667
- return
668
- }
605
+ try {
606
+ // Load the matches
607
+ try {
608
+ await this.loadMatches(pendingMatches.map((d) => d.id))
609
+ } catch (err) {
610
+ // swallow this error, since we'll display the
611
+ // errors on the route components
612
+ }
669
613
 
670
- matches.forEach((match) => {
671
- // Clear actions
672
- if (match.action) {
673
- match.action.current = undefined
674
- match.action.submissions = []
614
+ // Only apply the latest transition
615
+ if ((latestPromise = checkLatest())) {
616
+ return latestPromise
675
617
  }
676
- })
677
618
 
678
- router.state = {
679
- ...router.state,
680
- location: router.location,
681
- matches,
682
- pending: undefined,
683
- status: 'idle',
684
- }
619
+ const exitingMatchIds = this.state.matchIds.filter(
620
+ (id) => !this.state.pendingMatchIds.includes(id),
621
+ )
685
622
 
686
- router.notify()
687
- router.resolveNavigation()
688
- },
623
+ const enteringMatchIds = this.state.pendingMatchIds.filter(
624
+ (id) => !this.state.matchIds.includes(id),
625
+ )
689
626
 
690
- cleanMatchCache: () => {
691
- const now = Date.now()
627
+ const stayingMatchIds = this.state.matchIds.filter((id) =>
628
+ this.state.pendingMatchIds.includes(id),
629
+ )
692
630
 
693
- Object.keys(router.matchCache).forEach((matchId) => {
694
- const entry = router.matchCache[matchId]!
631
+ this.__store.setState((s) => ({
632
+ ...s,
633
+ status: 'idle',
634
+ resolvedLocation: s.location,
635
+ matchIds: s.pendingMatchIds,
636
+ pendingMatchIds: [],
637
+ }))
638
+ ;(
639
+ [
640
+ [exitingMatchIds, 'onLeave'],
641
+ [enteringMatchIds, 'onEnter'],
642
+ [stayingMatchIds, 'onTransition'],
643
+ ] as const
644
+ ).forEach(([matchIds, hook]) => {
645
+ matchIds.forEach((id) => {
646
+ const match = this.getRouteMatch(id)!
647
+ const route = this.getRoute(match.routeId)
648
+ route.options[hook]?.(match)
649
+ })
650
+ })
695
651
 
696
- // Don't remove loading matches
697
- if (entry.match.status === 'loading') {
698
- return
699
- }
652
+ this.#emit({
653
+ type: 'onLoad',
654
+ from: prevLocation,
655
+ to: this.state.location,
656
+ pathChanged: pathDidChange,
657
+ })
700
658
 
701
- // Do not remove successful matches that are still valid
702
- if (entry.gc > 0 && entry.gc > now) {
703
- return
659
+ resolve()
660
+ } catch (err) {
661
+ // Only apply the latest transition
662
+ if ((latestPromise = checkLatest())) {
663
+ return latestPromise
704
664
  }
705
665
 
706
- // Everything else gets removed
707
- delete router.matchCache[matchId]
708
- })
709
- },
666
+ reject(err)
667
+ }
668
+ })
710
669
 
711
- loadRoute: async (navigateOpts = router.location) => {
712
- const next = router.buildNext(navigateOpts)
713
- const matches = router.matchRoutes(next.pathname, {
714
- strictParseParams: true,
715
- })
716
- await router.loadMatches(matches)
717
- return matches
718
- },
670
+ this.latestLoadPromise = promise
719
671
 
720
- preloadRoute: async (navigateOpts = router.location, loaderOpts) => {
721
- const next = router.buildNext(navigateOpts)
722
- const matches = router.matchRoutes(next.pathname, {
723
- strictParseParams: true,
724
- })
725
- await router.loadMatches(matches, {
726
- preload: true,
727
- maxAge:
728
- loaderOpts.maxAge ??
729
- router.options.defaultPreloadMaxAge ??
730
- router.options.defaultLoaderMaxAge ??
731
- 0,
732
- gcMaxAge:
733
- loaderOpts.gcMaxAge ??
734
- router.options.defaultPreloadGcMaxAge ??
735
- router.options.defaultLoaderGcMaxAge ??
736
- 0,
737
- })
738
- return matches
739
- },
672
+ this.latestLoadPromise.then(() => {
673
+ this.cleanMatches()
674
+ })
740
675
 
741
- matchRoutes: (pathname, opts) => {
742
- router.cleanMatchCache()
676
+ return this.latestLoadPromise
677
+ }
743
678
 
744
- const matches: RouteMatch[] = []
679
+ #mergeMatches = (
680
+ prevMatchesById: Record<string, RouteMatch<TRouteTree>>,
681
+ nextMatches: AnyRouteMatch[],
682
+ ): Record<string, RouteMatch<TRouteTree>> => {
683
+ let matchesById = { ...prevMatchesById }
745
684
 
746
- if (!router.routeTree) {
747
- return matches
685
+ nextMatches.forEach((match) => {
686
+ if (!matchesById[match.id]) {
687
+ matchesById[match.id] = match
748
688
  }
749
689
 
750
- const existingMatches = [
751
- ...router.state.matches,
752
- ...(router.state.pending?.matches ?? []),
753
- ]
754
-
755
- const recurse = async (routes: Route<any, any>[]): Promise<void> => {
756
- const parentMatch = last(matches)
757
- let params = parentMatch?.params ?? {}
758
-
759
- const filteredRoutes = router.options.filterRoutes?.(routes) ?? routes
690
+ matchesById[match.id] = {
691
+ ...matchesById[match.id],
692
+ ...match,
693
+ }
694
+ })
760
695
 
761
- let foundRoutes: Route[] = []
696
+ return matchesById
697
+ }
762
698
 
763
- const findMatchInRoutes = (parentRoutes: Route[], routes: Route[]) => {
764
- routes.some((route) => {
765
- if (!route.routePath && route.childRoutes?.length) {
766
- return findMatchInRoutes(
767
- [...foundRoutes, route],
768
- route.childRoutes,
769
- )
770
- }
699
+ getRoute = (id: string): Route => {
700
+ const route = (this.routesById as any)[id]
771
701
 
772
- const fuzzy = !!(
773
- route.routePath !== '/' || route.childRoutes?.length
774
- )
702
+ invariant(route, `Route with id "${id as string}" not found`)
775
703
 
776
- const matchParams = matchPathname(pathname, {
777
- to: route.fullPath,
778
- fuzzy,
779
- caseSensitive:
780
- route.options.caseSensitive ?? router.options.caseSensitive,
781
- })
704
+ return route as any
705
+ }
782
706
 
783
- if (matchParams) {
784
- let parsedParams
707
+ preloadRoute = async (
708
+ navigateOpts: BuildNextOptions & {
709
+ maxAge?: number
710
+ } = this.state.location,
711
+ ) => {
712
+ let next = this.buildLocation(navigateOpts)
785
713
 
786
- try {
787
- parsedParams =
788
- route.options.parseParams?.(matchParams!) ?? matchParams
789
- } catch (err) {
790
- if (opts?.strictParseParams) {
791
- throw err
792
- }
793
- }
714
+ const matches = this.matchRoutes(next.pathname, next.search, {
715
+ throwOnError: true,
716
+ })
794
717
 
795
- params = {
796
- ...params,
797
- ...parsedParams,
798
- }
799
- }
718
+ this.__store.setState((s) => {
719
+ return {
720
+ ...s,
721
+ matchesById: this.#mergeMatches(s.matchesById, matches),
722
+ }
723
+ })
800
724
 
801
- if (!!matchParams) {
802
- foundRoutes = [...parentRoutes, route]
803
- }
725
+ await this.loadMatches(
726
+ matches.map((d) => d.id),
727
+ {
728
+ preload: true,
729
+ maxAge: navigateOpts.maxAge,
730
+ },
731
+ )
804
732
 
805
- return !!foundRoutes.length
806
- })
733
+ return [last(matches)!, matches] as const
734
+ }
807
735
 
808
- return !!foundRoutes.length
809
- }
736
+ cleanMatches = () => {
737
+ const now = Date.now()
810
738
 
811
- findMatchInRoutes([], filteredRoutes)
739
+ const outdatedMatchIds = Object.values(this.state.matchesById)
740
+ .filter((match) => {
741
+ const route = this.getRoute(match.routeId)
812
742
 
813
- if (!foundRoutes.length) {
814
- return
815
- }
743
+ return (
744
+ !this.state.matchIds.includes(match.id) &&
745
+ !this.state.pendingMatchIds.includes(match.id) &&
746
+ (match.preloadMaxAge > -1
747
+ ? match.updatedAt + match.preloadMaxAge < now
748
+ : true) &&
749
+ (route.options.gcMaxAge
750
+ ? match.updatedAt + route.options.gcMaxAge < now
751
+ : true)
752
+ )
753
+ })
754
+ .map((d) => d.id)
816
755
 
817
- foundRoutes.forEach((foundRoute) => {
818
- const interpolatedPath = interpolatePath(foundRoute.routePath, params)
819
- const matchId = interpolatePath(foundRoute.routeId, params, true)
820
-
821
- const match =
822
- existingMatches.find((d) => d.matchId === matchId) ||
823
- router.matchCache[matchId]?.match ||
824
- createRouteMatch(router, foundRoute, {
825
- parentMatch,
826
- matchId,
827
- params,
828
- pathname: joinPaths([pathname, interpolatedPath]),
829
- })
830
-
831
- matches.push(match)
756
+ if (outdatedMatchIds.length) {
757
+ this.__store.setState((s) => {
758
+ const matchesById = { ...s.matchesById }
759
+ outdatedMatchIds.forEach((id) => {
760
+ delete matchesById[id]
832
761
  })
762
+ return {
763
+ ...s,
764
+ matchesById,
765
+ }
766
+ })
767
+ }
768
+ }
833
769
 
834
- const foundRoute = last(foundRoutes)!
770
+ matchRoutes = (
771
+ pathname: string,
772
+ locationSearch: AnySearchSchema,
773
+ opts?: { throwOnError?: boolean; debug?: boolean },
774
+ ): RouteMatch<TRouteTree>[] => {
775
+ let routeParams: AnyPathParams = {}
776
+
777
+ let foundRoute = this.flatRoutes.find((route) => {
778
+ const matchedParams = matchPathname(
779
+ this.basepath,
780
+ trimPathRight(pathname),
781
+ {
782
+ to: route.fullPath,
783
+ caseSensitive:
784
+ route.options.caseSensitive ?? this.options.caseSensitive,
785
+ },
786
+ )
835
787
 
836
- if (foundRoute.childRoutes?.length) {
837
- recurse(foundRoute.childRoutes)
838
- }
788
+ if (matchedParams) {
789
+ routeParams = matchedParams
790
+ return true
839
791
  }
840
792
 
841
- recurse([router.routeTree])
793
+ return false
794
+ })
842
795
 
843
- cascadeLoaderData(matches)
796
+ let routeCursor: AnyRoute =
797
+ foundRoute || (this.routesById as any)['__root__']
844
798
 
845
- return matches
846
- },
799
+ let matchedRoutes: AnyRoute[] = [routeCursor]
800
+ // let includingLayouts = true
801
+ while (routeCursor?.parentRoute) {
802
+ routeCursor = routeCursor.parentRoute
803
+ if (routeCursor) matchedRoutes.unshift(routeCursor)
804
+ }
847
805
 
848
- loadMatches: async (resolvedMatches, loaderOpts) => {
849
- const matchPromises = resolvedMatches.map(async (match) => {
850
- // Validate the match (loads search params etc)
851
- match.__.validate()
852
- match.load(loaderOpts)
806
+ // Existing matches are matches that are already loaded along with
807
+ // pending matches that are still loading
853
808
 
854
- const search = match.search as { __data?: any }
809
+ const parseErrors = matchedRoutes.map((route) => {
810
+ let parsedParamsError
855
811
 
856
- if (search.__data && search.__data.matchId !== match.matchId) {
857
- return
858
- }
812
+ if (route.options.parseParams) {
813
+ try {
814
+ const parsedParams = route.options.parseParams(routeParams)
815
+ // Add the parsed params to the accumulated params bag
816
+ Object.assign(routeParams, parsedParams)
817
+ } catch (err: any) {
818
+ parsedParamsError = new PathParamError(err.message, {
819
+ cause: err,
820
+ })
821
+
822
+ if (opts?.throwOnError) {
823
+ throw parsedParamsError
824
+ }
859
825
 
860
- if (match.__.loadPromise) {
861
- // Wait for the first sign of activity from the match
862
- await match.__.loadPromise
826
+ return parsedParamsError
863
827
  }
864
- })
828
+ }
865
829
 
866
- router.notify()
830
+ return
831
+ })
867
832
 
868
- await Promise.all(matchPromises)
869
- },
833
+ const matches = matchedRoutes.map((route, index) => {
834
+ const interpolatedPath = interpolatePath(route.path, routeParams)
835
+ const key = route.options.key
836
+ ? route.options.key({
837
+ params: routeParams,
838
+ search: locationSearch,
839
+ }) ?? ''
840
+ : ''
870
841
 
871
- loadMatchData: async (routeMatch) => {
872
- if (isServer || !router.options.useServerData) {
873
- return (
874
- (await routeMatch.options.loader?.({
875
- // parentLoaderPromise: routeMatch.parentMatch?.__.dataPromise,
876
- params: routeMatch.params,
877
- search: routeMatch.routeSearch,
878
- signal: routeMatch.__.abortController.signal,
879
- })) ?? {}
880
- )
881
- } else {
882
- const next = router.buildNext({
883
- to: '.',
884
- search: (d: any) => ({
885
- ...(d ?? {}),
886
- __data: {
887
- matchId: routeMatch.matchId,
888
- },
889
- }),
890
- })
842
+ const stringifiedKey = key ? JSON.stringify(key) : ''
891
843
 
892
- const res = await fetch(next.href, {
893
- method: 'GET',
894
- // signal: routeMatch.__.abortController.signal,
895
- })
844
+ const matchId =
845
+ interpolatePath(route.id, routeParams, true) + stringifiedKey
896
846
 
897
- if (res.ok) {
898
- return res.json()
899
- }
847
+ // Waste not, want not. If we already have a match for this route,
848
+ // reuse it. This is important for layout routes, which might stick
849
+ // around between navigation actions that only change leaf routes.
850
+ const existingMatch = this.getRouteMatch(matchId)
900
851
 
901
- throw new Error('Failed to fetch match data')
852
+ if (existingMatch) {
853
+ return { ...existingMatch }
902
854
  }
903
- },
904
855
 
905
- invalidateRoute: (opts: MatchLocation) => {
906
- const next = router.buildNext(opts)
907
- const unloadedMatchIds = router
908
- .matchRoutes(next.pathname)
909
- .map((d) => d.matchId)
910
- ;[
911
- ...router.state.matches,
912
- ...(router.state.pending?.matches ?? []),
913
- ].forEach((match) => {
914
- if (unloadedMatchIds.includes(match.matchId)) {
915
- match.invalidate()
916
- }
917
- })
918
- },
856
+ // Create a fresh route match
857
+ const hasLoaders = !!(
858
+ route.options.loader ||
859
+ componentTypes.some((d) => (route.options[d] as any)?.preload)
860
+ )
919
861
 
920
- reload: () =>
921
- router.__.navigate({
922
- fromCurrent: true,
923
- replace: true,
924
- search: true,
925
- }),
862
+ const routeMatch: AnyRouteMatch = {
863
+ id: matchId,
864
+ key: stringifiedKey,
865
+ routeId: route.id,
866
+ params: routeParams,
867
+ pathname: joinPaths([this.basepath, interpolatedPath]),
868
+ updatedAt: Date.now(),
869
+ maxAge: -1,
870
+ preloadMaxAge: -1,
871
+ routeSearch: {},
872
+ search: {} as any,
873
+ status: hasLoaders ? 'pending' : 'success',
874
+ isFetching: false,
875
+ invalid: false,
876
+ error: undefined,
877
+ paramsError: parseErrors[index],
878
+ searchError: undefined,
879
+ loaderData: undefined,
880
+ loadPromise: Promise.resolve(),
881
+ routeContext: undefined!,
882
+ context: undefined!,
883
+ abortController: new AbortController(),
884
+ fetchedAt: 0,
885
+ }
926
886
 
927
- resolvePath: (from: string, path: string) => {
928
- return resolvePath(router.basepath!, from, cleanPath(path))
929
- },
887
+ return routeMatch
888
+ })
889
+
890
+ // Take each match and resolve its search params and context
891
+ // This has to happen after the matches are created or found
892
+ // so that we can use the parent match's search params and context
893
+ matches.forEach((match, i): any => {
894
+ const parentMatch = matches[i - 1]
895
+ const route = this.getRoute(match.routeId)
896
+
897
+ const searchInfo = (() => {
898
+ // Validate the search params and stabilize them
899
+ const parentSearchInfo = {
900
+ search: parentMatch?.search ?? locationSearch,
901
+ routeSearch: parentMatch?.routeSearch ?? locationSearch,
902
+ }
930
903
 
931
- matchRoute: (location, opts) => {
932
- // const location = router.buildNext(opts)
904
+ try {
905
+ const validator =
906
+ typeof route.options.validateSearch === 'object'
907
+ ? route.options.validateSearch.parse
908
+ : route.options.validateSearch
933
909
 
934
- location = {
935
- ...location,
936
- to: location.to
937
- ? router.resolvePath(location.from ?? '', location.to)
938
- : undefined,
939
- }
910
+ let routeSearch = validator?.(parentSearchInfo.search) ?? {}
940
911
 
941
- const next = router.buildNext(location)
912
+ let search = {
913
+ ...parentSearchInfo.search,
914
+ ...routeSearch,
915
+ }
942
916
 
943
- if (opts?.pending) {
944
- if (!router.state.pending?.location) {
945
- return false
917
+ routeSearch = replaceEqualDeep(match.routeSearch, routeSearch)
918
+ search = replaceEqualDeep(match.search, search)
919
+
920
+ return {
921
+ routeSearch,
922
+ search,
923
+ searchDidChange: match.routeSearch !== routeSearch,
924
+ }
925
+ } catch (err: any) {
926
+ match.searchError = new SearchParamError(err.message, {
927
+ cause: err,
928
+ })
929
+
930
+ if (opts?.throwOnError) {
931
+ throw match.searchError
932
+ }
933
+
934
+ return parentSearchInfo
946
935
  }
947
- return !!matchPathname(router.state.pending.location.pathname, {
948
- ...opts,
949
- to: next.pathname,
950
- })
951
- }
936
+ })()
952
937
 
953
- return !!matchPathname(router.state.location.pathname, {
954
- ...opts,
955
- to: next.pathname,
956
- })
938
+ Object.assign(match, searchInfo)
939
+ })
940
+
941
+ return matches as any
942
+ }
943
+
944
+ loadMatches = async (
945
+ matchIds: string[],
946
+ opts?: {
947
+ preload?: boolean
948
+ maxAge?: number
957
949
  },
950
+ ) => {
951
+ const getFreshMatches = () => matchIds.map((d) => this.getRouteMatch(d)!)
952
+
953
+ if (!opts?.preload) {
954
+ getFreshMatches().forEach((match) => {
955
+ // Update each match with its latest route data
956
+ this.setRouteMatch(match.id, (s) => ({
957
+ ...s,
958
+ routeSearch: match.routeSearch,
959
+ search: match.search,
960
+ routeContext: match.routeContext,
961
+ context: match.context,
962
+ error: match.error,
963
+ paramsError: match.paramsError,
964
+ searchError: match.searchError,
965
+ params: match.params,
966
+ preloadMaxAge: 0,
967
+ }))
968
+ })
969
+ }
958
970
 
959
- navigate: async ({ from, to = '.', search, hash, replace, params }) => {
960
- // If this link simply reloads the current route,
961
- // make sure it has a new key so it will trigger a data refresh
971
+ let firstBadMatchIndex: number | undefined
962
972
 
963
- // If this `to` is a valid external URL, return
964
- // null for LinkUtils
965
- const toString = String(to)
966
- const fromString = String(from)
973
+ // Check each match middleware to see if the route can be accessed
974
+ try {
975
+ for (const [index, match] of getFreshMatches().entries()) {
976
+ const parentMatch = getFreshMatches()[index - 1]
977
+ const route = this.getRoute(match.routeId)
967
978
 
968
- let isExternal
979
+ const handleError = (err: any, code: string) => {
980
+ err.routerCode = code
981
+ firstBadMatchIndex = firstBadMatchIndex ?? index
969
982
 
970
- try {
971
- new URL(`${toString}`)
972
- isExternal = true
973
- } catch (e) {}
983
+ if (isRedirect(err)) {
984
+ throw err
985
+ }
974
986
 
975
- invariant(
976
- !isExternal,
977
- 'Attempting to navigate to external url with router.navigate!',
978
- )
987
+ try {
988
+ route.options.onError?.(err)
989
+ } catch (errorHandlerErr) {
990
+ err = errorHandlerErr
979
991
 
980
- return router.__.navigate({
981
- from: fromString,
982
- to: toString,
983
- search,
984
- hash,
985
- replace,
986
- params,
987
- })
988
- },
992
+ if (isRedirect(errorHandlerErr)) {
993
+ throw errorHandlerErr
994
+ }
995
+ }
989
996
 
990
- buildLink: ({
991
- from,
992
- to = '.',
993
- search,
994
- params,
995
- hash,
996
- target,
997
- replace,
998
- activeOptions,
999
- preload,
1000
- preloadMaxAge: userPreloadMaxAge,
1001
- preloadGcMaxAge: userPreloadGcMaxAge,
1002
- preloadDelay: userPreloadDelay,
1003
- disabled,
1004
- }) => {
1005
- // If this link simply reloads the current route,
1006
- // make sure it has a new key so it will trigger a data refresh
997
+ this.setRouteMatch(match.id, (s) => ({
998
+ ...s,
999
+ error: err,
1000
+ status: 'error',
1001
+ updatedAt: Date.now(),
1002
+ }))
1003
+ }
1007
1004
 
1008
- // If this `to` is a valid external URL, return
1009
- // null for LinkUtils
1005
+ if (match.paramsError) {
1006
+ handleError(match.paramsError, 'PARSE_PARAMS')
1007
+ }
1010
1008
 
1011
- try {
1012
- new URL(`${to}`)
1013
- return {
1014
- type: 'external',
1015
- href: to,
1009
+ if (match.searchError) {
1010
+ handleError(match.searchError, 'VALIDATE_SEARCH')
1016
1011
  }
1017
- } catch (e) {}
1018
1012
 
1019
- const nextOpts = {
1020
- from,
1021
- to,
1022
- search,
1023
- params,
1024
- hash,
1025
- replace,
1013
+ let didError = false
1014
+
1015
+ try {
1016
+ const routeContext =
1017
+ (await route.options.beforeLoad?.({
1018
+ ...match,
1019
+ preload: !!opts?.preload,
1020
+ parentContext: parentMatch?.routeContext ?? {},
1021
+ context: parentMatch?.context ?? this?.options.context ?? {},
1022
+ })) ?? ({} as any)
1023
+
1024
+ const context = {
1025
+ ...(parentMatch?.context ?? this?.options.context),
1026
+ ...routeContext,
1027
+ } as any
1028
+
1029
+ this.setRouteMatch(match.id, (s) => ({
1030
+ ...s,
1031
+ context,
1032
+ routeContext,
1033
+ }))
1034
+ } catch (err) {
1035
+ handleError(err, 'BEFORE_LOAD')
1036
+ didError = true
1037
+ }
1038
+
1039
+ // If we errored, do not run the next matches' middleware
1040
+ if (didError) {
1041
+ break
1042
+ }
1043
+ }
1044
+ } catch (err) {
1045
+ if (isRedirect(err)) {
1046
+ if (!opts?.preload) this.navigate(err as any)
1047
+ return
1026
1048
  }
1027
1049
 
1028
- const next = router.buildNext(nextOpts)
1050
+ throw err
1051
+ }
1029
1052
 
1030
- preload = preload ?? router.options.defaultPreload
1031
- const preloadDelay =
1032
- userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0
1053
+ const validResolvedMatches = getFreshMatches().slice(0, firstBadMatchIndex)
1054
+ const matchPromises: Promise<any>[] = []
1055
+
1056
+ validResolvedMatches.forEach((match, index) => {
1057
+ matchPromises.push(
1058
+ (async () => {
1059
+ const parentMatchPromise = matchPromises[index - 1]
1060
+ const route = this.getRoute(match.routeId)
1061
+
1062
+ if (
1063
+ match.isFetching ||
1064
+ (match.status === 'success' &&
1065
+ !isMatchInvalid(match, {
1066
+ preload: opts?.preload,
1067
+ }))
1068
+ ) {
1069
+ return this.getRouteMatch(match.id)?.loadPromise
1070
+ }
1033
1071
 
1034
- // Compare path/hash for matches
1035
- const pathIsEqual = router.state.location.pathname === next.pathname
1036
- const currentPathSplit = router.state.location.pathname.split('/')
1037
- const nextPathSplit = next.pathname.split('/')
1038
- const pathIsFuzzyEqual = nextPathSplit.every(
1039
- (d, i) => d === currentPathSplit[i],
1040
- )
1041
- const hashIsEqual = router.state.location.hash === next.hash
1042
- // Combine the matches based on user options
1043
- const pathTest = activeOptions?.exact ? pathIsEqual : pathIsFuzzyEqual
1044
- const hashTest = activeOptions?.includeHash ? hashIsEqual : true
1045
-
1046
- // The final "active" test
1047
- const isActive = pathTest && hashTest
1048
-
1049
- // The click handler
1050
- const handleClick = (e: MouseEvent) => {
1051
- if (
1052
- !disabled &&
1053
- !isCtrlEvent(e) &&
1054
- !e.defaultPrevented &&
1055
- (!target || target === '_self') &&
1056
- e.button === 0
1057
- ) {
1058
- e.preventDefault()
1059
- if (pathIsEqual && !search && !hash) {
1060
- router.invalidateRoute(nextOpts)
1072
+ const fetchedAt = Date.now()
1073
+ const checkLatest = () => {
1074
+ const latest = this.getRouteMatch(match.id)
1075
+ return latest && latest.fetchedAt !== fetchedAt
1076
+ ? latest.loadPromise
1077
+ : undefined
1061
1078
  }
1062
1079
 
1063
- // All is well? Navigate!)
1064
- router.__.navigate(nextOpts)
1065
- }
1066
- }
1080
+ const handleIfRedirect = (err: any) => {
1081
+ if (isRedirect(err)) {
1082
+ if (!opts?.preload) {
1083
+ this.navigate(err as any)
1084
+ }
1085
+ return true
1086
+ }
1087
+ return false
1088
+ }
1067
1089
 
1068
- // The click handler
1069
- const handleFocus = (e: MouseEvent) => {
1070
- if (preload) {
1071
- router.preloadRoute(nextOpts, {
1072
- maxAge: userPreloadMaxAge,
1073
- gcMaxAge: userPreloadGcMaxAge,
1074
- })
1075
- }
1076
- }
1090
+ const load = async () => {
1091
+ let latestPromise
1092
+
1093
+ try {
1094
+ const componentsPromise = Promise.all(
1095
+ componentTypes.map(async (type) => {
1096
+ const component = route.options[type]
1097
+
1098
+ if ((component as any)?.preload) {
1099
+ await (component as any).preload()
1100
+ }
1101
+ }),
1102
+ )
1103
+
1104
+ const loaderPromise = route.options.loader?.({
1105
+ ...match,
1106
+ preload: !!opts?.preload,
1107
+ parentMatchPromise,
1108
+ })
1109
+
1110
+ const [_, loader] = await Promise.all([
1111
+ componentsPromise,
1112
+ loaderPromise,
1113
+ ])
1114
+ if ((latestPromise = checkLatest())) return await latestPromise
1077
1115
 
1078
- const handleEnter = (e: MouseEvent) => {
1079
- const target = (e.target || {}) as LinkCurrentTargetElement
1116
+ this.setRouteMatchData(match.id, () => loader, opts)
1117
+ } catch (error) {
1118
+ if ((latestPromise = checkLatest())) return await latestPromise
1119
+ if (handleIfRedirect(error)) return
1120
+
1121
+ try {
1122
+ route.options.onError?.(error)
1123
+ } catch (onErrorError) {
1124
+ error = onErrorError
1125
+ if (handleIfRedirect(onErrorError)) return
1126
+ }
1080
1127
 
1081
- if (preload) {
1082
- if (target.preloadTimeout) {
1083
- return
1128
+ this.setRouteMatch(match.id, (s) => ({
1129
+ ...s,
1130
+ error,
1131
+ status: 'error',
1132
+ isFetching: false,
1133
+ updatedAt: Date.now(),
1134
+ }))
1135
+ }
1084
1136
  }
1085
1137
 
1086
- target.preloadTimeout = setTimeout(() => {
1087
- target.preloadTimeout = null
1088
- router.preloadRoute(nextOpts, {
1089
- maxAge: userPreloadMaxAge,
1090
- gcMaxAge: userPreloadGcMaxAge,
1091
- })
1092
- }, preloadDelay)
1093
- }
1138
+ let loadPromise: Promise<void> | undefined
1139
+
1140
+ this.__store.batch(() => {
1141
+ this.setRouteMatch(match.id, (s) => ({
1142
+ ...s,
1143
+ // status: s.status !== 'success' ? 'pending' : s.status,
1144
+ isFetching: true,
1145
+ fetchedAt,
1146
+ invalid: false,
1147
+ }))
1148
+
1149
+ loadPromise = load()
1150
+
1151
+ this.setRouteMatch(match.id, (s) => ({
1152
+ ...s,
1153
+ loadPromise,
1154
+ }))
1155
+ })
1156
+
1157
+ await loadPromise
1158
+ })(),
1159
+ )
1160
+ })
1161
+
1162
+ await Promise.all(matchPromises)
1163
+ }
1164
+
1165
+ resolvePath = (from: string, path: string) => {
1166
+ return resolvePath(this.basepath!, from, cleanPath(path))
1167
+ }
1168
+
1169
+ navigate = async <
1170
+ TFrom extends RoutePaths<TRouteTree> = '/',
1171
+ TTo extends string = '',
1172
+ TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
1173
+ TMaskTo extends string = '',
1174
+ >({
1175
+ from,
1176
+ to = '' as any,
1177
+ ...rest
1178
+ }: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>) => {
1179
+ // If this link simply reloads the current route,
1180
+ // make sure it has a new key so it will trigger a data refresh
1181
+
1182
+ // If this `to` is a valid external URL, return
1183
+ // null for LinkUtils
1184
+ const toString = String(to)
1185
+ const fromString = typeof from === 'undefined' ? from : String(from)
1186
+ let isExternal
1187
+
1188
+ try {
1189
+ new URL(`${toString}`)
1190
+ isExternal = true
1191
+ } catch (e) {}
1192
+
1193
+ invariant(
1194
+ !isExternal,
1195
+ 'Attempting to navigate to external url with this.navigate!',
1196
+ )
1197
+
1198
+ return this.#buildAndCommitLocation({
1199
+ ...rest,
1200
+ from: fromString,
1201
+ to: toString,
1202
+ })
1203
+ }
1204
+
1205
+ matchRoute = <
1206
+ TRouteTree extends AnyRoute = AnyRoute,
1207
+ TFrom extends RoutePaths<TRouteTree> = '/',
1208
+ TTo extends string = '',
1209
+ TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
1210
+ >(
1211
+ location: ToOptions<TRouteTree, TFrom, TTo>,
1212
+ opts?: MatchRouteOptions,
1213
+ ): false | RouteById<TRouteTree, TResolved>['types']['allParams'] => {
1214
+ location = {
1215
+ ...location,
1216
+ to: location.to
1217
+ ? this.resolvePath(location.from ?? '', location.to)
1218
+ : undefined,
1219
+ } as any
1220
+
1221
+ const next = this.buildLocation(location)
1222
+ if (opts?.pending && this.state.status !== 'pending') {
1223
+ return false
1224
+ }
1225
+
1226
+ const baseLocation = opts?.pending
1227
+ ? this.state.location
1228
+ : this.state.resolvedLocation
1229
+
1230
+ if (!baseLocation) {
1231
+ return false
1232
+ }
1233
+
1234
+ const match = matchPathname(this.basepath, baseLocation.pathname, {
1235
+ ...opts,
1236
+ to: next.pathname,
1237
+ }) as any
1238
+
1239
+ if (!match) {
1240
+ return false
1241
+ }
1242
+
1243
+ if (opts?.includeSearch ?? true) {
1244
+ return partialDeepEqual(baseLocation.search, next.search) ? match : false
1245
+ }
1246
+
1247
+ return match
1248
+ }
1249
+
1250
+ buildLink = <
1251
+ TFrom extends RoutePaths<TRouteTree> = '/',
1252
+ TTo extends string = '',
1253
+ >(
1254
+ dest: LinkOptions<TRouteTree, TFrom, TTo>,
1255
+ ): LinkInfo => {
1256
+ // If this link simply reloads the current route,
1257
+ // make sure it has a new key so it will trigger a data refresh
1258
+
1259
+ // If this `to` is a valid external URL, return
1260
+ // null for LinkUtils
1261
+
1262
+ const {
1263
+ to,
1264
+ preload: userPreload,
1265
+ preloadDelay: userPreloadDelay,
1266
+ activeOptions,
1267
+ disabled,
1268
+ target,
1269
+ replace,
1270
+ resetScroll,
1271
+ } = dest
1272
+
1273
+ try {
1274
+ new URL(`${to}`)
1275
+ return {
1276
+ type: 'external',
1277
+ href: to as any,
1094
1278
  }
1279
+ } catch (e) {}
1280
+
1281
+ const nextOpts = dest
1282
+
1283
+ const next = this.buildLocation(nextOpts)
1284
+
1285
+ const preload = userPreload ?? this.options.defaultPreload
1286
+ const preloadDelay =
1287
+ userPreloadDelay ?? this.options.defaultPreloadDelay ?? 0
1288
+
1289
+ // Compare path/hash for matches
1290
+ const currentPathSplit = this.state.location.pathname.split('/')
1291
+ const nextPathSplit = next.pathname.split('/')
1292
+ const pathIsFuzzyEqual = nextPathSplit.every(
1293
+ (d, i) => d === currentPathSplit[i],
1294
+ )
1295
+ // Combine the matches based on user options
1296
+ const pathTest = activeOptions?.exact
1297
+ ? this.state.location.pathname === next.pathname
1298
+ : pathIsFuzzyEqual
1299
+ const hashTest = activeOptions?.includeHash
1300
+ ? this.state.location.hash === next.hash
1301
+ : true
1302
+ const searchTest =
1303
+ activeOptions?.includeSearch ?? true
1304
+ ? partialDeepEqual(this.state.location.search, next.search)
1305
+ : true
1306
+
1307
+ // The final "active" test
1308
+ const isActive = pathTest && hashTest && searchTest
1309
+
1310
+ // The click handler
1311
+ const handleClick = (e: MouseEvent) => {
1312
+ if (
1313
+ !disabled &&
1314
+ !isCtrlEvent(e) &&
1315
+ !e.defaultPrevented &&
1316
+ (!target || target === '_self') &&
1317
+ e.button === 0
1318
+ ) {
1319
+ e.preventDefault()
1320
+
1321
+ // All is well? Navigate!
1322
+ this.#commitLocation({ ...next, replace, resetScroll })
1323
+ }
1324
+ }
1325
+
1326
+ // The click handler
1327
+ const handleFocus = (e: MouseEvent) => {
1328
+ if (preload) {
1329
+ this.preloadRoute(nextOpts).catch((err) => {
1330
+ console.warn(err)
1331
+ console.warn(preloadWarning)
1332
+ })
1333
+ }
1334
+ }
1335
+
1336
+ const handleTouchStart = (e: TouchEvent) => {
1337
+ this.preloadRoute(nextOpts).catch((err) => {
1338
+ console.warn(err)
1339
+ console.warn(preloadWarning)
1340
+ })
1341
+ }
1095
1342
 
1096
- const handleLeave = (e: MouseEvent) => {
1097
- const target = (e.target || {}) as LinkCurrentTargetElement
1343
+ const handleEnter = (e: MouseEvent) => {
1344
+ const target = (e.target || {}) as LinkCurrentTargetElement
1098
1345
 
1346
+ if (preload) {
1099
1347
  if (target.preloadTimeout) {
1100
- clearTimeout(target.preloadTimeout)
1348
+ return
1349
+ }
1350
+
1351
+ target.preloadTimeout = setTimeout(() => {
1101
1352
  target.preloadTimeout = null
1353
+ this.preloadRoute(nextOpts).catch((err) => {
1354
+ console.warn(err)
1355
+ console.warn(preloadWarning)
1356
+ })
1357
+ }, preloadDelay)
1358
+ }
1359
+ }
1360
+
1361
+ const handleLeave = (e: MouseEvent) => {
1362
+ const target = (e.target || {}) as LinkCurrentTargetElement
1363
+
1364
+ if (target.preloadTimeout) {
1365
+ clearTimeout(target.preloadTimeout)
1366
+ target.preloadTimeout = null
1367
+ }
1368
+ }
1369
+
1370
+ return {
1371
+ type: 'internal',
1372
+ next,
1373
+ handleFocus,
1374
+ handleClick,
1375
+ handleEnter,
1376
+ handleLeave,
1377
+ handleTouchStart,
1378
+ isActive,
1379
+ disabled,
1380
+ }
1381
+ }
1382
+
1383
+ dehydrate = (): DehydratedRouter => {
1384
+ return {
1385
+ state: {
1386
+ matchIds: this.state.matchIds,
1387
+ dehydratedMatches: this.state.matches.map((d) =>
1388
+ pick(d, [
1389
+ 'fetchedAt',
1390
+ 'invalid',
1391
+ 'preloadMaxAge',
1392
+ 'maxAge',
1393
+ 'id',
1394
+ 'loaderData',
1395
+ 'status',
1396
+ 'updatedAt',
1397
+ ]),
1398
+ ),
1399
+ },
1400
+ }
1401
+ }
1402
+
1403
+ hydrate = async (__do_not_use_server_ctx?: HydrationCtx) => {
1404
+ let _ctx = __do_not_use_server_ctx
1405
+ // Client hydrates from window
1406
+ if (typeof document !== 'undefined') {
1407
+ _ctx = window.__TSR_DEHYDRATED__
1408
+ }
1409
+
1410
+ invariant(
1411
+ _ctx,
1412
+ 'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
1413
+ )
1414
+
1415
+ const ctx = _ctx
1416
+ this.dehydratedData = ctx.payload as any
1417
+ this.options.hydrate?.(ctx.payload as any)
1418
+ const dehydratedState = ctx.router.state
1419
+
1420
+ let matches = this.matchRoutes(
1421
+ this.state.location.pathname,
1422
+ this.state.location.search,
1423
+ ).map((match) => {
1424
+ const dehydratedMatch = dehydratedState.dehydratedMatches.find(
1425
+ (d) => d.id === match.id,
1426
+ )
1427
+
1428
+ invariant(
1429
+ dehydratedMatch,
1430
+ `Could not find a client-side match for dehydrated match with id: ${match.id}!`,
1431
+ )
1432
+
1433
+ if (dehydratedMatch) {
1434
+ return {
1435
+ ...match,
1436
+ ...dehydratedMatch,
1102
1437
  }
1103
1438
  }
1439
+ return match
1440
+ })
1104
1441
 
1442
+ this.__store.setState((s) => {
1105
1443
  return {
1106
- type: 'internal',
1107
- next,
1108
- handleFocus,
1109
- handleClick,
1110
- handleEnter,
1111
- handleLeave,
1112
- isActive,
1113
- disabled,
1444
+ ...s,
1445
+ matchIds: dehydratedState.matchIds,
1446
+ matches,
1447
+ matchesById: this.#mergeMatches(s.matchesById, matches),
1114
1448
  }
1115
- },
1116
- buildNext: (opts: BuildNextOptions) => {
1117
- const next = router.__.buildLocation(opts)
1118
-
1119
- const matches = router.matchRoutes(next.pathname)
1449
+ })
1450
+ }
1120
1451
 
1121
- const __preSearchFilters = matches
1122
- .map((match) => match.options.preSearchFilters ?? [])
1123
- .flat()
1124
- .filter(Boolean)
1452
+ injectedHtml: (string | (() => Promise<string> | string))[] = []
1125
1453
 
1126
- const __postSearchFilters = matches
1127
- .map((match) => match.options.postSearchFilters ?? [])
1128
- .flat()
1129
- .filter(Boolean)
1454
+ injectHtml = async (html: string | (() => Promise<string> | string)) => {
1455
+ this.injectedHtml.push(html)
1456
+ }
1130
1457
 
1131
- return router.__.buildLocation({
1132
- ...opts,
1133
- __preSearchFilters,
1134
- __postSearchFilters,
1458
+ dehydrateData = <T>(key: any, getData: T | (() => Promise<T> | T)) => {
1459
+ if (typeof document === 'undefined') {
1460
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key)
1461
+
1462
+ this.injectHtml(async () => {
1463
+ const id = `__TSR_DEHYDRATED__${strKey}`
1464
+ const data =
1465
+ typeof getData === 'function' ? await (getData as any)() : getData
1466
+ return `<script id='${id}' suppressHydrationWarning>window["__TSR_DEHYDRATED__${escapeJSON(
1467
+ strKey,
1468
+ )}"] = ${JSON.stringify(data)}
1469
+ ;(() => {
1470
+ var el = document.getElementById('${id}')
1471
+ el.parentElement.removeChild(el)
1472
+ })()
1473
+ </script>`
1135
1474
  })
1136
- },
1137
1475
 
1138
- __: {
1139
- buildRouteTree: (rootRouteConfig: RouteConfig) => {
1140
- const recurseRoutes = (
1141
- routeConfigs: RouteConfig[],
1142
- parent?: Route<TAllRouteInfo, any>,
1143
- ): Route<TAllRouteInfo, any>[] => {
1144
- return routeConfigs.map((routeConfig) => {
1145
- const routeOptions = routeConfig.options
1146
- const route = createRoute(routeConfig, routeOptions, parent, router)
1147
- const existingRoute = (router.routesById as any)[route.routeId]
1148
-
1149
- if (existingRoute) {
1150
- if (process.env.NODE_ENV !== 'production') {
1151
- console.warn(
1152
- `Duplicate routes found with id: ${String(route.routeId)}`,
1153
- router.routesById,
1154
- route,
1155
- )
1156
- }
1157
- throw new Error()
1158
- }
1476
+ return () => this.hydrateData<T>(key)
1477
+ }
1159
1478
 
1160
- ;(router.routesById as any)[route.routeId] = route
1479
+ return () => undefined
1480
+ }
1161
1481
 
1162
- const children = routeConfig.children as RouteConfig[]
1482
+ hydrateData = <T = unknown>(key: any) => {
1483
+ if (typeof document !== 'undefined') {
1484
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key)
1163
1485
 
1164
- route.childRoutes = children?.length
1165
- ? recurseRoutes(children, route)
1166
- : undefined
1486
+ return window[`__TSR_DEHYDRATED__${strKey}` as any] as T
1487
+ }
1167
1488
 
1168
- return route
1169
- })
1489
+ return undefined
1490
+ }
1491
+
1492
+ // resolveMatchPromise = (matchId: string, key: string, value: any) => {
1493
+ // this.state.matches
1494
+ // .find((d) => d.id === matchId)
1495
+ // ?.__promisesByKey[key]?.resolve(value)
1496
+ // }
1497
+
1498
+ #processRoutes = (routeTree: TRouteTree) => {
1499
+ this.routeTree = routeTree as any
1500
+ this.routesById = {} as any
1501
+ this.routesByPath = {} as any
1502
+ this.flatRoutes = [] as any
1503
+
1504
+ const recurseRoutes = (routes: AnyRoute[]) => {
1505
+ routes.forEach((route, i) => {
1506
+ route.init({ originalIndex: i, router: this })
1507
+
1508
+ const existingRoute = (this.routesById as any)[route.id]
1509
+
1510
+ invariant(
1511
+ !existingRoute,
1512
+ `Duplicate routes found with id: ${String(route.id)}`,
1513
+ )
1514
+ ;(this.routesById as any)[route.id] = route
1515
+
1516
+ if (!route.isRoot && route.path) {
1517
+ const trimmedFullPath = trimPathRight(route.fullPath)
1518
+ if (
1519
+ !(this.routesByPath as any)[trimmedFullPath] ||
1520
+ route.fullPath.endsWith('/')
1521
+ ) {
1522
+ ;(this.routesByPath as any)[trimmedFullPath] = route
1523
+ }
1170
1524
  }
1171
1525
 
1172
- const routes = recurseRoutes([rootRouteConfig])
1526
+ const children = route.children as Route[]
1173
1527
 
1174
- return routes[0]!
1175
- },
1528
+ if (children?.length) {
1529
+ recurseRoutes(children)
1530
+ }
1531
+ })
1532
+ }
1176
1533
 
1177
- parseLocation: (
1178
- location: History['location'],
1179
- previousLocation?: Location,
1180
- ): Location => {
1181
- const parsedSearch = router.options.parseSearch(location.search)
1534
+ recurseRoutes([routeTree])
1182
1535
 
1183
- return {
1184
- pathname: location.pathname,
1185
- searchStr: location.search,
1186
- search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1187
- hash: location.hash.split('#').reverse()[0] ?? '',
1188
- href: `${location.pathname}${location.search}${location.hash}`,
1189
- state: location.state as LocationState,
1190
- key: location.key,
1536
+ this.flatRoutes = (Object.values(this.routesByPath) as AnyRoute[])
1537
+ .map((d, i) => {
1538
+ const trimmed = trimPath(d.fullPath)
1539
+ const parsed = parsePathname(trimmed)
1540
+
1541
+ while (parsed.length > 1 && parsed[0]?.value === '/') {
1542
+ parsed.shift()
1191
1543
  }
1192
- },
1193
1544
 
1194
- navigate: (location: BuildNextOptions & { replace?: boolean }) => {
1195
- const next = router.buildNext(location)
1196
- return router.__.commitLocation(next, location.replace)
1197
- },
1545
+ const score = parsed.map((d) => {
1546
+ if (d.type === 'param') {
1547
+ return 0.5
1548
+ }
1198
1549
 
1199
- buildLocation: (dest: BuildNextOptions = {}): Location => {
1200
- // const resolvedFrom: Location = {
1201
- // ...router.location,
1202
- const fromPathname = dest.fromCurrent
1203
- ? router.location.pathname
1204
- : dest.from ?? router.location.pathname
1205
-
1206
- let pathname = resolvePath(
1207
- router.basepath ?? '/',
1208
- fromPathname,
1209
- `${dest.to ?? '.'}`,
1210
- )
1550
+ if (d.type === 'wildcard') {
1551
+ return 0.25
1552
+ }
1211
1553
 
1212
- const fromMatches = router.matchRoutes(router.location.pathname, {
1213
- strictParseParams: true,
1554
+ return 1
1214
1555
  })
1215
1556
 
1216
- const toMatches = router.matchRoutes(pathname)
1557
+ return { child: d, trimmed, parsed, index: i, score }
1558
+ })
1559
+ .sort((a, b) => {
1560
+ let isIndex = a.trimmed === '/' ? 1 : b.trimmed === '/' ? -1 : 0
1217
1561
 
1218
- const prevParams = { ...last(fromMatches)?.params }
1562
+ if (isIndex !== 0) return isIndex
1219
1563
 
1220
- let nextParams =
1221
- (dest.params ?? true) === true
1222
- ? prevParams
1223
- : functionalUpdate(dest.params!, prevParams)
1564
+ const length = Math.min(a.score.length, b.score.length)
1224
1565
 
1225
- if (nextParams) {
1226
- toMatches
1227
- .map((d) => d.options.stringifyParams)
1228
- .filter(Boolean)
1229
- .forEach((fn) => {
1230
- Object.assign({}, nextParams!, fn!(nextParams!))
1231
- })
1566
+ // Sort by length of score
1567
+ if (a.score.length !== b.score.length) {
1568
+ return b.score.length - a.score.length
1232
1569
  }
1233
1570
 
1234
- pathname = interpolatePath(pathname, nextParams ?? {})
1235
-
1236
- // Pre filters first
1237
- const preFilteredSearch = dest.__preSearchFilters?.length
1238
- ? dest.__preSearchFilters.reduce(
1239
- (prev, next) => next(prev),
1240
- router.location.search,
1241
- )
1242
- : router.location.search
1243
-
1244
- // Then the link/navigate function
1245
- const destSearch =
1246
- dest.search === true
1247
- ? preFilteredSearch // Preserve resolvedFrom true
1248
- : dest.search
1249
- ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1250
- : dest.__preSearchFilters?.length
1251
- ? preFilteredSearch // Preserve resolvedFrom filters
1252
- : {}
1253
-
1254
- // Then post filters
1255
- const postFilteredSearch = dest.__postSearchFilters?.length
1256
- ? dest.__postSearchFilters.reduce(
1257
- (prev, next) => next(prev),
1258
- destSearch,
1259
- )
1260
- : destSearch
1261
-
1262
- const search = replaceEqualDeep(
1263
- router.location.search,
1264
- postFilteredSearch,
1265
- )
1571
+ // Sort by min available score
1572
+ for (let i = 0; i < length; i++) {
1573
+ if (a.score[i] !== b.score[i]) {
1574
+ return b.score[i]! - a.score[i]!
1575
+ }
1576
+ }
1266
1577
 
1267
- const searchStr = router.options.stringifySearch(search)
1268
- let hash =
1269
- dest.hash === true
1270
- ? router.location.hash
1271
- : functionalUpdate(dest.hash!, router.location.hash)
1272
- hash = hash ? `#${hash}` : ''
1578
+ // Sort by min available parsed value
1579
+ for (let i = 0; i < length; i++) {
1580
+ if (a.parsed[i]!.value !== b.parsed[i]!.value) {
1581
+ return a.parsed[i]!.value! > b.parsed[i]!.value! ? 1 : -1
1582
+ }
1583
+ }
1273
1584
 
1274
- return {
1275
- pathname,
1276
- search,
1277
- searchStr,
1278
- state: router.location.state,
1279
- hash,
1280
- href: `${pathname}${searchStr}${hash}`,
1281
- key: dest.key,
1585
+ // Sort by length of trimmed full path
1586
+ if (a.trimmed !== b.trimmed) {
1587
+ return a.trimmed > b.trimmed ? 1 : -1
1282
1588
  }
1283
- },
1284
1589
 
1285
- commitLocation: (next: Location, replace?: boolean): Promise<void> => {
1286
- const id = '' + Date.now() + Math.random()
1590
+ // Sort by original index
1591
+ return a.index - b.index
1592
+ })
1593
+ .map((d, i) => {
1594
+ d.child.rank = i
1595
+ return d.child
1596
+ }) as any
1597
+ }
1598
+
1599
+ #parseLocation = (
1600
+ previousLocation?: ParsedLocation,
1601
+ ): ParsedLocation<FullSearchSchema<TRouteTree>> => {
1602
+ const parse = ({
1603
+ pathname,
1604
+ search,
1605
+ hash,
1606
+ state,
1607
+ }: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
1608
+ const parsedSearch = this.options.parseSearch(search)
1609
+
1610
+ return {
1611
+ pathname: pathname,
1612
+ searchStr: search,
1613
+ search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
1614
+ hash: hash.split('#').reverse()[0] ?? '',
1615
+ href: `${pathname}${search}${hash}`,
1616
+ state: replaceEqualDeep(previousLocation?.state, state) as HistoryState,
1617
+ }
1618
+ }
1287
1619
 
1288
- if (router.navigateTimeout) clearTimeout(router.navigateTimeout)
1620
+ const location = parse(this.history.location)
1289
1621
 
1290
- let nextAction: 'push' | 'replace' = 'replace'
1622
+ let { __tempLocation, __tempKey } = location.state
1291
1623
 
1292
- if (!replace) {
1293
- nextAction = 'push'
1294
- }
1624
+ if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
1625
+ // Sync up the location keys
1626
+ const parsedTempLocation = parse(__tempLocation) as any
1627
+ parsedTempLocation.state.key = location.state.key
1295
1628
 
1296
- const isSameUrl =
1297
- router.__.parseLocation(history.location).href === next.href
1629
+ delete parsedTempLocation.state.__tempLocation
1298
1630
 
1299
- if (isSameUrl && !next.key) {
1300
- nextAction = 'replace'
1301
- }
1631
+ return {
1632
+ ...parsedTempLocation,
1633
+ maskedLocation: location,
1634
+ }
1635
+ }
1302
1636
 
1303
- if (nextAction === 'replace') {
1304
- history.replace(
1305
- {
1306
- pathname: next.pathname,
1307
- hash: next.hash,
1308
- search: next.searchStr,
1309
- },
1310
- {
1311
- id,
1312
- ...next.state,
1313
- },
1637
+ return location
1638
+ }
1639
+
1640
+ buildLocation = (opts: BuildNextOptions = {}): ParsedLocation => {
1641
+ const build = (
1642
+ dest: BuildNextOptions & {
1643
+ unmaskOnReload?: boolean
1644
+ } = {},
1645
+ matches?: AnyRouteMatch[],
1646
+ ): ParsedLocation => {
1647
+ const from = this.state.location
1648
+
1649
+ const fromPathname = dest.from ?? from.pathname
1650
+
1651
+ let pathname = resolvePath(
1652
+ this.basepath ?? '/',
1653
+ fromPathname,
1654
+ `${dest.to ?? ''}`,
1655
+ )
1656
+
1657
+ const fromMatches = this.matchRoutes(fromPathname, from.search)
1658
+
1659
+ const prevParams = { ...last(fromMatches)?.params }
1660
+
1661
+ let nextParams =
1662
+ (dest.params ?? true) === true
1663
+ ? prevParams
1664
+ : functionalUpdate(dest.params!, prevParams)
1665
+
1666
+ if (nextParams) {
1667
+ matches
1668
+ ?.map((d) => this.getRoute(d.routeId).options.stringifyParams)
1669
+ .filter(Boolean)
1670
+ .forEach((fn) => {
1671
+ nextParams = { ...nextParams!, ...fn!(nextParams!) }
1672
+ })
1673
+ }
1674
+
1675
+ pathname = interpolatePath(pathname, nextParams ?? {})
1676
+
1677
+ const preSearchFilters =
1678
+ matches
1679
+ ?.map(
1680
+ (match) =>
1681
+ this.getRoute(match.routeId).options.preSearchFilters ?? [],
1314
1682
  )
1315
- } else {
1316
- history.push(
1317
- {
1318
- pathname: next.pathname,
1319
- hash: next.hash,
1320
- search: next.searchStr,
1321
- },
1322
- {
1323
- id,
1324
- },
1683
+ .flat()
1684
+ .filter(Boolean) ?? []
1685
+
1686
+ const postSearchFilters =
1687
+ matches
1688
+ ?.map(
1689
+ (match) =>
1690
+ this.getRoute(match.routeId).options.postSearchFilters ?? [],
1325
1691
  )
1326
- }
1692
+ .flat()
1693
+ .filter(Boolean) ?? []
1694
+
1695
+ // Pre filters first
1696
+ const preFilteredSearch = preSearchFilters?.length
1697
+ ? preSearchFilters?.reduce((prev, next) => next(prev), from.search)
1698
+ : from.search
1699
+
1700
+ // Then the link/navigate function
1701
+ const destSearch =
1702
+ dest.search === true
1703
+ ? preFilteredSearch // Preserve resolvedFrom true
1704
+ : dest.search
1705
+ ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1706
+ : preSearchFilters?.length
1707
+ ? preFilteredSearch // Preserve resolvedFrom filters
1708
+ : {}
1709
+
1710
+ // Then post filters
1711
+ const postFilteredSearch = postSearchFilters?.length
1712
+ ? postSearchFilters.reduce((prev, next) => next(prev), destSearch)
1713
+ : destSearch
1714
+
1715
+ const search = replaceEqualDeep(from.search, postFilteredSearch)
1716
+
1717
+ const searchStr = this.options.stringifySearch(search)
1718
+
1719
+ const hash =
1720
+ dest.hash === true
1721
+ ? from.hash
1722
+ : dest.hash
1723
+ ? functionalUpdate(dest.hash!, from.hash)
1724
+ : from.hash
1725
+
1726
+ const hashStr = hash ? `#${hash}` : ''
1727
+
1728
+ let nextState =
1729
+ dest.state === true
1730
+ ? from.state
1731
+ : dest.state
1732
+ ? functionalUpdate(dest.state, from.state)
1733
+ : from.state
1734
+
1735
+ nextState = replaceEqualDeep(from.state, nextState)
1736
+
1737
+ return {
1738
+ pathname,
1739
+ search,
1740
+ searchStr,
1741
+ state: nextState as any,
1742
+ hash,
1743
+ href: this.history.createHref(`${pathname}${searchStr}${hashStr}`),
1744
+ unmaskOnReload: dest.unmaskOnReload,
1745
+ }
1746
+ }
1327
1747
 
1328
- router.navigationPromise = new Promise((resolve) => {
1329
- const previousNavigationResolve = router.resolveNavigation
1748
+ const buildWithMatches = (
1749
+ dest: BuildNextOptions = {},
1750
+ maskedDest?: BuildNextOptions,
1751
+ ) => {
1752
+ let next = build(dest)
1753
+ let maskedNext = maskedDest ? build(maskedDest) : undefined
1330
1754
 
1331
- router.resolveNavigation = () => {
1332
- previousNavigationResolve()
1333
- resolve()
1755
+ if (!maskedNext) {
1756
+ let params = {}
1757
+
1758
+ let foundMask = this.options.routeMasks?.find((d) => {
1759
+ const match = matchPathname(this.basepath, next.pathname, {
1760
+ to: d.from,
1761
+ fuzzy: false,
1762
+ })
1763
+
1764
+ if (match) {
1765
+ params = match
1766
+ return true
1334
1767
  }
1768
+
1769
+ return false
1335
1770
  })
1336
1771
 
1337
- return router.navigationPromise
1338
- },
1772
+ if (foundMask) {
1773
+ foundMask = {
1774
+ ...foundMask,
1775
+ from: interpolatePath(foundMask.from, params) as any,
1776
+ }
1777
+ maskedDest = foundMask
1778
+ maskedNext = build(maskedDest)
1779
+ }
1780
+ }
1781
+
1782
+ const nextMatches = this.matchRoutes(next.pathname, next.search)
1783
+ const maskedMatches = maskedNext
1784
+ ? this.matchRoutes(maskedNext.pathname, maskedNext.search)
1785
+ : undefined
1786
+ const maskedFinal = maskedNext
1787
+ ? build(maskedDest, maskedMatches)
1788
+ : undefined
1789
+
1790
+ const final = build(dest, nextMatches)
1791
+
1792
+ if (maskedFinal) {
1793
+ final.maskedLocation = maskedFinal
1794
+ }
1795
+
1796
+ return final
1797
+ }
1798
+
1799
+ if (opts.mask) {
1800
+ return buildWithMatches(opts, {
1801
+ ...pick(opts, ['from']),
1802
+ ...opts.mask,
1803
+ })
1804
+ }
1805
+
1806
+ return buildWithMatches(opts)
1807
+ }
1808
+
1809
+ #buildAndCommitLocation = ({
1810
+ replace,
1811
+ resetScroll,
1812
+ ...rest
1813
+ }: BuildNextOptions & CommitLocationOptions = {}) => {
1814
+ const location = this.buildLocation(rest)
1815
+ return this.#commitLocation({
1816
+ ...location,
1817
+ replace,
1818
+ resetScroll,
1819
+ })
1820
+ }
1821
+
1822
+ #commitLocation = async (next: ParsedLocation & CommitLocationOptions) => {
1823
+ if (this.navigateTimeout) clearTimeout(this.navigateTimeout)
1824
+
1825
+ let nextAction: 'push' | 'replace' = 'replace'
1826
+
1827
+ if (!next.replace) {
1828
+ nextAction = 'push'
1829
+ }
1830
+
1831
+ const isSameUrl = this.state.location.href === next.href
1832
+
1833
+ if (isSameUrl) {
1834
+ nextAction = 'replace'
1835
+ }
1836
+
1837
+ let { maskedLocation, ...nextHistory } = next
1838
+
1839
+ if (maskedLocation) {
1840
+ nextHistory = {
1841
+ ...maskedLocation,
1842
+ state: {
1843
+ ...maskedLocation.state,
1844
+ __tempKey: undefined,
1845
+ __tempLocation: {
1846
+ ...nextHistory,
1847
+ search: nextHistory.searchStr,
1848
+ state: {
1849
+ ...nextHistory.state,
1850
+ __tempKey: undefined!,
1851
+ __tempLocation: undefined!,
1852
+ key: undefined!,
1853
+ },
1854
+ },
1855
+ },
1856
+ }
1857
+
1858
+ if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
1859
+ nextHistory.state.__tempKey = this.tempLocationKey
1860
+ }
1861
+ }
1862
+
1863
+ this.history[nextAction === 'push' ? 'push' : 'replace'](
1864
+ nextHistory.href,
1865
+ nextHistory.state,
1866
+ )
1867
+
1868
+ this.resetNextScroll = next.resetScroll ?? true
1869
+
1870
+ return this.latestLoadPromise
1871
+ }
1872
+
1873
+ getRouteMatch = (id: string): undefined | RouteMatch<TRouteTree> => {
1874
+ return this.state.matchesById[id]
1875
+ }
1876
+
1877
+ setRouteMatch = (
1878
+ id: string,
1879
+ updater: (prev: RouteMatch<TRouteTree>) => RouteMatch<TRouteTree>,
1880
+ ) => {
1881
+ this.__store.setState((prev) => {
1882
+ if (!prev.matchesById[id]) {
1883
+ return prev
1884
+ }
1885
+
1886
+ return {
1887
+ ...prev,
1888
+ matchesById: {
1889
+ ...prev.matchesById,
1890
+ [id]: updater(prev.matchesById[id] as any),
1891
+ },
1892
+ }
1893
+ })
1894
+ }
1895
+
1896
+ setRouteMatchData = (
1897
+ id: string,
1898
+ updater: (prev: any) => any,
1899
+ opts?: {
1900
+ updatedAt?: number
1901
+ maxAge?: number
1339
1902
  },
1903
+ ) => {
1904
+ const match = this.getRouteMatch(id)
1905
+
1906
+ if (!match) return
1907
+
1908
+ const route = this.getRoute(match.routeId)
1909
+ const updatedAt = opts?.updatedAt ?? Date.now()
1910
+
1911
+ const preloadMaxAge =
1912
+ opts?.maxAge ??
1913
+ route.options.preloadMaxAge ??
1914
+ this.options.defaultPreloadMaxAge ??
1915
+ 5000
1916
+
1917
+ const maxAge =
1918
+ opts?.maxAge ?? route.options.maxAge ?? this.options.defaultMaxAge ?? -1
1919
+
1920
+ this.setRouteMatch(id, (s) => ({
1921
+ ...s,
1922
+ error: undefined,
1923
+ status: 'success',
1924
+ isFetching: false,
1925
+ updatedAt: updatedAt,
1926
+ loaderData: functionalUpdate(updater, s.loaderData),
1927
+ preloadMaxAge,
1928
+ maxAge,
1929
+ }))
1340
1930
  }
1341
1931
 
1342
- router.update(userOptions)
1932
+ invalidate = async (opts?: {
1933
+ matchId?: string
1934
+ reload?: boolean
1935
+ __fromFocus?: boolean
1936
+ }): Promise<void> => {
1937
+ if (opts?.matchId) {
1938
+ this.setRouteMatch(opts.matchId, (s) => ({
1939
+ ...s,
1940
+ invalid: true,
1941
+ }))
1942
+
1943
+ const matchIndex = this.state.matches.findIndex(
1944
+ (d) => d.id === opts.matchId,
1945
+ )
1946
+ const childMatch = this.state.matches[matchIndex + 1]
1947
+
1948
+ if (childMatch) {
1949
+ return this.invalidate({
1950
+ matchId: childMatch.id,
1951
+ reload: false,
1952
+ __fromFocus: opts.__fromFocus,
1953
+ })
1954
+ }
1955
+ } else {
1956
+ this.__store.batch(() => {
1957
+ Object.values(this.state.matchesById).forEach((match) => {
1958
+ const route = this.getRoute(match.routeId)
1959
+ const shouldInvalidate = opts?.__fromFocus
1960
+ ? route.options.reloadOnWindowFocus ?? true
1961
+ : true
1962
+
1963
+ if (shouldInvalidate) {
1964
+ this.setRouteMatch(match.id, (s) => ({
1965
+ ...s,
1966
+ invalid: true,
1967
+ }))
1968
+ }
1969
+ })
1970
+ })
1971
+ }
1972
+
1973
+ if (opts?.reload ?? true) {
1974
+ return this.load()
1975
+ }
1976
+ }
1977
+ }
1343
1978
 
1344
- // Allow frameworks to hook into the router creation
1345
- router.options.createRouter?.(router)
1979
+ // Detect if we're in the DOM
1980
+ const isServer = typeof window === 'undefined' || !window.document.createElement
1346
1981
 
1347
- return router
1982
+ function getInitialRouterState(): RouterState<any> {
1983
+ return {
1984
+ status: 'idle',
1985
+ isFetching: false,
1986
+ resolvedLocation: null!,
1987
+ location: null!,
1988
+ matchesById: {},
1989
+ matchIds: [],
1990
+ pendingMatchIds: [],
1991
+ matches: [],
1992
+ pendingMatches: [],
1993
+ renderedMatchIds: [],
1994
+ renderedMatches: [],
1995
+ lastUpdated: Date.now(),
1996
+ }
1348
1997
  }
1349
1998
 
1350
1999
  function isCtrlEvent(e: MouseEvent) {
1351
2000
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
1352
2001
  }
1353
2002
 
1354
- function cascadeLoaderData(matches: RouteMatch<any, any>[]) {
1355
- matches.forEach((match, index) => {
1356
- const parent = matches[index - 1]
2003
+ export type AnyRedirect = Redirect<any, any, any>
1357
2004
 
1358
- if (parent) {
1359
- match.loaderData = replaceEqualDeep(match.loaderData, {
1360
- ...parent.loaderData,
1361
- ...match.routeLoaderData,
1362
- })
1363
- }
1364
- })
2005
+ export type Redirect<
2006
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
2007
+ TFrom extends RoutePaths<TRouteTree> = '/',
2008
+ TTo extends string = '',
2009
+ TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
2010
+ TMaskTo extends string = '',
2011
+ > = NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
2012
+ code?: number
2013
+ }
2014
+
2015
+ export function redirect<
2016
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
2017
+ TFrom extends RoutePaths<TRouteTree> = '/',
2018
+ TTo extends string = '',
2019
+ >(opts: Redirect<TRouteTree, TFrom, TTo>): Redirect<TRouteTree, TFrom, TTo> {
2020
+ ;(opts as any).isRedirect = true
2021
+ return opts
2022
+ }
2023
+
2024
+ export function isRedirect(obj: any): obj is AnyRedirect {
2025
+ return !!obj?.isRedirect
2026
+ }
2027
+
2028
+ export class SearchParamError extends Error {}
2029
+ export class PathParamError extends Error {}
2030
+
2031
+ function escapeJSON(jsonString: string) {
2032
+ return jsonString
2033
+ .replace(/\\/g, '\\\\') // Escape backslashes
2034
+ .replace(/'/g, "\\'") // Escape single quotes
2035
+ .replace(/"/g, '\\"') // Escape double quotes
2036
+ }
2037
+
2038
+ // A function that takes an import() argument which is a function and returns a new function that will
2039
+ // proxy arguments from the caller to the imported function, retaining all type
2040
+ // information along the way
2041
+ export function lazyFn<
2042
+ T extends Record<string, (...args: any[]) => any>,
2043
+ TKey extends keyof T = 'default',
2044
+ >(fn: () => Promise<T>, key?: TKey) {
2045
+ return async (...args: Parameters<T[TKey]>): Promise<ReturnType<T[TKey]>> => {
2046
+ const imported = await fn()
2047
+ return imported[key || 'default'](...args)
2048
+ }
2049
+ }
2050
+
2051
+ export function isMatchInvalid(
2052
+ match: AnyRouteMatch,
2053
+ opts?: { preload?: boolean },
2054
+ ) {
2055
+ const now = Date.now()
2056
+
2057
+ if (match.invalid) {
2058
+ return true
2059
+ }
2060
+
2061
+ if (opts?.preload) {
2062
+ return match.preloadMaxAge < 0
2063
+ ? false
2064
+ : match.updatedAt + match.preloadMaxAge < now
2065
+ }
2066
+
2067
+ return match.maxAge < 0 ? false : match.updatedAt + match.maxAge < now
1365
2068
  }