@tanstack/router-core 0.0.1-beta.14 → 0.0.1-beta.146

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