@tanstack/router-core 0.0.1-beta.15 → 0.0.1-beta.151

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 +1113 -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 +1417 -2105
  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 +604 -426
  25. package/build/umd/index.development.js +1640 -2224
  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 +120 -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 -204
  38. package/src/router.ts +1494 -1017
  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 -231
  57. package/build/cjs/packages/router-core/src/routeMatch.js.map +0 -1
  58. package/build/cjs/packages/router-core/src/router.js +0 -833
  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 -514
  64. package/src/routeMatch.ts +0 -319
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,1094 +227,1564 @@ 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,
306
+ const matchesByIdChanged = prev.matchesById !== this.state.matchesById
307
+ let matchesChanged
308
+ let pendingMatchesChanged
309
+
310
+ if (!matchesByIdChanged) {
311
+ matchesChanged =
312
+ prev.matchIds.length !== this.state.matchIds.length ||
313
+ prev.matchIds.some((d, i) => d !== this.state.matchIds[i])
314
+
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
+ }
322
+
323
+ if (matchesByIdChanged || matchesChanged) {
324
+ this.state.matches = this.state.matchIds.map((id) => {
325
+ return this.state.matchesById[id] as any
326
+ })
327
+ }
328
+
329
+ if (matchesByIdChanged || pendingMatchesChanged) {
330
+ this.state.pendingMatches = this.state.pendingMatchIds.map((id) => {
331
+ return this.state.matchesById[id] as any
332
+ })
333
+ }
334
+
335
+ this.state.isFetching = [
336
+ ...this.state.matches,
337
+ ...this.state.pendingMatches,
338
+ ].some((d) => d.isFetching)
339
+ },
340
+ defaultPriority: 'low',
341
+ },
342
+ )
343
+
344
+ this.state = this.__store.state
345
+
346
+ this.update(options)
347
+
348
+ const next = this.buildNext({
349
+ hash: true,
350
+ fromCurrent: true,
351
+ search: true,
352
+ state: true,
353
+ })
354
+
355
+ if (this.state.location.href !== next.href) {
356
+ this.#commitLocation({ ...next, replace: true })
357
+ }
360
358
  }
361
- }
362
359
 
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,
360
+ reset = () => {
361
+ this.__store.setState((s) => Object.assign(s, getInitialRouterState()))
379
362
  }
380
363
 
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
- )
364
+ mount = () => {
365
+ // If the router matches are empty, start loading the matches
366
+ // if (!this.state.matches.length) {
367
+ this.safeLoad()
368
+ // }
369
+ }
420
370
 
421
- if (
422
- router.state.isFetching !== isFetching ||
423
- router.state.isPreloading !== isPreloading
424
- ) {
425
- router.state = {
426
- ...router.state,
427
- isFetching,
428
- isPreloading,
429
- }
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()
430
387
  }
431
388
 
432
- cascadeLoaderData(router.state.matches)
433
- router.listeners.forEach((listener) => listener(router))
434
- },
389
+ this.history =
390
+ this.options.history ??
391
+ (isServer ? createMemoryHistory() : createBrowserHistory()!)
435
392
 
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
- },
393
+ const parsedLocation = this.#parseLocation()
451
394
 
452
- hydrateState: (dehydratedState) => {
453
- // Match the routes
454
- const matches = router.matchRoutes(router.location.pathname, {
455
- strictParseParams: true,
456
- })
395
+ this.__store.setState((s) => ({
396
+ ...s,
397
+ resolvedLocation: parsedLocation,
398
+ location: parsedLocation,
399
+ }))
457
400
 
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)
401
+ this.#unsubHistory = this.history.listen(() => {
402
+ this.safeLoad({
403
+ next: this.#parseLocation(this.state.location),
404
+ })
465
405
  })
406
+ }
466
407
 
467
- matches.forEach((match) => match.__.validate())
408
+ const { basepath, routeTree } = this.options
468
409
 
469
- router.state = {
470
- ...router.state,
471
- ...dehydratedState,
472
- matches,
473
- }
474
- },
410
+ this.basepath = `/${trimPath(basepath ?? '') ?? ''}`
475
411
 
476
- mount: () => {
477
- const next = router.__.buildLocation({
478
- to: '.',
479
- search: true,
480
- hash: true,
481
- })
412
+ if (routeTree && routeTree !== this.routeTree) {
413
+ this.#buildRouteTree(routeTree)
414
+ }
482
415
 
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
- }
416
+ return this
417
+ }
418
+
419
+ buildNext = (opts: BuildNextOptions): ParsedLocation => {
420
+ const next = this.#buildLocation(opts)
421
+
422
+ const __matches = this.matchRoutes(next.pathname, next.search)
423
+
424
+ return this.#buildLocation({
425
+ ...opts,
426
+ __matches,
427
+ })
428
+ }
429
+
430
+ cancelMatches = () => {
431
+ this.state.matches.forEach((match) => {
432
+ this.cancelMatch(match.id)
433
+ })
434
+ }
435
+
436
+ cancelMatch = (id: string) => {
437
+ this.getRouteMatch(id)?.abortController?.abort()
438
+ }
439
+
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
+ }
446
+
447
+ latestLoadPromise: Promise<void> = Promise.resolve()
488
448
 
489
- if (!router.state.matches.length) {
490
- router.load()
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
491
457
  }
492
458
 
493
- const unsub = router.history.listen((event) => {
494
- router.load(router.__.parseLocation(event.location, router.location))
459
+ // Cancel any pending matches
460
+ // this.cancelMatches()
461
+
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
+ }
472
+
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,
480
+ },
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
+ }))
495
489
  })
496
490
 
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
- }
491
+ try {
492
+ // Load the matches
493
+ await this.loadMatches(pendingMatches)
504
494
 
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)
495
+ // Only apply the latest transition
496
+ if ((latestPromise = checkLatest())) {
497
+ return await latestPromise
511
498
  }
512
- }
513
- },
514
499
 
515
- onFocus: () => {
516
- router.load()
517
- },
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
+ }
518
513
 
519
- update: (opts) => {
520
- const newHistory = opts?.history !== router.history
521
- if (!router.location || newHistory) {
522
- if (opts?.history) {
523
- router.history = opts.history
514
+ resolve()
515
+ } catch (err) {
516
+ // Only apply the latest transition
517
+ if ((latestPromise = checkLatest())) {
518
+ return await latestPromise
524
519
  }
525
- router.location = router.__.parseLocation(router.history.location)
526
- router.state.location = router.location
520
+
521
+ reject(err)
527
522
  }
523
+ })
524
+
525
+ this.latestLoadPromise = promise
528
526
 
529
- Object.assign(router.options, opts)
527
+ return this.latestLoadPromise
528
+ }
530
529
 
531
- const { basepath, routeConfig } = router.options
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
+ }
532
543
 
533
- router.basepath = cleanPath(`/${basepath ?? ''}`)
544
+ let hadNew = false
534
545
 
535
- if (routeConfig) {
536
- router.routesById = {} as any
537
- router.routeTree = router.__.buildRouteTree(routeConfig)
546
+ nextMatches.forEach((match) => {
547
+ if (!nextMatchesById[match.id]) {
548
+ hadNew = true
549
+ nextMatchesById[match.id] = match
538
550
  }
551
+ })
539
552
 
540
- return router as any
541
- },
553
+ if (!hadNew) {
554
+ return prevMatchesById
555
+ }
542
556
 
543
- cancelMatches: () => {
544
- ;[
545
- ...router.state.matches,
546
- ...(router.state.pending?.matches ?? []),
547
- ].forEach((match) => {
548
- match.cancel()
549
- })
550
- },
557
+ return nextMatchesById
558
+ }
559
+
560
+ getRoute = <TId extends keyof TRoutesInfo['routesById']>(
561
+ id: TId,
562
+ ): TRoutesInfo['routesById'][TId] => {
563
+ const route = this.routesById[id]
551
564
 
552
- load: async (next?: Location) => {
553
- const id = Math.random()
554
- router.startedLoadingAt = id
565
+ invariant(route, `Route with id "${id as string}" not found`)
555
566
 
556
- if (next) {
557
- // Ingest the new location
558
- router.location = next
567
+ return route
568
+ }
569
+
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),
559
584
  }
585
+ })
560
586
 
561
- // Cancel any pending matches
562
- router.cancelMatches()
587
+ await this.loadMatches(matches, {
588
+ preload: true,
589
+ maxAge: navigateOpts.maxAge,
590
+ })
563
591
 
