@tanstack/router-core 0.0.1-beta.2 → 0.0.1-beta.201

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