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

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