@tanstack/router-core 0.0.1-beta.4 → 0.0.1-beta.41

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