@tanstack/router-core 0.0.1-beta.16 → 0.0.1-beta.161

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