564
- // Match the routes
565
- const matches = router.matchRoutes(router.location.pathname, {
566
- strictParseParams: true,
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)
608
+ )
567
609
  })
610
+ .map((d) => d.id)
568
611
 
569
- if (typeof document !== 'undefined') {
570
- router.state = {
571
- ...router.state,
572
- pending: {
573
- matches: matches,
574
- location: router.location,
575
- },
576
- status: 'loading',
577
- }
578
- } else {
579
- router.state = {
580
- ...router.state,
581
- matches: matches,
582
- location: router.location,
583
- status: 'loading',
612
+ if (outdatedMatchIds.length) {
613
+ this.__store.setState((s) => {
614
+ const matchesById = { ...s.matchesById }
615
+ outdatedMatchIds.forEach((id) => {
616
+ delete matchesById[id]
617
+ })
618
+ return {
619
+ ...s,
620
+ matchesById,
584
621
  }
585
- }
586
-
587
- router.notify()
622
+ })
623
+ }
624
+ }
588
625
 
589
- // Load the matches
590
- await router.loadMatches(matches)
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(
635
+ this.basepath,
636
+ trimPathRight(pathname),
637
+ {
638
+ to: route.fullPath,
639
+ caseSensitive:
640
+ route.options.caseSensitive ?? this.options.caseSensitive,
641
+ },
642
+ )
591
643
 
592
- if (router.startedLoadingAt !== id) {
593
- // Ignore side-effects of match loading
594
- return router.navigationPromise
644
+ if (matchedParams) {
645
+ routeParams = matchedParams
646
+ return true
595
647
  }
596
648
 
597
- const previousMatches = router.state.matches
649
+ return false
650
+ })
598
651
 
599
- const exiting: RouteMatch[] = [],
600
- staying: RouteMatch[] = []
652
+ let routeCursor = foundRoute || (this.routesById['__root__'] as any)
601
653
 
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
- })
654
+ let matchedRoutes: AnyRoute[] = [routeCursor]
609
655
 
610
- const entering = matches.filter((d) => {
611
- return !previousMatches.find((dd) => dd.matchId === d.matchId)
612
- })
656
+ while (routeCursor?.parentRoute) {
657
+ routeCursor = routeCursor.parentRoute
658
+ if (routeCursor) matchedRoutes.unshift(routeCursor)
659
+ }
613
660
 
614
- const now = Date.now()
661
+ // Alright, by now we should have all of our
662
+ // matching routes and their param pairs, let's
663
+ // Turn them into actual `Match` objects and
664
+ // accumulate the params into a single params bag
665
+ let allParams = {}
615
666
 
