@tanstack/router-core 0.0.1-beta.13 → 0.0.1-beta.145

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/build/cjs/history.js +226 -0
  3. package/build/cjs/history.js.map +1 -0
  4. package/build/cjs/{packages/router-core/src/index.js → index.js} +33 -15
  5. package/build/cjs/{packages/router-core/src/index.js.map → index.js.map} +1 -1
  6. package/build/cjs/{packages/router-core/src/path.js → path.js} +45 -56
  7. package/build/cjs/path.js.map +1 -0
  8. package/build/cjs/{packages/router-core/src/qss.js → qss.js} +10 -16
  9. package/build/cjs/qss.js.map +1 -0
  10. package/build/cjs/route.js +147 -0
  11. package/build/cjs/route.js.map +1 -0
  12. package/build/cjs/router.js +1102 -0
  13. package/build/cjs/router.js.map +1 -0
  14. package/build/cjs/{packages/router-core/src/searchParams.js → searchParams.js} +11 -13
  15. package/build/cjs/searchParams.js.map +1 -0
  16. package/build/cjs/{packages/router-core/src/utils.js → utils.js} +54 -64
  17. package/build/cjs/utils.js.map +1 -0
  18. package/build/esm/index.js +1444 -2095
  19. package/build/esm/index.js.map +1 -1
  20. package/build/stats-html.html +59 -49
  21. package/build/stats-react.json +186 -249
  22. package/build/types/index.d.ts +559 -422
  23. package/build/umd/index.development.js +1675 -2223
  24. package/build/umd/index.development.js.map +1 -1
  25. package/build/umd/index.production.js +12 -2
  26. package/build/umd/index.production.js.map +1 -1
  27. package/package.json +11 -7
  28. package/src/history.ts +292 -0
  29. package/src/index.ts +2 -10
  30. package/src/link.ts +116 -113
  31. package/src/path.ts +37 -17
  32. package/src/qss.ts +1 -2
  33. package/src/route.ts +927 -218
  34. package/src/routeInfo.ts +121 -197
  35. package/src/router.ts +1483 -1008
  36. package/src/searchParams.ts +1 -1
  37. package/src/utils.ts +80 -49
  38. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +0 -33
  39. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +0 -1
  40. package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js +0 -33
  41. package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js.map +0 -1
  42. package/build/cjs/node_modules/history/index.js +0 -815
  43. package/build/cjs/node_modules/history/index.js.map +0 -1
  44. package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js +0 -30
  45. package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js.map +0 -1
  46. package/build/cjs/packages/router-core/src/path.js.map +0 -1
  47. package/build/cjs/packages/router-core/src/qss.js.map +0 -1
  48. package/build/cjs/packages/router-core/src/route.js +0 -147
  49. package/build/cjs/packages/router-core/src/route.js.map +0 -1
  50. package/build/cjs/packages/router-core/src/routeConfig.js +0 -69
  51. package/build/cjs/packages/router-core/src/routeConfig.js.map +0 -1
  52. package/build/cjs/packages/router-core/src/routeMatch.js +0 -226
  53. package/build/cjs/packages/router-core/src/routeMatch.js.map +0 -1
  54. package/build/cjs/packages/router-core/src/router.js +0 -823
  55. package/build/cjs/packages/router-core/src/router.js.map +0 -1
  56. package/build/cjs/packages/router-core/src/searchParams.js.map +0 -1
  57. package/build/cjs/packages/router-core/src/utils.js.map +0 -1
  58. package/src/frameworks.ts +0 -11
  59. package/src/routeConfig.ts +0 -489
  60. package/src/routeMatch.ts +0 -312
package/src/router.ts CHANGED
@@ -1,61 +1,87 @@
1
- import {
2
- BrowserHistory,
3
- createBrowserHistory,
4
- createMemoryHistory,
5
- HashHistory,
6
- History,
7
- MemoryHistory,
8
- } from 'history'
1
+ import { Store } from '@tanstack/react-store'
9
2
  import invariant from 'tiny-invariant'
10
- import { GetFrameworkGeneric } from './frameworks'
3
+
4
+ //
11
5
 
12
6
  import {
13
7
  LinkInfo,
14
8
  LinkOptions,
15
- NavigateOptionsAbsolute,
9
+ NavigateOptions,
16
10
  ToOptions,
17
- ValidFromPath,
11
+ ResolveRelativePath,
18
12
  } from './link'
19
13
  import {
20
14
  cleanPath,
21
15
  interpolatePath,
22
16
  joinPaths,
23
17
  matchPathname,
18
+ parsePathname,
24
19
  resolvePath,
20
+ trimPath,
21
+ trimPathRight,
25
22
  } from './path'
26
- import { AnyRoute, createRoute, Route } from './route'
27
23
  import {
28
- AnyLoaderData,
29
- AnyPathParams,
30
- AnyRouteConfig,
24
+ Route,
31
25
  AnySearchSchema,
32
- LoaderContext,
33
- RouteConfig,
34
- SearchFilter,
35
- } from './routeConfig'
26
+ AnyRoute,
27
+ RootRoute,
28
+ AnyContext,
29
+ AnyPathParams,
30
+ RouteProps,
31
+ RegisteredRouteComponent,
32
+ RegisteredRouteErrorComponent,
33
+ } from './route'
36
34
  import {
37
- AllRouteInfo,
38
- AnyAllRouteInfo,
39
- AnyRouteInfo,
40
- RouteInfo,
35
+ RoutesInfo,
36
+ AnyRoutesInfo,
41
37
  RoutesById,
38
+ RoutesByPath,
39
+ DefaultRoutesInfo,
42
40
  } from './routeInfo'
43
- import { createRouteMatch, RouteMatch } from './routeMatch'
44
41
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
45
42
  import {
46
43
  functionalUpdate,
47
44
  last,
45
+ NoInfer,
48
46
  pick,
49
47
  PickAsRequired,
50
- PickRequired,
51
- replaceEqualDeep,
52
48
  Timeout,
53
49
  Updater,
50
+ replaceEqualDeep,
51
+ partialDeepEqual,
54
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]
55
81
 
56
82
  export interface LocationState {}
57
83
 
58
- export interface Location<
84
+ export interface ParsedLocation<
59
85
  TSearchObj extends AnySearchSchema = {},
60
86
  TState extends LocationState = LocationState,
