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

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 +235 -0
  5. package/build/cjs/Matches.js.map +1 -0
  6. package/build/cjs/RouterProvider.js +159 -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 +123 -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 +1101 -0
  31. package/build/cjs/router.js.map +1 -0
  32. package/build/cjs/scroll-restoration.js +202 -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 +2174 -2557
  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 +18 -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 +2470 -2513
  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 +389 -0
  82. package/src/RouterProvider.tsx +226 -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 +1664 -0
  98. package/src/scroll-restoration.tsx +230 -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,1664 @@
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
+ if ((latestPromise = checkLatest())) return
1172
+
1173
+ didShowPending = true
1174
+ matches[index] = match = {
1175
+ ...match,
1176
+ showPending: true,
1177
+ }
1178
+
1179
+ this.setState((s) => ({
1180
+ ...s,
1181
+ matches: s.matches.map((d) =>
1182
+ d.id === match.id ? match : d,
1183
+ ),
1184
+ }))
1185
+ resolve()
1186
+ })
1187
+ }
1188
+
1189
+ try {
1190
+ const loaderData = await loadPromise
1191
+ if ((latestPromise = checkLatest())) return await latestPromise
1192
+
1193
+ const pendingMinMs =
1194
+ route.options.pendingMinMs ?? this.options.defaultPendingMinMs
1195
+
1196
+ if (didShowPending && pendingMinMs) {
1197
+ await new Promise((r) => setTimeout(r, pendingMinMs))
1198
+ }
1199
+
1200
+ if ((latestPromise = checkLatest())) return await latestPromise
1201
+
1202
+ matches[index] = match = {
1203
+ ...match,
1204
+ error: undefined,
1205
+ status: 'success',
1206
+ isFetching: false,
1207
+ updatedAt: Date.now(),
1208
+ loaderData,
1209
+ loadPromise: undefined,
1210
+ }
1211
+ } catch (error) {
1212
+ if ((latestPromise = checkLatest())) return await latestPromise
1213
+ if (handleIfRedirect(error)) return
1214
+
1215
+ try {
1216
+ route.options.onError?.(error)
1217
+ } catch (onErrorError) {
1218
+ error = onErrorError
1219
+ if (handleIfRedirect(onErrorError)) return
1220
+ }
1221
+
1222
+ matches[index] = match = {
1223
+ ...match,
1224
+ error,
1225
+ status: 'error',
1226
+ isFetching: false,
1227
+ updatedAt: Date.now(),
1228
+ }
1229
+ }
1230
+
1231
+ if (!preload) {
1232
+ this.setState((s) => ({
1233
+ ...s,
1234
+ matches: s.matches.map((d) => (d.id === match.id ? match : d)),
1235
+ }))
1236
+ }
1237
+
1238
+ resolve()
1239
+ })
1240
+ })(),
1241
+ )
1242
+ })
1243
+
1244
+ await Promise.all(matchPromises)
1245
+ return matches
1246
+ }
1247
+
1248
+ invalidate = () =>
1249
+ this.load({
1250
+ invalidate: true,
1251
+ })
1252
+
1253
+ load = async (opts?: { invalidate?: boolean }): Promise<void> => {
1254
+ const promise = new Promise<void>(async (resolve, reject) => {
1255
+ const next = this.latestLocation
1256
+ const prevLocation = this.state.resolvedLocation
1257
+ const pathDidChange = prevLocation!.href !== next.href
1258
+ let latestPromise: Promise<void> | undefined | null
1259
+
1260
+ // Cancel any pending matches
1261
+ this.cancelMatches()
1262
+
1263
+ this.emit({
1264
+ type: 'onBeforeLoad',
1265
+ fromLocation: prevLocation,
1266
+ toLocation: next,
1267
+ pathChanged: pathDidChange,
1268
+ })
1269
+
1270
+ // Match the routes
1271
+ let matches: RouteMatch<any, any>[] = this.matchRoutes(
1272
+ next.pathname,
1273
+ next.search,
1274
+ {
1275
+ debug: true,
1276
+ },
1277
+ )
1278
+
1279
+ this.pendingMatches = matches
1280
+
1281
+ const previousMatches = this.state.matches
1282
+
1283
+ // Ingest the new matches
1284
+ this.setState((s) => ({
1285
+ ...s,
1286
+ // status: 'pending',
1287
+ location: next,
1288
+ matches,
1289
+ }))
1290
+
1291
+ try {
1292
+ try {
1293
+ // Load the matches
1294
+ await this.loadMatches({
1295
+ matches,
1296
+ checkLatest: () => this.checkLatest(promise),
1297
+ invalidate: opts?.invalidate,
1298
+ })
1299
+ } catch (err) {
1300
+ // swallow this error, since we'll display the
1301
+ // errors on the route components
1302
+ }
1303
+
1304
+ // Only apply the latest transition
1305
+ if ((latestPromise = this.checkLatest(promise))) {
1306
+ return latestPromise
1307
+ }
1308
+
1309
+ const exitingMatchIds = previousMatches.filter(
1310
+ (id) => !this.pendingMatches.includes(id),
1311
+ )
1312
+ const enteringMatchIds = this.pendingMatches.filter(
1313
+ (id) => !previousMatches.includes(id),
1314
+ )
1315
+ const stayingMatchIds = previousMatches.filter((id) =>
1316
+ this.pendingMatches.includes(id),
1317
+ )
1318
+
1319
+ // setState((s) => ({
1320
+ // ...s,
1321
+ // status: 'idle',
1322
+ // resolvedLocation: s.location,
1323
+ // }))
1324
+
1325
+ //
1326
+ ;(
1327
+ [
1328
+ [exitingMatchIds, 'onLeave'],
1329
+ [enteringMatchIds, 'onEnter'],
1330
+ [stayingMatchIds, 'onTransition'],
1331
+ ] as const
1332
+ ).forEach(([matches, hook]) => {
1333
+ matches.forEach((match) => {
1334
+ this.looseRoutesById[match.routeId]!.options[hook]?.(match)
1335
+ })
1336
+ })
1337
+
1338
+ this.emit({
1339
+ type: 'onLoad',
1340
+ fromLocation: prevLocation,
1341
+ toLocation: next,
1342
+ pathChanged: pathDidChange,
1343
+ })
1344
+
1345
+ resolve()
1346
+ } catch (err) {
1347
+ // Only apply the latest transition
1348
+ if ((latestPromise = this.checkLatest(promise))) {
1349
+ return latestPromise
1350
+ }
1351
+
1352
+ reject(err)
1353
+ }
1354
+ })
1355
+
1356
+ this.latestLoadPromise = promise
1357
+
1358
+ return this.latestLoadPromise
1359
+ }
1360
+
1361
+ preloadRoute = async (
1362
+ navigateOpts: BuildNextOptions = this.state.location,
1363
+ ) => {
1364
+ let next = this.buildLocation(navigateOpts)
1365
+
1366
+ let matches = this.matchRoutes(next.pathname, next.search, {
1367
+ throwOnError: true,
1368
+ })
1369
+
1370
+ await this.loadMatches({
1371
+ matches,
1372
+ preload: true,
1373
+ checkLatest: () => undefined,
1374
+ })
1375
+
1376
+ return [last(matches)!, matches] as const
1377
+ }
1378
+
1379
+ buildLink: BuildLinkFn<TRouteTree> = (dest) => {
1380
+ // If this link simply reloads the current route,
1381
+ // make sure it has a new key so it will trigger a data refresh
1382
+
1383
+ // If this `to` is a valid external URL, return
1384
+ // null for LinkUtils
1385
+
1386
+ const {
1387
+ to,
1388
+ preload: userPreload,
1389
+ preloadDelay: userPreloadDelay,
1390
+ activeOptions,
1391
+ disabled,
1392
+ target,
1393
+ replace,
1394
+ resetScroll,
1395
+ startTransition,
1396
+ } = dest
1397
+
1398
+ try {
1399
+ new URL(`${to}`)
1400
+ return {
1401
+ type: 'external',
1402
+ href: to as any,
1403
+ }
1404
+ } catch (e) {}
1405
+
1406
+ const nextOpts = dest
1407
+ const next = this.buildLocation(nextOpts as any)
1408
+
1409
+ const preload = userPreload ?? this.options.defaultPreload
1410
+ const preloadDelay =
1411
+ userPreloadDelay ?? this.options.defaultPreloadDelay ?? 0
1412
+
1413
+ // Compare path/hash for matches
1414
+ const currentPathSplit = this.latestLocation.pathname.split('/')
1415
+ const nextPathSplit = next.pathname.split('/')
1416
+ const pathIsFuzzyEqual = nextPathSplit.every(
1417
+ (d, i) => d === currentPathSplit[i],
1418
+ )
1419
+ // Combine the matches based on user this.options
1420
+ const pathTest = activeOptions?.exact
1421
+ ? this.latestLocation.pathname === next.pathname
1422
+ : pathIsFuzzyEqual
1423
+ const hashTest = activeOptions?.includeHash
1424
+ ? this.latestLocation.hash === next.hash
1425
+ : true
1426
+ const searchTest =
1427
+ activeOptions?.includeSearch ?? true
1428
+ ? deepEqual(this.latestLocation.search, next.search, true)
1429
+ : true
1430
+
1431
+ // The final "active" test
1432
+ const isActive = pathTest && hashTest && searchTest
1433
+
1434
+ // The click handler
1435
+ const handleClick = (e: MouseEvent) => {
1436
+ if (
1437
+ !disabled &&
1438
+ !isCtrlEvent(e) &&
1439
+ !e.defaultPrevented &&
1440
+ (!target || target === '_self') &&
1441
+ e.button === 0
1442
+ ) {
1443
+ e.preventDefault()
1444
+
1445
+ // All is well? Navigate!
1446
+ this.commitLocation({ ...next, replace, resetScroll, startTransition })
1447
+ }
1448
+ }
1449
+
1450
+ // The click handler
1451
+ const handleFocus = (e: MouseEvent) => {
1452
+ if (preload) {
1453
+ this.preloadRoute(nextOpts as any).catch((err) => {
1454
+ console.warn(err)
1455
+ console.warn(preloadWarning)
1456
+ })
1457
+ }
1458
+ }
1459
+
1460
+ const handleTouchStart = (e: TouchEvent) => {
1461
+ this.preloadRoute(nextOpts as any).catch((err) => {
1462
+ console.warn(err)
1463
+ console.warn(preloadWarning)
1464
+ })
1465
+ }
1466
+
1467
+ const handleEnter = (e: MouseEvent) => {
1468
+ const target = (e.target || {}) as LinkCurrentTargetElement
1469
+
1470
+ if (preload) {
1471
+ if (target.preloadTimeout) {
1472
+ return
1473
+ }
1474
+
1475
+ target.preloadTimeout = setTimeout(() => {
1476
+ target.preloadTimeout = null
1477
+ this.preloadRoute(nextOpts as any).catch((err) => {
1478
+ console.warn(err)
1479
+ console.warn(preloadWarning)
1480
+ })
1481
+ }, preloadDelay)
1482
+ }
1483
+ }
1484
+
1485
+ const handleLeave = (e: MouseEvent) => {
1486
+ const target = (e.target || {}) as LinkCurrentTargetElement
1487
+
1488
+ if (target.preloadTimeout) {
1489
+ clearTimeout(target.preloadTimeout)
1490
+ target.preloadTimeout = null
1491
+ }
1492
+ }
1493
+
1494
+ return {
1495
+ type: 'internal',
1496
+ next,
1497
+ handleFocus,
1498
+ handleClick,
1499
+ handleEnter,
1500
+ handleLeave,
1501
+ handleTouchStart,
1502
+ isActive,
1503
+ disabled,
1504
+ }
1505
+ }
1506
+
1507
+ matchRoute: MatchRouteFn<TRouteTree> = (location, opts) => {
1508
+ location = {
1509
+ ...location,
1510
+ to: location.to
1511
+ ? this.resolvePathWithBase((location.from || '') as string, location.to)
1512
+ : undefined,
1513
+ } as any
1514
+
1515
+ const next = this.buildLocation(location as any)
1516
+
1517
+ if (opts?.pending && this.state.status !== 'pending') {
1518
+ return false
1519
+ }
1520
+
1521
+ const baseLocation = opts?.pending
1522
+ ? this.latestLocation
1523
+ : this.state.resolvedLocation
1524
+
1525
+ // const baseLocation = state.resolvedLocation
1526
+
1527
+ if (!baseLocation) {
1528
+ return false
1529
+ }
1530
+
1531
+ const match = matchPathname(this.basepath, baseLocation.pathname, {
1532
+ ...opts,
1533
+ to: next.pathname,
1534
+ }) as any
1535
+
1536
+ if (!match) {
1537
+ return false
1538
+ }
1539
+
1540
+ if (match && (opts?.includeSearch ?? true)) {
1541
+ return deepEqual(baseLocation.search, next.search, true) ? match : false
1542
+ }
1543
+
1544
+ return match
1545
+ }
1546
+
1547
+ injectHtml = async (html: string | (() => Promise<string> | string)) => {
1548
+ this.injectedHtml.push(html)
1549
+ }
1550
+
1551
+ dehydrateData = <T>(key: any, getData: T | (() => Promise<T> | T)) => {
1552
+ if (typeof document === 'undefined') {
1553
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key)
1554
+
1555
+ this.injectHtml(async () => {
1556
+ const id = `__TSR_DEHYDRATED__${strKey}`
1557
+ const data =
1558
+ typeof getData === 'function' ? await (getData as any)() : getData
1559
+ return `<script id='${id}' suppressHydrationWarning>window["__TSR_DEHYDRATED__${escapeJSON(
1560
+ strKey,
1561
+ )}"] = ${JSON.stringify(data)}
1562
+ ;(() => {
1563
+ var el = document.getElementById('${id}')
1564
+ el.parentElement.removeChild(el)
1565
+ })()
1566
+ </script>`
1567
+ })
1568
+
1569
+ return () => this.hydrateData<T>(key)
1570
+ }
1571
+
1572
+ return () => undefined
1573
+ }
1574
+
1575
+ hydrateData = <T extends any = unknown>(key: any) => {
1576
+ if (typeof document !== 'undefined') {
1577
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key)
1578
+
1579
+ return window[`__TSR_DEHYDRATED__${strKey}` as any] as T
1580
+ }
1581
+
1582
+ return undefined
1583
+ }
1584
+
1585
+ // dehydrate = (): DehydratedRouter => {
1586
+ // return {
1587
+ // state: {
1588
+ // dehydratedMatches: this.state.matches.map((d) =>
1589
+ // pick(d, ['fetchedAt', 'invalid', 'id', 'status', 'updatedAt']),
1590
+ // ),
1591
+ // },
1592
+ // }
1593
+ // }
1594
+
1595
+ // hydrate = async (__do_not_use_server_ctx?: HydrationCtx) => {
1596
+ // let _ctx = __do_not_use_server_ctx
1597
+ // // Client hydrates from window
1598
+ // if (typeof document !== 'undefined') {
1599
+ // _ctx = window.__TSR_DEHYDRATED__
1600
+ // }
1601
+
1602
+ // invariant(
1603
+ // _ctx,
1604
+ // 'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
1605
+ // )
1606
+
1607
+ // const ctx = _ctx
1608
+ // this.dehydratedData = ctx.payload as any
1609
+ // this.options.hydrate?.(ctx.payload as any)
1610
+ // const dehydratedState = ctx.router.state
1611
+
1612
+ // let matches = this.matchRoutes(
1613
+ // this.state.location.pathname,
1614
+ // this.state.location.search,
1615
+ // ).map((match) => {
1616
+ // const dehydratedMatch = dehydratedState.dehydratedMatches.find(
1617
+ // (d) => d.id === match.id,
1618
+ // )
1619
+
1620
+ // invariant(
1621
+ // dehydratedMatch,
1622
+ // `Could not find a client-side match for dehydrated match with id: ${match.id}!`,
1623
+ // )
1624
+
1625
+ // if (dehydratedMatch) {
1626
+ // return {
1627
+ // ...match,
1628
+ // ...dehydratedMatch,
1629
+ // }
1630
+ // }
1631
+ // return match
1632
+ // })
1633
+
1634
+ // this.setState((s) => {
1635
+ // return {
1636
+ // ...s,
1637
+ // matches: dehydratedState.dehydratedMatches as any,
1638
+ // }
1639
+ // })
1640
+ // }
1641
+
1642
+ // resolveMatchPromise = (matchId: string, key: string, value: any) => {
1643
+ // state.matches
1644
+ // .find((d) => d.id === matchId)
1645
+ // ?.__promisesByKey[key]?.resolve(value)
1646
+ // }
1647
+ }
1648
+
1649
+ // A function that takes an import() argument which is a function and returns a new function that will
1650
+ // proxy arguments from the caller to the imported function, retaining all type
1651
+ // information along the way
1652
+ export function lazyFn<
1653
+ T extends Record<string, (...args: any[]) => any>,
1654
+ TKey extends keyof T = 'default',
1655
+ >(fn: () => Promise<T>, key?: TKey) {
1656
+ return async (...args: Parameters<T[TKey]>): Promise<ReturnType<T[TKey]>> => {
1657
+ const imported = await fn()
1658
+ return imported[key || 'default'](...args)
1659
+ }
1660
+ }
1661
+
1662
+ function isCtrlEvent(e: MouseEvent) {
1663
+ return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
1664
+ }