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

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