61
87
  > {
@@ -77,137 +103,119 @@ export interface FromLocation {
77
103
 
78
104
  export type SearchSerializer = (searchObj: Record<string, any>) => string
79
105
  export type SearchParser = (searchStr: string) => Record<string, any>
80
- export type FilterRoutesFn = <TRoute extends Route<any, RouteInfo>>(
81
- routeConfigs: TRoute[],
82
- ) => TRoute[]
83
-
84
- export interface RouterOptions<TRouteConfig extends AnyRouteConfig> {
85
- history?: BrowserHistory | MemoryHistory | HashHistory
86
- stringifySearch?: SearchSerializer
87
- parseSearch?: SearchParser
88
- filterRoutes?: FilterRoutesFn
89
- defaultPreload?: false | 'intent'
90
- defaultPreloadMaxAge?: number
91
- defaultPreloadGcMaxAge?: number
92
- defaultPreloadDelay?: number
93
- useErrorBoundary?: boolean
94
- defaultComponent?: GetFrameworkGeneric<'Component'>
95
- defaultErrorComponent?: GetFrameworkGeneric<'Component'>
96
- defaultPendingComponent?: GetFrameworkGeneric<'Component'>
97
- defaultLoaderMaxAge?: number
98
- defaultLoaderGcMaxAge?: number
99
- caseSensitive?: boolean
100
- routeConfig?: TRouteConfig
101
- basepath?: string
102
- createRouter?: (router: Router<any, any>) => void
103
- createRoute?: (opts: { route: AnyRoute; router: Router<any, any> }) => void
104
- loadComponent?: (
105
- component: GetFrameworkGeneric<'Component'>,
106
- ) => Promise<GetFrameworkGeneric<'Component'>>
107
- // renderComponent?: (
108
- // component: GetFrameworkGeneric<'Component'>,
109
- // ) => GetFrameworkGeneric<'Element'>
110
- }
111
106
 
112
- export interface Action<
113
- TPayload = unknown,
114
- TResponse = unknown,
115
- // TError = unknown,
116
- > {
117
- submit: (
118
- submission?: TPayload,
119
- actionOpts?: { invalidate?: boolean; multi?: boolean },
120
- ) => Promise<TResponse>
121
- current?: ActionState<TPayload, TResponse>
122
- latest?: ActionState<TPayload, TResponse>
123
- submissions: ActionState<TPayload, TResponse>[]
107
+ export type HydrationCtx = {
108
+ router: DehydratedRouter
109
+ payload: Record<string, any>
124
110
  }
125
111
 
126
- export interface ActionState<
127
- TPayload = unknown,
128
- TResponse = unknown,
129
- // TError = unknown,
112
+ export interface RouteMatch<
113
+ TRoutesInfo extends AnyRoutesInfo = DefaultRoutesInfo,
114
+ TRoute extends AnyRoute = Route,
130
115
  > {
131
- submittedAt: number
116
+ id: string
117
+ key?: string
118
+ routeId: string
119
+ pathname: string
120
+ params: TRoute['__types']['allParams']
132
121
  status: 'idle' | 'pending' | 'success' | 'error'
133
- submission: TPayload
134
- isMulti: boolean
135
- data?: TResponse
136
- 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
137
140
  }
138
141
 
139
- export interface Loader<
140
- TFullSearchSchema extends AnySearchSchema = {},
141
- TAllParams extends AnyPathParams = {},
142
- TRouteLoaderData = AnyLoaderData,
143
- > {
144
- fetch: keyof PickRequired<TFullSearchSchema> extends never
145
- ? keyof TAllParams extends never
146
- ? (loaderContext: { signal?: AbortSignal }) => Promise<TRouteLoaderData>
147
- : (loaderContext: {
148
- params: TAllParams
149
- search?: TFullSearchSchema
150
- signal?: AbortSignal
151
- }) => Promise<TRouteLoaderData>
152
- : keyof TAllParams extends never
153
- ? (loaderContext: {
154
- search: TFullSearchSchema
155
- params: TAllParams
156
- signal?: AbortSignal
157
- }) => Promise<TRouteLoaderData>
158
- : (loaderContext: {
159
- search: TFullSearchSchema
160
- signal?: AbortSignal
161
- }) => Promise<TRouteLoaderData>
162
- current?: LoaderState<TFullSearchSchema, TAllParams>
163
- latest?: LoaderState<TFullSearchSchema, TAllParams>
164
- pending: LoaderState<TFullSearchSchema, TAllParams>[]
165
- }
142
+ export type AnyRouteMatch = RouteMatch<AnyRoutesInfo, AnyRoute>
166
143
 
167
- export interface LoaderState<
168
- TFullSearchSchema = unknown,
169
- 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>,
170
156
  > {
171
- loadedAt: number
172
- 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
173
186
  }
174
187
 
175
- export interface RouterState {
176
- status: 'idle' | 'loading'
177
- location: Location
178
- matches: RouteMatch[]
179
- lastUpdated: number
180
- actions: Record<string, Action>
181
- loaders: Record<string, Loader>
182
- pending?: PendingState
188
+ export interface RouterState<
189
+ TRoutesInfo extends AnyRoutesInfo = AnyRoutesInfo,
190
+ TState extends LocationState = LocationState,
191
+ > {
192
+ status: 'idle' | 'pending'
183
193
  isFetching: boolean
184
- isPreloading: boolean
185
- }
186
-
187
- export interface PendingState {
188
- location: Location
189
- 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
190
205
  }
191
206
 
192
- type Listener = (router: Router<any, any>) => void
193
-
194
207
  export type ListenerFn = () => void
195
208
 
196
209
  export interface BuildNextOptions {
197
210
  to?: string | number | null
198
- params?: true | Updater<Record<string, any>>
211
+ params?: true | Updater<unknown>
199
212
  search?: true | Updater<unknown>
200
213
  hash?: true | Updater<string>
214
+ state?: LocationState
201
215
  key?: string
202
216
  from?: string
203
217
  fromCurrent?: boolean
204
- __preSearchFilters?: SearchFilter<any>[]
205
- __postSearchFilters?: SearchFilter<any>[]
206
- }
207
-
208
- export type MatchCacheEntry = {
209
- gc: number
210
- match: RouteMatch
218
+ __matches?: AnyRouteMatch[]
211
219
  }
212
220
 
213
221
  export interface MatchLocation {
@@ -219,1082 +227,1549 @@ export interface MatchLocation {
219
227
  }
220
228
 
221
229
  export interface MatchRouteOptions {
222
- pending: boolean
230
+ pending?: boolean
223
231
  caseSensitive?: boolean
232
+ includeSearch?: boolean
233
+ fuzzy?: boolean
224
234
  }
225
235
 
226
236
  type LinkCurrentTargetElement = {
227
237
  preloadTimeout?: null | ReturnType<typeof setTimeout>
228
238
  }
229
239
 
230
- interface DehydratedRouterState
231
- extends Pick<RouterState, 'status' | 'location' | 'lastUpdated'> {
232
- matches: DehydratedRouteMatch[]
240
+ export interface DehydratedRouterState
241
+ extends Pick<RouterState, 'status' | 'location' | 'lastUpdated'> {}
242
+
243
+ export interface DehydratedRouter {
244
+ state: DehydratedRouterState
233
245
  }
234
246
 
235
- interface DehydratedRouteMatch
236
- extends Pick<
237
- RouteMatch<any, any>,
238
- | 'matchId'
239
- | 'status'
240
- | 'routeLoaderData'
241
- | 'loaderData'
242
- | 'isInvalid'
243
- | 'invalidAt'
244
- > {}
245
-
246
- export interface Router<
247
- TRouteConfig extends AnyRouteConfig = RouteConfig,
248
- 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>,
249
263
  > {
250
- history: BrowserHistory | MemoryHistory | HashHistory
264
+ types!: {
265
+ RootRoute: TRouteTree
266
+ RoutesInfo: TRoutesInfo
267
+ }
268
+
251
269
  options: PickAsRequired<
252
- RouterOptions<TRouteConfig>,
253
- 'stringifySearch' | 'parseSearch'
270
+ RouterOptions<TRouteTree, TDehydrated>,
271
+ 'stringifySearch' | 'parseSearch' | 'context'
254
272
  >
255
- // Computed in this.update()
256
- basepath: string
257
- // Internal:
258
- allRouteInfo: TAllRouteInfo
259
- listeners: Listener[]
260
- location: Location
261
- navigateTimeout?: Timeout
262
- nextAction?: 'push' | 'replace'
263
- state: RouterState
264
- routeTree: Route<TAllRouteInfo, RouteInfo>
265
- routesById: RoutesById<TAllRouteInfo>
266
- navigationPromise: Promise<void>
267
- startedLoadingAt: number
268
- resolveNavigation: () => void
269
- subscribe: (listener: Listener) => () => void
270
- reset: () => void
271
- notify: () => void
272
- mount: () => () => void
273
- onFocus: () => void
274
- update: <TRouteConfig extends RouteConfig = RouteConfig>(
275
- opts?: RouterOptions<TRouteConfig>,
276
- ) => Router<TRouteConfig>
277
-
278
- buildNext: (opts: BuildNextOptions) => Location
279
- cancelMatches: () => void
280
- load: (next?: Location) => Promise<void>
281
- matchCache: Record<string, MatchCacheEntry>
282
- cleanMatchCache: () => void
283
- getRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
284
- id: TId,
285
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
286
- loadRoute: (navigateOpts: BuildNextOptions) => Promise<RouteMatch[]>
287
- preloadRoute: (
288
- navigateOpts: BuildNextOptions,
289
- loaderOpts: { maxAge?: number; gcMaxAge?: number },
290
- ) => Promise<RouteMatch[]>
291
- matchRoutes: (
292
- pathname: string,
293
- opts?: { strictParseParams?: boolean },
294
- ) => RouteMatch[]
295
- loadMatches: (
296
- resolvedMatches: RouteMatch[],
297
- loaderOpts?:
298
- | { preload: true; maxAge: number; gcMaxAge: number }
299
- | { preload?: false; maxAge?: never; gcMaxAge?: never },
300
- ) => Promise<void>
301
- invalidateRoute: (opts: MatchLocation) => void
302
- reload: () => Promise<void>
303
- resolvePath: (from: string, path: string) => string
304
- navigate: <
305
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
306
- TTo extends string = '.',
307
- >(
308
- opts: NavigateOptionsAbsolute<TAllRouteInfo, TFrom, TTo>,
309
- ) => Promise<void>
310
- matchRoute: <
311
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
312
- TTo extends string = '.',
313
- >(
314
- matchLocation: ToOptions<TAllRouteInfo, TFrom, TTo>,
315
- opts?: MatchRouteOptions,
316
- ) => boolean
317
- buildLink: <
318
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
319
- TTo extends string = '.',
320
- >(
321
- opts: LinkOptions<TAllRouteInfo, TFrom, TTo>,
322
- ) => LinkInfo
323
- dehydrateState: () => DehydratedRouterState
324
- hydrateState: (state: DehydratedRouterState) => void
325
- __: {
326
- buildRouteTree: (
327
- routeConfig: RouteConfig,
328
- ) => Route<TAllRouteInfo, AnyRouteInfo>
329
- parseLocation: (
330
- location: History['location'],
331
- previousLocation?: Location,
332
- ) => Location
333
- buildLocation: (dest: BuildNextOptions) => Location
334
- commitLocation: (next: Location, replace?: boolean) => Promise<void>
335
- navigate: (
336
- location: BuildNextOptions & { replace?: boolean },
337
- ) => Promise<void>
338
- }
339
- }
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
+ }
340
297
 
341
- // Detect if we're in the DOM
342
- const isServer =
343
- typeof window === 'undefined' || !window.document?.createElement
298
+ this.__store = new Store<RouterState<TRoutesInfo>>(
299
+ getInitialRouterState(),
300
+ {
301
+ onUpdate: () => {
302
+ const prev = this.state
344
303
 
345
- // This is the default history object if none is defined
346
- const createDefaultHistory = () =>
347
- isServer ? createMemoryHistory() : createBrowserHistory()
304
+ this.state = this.__store.state
348
305
 
349
- function getInitialRouterState(): RouterState {
350
- return {
351
- status: 'idle',
352
- location: null!,
353
- matches: [],
354
- actions: {},
355
- loaders: {},
356
- lastUpdated: Date.now(),
357
- isFetching: false,
358
- isPreloading: false,
359
- }
360
- }
306
+ const matchesByIdChanged = prev.matchesById !== this.state.matchesById
307
+ let matchesChanged
308
+ let pendingMatchesChanged
361
309
 
362
- export function createRouter<
363
- TRouteConfig extends AnyRouteConfig = RouteConfig,
364
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
365
- >(
366
- userOptions?: RouterOptions<TRouteConfig>,
367
- ): Router<TRouteConfig, TAllRouteInfo> {
368
- const history = userOptions?.history || createDefaultHistory()
369
-
370
- const originalOptions = {
371
- defaultLoaderGcMaxAge: 5 * 60 * 1000,
372
- defaultLoaderMaxAge: 0,
373
- defaultPreloadMaxAge: 2000,
374
- defaultPreloadDelay: 50,
375
- ...userOptions,
376
- stringifySearch: userOptions?.stringifySearch ?? defaultStringifySearch,
377
- parseSearch: userOptions?.parseSearch ?? defaultParseSearch,
378
- }
310
+ if (!matchesByIdChanged) {
311
+ matchesChanged =
312
+ prev.matchIds.length !== this.state.matchIds.length ||
313
+ prev.matchIds.some((d, i) => d !== this.state.matchIds[i])
379
314
 
380
- let router: Router<TRouteConfig, TAllRouteInfo> = {
381
- history,
382
- options: originalOptions,
383
- listeners: [],
384
- // Resolved after construction
385
- basepath: '',
386
- routeTree: undefined!,
387
- routesById: {} as any,
388
- location: undefined!,
389
- allRouteInfo: undefined!,
390
- //
391
- navigationPromise: Promise.resolve(),
392
- resolveNavigation: () => {},
393
- matchCache: {},
394
- state: getInitialRouterState(),
395
- reset: () => {
396
- router.state = getInitialRouterState()
397
- router.notify()
398
- },
399
- startedLoadingAt: Date.now(),
400
- subscribe: (listener: Listener): (() => void) => {
401
- router.listeners.push(listener as Listener)
402
- return () => {
403
- router.listeners = router.listeners.filter((x) => x !== listener)
404
- }
405
- },
406
- getRoute: (id) => {
407
- return router.routesById[id]
408
- },
409
- notify: (): void => {
410
- router.state = {
411
- ...router.state,
412
- isFetching:
413
- router.state.status === 'loading' ||
414
- router.state.matches.some((d) => d.isFetching),
415
- isPreloading: Object.values(router.matchCache).some(
416
- (d) =>
417
- d.match.isFetching &&
418
- !router.state.matches.find((dd) => dd.matchId === d.match.matchId),
419
- ),
420
- }
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
+ }
421
322
 
422
- cascadeLoaderData(router.state.matches)
423
- router.listeners.forEach((listener) => listener(router))
424
- },
323
+ if (matchesByIdChanged || matchesChanged) {
324
+ this.state.matches = this.state.matchIds.map((id) => {
325
+ return this.state.matchesById[id] as any
326
+ })
327
+ }
425
328
 
426
- dehydrateState: () => {
427
- return {
428
- ...pick(router.state, ['status', 'location', 'lastUpdated']),
429
- matches: router.state.matches.map((match) =>
430
- pick(match, [
431
- 'matchId',
432
- 'status',
433
- 'routeLoaderData',
434
- 'loaderData',
435
- 'isInvalid',
436
- 'invalidAt',
437
- ]),
438
- ),
439
- }
440
- },
329
+ if (matchesByIdChanged || pendingMatchesChanged) {
330
+ this.state.pendingMatches = this.state.pendingMatchIds.map((id) => {
331
+ return this.state.matchesById[id] as any
332
+ })
333
+ }
441
334
 
442
- hydrateState: (dehydratedState) => {
443
- // Match the routes
444
- const matches = router.matchRoutes(router.location.pathname, {
445
- strictParseParams: true,
446
- })
335
+ this.state.isFetching = [
336
+ ...this.state.matches,
337
+ ...this.state.pendingMatches,
338
+ ].some((d) => d.isFetching)
339
+ },
340
+ defaultPriority: 'low',
341
+ },
342
+ )
447
343
 
448
- matches.forEach((match, index) => {
449
- const dehydratedMatch = dehydratedState.matches[index]
450
- invariant(
451
- dehydratedMatch,
452
- 'Oh no! Dehydrated route matches did not match the active state of the router 😬',
453
- )
454
- Object.assign(match, dehydratedMatch)
455
- })
344
+ this.state = this.__store.state
456
345
 
457
- router.loadMatches(matches)
346
+ this.update(options)
458
347
 
459
- router.state = {
460
- ...router.state,
461
- ...dehydratedState,
462
- matches,
463
- }
464
- },
348
+ const next = this.buildNext({
349
+ hash: true,
350
+ fromCurrent: true,
351
+ search: true,
352
+ state: true,
353
+ })
465
354
 
466
- mount: () => {
467
- const next = router.__.buildLocation({
468
- to: '.',
469
- search: true,
470
- hash: true,
471
- })
355
+ if (this.state.location.href !== next.href) {
356
+ this.#commitLocation({ ...next, replace: true })
357
+ }
358
+ }
359
+
360
+ reset = () => {
361
+ this.__store.setState((s) => Object.assign(s, getInitialRouterState()))
362
+ }
472
363
 
473
- // If the current location isn't updated, trigger a navigation
474
- // to the current location. Otherwise, load the current location.
475
- if (next.href !== router.location.href) {
476
- router.__.commitLocation(next, true)
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
+ }
380
+
381
+ if (
382
+ !this.history ||
383
+ (this.options.history && this.options.history !== this.history)
384
+ ) {
385
+ if (this.#unsubHistory) {
386
+ this.#unsubHistory()
477
387
  }
478
388
 
479
- // router.load()
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
+ }))
480
400
 
481
- const unsub = router.history.listen((event) => {
482
- router.load(router.__.parseLocation(event.location, router.location))
401
+ this.#unsubHistory = this.history.listen(() => {
402
+ this.safeLoad({
403
+ next: this.#parseLocation(this.state.location),
404
+ })
483
405
  })
406
+ }
484
407
 
485
- // addEventListener does not exist in React Native, but window does
486
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
487
- if (!isServer && window.addEventListener) {
488
- // Listen to visibillitychange and focus
489
- window.addEventListener('visibilitychange', router.onFocus, false)
490
- window.addEventListener('focus', router.onFocus, false)
491
- }
408
+ const { basepath, routeTree } = this.options
492
409
 
493
- return () => {
494
- unsub()
495
- if (!isServer && window.removeEventListener) {
496
- // Be sure to unsubscribe if a new handler is set
497
- window.removeEventListener('visibilitychange', router.onFocus)
498
- window.removeEventListener('focus', router.onFocus)
499
- }
500
- }
501
- },
410
+ this.basepath = `/${trimPath(basepath ?? '') ?? ''}`
502
411
 
503
- onFocus: () => {
504
- router.load()
505
- },
412
+ if (routeTree && routeTree !== this.routeTree) {
413
+ this.#buildRouteTree(routeTree)
414
+ }
506
415
 
507
- update: (opts) => {
508
- const newHistory = opts?.history !== router.history
509
- if (!router.location || newHistory) {
510
- if (opts?.history) {
511
- router.history = opts.history
512
- }
513
- router.location = router.__.parseLocation(router.history.location)
514
- router.state.location = router.location
515
- }
416
+ return this
417
+ }
516
418
 
517
- Object.assign(router.options, opts)
419
+ buildNext = (opts: BuildNextOptions): ParsedLocation => {
420
+ const next = this.#buildLocation(opts)
518
421
 
519
- const { basepath, routeConfig } = router.options
422
+ const __matches = this.matchRoutes(next.pathname, next.search)
520
423
 
521
- router.basepath = cleanPath(`/${basepath ?? ''}`)
424
+ return this.#buildLocation({
425
+ ...opts,
426
+ __matches,
427
+ })
428
+ }
522
429
 
523
- if (routeConfig) {
524
- router.routesById = {} as any
525
- router.routeTree = router.__.buildRouteTree(routeConfig)
526
- }
430
+ cancelMatches = () => {
431
+ this.state.matches.forEach((match) => {
432
+ this.cancelMatch(match.id)
433
+ })
434
+ }
527
435
 
528
- return router as any
529
- },
436
+ cancelMatch = (id: string) => {
437
+ this.getRouteMatch(id)?.abortController?.abort()
438
+ }
530
439
 