616
- exiting.forEach((d) => {
617
- d.__.onExit?.({
618
- params: d.params,
619
- search: d.routeSearch,
667
+ // Existing matches are matches that are already loaded along with
668
+ // pending matches that are still loading
669
+
670
+ const matches = matchedRoutes.map((route) => {
671
+ let parsedParams
672
+ let parsedParamsError
673
+
674
+ try {
675
+ parsedParams =
676
+ (route.options.parseParams as any)?.(routeParams!) ?? routeParams
677
+ // (typeof route.options.parseParams === 'object' &&
678
+ // route.options.parseParams.parse
679
+ // ? route.options.parseParams.parse(routeParams)
680
+ // : (route.options.parseParams as any)?.(routeParams!)) ?? routeParams
681
+ } catch (err: any) {
682
+ parsedParamsError = new PathParamError(err.message, {
683
+ cause: err,
620
684
  })
621
685
 
622
- // Clear idle error states when match leaves
623
- if (d.status === 'error' && !d.isFetching) {
624
- d.status = 'idle'
625
- d.error = undefined
686
+ if (opts?.throwOnError) {
687
+ throw parsedParamsError
626
688
  }
627
- const gc = Math.max(
628
- d.options.loaderGcMaxAge ?? router.options.defaultLoaderGcMaxAge ?? 0,
629
- d.options.loaderMaxAge ?? router.options.defaultLoaderMaxAge ?? 0,
630
- )
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
- })
689
+ }
638
690
 
639
- staying.forEach((d) => {
640
- d.options.onTransition?.({
641
- params: d.params,
642
- search: d.routeSearch,
643
- })
644
- })
691
+ // Add the parsed params to the accumulated params bag
692
+ Object.assign(allParams, parsedParams)
645
693
 
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]
652
- })
694
+ const interpolatedPath = interpolatePath(route.path, allParams)
695
+ const key = route.options.key
696
+ ? route.options.key({
697
+ params: allParams,
698
+ search: locationSearch,
699
+ }) ?? ''
700
+ : ''
701
+
702
+ const stringifiedKey = key ? JSON.stringify(key) : ''
703
+
704
+ const matchId =
705
+ interpolatePath(route.id, allParams, true) + stringifiedKey
706
+
707
+ // Waste not, want not. If we already have a match for this route,
708
+ // reuse it. This is important for layout routes, which might stick
709
+ // around between navigation actions that only change leaf routes.
710
+ const existingMatch = this.getRouteMatch(matchId)
653
711
 
654
- if (router.startedLoadingAt !== id) {
655
- // Ignore side-effects of match loading
656
- return
712
+ if (existingMatch) {
713
+ return { ...existingMatch }
657
714
  }
658
715
 
659
- matches.forEach((match) => {
660
- // Clear actions
661
- if (match.action) {
662
- match.action.current = undefined
663
- match.action.submissions = []
664
- }
665
- })
716
+ // Create a fresh route match
717
+ const hasLoaders = !!(
718
+ route.options.loader ||
719
+ componentTypes.some((d) => (route.options[d] as any)?.preload)
720
+ )
666
721
 
667
- router.state = {
668
- ...router.state,
669
- location: router.location,
670
- matches,
671
- pending: undefined,
672
- status: 'idle',
722
+ const routeMatch: RouteMatch = {
723
+ id: matchId,
724
+ key: stringifiedKey,
725
+ routeId: route.id,
726
+ params: allParams,
727
+ pathname: joinPaths([this.basepath, interpolatedPath]),
728
+ updatedAt: Date.now(),
729
+ invalidAt: Infinity,
730
+ preloadInvalidAt: Infinity,
731
+ routeSearch: {},
732
+ search: {} as any,
733
+ status: hasLoaders ? 'idle' : 'success',
734
+ isFetching: false,
735
+ invalid: false,
736
+ error: undefined,
737
+ paramsError: parsedParamsError,
738
+ searchError: undefined,
739
+ loaderData: undefined,
740
+ loadPromise: Promise.resolve(),
741
+ routeContext: undefined!,
742
+ context: undefined!,
743
+ abortController: new AbortController(),
744
+ fetchedAt: 0,
673
745
  }
674
746
 
675
- router.notify()
676
- router.resolveNavigation()
677
- },
747
+ return routeMatch
748
+ })
749
+
750
+ // Take each match and resolve its search params and context
751
+ // This has to happen after the matches are created or found
752
+ // so that we can use the parent match's search params and context
753
+ matches.forEach((match, i): any => {
754
+ const parentMatch = matches[i - 1]
755
+ const route = this.getRoute(match.routeId)
756
+
757
+ const searchInfo = (() => {
758
+ // Validate the search params and stabilize them
759
+ const parentSearchInfo = {
760
+ search: parentMatch?.search ?? locationSearch,
761
+ routeSearch: parentMatch?.routeSearch ?? locationSearch,
762
+ }
678
763
 
679
- cleanMatchCache: () => {
680
- const now = Date.now()
764
+ try {
765
+ const validator =
766
+ typeof route.options.validateSearch === 'object'
767
+ ? route.options.validateSearch.parse
768
+ : route.options.validateSearch
681
769
 
682
- Object.keys(router.matchCache).forEach((matchId) => {
683
- const entry = router.matchCache[matchId]!
770
+ const routeSearch = validator?.(parentSearchInfo.search) ?? {}
684
771
 
685
- // Don't remove loading matches
686
- if (entry.match.status === 'loading') {
687
- return
688
- }
772
+ const search = {
773
+ ...parentSearchInfo.search,
774
+ ...routeSearch,
775
+ }
689
776
 
690
- // Do not remove successful matches that are still valid
691
- if (entry.gc > 0 && entry.gc > now) {
692
- return
777
+ return {
778
+ routeSearch: replaceEqualDeep(match.routeSearch, routeSearch),
779
+ search: replaceEqualDeep(match.search, search),
780
+ }
781
+ } catch (err: any) {
782
+ match.searchError = new SearchParamError(err.message, {
783
+ cause: err,
784
+ })
785
+
786
+ if (opts?.throwOnError) {
787
+ throw match.searchError
788
+ }
789
+
790
+ return parentSearchInfo
693
791
  }
792
+ })()
694
793
 
695
- // Everything else gets removed
696
- delete router.matchCache[matchId]
794
+ Object.assign(match, {
795
+ ...searchInfo,
697
796
  })
698
- },
699
797
 
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
- },
798
+ const contextInfo = (() => {
799
+ try {
800
+ const routeContext =
801
+ route.options.getContext?.({
802
+ parentContext: parentMatch?.routeContext ?? {},
803
+ context: parentMatch?.context ?? this?.options.context ?? {},
804
+ params: match.params,
805
+ search: match.search,
806
+ }) || ({} as any)
807
+
808
+ const context = {
809
+ ...(parentMatch?.context ?? this?.options.context),
810
+ ...routeContext,
811
+ } as any
812
+
813
+ return {
814
+ context,
815
+ routeContext,
816
+ }
817
+ } catch (err) {
818
+ route.options.onError?.(err)
819
+ throw err
820
+ }
821
+ })()
708
822
 
709
- preloadRoute: async (navigateOpts = router.location, loaderOpts) => {
710
- const next = router.buildNext(navigateOpts)
711
- const matches = router.matchRoutes(next.pathname, {
712
- strictParseParams: true,
823
+ Object.assign(match, {
824
+ ...contextInfo,
713
825
  })
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
- },
826
+ })
729
827
 
730
- matchRoutes: (pathname, opts) => {
731
- router.cleanMatchCache()
828
+ return matches as any
829
+ }
732
830
 
733
- const matches: RouteMatch[] = []
831
+ loadMatches = async (
832
+ resolvedMatches: AnyRouteMatch[],
833
+ opts?: {
834
+ preload?: boolean
835
+ maxAge?: number
836
+ },
837
+ ) => {
838
+ this.cleanMatches()
839
+
840
+ if (!opts?.preload) {
841
+ resolvedMatches.forEach((match) => {
842
+ // Update each match with its latest route 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
+ }
734
856
 
735
- if (!router.routeTree) {
736
- return matches
737
- }
857
+ let firstBadMatchIndex: number | undefined
738
858
 
739
- const existingMatches = [
740
- ...router.state.matches,
741
- ...(router.state.pending?.matches ?? []),
742
- ]
859
+ // Check each match middleware to see if the route can be accessed
860
+ try {
861
+ for (const [index, match] of resolvedMatches.entries()) {
862
+ const route = this.getRoute(match.routeId)
743
863
 
744
- const recurse = async (routes: Route<any, any>[]): Promise<void> => {
745
- const parentMatch = last(matches)
746
- let params = parentMatch?.params ?? {}
864
+ const handleError = (
865
+ err: any,
866
+ handler: undefined | ((err: any) => void),
867
+ ) => {
868
+ firstBadMatchIndex = firstBadMatchIndex ?? index
869
+ handler = handler || route.options.onError
747
870
 
748
- const filteredRoutes = router.options.filterRoutes?.(routes) ?? routes
871
+ if (isRedirect(err)) {
872
+ throw err
873
+ }
749
874
 
750
- let foundRoutes: Route[] = []
875
+ try {
876
+ handler?.(err)
877
+ } catch (errorHandlerErr) {
878
+ err = errorHandlerErr
751
879
 
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
- )
880
+ if (isRedirect(errorHandlerErr)) {
881
+ throw errorHandlerErr
759
882
  }
883
+ }
760
884
 
761
- const fuzzy = !!(
762
- route.routePath !== '/' || route.childRoutes?.length
763
- )
764
-
765
- const matchParams = matchPathname(pathname, {
766
- to: route.fullPath,
767
- fuzzy,
768
- caseSensitive:
769
- route.options.caseSensitive ?? router.options.caseSensitive,
770
- })
771
-
772
- if (matchParams) {
773
- let parsedParams
885
+ this.setRouteMatch(match.id, (s) => ({
886
+ ...s,
887
+ error: err,
888
+ status: 'error',
889
+ updatedAt: Date.now(),
890
+ }))
891
+ }
774
892
 
775
- try {
776
- parsedParams =
777
- route.options.parseParams?.(matchParams!) ?? matchParams
778
- } catch (err) {
779
- if (opts?.strictParseParams) {
780
- throw err
781
- }
782
- }
893
+ if (match.paramsError) {
894
+ handleError(match.paramsError, route.options.onParseParamsError)
895
+ }
783
896
 
784
- params = {
785
- ...params,
786
- ...parsedParams,
787
- }
788
- }
897
+ if (match.searchError) {
898
+ handleError(match.searchError, route.options.onValidateSearchError)
899
+ }
789
900
 
790
- if (!!matchParams) {
791
- foundRoutes = [...parentRoutes, route]
792
- }
901
+ let didError = false
793
902
 
794
- return !!foundRoutes.length
903
+ try {
904
+ await route.options.beforeLoad?.({
905
+ ...match,
906
+ preload: !!opts?.preload,
795
907
  })
908
+ } catch (err) {
909
+ handleError(err, route.options.onBeforeLoadError)
910
+ didError = true
911
+ }
796
912
 
797
- return !!foundRoutes.length
913
+ // If we errored, do not run the next matches' middleware
914
+ if (didError) {
915
+ break
798
916
  }
917
+ }
918
+ } catch (err) {
919
+ if (!opts?.preload) {
920
+ this.navigate(err as any)
921
+ }
799
922
 
800
- findMatchInRoutes([], filteredRoutes)
923
+ throw err
924
+ }
801
925
 
802
- if (!foundRoutes.length) {
803
- return
804
- }
926
+ const validResolvedMatches = resolvedMatches.slice(0, firstBadMatchIndex)
927
+ const matchPromises: Promise<any>[] = []
928
+
929
+ validResolvedMatches.forEach((match, index) => {
930
+ matchPromises.push(
931
+ (async () => {
932
+ const parentMatchPromise = matchPromises[index - 1]
933
+ const route = this.getRoute(match.routeId)
934
+
935
+ if (
936
+ match.isFetching ||
937
+ (match.status === 'success' &&
938
+ !this.getIsInvalid({ matchId: match.id, preload: opts?.preload }))
939
+ ) {
940
+ return this.getRouteMatch(match.id)?.loadPromise
941
+ }
805
942
 
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
- parentMatch,
815
- matchId,
816
- params,
817
- pathname: joinPaths([pathname, interpolatedPath]),
818
- })
943
+ const fetchedAt = Date.now()
944
+ const checkLatest = () => {
945
+ const latest = this.getRouteMatch(match.id)
946
+ return latest && latest.fetchedAt !== fetchedAt
947
+ ? latest.loadPromise
948
+ : undefined
949
+ }
819
950
 
820
- matches.push(match)
821
- })
951
+ const loadPromise = (async () => {
952
+ let latestPromise
822
953
 
823
- const foundRoute = last(foundRoutes)!
954
+ const componentsPromise = Promise.all(
955
+ componentTypes.map(async (type) => {
956
+ const component = route.options[type]
824
957
 
825
- if (foundRoute.childRoutes?.length) {
826
- recurse(foundRoute.childRoutes)
827
- }
828
- }
958
+ if ((component as any)?.preload) {
959
+ await (component as any).preload()
960
+ }
961
+ }),
962
+ )
829
963
 
830
- recurse([router.routeTree])
964
+ const loaderPromise = route.options.loader?.({
965
+ ...match,
966
+ preload: !!opts?.preload,
967
+ parentMatchPromise,
968
+ })
831
969
 
832
- cascadeLoaderData(matches)
970
+ const handleError = (err: any) => {
971
+ if (isRedirect(err)) {
972
+ if (!opts?.preload) {
973
+ this.navigate(err as any)
974
+ }
975
+ return true
976
+ }
833
977
 
834
- return matches
835
- },
978
+ return false
979
+ }
836
980
 
837
- loadMatches: async (resolvedMatches, loaderOpts) => {
838
- const matchPromises = resolvedMatches.map(async (match) => {
839
- // Validate the match (loads search params etc)
840
- match.__.validate()
841
- match.load(loaderOpts)
981
+ try {
982
+ const [_, loader] = await Promise.all([
983
+ componentsPromise,
984
+ loaderPromise,
985
+ ])
986
+ if ((latestPromise = checkLatest())) return await latestPromise
842
987
 
843
- if (match.__.loadPromise) {
844
- // Wait for the first sign of activity from the match
845
- await match.__.loadPromise
846
- }
847
- })
988
+ this.setRouteMatchData(match.id, () => loader, opts)
989
+ } catch (err) {
990
+ if ((latestPromise = checkLatest())) return await latestPromise
848
991
 
849
- router.notify()
992
+ if (handleError(err)) {
993
+ return
994
+ }
850
995
 
851
- await Promise.all(matchPromises)
852
- },
996
+ const errorHandler =
997
+ route.options.onLoadError ?? route.options.onError
853
998
 
854
- invalidateRoute: (opts: MatchLocation) => {
855
- const next = router.buildNext(opts)
856
- const unloadedMatchIds = router
857
- .matchRoutes(next.pathname)
858
- .map((d) => d.matchId)
859
- ;[
860
- ...router.state.matches,
861
- ...(router.state.pending?.matches ?? []),
862
- ].forEach((match) => {
863
- if (unloadedMatchIds.includes(match.matchId)) {
864
- match.invalidate()
865
- }
866
- })
867
- },
999
+ let caughtError = err
868
1000
 
869
- reload: () =>
870
- router.__.navigate({
871
- fromCurrent: true,
872
- replace: true,
873
- search: true,
874
- }),
1001
+ try {
1002
+ errorHandler?.(err)
1003
+ } catch (errorHandlerErr) {
1004
+ caughtError = errorHandlerErr
875
1005
 
876
- resolvePath: (from: string, path: string) => {
877
- return resolvePath(router.basepath!, from, cleanPath(path))
878
- },
1006
+ if (handleError(errorHandlerErr)) {
1007
+ return
1008
+ }
1009
+ }
879
1010
 
880
- matchRoute: (location, opts) => {
881
- // const location = router.buildNext(opts)
1011
+ this.setRouteMatch(match.id, (s) => ({
1012
+ ...s,
1013
+ error: caughtError,
1014
+ status: 'error',
1015
+ isFetching: false,
1016
+ updatedAt: Date.now(),
1017
+ }))
1018
+ }
1019
+ })()
1020
+
1021
+ this.setRouteMatch(match.id, (s) => ({
1022
+ ...s,
1023
+ status: s.status !== 'success' ? 'pending' : s.status,
1024
+ isFetching: true,
1025
+ loadPromise,
1026
+ fetchedAt,
1027
+ invalid: false,
1028
+ }))
1029
+
1030
+ await loadPromise
1031
+ })(),
1032
+ )
1033
+ })
882
1034
 
883
- location = {
884
- ...location,
885
- to: location.to
886
- ? router.resolvePath(location.from ?? '', location.to)
887
- : undefined,
888
- }
1035
+ await Promise.all(matchPromises)
1036
+ }
889
1037
 
890
- const next = router.buildNext(location)
1038
+ reload = () => {
1039
+ return this.navigate({
1040
+ fromCurrent: true,
1041
+ replace: true,
1042
+ search: true,
1043
+ } as any)
1044
+ }
891
1045
 
892
- if (opts?.pending) {
893
- if (!router.state.pending?.location) {
894
- return false
895
- }
896
- return !!matchPathname(router.state.pending.location.pathname, {
897
- ...opts,
898
- to: next.pathname,
899
- })
900
- }
1046
+ resolvePath = (from: string, path: string) => {
1047
+ return resolvePath(this.basepath!, from, cleanPath(path))
1048
+ }
901
1049
 
902
- return !!matchPathname(router.state.location.pathname, {
903
- ...opts,
904
- to: next.pathname,
905
- })
906
- },
1050
+ navigate = async <TFrom extends string = '/', TTo extends string = ''>({
1051
+ from,
1052
+ to = '' as any,
1053
+ search,
1054
+ hash,
1055
+ replace,
1056
+ params,
1057
+ }: NavigateOptions<TRoutesInfo, TFrom, TTo>) => {
1058
+ // If this link simply reloads the current route,
1059
+ // make sure it has a new key so it will trigger a data refresh
1060
+
1061
+ // If this `to` is a valid external URL, return
1062
+ // null for LinkUtils
1063
+ const toString = String(to)
1064
+ const fromString = typeof from === 'undefined' ? from : String(from)
1065
+ let isExternal
1066
+
1067
+ try {
1068
+ new URL(`${toString}`)
1069
+ isExternal = true
1070
+ } catch (e) {}
1071
+
1072
+ invariant(
1073
+ !isExternal,
1074
+ 'Attempting to navigate to external url with this.navigate!',
1075
+ )
1076
+
1077
+ return this.#commitLocation({
1078
+ from: fromString,
1079
+ to: toString,
1080
+ search,
1081
+ hash,
1082
+ replace,
1083
+ params,
1084
+ })
1085
+ }
907
1086
 
