@tanstack/react-router 0.0.1-beta.223 → 0.0.1-beta.225

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