@tanstack/router-core 0.0.1-beta.5 → 0.0.1-beta.51

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