531
- cancelMatches: () => {
532
- ;[
533
- ...router.state.matches,
534
- ...(router.state.pending?.matches ?? []),
535
- ].forEach((match) => {
536
- match.cancel()
537
- })
538
- },
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
+ }
539
446
 
540
- load: async (next?: Location) => {
541
- const id = Math.random()
542
- router.startedLoadingAt = id
447
+ latestLoadPromise: Promise<void> = Promise.resolve()
543
448
 
544
- if (next) {
545
- // Ingest the new location
546
- 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
547
457
  }
548
458
 
549
459
  // Cancel any pending matches
550
- router.cancelMatches()
460
+ // this.cancelMatches()
551
461
 
552
- // Match the routes
553
- const matches = router.matchRoutes(router.location.pathname, {
554
- strictParseParams: true,
555
- })
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
+ }
556
472
 
557
- if (typeof document !== 'undefined') {
558
- router.state = {
559
- ...router.state,
560
- pending: {
561
- matches: matches,
562
- 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,
563
480
  },
564
- 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
565
498
  }
566
- } else {
567
- router.state = {
568
- ...router.state,
569
- matches: matches,
570
- location: router.location,
571
- status: 'loading',
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?.()
512
+ }
513
+
514
+ resolve()
515
+ } catch (err) {
516
+ // Only apply the latest transition
517
+ if ((latestPromise = checkLatest())) {
518
+ return await latestPromise
572
519
  }
520
+
521
+ reject(err)
573
522
  }
