@tanstack/react-router 0.0.1-beta.23 → 0.0.1-beta.230

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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/build/cjs/CatchBoundary.js +123 -0
  3. package/build/cjs/CatchBoundary.js.map +1 -0
  4. package/build/cjs/Matches.js +232 -0
  5. package/build/cjs/Matches.js.map +1 -0
  6. package/build/cjs/RouterProvider.js +158 -0
  7. package/build/cjs/RouterProvider.js.map +1 -0
  8. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +2 -22
  9. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -1
  10. package/build/cjs/awaited.js +43 -0
  11. package/build/cjs/awaited.js.map +1 -0
  12. package/build/cjs/defer.js +37 -0
  13. package/build/cjs/defer.js.map +1 -0
  14. package/build/cjs/fileRoute.js +27 -0
  15. package/build/cjs/fileRoute.js.map +1 -0
  16. package/build/cjs/index.js +122 -0
  17. package/build/cjs/index.js.map +1 -0
  18. package/build/cjs/lazyRouteComponent.js +54 -0
  19. package/build/cjs/lazyRouteComponent.js.map +1 -0
  20. package/build/cjs/link.js +148 -0
  21. package/build/cjs/link.js.map +1 -0
  22. package/build/cjs/path.js +209 -0
  23. package/build/cjs/path.js.map +1 -0
  24. package/build/cjs/qss.js +63 -0
  25. package/build/cjs/qss.js.map +1 -0
  26. package/build/cjs/redirects.js +25 -0
  27. package/build/cjs/redirects.js.map +1 -0
  28. package/build/cjs/route.js +134 -0
  29. package/build/cjs/route.js.map +1 -0
  30. package/build/cjs/router.js +1099 -0
  31. package/build/cjs/router.js.map +1 -0
  32. package/build/cjs/scroll-restoration.js +179 -0
  33. package/build/cjs/scroll-restoration.js.map +1 -0
  34. package/build/cjs/searchParams.js +81 -0
  35. package/build/cjs/searchParams.js.map +1 -0
  36. package/build/cjs/useBlocker.js +61 -0
  37. package/build/cjs/useBlocker.js.map +1 -0
  38. package/build/cjs/useNavigate.js +75 -0
  39. package/build/cjs/useNavigate.js.map +1 -0
  40. package/build/cjs/useParams.js +26 -0
  41. package/build/cjs/useParams.js.map +1 -0
  42. package/build/cjs/useSearch.js +25 -0
  43. package/build/cjs/useSearch.js.map +1 -0
  44. package/build/cjs/utils.js +239 -0
  45. package/build/cjs/utils.js.map +1 -0
  46. package/build/esm/index.js +2149 -2560
  47. package/build/esm/index.js.map +1 -1
  48. package/build/stats-html.html +3498 -2694
  49. package/build/stats-react.json +927 -42
  50. package/build/types/CatchBoundary.d.ts +33 -0
  51. package/build/types/Matches.d.ts +57 -0
  52. package/build/types/RouterProvider.d.ts +41 -0
  53. package/build/types/awaited.d.ts +9 -0
  54. package/build/types/defer.d.ts +19 -0
  55. package/build/types/fileRoute.d.ts +35 -0
  56. package/build/types/history.d.ts +7 -0
  57. package/build/types/index.d.ts +27 -108
  58. package/build/types/injectHtml.d.ts +0 -0
  59. package/build/types/lazyRouteComponent.d.ts +2 -0
  60. package/build/types/link.d.ts +105 -0
  61. package/build/types/location.d.ts +14 -0
  62. package/build/types/path.d.ts +16 -0
  63. package/build/types/qss.d.ts +2 -0
  64. package/build/types/redirects.d.ts +10 -0
  65. package/build/types/route.d.ts +278 -0
  66. package/build/types/routeInfo.d.ts +22 -0
  67. package/build/types/router.d.ts +167 -0
  68. package/build/types/scroll-restoration.d.ts +6 -0
  69. package/build/types/searchParams.d.ts +7 -0
  70. package/build/types/useBlocker.d.ts +8 -0
  71. package/build/types/useNavigate.d.ts +20 -0
  72. package/build/types/useParams.d.ts +7 -0
  73. package/build/types/useSearch.d.ts +7 -0
  74. package/build/types/utils.d.ts +66 -0
  75. package/build/umd/index.development.js +2443 -2515
  76. package/build/umd/index.development.js.map +1 -1
  77. package/build/umd/index.production.js +4 -4
  78. package/build/umd/index.production.js.map +1 -1
  79. package/package.json +9 -10
  80. package/src/CatchBoundary.tsx +98 -0
  81. package/src/Matches.tsx +387 -0
  82. package/src/RouterProvider.tsx +224 -0
  83. package/src/awaited.tsx +40 -0
  84. package/src/defer.ts +55 -0
  85. package/src/fileRoute.ts +154 -0
  86. package/src/history.ts +8 -0
  87. package/src/index.tsx +28 -709
  88. package/src/injectHtml.ts +28 -0
  89. package/src/lazyRouteComponent.tsx +33 -0
  90. package/src/link.tsx +508 -0
  91. package/src/location.ts +15 -0
  92. package/src/path.ts +256 -0
  93. package/src/qss.ts +53 -0
  94. package/src/redirects.ts +31 -0
  95. package/src/route.ts +861 -0
  96. package/src/routeInfo.ts +68 -0
  97. package/src/router.ts +1660 -0
  98. package/src/scroll-restoration.tsx +192 -0
  99. package/src/searchParams.ts +79 -0
  100. package/src/useBlocker.tsx +34 -0
  101. package/src/useNavigate.tsx +109 -0
  102. package/src/useParams.tsx +25 -0
  103. package/src/useSearch.tsx +25 -0
  104. package/src/utils.ts +350 -0
  105. package/build/cjs/react-router/src/index.js +0 -473
  106. package/build/cjs/react-router/src/index.js.map +0 -1
  107. package/build/cjs/router-core/build/esm/index.js +0 -2527
  108. package/build/cjs/router-core/build/esm/index.js.map +0 -1
