@tanstack/router-core 0.0.1-alpha.1 → 0.0.1-alpha.11

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