523
+ })
574
524
 
575
- router.notify()
525
+ this.latestLoadPromise = promise
576
526
 
577
- // Load the matches
578
- await router.loadMatches(matches)
527
+ return this.latestLoadPromise
528
+ }
579
529
 
580
- if (router.startedLoadingAt !== id) {
581
- // Ignore side-effects of match loading
582
- return router.navigationPromise
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
+ }
543
+
544
+ let hadNew = false
545
+
546
+ nextMatches.forEach((match) => {
547
+ if (!nextMatchesById[match.id]) {
548
+ hadNew = true
549
+ nextMatchesById[match.id] = match
583
550
  }
551
+ })
584
552
 
585
- const previousMatches = router.state.matches
553
+ if (!hadNew) {
554
+ return prevMatchesById
555
+ }
586
556
 
587
- const exiting: RouteMatch[] = [],
588
- staying: RouteMatch[] = []
557
+ return nextMatchesById
558
+ }
589
559
 
590
- previousMatches.forEach((d) => {
591
- if (matches.find((dd) => dd.matchId === d.matchId)) {
592
- staying.push(d)
593
- } else {
594
- exiting.push(d)
595
- }
596
- })
560
+ getRoute = <TId extends keyof TRoutesInfo['routesById']>(
561
+ id: TId,
562
+ ): TRoutesInfo['routesById'][TId] => {
563
+ const route = this.routesById[id]
597
564
 
598
- const entering = matches.filter((d) => {
599
- return !previousMatches.find((dd) => dd.matchId === d.matchId)
600
- })
565
+ invariant(route, `Route with id "${id as string}" not found`)
601
566
 
602
- const now = Date.now()
567
+ return route
568
+ }
603
569
 
604
- exiting.forEach((d) => {
605
- d.__.onExit?.({
606
- params: d.params,
607
- search: d.routeSearch,
608
- })
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
+ })
609
586
 