package/src/router.ts ADDED
@@ -0,0 +1,1660 @@
1
+ import {
2
+ HistoryLocation,
3
+ HistoryState,
4
+ RouterHistory,
5
+ createBrowserHistory,
6
+ } from '@tanstack/history'
7
+
8
+ //
9
+
10
+ import {
11
+ AnySearchSchema,
12
+ AnyRoute,
13
+ AnyContext,
14
+ AnyPathParams,
15
+ RouteMask,
16
+ Route,
17
+ LoaderFnContext,
18
+ } from './route'
19
+ import { FullSearchSchema, RoutesById, RoutesByPath } from './routeInfo'
20
+ import { defaultParseSearch, defaultStringifySearch } from './searchParams'
21
+ import {
22
+ PickAsRequired,
23
+ Updater,
24
+ NonNullableUpdater,
25
+ replaceEqualDeep,
26
+ deepEqual,
27
+ escapeJSON,
28
+ functionalUpdate,
29
+ last,
30
+ pick,
31
+ } from './utils'
32
+ import {
33
+ ErrorRouteComponent,
34
+ PendingRouteComponent,
35
+ RouteComponent,
36
+ } from './route'
37
+ import { AnyRouteMatch, RouteMatch } from './Matches'
38
+ import { ParsedLocation } from './location'
39
+ import { LocationState } from './location'
40
+ import { SearchSerializer, SearchParser } from './searchParams'
41
+ import {
42
+ BuildLinkFn,
43
+ BuildLocationFn,
44
+ CommitLocationOptions,
45
+ InjectedHtmlEntry,
46
+ MatchRouteFn,
47
+ NavigateFn,
48
+ PathParamError,
49
+ SearchParamError,
50
+ getInitialRouterState,
51
+ getRouteMatch,
52
+ } from './RouterProvider'
53
+ import {
54
+ cleanPath,
55
+ interpolatePath,
56
+ joinPaths,
57
+ matchPathname,
58
+ parsePathname,
59
+ resolvePath,
60
+ trimPath,
61
+ trimPathRight,
62
+ } from './path'
63
+ import invariant from 'tiny-invariant'
64
+ import { isRedirect } from './redirects'
65
+ import warning from 'tiny-warning'
66
+
67
+ //
68
+
69
+ declare global {
70
+ interface Window {
71
+ __TSR_DEHYDRATED__?: HydrationCtx
72
+ __TSR_ROUTER_CONTEXT__?: React.Context<Router<any>>
73
+ }
74
+ }
75
+
76
+ export interface Register {
77
+ // router: Router
78
+ }
79
+
80
+ export type AnyRouter = Router<AnyRoute, any>
81
+
82
+ export type RegisteredRouter = Register extends {
83
+ router: infer TRouter extends AnyRouter
84
+ }
85
+ ? TRouter
86
+ : AnyRouter
87
+
88
+ export type HydrationCtx = {
89
+ router: DehydratedRouter
90
+ payload: Record<string, any>
91
+ }
92
+
93
+ export type RouterContextOptions<TRouteTree extends AnyRoute> =
94
+ AnyContext extends TRouteTree['types']['routerContext']
95
+ ? {
96
+ context?: TRouteTree['types']['routerContext']
97
+ }
98
+ : {
99
+ context: TRouteTree['types']['routerContext']
100
+ }
101
+
102
+ export interface RouterOptions<
103
+ TRouteTree extends AnyRoute,
104
+ TDehydrated extends Record<string, any> = Record<string, any>,
105
+ > {
106
+ history?: RouterHistory
107
+ stringifySearch?: SearchSerializer
108
+ parseSearch?: SearchParser
109
+ defaultPreload?: false | 'intent'
110
+ defaultPreloadDelay?: number
111
+ defaultComponent?: RouteComponent<AnySearchSchema, AnyPathParams, AnyContext>
112
+ defaultErrorComponent?: ErrorRouteComponent<
113
+ AnySearchSchema,
114
+ AnyPathParams,
115
+ AnyContext
116
+ >
117
+ defaultPendingComponent?: PendingRouteComponent<
118
+ AnySearchSchema,
119
+ AnyPathParams,
120
+ AnyContext
121
+ >
122
+ defaultPendingMs?: number
123
+ defaultPendingMinMs?: number
124
+ caseSensitive?: boolean
125
+ routeTree?: TRouteTree
126
+ basepath?: string
127
+ context?: TRouteTree['types']['routerContext']
128
+ // dehydrate?: () => TDehydrated
129
+ // hydrate?: (dehydrated: TDehydrated) => void
130
+ routeMasks?: RouteMask<TRouteTree>[]
131
+ unmaskOnReload?: boolean
132
+ }
133
+
134
+ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
135
+ status: 'pending' | 'idle'
136
+ matches: RouteMatch<TRouteTree>[]
137
+ pendingMatches: RouteMatch<TRouteTree>[]
138
+ location: ParsedLocation<FullSearchSchema<TRouteTree>>
139
+ resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
140
+ lastUpdated: number
141
+ }
142
+
143
+ export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void
144
+
145
+ export interface BuildNextOptions {
146
+ to?: string | number | null
147
+ params?: true | Updater<unknown>
148
+ search?: true | Updater<unknown>
149
+ hash?: true | Updater<string>
150
+ state?: true | NonNullableUpdater<LocationState>
151
+ mask?: {
152
+ to?: string | number | null
153
+ params?: true | Updater<unknown>
154
+ search?: true | Updater<unknown>
155
+ hash?: true | Updater<string>
156
+ state?: true | NonNullableUpdater<LocationState>
157
+ unmaskOnReload?: boolean
158
+ }
159
+ from?: string
160
+ }
161
+
162
+ export interface DehydratedRouterState {
163
+ dehydratedMatches: DehydratedRouteMatch[]
164
+ }
165
+
166
+ export type DehydratedRouteMatch = Pick<
167
+ RouteMatch,
168
+ 'fetchedAt' | 'invalid' | 'id' | 'status' | 'updatedAt'
169
+ >
170
+
171
+ export interface DehydratedRouter {
172
+ state: DehydratedRouterState
173
+ }
174
+
175
+ export type RouterConstructorOptions<
176
+ TRouteTree extends AnyRoute,
177
+ TDehydrated extends Record<string, any>,
178
+ > = Omit<RouterOptions<TRouteTree, TDehydrated>, 'context'> &
179
+ RouterContextOptions<TRouteTree>
180
+
181
+ export const componentTypes = [
182
+ 'component',
183
+ 'errorComponent',
184
+ 'pendingComponent',
185
+ ] as const
186
+
187
+ export type RouterEvents = {
188
+ onBeforeLoad: {
189
+ type: 'onBeforeLoad'
190
+ fromLocation: ParsedLocation
191
+ toLocation: ParsedLocation
192
+ pathChanged: boolean
193
+ }
194
+ onLoad: {
195
+ type: 'onLoad'
196
+ fromLocation: ParsedLocation
197
+ toLocation: ParsedLocation
198
+ pathChanged: boolean
199
+ }
200
+ onResolved: {
201
+ type: 'onResolved'
202
+ fromLocation: ParsedLocation
203
+ toLocation: ParsedLocation
204
+ pathChanged: boolean
205
+ }
206
+ }
207
+
208
+ export type RouterEvent = RouterEvents[keyof RouterEvents]
209
+
210
+ export type RouterListener<TRouterEvent extends RouterEvent> = {
211
+ eventType: TRouterEvent['type']
212
+ fn: ListenerFn<TRouterEvent>
213
+ }
214
+
215
+ type LinkCurrentTargetElement = {
216
+ preloadTimeout?: null | ReturnType<typeof setTimeout>
217
+ }
218
+
219
+ const preloadWarning = 'Error preloading route! ☝️'
220
+
221
+ export class Router<
222
+ TRouteTree extends AnyRoute = AnyRoute,
223
+ TDehydrated extends Record<string, any> = Record<string, any>,
224
+ > {
225
+ // Option-independent properties
226
+ tempLocationKey: string | undefined = `${Math.round(
227
+ Math.random() * 10000000,
228
+ )}`
229
+ resetNextScroll: boolean = true
230
+ navigateTimeout: NodeJS.Timeout | null = null
231
+ latestLoadPromise: Promise<void> = Promise.resolve()
232
+ subscribers = new Set<RouterListener<RouterEvent>>()
233
+ pendingMatches: AnyRouteMatch[] = []
234
+ injectedHtml: InjectedHtmlEntry[] = []
235
+ dehydratedData?: TDehydrated
236
+
237
+ // Must build in constructor
238
+ state!: RouterState<TRouteTree>
239
+ options!: PickAsRequired<
240
+ RouterOptions<TRouteTree, TDehydrated>,
241
+ 'stringifySearch' | 'parseSearch' | 'context'
242
+ >
243
+ history!: RouterHistory
244
+ latestLocation!: ParsedLocation
245
+ basepath!: string
246
+ routeTree!: TRouteTree
247
+ routesById!: RoutesById<TRouteTree>
248
+ routesByPath!: RoutesByPath<TRouteTree>
249
+ flatRoutes!: AnyRoute[]
250
+
251
+ constructor(options: RouterConstructorOptions<TRouteTree, TDehydrated>) {
252
+ this.updateOptions({
253
+ defaultPreloadDelay: 50,
254
+ defaultPendingMs: 1000,
255
+ defaultPendingMinMs: 500,
256
+ context: undefined!,
257
+ ...options,
258
+ stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
259
+ parseSearch: options?.parseSearch ?? defaultParseSearch,
260
+ })
261
+ }
262
+
263
+ startReactTransition: (fn: () => void) => void = () => {
264
+ warning(
265
+ false,
266
+ 'startReactTransition implementation is missing. If you see this, please file an issue.',
267
+ )
268
+ }
269
+
270
+ setState: (
271
+ fn: (s: RouterState<TRouteTree>) => RouterState<TRouteTree>,
272
+ ) => void = () => {
273
+ warning(
274
+ false,
275
+ 'setState implementation is missing. If you see this, please file an issue.',
276
+ )
277
+ }
278
+
279
+ updateOptions = (
280
+ newOptions: PickAsRequired<
281
+ RouterOptions<TRouteTree, TDehydrated>,
282
+ 'stringifySearch' | 'parseSearch' | 'context'
283
+ >,
284
+ ) => {
285
+ this.options = {
286
+ ...this.options,
287
+ ...newOptions,
288
+ }
289
+
290
+ this.basepath = `/${trimPath(newOptions.basepath ?? '') ?? ''}`
291
+
292
+ if (
293
+ !this.history ||
294
+ (this.options.history && this.options.history !== this.history)
295
+ ) {
296
+ this.history = this.options.history ?? createBrowserHistory()
297
+ this.latestLocation = this.parseLocation()
298
+ }
299
+
300
+ if (this.options.routeTree !== this.routeTree) {
301
+ this.routeTree = this.options.routeTree as TRouteTree
302
+ this.buildRouteTree()
303
+ }
304
+
305
+ if (!this.state) {
306
+ this.state = getInitialRouterState(this.latestLocation)
307
+ }
308
+ }
309
+
310
+ buildRouteTree = () => {
311
+ this.routesById = {} as RoutesById<TRouteTree>
312
+ this.routesByPath = {} as RoutesByPath<TRouteTree>
313
+
314
+ const recurseRoutes = (childRoutes: AnyRoute[]) => {
315
+ childRoutes.forEach((childRoute, i) => {
316
+ // if (typeof childRoute === 'function') {
317
+ // childRoute = (childRoute as any)()
318
+ // }
319
+ childRoute.init({ originalIndex: i })
320
+
321
+ const existingRoute = (this.routesById as any)[childRoute.id]
322
+
323
+ invariant(
324
+ !existingRoute,
325
+ `Duplicate routes found with id: ${String(childRoute.id)}`,
326
+ )
327
+ ;(this.routesById as any)[childRoute.id] = childRoute
328
+
329
+ if (!childRoute.isRoot && childRoute.path) {
330
+ const trimmedFullPath = trimPathRight(childRoute.fullPath)
331
+ if (
332
+ !(this.routesByPath as any)[trimmedFullPath] ||
333
+ childRoute.fullPath.endsWith('/')
334
+ ) {
335
+ ;(this.routesByPath as any)[trimmedFullPath] = childRoute
336
+ }
337
+ }
338
+
339
+ const children = childRoute.children as Route[]
340
+
341
+ if (children?.length) {
342
+ recurseRoutes(children)
343
+ }
344
+ })
345
+ }
346
+
347
+ recurseRoutes([this.routeTree])
348
+
349
+ this.flatRoutes = (Object.values(this.routesByPath) as AnyRoute[])
350
+ .map((d, i) => {
351
+ const trimmed = trimPath(d.fullPath)
352
+ const parsed = parsePathname(trimmed)
353
+
354
+ while (parsed.length > 1 && parsed[0]?.value === '/') {
355
+ parsed.shift()
356
+ }
357
+
358
+ const score = parsed.map((d) => {
359
+ if (d.type === 'param') {
360
+ return 0.5
361
+ }
362
+
363
+ if (d.type === 'wildcard') {
364
+ return 0.25
365
+ }
366
+
367
+ return 1
368
+ })
369
+
370
+ return { child: d, trimmed, parsed, index: i, score }
371
+ })
372
+ .sort((a, b) => {
373
+ let isIndex = a.trimmed === '/' ? 1 : b.trimmed === '/' ? -1 : 0
374
+
375
+ if (isIndex !== 0) return isIndex
376
+
377
+ const length = Math.min(a.score.length, b.score.length)
378
+
379
+ // Sort by length of score
380
+ if (a.score.length !== b.score.length) {
381
+ return b.score.length - a.score.length
382
+ }
383
+
384
+ // Sort by min available score
385
+ for (let i = 0; i < length; i++) {
386
+ if (a.score[i] !== b.score[i]) {
387
+ return b.score[i]! - a.score[i]!
388
+ }
389
+ }
390
+
391
+ // Sort by min available parsed value
392
+ for (let i = 0; i < length; i++) {
393
+ if (a.parsed[i]!.value !== b.parsed[i]!.value) {
394
+ return a.parsed[i]!.value! > b.parsed[i]!.value! ? 1 : -1
395
+ }
396
+ }
397
+
398
+ // Sort by length of trimmed full path
399
+ if (a.trimmed !== b.trimmed) {
400
+ return a.trimmed > b.trimmed ? 1 : -1
401
+ }
402
+
403
+ // Sort by original index
404
+ return a.index - b.index
405
+ })
406
+ .map((d, i) => {
407
+ d.child.rank = i
408
+ return d.child
409
+ })
410
+ }
411
+
412
+ subscribe = <TType extends keyof RouterEvents>(
413
+ eventType: TType,
414
+ fn: ListenerFn<RouterEvents[TType]>,
415
+ ) => {
416
+ const listener: RouterListener<any> = {
417
+ eventType,
418
+ fn,
419
+ }
420
+
421
+ this.subscribers.add(listener)
422
+
423
+ return () => {
424
+ this.subscribers.delete(listener)
425
+ }
426
+ }
427
+
428
+ emit = (routerEvent: RouterEvent) => {
429
+ this.subscribers.forEach((listener) => {
430
+ if (listener.eventType === routerEvent.type) {
431
+ listener.fn(routerEvent)
432
+ }
433
+ })
434
+ }
435
+
436
+ checkLatest = (promise: Promise<void>): undefined | Promise<void> => {
437
+ return this.latestLoadPromise !== promise
438
+ ? this.latestLoadPromise
439
+ : undefined
440
+ }
441
+
442
+ parseLocation = (
443
+ previousLocation?: ParsedLocation,
444
+ ): ParsedLocation<FullSearchSchema<TRouteTree>> => {
445
+ const parse = ({
446
+ pathname,
447
+ search,
448
+ hash,
449
+ state,
450
+ }: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
451
+ const parsedSearch = this.options.parseSearch(search)
452
+
453
+ return {
454
+ pathname: pathname,
455
+ searchStr: search,
456
+ search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
457
+ hash: hash.split('#').reverse()[0] ?? '',
458
+ href: `${pathname}${search}${hash}`,
459
+ state: replaceEqualDeep(previousLocation?.state, state) as HistoryState,
460
+ }
461
+ }
462
+
463
+ const location = parse(this.history.location)
464
+
465
+ let { __tempLocation, __tempKey } = location.state
466
+
467
+ if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
468
+ // Sync up the location keys
469
+ const parsedTempLocation = parse(__tempLocation) as any
470
+ parsedTempLocation.state.key = location.state.key
471
+
472
+ delete parsedTempLocation.state.__tempLocation
473
+
474
+ return {
475
+ ...parsedTempLocation,
476
+ maskedLocation: location,
477
+ }
478
+ }
479
+
480
+ return location
481
+ }
482
+
483
+ resolvePathWithBase = (from: string, path: string) => {
484
+ return resolvePath(this.basepath!, from, cleanPath(path))
485
+ }
486
+
487
+ get looseRoutesById() {
488
+ return this.routesById as Record<string, AnyRoute>
489
+ }
490
+
491
+ matchRoutes = <TRouteTree extends AnyRoute>(
492
+ pathname: string,
493
+ locationSearch: AnySearchSchema,
494
+ opts?: { throwOnError?: boolean; debug?: boolean },
495
+ ): RouteMatch<TRouteTree>[] => {
496
+ let routeParams: AnyPathParams = {}
497
+
498
+ let foundRoute = this.flatRoutes.find((route) => {
499
+ const matchedParams = matchPathname(
500
+ this.basepath,
501
+ trimPathRight(pathname),
502
+ {
503
+ to: route.fullPath,
504
+ caseSensitive:
505
+ route.options.caseSensitive ?? this.options.caseSensitive,
506
+ fuzzy: false,
507
+ },
508
+ )
509
+
510
+ if (matchedParams) {
511
+ routeParams = matchedParams
512
+ return true
513
+ }
514
+
515
+ return false
516
+ })
517
+
518
+ let routeCursor: AnyRoute =
519
+ foundRoute || (this.routesById as any)['__root__']
520
+
521
+ let matchedRoutes: AnyRoute[] = [routeCursor]
522
+ // let includingLayouts = true
523
+ while (routeCursor?.parentRoute) {
524
+ routeCursor = routeCursor.parentRoute
525
+ if (routeCursor) matchedRoutes.unshift(routeCursor)
526
+ }
527
+
528
+ // Existing matches are matches that are already loaded along with
529
+ // pending matches that are still loading
530
+
531
+ const parseErrors = matchedRoutes.map((route) => {
532
+ let parsedParamsError
533
+
534
+ if (route.options.parseParams) {
535
+ try {
536
+ const parsedParams = route.options.parseParams(routeParams)
537
+ // Add the parsed params to the accumulated params bag
538
+ Object.assign(routeParams, parsedParams)
539
+ } catch (err: any) {
540
+ parsedParamsError = new PathParamError(err.message, {
541
+ cause: err,
542
+ })
543
+
544
+ if (opts?.throwOnError) {
545
+ throw parsedParamsError
546
+ }
547
+
548
+ return parsedParamsError
549
+ }
550
+ }
551
+
552
+ return
553
+ })
554
+
555
+ const matches: AnyRouteMatch[] = []
556
+
557
+ matchedRoutes.forEach((route, index) => {
558
+ // Take each matched route and resolve + validate its search params
559
+ // This has to happen serially because each route's search params
560
+ // can depend on the parent route's search params
561
+ // It must also happen before we create the match so that we can
562
+ // pass the search params to the route's potential key function
563
+ // which is used to uniquely identify the route match in state
564
+
565
+ const parentMatch = matches[index - 1]
566
+
567
+ const [preMatchSearch, searchError]: [Record<string, any>, any] = (() => {
568
+ // Validate the search params and stabilize them
569
+ const parentSearch = parentMatch?.search ?? locationSearch
570
+
571
+ try {
572
+ const validator =
573
+ typeof route.options.validateSearch === 'object'
574
+ ? route.options.validateSearch.parse
575
+ : route.options.validateSearch
576
+
577
+ let search = validator?.(parentSearch) ?? {}
578
+
579
+ return [
580
+ {
581
+ ...parentSearch,
582
+ ...search,
583
+ },
584
+ undefined,
585
+ ]
586
+ } catch (err: any) {
587
+ const searchError = new SearchParamError(err.message, {
588
+ cause: err,
589
+ })
590
+
591
+ if (opts?.throwOnError) {
592
+ throw searchError
593
+ }
594
+
595
+ return [parentSearch, searchError]
596
+ }
597
+ })()
598
+
599
+ const interpolatedPath = interpolatePath(route.path, routeParams)
600
+ const matchId =
601
+ interpolatePath(route.id, routeParams, true) +
602
+ (route.options.key?.({
603
+ search: preMatchSearch,
604
+ location: this.state.location,
605
+ }) ?? '')
606
+
607
+ // Waste not, want not. If we already have a match for this route,
608
+ // reuse it. This is important for layout routes, which might stick
609
+ // around between navigation actions that only change leaf routes.
610
+ const existingMatch = getRouteMatch(this.state, matchId)
611
+
612
+ const cause = this.state.matches.find((d) => d.id === matchId)
613
+ ? 'stay'
614
+ : 'enter'
615
+
616
+ // Create a fresh route match
617
+ const hasLoaders = !!(
618
+ route.options.loader ||
619
+ componentTypes.some((d) => (route.options[d] as any)?.preload)
620
+ )
621
+
622
+ const match: AnyRouteMatch = existingMatch
623
+ ? { ...existingMatch, cause }
624
+ : {
625
+ id: matchId,
626
+ routeId: route.id,
627
+ params: routeParams,
628
+ pathname: joinPaths([this.basepath, interpolatedPath]),
629
+ updatedAt: Date.now(),
630
+ search: {} as any,
631
+ searchError: undefined,
632
+ status: hasLoaders ? 'pending' : 'success',
633
+ showPending: false,
634
+ isFetching: false,
635
+ invalid: false,
636
+ error: undefined,
637
+ paramsError: parseErrors[index],
638
+ loadPromise: Promise.resolve(),
639
+ context: undefined!,
640
+ abortController: new AbortController(),
641
+ shouldReloadDeps: undefined,
642
+ fetchedAt: 0,
643
+ cause,
644
+ }
645
+
646
+ // Regardless of whether we're reusing an existing match or creating
647
+ // a new one, we need to update the match's search params
648
+ match.search = replaceEqualDeep(match.search, preMatchSearch)
649
+ // And also update the searchError if there is one
650
+ match.searchError = searchError
651
+
652
+ matches.push(match)
653
+ })
654
+
655
+ return matches as any
656
+ }
657
+
658
+ cancelMatch = (id: string) => {
659
+ getRouteMatch(this.state, id)?.abortController?.abort()
660
+ }
661
+
662
+ cancelMatches = () => {
663
+ this.state.matches.forEach((match) => {
664
+ this.cancelMatch(match.id)
665
+ })
666
+ }
667
+
668
+ buildLocation: BuildLocationFn<TRouteTree> = (opts) => {
669
+ const build = (
670
+ dest: BuildNextOptions & {
671
+ unmaskOnReload?: boolean
672
+ } = {},
673
+ matches?: AnyRouteMatch[],
674
+ ): ParsedLocation => {
675
+ const from = this.latestLocation
676
+ const fromPathname = dest.from ?? from.pathname
677
+
678
+ let pathname = this.resolvePathWithBase(fromPathname, `${dest.to ?? ''}`)
679
+
680
+ const fromMatches = this.matchRoutes(fromPathname, from.search)
681
+ const stayingMatches = matches?.filter(
682
+ (d) => fromMatches?.find((e) => e.routeId === d.routeId),
683
+ )
684
+
685
+ const prevParams = { ...last(fromMatches)?.params }
686
+
687
+ let nextParams =
688
+ (dest.params ?? true) === true
689
+ ? prevParams
690
+ : functionalUpdate(dest.params!, prevParams)
691
+
692
+ if (nextParams) {
693
+ matches
694
+ ?.map((d) => this.looseRoutesById[d.routeId]!.options.stringifyParams)
695
+ .filter(Boolean)
696
+ .forEach((fn) => {
697
+ nextParams = { ...nextParams!, ...fn!(nextParams!) }
698
+ })
699
+ }
700
+
701
+ pathname = interpolatePath(pathname, nextParams ?? {})
702
+
703
+ const preSearchFilters =
704
+ stayingMatches
705
+ ?.map(
706
+ (match) =>
707
+ this.looseRoutesById[match.routeId]!.options.preSearchFilters ??
708
+ [],
709
+ )
710
+ .flat()
711
+ .filter(Boolean) ?? []
712
+
713
+ const postSearchFilters =
714
+ stayingMatches
715
+ ?.map(
716
+ (match) =>
717
+ this.looseRoutesById[match.routeId]!.options.postSearchFilters ??
718
+ [],
719
+ )
720
+ .flat()
721
+ .filter(Boolean) ?? []
722
+
723
+ // Pre filters first
724
+ const preFilteredSearch = preSearchFilters?.length
725
+ ? preSearchFilters?.reduce(
726
+ (prev, next) => next(prev) as any,
727
+ from.search,
728
+ )
729
+ : from.search
730
+
731
+ // Then the link/navigate function
732
+ const destSearch =
733
+ dest.search === true
734
+ ? preFilteredSearch // Preserve resolvedFrom true
735
+ : dest.search
736
+ ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
737
+ : preSearchFilters?.length
738
+ ? preFilteredSearch // Preserve resolvedFrom filters
739
+ : {}
740
+
741
+ // Then post filters
742
+ const postFilteredSearch = postSearchFilters?.length
743
+ ? postSearchFilters.reduce((prev, next) => next(prev), destSearch)
744
+ : destSearch
745
+
746
+ const search = replaceEqualDeep(from.search, postFilteredSearch)
747
+
748
+ const searchStr = this.options.stringifySearch(search)
749
+
750
+ const hash =
751
+ dest.hash === true
752
+ ? from.hash
753
+ : dest.hash
754
+ ? functionalUpdate(dest.hash!, from.hash)
755
+ : from.hash
756
+
757
+ const hashStr = hash ? `#${hash}` : ''
758
+
759
+ let nextState =
760
+ dest.state === true
761
+ ? from.state
762
+ : dest.state
763
+ ? functionalUpdate(dest.state, from.state)
764
+ : from.state
765
+
766
+ nextState = replaceEqualDeep(from.state, nextState)
767
+
768
+ return {
769
+ pathname,
770
+ search,
771
+ searchStr,
772
+ state: nextState as any,
773
+ hash,
774
+ href: this.history.createHref(`${pathname}${searchStr}${hashStr}`),
775
+ unmaskOnReload: dest.unmaskOnReload,
776
+ }
777
+ }
778
+
779
+ const buildWithMatches = (
780
+ dest: BuildNextOptions = {},
781
+ maskedDest?: BuildNextOptions,
782
+ ) => {
783
+ let next = build(dest)
784
+ let maskedNext = maskedDest ? build(maskedDest) : undefined
785
+
786
+ if (!maskedNext) {
787
+ let params = {}
788
+
789
+ let foundMask = this.options.routeMasks?.find((d) => {
790
+ const match = matchPathname(this.basepath, next.pathname, {
791
+ to: d.from,
792
+ caseSensitive: false,
793
+ fuzzy: false,
794
+ })
795
+
796
+ if (match) {
797
+ params = match
798
+ return true
799
+ }
800
+
801
+ return false
802
+ })
803
+
804
+ if (foundMask) {
805
+ foundMask = {
806
+ ...foundMask,
807
+ from: interpolatePath(foundMask.from, params) as any,
808
+ }
809
+ maskedDest = foundMask
810
+ maskedNext = build(maskedDest)
811
+ }
812
+ }
813
+
814
+ const nextMatches = this.matchRoutes(next.pathname, next.search)
815
+ const maskedMatches = maskedNext
816
+ ? this.matchRoutes(maskedNext.pathname, maskedNext.search)
817
+ : undefined
818
+ const maskedFinal = maskedNext
819
+ ? build(maskedDest, maskedMatches)
820
+ : undefined
821
+
822
+ const final = build(dest, nextMatches)
823
+
824
+ if (maskedFinal) {
825
+ final.maskedLocation = maskedFinal
826
+ }
827
+
828
+ return final
829
+ }
830
+
831
+ if (opts.mask) {
832
+ return buildWithMatches(opts, {
833
+ ...pick(opts, ['from']),
834
+ ...opts.mask,
835
+ })
836
+ }
837
+
838
+ return buildWithMatches(opts)
839
+ }
840
+
841
+ commitLocation = async ({
842
+ startTransition,
843
+ ...next
844
+ }: ParsedLocation & CommitLocationOptions) => {
845
+ if (this.navigateTimeout) clearTimeout(this.navigateTimeout)
846
+
847
+ const isSameUrl = this.latestLocation.href === next.href
848
+
849
+ // If the next urls are the same and we're not replacing,
850
+ // do nothing
851
+ if (!isSameUrl || !next.replace) {
852
+ let { maskedLocation, ...nextHistory } = next
853
+
854
+ if (maskedLocation) {
855
+ nextHistory = {
856
+ ...maskedLocation,
857
+ state: {
858
+ ...maskedLocation.state,
859
+ __tempKey: undefined,
860
+ __tempLocation: {
861
+ ...nextHistory,
862
+ search: nextHistory.searchStr,
863
+ state: {
864
+ ...nextHistory.state,
865
+ __tempKey: undefined!,
866
+ __tempLocation: undefined!,
867
+ key: undefined!,
868
+ },
869
+ },
870
+ },
871
+ }
872
+
873
+ if (
874
+ nextHistory.unmaskOnReload ??
875
+ this.options.unmaskOnReload ??
876
+ false
877
+ ) {
878
+ nextHistory.state.__tempKey = this.tempLocationKey
879
+ }
880
+ }
881
+
882
+ const apply = () => {
883
+ this.history[next.replace ? 'replace' : 'push'](
884
+ nextHistory.href,
885
+ nextHistory.state,
886
+ )
887
+ }
888
+
889
+ if (startTransition ?? true) {
890
+ this.startReactTransition(apply)
891
+ } else {
892
+ apply()
893
+ }
894
+ }
895
+
896
+ this.resetNextScroll = next.resetScroll ?? true
897
+
898
+ return this.latestLoadPromise
899
+ }
900
+
901
+ buildAndCommitLocation = ({
902
+ replace,
903
+ resetScroll,
904
+ startTransition,
905
+ ...rest
906
+ }: BuildNextOptions & CommitLocationOptions = {}) => {
907
+ const location = this.buildLocation(rest)
908
+ return this.commitLocation({
909
+ ...location,
910
+ startTransition,
911
+ replace,
912
+ resetScroll,
913
+ })
914
+ }
915
+
916
+ navigate: NavigateFn<TRouteTree> = ({ from, to = '', ...rest }) => {
917
+ // If this link simply reloads the current route,
918
+ // make sure it has a new key so it will trigger a data refresh
919
+
920
+ // If this `to` is a valid external URL, return
921
+ // null for LinkUtils
922
+ const toString = String(to)
923
+ const fromString = typeof from === 'undefined' ? from : String(from)
924
+ let isExternal
925
+
926
+ try {
927
+ new URL(`${toString}`)
928
+ isExternal = true
929
+ } catch (e) {}
930
+
931
+ invariant(
932
+ !isExternal,
933
+ 'Attempting to navigate to external url with this.navigate!',
934
+ )
935
+
936
+ return this.buildAndCommitLocation({
937
+ ...rest,
938
+ from: fromString,
939
+ to: toString,
940
+ })
941
+ }
942
+
943
+ loadMatches = async ({
944
+ checkLatest,
945
+ matches,
946
+ preload,
947
+ invalidate,
948
+ }: {
949
+ checkLatest: () => Promise<void> | undefined
950
+ matches: AnyRouteMatch[]
951
+ preload?: boolean
952
+ invalidate?: boolean
953
+ }): Promise<RouteMatch[]> => {
954
+ let latestPromise
955
+ let firstBadMatchIndex: number | undefined
956
+
957
+ // Check each match middleware to see if the route can be accessed
958
+ try {
959
+ for (let [index, match] of matches.entries()) {
960
+ const parentMatch = matches[index - 1]
961
+ const route = this.looseRoutesById[match.routeId]!
962
+
963
+ const handleError = (err: any, code: string) => {
964
+ err.routerCode = code
965
+ firstBadMatchIndex = firstBadMatchIndex ?? index
966
+
967
+ if (isRedirect(err)) {
968
+ throw err
969
+ }
970
+
971
+ try {
972
+ route.options.onError?.(err)
973
+ } catch (errorHandlerErr) {
974
+ err = errorHandlerErr
975
+
976
+ if (isRedirect(errorHandlerErr)) {
977
+ throw errorHandlerErr
978
+ }
979
+ }
980
+
981
+ matches[index] = match = {
982
+ ...match,
983
+ error: err,
984
+ status: 'error',
985
+ updatedAt: Date.now(),
986
+ }
987
+ }
988
+
989
+ try {
990
+ if (match.paramsError) {
991
+ handleError(match.paramsError, 'PARSE_PARAMS')
992
+ }
993
+
994
+ if (match.searchError) {
995
+ handleError(match.searchError, 'VALIDATE_SEARCH')
996
+ }
997
+
998
+ const parentContext =
999
+ parentMatch?.context ?? this.options.context ?? {}
1000
+
1001
+ const beforeLoadContext =
1002
+ (await route.options.beforeLoad?.({
1003
+ search: match.search,
1004
+ abortController: match.abortController,
1005
+ params: match.params,
1006
+ preload: !!preload,
1007
+ context: parentContext,
1008
+ location: this.state.location,
1009
+ // TOOD: just expose state and router, etc
1010
+ navigate: (opts) =>
1011
+ this.navigate({ ...opts, from: match.pathname } as any),
1012
+ buildLocation: this.buildLocation,
1013
+ cause: match.cause,
1014
+ })) ?? ({} as any)
1015
+
1016
+ const context = {
1017
+ ...parentContext,
1018
+ ...beforeLoadContext,
1019
+ }
1020
+
1021
+ matches[index] = match = {
1022
+ ...match,
1023
+ context: replaceEqualDeep(match.context, context),
1024
+ }
1025
+ } catch (err) {
1026
+ handleError(err, 'BEFORE_LOAD')
1027
+ break
1028
+ }
1029
+ }
1030
+ } catch (err) {
1031
+ if (isRedirect(err)) {
1032
+ if (!preload) this.navigate(err as any)
1033
+ return matches
1034
+ }
1035
+
1036
+ throw err
1037
+ }
1038
+
1039
+ const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
1040
+ const matchPromises: Promise<any>[] = []
1041
+
1042
+ validResolvedMatches.forEach((match, index) => {
1043
+ matchPromises.push(
1044
+ (async () => {
1045
+ const parentMatchPromise = matchPromises[index - 1]
1046
+ const route = this.looseRoutesById[match.routeId]!
1047
+
1048
+ const handleIfRedirect = (err: any) => {
1049
+ if (isRedirect(err)) {
1050
+ if (!preload) {
1051
+ this.navigate(err as any)
1052
+ }
1053
+ return true
1054
+ }
1055
+ return false
1056
+ }
1057
+
1058
+ let loadPromise: Promise<void> | undefined
1059
+
1060
+ matches[index] = match = {
1061
+ ...match,
1062
+ fetchedAt: Date.now(),
1063
+ invalid: false,
1064
+ showPending: false,
1065
+ }
1066
+
1067
+ const pendingMs =
1068
+ route.options.pendingMs ?? this.options.defaultPendingMs
1069
+
1070
+ let pendingPromise: Promise<void> | undefined
1071
+
1072
+ if (
1073
+ !preload &&
1074
+ pendingMs &&
1075
+ (route.options.pendingComponent ??
1076
+ this.options.defaultPendingComponent)
1077
+ ) {
1078
+ pendingPromise = new Promise((r) => setTimeout(r, pendingMs))
1079
+ }
1080
+
1081
+ if (match.isFetching) {
1082
+ loadPromise = getRouteMatch(this.state, match.id)?.loadPromise
1083
+ } else {
1084
+ const loaderContext: LoaderFnContext = {
1085
+ params: match.params,
1086
+ search: match.search,
1087
+ preload: !!preload,
1088
+ parentMatchPromise,
1089
+ abortController: match.abortController,
1090
+ context: match.context,
1091
+ location: this.state.location,
1092
+ navigate: (opts) =>
1093
+ this.navigate({ ...opts, from: match.pathname } as any),
1094
+ cause: match.cause,
1095
+ }
1096
+
1097
+ // Default to reloading the route all the time
1098
+ let shouldReload = true
1099
+
1100
+ let shouldReloadDeps =
1101
+ typeof route.options.shouldReload === 'function'
1102
+ ? route.options.shouldReload?.(loaderContext)
1103
+ : !!(route.options.shouldReload ?? true)
1104
+
1105
+ if (match.cause === 'enter' || invalidate) {
1106
+ match.shouldReloadDeps = shouldReloadDeps
1107
+ } else if (match.cause === 'stay') {
1108
+ if (typeof shouldReloadDeps === 'object') {
1109
+ // compare the deps to see if they've changed
1110
+ shouldReload = !deepEqual(
1111
+ shouldReloadDeps,
1112
+ match.shouldReloadDeps,
1113
+ )
1114
+
1115
+ match.shouldReloadDeps = shouldReloadDeps
1116
+ } else {
1117
+ shouldReload = !!shouldReloadDeps
1118
+ }
1119
+ }
1120
+
1121
+ // If the user doesn't want the route to reload, just
1122
+ // resolve with the existing loader data
1123
+
1124
+ if (!shouldReload) {
1125
+ loadPromise = Promise.resolve(match.loaderData)
1126
+ } else {
1127
+ // Otherwise, load the route
1128
+ matches[index] = match = {
1129
+ ...match,
1130
+ isFetching: true,
1131
+ }
1132
+
1133
+ const componentsPromise = Promise.all(
1134
+ componentTypes.map(async (type) => {
1135
+ const component = route.options[type]
1136
+
1137
+ if ((component as any)?.preload) {
1138
+ await (component as any).preload()
1139
+ }
1140
+ }),
1141
+ )
1142
+
1143
+ const loaderPromise = route.options.loader?.(loaderContext)
1144
+
1145
+ loadPromise = Promise.all([
1146
+ componentsPromise,
1147
+ loaderPromise,
1148
+ ]).then((d) => d[1])
1149
+ }
1150
+ }
1151
+
1152
+ matches[index] = match = {
1153
+ ...match,
1154
+ loadPromise,
1155
+ }
1156
+
1157
+ if (!preload) {
1158
+ this.setState((s) => ({
1159
+ ...s,
1160
+ matches: s.matches.map((d) => (d.id === match.id ? match : d)),
1161
+ }))
1162
+ }
1163
+
1164
+ let didShowPending = false
1165
+
1166
+ await new Promise<void>(async (resolve) => {
1167
+ // If the route has a pending component and a pendingMs option,
1168
+ // forcefully show the pending component
1169
+ if (pendingPromise) {
1170
+ pendingPromise.then(() => {
1171
+ didShowPending = true
1172
+ matches[index] = match = {
1173
+ ...match,
1174
+ showPending: true,
1175
+ }
1176
+
1177
+ this.setState((s) => ({
1178
+ ...s,
1179
+ matches: s.matches.map((d) =>
1180
+ d.id === match.id ? match : d,
1181
+ ),
1182
+ }))
1183
+ resolve()
1184
+ })
1185
+ }
1186
+
1187
+ try {
1188
+ const loaderData = await loadPromise
1189
+ if ((latestPromise = checkLatest())) return await latestPromise
1190
+
1191
+ const pendingMinMs =
1192
+ route.options.pendingMinMs ?? this.options.defaultPendingMinMs
1193
+
1194
+ if (didShowPending && pendingMinMs) {
1195
+ await new Promise((r) => setTimeout(r, pendingMinMs))
1196
+ }
1197
+
1198
+ matches[index] = match = {
1199
+ ...match,
1200
+ error: undefined,
1201
+ status: 'success',
1202
+ isFetching: false,
1203
+ updatedAt: Date.now(),
1204
+ loaderData,
1205
+ loadPromise: undefined,
1206
+ }
1207
+ } catch (error) {
1208
+ if ((latestPromise = checkLatest())) return await latestPromise
1209
+ if (handleIfRedirect(error)) return
1210
+
1211
+ try {
1212
+ route.options.onError?.(error)
1213
+ } catch (onErrorError) {
1214
+ error = onErrorError
1215
+ if (handleIfRedirect(onErrorError)) return
1216
+ }
1217
+
1218
+ matches[index] = match = {
1219
+ ...match,
1220
+ error,
1221
+ status: 'error',
1222
+ isFetching: false,
1223
+ updatedAt: Date.now(),
1224
+ }
1225
+ }
1226
+
1227
+ if (!preload) {
1228
+ this.setState((s) => ({
1229
+ ...s,
1230
+ matches: s.matches.map((d) => (d.id === match.id ? match : d)),
1231
+ }))
1232
+ }
1233
+
1234
+ resolve()
1235
+ })
1236
+ })(),
1237
+ )
1238
+ })
1239
+
1240
+ await Promise.all(matchPromises)
1241
+ return matches
1242
+ }
1243
+
1244
+ invalidate = () =>
1245
+ this.load({
1246
+ invalidate: true,
1247
+ })
1248
+
1249
+ load = async (opts?: { invalidate?: boolean }): Promise<void> => {
1250
+ const promise = new Promise<void>(async (resolve, reject) => {
1251
+ const next = this.latestLocation
1252
+ const prevLocation = this.state.resolvedLocation
1253
+ const pathDidChange = prevLocation!.href !== next.href
1254
+ let latestPromise: Promise<void> | undefined | null
1255
+
1256
+ // Cancel any pending matches
1257
+ this.cancelMatches()
1258
+
1259
+ this.emit({
1260
+ type: 'onBeforeLoad',
1261
+ fromLocation: prevLocation,
1262
+ toLocation: next,
1263
+ pathChanged: pathDidChange,
1264
+ })
1265
+
1266
+ // Match the routes
1267
+ let matches: RouteMatch<any, any>[] = this.matchRoutes(
1268
+ next.pathname,
1269
+ next.search,
1270
+ {
1271
+ debug: true,
1272
+ },
1273
+ )
1274
+
1275
+ this.pendingMatches = matches
1276
+
1277
+ const previousMatches = this.state.matches
1278
+
1279
+ // Ingest the new matches
1280
+ this.setState((s) => ({
1281
+ ...s,
1282
+ status: 'pending',
1283
+ location: next,
1284
+ matches,
1285
+ }))
1286
+
1287
+ try {
1288
+ try {
1289
+ // Load the matches
1290
+ await this.loadMatches({
1291
+ matches,
1292
+ checkLatest: () => this.checkLatest(promise),
1293
+ invalidate: opts?.invalidate,
1294
+ })
1295
+ } catch (err) {
1296
+ // swallow this error, since we'll display the
1297
+ // errors on the route components
1298
+ }
1299
+
1300
+ // Only apply the latest transition
1301
+ if ((latestPromise = this.checkLatest(promise))) {
1302
+ return latestPromise
1303
+ }
1304
+
1305
+ const exitingMatchIds = previousMatches.filter(
1306
+ (id) => !this.pendingMatches.includes(id),
1307
+ )
1308
+ const enteringMatchIds = this.pendingMatches.filter(
1309
+ (id) => !previousMatches.includes(id),
1310
+ )
1311
+ const stayingMatchIds = previousMatches.filter((id) =>
1312
+ this.pendingMatches.includes(id),
1313
+ )
1314
+
1315
+ // setState((s) => ({
1316
+ // ...s,
1317
+ // status: 'idle',
1318
+ // resolvedLocation: s.location,
1319
+ // }))
1320
+
1321
+ //
1322
+ ;(
1323
+ [
1324
+ [exitingMatchIds, 'onLeave'],
1325
+ [enteringMatchIds, 'onEnter'],
1326
+ [stayingMatchIds, 'onTransition'],
1327
+ ] as const
1328
+ ).forEach(([matches, hook]) => {
1329
+ matches.forEach((match) => {
1330
+ this.looseRoutesById[match.routeId]!.options[hook]?.(match)
1331
+ })
1332
+ })
1333
+
1334
+ this.emit({
1335
+ type: 'onLoad',
1336
+ fromLocation: prevLocation,
1337
+ toLocation: next,
1338
+ pathChanged: pathDidChange,
1339
+ })
1340
+
1341
+ resolve()
1342
+ } catch (err) {
1343
+ // Only apply the latest transition
1344
+ if ((latestPromise = this.checkLatest(promise))) {
1345
+ return latestPromise
1346
+ }
1347
+
1348
+ reject(err)
1349
+ }
1350
+ })
1351
+
1352
+ this.latestLoadPromise = promise
1353
+
1354
+ return this.latestLoadPromise
1355
+ }
1356
+
1357
+ preloadRoute = async (
1358
+ navigateOpts: BuildNextOptions = this.state.location,
1359
+ ) => {
1360
+ let next = this.buildLocation(navigateOpts)
1361
+
1362
+ let matches = this.matchRoutes(next.pathname, next.search, {
1363
+ throwOnError: true,
1364
+ })
1365
+
1366
+ await this.loadMatches({
1367
+ matches,
1368
+ preload: true,
1369
+ checkLatest: () => undefined,
1370
+ })
1371
+
1372
+ return [last(matches)!, matches] as const
1373
+ }
1374
+
1375
+ buildLink: BuildLinkFn<TRouteTree> = (dest) => {
1376
+ // If this link simply reloads the current route,
1377
+ // make sure it has a new key so it will trigger a data refresh
1378
+
1379
+ // If this `to` is a valid external URL, return
1380
+ // null for LinkUtils
1381
+
1382
+ const {
1383
+ to,
1384
+ preload: userPreload,
1385
+ preloadDelay: userPreloadDelay,
1386
+ activeOptions,
1387
+ disabled,
1388
+ target,
1389
+ replace,
1390
+ resetScroll,
1391
+ startTransition,
1392
+ } = dest
1393
+
1394
+ try {
1395
+ new URL(`${to}`)
1396
+ return {
1397
+ type: 'external',
1398
+ href: to as any,
1399
+ }
1400
+ } catch (e) {}
1401
+
1402
+ const nextOpts = dest
1403
+ const next = this.buildLocation(nextOpts as any)
1404
+
1405
+ const preload = userPreload ?? this.options.defaultPreload
1406
+ const preloadDelay =
1407
+ userPreloadDelay ?? this.options.defaultPreloadDelay ?? 0
1408
+
1409
+ // Compare path/hash for matches
1410
+ const currentPathSplit = this.latestLocation.pathname.split('/')
1411
+ const nextPathSplit = next.pathname.split('/')
1412
+ const pathIsFuzzyEqual = nextPathSplit.every(
1413
+ (d, i) => d === currentPathSplit[i],
1414
+ )
1415
+ // Combine the matches based on user this.options
1416
+ const pathTest = activeOptions?.exact
1417
+ ? this.latestLocation.pathname === next.pathname
1418
+ : pathIsFuzzyEqual
1419
+ const hashTest = activeOptions?.includeHash
1420
+ ? this.latestLocation.hash === next.hash
1421
+ : true
1422
+ const searchTest =
1423
+ activeOptions?.includeSearch ?? true
1424
+ ? deepEqual(this.latestLocation.search, next.search, true)
1425
+ : true
1426
+
1427
+ // The final "active" test
1428
+ const isActive = pathTest && hashTest && searchTest
1429
+
1430
+ // The click handler
1431
+ const handleClick = (e: MouseEvent) => {
1432
+ if (
1433
+ !disabled &&
1434
+ !isCtrlEvent(e) &&
1435
+ !e.defaultPrevented &&
1436
+ (!target || target === '_self') &&
1437
+ e.button === 0
1438
+ ) {
1439
+ e.preventDefault()
1440
+
1441
+ // All is well? Navigate!
1442
+ this.commitLocation({ ...next, replace, resetScroll, startTransition })
1443
+ }
1444
+ }
1445
+
1446
+ // The click handler
1447
+ const handleFocus = (e: MouseEvent) => {
1448
+ if (preload) {
1449
+ this.preloadRoute(nextOpts as any).catch((err) => {
1450
+ console.warn(err)
1451
+ console.warn(preloadWarning)
1452
+ })
1453
+ }
1454
+ }
1455
+
1456
+ const handleTouchStart = (e: TouchEvent) => {
1457
+ this.preloadRoute(nextOpts as any).catch((err) => {
1458
+ console.warn(err)
1459
+ console.warn(preloadWarning)
1460
+ })
1461
+ }
1462
+
1463
+ const handleEnter = (e: MouseEvent) => {
1464
+ const target = (e.target || {}) as LinkCurrentTargetElement
1465
+
1466
+ if (preload) {
1467
+ if (target.preloadTimeout) {
1468
+ return
1469
+ }
1470
+
1471
+ target.preloadTimeout = setTimeout(() => {
1472
+ target.preloadTimeout = null
1473
+ this.preloadRoute(nextOpts as any).catch((err) => {
1474
+ console.warn(err)
1475
+ console.warn(preloadWarning)
1476
+ })
1477
+ }, preloadDelay)
1478
+ }
1479
+ }
1480
+
1481
+ const handleLeave = (e: MouseEvent) => {
1482
+ const target = (e.target || {}) as LinkCurrentTargetElement
1483
+
1484
+ if (target.preloadTimeout) {
1485
+ clearTimeout(target.preloadTimeout)
1486
+ target.preloadTimeout = null
1487
+ }
1488
+ }
1489
+
1490
+ return {
1491
+ type: 'internal',
1492
+ next,
1493
+ handleFocus,
1494
+ handleClick,
1495
+ handleEnter,
1496
+ handleLeave,
1497
+ handleTouchStart,
1498
+ isActive,
1499
+ disabled,
1500
+ }
1501
+ }
1502
+
1503
+ matchRoute: MatchRouteFn<TRouteTree> = (location, opts) => {
1504
+ location = {
1505
+ ...location,
1506
+ to: location.to
1507
+ ? this.resolvePathWithBase((location.from || '') as string, location.to)
1508
+ : undefined,
1509
+ } as any
1510
+
1511
+ const next = this.buildLocation(location as any)
1512
+
1513
+ if (opts?.pending && this.state.status !== 'pending') {
1514
+ return false
1515
+ }
1516
+
1517
+ const baseLocation = opts?.pending
1518
+ ? this.latestLocation
1519
+ : this.state.resolvedLocation
1520
+
1521
+ // const baseLocation = state.resolvedLocation
1522
+
1523
+ if (!baseLocation) {
1524
+ return false
1525
+ }
1526
+
1527
+ const match = matchPathname(this.basepath, baseLocation.pathname, {
1528
+ ...opts,
1529
+ to: next.pathname,
1530
+ }) as any
1531
+
1532
+ if (!match) {
1533
+ return false
1534
+ }
1535
+
1536
+ if (match && (opts?.includeSearch ?? true)) {
1537
+ return deepEqual(baseLocation.search, next.search, true) ? match : false
1538
+ }
1539
+
1540
+ return match
1541
+ }
1542
+
1543
+ injectHtml = async (html: string | (() => Promise<string> | string)) => {
1544
+ this.injectedHtml.push(html)
1545
+ }
1546
+
1547
+ dehydrateData = <T>(key: any, getData: T | (() => Promise<T> | T)) => {
1548
+ if (typeof document === 'undefined') {
1549
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key)
1550
+
1551
+ this.injectHtml(async () => {
1552
+ const id = `__TSR_DEHYDRATED__${strKey}`
1553
+ const data =
1554
+ typeof getData === 'function' ? await (getData as any)() : getData
1555
+ return `<script id='${id}' suppressHydrationWarning>window["__TSR_DEHYDRATED__${escapeJSON(
1556
+ strKey,
1557
+ )}"] = ${JSON.stringify(data)}
1558
+ ;(() => {
1559
+ var el = document.getElementById('${id}')
1560
+ el.parentElement.removeChild(el)
1561
+ })()
1562
+ </script>`
1563
+ })
1564
+
1565
+ return () => this.hydrateData<T>(key)
1566
+ }
1567
+
1568
+ return () => undefined
1569
+ }
1570
+
1571
+ hydrateData = <T extends any = unknown>(key: any) => {
1572
+ if (typeof document !== 'undefined') {
1573
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key)
1574
+
1575
+ return window[`__TSR_DEHYDRATED__${strKey}` as any] as T
1576
+ }
1577
+
1578
+ return undefined
1579
+ }
1580
+
1581
+ // dehydrate = (): DehydratedRouter => {
1582
+ // return {
1583
+ // state: {
1584
+ // dehydratedMatches: this.state.matches.map((d) =>
1585
+ // pick(d, ['fetchedAt', 'invalid', 'id', 'status', 'updatedAt']),
1586
+ // ),
1587
+ // },
1588
+ // }
1589
+ // }
1590
+
1591
+ // hydrate = async (__do_not_use_server_ctx?: HydrationCtx) => {
1592
+ // let _ctx = __do_not_use_server_ctx
1593
+ // // Client hydrates from window
1594
+ // if (typeof document !== 'undefined') {
1595
+ // _ctx = window.__TSR_DEHYDRATED__
1596
+ // }
1597
+
1598
+ // invariant(
1599
+ // _ctx,
1600
+ // 'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
1601
+ // )
1602
+
1603
+ // const ctx = _ctx
1604
+ // this.dehydratedData = ctx.payload as any
1605
+ // this.options.hydrate?.(ctx.payload as any)
1606
+ // const dehydratedState = ctx.router.state
1607
+
1608
+ // let matches = this.matchRoutes(
1609
+ // this.state.location.pathname,
1610
+ // this.state.location.search,
1611
+ // ).map((match) => {
1612
+ // const dehydratedMatch = dehydratedState.dehydratedMatches.find(
1613
+ // (d) => d.id === match.id,
1614
+ // )
1615
+
1616
+ // invariant(
1617
+ // dehydratedMatch,
1618
+ // `Could not find a client-side match for dehydrated match with id: ${match.id}!`,
1619
+ // )
1620
+
1621
+ // if (dehydratedMatch) {
1622
+ // return {
1623
+ // ...match,
1624
+ // ...dehydratedMatch,
1625
+ // }
1626
+ // }
1627
+ // return match
1628
+ // })
1629
+
1630
+ // this.setState((s) => {
1631
+ // return {
1632
+ // ...s,
1633
+ // matches: dehydratedState.dehydratedMatches as any,
1634
+ // }
1635
+ // })
1636
+ // }
1637
+
1638
+ // resolveMatchPromise = (matchId: string, key: string, value: any) => {
1639
+ // state.matches
1640
+ // .find((d) => d.id === matchId)
1641
+ // ?.__promisesByKey[key]?.resolve(value)
1642
+ // }
1643
+ }
1644
+
1645
+ // A function that takes an import() argument which is a function and returns a new function that will
1646
+ // proxy arguments from the caller to the imported function, retaining all type
1647
+ // information along the way
1648
+ export function lazyFn<
1649
+ T extends Record<string, (...args: any[]) => any>,
1650
+ TKey extends keyof T = 'default',
1651
+ >(fn: () => Promise<T>, key?: TKey) {
1652
+ return async (...args: Parameters<T[TKey]>): Promise<ReturnType<T[TKey]>> => {
1653
+ const imported = await fn()
1654
+ return imported[key || 'default'](...args)
1655
+ }
1656
+ }
1657
+
1658
+ function isCtrlEvent(e: MouseEvent) {
1659
+ return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
1660
+ }