908
- navigate: async ({ from, to = '.', search, hash, replace, params }) => {
909
- // If this link simply reloads the current route,
910
- // make sure it has a new key so it will trigger a data refresh
1087
+ matchRoute = <
1088
+ TFrom extends string = '/',
1089
+ TTo extends string = '',
1090
+ TResolved extends string = ResolveRelativePath<TFrom, NoInfer<TTo>>,
1091
+ >(
1092
+ location: ToOptions<TRoutesInfo, TFrom, TTo>,
1093
+ opts?: MatchRouteOptions,
1094
+ ): false | TRoutesInfo['routesById'][TResolved]['__types']['allParams'] => {
1095
+ location = {
1096
+ ...location,
1097
+ to: location.to
1098
+ ? this.resolvePath(location.from ?? '', location.to)
1099
+ : undefined,
1100
+ } as any
1101
+
1102
+ const next = this.buildNext(location)
1103
+ if (opts?.pending && this.state.status !== 'pending') {
1104
+ return false
1105
+ }
911
1106
 
912
- // If this `to` is a valid external URL, return
913
- // null for LinkUtils
914
- const toString = String(to)
915
- const fromString = String(from)
1107
+ const baseLocation = opts?.pending
1108
+ ? this.state.location
1109
+ : this.state.resolvedLocation
916
1110
 
917
- let isExternal
1111
+ if (!baseLocation) {
1112
+ return false
1113
+ }
918
1114
 
919
- try {
920
- new URL(`${toString}`)
921
- isExternal = true
922
- } catch (e) {}
1115
+ const match = matchPathname(this.basepath, baseLocation.pathname, {
1116
+ ...opts,
1117
+ to: next.pathname,
1118
+ }) as any
923
1119
 
924
- invariant(
925
- !isExternal,
926
- 'Attempting to navigate to external url with router.navigate!',
927
- )
1120
+ if (!match) {
1121
+ return false
1122
+ }
928
1123
 
929
- return router.__.navigate({
930
- from: fromString,
931
- to: toString,
932
- search,
933
- hash,
934
- replace,
935
- params,
936
- })
937
- },
1124
+ if (opts?.includeSearch ?? true) {
1125
+ return partialDeepEqual(baseLocation.search, next.search) ? match : false
1126
+ }
938
1127
 
939
- buildLink: ({
1128
+ return match
1129
+ }
1130
+
1131
+ buildLink = <TFrom extends string = '/', TTo extends string = ''>({
1132
+ from,
1133
+ to = '.' as any,
1134
+ search,
1135
+ params,
1136
+ hash,
1137
+ target,
1138
+ replace,
1139
+ activeOptions,
1140
+ preload,
1141
+ preloadDelay: userPreloadDelay,
1142
+ disabled,
1143
+ }: LinkOptions<TRoutesInfo, TFrom, TTo>): LinkInfo => {
1144
+ // If this link simply reloads the current route,
1145
+ // make sure it has a new key so it will trigger a data refresh
1146
+
1147
+ // If this `to` is a valid external URL, return
1148
+ // null for LinkUtils
1149
+
1150
+ try {
1151
+ new URL(`${to}`)
1152
+ return {
1153
+ type: 'external',
1154
+ href: to,
1155
+ }
1156
+ } catch (e) {}
1157
+
1158
+ const nextOpts = {
940
1159
  from,
941
- to = '.',
1160
+ to,
942
1161
  search,
943
1162
  params,
944
1163
  hash,
945
- target,
946
1164
  replace,
947
- activeOptions,
948
- preload,
949
- preloadMaxAge: userPreloadMaxAge,
950
- preloadGcMaxAge: userPreloadGcMaxAge,
951
- preloadDelay: userPreloadDelay,
952
- disabled,
953
- }) => {
954
- // If this link simply reloads the current route,
955
- // make sure it has a new key so it will trigger a data refresh
1165
+ }
956
1166
 
957
- // If this `to` is a valid external URL, return
958
- // null for LinkUtils
1167
+ const next = this.buildNext(nextOpts)
1168
+
1169
+ preload = preload ?? this.options.defaultPreload
1170
+ const preloadDelay =
1171
+ userPreloadDelay ?? this.options.defaultPreloadDelay ?? 0
1172
+
1173
+ // Compare path/hash for matches
1174
+ const currentPathSplit = this.state.location.pathname.split('/')
1175
+ const nextPathSplit = next.pathname.split('/')
1176
+ const pathIsFuzzyEqual = nextPathSplit.every(
1177
+ (d, i) => d === currentPathSplit[i],
1178
+ )
1179
+ // Combine the matches based on user options
1180
+ const pathTest = activeOptions?.exact
1181
+ ? this.state.location.pathname === next.pathname
1182
+ : pathIsFuzzyEqual
1183
+ const hashTest = activeOptions?.includeHash
1184
+ ? this.state.location.hash === next.hash
1185
+ : true
1186
+ const searchTest =
1187
+ activeOptions?.includeSearch ?? true
1188
+ ? partialDeepEqual(this.state.location.search, next.search)
1189
+ : true
1190
+
1191
+ // The final "active" test
1192
+ const isActive = pathTest && hashTest && searchTest
1193
+
1194
+ // The click handler
1195
+ const handleClick = (e: MouseEvent) => {
1196
+ if (
1197
+ !disabled &&
1198
+ !isCtrlEvent(e) &&
1199
+ !e.defaultPrevented &&
1200
+ (!target || target === '_self') &&
1201
+ e.button === 0
1202
+ ) {
1203
+ e.preventDefault()
959
1204
 
960
- try {
961
- new URL(`${to}`)
962
- return {
963
- type: 'external',
964
- href: to,
965
- }
966
- } catch (e) {}
967
-
968
- const nextOpts = {
969
- from,
970
- to,
971
- search,
972
- params,
973
- hash,
974
- replace,
1205
+ // All is well? Navigate!
1206
+ this.#commitLocation(nextOpts as any)
975
1207
  }
1208
+ }
976
1209
 
977
- const next = router.buildNext(nextOpts)
1210
+ // The click handler
1211
+ const handleFocus = (e: MouseEvent) => {
1212
+ if (preload) {
1213
+ this.preloadRoute(nextOpts).catch((err) => {
1214
+ console.warn(err)
1215
+ console.warn('Error preloading route! ☝️')
1216
+ })
1217
+ }
1218
+ }
978
1219
 
979
- preload = preload ?? router.options.defaultPreload
980
- const preloadDelay =
981
- userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0
1220
+ const handleTouchStart = (e: TouchEvent) => {
1221
+ this.preloadRoute(nextOpts).catch((err) => {
1222
+ console.warn(err)
1223
+ console.warn('Error preloading route! ☝️')
1224
+ })
1225
+ }
982
1226
 
983
- // Compare path/hash for matches
984
- const pathIsEqual = router.state.location.pathname === next.pathname
985
- const currentPathSplit = router.state.location.pathname.split('/')
986
- const nextPathSplit = next.pathname.split('/')
987
- const pathIsFuzzyEqual = nextPathSplit.every(
988
- (d, i) => d === currentPathSplit[i],
989
- )
990
- const hashIsEqual = router.state.location.hash === next.hash
991
- // Combine the matches based on user options
992
- const pathTest = activeOptions?.exact ? pathIsEqual : pathIsFuzzyEqual
993
- const hashTest = activeOptions?.includeHash ? hashIsEqual : true
994
-
995
- // The final "active" test
996
- const isActive = pathTest && hashTest
997
-
998
- // The click handler
999
- const handleClick = (e: MouseEvent) => {
1000
- if (
1001
- !disabled &&
1002
- !isCtrlEvent(e) &&
1003
- !e.defaultPrevented &&
1004
- (!target || target === '_self') &&
1005
- e.button === 0
1006
- ) {
1007
- e.preventDefault()
1008
- if (pathIsEqual && !search && !hash) {
1009
- router.invalidateRoute(nextOpts)
1010
- }
1227
+ const handleEnter = (e: MouseEvent) => {
1228
+ const target = (e.target || {}) as LinkCurrentTargetElement
1011
1229
 
1012
- // All is well? Navigate!)
1013
- router.__.navigate(nextOpts)
1230
+ if (preload) {
1231
+ if (target.preloadTimeout) {
1232
+ return
1014
1233
  }
1015
- }
1016
1234
 
1017
- // The click handler
1018
- const handleFocus = (e: MouseEvent) => {
1019
- if (preload) {
1020
- router.preloadRoute(nextOpts, {
1021
- maxAge: userPreloadMaxAge,
1022
- gcMaxAge: userPreloadGcMaxAge,
1235
+ target.preloadTimeout = setTimeout(() => {
1236
+ target.preloadTimeout = null
1237
+ this.preloadRoute(nextOpts).catch((err) => {
1238
+ console.warn(err)
1239
+ console.warn('Error preloading route! ☝️')
1023
1240
  })
1024
- }
1241
+ }, preloadDelay)
1025
1242
  }
1243
+ }
1026
1244
 
1027
- const handleEnter = (e: MouseEvent) => {
1028
- const target = (e.target || {}) as LinkCurrentTargetElement
1029
-
1030
- if (preload) {
1031
- if (target.preloadTimeout) {
1032
- return
1033
- }
1245
+ const handleLeave = (e: MouseEvent) => {
1246
+ const target = (e.target || {}) as LinkCurrentTargetElement
1034
1247
 
1035
- target.preloadTimeout = setTimeout(() => {
1036
- target.preloadTimeout = null
1037
- router.preloadRoute(nextOpts, {
1038
- maxAge: userPreloadMaxAge,
1039
- gcMaxAge: userPreloadGcMaxAge,
1040
- })
1041
- }, preloadDelay)
1042
- }
1248
+ if (target.preloadTimeout) {
1249
+ clearTimeout(target.preloadTimeout)
1250
+ target.preloadTimeout = null
1043
1251
  }
1252
+ }
1044
1253
 
1045
- const handleLeave = (e: MouseEvent) => {
1046
- const target = (e.target || {}) as LinkCurrentTargetElement
1254
+ return {
1255
+ type: 'internal',
1256
+ next,
1257
+ handleFocus,
1258
+ handleClick,
1259
+ handleEnter,
1260
+ handleLeave,
1261
+ handleTouchStart,
1262
+ isActive,
1263
+ disabled,
1264
+ }
1265
+ }
1047
1266
 
1048
- if (target.preloadTimeout) {
1049
- clearTimeout(target.preloadTimeout)
1050
- target.preloadTimeout = null
1051
- }
1052
- }
1267
+ dehydrate = (): DehydratedRouter => {
1268
+ return {
1269
+ state: pick(this.state, ['location', 'status', 'lastUpdated']),
1270
+ }
1271
+ }
1053
1272
 
1273
+ hydrate = async (__do_not_use_server_ctx?: HydrationCtx) => {
1274
+ let _ctx = __do_not_use_server_ctx
1275
+ // Client hydrates from window
1276
+ if (typeof document !== 'undefined') {
1277
+ _ctx = window.__TSR_DEHYDRATED__
1278
+ }
1279
+
1280
+ invariant(
1281
+ _ctx,
1282
+ 'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
1283
+ )
1284
+
1285
+ const ctx = _ctx
1286
+ this.dehydratedData = ctx.payload as any
1287
+ this.options.hydrate?.(ctx.payload as any)
1288
+
1289
+ this.__store.setState((s) => {
1054
1290
  return {
1055
- type: 'internal',
1056
- next,
1057
- handleFocus,
1058
- handleClick,
1059
- handleEnter,
1060
- handleLeave,
1061
- isActive,
1062
- disabled,
1291
+ ...s,
1292
+ ...ctx.router.state,
1293
+ resolvedLocation: ctx.router.state.location,
1063
1294
  }
1064
- },
1065
- buildNext: (opts: BuildNextOptions) => {
1066
- const next = router.__.buildLocation(opts)
1295
+ })
1067
1296
 
1068
- const matches = router.matchRoutes(next.pathname)
1297
+ await this.load()
1069
1298
 
1070
- const __preSearchFilters = matches
1071
- .map((match) => match.options.preSearchFilters ?? [])
1072
- .flat()
1073
- .filter(Boolean)
1299
+ return
1300
+ }
1074
1301
 
1075
- const __postSearchFilters = matches
1076
- .map((match) => match.options.postSearchFilters ?? [])
1077
- .flat()
1078
- .filter(Boolean)
1302
+ injectedHtml: (string | (() => Promise<string> | string))[] = []
1303
+
1304
+ injectHtml = async (html: string | (() => Promise<string> | string)) => {
1305
+ this.injectedHtml.push(html)
1306
+ }
1079
1307
 
1080
- return router.__.buildLocation({
1081
- ...opts,
1082
- __preSearchFilters,
1083
- __postSearchFilters,
1308
+ dehydrateData = <T>(key: any, getData: T | (() => Promise<T> | T)) => {
1309
+ if (typeof document === 'undefined') {
1310
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key)
1311
+
1312
+ this.injectHtml(async () => {
1313
+ const id = `__TSR_DEHYDRATED__${strKey}`
1314
+ const data =
1315
+ typeof getData === 'function' ? await (getData as any)() : getData
1316
+ return `<script id='${id}' suppressHydrationWarning>window["__TSR_DEHYDRATED__${escapeJSON(
1317
+ strKey,
1318
+ )}"] = ${JSON.stringify(data)}
1319
+ ;(() => {
1320
+ var el = document.getElementById('${id}')
1321
+ el.parentElement.removeChild(el)
1322
+ })()
1323
+ </script>`
1084
1324
  })
1085
- },
1086
1325
 
1087
- __: {
1088
- buildRouteTree: (rootRouteConfig: RouteConfig) => {
1089
- const recurseRoutes = (
1090
- routeConfigs: RouteConfig[],
1091
- parent?: Route<TAllRouteInfo, any>,
1092
- ): Route<TAllRouteInfo, any>[] => {
1093
- return routeConfigs.map((routeConfig) => {
1094
- const routeOptions = routeConfig.options
1095
- const route = createRoute(routeConfig, routeOptions, parent, router)
1096
- const existingRoute = (router.routesById as any)[route.routeId]
1097
-
1098
- if (existingRoute) {
1099
- if (process.env.NODE_ENV !== 'production') {
1100
- console.warn(
1101
- `Duplicate routes found with id: ${String(route.routeId)}`,
1102
- router.routesById,
1103
- route,
1104
- )
1105
- }
1106
- throw new Error()
1107
- }
1326
+ return () => this.hydrateData<T>(key)
1327
+ }
1108
1328
 
1109
- ;(router.routesById as any)[route.routeId] = route
1329
+ return () => undefined
1330
+ }
1110
1331
 
1111
- const children = routeConfig.children as RouteConfig[]
1332
+ hydrateData = <T = unknown>(key: any) => {
1333
+ if (typeof document !== 'undefined') {
1334
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key)
1112
1335
 
1113
- route.childRoutes = children?.length
1114
- ? recurseRoutes(children, route)
1115
- : undefined
1336
+ return window[`__TSR_DEHYDRATED__${strKey}` as any] as T
1337
+ }
1116
1338
 
1117
- return route
1118
- })
1339
+ return undefined
1340
+ }
1341
+
1342
+ // resolveMatchPromise = (matchId: string, key: string, value: any) => {
1343
+ // this.state.matches
1344
+ // .find((d) => d.id === matchId)
1345
+ // ?.__promisesByKey[key]?.resolve(value)
1346
+ // }
1347
+
1348
+ #buildRouteTree = (routeTree: TRouteTree) => {
1349
+ this.routeTree = routeTree as any
1350
+ this.routesById = {} as any
1351
+ this.routesByPath = {} as any
1352
+ this.flatRoutes = [] as any
1353
+
1354
+ const recurseRoutes = (routes: AnyRoute[]) => {
1355
+ routes.forEach((route, i) => {
1356
+ route.init({ originalIndex: i, router: this })
1357
+
1358
+ const existingRoute = (this.routesById as any)[route.id]
1359
+
1360
+ invariant(
1361
+ !existingRoute,
1362
+ `Duplicate routes found with id: ${String(route.id)}`,
1363
+ )
1364
+ ;(this.routesById as any)[route.id] = route
1365
+
1366
+ if (!route.isRoot && route.path) {
1367
+ const trimmedFullPath = trimPathRight(route.fullPath)
1368
+ if (
1369
+ !this.routesByPath[trimmedFullPath] ||
1370
+ route.fullPath.endsWith('/')
1371
+ ) {
1372
+ ;(this.routesByPath as any)[trimmedFullPath] = route
1373
+ }
1119
1374
  }
1120
1375
 
1121
- const routes = recurseRoutes([rootRouteConfig])
1376
+ const children = route.children as Route[]
1122
1377
 
1123
- return routes[0]!
1124
- },
1378
+ if (children?.length) {
1379
+ recurseRoutes(children)
1380
+ }
1381
+ })
1382
+ }
1125
1383
 
1126
- parseLocation: (
1127
- location: History['location'],
1128
- previousLocation?: Location,
1129
- ): Location => {
1130
- const parsedSearch = router.options.parseSearch(location.search)
1384
+ recurseRoutes([routeTree])
1131
1385
 
1132
- return {
1133
- pathname: location.pathname,
1134
- searchStr: location.search,
1135
- search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1136
- hash: location.hash.split('#').reverse()[0] ?? '',
1137
- href: `${location.pathname}${location.search}${location.hash}`,
1138
- state: location.state as LocationState,
1139
- key: location.key,
1386
+ this.flatRoutes = (Object.values(this.routesByPath) as AnyRoute[])
1387
+ .map((d, i) => {
1388
+ const trimmed = trimPath(d.fullPath)
1389
+ const parsed = parsePathname(trimmed)
1390
+
1391
+ while (parsed.length > 1 && parsed[0]?.value === '/') {
1392
+ parsed.shift()
1140
1393
  }
1141
- },
1142
1394
 
1143
- navigate: (location: BuildNextOptions & { replace?: boolean }) => {
1144
- const next = router.buildNext(location)
1145
- return router.__.commitLocation(next, location.replace)
1146
- },
1395
+ const score = parsed.map((d) => {
1396
+ if (d.type === 'param') {
1397
+ return 0.5
1398
+ }
1147
1399
 
1148
- buildLocation: (dest: BuildNextOptions = {}): Location => {
1149
- // const resolvedFrom: Location = {
1150
- // ...router.location,
1151
- const fromPathname = dest.fromCurrent
1152
- ? router.location.pathname
1153
- : dest.from ?? router.location.pathname
1154
-
1155
- let pathname = resolvePath(
1156
- router.basepath ?? '/',
1157
- fromPathname,
1158
- `${dest.to ?? '.'}`,
1159
- )
1400
+ if (d.type === 'wildcard') {
1401
+ return 0.25
1402
+ }
1160
1403
 
1161
- const fromMatches = router.matchRoutes(router.location.pathname, {
1162
- strictParseParams: true,
1404
+ return 1
1163
1405
  })
1164
1406
 
1165
- const toMatches = router.matchRoutes(pathname)
1407
+ return { child: d, trimmed, parsed, index: i, score }
1408
+ })
1409
+ .sort((a, b) => {
1410
+ let isIndex = a.trimmed === '/' ? 1 : b.trimmed === '/' ? -1 : 0
1166
1411
 
1167
- const prevParams = { ...last(fromMatches)?.params }
1412
+ if (isIndex !== 0) return isIndex
1168
1413
 
1169
- let nextParams =
1170
- (dest.params ?? true) === true
1171
- ? prevParams
1172
- : functionalUpdate(dest.params!, prevParams)
1414
+ const length = Math.min(a.score.length, b.score.length)
1173
1415
 
1174
- if (nextParams) {
1175
- toMatches
1176
- .map((d) => d.options.stringifyParams)
1177
- .filter(Boolean)
1178
- .forEach((fn) => {
1179
- Object.assign({}, nextParams!, fn!(nextParams!))
1180
- })
1416
+ // Sort by length of score
1417
+ if (a.score.length !== b.score.length) {
1418
+ return b.score.length - a.score.length
1181
1419
  }
1182
1420
 
1183
- pathname = interpolatePath(pathname, nextParams ?? {})
1421
+ // Sort by min available score
1422
+ for (let i = 0; i < length; i++) {
1423
+ if (a.score[i] !== b.score[i]) {
1424
+ return b.score[i]! - a.score[i]!
1425
+ }
1426
+ }
1184
1427
 
1185
- // Pre filters first
1186
- const preFilteredSearch = dest.__preSearchFilters?.length
1187
- ? dest.__preSearchFilters.reduce(
1188
- (prev, next) => next(prev),
1189
- router.location.search,
1190
- )
1191
- : router.location.search
1192
-
1193
- // Then the link/navigate function
1194
- const destSearch =
1195
- dest.search === true
1196
- ? preFilteredSearch // Preserve resolvedFrom true
1197
- : dest.search
1198
- ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1199
- : dest.__preSearchFilters?.length
1200
- ? preFilteredSearch // Preserve resolvedFrom filters
1201
- : {}
1202
-
1203
- // Then post filters
1204
- const postFilteredSearch = dest.__postSearchFilters?.length
1205
- ? dest.__postSearchFilters.reduce(
1206
- (prev, next) => next(prev),
1207
- destSearch,
1208
- )
1209
- : destSearch
1428
+ // Sort by min available parsed value
1429
+ for (let i = 0; i < length; i++) {
1430
+ if (a.parsed[i]!.value !== b.parsed[i]!.value) {
1431
+ return a.parsed[i]!.value! > b.parsed[i]!.value! ? 1 : -1
1432
+ }
1433
+ }
1210
1434
 
1211
- const search = replaceEqualDeep(
1212
- router.location.search,
1213
- postFilteredSearch,
1214
- )
1435
+ // Sort by length of trimmed full path
1436
+ if (a.trimmed !== b.trimmed) {
1437
+ return a.trimmed > b.trimmed ? 1 : -1
1438
+ }
1215
1439
 
1216
- const searchStr = router.options.stringifySearch(search)
1217
- let hash =
1218
- dest.hash === true
1219
- ? router.location.hash
1220
- : functionalUpdate(dest.hash!, router.location.hash)
1221
- hash = hash ? `#${hash}` : ''
1440
+ // Sort by original index
1441
+ return a.index - b.index
1442
+ })
1443
+ .map((d, i) => {
1444
+ d.child.rank = i
1445
+ return d.child
1446
+ }) as any
1447
+ }
1222
1448
 
1223
- return {
1224
- pathname,
1225
- search,
1226
- searchStr,
1227
- state: router.location.state,
1228
- hash,
1229
- href: `${pathname}${searchStr}${hash}`,
1230
- key: dest.key,
1231
- }
1232
- },
1449
+ #parseLocation = (previousLocation?: ParsedLocation): ParsedLocation => {
1450
+ let { pathname, search, hash, state } = this.history.location
1233
1451
 
1234
- commitLocation: (next: Location, replace?: boolean): Promise<void> => {
1235
- const id = '' + Date.now() + Math.random()
1452
+ const parsedSearch = this.options.parseSearch(search)
1236
1453
 
1237
- if (router.navigateTimeout) clearTimeout(router.navigateTimeout)
1454
+ return {
1455
+ pathname: pathname,
1456
+ searchStr: search,
1457
+ search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1458
+ hash: hash.split('#').reverse()[0] ?? '',
1459
+ href: `${pathname}${search}${hash}`,
1460
+ state: state as LocationState,
1461
+ key: state?.key || '__init__',
1462
+ }
1463
+ }
1238
1464
 
1239
- let nextAction: 'push' | 'replace' = 'replace'
1465
+ #buildLocation = (dest: BuildNextOptions = {}): ParsedLocation => {
1466
+ dest.fromCurrent = dest.fromCurrent ?? dest.to === ''
1240
1467
 
1241
- if (!replace) {
1242
- nextAction = 'push'
1243
- }
1468
+ const fromPathname = dest.fromCurrent
1469
+ ? this.state.location.pathname
1470
+ : dest.from ?? this.state.location.pathname
1244
1471
 
1245
- const isSameUrl =
1246
- router.__.parseLocation(history.location).href === next.href
1472
+ let pathname = resolvePath(
1473
+ this.basepath ?? '/',
1474
+ fromPathname,
1475
+ `${dest.to ?? ''}`,
1476
+ )
1247
1477
 
1248
- if (isSameUrl && !next.key) {
1249
- nextAction = 'replace'
1250
- }
1478
+ const fromMatches = this.matchRoutes(
1479
+ this.state.location.pathname,
1480
+ this.state.location.search,
1481
+ )
1251
1482
 
1252
- if (nextAction === 'replace') {
1253
- history.replace(
1254
- {
1255
- pathname: next.pathname,
1256
- hash: next.hash,
1257
- search: next.searchStr,
1258
- },
1259
- {
1260
- id,
1261
- },
1262
- )
1263
- } else {
1264
- history.push(
1265
- {
1266
- pathname: next.pathname,
1267
- hash: next.hash,
1268
- search: next.searchStr,
1269
- },
1270
- {
1271
- id,
1272
- },
1273
- )
1274
- }
1483
+ const prevParams = { ...last(fromMatches)?.params }
1275
1484
 
1276
- router.navigationPromise = new Promise((resolve) => {
1277
- const previousNavigationResolve = router.resolveNavigation
1485
+ let nextParams =
1486
+ (dest.params ?? true) === true
1487
+ ? prevParams
1488
+ : functionalUpdate(dest.params!, prevParams)
1278
1489
 
1279
- router.resolveNavigation = () => {
1280
- previousNavigationResolve()
1281
- resolve()
1282
- }
1490
+ if (nextParams) {
1491
+ dest.__matches
1492
+ ?.map((d) => this.getRoute(d.routeId).options.stringifyParams)
1493
+ .filter(Boolean)
1494
+ .forEach((fn) => {
1495
+ nextParams = { ...nextParams!, ...fn!(nextParams!) }
1283
1496
  })
1497
+ }
1498
+
1499
+ pathname = interpolatePath(pathname, nextParams ?? {})
1500
+
1501
+ const preSearchFilters =
1502
+ dest.__matches
1503
+ ?.map(
1504
+ (match) =>
1505
+ this.getRoute(match.routeId).options.preSearchFilters ?? [],
1506
+ )
1507
+ .flat()
1508
+ .filter(Boolean) ?? []
1284
1509
 
1285
- return router.navigationPromise
1510
+ const postSearchFilters =
1511
+ dest.__matches
1512
+ ?.map(
1513
+ (match) =>
1514
+ this.getRoute(match.routeId).options.postSearchFilters ?? [],
1515
+ )
1516
+ .flat()
1517
+ .filter(Boolean) ?? []
1518
+
1519
+ // Pre filters first
1520
+ const preFilteredSearch = preSearchFilters?.length
1521
+ ? preSearchFilters?.reduce(
1522
+ (prev, next) => next(prev),
1523
+ this.state.location.search,
1524
+ )
1525
+ : this.state.location.search
1526
+
1527
+ // Then the link/navigate function
1528
+ const destSearch =
1529
+ dest.search === true
1530
+ ? preFilteredSearch // Preserve resolvedFrom true
1531
+ : dest.search
1532
+ ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1533
+ : preSearchFilters?.length
1534
+ ? preFilteredSearch // Preserve resolvedFrom filters
1535
+ : {}
1536
+
1537
+ // Then post filters
1538
+ const postFilteredSearch = postSearchFilters?.length
1539
+ ? postSearchFilters.reduce((prev, next) => next(prev), destSearch)
1540
+ : destSearch
1541
+
1542
+ const search = replaceEqualDeep(
1543
+ this.state.location.search,
1544
+ postFilteredSearch,
1545
+ )
1546
+
1547
+ const searchStr = this.options.stringifySearch(search)
1548
+
1549
+ const hash =
1550
+ dest.hash === true
1551
+ ? this.state.location.hash
1552
+ : functionalUpdate(dest.hash!, this.state.location.hash)
1553
+
1554
+ const hashStr = hash ? `#${hash}` : ''
1555
+
1556
+ const nextState =
1557
+ dest.state === true
1558
+ ? this.state.location.state
1559
+ : functionalUpdate(dest.state, this.state.location.state)!
1560
+
1561
+ return {
1562
+ pathname,
1563
+ search,
1564
+ searchStr,
1565
+ state: nextState,
1566
+ hash,
1567
+ href: this.history.createHref(`${pathname}${searchStr}${hashStr}`),
1568
+ key: dest.key,
1569
+ }
1570
+ }
1571
+
1572
+ #commitLocation = async (
1573
+ location: BuildNextOptions & { replace?: boolean },
1574
+ ) => {
1575
+ const next = this.buildNext(location)
1576
+ const id = '' + Date.now() + Math.random()
1577
+
1578
+ if (this.navigateTimeout) clearTimeout(this.navigateTimeout)
1579
+
1580
+ let nextAction: 'push' | 'replace' = 'replace'
1581
+
1582
+ if (!location.replace) {
1583
+ nextAction = 'push'
1584
+ }
1585
+
1586
+ const isSameUrl = this.state.location.href === next.href
1587
+
1588
+ if (isSameUrl && !next.key) {
1589
+ nextAction = 'replace'
1590
+ }
1591
+
1592
+ const href = `${next.pathname}${next.searchStr}${
1593
+ next.hash ? `#${next.hash}` : ''
1594
+ }`
1595
+
1596
+ this.history[nextAction === 'push' ? 'push' : 'replace'](href, {
1597
+ id,
1598
+ ...next.state,
1599
+ })
1600
+
1601
+ return this.latestLoadPromise
1602
+ }
1603
+
1604
+ getRouteMatch = (
1605
+ id: string,
1606
+ ): undefined | RouteMatch<TRoutesInfo, AnyRoute> => {
1607
+ return this.state.matchesById[id]
1608
+ }
1609
+
1610
+ setRouteMatch = (
1611
+ id: string,
1612
+ updater: (
1613
+ prev: RouteMatch<TRoutesInfo, AnyRoute>,
1614
+ ) => RouteMatch<TRoutesInfo, AnyRoute>,
1615
+ ) => {
1616
+ this.__store.setState((prev) => ({
1617
+ ...prev,
1618
+ matchesById: {
1619
+ ...prev.matchesById,
1620
+ [id]: updater(prev.matchesById[id] as any),
1286
1621
  },
1622
+ }))
1623
+ }
1624
+
1625
+ setRouteMatchData = (
1626
+ id: string,
1627
+ updater: (prev: any) => any,
1628
+ opts?: {
1629
+ updatedAt?: number
1630
+ maxAge?: number
1287
1631
  },
1632
+ ) => {
1633
+ const match = this.getRouteMatch(id)
1634
+
1635
+ if (!match) return
1636
+
1637
+ const route = this.getRoute(match.routeId)
1638
+ const updatedAt = opts?.updatedAt ?? Date.now()
1639
+
1640
+ const preloadInvalidAt =
1641
+ updatedAt +
1642
+ (opts?.maxAge ??
1643
+ route.options.preloadMaxAge ??
1644
+ this.options.defaultPreloadMaxAge ??
1645
+ 5000)
1646
+
1647
+ const invalidAt =
1648
+ updatedAt +
1649
+ (opts?.maxAge ??
1650
+ route.options.maxAge ??
1651
+ this.options.defaultMaxAge ??
1652
+ Infinity)
1653
+
1654
+ this.setRouteMatch(id, (s) => ({
1655
+ ...s,
1656
+ error: undefined,
1657
+ status: 'success',
1658
+ isFetching: false,
1659
+ updatedAt: Date.now(),
1660
+ loaderData: functionalUpdate(updater, s.loaderData),
1661
+ preloadInvalidAt,
1662
+ invalidAt,
1663
+ }))
1664
+
1665
+ if (this.state.matches.find((d) => d.id === id)) {
1666
+ }
1667
+ }
1668
+
1669
+ invalidate = async (opts?: {
1670
+ matchId?: string
1671
+ reload?: boolean
1672
+ }): Promise<void> => {
1673
+ if (opts?.matchId) {
1674
+ this.setRouteMatch(opts.matchId, (s) => ({
1675
+ ...s,
1676
+ invalid: true,
1677
+ }))
1678
+ const matchIndex = this.state.matches.findIndex(
1679
+ (d) => d.id === opts.matchId,
1680
+ )
1681
+ const childMatch = this.state.matches[matchIndex + 1]
1682
+
1683
+ if (childMatch) {
1684
+ return this.invalidate({ matchId: childMatch.id, reload: false })
1685
+ }
1686
+ } else {
1687
+ this.__store.batch(() => {
1688
+ Object.values(this.state.matchesById).forEach((match) => {
1689
+ this.setRouteMatch(match.id, (s) => ({
1690
+ ...s,
1691
+ invalid: true,
1692
+ }))
1693
+ })
1694
+ })
1695
+ }
1696
+
1697
+ if (opts?.reload ?? true) {
1698
+ return this.reload()
1699
+ }
1288
1700
  }
1289
1701
 
1290
- router.update(userOptions)
1702
+ getIsInvalid = (opts?: { matchId: string; preload?: boolean }): boolean => {
1703
+ if (!opts?.matchId) {
1704
+ return !!this.state.matches.find((d) =>
1705
+ this.getIsInvalid({ matchId: d.id, preload: opts?.preload }),
1706
+ )
1707
+ }
1708
+
1709
+ const match = this.getRouteMatch(opts?.matchId)
1710
+
1711
+ if (!match) {
1712
+ return false
1713
+ }
1291
1714
 
1292
- // Allow frameworks to hook into the router creation
1293
- router.options.createRouter?.(router)
1715
+ const now = Date.now()
1294
1716
 
1295
- return router
1717
+ return (
1718
+ match.invalid ||
1719
+ (opts?.preload ? match.preloadInvalidAt : match.invalidAt) < now
1720
+ )
1721
+ }
1722
+ }
1723
+
1724
+ // Detect if we're in the DOM
1725
+ const isServer = typeof window === 'undefined' || !window.document.createElement
1726
+
1727
+ function getInitialRouterState(): RouterState<any, any> {
1728
+ return {
1729
+ status: 'idle',
1730
+ isFetching: false,
1731
+ resolvedLocation: null!,
1732
+ location: null!,
1733
+ matchesById: {},
1734
+ matchIds: [],
1735
+ pendingMatchIds: [],
1736
+ matches: [],
1737
+ pendingMatches: [],
1738
+ lastUpdated: Date.now(),
1739
+ }
1296
1740
  }
1297
1741
 
1298
1742
  function isCtrlEvent(e: MouseEvent) {
1299
1743
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
1300
1744
  }
1301
1745
 
1302
- function cascadeLoaderData(matches: RouteMatch<any, any>[]) {
1303
- matches.forEach((match, index) => {
1304
- const parent = matches[index - 1]
1746
+ export type AnyRedirect = Redirect<any, any, any>
1305
1747
 
1306
- if (parent) {
1307
- match.loaderData = replaceEqualDeep(match.loaderData, {
1308
- ...parent.loaderData,
1309
- ...match.routeLoaderData,
1310
- })
1311
- }
1312
- })
1748
+ export type Redirect<
1749
+ TRoutesInfo extends AnyRoutesInfo = RegisteredRoutesInfo,
1750
+ TFrom extends TRoutesInfo['routePaths'] = '/',
1751
+ TTo extends string = '',
1752
+ > = NavigateOptions<TRoutesInfo, TFrom, TTo> & {
1753
+ code?: number
1754
+ }
1755
+
1756
+ export function redirect<
1757
+ TRoutesInfo extends AnyRoutesInfo = RegisteredRoutesInfo,
1758
+ TFrom extends TRoutesInfo['routePaths'] = '/',
1759
+ TTo extends string = '',
1760
+ >(opts: Redirect<TRoutesInfo, TFrom, TTo>): Redirect<TRoutesInfo, TFrom, TTo> {
1761
+ ;(opts as any).isRedirect = true
1762
+ return opts
1763
+ }
1764
+
1765
+ export function isRedirect(obj: any): obj is AnyRedirect {
1766
+ return !!obj?.isRedirect
1767
+ }
1768
+
1769
+ export class SearchParamError extends Error {}
1770
+ export class PathParamError extends Error {}
1771
+
1772
+ function escapeJSON(jsonString: string) {
1773
+ return jsonString
1774
+ .replace(/\\/g, '\\\\') // Escape backslashes
1775
+ .replace(/'/g, "\\'") // Escape single quotes
1776
+ .replace(/"/g, '\\"') // Escape double quotes
1777
+ }
1778
+
1779
+ // A function that takes an import() argument which is a function and returns a new function that will
1780
+ // proxy arguments from the caller to the imported function, retaining all type
1781
+ // information along the way
1782
+ export function lazyFn<
1783
+ T extends Record<string, (...args: any[]) => any>,
1784
+ TKey extends keyof T = 'default',
1785
+ >(fn: () => Promise<T>, key?: TKey) {
1786
+ return async (...args: Parameters<T[TKey]>): Promise<ReturnType<T[TKey]>> => {
1787
+ const imported = await fn()
1788
+ return imported[key || 'default'](...args)
1789
+ }
1313
1790
  }