610
- // Clear idle error states when match leaves
611
- if (d.status === 'error' && !d.isFetching) {
612
- d.status = 'idle'
613
- d.error = undefined
614
- }
615
- const gc = Math.max(
616
- d.options.loaderGcMaxAge ?? router.options.defaultLoaderGcMaxAge ?? 0,
617
- 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)
618
608
  )
619
- if (gc > 0) {
620
- router.matchCache[d.matchId] = {
621
- gc: gc == Infinity ? Number.MAX_SAFE_INTEGER : now + gc,
622
- match: d,
623
- }
624
- }
625
609
  })
610
+ .map((d) => d.id)
626
611
 
627
- staying.forEach((d) => {
628
- d.options.onTransition?.({
629
- params: d.params,
630
- 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]
631
617
  })
618
+ return {
619
+ ...s,
620
+ matchesById,
621
+ }
632
622
  })
623
+ }
624
+ }
633
625
 
634
- entering.forEach((d) => {
635
- d.__.onExit = d.options.onMatch?.({
636
- params: d.params,
637
- search: d.search,
638
- })
639
- 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,
640
638
  })
641
639
 
642
- if (router.startedLoadingAt !== id) {
643
- // Ignore side-effects of match loading
644
- return
640
+ if (matchedParams) {
641
+ routeParams = matchedParams
642
+ return true
645
643
  }
646
644
 
647
- matches.forEach((match) => {
648
- // Clear actions
649
- if (match.action) {
650
- match.action.current = undefined
651
- match.action.submissions = []
652
- }
653
- })
645
+ return false
646
+ })
654
647
 
655
- router.state = {
656
- ...router.state,
657
- location: router.location,
658
- matches,
659
- pending: undefined,
660
- status: 'idle',
661
- }
648
+ let routeCursor = foundRoute || (this.routesById['__root__'] as any)
662
649
 
663
- router.notify()
664
- router.resolveNavigation()
665
- },
650
+ let matchedRoutes: AnyRoute[] = [routeCursor]
666
651
 
667
- cleanMatchCache: () => {
668
- const now = Date.now()
652
+ while (routeCursor?.parentRoute) {
653
+ routeCursor = routeCursor.parentRoute
654
+ if (routeCursor) matchedRoutes.unshift(routeCursor)
655
+ }
669
656
 
670
- Object.keys(router.matchCache).forEach((matchId) => {
671
- 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 = {}
672
662
 
673
- // Don't remove loading matches
674
- if (entry.match.status === 'loading') {
675
- return
676
- }
663
+ // Existing matches are matches that are already loaded along with
664
+ // pending matches that are still loading
677
665
 
678
- // Do not remove successful matches that are still valid
679
- if (entry.gc > 0 && entry.gc > now) {
680
- 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
681
684
  }
685
+ }
682
686
 
683
- // Everything else gets removed
684
- delete router.matchCache[matchId]
685
- })
686
- },
687
+ // Add the parsed params to the accumulated params bag
688
+ Object.assign(allParams, parsedParams)
687
689
 
688
- loadRoute: async (navigateOpts = router.location) => {
689
- const next = router.buildNext(navigateOpts)
690
- const matches = router.matchRoutes(next.pathname, {
691
- strictParseParams: true,
692
- })
693
- await router.loadMatches(matches)
694
- return matches
695
- },
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
+ : ''
696
697
 
697
- preloadRoute: async (navigateOpts = router.location, loaderOpts) => {
698
- const next = router.buildNext(navigateOpts)
699
- const matches = router.matchRoutes(next.pathname, {
700
- strictParseParams: true,
701
- })
702
- await router.loadMatches(matches, {
703
- preload: true,
704
- maxAge:
705
- loaderOpts.maxAge ??
706
- router.options.defaultPreloadMaxAge ??
707
- router.options.defaultLoaderMaxAge ??
708
- 0,
709
- gcMaxAge:
710
- loaderOpts.gcMaxAge ??
711
- router.options.defaultPreloadGcMaxAge ??
712
- router.options.defaultLoaderGcMaxAge ??
713
- 0,
714
- })
715
- return matches
716
- },
698
+ const stringifiedKey = key ? JSON.stringify(key) : ''
717
699
 
718
- matchRoutes: (pathname, opts) => {
719
- router.cleanMatchCache()
700
+ const matchId =
701
+ interpolatePath(route.id, allParams, true) + stringifiedKey
720
702
 
721
- const matches: RouteMatch[] = []
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)
722
707
 
723
- if (!router.routeTree) {
724
- return matches
708
+ if (existingMatch) {
709
+ return { ...existingMatch }
725
710
  }
726
711
 
727
- const existingMatches = [
728
- ...router.state.matches,
729
- ...(router.state.pending?.matches ?? []),
730
- ]
712
+ // Create a fresh route match
713
+ const hasLoaders = !!(
714
+ route.options.loader ||
715
+ componentTypes.some((d) => (route.options[d] as any)?.preload)
716
+ )
731
717
 
732
- const recurse = async (routes: Route<any, any>[]): Promise<void> => {
733
- const parentMatch = last(matches)
734
- let params = parentMatch?.params ?? {}
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,
741
+ }
735
742
 
736
- const filteredRoutes = router.options.filterRoutes?.(routes) ?? routes
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
+ }
737
759
 
738
- let foundRoutes: Route[] = []
760
+ try {
761
+ const validator =
762
+ typeof route.options.validateSearch === 'object'
763
+ ? route.options.validateSearch.parse
764
+ : route.options.validateSearch
739
765
 
740
- const findMatchInRoutes = (parentRoutes: Route[], routes: Route[]) => {
741
- routes.some((route) => {
742
- if (!route.routePath && route.childRoutes?.length) {
743
- return findMatchInRoutes(
744
- [...foundRoutes, route],
745
- route.childRoutes,
746
- )
747
- }
766
+ const routeSearch = validator?.(parentSearchInfo.search) ?? {}
748
767
 
749
- const fuzzy = !!(
750
- route.routePath !== '/' || route.childRoutes?.length
751
- )
768
+ const search = {
769
+ ...parentSearchInfo.search,
770
+ ...routeSearch,
771
+ }
752
772
 
753
- const matchParams = matchPathname(pathname, {
754
- to: route.fullPath,
755
- fuzzy,
756
- caseSensitive:
757
- route.options.caseSensitive ?? router.options.caseSensitive,
758
- })
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
+ })
759
781
 
760
- if (matchParams) {
761
- let parsedParams
782
+ if (opts?.throwOnError) {
783
+ throw match.searchError
784
+ }
762
785
 
763
- try {
764
- parsedParams =
765
- route.options.parseParams?.(matchParams!) ?? matchParams
766
- } catch (err) {
767
- if (opts?.strictParseParams) {
768
- throw err
769
- }
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
- params = {
773
- ...params,
774
- ...parsedParams,
775
- }
776
- }
815
+ Object.assign(match, {
816
+ ...searchInfo,
817
+ ...contextInfo,
818
+ })
819
+ })
777
820
 
778
- if (!!matchParams) {
779
- foundRoutes = [...parentRoutes, route]
821
+ return matches as any
822
+ }
823
+
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
+ }
855
+
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
780
865
  }
781
866
 
782
- return !!foundRoutes.length
783
- })
867
+ try {
868
+ handler?.(err)
869
+ } catch (errorHandlerErr) {
870
+ err = errorHandlerErr
784
871
 
785
- return !!foundRoutes.length
786
- }
872
+ if (isRedirect(errorHandlerErr)) {
873
+ throw errorHandlerErr
874
+ }
875
+ }
787
876
 
788
- findMatchInRoutes([], filteredRoutes)
877
+ this.setRouteMatch(match.id, (s) => ({
878
+ ...s,
879
+ error: err,
880
+ status: 'error',
881
+ updatedAt: Date.now(),
882
+ }))
883
+ }
789
884
 
