@tanstack/router-core 0.0.1-alpha.0 → 0.0.1-alpha.10

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