@tanstack/router-core 0.0.1-alpha.5 → 0.0.1-alpha.7

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