790
- if (!foundRoutes.length) {
791
- return
792
- }
885
+ if (match.paramsError) {
886
+ handleError(match.paramsError, route.options.onParseParamsError)
887
+ }
793
888
 
794
- foundRoutes.forEach((foundRoute) => {
795
- const interpolatedPath = interpolatePath(foundRoute.routePath, params)
796
- const matchId = interpolatePath(foundRoute.routeId, params, true)
797
-
798
- const match =
799
- existingMatches.find((d) => d.matchId === matchId) ||
800
- router.matchCache[matchId]?.match ||
801
- createRouteMatch(router, foundRoute, {
802
- matchId,
803
- params,
804
- pathname: joinPaths([pathname, interpolatedPath]),
889
+ if (match.searchError) {
890
+ handleError(match.searchError, route.options.onValidateSearchError)
891
+ }
892
+
893
+ try {
894
+ await route.options.beforeLoad?.({
895
+ ...match,
896
+ preload: !!opts?.preload,
805
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
+ }
806
907
 
807
- matches.push(match)
808
- })
908
+ throw err
909
+ }
809
910
 
810
- 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
+ }
811
927
 
812
- if (foundRoute.childRoutes?.length) {
813
- recurse(foundRoute.childRoutes)
814
- }
815
- }
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
+ }
816
935
 
817
- recurse([router.routeTree])
936
+ const loadPromise = (async () => {
937
+ let latestPromise
818
938
 
819
- cascadeLoaderData(matches)
939
+ const componentsPromise = Promise.all(
940
+ componentTypes.map(async (type) => {
941
+ const component = route.options[type]
820
942
 
821
- return matches
822
- },
943
+ if ((component as any)?.preload) {
944
+ await (component as any).preload()
945
+ }
946
+ }),
947
+ )
823
948
 
824
- loadMatches: async (resolvedMatches, loaderOpts) => {
825
- const matchPromises = resolvedMatches.map(async (match) => {
826
- // Validate the match (loads search params etc)
827
- match.__.validate()
828
- match.load(loaderOpts)
949
+ const loaderPromise = route.options.loader?.({
950
+ ...match,
951
+ preload: !!opts?.preload,
952
+ parentMatchPromise,
953
+ })
829
954
 
830
- if (match.__.loadPromise) {
831
- // Wait for the first sign of activity from the match
832
- await match.__.loadPromise
833
- }
834
- })
955
+ const handleError = (err: any) => {
956
+ if (isRedirect(err)) {
957
+ if (!opts?.preload) {
958
+ this.navigate(err as any)
959
+ }
960
+ return true
961
+ }
835
962
 
836
- router.notify()
963
+ return false
964
+ }
837
965
 
838
- await Promise.all(matchPromises)
839
- },
966
+ try {
967
+ const [_, loader] = await Promise.all([
968
+ componentsPromise,
969
+ loaderPromise,
970
+ ])
971
+ if ((latestPromise = checkLatest())) return await latestPromise
840
972
 
841
- invalidateRoute: (opts: MatchLocation) => {
842
- const next = router.buildNext(opts)
843
- const unloadedMatchIds = router
844
- .matchRoutes(next.pathname)
845
- .map((d) => d.matchId)
846
- ;[
847
- ...router.state.matches,
848
- ...(router.state.pending?.matches ?? []),
849
- ].forEach((match) => {
850
- if (unloadedMatchIds.includes(match.matchId)) {
851
- match.invalidate()
852
- }
853
- })
854
- },
973
+ this.setRouteMatchData(match.id, () => loader, opts)
974
+ } catch (err) {
975
+ if ((latestPromise = checkLatest())) return await latestPromise
855
976
 
856
- reload: () =>
857
- router.__.navigate({
858
- fromCurrent: true,
859
- replace: true,
860
- search: true,
861
- }),
977
+ if (handleError(err)) {
978
+ return
979
+ }
862
980
 
863
- resolvePath: (from: string, path: string) => {
864
- return resolvePath(router.basepath!, from, cleanPath(path))
865
- },
981
+ const errorHandler =
982
+ route.options.onLoadError ?? route.options.onError
866
983
 
867
- matchRoute: (location, opts) => {
868
- // const location = router.buildNext(opts)
984
+ let caughtError = err
869
985
 
870
- location = {
871
- ...location,
872
- to: location.to
873
- ? router.resolvePath(location.from ?? '', location.to)
874
- : undefined,
875
- }
986
+ try {
987
+ errorHandler?.(err)
988
+ } catch (errorHandlerErr) {
989
+ caughtError = errorHandlerErr
876
990
 
877
- const next = router.buildNext(location)
991
+ if (handleError(errorHandlerErr)) {
992
+ return
993
+ }
994
+ }
878
995
 
879
- if (opts?.pending) {
880
- if (!router.state.pending?.location) {
881
- return false
882
- }
883
- return !!matchPathname(router.state.pending.location.pathname, {
884
- ...opts,
885
- to: next.pathname,
886
- })
887
- }
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
+ })
888
1019
 
889
- return !!matchPathname(router.state.location.pathname, {
890
- ...opts,
891
- to: next.pathname,
892
- })
893
- },
1020
+ await Promise.all(matchPromises)
1021
+ }
894
1022
 
895
- navigate: async ({ from, to = '.', search, hash, replace, params }) => {
896
- // If this link simply reloads the current route,
897
- // 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
+ }
898
1030
 
899
- // If this `to` is a valid external URL, return
900
- // null for LinkUtils
901
- const toString = String(to)
902
- const fromString = String(from)
1031
+ resolvePath = (from: string, path: string) => {
1032
+ return resolvePath(this.basepath!, from, cleanPath(path))
1033
+ }
903
1034
 
904
- 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
+ }
905
1071
 
906
- try {
907
- new URL(`${toString}`)
908
- isExternal = true
909
- } 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
+ }
910
1091
 
911
- invariant(
912
- !isExternal,
913
- 'Attempting to navigate to external url with router.navigate!',
914
- )
1092
+ const baseLocation = opts?.pending
1093
+ ? this.state.location
1094
+ : this.state.resolvedLocation
915
1095
 
916
- return router.__.navigate({
917
- from: fromString,
918
- to: toString,
919
- search,
920
- hash,
921
- replace,
922
- params,
923
- })
924
- },
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
925
1104
 
926
- buildLink: ({
1105
+ if (!match) {
1106
+ return false
1107
+ }
1108
+
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 = {
927
1144
  from,
928
- to = '.',
1145
+ to,
929
1146
  search,
930
1147
  params,
931
1148
  hash,
932
- target,
933
1149
  replace,
934
- activeOptions,
935
- preload,
936
- preloadMaxAge: userPreloadMaxAge,
937
- preloadGcMaxAge: userPreloadGcMaxAge,
938
- preloadDelay: userPreloadDelay,
939
- disabled,
940
- }) => {
941
- // If this link simply reloads the current route,
942
- // make sure it has a new key so it will trigger a data refresh
943
-
944
- // If this `to` is a valid external URL, return
945
- // null for LinkUtils
1150
+ }
946
1151
 
947
- try {
948
- new URL(`${to}`)
949
- return {
950
- type: 'external',
951
- href: to,
952
- }
953
- } catch (e) {}
954
-
955
- const nextOpts = {
956
- from,
957
- to,
958
- search,
959
- params,
960
- hash,
961
- replace,
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()
1189
+
1190
+ // All is well? Navigate!
1191
+ this.#commitLocation(nextOpts as any)
962
1192
  }
1193
+ }
963
1194
 
964
- 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
+ }
965
1204
 
966
- preload = preload ?? router.options.defaultPreload
967
- const preloadDelay =
968
- 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
+ }
969
1211
 
970
- // Compare path/hash for matches
971
- const pathIsEqual = router.state.location.pathname === next.pathname
972
- const currentPathSplit = router.state.location.pathname.split('/')
973
- const nextPathSplit = next.pathname.split('/')
974
- const pathIsFuzzyEqual = nextPathSplit.every(
975
- (d, i) => d === currentPathSplit[i],
976
- )
977
- const hashIsEqual = router.state.location.hash === next.hash
978
- // Combine the matches based on user options
979
- const pathTest = activeOptions?.exact ? pathIsEqual : pathIsFuzzyEqual
980
- const hashTest = activeOptions?.includeHash ? hashIsEqual : true
981
-
982
- // The final "active" test
983
- const isActive = pathTest && hashTest
984
-
985
- // The click handler
986
- const handleClick = (e: MouseEvent) => {
987
- if (
988
- !disabled &&
989
- !isCtrlEvent(e) &&
990
- !e.defaultPrevented &&
991
- (!target || target === '_self') &&
992
- e.button === 0
993
- ) {
994
- e.preventDefault()
995
- if (pathIsEqual && !search && !hash) {
996
- router.invalidateRoute(nextOpts)
997
- }
1212
+ const handleEnter = (e: MouseEvent) => {
1213
+ const target = (e.target || {}) as LinkCurrentTargetElement
998
1214
 
999
- // All is well? Navigate!)
1000
- router.__.navigate(nextOpts)
1215
+ if (preload) {
1216
+ if (target.preloadTimeout) {
1217
+ return
1001
1218
  }
1002
- }
1003
1219
 
1004
- // The click handler
1005
- const handleFocus = (e: MouseEvent) => {
1006
- if (preload) {
1007
- router.preloadRoute(nextOpts, {
1008
- maxAge: userPreloadMaxAge,
1009
- 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! ☝️')
1010
1225
  })
1011
- }
1226
+ }, preloadDelay)
1012
1227
  }
1228
+ }
1013
1229
 
1014
- const handleEnter = (e: MouseEvent) => {
1015
- const target = (e.target || {}) as LinkCurrentTargetElement
1016
-
1017
- if (preload) {
1018
- if (target.preloadTimeout) {
1019
- return
1020
- }
1230
+ const handleLeave = (e: MouseEvent) => {
1231
+ const target = (e.target || {}) as LinkCurrentTargetElement
1021
1232
 
1022
- target.preloadTimeout = setTimeout(() => {
1023
- target.preloadTimeout = null
1024
- router.preloadRoute(nextOpts, {
1025
- maxAge: userPreloadMaxAge,
1026
- gcMaxAge: userPreloadGcMaxAge,
1027
- })
1028
- }, preloadDelay)
1029
- }
1233
+ if (target.preloadTimeout) {
1234
+ clearTimeout(target.preloadTimeout)
1235
+ target.preloadTimeout = null
1030
1236
  }
1237
+ }
1031
1238
 
1032
- const handleLeave = (e: MouseEvent) => {
1033
- 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
+ }
1034
1251
 
1035
- if (target.preloadTimeout) {
1036
- clearTimeout(target.preloadTimeout)
1037
- target.preloadTimeout = null
1038
- }
1039
- }
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
+ }
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
+ )
1040
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) => {
1041
1275
  return {
1042
- type: 'internal',
1043
- next,
1044
- handleFocus,
1045
- handleClick,
1046
- handleEnter,
1047
- handleLeave,
1048
- isActive,
1049
- disabled,
1276
+ ...s,
1277
+ ...ctx.router.state,
1278
+ resolvedLocation: ctx.router.state.location,
1050
1279
  }
1051
- },
1052
- buildNext: (opts: BuildNextOptions) => {
1053
- const next = router.__.buildLocation(opts)
1280
+ })
1054
1281
 
1055
- const matches = router.matchRoutes(next.pathname)
1282
+ await this.load()
1056
1283
 
1057
- const __preSearchFilters = matches
1058
- .map((match) => match.options.preSearchFilters ?? [])
1059
- .flat()
1060
- .filter(Boolean)
1284
+ return
1285
+ }
1061
1286
 
1062
- const __postSearchFilters = matches
1063
- .map((match) => match.options.postSearchFilters ?? [])
1064
- .flat()
1065
- .filter(Boolean)
1287
+ injectedHtml: (string | (() => Promise<string> | string))[] = []
1066
1288
 
1067
- return router.__.buildLocation({
1068
- ...opts,
1069
- __preSearchFilters,
1070
- __postSearchFilters,
1289
+ injectHtml = async (html: string | (() => Promise<string> | string)) => {
1290
+ this.injectedHtml.push(html)
1291
+ }
1292
+
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>`
1071
1309
  })
1072
- },
1073
1310
 
1074
- __: {
1075
- buildRouteTree: (rootRouteConfig: RouteConfig) => {
1076
- const recurseRoutes = (
1077
- routeConfigs: RouteConfig[],
1078
- parent?: Route<TAllRouteInfo, any>,
1079
- ): Route<TAllRouteInfo, any>[] => {
1080
- return routeConfigs.map((routeConfig) => {
1081
- const routeOptions = routeConfig.options
1082
- const route = createRoute(routeConfig, routeOptions, parent, router)
1083
- const existingRoute = (router.routesById as any)[route.routeId]
1084
-
1085
- if (existingRoute) {
1086
- if (process.env.NODE_ENV !== 'production') {
1087
- console.warn(
1088
- `Duplicate routes found with id: ${String(route.routeId)}`,
1089
- router.routesById,
1090
- route,
1091
- )
1092
- }
1093
- throw new Error()
1094
- }
1311
+ return () => this.hydrateData<T>(key)
1312
+ }
1313
+
1314
+ return () => undefined
1315
+ }
1095
1316
 
1096
- ;(router.routesById as any)[route.routeId] = route
1317
+ hydrateData = <T = unknown>(key: any) => {
1318
+ if (typeof document !== 'undefined') {
1319
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key)
1097
1320
 
1098
- const children = routeConfig.children as RouteConfig[]
1321
+ return window[`__TSR_DEHYDRATED__${strKey}` as any] as T
1322
+ }
1099
1323
 
1100
- route.childRoutes = children?.length
1101
- ? recurseRoutes(children, route)
1102
- : undefined
1324
+ return undefined
1325
+ }
1103
1326
 
1104
- return route
1105
- })
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
+ }
1106
1359
  }
1107
1360
 
1108
- const routes = recurseRoutes([rootRouteConfig])
1361
+ const children = route.children as Route[]
1109
1362
 
1110
- return routes[0]!
1111
- },
1363
+ if (children?.length) {
1364
+ recurseRoutes(children)
1365
+ }
1366
+ })
1367
+ }
1112
1368
 
1113
- parseLocation: (
1114
- location: History['location'],
1115
- previousLocation?: Location,
1116
- ): Location => {
1117
- const parsedSearch = router.options.parseSearch(location.search)
1369
+ recurseRoutes([routeTree])
1118
1370
 
1119
- return {
1120
- pathname: location.pathname,
1121
- searchStr: location.search,
1122
- search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1123
- hash: location.hash.split('#').reverse()[0] ?? '',
1124
- href: `${location.pathname}${location.search}${location.hash}`,
1125
- state: location.state as LocationState,
1126
- 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()
1127
1378
  }
1128
- },
1129
1379
 
1130
- navigate: (location: BuildNextOptions & { replace?: boolean }) => {
1131
- const next = router.buildNext(location)
1132
- return router.__.commitLocation(next, location.replace)
1133
- },
1380
+ const score = parsed.map((d) => {
1381
+ if (d.type === 'param') {
1382
+ return 0.5
1383
+ }
1134
1384
 
1135
- buildLocation: (dest: BuildNextOptions = {}): Location => {
1136
- // const resolvedFrom: Location = {
1137
- // ...router.location,
1138
- const fromPathname = dest.fromCurrent
1139
- ? router.location.pathname
1140
- : dest.from ?? router.location.pathname
1141
-
1142
- let pathname = resolvePath(
1143
- router.basepath ?? '/',
1144
- fromPathname,
1145
- `${dest.to ?? '.'}`,
1146
- )
1385
+ if (d.type === 'wildcard') {
1386
+ return 0.25
1387
+ }
1147
1388
 
1148
- const fromMatches = router.matchRoutes(router.location.pathname, {
1149
- strictParseParams: true,
1389
+ return 1
1150
1390
  })
1151
1391
 
1152
- 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
1153
1396
 
1154
- const prevParams = { ...last(fromMatches)?.params }
1397
+ if (isIndex !== 0) return isIndex
1155
1398
 
1156
- let nextParams =
1157
- (dest.params ?? true) === true
1158
- ? prevParams
1159
- : functionalUpdate(dest.params!, prevParams)
1399
+ const length = Math.min(a.score.length, b.score.length)
1160
1400
 
1161
- if (nextParams) {
1162
- toMatches
1163
- .map((d) => d.options.stringifyParams)
1164
- .filter(Boolean)
1165
- .forEach((fn) => {
1166
- Object.assign({}, nextParams!, fn!(nextParams!))
1167
- })
1401
+ // Sort by length of score
1402
+ if (a.score.length !== b.score.length) {
1403
+ return b.score.length - a.score.length
1168
1404
  }
1169
1405
 
1170
- 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
+ }
1171
1412
 
1172
- // Pre filters first
1173
- const preFilteredSearch = dest.__preSearchFilters?.length
1174
- ? dest.__preSearchFilters.reduce(
1175
- (prev, next) => next(prev),
1176
- router.location.search,
1177
- )
1178
- : router.location.search
1179
-
1180
- // Then the link/navigate function
1181
- const destSearch =
1182
- dest.search === true
1183
- ? preFilteredSearch // Preserve resolvedFrom true
1184
- : dest.search
1185
- ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1186
- : dest.__preSearchFilters?.length
1187
- ? preFilteredSearch // Preserve resolvedFrom filters
1188
- : {}
1189
-
1190
- // Then post filters
1191
- const postFilteredSearch = dest.__postSearchFilters?.length
1192
- ? dest.__postSearchFilters.reduce(
1193
- (prev, next) => next(prev),
1194
- destSearch,
1195
- )
1196
- : 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
+ }
1197
1419
 
1198
- const search = replaceEqualDeep(
1199
- router.location.search,
1200
- postFilteredSearch,
1201
- )
1420
+ // Sort by length of trimmed full path
1421
+ if (a.trimmed !== b.trimmed) {
1422
+ return a.trimmed > b.trimmed ? 1 : -1
1423
+ }
1202
1424
 
1203
- const searchStr = router.options.stringifySearch(search)
1204
- let hash =
1205
- dest.hash === true
1206
- ? router.location.hash
1207
- : functionalUpdate(dest.hash!, router.location.hash)
1208
- 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
+ }
1209
1433
 
1210
- return {
1211
- pathname,
1212
- search,
1213
- searchStr,
1214
- state: router.location.state,
1215
- hash,
1216
- href: `${pathname}${searchStr}${hash}`,
1217
- key: dest.key,
1218
- }
1219
- },
1434
+ #parseLocation = (previousLocation?: ParsedLocation): ParsedLocation => {
1435
+ let { pathname, search, hash, state } = this.history.location
1220
1436
 
1221
- commitLocation: (next: Location, replace?: boolean): Promise<void> => {
1222
- const id = '' + Date.now() + Math.random()
1437
+ const parsedSearch = this.options.parseSearch(search)
1223
1438
 
1224
- 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
+ }
1225
1449
 
1226
- let nextAction: 'push' | 'replace' = 'replace'
1450
+ #buildLocation = (dest: BuildNextOptions = {}): ParsedLocation => {
1451
+ dest.fromCurrent = dest.fromCurrent ?? dest.to === ''
1227
1452
 
1228
- if (!replace) {
1229
- nextAction = 'push'
1230
- }
1453
+ const fromPathname = dest.fromCurrent
1454
+ ? this.state.location.pathname
1455
+ : dest.from ?? this.state.location.pathname
1231
1456
 
1232
- const isSameUrl =
1233
- router.__.parseLocation(history.location).href === next.href
1457
+ let pathname = resolvePath(
1458
+ this.basepath ?? '/',
1459
+ fromPathname,
1460
+ `${dest.to ?? ''}`,
1461
+ )
1234
1462
 
1235
- if (isSameUrl && !next.key) {
1236
- nextAction = 'replace'
1237
- }
1463
+ const fromMatches = this.matchRoutes(
1464
+ this.state.location.pathname,
1465
+ this.state.location.search,
1466
+ )
1238
1467
 
1239
- if (nextAction === 'replace') {
1240
- history.replace(
1241
- {
1242
- pathname: next.pathname,
1243
- hash: next.hash,
1244
- search: next.searchStr,
1245
- },
1246
- {
1247
- id,
1248
- },
1249
- )
1250
- } else {
1251
- history.push(
1252
- {
1253
- pathname: next.pathname,
1254
- hash: next.hash,
1255
- search: next.searchStr,
1256
- },
1257
- {
1258
- id,
1259
- },
1260
- )
1261
- }
1468
+ const prevParams = { ...last(fromMatches)?.params }
1262
1469
 
1263
- router.navigationPromise = new Promise((resolve) => {
1264
- const previousNavigationResolve = router.resolveNavigation
1470
+ let nextParams =
1471
+ (dest.params ?? true) === true
1472
+ ? prevParams
1473
+ : functionalUpdate(dest.params!, prevParams)
1265
1474
 
1266
- router.resolveNavigation = () => {
1267
- previousNavigationResolve()
1268
- resolve()
1269
- }
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!) }
1270
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) ?? []
1271
1503
 
1272
- return router.navigationPromise
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
1572
+
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),
1273
1606
  },
1607
+ }))
1608
+ }
1609
+
1610
+ setRouteMatchData = (
1611
+ id: string,
1612
+ updater: (prev: any) => any,
1613
+ opts?: {
1614
+ updatedAt?: number
1615
+ maxAge?: number
1274
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
+ }
1275
1652
  }
1276
1653
 
1277
- router.update(userOptions)
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
+ }
1685
+ }
1278
1686
 
1279
- // Allow frameworks to hook into the router creation
1280
- router.options.createRouter?.(router)
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
+ }
1281
1693
 
1282
- return router
1694
+ const match = this.getRouteMatch(opts?.matchId)
1695
+
1696
+ if (!match) {
1697
+ return false
1698
+ }
1699
+
1700
+ const now = Date.now()
1701
+
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
+ }
1283
1725
  }
1284
1726
 
1285
1727
  function isCtrlEvent(e: MouseEvent) {
1286
1728
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
1287
1729
  }
1288
1730
 
1289
- function cascadeLoaderData(matches: RouteMatch<any, any>[]) {
1290
- matches.forEach((match, index) => {
1291
- const parent = matches[index - 1]
1731
+ export type AnyRedirect = Redirect<any, any, any>
1292
1732
 
1293
- if (parent) {
1294
- match.loaderData = replaceEqualDeep(match.loaderData, {
1295
- ...parent.loaderData,
1296
- ...match.routeLoaderData,
1297
- })
1298
- }
1299
- })
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
+ }
1300
1775
  }