@tanstack/react-router 1.6.1 → 1.7.0

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.
@@ -18,69 +18,6 @@ import * as React from 'react';
18
18
  import { useStore } from '@tanstack/react-store';
19
19
  import { Store } from '@tanstack/store';
20
20
 
21
- let routerContext = /*#__PURE__*/React.createContext(null);
22
- if (typeof document !== 'undefined') {
23
- if (window.__TSR_ROUTER_CONTEXT__) {
24
- routerContext = window.__TSR_ROUTER_CONTEXT__;
25
- } else {
26
- window.__TSR_ROUTER_CONTEXT__ = routerContext;
27
- }
28
- }
29
-
30
- function useRouter(opts) {
31
- const resolvedContext = typeof document !== 'undefined' ? window.__TSR_ROUTER_CONTEXT__ || routerContext : routerContext;
32
- const value = React.useContext(resolvedContext);
33
- warning(!((opts?.warn ?? true) && !value), 'useRouter must be used inside a <RouterProvider> component!');
34
- return value;
35
- }
36
-
37
- function defer(_promise) {
38
- const promise = _promise;
39
- if (!promise.__deferredState) {
40
- promise.__deferredState = {
41
- uid: Math.random().toString(36).slice(2),
42
- status: 'pending'
43
- };
44
- const state = promise.__deferredState;
45
- promise.then(data => {
46
- state.status = 'success';
47
- state.data = data;
48
- }).catch(error => {
49
- state.status = 'error';
50
- state.error = error;
51
- });
52
- }
53
- return promise;
54
- }
55
- function isDehydratedDeferred(obj) {
56
- return typeof obj === 'object' && obj !== null && !(obj instanceof Promise) && !obj.then && '__deferredState' in obj;
57
- }
58
-
59
- function useAwaited({
60
- promise
61
- }) {
62
- const router = useRouter();
63
- let state = promise.__deferredState;
64
- const key = `__TSR__DEFERRED__${state.uid}`;
65
- if (isDehydratedDeferred(promise)) {
66
- state = router.hydrateData(key);
67
- promise = Promise.resolve(state.data);
68
- promise.__deferredState = state;
69
- }
70
- if (state.status === 'pending') {
71
- throw promise;
72
- }
73
- if (state.status === 'error') {
74
- throw state.error;
75
- }
76
- router.dehydrateData(key, state);
77
- return [state.data];
78
- }
79
- function Await(props) {
80
- const awaited = useAwaited(props);
81
- return props.children(...awaited);
82
- }
83
-
84
21
  function CatchBoundary(props) {
85
22
  const errorComponent = props.errorComponent ?? ErrorComponent;
86
23
  return /*#__PURE__*/React.createElement(CatchBoundaryImpl, {
@@ -172,6 +109,22 @@ function ErrorComponent({
172
109
  }, error.message ? /*#__PURE__*/React.createElement("code", null, error.message) : null)) : null);
173
110
  }
174
111
 
112
+ let routerContext = /*#__PURE__*/React.createContext(null);
113
+ if (typeof document !== 'undefined') {
114
+ if (window.__TSR_ROUTER_CONTEXT__) {
115
+ routerContext = window.__TSR_ROUTER_CONTEXT__;
116
+ } else {
117
+ window.__TSR_ROUTER_CONTEXT__ = routerContext;
118
+ }
119
+ }
120
+
121
+ function useRouter(opts) {
122
+ const resolvedContext = typeof document !== 'undefined' ? window.__TSR_ROUTER_CONTEXT__ || routerContext : routerContext;
123
+ const value = React.useContext(resolvedContext);
124
+ warning(!((opts?.warn ?? true) && !value), 'useRouter must be used inside a <RouterProvider> component!');
125
+ return value;
126
+ }
127
+
175
128
  function useRouterState(opts) {
176
129
  const contextRouter = useRouter({
177
130
  warn: opts?.router === undefined
@@ -443,7 +396,12 @@ function MatchInner({
443
396
  select: s => pick(getRenderedMatches(s).find(d => d.id === matchId), ['status', 'error', 'showPending', 'loadPromise'])
444
397
  });
445
398
  if (match.status === 'error') {
446
- throw match.error;
399
+ if (isServerSideError(match.error)) {
400
+ const deserializeError = router.options.errorSerializer?.deserialize ?? defaultDeserializeError;
401
+ throw deserializeError(match.error.data);
402
+ } else {
403
+ throw match.error;
404
+ }
447
405
  }
448
406
  if (match.status === 'pending') {
449
407
  if (match.showPending) {
@@ -564,114 +522,353 @@ function useLoaderData(opts) {
564
522
  }
565
523
  });
566
524
  }
567
-
568
- function joinPaths(paths) {
569
- return cleanPath(paths.filter(Boolean).join('/'));
570
- }
571
- function cleanPath(path) {
572
- // remove double slashes
573
- return path.replace(/\/{2,}/g, '/');
574
- }
575
- function trimPathLeft(path) {
576
- return path === '/' ? path : path.replace(/^\/{1,}/, '');
525
+ function isServerSideError(error) {
526
+ if (!(typeof error === 'object' && error && 'data' in error)) return false;
527
+ if (!('__isServerError' in error && error.__isServerError)) return false;
528
+ if (!(typeof error.data === 'object' && error.data)) return false;
529
+ return error.__isServerError === true;
530
+ }
531
+ function defaultDeserializeError(serializedData) {
532
+ if ('name' in serializedData && 'message' in serializedData) {
533
+ const error = new Error(serializedData.message);
534
+ error.name = serializedData.name;
535
+ return error;
536
+ }
537
+ return serializedData.data;
577
538
  }
578
- function trimPathRight(path) {
579
- return path === '/' ? path : path.replace(/\/{1,}$/, '');
539
+
540
+ // @ts-nocheck
541
+
542
+ // qss has been slightly modified and inlined here for our use cases (and compression's sake). We've included it as a hard dependency for MIT license attribution.
543
+
544
+ function encode(obj, pfx) {
545
+ var k,
546
+ i,
547
+ tmp,
548
+ str = '';
549
+ for (k in obj) {
550
+ if ((tmp = obj[k]) !== void 0) {
551
+ if (Array.isArray(tmp)) {
552
+ for (i = 0; i < tmp.length; i++) {
553
+ str && (str += '&');
554
+ str += encodeURIComponent(k) + '=' + encodeURIComponent(tmp[i]);
555
+ }
556
+ } else {
557
+ str && (str += '&');
558
+ str += encodeURIComponent(k) + '=' + encodeURIComponent(tmp);
559
+ }
560
+ }
561
+ }
562
+ return (pfx || '') + str;
580
563
  }
581
- function trimPath(path) {
582
- return trimPathRight(trimPathLeft(path));
564
+ function toValue(mix) {
565
+ if (!mix) return '';
566
+ var str = decodeURIComponent(mix);
567
+ if (str === 'false') return false;
568
+ if (str === 'true') return true;
569
+ return +str * 0 === 0 && +str + '' === str ? +str : str;
583
570
  }
584
- function resolvePath(basepath, base, to) {
585
- base = base.replace(new RegExp(`^${basepath}`), '/');
586
- to = to.replace(new RegExp(`^${basepath}`), '/');
587
- let baseSegments = parsePathname(base);
588
- const toSegments = parsePathname(to);
589
- toSegments.forEach((toSegment, index) => {
590
- if (toSegment.value === '/') {
591
- if (!index) {
592
- // Leading slash
593
- baseSegments = [toSegment];
594
- } else if (index === toSegments.length - 1) {
595
- // Trailing Slash
596
- baseSegments.push(toSegment);
597
- } else ;
598
- } else if (toSegment.value === '..') {
599
- // Extra trailing slash? pop it off
600
- if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
601
- baseSegments.pop();
602
- }
603
- baseSegments.pop();
604
- } else if (toSegment.value === '.') {
605
- return;
571
+ function decode(str) {
572
+ var tmp,
573
+ k,
574
+ out = {},
575
+ arr = str.split('&');
576
+ while (tmp = arr.shift()) {
577
+ tmp = tmp.split('=');
578
+ k = tmp.shift();
579
+ if (out[k] !== void 0) {
580
+ out[k] = [].concat(out[k], toValue(tmp.shift()));
606
581
  } else {
607
- baseSegments.push(toSegment);
582
+ out[k] = toValue(tmp.shift());
608
583
  }
609
- });
610
- const joined = joinPaths([basepath, ...baseSegments.map(d => d.value)]);
611
- return cleanPath(joined);
612
- }
613
- function parsePathname(pathname) {
614
- if (!pathname) {
615
- return [];
616
- }
617
- pathname = cleanPath(pathname);
618
- const segments = [];
619
- if (pathname.slice(0, 1) === '/') {
620
- pathname = pathname.substring(1);
621
- segments.push({
622
- type: 'pathname',
623
- value: '/'
624
- });
625
- }
626
- if (!pathname) {
627
- return segments;
628
584
  }
585
+ return out;
586
+ }
629
587
 
630
- // Remove empty segments and '.' segments
631
- const split = pathname.split('/').filter(Boolean);
632
- segments.push(...split.map(part => {
633
- if (part === '$' || part === '*') {
634
- return {
635
- type: 'wildcard',
636
- value: part
637
- };
588
+ const defaultParseSearch = parseSearchWith(JSON.parse);
589
+ const defaultStringifySearch = stringifySearchWith(JSON.stringify, JSON.parse);
590
+ function parseSearchWith(parser) {
591
+ return searchStr => {
592
+ if (searchStr.substring(0, 1) === '?') {
593
+ searchStr = searchStr.substring(1);
638
594
  }
639
- if (part.charAt(0) === '$') {
640
- return {
641
- type: 'param',
642
- value: part
643
- };
595
+ let query = decode(searchStr);
596
+
597
+ // Try to parse any query params that might be json
598
+ for (let key in query) {
599
+ const value = query[key];
600
+ if (typeof value === 'string') {
601
+ try {
602
+ query[key] = parser(value);
603
+ } catch (err) {
604
+ //
605
+ }
606
+ }
644
607
  }
645
- return {
646
- type: 'pathname',
647
- value: part
648
- };
649
- }));
650
- if (pathname.slice(-1) === '/') {
651
- pathname = pathname.substring(1);
652
- segments.push({
653
- type: 'pathname',
654
- value: '/'
655
- });
656
- }
657
- return segments;
608
+ return query;
609
+ };
658
610
  }
659
- function interpolatePath(path, params, leaveWildcards = false) {
660
- const interpolatedPathSegments = parsePathname(path);
661
- return joinPaths(interpolatedPathSegments.map(segment => {
662
- if (segment.type === 'wildcard') {
663
- const value = params[segment.value];
664
- if (leaveWildcards) return `${segment.value}${value ?? ''}`;
665
- return value;
666
- }
667
- if (segment.type === 'param') {
668
- return params[segment.value.substring(1)] ?? 'undefined';
611
+ function stringifySearchWith(stringify, parser) {
612
+ function stringifyValue(val) {
613
+ if (typeof val === 'object' && val !== null) {
614
+ try {
615
+ return stringify(val);
616
+ } catch (err) {
617
+ // silent
618
+ }
619
+ } else if (typeof val === 'string' && typeof parser === 'function') {
620
+ try {
621
+ // Check if it's a valid parseable string.
622
+ // If it is, then stringify it again.
623
+ parser(val);
624
+ return stringify(val);
625
+ } catch (err) {
626
+ // silent
627
+ }
669
628
  }
670
- return segment.value;
671
- }));
672
- }
673
- function matchPathname(basepath, currentPathname, matchLocation) {
674
- const pathParams = matchByPath(basepath, currentPathname, matchLocation);
629
+ return val;
630
+ }
631
+ return search => {
632
+ search = {
633
+ ...search
634
+ };
635
+ if (search) {
636
+ Object.keys(search).forEach(key => {
637
+ const val = search[key];
638
+ if (typeof val === 'undefined' || val === undefined) {
639
+ delete search[key];
640
+ } else {
641
+ search[key] = stringifyValue(val);
642
+ }
643
+ });
644
+ }
645
+ const searchStr = encode(search).toString();
646
+ return searchStr ? `?${searchStr}` : '';
647
+ };
648
+ }
649
+
650
+ const useTransition = React.useTransition || (() => [false, cb => {
651
+ cb();
652
+ }]);
653
+ function RouterProvider({
654
+ router,
655
+ ...rest
656
+ }) {
657
+ // Allow the router to update options on the router instance
658
+ router.update({
659
+ ...router.options,
660
+ ...rest,
661
+ context: {
662
+ ...router.options.context,
663
+ ...rest?.context
664
+ }
665
+ });
666
+ const matches = router.options.InnerWrap ? /*#__PURE__*/React.createElement(router.options.InnerWrap, null, /*#__PURE__*/React.createElement(Matches, null)) : /*#__PURE__*/React.createElement(Matches, null);
667
+ const provider = /*#__PURE__*/React.createElement(routerContext.Provider, {
668
+ value: router
669
+ }, matches, /*#__PURE__*/React.createElement(Transitioner, null));
670
+ if (router.options.Wrap) {
671
+ return /*#__PURE__*/React.createElement(router.options.Wrap, null, provider);
672
+ }
673
+ return provider;
674
+ }
675
+ function Transitioner() {
676
+ const mountLoadCount = React.useRef(0);
677
+ const router = useRouter();
678
+ const routerState = useRouterState({
679
+ select: s => pick(s, ['isLoading', 'location', 'resolvedLocation', 'isTransitioning'])
680
+ });
681
+ const [isTransitioning, startReactTransition] = useTransition();
682
+ router.startReactTransition = startReactTransition;
683
+ React.useEffect(() => {
684
+ if (isTransitioning) {
685
+ router.__store.setState(s => ({
686
+ ...s,
687
+ isTransitioning
688
+ }));
689
+ }
690
+ }, [isTransitioning]);
691
+ const tryLoad = () => {
692
+ const apply = cb => {
693
+ if (!routerState.isTransitioning) {
694
+ startReactTransition(() => cb());
695
+ } else {
696
+ cb();
697
+ }
698
+ };
699
+ apply(() => {
700
+ try {
701
+ router.load();
702
+ } catch (err) {
703
+ console.error(err);
704
+ }
705
+ });
706
+ };
707
+ useLayoutEffect$1(() => {
708
+ const unsub = router.history.subscribe(() => {
709
+ router.latestLocation = router.parseLocation(router.latestLocation);
710
+ if (routerState.location !== router.latestLocation) {
711
+ tryLoad();
712
+ }
713
+ });
714
+ const nextLocation = router.buildLocation({
715
+ search: true,
716
+ params: true,
717
+ hash: true,
718
+ state: true
719
+ });
720
+ if (routerState.location.href !== nextLocation.href) {
721
+ router.commitLocation({
722
+ ...nextLocation,
723
+ replace: true
724
+ });
725
+ }
726
+ return () => {
727
+ unsub();
728
+ };
729
+ }, [router.history]);
730
+ useLayoutEffect$1(() => {
731
+ if (React.useTransition ? routerState.isTransitioning && !isTransitioning : !routerState.isLoading && routerState.resolvedLocation !== routerState.location) {
732
+ router.emit({
733
+ type: 'onResolved',
734
+ fromLocation: routerState.resolvedLocation,
735
+ toLocation: routerState.location,
736
+ pathChanged: routerState.location.href !== routerState.resolvedLocation?.href
737
+ });
738
+ if (document.querySelector) {
739
+ if (routerState.location.hash !== '') {
740
+ const el = document.getElementById(routerState.location.hash);
741
+ if (el) {
742
+ el.scrollIntoView();
743
+ }
744
+ }
745
+ }
746
+ router.__store.setState(s => ({
747
+ ...s,
748
+ isTransitioning: false,
749
+ resolvedLocation: s.location
750
+ }));
751
+ }
752
+ }, [routerState.isTransitioning, isTransitioning, routerState.isLoading, routerState.resolvedLocation, routerState.location]);
753
+ useLayoutEffect$1(() => {
754
+ if (!window.__TSR_DEHYDRATED__ && !mountLoadCount.current) {
755
+ mountLoadCount.current++;
756
+ tryLoad();
757
+ }
758
+ }, []);
759
+ return null;
760
+ }
761
+ function getRouteMatch(state, id) {
762
+ return [...state.cachedMatches, ...(state.pendingMatches ?? []), ...state.matches].find(d => d.id === id);
763
+ }
764
+
765
+ function joinPaths(paths) {
766
+ return cleanPath(paths.filter(Boolean).join('/'));
767
+ }
768
+ function cleanPath(path) {
769
+ // remove double slashes
770
+ return path.replace(/\/{2,}/g, '/');
771
+ }
772
+ function trimPathLeft(path) {
773
+ return path === '/' ? path : path.replace(/^\/{1,}/, '');
774
+ }
775
+ function trimPathRight(path) {
776
+ return path === '/' ? path : path.replace(/\/{1,}$/, '');
777
+ }
778
+ function trimPath(path) {
779
+ return trimPathRight(trimPathLeft(path));
780
+ }
781
+ function resolvePath(basepath, base, to) {
782
+ base = base.replace(new RegExp(`^${basepath}`), '/');
783
+ to = to.replace(new RegExp(`^${basepath}`), '/');
784
+ let baseSegments = parsePathname(base);
785
+ const toSegments = parsePathname(to);
786
+ toSegments.forEach((toSegment, index) => {
787
+ if (toSegment.value === '/') {
788
+ if (!index) {
789
+ // Leading slash
790
+ baseSegments = [toSegment];
791
+ } else if (index === toSegments.length - 1) {
792
+ // Trailing Slash
793
+ baseSegments.push(toSegment);
794
+ } else ;
795
+ } else if (toSegment.value === '..') {
796
+ // Extra trailing slash? pop it off
797
+ if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
798
+ baseSegments.pop();
799
+ }
800
+ baseSegments.pop();
801
+ } else if (toSegment.value === '.') {
802
+ return;
803
+ } else {
804
+ baseSegments.push(toSegment);
805
+ }
806
+ });
807
+ const joined = joinPaths([basepath, ...baseSegments.map(d => d.value)]);
808
+ return cleanPath(joined);
809
+ }
810
+ function parsePathname(pathname) {
811
+ if (!pathname) {
812
+ return [];
813
+ }
814
+ pathname = cleanPath(pathname);
815
+ const segments = [];
816
+ if (pathname.slice(0, 1) === '/') {
817
+ pathname = pathname.substring(1);
818
+ segments.push({
819
+ type: 'pathname',
820
+ value: '/'
821
+ });
822
+ }
823
+ if (!pathname) {
824
+ return segments;
825
+ }
826
+
827
+ // Remove empty segments and '.' segments
828
+ const split = pathname.split('/').filter(Boolean);
829
+ segments.push(...split.map(part => {
830
+ if (part === '$' || part === '*') {
831
+ return {
832
+ type: 'wildcard',
833
+ value: part
834
+ };
835
+ }
836
+ if (part.charAt(0) === '$') {
837
+ return {
838
+ type: 'param',
839
+ value: part
840
+ };
841
+ }
842
+ return {
843
+ type: 'pathname',
844
+ value: part
845
+ };
846
+ }));
847
+ if (pathname.slice(-1) === '/') {
848
+ pathname = pathname.substring(1);
849
+ segments.push({
850
+ type: 'pathname',
851
+ value: '/'
852
+ });
853
+ }
854
+ return segments;
855
+ }
856
+ function interpolatePath(path, params, leaveWildcards = false) {
857
+ const interpolatedPathSegments = parsePathname(path);
858
+ return joinPaths(interpolatedPathSegments.map(segment => {
859
+ if (segment.type === 'wildcard') {
860
+ const value = params[segment.value];
861
+ if (leaveWildcards) return `${segment.value}${value ?? ''}`;
862
+ return value;
863
+ }
864
+ if (segment.type === 'param') {
865
+ return params[segment.value.substring(1)] ?? 'undefined';
866
+ }
867
+ return segment.value;
868
+ }));
869
+ }
870
+ function matchPathname(basepath, currentPathname, matchLocation) {
871
+ const pathParams = matchByPath(basepath, currentPathname, matchLocation);
675
872
  // const searchMatched = matchBySearch(location.search, matchLocation)
676
873
 
677
874
  if (matchLocation.to && !pathParams) {
@@ -756,1827 +953,1684 @@ function matchByPath(basepath, from, matchLocation) {
756
953
  return isMatch ? params : undefined;
757
954
  }
758
955
 
759
- function useParams(opts) {
760
- return useRouterState({
761
- select: state => {
762
- const params = last(getRenderedMatches(state))?.params;
763
- return opts?.select ? opts.select(params) : params;
764
- }
765
- });
766
- }
956
+ // Detect if we're in the DOM
767
957
 
768
- function useSearch(opts) {
769
- return useMatch({
770
- ...opts,
771
- select: match => {
772
- return opts?.select ? opts.select(match.search) : match.search;
773
- }
774
- });
958
+ function redirect(opts) {
959
+ opts.isRedirect = true;
960
+ if (opts.throw) {
961
+ throw opts;
962
+ }
963
+ return opts;
964
+ }
965
+ function isRedirect(obj) {
966
+ return !!obj?.isRedirect;
775
967
  }
776
968
 
777
- const rootRouteId = '__root__';
778
-
779
- // The parse type here allows a zod schema to be passed directly to the validator
780
-
781
- // TODO: This is part of a future APi to move away from classes and
782
- // towards a more functional API. It's not ready yet.
783
-
784
- // type RouteApiInstance<
785
- // TId extends RouteIds<RegisteredRouter['routeTree']>,
786
- // TRoute extends AnyRoute = RouteById<RegisteredRouter['routeTree'], TId>,
787
- // TFullSearchSchema extends Record<
788
- // string,
789
- // any
790
- // > = TRoute['types']['fullSearchSchema'],
791
- // TAllParams extends AnyPathParams = TRoute['types']['allParams'],
792
- // TAllContext extends Record<string, any> = TRoute['types']['allContext'],
793
- // TLoaderDeps extends Record<string, any> = TRoute['types']['loaderDeps'],
794
- // TLoaderData extends any = TRoute['types']['loaderData'],
795
- // > = {
796
- // id: TId
797
- // useMatch: <TSelected = TAllContext>(opts?: {
798
- // select?: (s: TAllContext) => TSelected
799
- // }) => TSelected
800
-
801
- // useRouteContext: <TSelected = TAllContext>(opts?: {
802
- // select?: (s: TAllContext) => TSelected
803
- // }) => TSelected
804
-
805
- // useSearch: <TSelected = TFullSearchSchema>(opts?: {
806
- // select?: (s: TFullSearchSchema) => TSelected
807
- // }) => TSelected
808
-
809
- // useParams: <TSelected = TAllParams>(opts?: {
810
- // select?: (s: TAllParams) => TSelected
811
- // }) => TSelected
812
-
813
- // useLoaderDeps: <TSelected = TLoaderDeps>(opts?: {
814
- // select?: (s: TLoaderDeps) => TSelected
815
- // }) => TSelected
816
-
817
- // useLoaderData: <TSelected = TLoaderData>(opts?: {
818
- // select?: (s: TLoaderData) => TSelected
819
- // }) => TSelected
820
- // }
821
-
822
- // export function RouteApi_v2<
823
- // TId extends RouteIds<RegisteredRouter['routeTree']>,
824
- // TRoute extends AnyRoute = RouteById<RegisteredRouter['routeTree'], TId>,
825
- // TFullSearchSchema extends Record<
826
- // string,
827
- // any
828
- // > = TRoute['types']['fullSearchSchema'],
829
- // TAllParams extends AnyPathParams = TRoute['types']['allParams'],
830
- // TAllContext extends Record<string, any> = TRoute['types']['allContext'],
831
- // TLoaderDeps extends Record<string, any> = TRoute['types']['loaderDeps'],
832
- // TLoaderData extends any = TRoute['types']['loaderData'],
833
- // >({
834
- // id,
835
- // }: {
836
- // id: TId
837
- // }): RouteApiInstance<
838
- // TId,
839
- // TRoute,
840
- // TFullSearchSchema,
841
- // TAllParams,
842
- // TAllContext,
843
- // TLoaderDeps,
844
- // TLoaderData
845
- // > {
846
- // return {
847
- // id,
848
-
849
- // useMatch: (opts) => {
850
- // return useMatch({ ...opts, from: id })
851
- // },
852
-
853
- // useRouteContext: (opts) => {
854
- // return useMatch({
855
- // ...opts,
856
- // from: id,
857
- // select: (d: any) => (opts?.select ? opts.select(d.context) : d.context),
858
- // } as any)
859
- // },
860
-
861
- // useSearch: (opts) => {
862
- // return useSearch({ ...opts, from: id } as any)
863
- // },
864
-
865
- // useParams: (opts) => {
866
- // return useParams({ ...opts, from: id } as any)
867
- // },
868
-
869
- // useLoaderDeps: (opts) => {
870
- // return useLoaderDeps({ ...opts, from: id } as any) as any
871
- // },
872
-
873
- // useLoaderData: (opts) => {
874
- // return useLoaderData({ ...opts, from: id } as any) as any
875
- // },
876
- // }
877
- // }
969
+ // import warning from 'tiny-warning'
878
970
 
879
- class RouteApi {
880
- constructor({
881
- id
882
- }) {
883
- this.id = id;
884
- }
885
- useMatch = opts => {
886
- return useMatch({
887
- select: opts?.select,
888
- from: this.id
889
- });
890
- };
891
- useRouteContext = opts => {
892
- return useMatch({
893
- from: this.id,
894
- select: d => opts?.select ? opts.select(d.context) : d.context
895
- });
896
- };
897
- useSearch = opts => {
898
- return useSearch({
899
- ...opts,
900
- from: this.id
901
- });
902
- };
903
- useParams = opts => {
904
- return useParams({
905
- ...opts,
906
- from: this.id
907
- });
908
- };
909
- useLoaderDeps = opts => {
910
- return useLoaderDeps({
911
- ...opts,
912
- from: this.id
913
- });
914
- };
915
- useLoaderData = opts => {
916
- return useLoaderData({
917
- ...opts,
918
- from: this.id
919
- });
920
- };
921
- }
922
- class Route {
923
- // Set up in this.init()
971
+ //
924
972
 
925
- // customId!: TCustomId
973
+ const componentTypes = ['component', 'errorComponent', 'pendingComponent'];
974
+ class Router {
975
+ // Option-independent properties
976
+ tempLocationKey = `${Math.round(Math.random() * 10000000)}`;
977
+ resetNextScroll = true;
978
+ navigateTimeout = null;
979
+ latestLoadPromise = Promise.resolve();
980
+ subscribers = new Set();
981
+ injectedHtml = [];
926
982
 
927
- // Optional
983
+ // Must build in constructor
928
984
 
929
985
  constructor(options) {
930
- this.options = options || {};
931
- this.isRoot = !options?.getParentRoute;
932
- invariant(!(options?.id && options?.path), `Route cannot have both an 'id' and a 'path' option.`);
933
- this.$$typeof = Symbol.for('react.memo');
986
+ this.update({
987
+ defaultPreloadDelay: 50,
988
+ defaultPendingMs: 1000,
989
+ defaultPendingMinMs: 500,
990
+ context: undefined,
991
+ ...options,
992
+ stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
993
+ parseSearch: options?.parseSearch ?? defaultParseSearch
994
+ });
934
995
  }
935
- init = opts => {
936
- this.originalIndex = opts.originalIndex;
937
- const options = this.options;
938
- const isRoot = !options?.path && !options?.id;
939
- this.parentRoute = this.options?.getParentRoute?.();
940
- if (isRoot) {
941
- this.path = rootRouteId;
942
- } else {
943
- invariant(this.parentRoute, `Child Route instances must pass a 'getParentRoute: () => ParentRoute' option that returns a Route instance.`);
944
- }
945
- let path = isRoot ? rootRouteId : options.path;
946
996
 
947
- // If the path is anything other than an index path, trim it up
948
- if (path && path !== '/') {
949
- path = trimPath(path);
997
+ // These are default implementations that can optionally be overridden
998
+ // by the router provider once rendered. We provide these so that the
999
+ // router can be used in a non-react environment if necessary
1000
+ startReactTransition = fn => fn();
1001
+ update = newOptions => {
1002
+ const previousOptions = this.options;
1003
+ this.options = {
1004
+ ...this.options,
1005
+ ...newOptions
1006
+ };
1007
+ if (!this.basepath || newOptions.basepath && newOptions.basepath !== previousOptions.basepath) {
1008
+ if (newOptions.basepath === undefined || newOptions.basepath === '' || newOptions.basepath === '/') {
1009
+ this.basepath = '/';
1010
+ } else {
1011
+ this.basepath = `/${trimPath(newOptions.basepath)}`;
1012
+ }
950
1013
  }
951
- const customId = options?.id || path;
952
-
953
- // Strip the parentId prefix from the first level of children
954
- let id = isRoot ? rootRouteId : joinPaths([this.parentRoute.id === rootRouteId ? '' : this.parentRoute.id, customId]);
955
- if (path === rootRouteId) {
956
- path = '/';
1014
+ if (!this.history || this.options.history && this.options.history !== this.history) {
1015
+ this.history = this.options.history ?? (typeof document !== 'undefined' ? createBrowserHistory() : createMemoryHistory({
1016
+ initialEntries: [this.options.basepath || '/']
1017
+ }));
1018
+ this.latestLocation = this.parseLocation();
957
1019
  }
958
- if (id !== rootRouteId) {
959
- id = joinPaths(['/', id]);
1020
+ if (this.options.routeTree !== this.routeTree) {
1021
+ this.routeTree = this.options.routeTree;
1022
+ this.buildRouteTree();
1023
+ }
1024
+ if (!this.__store) {
1025
+ this.__store = new Store(getInitialRouterState(this.latestLocation), {
1026
+ onUpdate: () => {
1027
+ this.__store.state = {
1028
+ ...this.state,
1029
+ status: this.state.isTransitioning || this.state.isLoading ? 'pending' : 'idle'
1030
+ };
1031
+ }
1032
+ });
960
1033
  }
961
- const fullPath = id === rootRouteId ? '/' : joinPaths([this.parentRoute.fullPath, path]);
962
- this.path = path;
963
- this.id = id;
964
- // this.customId = customId as TCustomId
965
- this.fullPath = fullPath;
966
- this.to = fullPath;
967
- };
968
- addChildren = children => {
969
- this.children = children;
970
- return this;
971
- };
972
- updateLoader = options => {
973
- Object.assign(this.options, options);
974
- return this;
975
1034
  };
976
- update = options => {
977
- Object.assign(this.options, options);
978
- return this;
979
- };
980
- useMatch = opts => {
981
- return useMatch({
982
- ...opts,
983
- from: this.id
1035
+ get state() {
1036
+ return this.__store.state;
1037
+ }
1038
+ buildRouteTree = () => {
1039
+ this.routesById = {};
1040
+ this.routesByPath = {};
1041
+ const notFoundRoute = this.options.notFoundRoute;
1042
+ if (notFoundRoute) {
1043
+ notFoundRoute.init({
1044
+ originalIndex: 99999999999
1045
+ });
1046
+ this.routesById[notFoundRoute.id] = notFoundRoute;
1047
+ }
1048
+ const recurseRoutes = childRoutes => {
1049
+ childRoutes.forEach((childRoute, i) => {
1050
+ childRoute.init({
1051
+ originalIndex: i
1052
+ });
1053
+ const existingRoute = this.routesById[childRoute.id];
1054
+ invariant(!existingRoute, `Duplicate routes found with id: ${String(childRoute.id)}`);
1055
+ this.routesById[childRoute.id] = childRoute;
1056
+ if (!childRoute.isRoot && childRoute.path) {
1057
+ const trimmedFullPath = trimPathRight(childRoute.fullPath);
1058
+ if (!this.routesByPath[trimmedFullPath] || childRoute.fullPath.endsWith('/')) {
1059
+ this.routesByPath[trimmedFullPath] = childRoute;
1060
+ }
1061
+ }
1062
+ const children = childRoute.children;
1063
+ if (children?.length) {
1064
+ recurseRoutes(children);
1065
+ }
1066
+ });
1067
+ };
1068
+ recurseRoutes([this.routeTree]);
1069
+ const scoredRoutes = [];
1070
+ Object.values(this.routesById).forEach((d, i) => {
1071
+ if (d.isRoot || !d.path) {
1072
+ return;
1073
+ }
1074
+ const trimmed = trimPathLeft(d.fullPath);
1075
+ const parsed = parsePathname(trimmed);
1076
+ while (parsed.length > 1 && parsed[0]?.value === '/') {
1077
+ parsed.shift();
1078
+ }
1079
+ const scores = parsed.map(d => {
1080
+ if (d.value === '/') {
1081
+ return 0.75;
1082
+ }
1083
+ if (d.type === 'param') {
1084
+ return 0.5;
1085
+ }
1086
+ if (d.type === 'wildcard') {
1087
+ return 0.25;
1088
+ }
1089
+ return 1;
1090
+ });
1091
+ scoredRoutes.push({
1092
+ child: d,
1093
+ trimmed,
1094
+ parsed,
1095
+ index: i,
1096
+ scores
1097
+ });
984
1098
  });
985
- };
986
- useRouteContext = opts => {
987
- return useMatch({
988
- ...opts,
989
- from: this.id,
990
- select: d => opts?.select ? opts.select(d.context) : d.context
1099
+ this.flatRoutes = scoredRoutes.sort((a, b) => {
1100
+ const minLength = Math.min(a.scores.length, b.scores.length);
1101
+
1102
+ // Sort by min available score
1103
+ for (let i = 0; i < minLength; i++) {
1104
+ if (a.scores[i] !== b.scores[i]) {
1105
+ return b.scores[i] - a.scores[i];
1106
+ }
1107
+ }
1108
+
1109
+ // Sort by length of score
1110
+ if (a.scores.length !== b.scores.length) {
1111
+ return b.scores.length - a.scores.length;
1112
+ }
1113
+
1114
+ // Sort by min available parsed value
1115
+ for (let i = 0; i < minLength; i++) {
1116
+ if (a.parsed[i].value !== b.parsed[i].value) {
1117
+ return a.parsed[i].value > b.parsed[i].value ? 1 : -1;
1118
+ }
1119
+ }
1120
+
1121
+ // Sort by original index
1122
+ return a.index - b.index;
1123
+ }).map((d, i) => {
1124
+ d.child.rank = i;
1125
+ return d.child;
991
1126
  });
992
1127
  };
993
- useSearch = opts => {
994
- return useSearch({
995
- ...opts,
996
- from: this.id
997
- });
1128
+ subscribe = (eventType, fn) => {
1129
+ const listener = {
1130
+ eventType,
1131
+ fn
1132
+ };
1133
+ this.subscribers.add(listener);
1134
+ return () => {
1135
+ this.subscribers.delete(listener);
1136
+ };
998
1137
  };
999
- useParams = opts => {
1000
- return useParams({
1001
- ...opts,
1002
- from: this.id
1138
+ emit = routerEvent => {
1139
+ this.subscribers.forEach(listener => {
1140
+ if (listener.eventType === routerEvent.type) {
1141
+ listener.fn(routerEvent);
1142
+ }
1003
1143
  });
1004
1144
  };
1005
- useLoaderDeps = opts => {
1006
- return useLoaderDeps({
1007
- ...opts,
1008
- from: this.id
1009
- });
1145
+ checkLatest = promise => {
1146
+ return this.latestLoadPromise !== promise ? this.latestLoadPromise : undefined;
1010
1147
  };
1011
- useLoaderData = opts => {
1012
- return useLoaderData({
1013
- ...opts,
1014
- from: this.id
1015
- });
1148
+ parseLocation = previousLocation => {
1149
+ const parse = ({
1150
+ pathname,
1151
+ search,
1152
+ hash,
1153
+ state
1154
+ }) => {
1155
+ const parsedSearch = this.options.parseSearch(search);
1156
+ return {
1157
+ pathname: pathname,
1158
+ searchStr: search,
1159
+ search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1160
+ hash: hash.split('#').reverse()[0] ?? '',
1161
+ href: `${pathname}${search}${hash}`,
1162
+ state: replaceEqualDeep(previousLocation?.state, state)
1163
+ };
1164
+ };
1165
+ const location = parse(this.history.location);
1166
+ let {
1167
+ __tempLocation,
1168
+ __tempKey
1169
+ } = location.state;
1170
+ if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
1171
+ // Sync up the location keys
1172
+ const parsedTempLocation = parse(__tempLocation);
1173
+ parsedTempLocation.state.key = location.state.key;
1174
+ delete parsedTempLocation.state.__tempLocation;
1175
+ return {
1176
+ ...parsedTempLocation,
1177
+ maskedLocation: location
1178
+ };
1179
+ }
1180
+ return location;
1016
1181
  };
1017
- }
1018
- function rootRouteWithContext() {
1019
- return options => {
1020
- return new RootRoute(options);
1182
+ resolvePathWithBase = (from, path) => {
1183
+ return resolvePath(this.basepath, from, cleanPath(path));
1021
1184
  };
1022
- }
1023
- class RootRoute extends Route {
1024
- constructor(options) {
1025
- super(options);
1185
+ get looseRoutesById() {
1186
+ return this.routesById;
1026
1187
  }
1027
- }
1028
- function createRouteMask(opts) {
1029
- return opts;
1030
- }
1031
-
1032
- //
1033
-
1034
- class NotFoundRoute extends Route {
1035
- constructor(options) {
1036
- super({
1037
- ...options,
1038
- id: '404'
1188
+ matchRoutes = (pathname, locationSearch, opts) => {
1189
+ let routeParams = {};
1190
+ let foundRoute = this.flatRoutes.find(route => {
1191
+ const matchedParams = matchPathname(this.basepath, trimPathRight(pathname), {
1192
+ to: route.fullPath,
1193
+ caseSensitive: route.options.caseSensitive ?? this.options.caseSensitive,
1194
+ fuzzy: true
1195
+ });
1196
+ if (matchedParams) {
1197
+ routeParams = matchedParams;
1198
+ return true;
1199
+ }
1200
+ return false;
1039
1201
  });
1040
- }
1041
- }
1042
-
1043
- class FileRoute {
1044
- constructor(path) {
1045
- this.path = path;
1046
- }
1047
- createRoute = options => {
1048
- const route = new Route(options);
1049
- route.isRoot = false;
1050
- return route;
1051
- };
1052
- }
1053
- function FileRouteLoader(_path) {
1054
- return loaderFn => loaderFn;
1055
- }
1202
+ let routeCursor = foundRoute || this.routesById['__root__'];
1203
+ let matchedRoutes = [routeCursor];
1056
1204
 
1057
- function lazyRouteComponent(importer, exportName) {
1058
- let loadPromise;
1059
- const load = () => {
1060
- if (!loadPromise) {
1061
- loadPromise = importer();
1205
+ // Check to see if the route needs a 404 entry
1206
+ if (
1207
+ // If we found a route, and it's not an index route and we have left over path
1208
+ (foundRoute ? foundRoute.path !== '/' && routeParams['**'] :
1209
+ // Or if we didn't find a route and we have left over path
1210
+ trimPathRight(pathname)) &&
1211
+ // And we have a 404 route configured
1212
+ this.options.notFoundRoute) {
1213
+ matchedRoutes.push(this.options.notFoundRoute);
1214
+ }
1215
+ while (routeCursor?.parentRoute) {
1216
+ routeCursor = routeCursor.parentRoute;
1217
+ if (routeCursor) matchedRoutes.unshift(routeCursor);
1062
1218
  }
1063
- return loadPromise;
1064
- };
1065
- const lazyComp = /*#__PURE__*/React.lazy(async () => {
1066
- const moduleExports = await load();
1067
- const comp = moduleExports[exportName ?? 'default'];
1068
- return {
1069
- default: comp
1070
- };
1071
- });
1072
- lazyComp.preload = load;
1073
- return lazyComp;
1074
- }
1075
1219
 
1076
- function _extends() {
1077
- _extends = Object.assign ? Object.assign.bind() : function (target) {
1078
- for (var i = 1; i < arguments.length; i++) {
1079
- var source = arguments[i];
1080
- for (var key in source) {
1081
- if (Object.prototype.hasOwnProperty.call(source, key)) {
1082
- target[key] = source[key];
1220
+ // Existing matches are matches that are already loaded along with
1221
+ // pending matches that are still loading
1222
+
1223
+ const parseErrors = matchedRoutes.map(route => {
1224
+ let parsedParamsError;
1225
+ if (route.options.parseParams) {
1226
+ try {
1227
+ const parsedParams = route.options.parseParams(routeParams);
1228
+ // Add the parsed params to the accumulated params bag
1229
+ Object.assign(routeParams, parsedParams);
1230
+ } catch (err) {
1231
+ parsedParamsError = new PathParamError(err.message, {
1232
+ cause: err
1233
+ });
1234
+ if (opts?.throwOnError) {
1235
+ throw parsedParamsError;
1236
+ }
1237
+ return parsedParamsError;
1083
1238
  }
1084
1239
  }
1085
- }
1086
- return target;
1087
- };
1088
- return _extends.apply(this, arguments);
1089
- }
1090
-
1091
- const preloadWarning = 'Error preloading route! ☝️';
1092
- function useLinkProps(options) {
1093
- const router = useRouter();
1094
- const matchPathname = useMatch({
1095
- strict: false,
1096
- select: s => s.pathname
1097
- });
1098
- const {
1099
- // custom props
1100
- children,
1101
- target,
1102
- activeProps = () => ({
1103
- className: 'active'
1104
- }),
1105
- inactiveProps = () => ({}),
1106
- activeOptions,
1107
- disabled,
1108
- hash,
1109
- search,
1110
- params,
1111
- to,
1112
- state,
1113
- mask,
1114
- preload: userPreload,
1115
- preloadDelay: userPreloadDelay,
1116
- replace,
1117
- startTransition,
1118
- resetScroll,
1119
- // element props
1120
- style,
1121
- className,
1122
- onClick,
1123
- onFocus,
1124
- onMouseEnter,
1125
- onMouseLeave,
1126
- onTouchStart,
1127
- ...rest
1128
- } = options;
1129
-
1130
- // If this link simply reloads the current route,
1131
- // make sure it has a new key so it will trigger a data refresh
1240
+ return;
1241
+ });
1242
+ const matches = [];
1243
+ matchedRoutes.forEach((route, index) => {
1244
+ // Take each matched route and resolve + validate its search params
1245
+ // This has to happen serially because each route's search params
1246
+ // can depend on the parent route's search params
1247
+ // It must also happen before we create the match so that we can
1248
+ // pass the search params to the route's potential key function
1249
+ // which is used to uniquely identify the route match in state
1132
1250
 
1133
- // If this `to` is a valid external URL, return
1134
- // null for LinkUtils
1251
+ const parentMatch = matches[index - 1];
1252
+ const [preMatchSearch, searchError] = (() => {
1253
+ // Validate the search params and stabilize them
1254
+ const parentSearch = parentMatch?.search ?? locationSearch;
1255
+ try {
1256
+ const validator = typeof route.options.validateSearch === 'object' ? route.options.validateSearch.parse : route.options.validateSearch;
1257
+ let search = validator?.(parentSearch) ?? {};
1258
+ return [{
1259
+ ...parentSearch,
1260
+ ...search
1261
+ }, undefined];
1262
+ } catch (err) {
1263
+ const searchError = new SearchParamError(err.message, {
1264
+ cause: err
1265
+ });
1266
+ if (opts?.throwOnError) {
1267
+ throw searchError;
1268
+ }
1269
+ return [parentSearch, searchError];
1270
+ }
1271
+ })();
1135
1272
 
1136
- const dest = {
1137
- from: options.to ? matchPathname : undefined,
1138
- ...options
1139
- };
1140
- let type = 'internal';
1141
- try {
1142
- new URL(`${to}`);
1143
- type = 'external';
1144
- } catch {}
1145
- if (type === 'external') {
1146
- return {
1147
- href: to
1148
- };
1149
- }
1150
- const next = router.buildLocation(dest);
1151
- const preload = userPreload ?? router.options.defaultPreload;
1152
- const preloadDelay = userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0;
1153
- const isActive = useRouterState({
1154
- select: s => {
1155
- // Compare path/hash for matches
1156
- const currentPathSplit = s.location.pathname.split('/');
1157
- const nextPathSplit = next.pathname.split('/');
1158
- const pathIsFuzzyEqual = nextPathSplit.every((d, i) => d === currentPathSplit[i]);
1159
- // Combine the matches based on user router.options
1160
- const pathTest = activeOptions?.exact ? s.location.pathname === next.pathname : pathIsFuzzyEqual;
1161
- const hashTest = activeOptions?.includeHash ? s.location.hash === next.hash : true;
1162
- const searchTest = activeOptions?.includeSearch ?? true ? deepEqual(s.location.search, next.search, !activeOptions?.exact) : true;
1273
+ // This is where we need to call route.options.loaderDeps() to get any additional
1274
+ // deps that the route's loader function might need to run. We need to do this
1275
+ // before we create the match so that we can pass the deps to the route's
1276
+ // potential key function which is used to uniquely identify the route match in state
1163
1277
 
1164
- // The final "active" test
1165
- return pathTest && hashTest && searchTest;
1166
- }
1167
- });
1278
+ const loaderDeps = route.options.loaderDeps?.({
1279
+ search: preMatchSearch
1280
+ }) ?? '';
1281
+ const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : '';
1282
+ const interpolatedPath = interpolatePath(route.fullPath, routeParams);
1283
+ const matchId = interpolatePath(route.id, routeParams, true) + loaderDepsHash;
1168
1284
 
1169
- // The click handler
1170
- const handleClick = e => {
1171
- if (!disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!target || target === '_self') && e.button === 0) {
1172
- e.preventDefault();
1285
+ // Waste not, want not. If we already have a match for this route,
1286
+ // reuse it. This is important for layout routes, which might stick
1287
+ // around between navigation actions that only change leaf routes.
1288
+ const existingMatch = getRouteMatch(this.state, matchId);
1289
+ const cause = this.state.matches.find(d => d.id === matchId) ? 'stay' : 'enter';
1173
1290
 
1174
- // All is well? Navigate!
1175
- router.commitLocation({
1176
- ...next,
1177
- replace,
1178
- resetScroll,
1179
- startTransition
1180
- });
1181
- }
1182
- };
1291
+ // Create a fresh route match
1292
+ const hasLoaders = !!(route.options.loader || componentTypes.some(d => route.options[d]?.preload));
1293
+ const match = existingMatch ? {
1294
+ ...existingMatch,
1295
+ cause
1296
+ } : {
1297
+ id: matchId,
1298
+ routeId: route.id,
1299
+ params: routeParams,
1300
+ pathname: joinPaths([this.basepath, interpolatedPath]),
1301
+ updatedAt: Date.now(),
1302
+ search: {},
1303
+ searchError: undefined,
1304
+ status: hasLoaders ? 'pending' : 'success',
1305
+ showPending: false,
1306
+ isFetching: false,
1307
+ error: undefined,
1308
+ paramsError: parseErrors[index],
1309
+ loadPromise: Promise.resolve(),
1310
+ routeContext: undefined,
1311
+ context: undefined,
1312
+ abortController: new AbortController(),
1313
+ fetchCount: 0,
1314
+ cause,
1315
+ loaderDeps,
1316
+ invalid: false,
1317
+ preload: false
1318
+ };
1183
1319
 
1184
- // The click handler
1185
- const handleFocus = e => {
1186
- if (preload) {
1187
- router.preloadRoute(dest).catch(err => {
1188
- console.warn(err);
1189
- console.warn(preloadWarning);
1190
- });
1191
- }
1192
- };
1193
- const handleTouchStart = e => {
1194
- if (preload) {
1195
- router.preloadRoute(dest).catch(err => {
1196
- console.warn(err);
1197
- console.warn(preloadWarning);
1198
- });
1199
- }
1200
- };
1201
- const handleEnter = e => {
1202
- const target = e.target || {};
1203
- if (preload) {
1204
- if (target.preloadTimeout) {
1205
- return;
1206
- }
1207
- target.preloadTimeout = setTimeout(() => {
1208
- target.preloadTimeout = null;
1209
- router.preloadRoute(dest).catch(err => {
1210
- console.warn(err);
1211
- console.warn(preloadWarning);
1212
- });
1213
- }, preloadDelay);
1214
- }
1320
+ // Regardless of whether we're reusing an existing match or creating
1321
+ // a new one, we need to update the match's search params
1322
+ match.search = replaceEqualDeep(match.search, preMatchSearch);
1323
+ // And also update the searchError if there is one
1324
+ match.searchError = searchError;
1325
+ matches.push(match);
1326
+ });
1327
+ return matches;
1215
1328
  };
1216
- const handleLeave = e => {
1217
- const target = e.target || {};
1218
- if (target.preloadTimeout) {
1219
- clearTimeout(target.preloadTimeout);
1220
- target.preloadTimeout = null;
1221
- }
1329
+ cancelMatch = id => {
1330
+ getRouteMatch(this.state, id)?.abortController?.abort();
1222
1331
  };
1223
- const composeHandlers = handlers => e => {
1224
- if (e.persist) e.persist();
1225
- handlers.filter(Boolean).forEach(handler => {
1226
- if (e.defaultPrevented) return;
1227
- handler(e);
1332
+ cancelMatches = () => {
1333
+ this.state.pendingMatches?.forEach(match => {
1334
+ this.cancelMatch(match.id);
1228
1335
  });
1229
1336
  };
1230
-
1231
- // Get the active props
1232
- const resolvedActiveProps = isActive ? functionalUpdate(activeProps, {}) ?? {} : {};
1233
-
1234
- // Get the inactive props
1235
- const resolvedInactiveProps = isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {};
1236
- return {
1237
- ...resolvedActiveProps,
1238
- ...resolvedInactiveProps,
1239
- ...rest,
1240
- href: disabled ? undefined : next.maskedLocation ? next.maskedLocation.href : next.href,
1241
- onClick: composeHandlers([onClick, handleClick]),
1242
- onFocus: composeHandlers([onFocus, handleFocus]),
1243
- onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
1244
- onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
1245
- onTouchStart: composeHandlers([onTouchStart, handleTouchStart]),
1246
- target,
1247
- style: {
1248
- ...style,
1249
- ...resolvedActiveProps.style,
1250
- ...resolvedInactiveProps.style
1251
- },
1252
- className: [className, resolvedActiveProps.className, resolvedInactiveProps.className].filter(Boolean).join(' ') || undefined,
1253
- ...(disabled ? {
1254
- role: 'link',
1255
- 'aria-disabled': true
1256
- } : undefined),
1257
- ['data-status']: isActive ? 'active' : undefined
1258
- };
1259
- }
1260
- const Link = /*#__PURE__*/React.forwardRef((props, ref) => {
1261
- const linkProps = useLinkProps(props);
1262
- return /*#__PURE__*/React.createElement("a", _extends({
1263
- ref: ref
1264
- }, linkProps, {
1265
- children: typeof props.children === 'function' ? props.children({
1266
- isActive: linkProps['data-status'] === 'active'
1267
- }) : props.children
1268
- }));
1269
- });
1270
- function isCtrlEvent(e) {
1271
- return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
1272
- }
1273
-
1274
- // @ts-nocheck
1275
-
1276
- // qss has been slightly modified and inlined here for our use cases (and compression's sake). We've included it as a hard dependency for MIT license attribution.
1277
-
1278
- function encode(obj, pfx) {
1279
- var k,
1280
- i,
1281
- tmp,
1282
- str = '';
1283
- for (k in obj) {
1284
- if ((tmp = obj[k]) !== void 0) {
1285
- if (Array.isArray(tmp)) {
1286
- for (i = 0; i < tmp.length; i++) {
1287
- str && (str += '&');
1288
- str += encodeURIComponent(k) + '=' + encodeURIComponent(tmp[i]);
1289
- }
1290
- } else {
1291
- str && (str += '&');
1292
- str += encodeURIComponent(k) + '=' + encodeURIComponent(tmp);
1337
+ buildLocation = opts => {
1338
+ const build = (dest = {}, matches) => {
1339
+ const relevantMatches = this.state.pendingMatches || this.state.matches;
1340
+ const fromSearch = relevantMatches[relevantMatches.length - 1]?.search || this.latestLocation.search;
1341
+ let pathname = this.resolvePathWithBase(dest.from ?? this.latestLocation.pathname, `${dest.to ?? ''}`);
1342
+ const fromMatches = this.matchRoutes(this.latestLocation.pathname, fromSearch);
1343
+ const stayingMatches = matches?.filter(d => fromMatches?.find(e => e.routeId === d.routeId));
1344
+ const prevParams = {
1345
+ ...last(fromMatches)?.params
1346
+ };
1347
+ let nextParams = (dest.params ?? true) === true ? prevParams : functionalUpdate(dest.params, prevParams);
1348
+ if (nextParams) {
1349
+ matches?.map(d => this.looseRoutesById[d.routeId].options.stringifyParams).filter(Boolean).forEach(fn => {
1350
+ nextParams = {
1351
+ ...nextParams,
1352
+ ...fn(nextParams)
1353
+ };
1354
+ });
1293
1355
  }
1294
- }
1295
- }
1296
- return (pfx || '') + str;
1297
- }
1298
- function toValue(mix) {
1299
- if (!mix) return '';
1300
- var str = decodeURIComponent(mix);
1301
- if (str === 'false') return false;
1302
- if (str === 'true') return true;
1303
- return +str * 0 === 0 && +str + '' === str ? +str : str;
1304
- }
1305
- function decode(str) {
1306
- var tmp,
1307
- k,
1308
- out = {},
1309
- arr = str.split('&');
1310
- while (tmp = arr.shift()) {
1311
- tmp = tmp.split('=');
1312
- k = tmp.shift();
1313
- if (out[k] !== void 0) {
1314
- out[k] = [].concat(out[k], toValue(tmp.shift()));
1315
- } else {
1316
- out[k] = toValue(tmp.shift());
1317
- }
1318
- }
1319
- return out;
1320
- }
1321
-
1322
- // Detect if we're in the DOM
1356
+ pathname = interpolatePath(pathname, nextParams ?? {});
1357
+ const preSearchFilters = stayingMatches?.map(match => this.looseRoutesById[match.routeId].options.preSearchFilters ?? []).flat().filter(Boolean) ?? [];
1358
+ const postSearchFilters = stayingMatches?.map(match => this.looseRoutesById[match.routeId].options.postSearchFilters ?? []).flat().filter(Boolean) ?? [];
1323
1359
 
1324
- function redirect(opts) {
1325
- opts.isRedirect = true;
1326
- if (opts.throw) {
1327
- throw opts;
1328
- }
1329
- return opts;
1330
- }
1331
- function isRedirect(obj) {
1332
- return !!obj?.isRedirect;
1333
- }
1360
+ // Pre filters first
1361
+ const preFilteredSearch = preSearchFilters?.length ? preSearchFilters?.reduce((prev, next) => next(prev), fromSearch) : fromSearch;
1334
1362
 
1335
- const defaultParseSearch = parseSearchWith(JSON.parse);
1336
- const defaultStringifySearch = stringifySearchWith(JSON.stringify, JSON.parse);
1337
- function parseSearchWith(parser) {
1338
- return searchStr => {
1339
- if (searchStr.substring(0, 1) === '?') {
1340
- searchStr = searchStr.substring(1);
1341
- }
1342
- let query = decode(searchStr);
1363
+ // Then the link/navigate function
1364
+ const destSearch = dest.search === true ? preFilteredSearch // Preserve resolvedFrom true
1365
+ : dest.search ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1366
+ : preSearchFilters?.length ? preFilteredSearch // Preserve resolvedFrom filters
1367
+ : {};
1343
1368
 
1344
- // Try to parse any query params that might be json
1345
- for (let key in query) {
1346
- const value = query[key];
1347
- if (typeof value === 'string') {
1348
- try {
1349
- query[key] = parser(value);
1350
- } catch (err) {
1351
- //
1369
+ // Then post filters
1370
+ const postFilteredSearch = postSearchFilters?.length ? postSearchFilters.reduce((prev, next) => next(prev), destSearch) : destSearch;
1371
+ const search = replaceEqualDeep(fromSearch, postFilteredSearch);
1372
+ const searchStr = this.options.stringifySearch(search);
1373
+ const hash = dest.hash === true ? this.latestLocation.hash : dest.hash ? functionalUpdate(dest.hash, this.latestLocation.hash) : undefined;
1374
+ const hashStr = hash ? `#${hash}` : '';
1375
+ let nextState = dest.state === true ? this.latestLocation.state : dest.state ? functionalUpdate(dest.state, this.latestLocation.state) : this.latestLocation.state;
1376
+ nextState = replaceEqualDeep(this.latestLocation.state, nextState);
1377
+ return {
1378
+ pathname,
1379
+ search,
1380
+ searchStr,
1381
+ state: nextState,
1382
+ hash: hash ?? '',
1383
+ href: `${pathname}${searchStr}${hashStr}`,
1384
+ unmaskOnReload: dest.unmaskOnReload
1385
+ };
1386
+ };
1387
+ const buildWithMatches = (dest = {}, maskedDest) => {
1388
+ let next = build(dest);
1389
+ let maskedNext = maskedDest ? build(maskedDest) : undefined;
1390
+ if (!maskedNext) {
1391
+ let params = {};
1392
+ let foundMask = this.options.routeMasks?.find(d => {
1393
+ const match = matchPathname(this.basepath, next.pathname, {
1394
+ to: d.from,
1395
+ caseSensitive: false,
1396
+ fuzzy: false
1397
+ });
1398
+ if (match) {
1399
+ params = match;
1400
+ return true;
1401
+ }
1402
+ return false;
1403
+ });
1404
+ if (foundMask) {
1405
+ maskedDest = {
1406
+ ...pick(opts, ['from']),
1407
+ ...foundMask,
1408
+ params
1409
+ };
1410
+ maskedNext = build(maskedDest);
1352
1411
  }
1353
1412
  }
1354
- }
1355
- return query;
1356
- };
1357
- }
1358
- function stringifySearchWith(stringify, parser) {
1359
- function stringifyValue(val) {
1360
- if (typeof val === 'object' && val !== null) {
1361
- try {
1362
- return stringify(val);
1363
- } catch (err) {
1364
- // silent
1365
- }
1366
- } else if (typeof val === 'string' && typeof parser === 'function') {
1367
- try {
1368
- // Check if it's a valid parseable string.
1369
- // If it is, then stringify it again.
1370
- parser(val);
1371
- return stringify(val);
1372
- } catch (err) {
1373
- // silent
1413
+ const nextMatches = this.matchRoutes(next.pathname, next.search);
1414
+ const maskedMatches = maskedNext ? this.matchRoutes(maskedNext.pathname, maskedNext.search) : undefined;
1415
+ const maskedFinal = maskedNext ? build(maskedDest, maskedMatches) : undefined;
1416
+ const final = build(dest, nextMatches);
1417
+ if (maskedFinal) {
1418
+ final.maskedLocation = maskedFinal;
1374
1419
  }
1375
- }
1376
- return val;
1377
- }
1378
- return search => {
1379
- search = {
1380
- ...search
1420
+ return final;
1381
1421
  };
1382
- if (search) {
1383
- Object.keys(search).forEach(key => {
1384
- const val = search[key];
1385
- if (typeof val === 'undefined' || val === undefined) {
1386
- delete search[key];
1387
- } else {
1388
- search[key] = stringifyValue(val);
1389
- }
1422
+ if (opts.mask) {
1423
+ return buildWithMatches(opts, {
1424
+ ...pick(opts, ['from']),
1425
+ ...opts.mask
1390
1426
  });
1391
1427
  }
1392
- const searchStr = encode(search).toString();
1393
- return searchStr ? `?${searchStr}` : '';
1428
+ return buildWithMatches(opts);
1394
1429
  };
1395
- }
1430
+ commitLocation = async ({
1431
+ startTransition,
1432
+ ...next
1433
+ }) => {
1434
+ if (this.navigateTimeout) clearTimeout(this.navigateTimeout);
1435
+ const isSameUrl = this.latestLocation.href === next.href;
1396
1436
 
1397
- const useTransition = React.useTransition || (() => [false, cb => {
1398
- cb();
1399
- }]);
1400
- function RouterProvider({
1401
- router,
1402
- ...rest
1403
- }) {
1404
- // Allow the router to update options on the router instance
1405
- router.update({
1406
- ...router.options,
1407
- ...rest,
1408
- context: {
1409
- ...router.options.context,
1410
- ...rest?.context
1411
- }
1412
- });
1413
- const matches = router.options.InnerWrap ? /*#__PURE__*/React.createElement(router.options.InnerWrap, null, /*#__PURE__*/React.createElement(Matches, null)) : /*#__PURE__*/React.createElement(Matches, null);
1414
- const provider = /*#__PURE__*/React.createElement(routerContext.Provider, {
1415
- value: router
1416
- }, matches, /*#__PURE__*/React.createElement(Transitioner, null));
1417
- if (router.options.Wrap) {
1418
- return /*#__PURE__*/React.createElement(router.options.Wrap, null, provider);
1419
- }
1420
- return provider;
1421
- }
1422
- function Transitioner() {
1423
- const mountLoadCount = React.useRef(0);
1424
- const router = useRouter();
1425
- const routerState = useRouterState({
1426
- select: s => pick(s, ['isLoading', 'location', 'resolvedLocation', 'isTransitioning'])
1427
- });
1428
- const [isTransitioning, startReactTransition] = useTransition();
1429
- router.startReactTransition = startReactTransition;
1430
- React.useEffect(() => {
1431
- if (isTransitioning) {
1432
- router.__store.setState(s => ({
1433
- ...s,
1434
- isTransitioning
1435
- }));
1436
- }
1437
- }, [isTransitioning]);
1438
- const tryLoad = () => {
1439
- const apply = cb => {
1440
- if (!routerState.isTransitioning) {
1441
- startReactTransition(() => cb());
1442
- } else {
1443
- cb();
1437
+ // If the next urls are the same and we're not replacing,
1438
+ // do nothing
1439
+ if (!isSameUrl || !next.replace) {
1440
+ let {
1441
+ maskedLocation,
1442
+ ...nextHistory
1443
+ } = next;
1444
+ if (maskedLocation) {
1445
+ nextHistory = {
1446
+ ...maskedLocation,
1447
+ state: {
1448
+ ...maskedLocation.state,
1449
+ __tempKey: undefined,
1450
+ __tempLocation: {
1451
+ ...nextHistory,
1452
+ search: nextHistory.searchStr,
1453
+ state: {
1454
+ ...nextHistory.state,
1455
+ __tempKey: undefined,
1456
+ __tempLocation: undefined,
1457
+ key: undefined
1458
+ }
1459
+ }
1460
+ }
1461
+ };
1462
+ if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
1463
+ nextHistory.state.__tempKey = this.tempLocationKey;
1464
+ }
1444
1465
  }
1445
- };
1446
- apply(() => {
1447
- try {
1448
- router.load();
1449
- } catch (err) {
1450
- console.error(err);
1466
+ const apply = () => {
1467
+ this.history[next.replace ? 'replace' : 'push'](nextHistory.href, nextHistory.state);
1468
+ };
1469
+ if (startTransition ?? true) {
1470
+ this.startReactTransition(apply);
1471
+ } else {
1472
+ apply();
1451
1473
  }
1452
- });
1474
+ }
1475
+ this.resetNextScroll = next.resetScroll ?? true;
1476
+ return this.latestLoadPromise;
1453
1477
  };
1454
- useLayoutEffect$1(() => {
1455
- const unsub = router.history.subscribe(() => {
1456
- router.latestLocation = router.parseLocation(router.latestLocation);
1457
- if (routerState.location !== router.latestLocation) {
1458
- tryLoad();
1459
- }
1478
+ buildAndCommitLocation = ({
1479
+ replace,
1480
+ resetScroll,
1481
+ startTransition,
1482
+ ...rest
1483
+ } = {}) => {
1484
+ const location = this.buildLocation(rest);
1485
+ return this.commitLocation({
1486
+ ...location,
1487
+ startTransition,
1488
+ replace,
1489
+ resetScroll
1460
1490
  });
1461
- const nextLocation = router.buildLocation({
1462
- search: true,
1463
- params: true,
1464
- hash: true,
1465
- state: true
1491
+ };
1492
+ navigate = ({
1493
+ from,
1494
+ to,
1495
+ ...rest
1496
+ }) => {
1497
+ // If this link simply reloads the current route,
1498
+ // make sure it has a new key so it will trigger a data refresh
1499
+
1500
+ // If this `to` is a valid external URL, return
1501
+ // null for LinkUtils
1502
+ const toString = String(to);
1503
+ // const fromString = from !== undefined ? String(from) : from
1504
+ let isExternal;
1505
+ try {
1506
+ new URL(`${toString}`);
1507
+ isExternal = true;
1508
+ } catch (e) {}
1509
+ invariant(!isExternal, 'Attempting to navigate to external url with this.navigate!');
1510
+ return this.buildAndCommitLocation({
1511
+ ...rest,
1512
+ from,
1513
+ to
1514
+ // to: toString,
1466
1515
  });
1467
- if (routerState.location.href !== nextLocation.href) {
1468
- router.commitLocation({
1469
- ...nextLocation,
1470
- replace: true
1471
- });
1472
- }
1473
- return () => {
1474
- unsub();
1516
+ };
1517
+ loadMatches = async ({
1518
+ checkLatest,
1519
+ matches,
1520
+ preload
1521
+ }) => {
1522
+ let latestPromise;
1523
+ let firstBadMatchIndex;
1524
+ const updateMatch = match => {
1525
+ // const isPreload = this.state.cachedMatches.find((d) => d.id === match.id)
1526
+ const isPending = this.state.pendingMatches?.find(d => d.id === match.id);
1527
+ const isMatched = this.state.matches.find(d => d.id === match.id);
1528
+ const matchesKey = isPending ? 'pendingMatches' : isMatched ? 'matches' : 'cachedMatches';
1529
+ this.__store.setState(s => ({
1530
+ ...s,
1531
+ [matchesKey]: s[matchesKey]?.map(d => d.id === match.id ? match : d)
1532
+ }));
1475
1533
  };
1476
- }, [router.history]);
1477
- useLayoutEffect$1(() => {
1478
- if (React.useTransition ? routerState.isTransitioning && !isTransitioning : !routerState.isLoading && routerState.resolvedLocation !== routerState.location) {
1479
- router.emit({
1480
- type: 'onResolved',
1481
- fromLocation: routerState.resolvedLocation,
1482
- toLocation: routerState.location,
1483
- pathChanged: routerState.location.href !== routerState.resolvedLocation?.href
1484
- });
1485
- if (document.querySelector) {
1486
- if (routerState.location.hash !== '') {
1487
- const el = document.getElementById(routerState.location.hash);
1488
- if (el) {
1489
- el.scrollIntoView();
1534
+
1535
+ // Check each match middleware to see if the route can be accessed
1536
+ try {
1537
+ for (let [index, match] of matches.entries()) {
1538
+ const parentMatch = matches[index - 1];
1539
+ const route = this.looseRoutesById[match.routeId];
1540
+ const abortController = new AbortController();
1541
+ const handleErrorAndRedirect = (err, code) => {
1542
+ err.routerCode = code;
1543
+ firstBadMatchIndex = firstBadMatchIndex ?? index;
1544
+ if (isRedirect(err)) {
1545
+ throw err;
1546
+ }
1547
+ try {
1548
+ route.options.onError?.(err);
1549
+ } catch (errorHandlerErr) {
1550
+ err = errorHandlerErr;
1551
+ if (isRedirect(errorHandlerErr)) {
1552
+ throw errorHandlerErr;
1553
+ }
1554
+ }
1555
+ matches[index] = match = {
1556
+ ...match,
1557
+ error: err,
1558
+ status: 'error',
1559
+ updatedAt: Date.now(),
1560
+ abortController: new AbortController()
1561
+ };
1562
+ };
1563
+ try {
1564
+ if (match.paramsError) {
1565
+ handleErrorAndRedirect(match.paramsError, 'PARSE_PARAMS');
1566
+ }
1567
+ if (match.searchError) {
1568
+ handleErrorAndRedirect(match.searchError, 'VALIDATE_SEARCH');
1569
+ }
1570
+ const parentContext = parentMatch?.context ?? this.options.context ?? {};
1571
+ const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
1572
+ const pendingPromise = typeof pendingMs === 'number' && pendingMs <= 0 ? Promise.resolve() : new Promise(r => setTimeout(r, pendingMs));
1573
+ const beforeLoadContext = (await route.options.beforeLoad?.({
1574
+ search: match.search,
1575
+ abortController,
1576
+ params: match.params,
1577
+ preload: !!preload,
1578
+ context: parentContext,
1579
+ location: this.state.location,
1580
+ // TOOD: just expose state and router, etc
1581
+ navigate: opts => this.navigate({
1582
+ ...opts,
1583
+ from: match.pathname
1584
+ }),
1585
+ buildLocation: this.buildLocation,
1586
+ cause: preload ? 'preload' : match.cause
1587
+ })) ?? {};
1588
+ if (isRedirect(beforeLoadContext)) {
1589
+ throw beforeLoadContext;
1490
1590
  }
1591
+ const context = {
1592
+ ...parentContext,
1593
+ ...beforeLoadContext
1594
+ };
1595
+ matches[index] = match = {
1596
+ ...match,
1597
+ routeContext: replaceEqualDeep(match.routeContext, beforeLoadContext),
1598
+ context: replaceEqualDeep(match.context, context),
1599
+ abortController,
1600
+ pendingPromise
1601
+ };
1602
+ } catch (err) {
1603
+ handleErrorAndRedirect(err, 'BEFORE_LOAD');
1604
+ break;
1491
1605
  }
1492
1606
  }
1493
- router.__store.setState(s => ({
1494
- ...s,
1495
- isTransitioning: false,
1496
- resolvedLocation: s.location
1497
- }));
1498
- }
1499
- }, [routerState.isTransitioning, isTransitioning, routerState.isLoading, routerState.resolvedLocation, routerState.location]);
1500
- useLayoutEffect$1(() => {
1501
- if (!window.__TSR_DEHYDRATED__ && !mountLoadCount.current) {
1502
- mountLoadCount.current++;
1503
- tryLoad();
1607
+ } catch (err) {
1608
+ if (isRedirect(err)) {
1609
+ if (!preload) this.navigate(err);
1610
+ return matches;
1611
+ }
1612
+ throw err;
1504
1613
  }
1505
- }, []);
1506
- return null;
1507
- }
1508
- function getRouteMatch(state, id) {
1509
- return [...state.cachedMatches, ...(state.pendingMatches ?? []), ...state.matches].find(d => d.id === id);
1510
- }
1511
-
1512
- // import warning from 'tiny-warning'
1513
-
1514
- //
1515
-
1516
- const componentTypes = ['component', 'errorComponent', 'pendingComponent'];
1517
- class Router {
1518
- // Option-independent properties
1519
- tempLocationKey = `${Math.round(Math.random() * 10000000)}`;
1520
- resetNextScroll = true;
1521
- navigateTimeout = null;
1522
- latestLoadPromise = Promise.resolve();
1523
- subscribers = new Set();
1524
- injectedHtml = [];
1525
-
1526
- // Must build in constructor
1614
+ const validResolvedMatches = matches.slice(0, firstBadMatchIndex);
1615
+ const matchPromises = [];
1616
+ validResolvedMatches.forEach((match, index) => {
1617
+ matchPromises.push(new Promise(async resolve => {
1618
+ const parentMatchPromise = matchPromises[index - 1];
1619
+ const route = this.looseRoutesById[match.routeId];
1620
+ const handleErrorAndRedirect = err => {
1621
+ if (isRedirect(err)) {
1622
+ if (!preload) {
1623
+ this.navigate(err);
1624
+ }
1625
+ return true;
1626
+ }
1627
+ return false;
1628
+ };
1629
+ let loadPromise;
1630
+ matches[index] = match = {
1631
+ ...match,
1632
+ showPending: false
1633
+ };
1634
+ let didShowPending = false;
1635
+ const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
1636
+ const pendingMinMs = route.options.pendingMinMs ?? this.options.defaultPendingMinMs;
1637
+ const shouldPending = !preload && typeof pendingMs === 'number' && (route.options.pendingComponent ?? this.options.defaultPendingComponent);
1638
+ const loaderContext = {
1639
+ params: match.params,
1640
+ deps: match.loaderDeps,
1641
+ preload: !!preload,
1642
+ parentMatchPromise,
1643
+ abortController: match.abortController,
1644
+ context: match.context,
1645
+ location: this.state.location,
1646
+ navigate: opts => this.navigate({
1647
+ ...opts,
1648
+ from: match.pathname
1649
+ }),
1650
+ cause: preload ? 'preload' : match.cause
1651
+ };
1652
+ const fetch = async () => {
1653
+ if (match.isFetching) {
1654
+ loadPromise = getRouteMatch(this.state, match.id)?.loadPromise;
1655
+ } else {
1656
+ // If the user doesn't want the route to reload, just
1657
+ // resolve with the existing loader data
1527
1658
 
1528
- constructor(options) {
1529
- this.update({
1530
- defaultPreloadDelay: 50,
1531
- defaultPendingMs: 1000,
1532
- defaultPendingMinMs: 500,
1533
- context: undefined,
1534
- ...options,
1535
- stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
1536
- parseSearch: options?.parseSearch ?? defaultParseSearch
1537
- });
1538
- }
1659
+ if (match.fetchCount && match.status === 'success') {
1660
+ resolve();
1661
+ }
1539
1662
 
1540
- // These are default implementations that can optionally be overridden
1541
- // by the router provider once rendered. We provide these so that the
1542
- // router can be used in a non-react environment if necessary
1543
- startReactTransition = fn => fn();
1544
- update = newOptions => {
1545
- const previousOptions = this.options;
1546
- this.options = {
1547
- ...this.options,
1548
- ...newOptions
1549
- };
1550
- if (!this.basepath || newOptions.basepath && newOptions.basepath !== previousOptions.basepath) {
1551
- if (newOptions.basepath === undefined || newOptions.basepath === '' || newOptions.basepath === '/') {
1552
- this.basepath = '/';
1553
- } else {
1554
- this.basepath = `/${trimPath(newOptions.basepath)}`;
1555
- }
1556
- }
1557
- if (!this.history || this.options.history && this.options.history !== this.history) {
1558
- this.history = this.options.history ?? (typeof document !== 'undefined' ? createBrowserHistory() : createMemoryHistory({
1559
- initialEntries: [this.options.basepath || '/']
1560
- }));
1561
- this.latestLocation = this.parseLocation();
1562
- }
1563
- if (this.options.routeTree !== this.routeTree) {
1564
- this.routeTree = this.options.routeTree;
1565
- this.buildRouteTree();
1566
- }
1567
- if (!this.__store) {
1568
- this.__store = new Store(getInitialRouterState(this.latestLocation), {
1569
- onUpdate: () => {
1570
- this.__store.state = {
1571
- ...this.state,
1572
- status: this.state.isTransitioning || this.state.isLoading ? 'pending' : 'idle'
1663
+ // Otherwise, load the route
1664
+ matches[index] = match = {
1665
+ ...match,
1666
+ isFetching: true,
1667
+ fetchCount: match.fetchCount + 1
1668
+ };
1669
+ const componentsPromise = Promise.all(componentTypes.map(async type => {
1670
+ const component = route.options[type];
1671
+ if (component?.preload) {
1672
+ await component.preload();
1673
+ }
1674
+ }));
1675
+ const loaderPromise = route.options.loader?.(loaderContext);
1676
+ loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
1677
+ }
1678
+ matches[index] = match = {
1679
+ ...match,
1680
+ loadPromise
1573
1681
  };
1574
- }
1575
- });
1576
- }
1577
- };
1578
- get state() {
1579
- return this.__store.state;
1580
- }
1581
- buildRouteTree = () => {
1582
- this.routesById = {};
1583
- this.routesByPath = {};
1584
- const notFoundRoute = this.options.notFoundRoute;
1585
- if (notFoundRoute) {
1586
- notFoundRoute.init({
1587
- originalIndex: 99999999999
1588
- });
1589
- this.routesById[notFoundRoute.id] = notFoundRoute;
1590
- }
1591
- const recurseRoutes = childRoutes => {
1592
- childRoutes.forEach((childRoute, i) => {
1593
- childRoute.init({
1594
- originalIndex: i
1595
- });
1596
- const existingRoute = this.routesById[childRoute.id];
1597
- invariant(!existingRoute, `Duplicate routes found with id: ${String(childRoute.id)}`);
1598
- this.routesById[childRoute.id] = childRoute;
1599
- if (!childRoute.isRoot && childRoute.path) {
1600
- const trimmedFullPath = trimPathRight(childRoute.fullPath);
1601
- if (!this.routesByPath[trimmedFullPath] || childRoute.fullPath.endsWith('/')) {
1602
- this.routesByPath[trimmedFullPath] = childRoute;
1682
+ updateMatch(match);
1683
+ try {
1684
+ const loaderData = await loadPromise;
1685
+ if (latestPromise = checkLatest()) return await latestPromise;
1686
+ if (isRedirect(loaderData)) {
1687
+ if (handleErrorAndRedirect(loaderData)) return;
1688
+ }
1689
+ if (didShowPending && pendingMinMs) {
1690
+ await new Promise(r => setTimeout(r, pendingMinMs));
1691
+ }
1692
+ if (latestPromise = checkLatest()) return await latestPromise;
1693
+ matches[index] = match = {
1694
+ ...match,
1695
+ error: undefined,
1696
+ status: 'success',
1697
+ isFetching: false,
1698
+ updatedAt: Date.now(),
1699
+ loaderData,
1700
+ loadPromise: undefined
1701
+ };
1702
+ } catch (error) {
1703
+ if (latestPromise = checkLatest()) return await latestPromise;
1704
+ if (handleErrorAndRedirect(error)) return;
1705
+ try {
1706
+ route.options.onError?.(error);
1707
+ } catch (onErrorError) {
1708
+ error = onErrorError;
1709
+ if (handleErrorAndRedirect(onErrorError)) return;
1710
+ }
1711
+ matches[index] = match = {
1712
+ ...match,
1713
+ error,
1714
+ status: 'error',
1715
+ isFetching: false
1716
+ };
1603
1717
  }
1604
- }
1605
- const children = childRoute.children;
1606
- if (children?.length) {
1607
- recurseRoutes(children);
1608
- }
1609
- });
1610
- };
1611
- recurseRoutes([this.routeTree]);
1612
- const scoredRoutes = [];
1613
- Object.values(this.routesById).forEach((d, i) => {
1614
- if (d.isRoot || !d.path) {
1615
- return;
1616
- }
1617
- const trimmed = trimPathLeft(d.fullPath);
1618
- const parsed = parsePathname(trimmed);
1619
- while (parsed.length > 1 && parsed[0]?.value === '/') {
1620
- parsed.shift();
1621
- }
1622
- const scores = parsed.map(d => {
1623
- if (d.value === '/') {
1624
- return 0.75;
1625
- }
1626
- if (d.type === 'param') {
1627
- return 0.5;
1628
- }
1629
- if (d.type === 'wildcard') {
1630
- return 0.25;
1631
- }
1632
- return 1;
1633
- });
1634
- scoredRoutes.push({
1635
- child: d,
1636
- trimmed,
1637
- parsed,
1638
- index: i,
1639
- scores
1640
- });
1641
- });
1642
- this.flatRoutes = scoredRoutes.sort((a, b) => {
1643
- const minLength = Math.min(a.scores.length, b.scores.length);
1644
-
1645
- // Sort by min available score
1646
- for (let i = 0; i < minLength; i++) {
1647
- if (a.scores[i] !== b.scores[i]) {
1648
- return b.scores[i] - a.scores[i];
1649
- }
1650
- }
1718
+ updateMatch(match);
1719
+ };
1651
1720
 
1652
- // Sort by length of score
1653
- if (a.scores.length !== b.scores.length) {
1654
- return b.scores.length - a.scores.length;
1655
- }
1721
+ // This is where all of the stale-while-revalidate magic happens
1722
+ const age = Date.now() - match.updatedAt;
1723
+ let staleAge = preload ? route.options.preloadStaleTime ?? this.options.defaultPreloadStaleTime ?? 30_000 // 30 seconds for preloads by default
1724
+ : route.options.staleTime ?? this.options.defaultStaleTime ?? 0;
1656
1725
 
1657
- // Sort by min available parsed value
1658
- for (let i = 0; i < minLength; i++) {
1659
- if (a.parsed[i].value !== b.parsed[i].value) {
1660
- return a.parsed[i].value > b.parsed[i].value ? 1 : -1;
1661
- }
1662
- }
1726
+ // Default to reloading the route all the time
1727
+ let shouldReload;
1728
+ const shouldReloadOption = route.options.shouldReload;
1663
1729
 
1664
- // Sort by original index
1665
- return a.index - b.index;
1666
- }).map((d, i) => {
1667
- d.child.rank = i;
1668
- return d.child;
1669
- });
1670
- };
1671
- subscribe = (eventType, fn) => {
1672
- const listener = {
1673
- eventType,
1674
- fn
1675
- };
1676
- this.subscribers.add(listener);
1677
- return () => {
1678
- this.subscribers.delete(listener);
1679
- };
1680
- };
1681
- emit = routerEvent => {
1682
- this.subscribers.forEach(listener => {
1683
- if (listener.eventType === routerEvent.type) {
1684
- listener.fn(routerEvent);
1685
- }
1730
+ // Allow shouldReload to get the last say,
1731
+ // if provided.
1732
+ shouldReload = typeof shouldReloadOption === 'function' ? shouldReloadOption(loaderContext) : shouldReloadOption;
1733
+ matches[index] = match = {
1734
+ ...match,
1735
+ preload: !!preload && !this.state.matches.find(d => d.id === match.id)
1736
+ };
1737
+ if (match.status !== 'success') {
1738
+ // If we need to potentially show the pending component,
1739
+ // start a timer to show it after the pendingMs
1740
+ if (shouldPending) {
1741
+ match.pendingPromise?.then(async () => {
1742
+ if (latestPromise = checkLatest()) return latestPromise;
1743
+ didShowPending = true;
1744
+ matches[index] = match = {
1745
+ ...match,
1746
+ showPending: true
1747
+ };
1748
+ updateMatch(match);
1749
+ resolve();
1750
+ });
1751
+ }
1752
+
1753
+ // Critical Fetching, we need to await
1754
+ await fetch();
1755
+ } else if (match.invalid || (shouldReload ?? age > staleAge)) {
1756
+ // Background Fetching, no need to wait
1757
+ fetch();
1758
+ }
1759
+ resolve();
1760
+ }));
1686
1761
  });
1762
+ await Promise.all(matchPromises);
1763
+ return matches;
1687
1764
  };
1688
- checkLatest = promise => {
1689
- return this.latestLoadPromise !== promise ? this.latestLoadPromise : undefined;
1690
- };
1691
- parseLocation = previousLocation => {
1692
- const parse = ({
1693
- pathname,
1694
- search,
1695
- hash,
1696
- state
1697
- }) => {
1698
- const parsedSearch = this.options.parseSearch(search);
1699
- return {
1700
- pathname: pathname,
1701
- searchStr: search,
1702
- search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1703
- hash: hash.split('#').reverse()[0] ?? '',
1704
- href: `${pathname}${search}${hash}`,
1705
- state: replaceEqualDeep(previousLocation?.state, state)
1706
- };
1707
- };
1708
- const location = parse(this.history.location);
1709
- let {
1710
- __tempLocation,
1711
- __tempKey
1712
- } = location.state;
1713
- if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
1714
- // Sync up the location keys
1715
- const parsedTempLocation = parse(__tempLocation);
1716
- parsedTempLocation.state.key = location.state.key;
1717
- delete parsedTempLocation.state.__tempLocation;
1718
- return {
1719
- ...parsedTempLocation,
1720
- maskedLocation: location
1721
- };
1722
- }
1723
- return location;
1724
- };
1725
- resolvePathWithBase = (from, path) => {
1726
- return resolvePath(this.basepath, from, cleanPath(path));
1727
- };
1728
- get looseRoutesById() {
1729
- return this.routesById;
1730
- }
1731
- matchRoutes = (pathname, locationSearch, opts) => {
1732
- let routeParams = {};
1733
- let foundRoute = this.flatRoutes.find(route => {
1734
- const matchedParams = matchPathname(this.basepath, trimPathRight(pathname), {
1735
- to: route.fullPath,
1736
- caseSensitive: route.options.caseSensitive ?? this.options.caseSensitive,
1737
- fuzzy: true
1738
- });
1739
- if (matchedParams) {
1740
- routeParams = matchedParams;
1741
- return true;
1742
- }
1743
- return false;
1765
+ invalidate = () => {
1766
+ const invalidate = d => ({
1767
+ ...d,
1768
+ invalid: true
1744
1769
  });
1745
- let routeCursor = foundRoute || this.routesById['__root__'];
1746
- let matchedRoutes = [routeCursor];
1770
+ this.__store.setState(s => ({
1771
+ ...s,
1772
+ matches: s.matches.map(invalidate),
1773
+ cachedMatches: s.cachedMatches.map(invalidate),
1774
+ pendingMatches: s.pendingMatches?.map(invalidate)
1775
+ }));
1776
+ this.load();
1777
+ };
1778
+ load = async () => {
1779
+ const promise = new Promise(async (resolve, reject) => {
1780
+ const next = this.latestLocation;
1781
+ const prevLocation = this.state.resolvedLocation;
1782
+ const pathDidChange = prevLocation.href !== next.href;
1783
+ let latestPromise;
1747
1784
 
1748
- // Check to see if the route needs a 404 entry
1749
- if (
1750
- // If we found a route, and it's not an index route and we have left over path
1751
- (foundRoute ? foundRoute.path !== '/' && routeParams['**'] :
1752
- // Or if we didn't find a route and we have left over path
1753
- trimPathRight(pathname)) &&
1754
- // And we have a 404 route configured
1755
- this.options.notFoundRoute) {
1756
- matchedRoutes.push(this.options.notFoundRoute);
1757
- }
1758
- while (routeCursor?.parentRoute) {
1759
- routeCursor = routeCursor.parentRoute;
1760
- if (routeCursor) matchedRoutes.unshift(routeCursor);
1761
- }
1785
+ // Cancel any pending matches
1786
+ this.cancelMatches();
1787
+ this.emit({
1788
+ type: 'onBeforeLoad',
1789
+ fromLocation: prevLocation,
1790
+ toLocation: next,
1791
+ pathChanged: pathDidChange
1792
+ });
1793
+ let pendingMatches;
1794
+ const previousMatches = this.state.matches;
1795
+ this.__store.batch(() => {
1796
+ this.cleanCache();
1762
1797
 
1763
- // Existing matches are matches that are already loaded along with
1764
- // pending matches that are still loading
1798
+ // Match the routes
1799
+ pendingMatches = this.matchRoutes(next.pathname, next.search, {
1800
+ debug: true
1801
+ });
1765
1802
 
1766
- const parseErrors = matchedRoutes.map(route => {
1767
- let parsedParamsError;
1768
- if (route.options.parseParams) {
1803
+ // Ingest the new matches
1804
+ // If a cached moved to pendingMatches, remove it from cachedMatches
1805
+ this.__store.setState(s => ({
1806
+ ...s,
1807
+ isLoading: true,
1808
+ location: next,
1809
+ pendingMatches,
1810
+ cachedMatches: s.cachedMatches.filter(d => {
1811
+ return !pendingMatches.find(e => e.id === d.id);
1812
+ })
1813
+ }));
1814
+ });
1815
+ try {
1769
1816
  try {
1770
- const parsedParams = route.options.parseParams(routeParams);
1771
- // Add the parsed params to the accumulated params bag
1772
- Object.assign(routeParams, parsedParams);
1773
- } catch (err) {
1774
- parsedParamsError = new PathParamError(err.message, {
1775
- cause: err
1817
+ // Load the matches
1818
+ await this.loadMatches({
1819
+ matches: pendingMatches,
1820
+ checkLatest: () => this.checkLatest(promise)
1776
1821
  });
1777
- if (opts?.throwOnError) {
1778
- throw parsedParamsError;
1779
- }
1780
- return parsedParamsError;
1781
- }
1782
- }
1783
- return;
1784
- });
1785
- const matches = [];
1786
- matchedRoutes.forEach((route, index) => {
1787
- // Take each matched route and resolve + validate its search params
1788
- // This has to happen serially because each route's search params
1789
- // can depend on the parent route's search params
1790
- // It must also happen before we create the match so that we can
1791
- // pass the search params to the route's potential key function
1792
- // which is used to uniquely identify the route match in state
1793
-
1794
- const parentMatch = matches[index - 1];
1795
- const [preMatchSearch, searchError] = (() => {
1796
- // Validate the search params and stabilize them
1797
- const parentSearch = parentMatch?.search ?? locationSearch;
1798
- try {
1799
- const validator = typeof route.options.validateSearch === 'object' ? route.options.validateSearch.parse : route.options.validateSearch;
1800
- let search = validator?.(parentSearch) ?? {};
1801
- return [{
1802
- ...parentSearch,
1803
- ...search
1804
- }, undefined];
1805
1822
  } catch (err) {
1806
- const searchError = new SearchParamError(err.message, {
1807
- cause: err
1808
- });
1809
- if (opts?.throwOnError) {
1810
- throw searchError;
1811
- }
1812
- return [parentSearch, searchError];
1823
+ // swallow this error, since we'll display the
1824
+ // errors on the route components
1813
1825
  }
1814
- })();
1815
-
1816
- // This is where we need to call route.options.loaderDeps() to get any additional
1817
- // deps that the route's loader function might need to run. We need to do this
1818
- // before we create the match so that we can pass the deps to the route's
1819
- // potential key function which is used to uniquely identify the route match in state
1820
1826
 
1821
- const loaderDeps = route.options.loaderDeps?.({
1822
- search: preMatchSearch
1823
- }) ?? '';
1824
- const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : '';
1825
- const interpolatedPath = interpolatePath(route.fullPath, routeParams);
1826
- const matchId = interpolatePath(route.id, routeParams, true) + loaderDepsHash;
1827
+ // Only apply the latest transition
1828
+ if (latestPromise = this.checkLatest(promise)) {
1829
+ return latestPromise;
1830
+ }
1831
+ const exitingMatches = previousMatches.filter(match => !pendingMatches.find(d => d.id === match.id));
1832
+ const enteringMatches = pendingMatches.filter(match => !previousMatches.find(d => d.id === match.id));
1833
+ const stayingMatches = previousMatches.filter(match => pendingMatches.find(d => d.id === match.id));
1827
1834
 
1828
- // Waste not, want not. If we already have a match for this route,
1829
- // reuse it. This is important for layout routes, which might stick
1830
- // around between navigation actions that only change leaf routes.
1831
- const existingMatch = getRouteMatch(this.state, matchId);
1832
- const cause = this.state.matches.find(d => d.id === matchId) ? 'stay' : 'enter';
1835
+ // Commit the pending matches. If a previous match was
1836
+ // removed, place it in the cachedMatches
1837
+ this.__store.batch(() => {
1838
+ this.__store.setState(s => ({
1839
+ ...s,
1840
+ isLoading: false,
1841
+ matches: s.pendingMatches,
1842
+ pendingMatches: undefined,
1843
+ cachedMatches: [...s.cachedMatches, ...exitingMatches.filter(d => d.status !== 'error')]
1844
+ }));
1845
+ this.cleanCache();
1846
+ })
1833
1847
 
1834
- // Create a fresh route match
1835
- const hasLoaders = !!(route.options.loader || componentTypes.some(d => route.options[d]?.preload));
1836
- const match = existingMatch ? {
1837
- ...existingMatch,
1838
- cause
1839
- } : {
1840
- id: matchId,
1841
- routeId: route.id,
1842
- params: routeParams,
1843
- pathname: joinPaths([this.basepath, interpolatedPath]),
1844
- updatedAt: Date.now(),
1845
- search: {},
1846
- searchError: undefined,
1847
- status: hasLoaders ? 'pending' : 'success',
1848
- showPending: false,
1849
- isFetching: false,
1850
- error: undefined,
1851
- paramsError: parseErrors[index],
1852
- loadPromise: Promise.resolve(),
1853
- routeContext: undefined,
1854
- context: undefined,
1855
- abortController: new AbortController(),
1856
- fetchCount: 0,
1857
- cause,
1858
- loaderDeps,
1859
- invalid: false,
1860
- preload: false
1861
- };
1862
-
1863
- // Regardless of whether we're reusing an existing match or creating
1864
- // a new one, we need to update the match's search params
1865
- match.search = replaceEqualDeep(match.search, preMatchSearch);
1866
- // And also update the searchError if there is one
1867
- match.searchError = searchError;
1868
- matches.push(match);
1869
- });
1870
- return matches;
1871
- };
1872
- cancelMatch = id => {
1873
- getRouteMatch(this.state, id)?.abortController?.abort();
1874
- };
1875
- cancelMatches = () => {
1876
- this.state.pendingMatches?.forEach(match => {
1877
- this.cancelMatch(match.id);
1878
- });
1879
- };
1880
- buildLocation = opts => {
1881
- const build = (dest = {}, matches) => {
1882
- const relevantMatches = this.state.pendingMatches || this.state.matches;
1883
- const fromSearch = relevantMatches[relevantMatches.length - 1]?.search || this.latestLocation.search;
1884
- let pathname = this.resolvePathWithBase(dest.from ?? this.latestLocation.pathname, `${dest.to ?? ''}`);
1885
- const fromMatches = this.matchRoutes(this.latestLocation.pathname, fromSearch);
1886
- const stayingMatches = matches?.filter(d => fromMatches?.find(e => e.routeId === d.routeId));
1887
- const prevParams = {
1888
- ...last(fromMatches)?.params
1889
- };
1890
- let nextParams = (dest.params ?? true) === true ? prevParams : functionalUpdate(dest.params, prevParams);
1891
- if (nextParams) {
1892
- matches?.map(d => this.looseRoutesById[d.routeId].options.stringifyParams).filter(Boolean).forEach(fn => {
1893
- nextParams = {
1894
- ...nextParams,
1895
- ...fn(nextParams)
1896
- };
1897
- });
1898
- }
1899
- pathname = interpolatePath(pathname, nextParams ?? {});
1900
- const preSearchFilters = stayingMatches?.map(match => this.looseRoutesById[match.routeId].options.preSearchFilters ?? []).flat().filter(Boolean) ?? [];
1901
- const postSearchFilters = stayingMatches?.map(match => this.looseRoutesById[match.routeId].options.postSearchFilters ?? []).flat().filter(Boolean) ?? [];
1902
-
1903
- // Pre filters first
1904
- const preFilteredSearch = preSearchFilters?.length ? preSearchFilters?.reduce((prev, next) => next(prev), fromSearch) : fromSearch;
1905
-
1906
- // Then the link/navigate function
1907
- const destSearch = dest.search === true ? preFilteredSearch // Preserve resolvedFrom true
1908
- : dest.search ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1909
- : preSearchFilters?.length ? preFilteredSearch // Preserve resolvedFrom filters
1910
- : {};
1911
-
1912
- // Then post filters
1913
- const postFilteredSearch = postSearchFilters?.length ? postSearchFilters.reduce((prev, next) => next(prev), destSearch) : destSearch;
1914
- const search = replaceEqualDeep(fromSearch, postFilteredSearch);
1915
- const searchStr = this.options.stringifySearch(search);
1916
- const hash = dest.hash === true ? this.latestLocation.hash : dest.hash ? functionalUpdate(dest.hash, this.latestLocation.hash) : undefined;
1917
- const hashStr = hash ? `#${hash}` : '';
1918
- let nextState = dest.state === true ? this.latestLocation.state : dest.state ? functionalUpdate(dest.state, this.latestLocation.state) : this.latestLocation.state;
1919
- nextState = replaceEqualDeep(this.latestLocation.state, nextState);
1920
- return {
1921
- pathname,
1922
- search,
1923
- searchStr,
1924
- state: nextState,
1925
- hash: hash ?? '',
1926
- href: `${pathname}${searchStr}${hashStr}`,
1927
- unmaskOnReload: dest.unmaskOnReload
1928
- };
1929
- };
1930
- const buildWithMatches = (dest = {}, maskedDest) => {
1931
- let next = build(dest);
1932
- let maskedNext = maskedDest ? build(maskedDest) : undefined;
1933
- if (!maskedNext) {
1934
- let params = {};
1935
- let foundMask = this.options.routeMasks?.find(d => {
1936
- const match = matchPathname(this.basepath, next.pathname, {
1937
- to: d.from,
1938
- caseSensitive: false,
1939
- fuzzy: false
1848
+ //
1849
+ ;
1850
+ [[exitingMatches, 'onLeave'], [enteringMatches, 'onEnter'], [stayingMatches, 'onStay']].forEach(([matches, hook]) => {
1851
+ matches.forEach(match => {
1852
+ this.looseRoutesById[match.routeId].options[hook]?.(match);
1940
1853
  });
1941
- if (match) {
1942
- params = match;
1943
- return true;
1944
- }
1945
- return false;
1946
1854
  });
1947
- if (foundMask) {
1948
- maskedDest = {
1949
- ...pick(opts, ['from']),
1950
- ...foundMask,
1951
- params
1952
- };
1953
- maskedNext = build(maskedDest);
1855
+ this.emit({
1856
+ type: 'onLoad',
1857
+ fromLocation: prevLocation,
1858
+ toLocation: next,
1859
+ pathChanged: pathDidChange
1860
+ });
1861
+ resolve();
1862
+ } catch (err) {
1863
+ // Only apply the latest transition
1864
+ if (latestPromise = this.checkLatest(promise)) {
1865
+ return latestPromise;
1954
1866
  }
1867
+ reject(err);
1955
1868
  }
1956
- const nextMatches = this.matchRoutes(next.pathname, next.search);
1957
- const maskedMatches = maskedNext ? this.matchRoutes(maskedNext.pathname, maskedNext.search) : undefined;
1958
- const maskedFinal = maskedNext ? build(maskedDest, maskedMatches) : undefined;
1959
- const final = build(dest, nextMatches);
1960
- if (maskedFinal) {
1961
- final.maskedLocation = maskedFinal;
1962
- }
1963
- return final;
1964
- };
1965
- if (opts.mask) {
1966
- return buildWithMatches(opts, {
1967
- ...pick(opts, ['from']),
1968
- ...opts.mask
1969
- });
1970
- }
1971
- return buildWithMatches(opts);
1869
+ });
1870
+ this.latestLoadPromise = promise;
1871
+ return this.latestLoadPromise;
1972
1872
  };
1973
- commitLocation = async ({
1974
- startTransition,
1975
- ...next
1976
- }) => {
1977
- if (this.navigateTimeout) clearTimeout(this.navigateTimeout);
1978
- const isSameUrl = this.latestLocation.href === next.href;
1979
-
1980
- // If the next urls are the same and we're not replacing,
1981
- // do nothing
1982
- if (!isSameUrl || !next.replace) {
1983
- let {
1984
- maskedLocation,
1985
- ...nextHistory
1986
- } = next;
1987
- if (maskedLocation) {
1988
- nextHistory = {
1989
- ...maskedLocation,
1990
- state: {
1991
- ...maskedLocation.state,
1992
- __tempKey: undefined,
1993
- __tempLocation: {
1994
- ...nextHistory,
1995
- search: nextHistory.searchStr,
1996
- state: {
1997
- ...nextHistory.state,
1998
- __tempKey: undefined,
1999
- __tempLocation: undefined,
2000
- key: undefined
2001
- }
2002
- }
1873
+ cleanCache = () => {
1874
+ // This is where all of the garbage collection magic happens
1875
+ this.__store.setState(s => {
1876
+ return {
1877
+ ...s,
1878
+ cachedMatches: s.cachedMatches.filter(d => {
1879
+ const route = this.looseRoutesById[d.routeId];
1880
+ if (!route.options.loader) {
1881
+ return false;
2003
1882
  }
2004
- };
2005
- if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
2006
- nextHistory.state.__tempKey = this.tempLocationKey;
2007
- }
2008
- }
2009
- const apply = () => {
2010
- this.history[next.replace ? 'replace' : 'push'](nextHistory.href, nextHistory.state);
1883
+
1884
+ // If the route was preloaded, use the preloadGcTime
1885
+ // otherwise, use the gcTime
1886
+ const gcTime = (d.preload ? route.options.preloadGcTime ?? this.options.defaultPreloadGcTime : route.options.gcTime ?? this.options.defaultGcTime) ?? 5 * 60 * 1000;
1887
+ return d.status !== 'error' && Date.now() - d.updatedAt < gcTime;
1888
+ })
2011
1889
  };
2012
- if (startTransition ?? true) {
2013
- this.startReactTransition(apply);
2014
- } else {
2015
- apply();
2016
- }
2017
- }
2018
- this.resetNextScroll = next.resetScroll ?? true;
2019
- return this.latestLoadPromise;
2020
- };
2021
- buildAndCommitLocation = ({
2022
- replace,
2023
- resetScroll,
2024
- startTransition,
2025
- ...rest
2026
- } = {}) => {
2027
- const location = this.buildLocation(rest);
2028
- return this.commitLocation({
2029
- ...location,
2030
- startTransition,
2031
- replace,
2032
- resetScroll
2033
1890
  });
2034
1891
  };
2035
- navigate = ({
2036
- from,
2037
- to,
2038
- ...rest
2039
- }) => {
2040
- // If this link simply reloads the current route,
2041
- // make sure it has a new key so it will trigger a data refresh
2042
-
2043
- // If this `to` is a valid external URL, return
2044
- // null for LinkUtils
2045
- const toString = String(to);
2046
- // const fromString = from !== undefined ? String(from) : from
2047
- let isExternal;
2048
- try {
2049
- new URL(`${toString}`);
2050
- isExternal = true;
2051
- } catch (e) {}
2052
- invariant(!isExternal, 'Attempting to navigate to external url with this.navigate!');
2053
- return this.buildAndCommitLocation({
2054
- ...rest,
2055
- from,
2056
- to
2057
- // to: toString,
1892
+ preloadRoute = async (navigateOpts = this.state.location) => {
1893
+ let next = this.buildLocation(navigateOpts);
1894
+ let matches = this.matchRoutes(next.pathname, next.search, {
1895
+ throwOnError: true
2058
1896
  });
2059
- };
2060
- loadMatches = async ({
2061
- checkLatest,
2062
- matches,
2063
- preload
2064
- }) => {
2065
- let latestPromise;
2066
- let firstBadMatchIndex;
2067
- const updateMatch = match => {
2068
- // const isPreload = this.state.cachedMatches.find((d) => d.id === match.id)
2069
- const isPending = this.state.pendingMatches?.find(d => d.id === match.id);
2070
- const isMatched = this.state.matches.find(d => d.id === match.id);
2071
- const matchesKey = isPending ? 'pendingMatches' : isMatched ? 'matches' : 'cachedMatches';
2072
- this.__store.setState(s => ({
2073
- ...s,
2074
- [matchesKey]: s[matchesKey]?.map(d => d.id === match.id ? match : d)
2075
- }));
2076
- };
2077
-
2078
- // Check each match middleware to see if the route can be accessed
2079
- try {
2080
- for (let [index, match] of matches.entries()) {
2081
- const parentMatch = matches[index - 1];
2082
- const route = this.looseRoutesById[match.routeId];
2083
- const abortController = new AbortController();
2084
- const handleErrorAndRedirect = (err, code) => {
2085
- err.routerCode = code;
2086
- firstBadMatchIndex = firstBadMatchIndex ?? index;
2087
- if (isRedirect(err)) {
2088
- throw err;
2089
- }
2090
- try {
2091
- route.options.onError?.(err);
2092
- } catch (errorHandlerErr) {
2093
- err = errorHandlerErr;
2094
- if (isRedirect(errorHandlerErr)) {
2095
- throw errorHandlerErr;
2096
- }
2097
- }
2098
- matches[index] = match = {
2099
- ...match,
2100
- error: err,
2101
- status: 'error',
2102
- updatedAt: Date.now(),
2103
- abortController: new AbortController()
2104
- };
2105
- };
2106
- try {
2107
- if (match.paramsError) {
2108
- handleErrorAndRedirect(match.paramsError, 'PARSE_PARAMS');
2109
- }
2110
- if (match.searchError) {
2111
- handleErrorAndRedirect(match.searchError, 'VALIDATE_SEARCH');
2112
- }
2113
- const parentContext = parentMatch?.context ?? this.options.context ?? {};
2114
- const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
2115
- const pendingPromise = typeof pendingMs === 'number' && pendingMs <= 0 ? Promise.resolve() : new Promise(r => setTimeout(r, pendingMs));
2116
- const beforeLoadContext = (await route.options.beforeLoad?.({
2117
- search: match.search,
2118
- abortController,
2119
- params: match.params,
2120
- preload: !!preload,
2121
- context: parentContext,
2122
- location: this.state.location,
2123
- // TOOD: just expose state and router, etc
2124
- navigate: opts => this.navigate({
2125
- ...opts,
2126
- from: match.pathname
2127
- }),
2128
- buildLocation: this.buildLocation,
2129
- cause: preload ? 'preload' : match.cause
2130
- })) ?? {};
2131
- if (isRedirect(beforeLoadContext)) {
2132
- throw beforeLoadContext;
2133
- }
2134
- const context = {
2135
- ...parentContext,
2136
- ...beforeLoadContext
2137
- };
2138
- matches[index] = match = {
2139
- ...match,
2140
- routeContext: replaceEqualDeep(match.routeContext, beforeLoadContext),
2141
- context: replaceEqualDeep(match.context, context),
2142
- abortController,
2143
- pendingPromise
2144
- };
2145
- } catch (err) {
2146
- handleErrorAndRedirect(err, 'BEFORE_LOAD');
2147
- break;
1897
+ const loadedMatchIds = Object.fromEntries([...this.state.matches, ...(this.state.pendingMatches ?? []), ...this.state.cachedMatches]?.map(d => [d.id, true]));
1898
+ this.__store.batch(() => {
1899
+ matches.forEach(match => {
1900
+ if (!loadedMatchIds[match.id]) {
1901
+ this.__store.setState(s => ({
1902
+ ...s,
1903
+ cachedMatches: [...s.cachedMatches, match]
1904
+ }));
2148
1905
  }
1906
+ });
1907
+ });
1908
+ matches = await this.loadMatches({
1909
+ matches,
1910
+ preload: true,
1911
+ checkLatest: () => undefined
1912
+ });
1913
+ return matches;
1914
+ };
1915
+ matchRoute = (location, opts) => {
1916
+ location = {
1917
+ ...location,
1918
+ to: location.to ? this.resolvePathWithBase(location.from || '', location.to) : undefined
1919
+ };
1920
+ const next = this.buildLocation(location);
1921
+ if (opts?.pending && this.state.status !== 'pending') {
1922
+ return false;
1923
+ }
1924
+ const baseLocation = opts?.pending ? this.latestLocation : this.state.resolvedLocation;
1925
+ if (!baseLocation) {
1926
+ return false;
1927
+ }
1928
+ const match = matchPathname(this.basepath, baseLocation.pathname, {
1929
+ ...opts,
1930
+ to: next.pathname
1931
+ });
1932
+ if (!match) {
1933
+ return false;
1934
+ }
1935
+ if (match && (opts?.includeSearch ?? true)) {
1936
+ return deepEqual(baseLocation.search, next.search, true) ? match : false;
1937
+ }
1938
+ return match;
1939
+ };
1940
+ injectHtml = async html => {
1941
+ this.injectedHtml.push(html);
1942
+ };
1943
+ dehydrateData = (key, getData) => {
1944
+ if (typeof document === 'undefined') {
1945
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key);
1946
+ this.injectHtml(async () => {
1947
+ const id = `__TSR_DEHYDRATED__${strKey}`;
1948
+ const data = typeof getData === 'function' ? await getData() : getData;
1949
+ return `<script id='${id}' suppressHydrationWarning>window["__TSR_DEHYDRATED__${escapeJSON(strKey)}"] = ${JSON.stringify(data)}
1950
+ ;(() => {
1951
+ var el = document.getElementById('${id}')
1952
+ el.parentElement.removeChild(el)
1953
+ })()
1954
+ </script>`;
1955
+ });
1956
+ return () => this.hydrateData(key);
1957
+ }
1958
+ return () => undefined;
1959
+ };
1960
+ hydrateData = key => {
1961
+ if (typeof document !== 'undefined') {
1962
+ const strKey = typeof key === 'string' ? key : JSON.stringify(key);
1963
+ return window[`__TSR_DEHYDRATED__${strKey}`];
1964
+ }
1965
+ return undefined;
1966
+ };
1967
+ dehydrate = () => {
1968
+ const pickError = this.options.errorSerializer?.serialize ?? defaultSerializeError;
1969
+ return {
1970
+ state: {
1971
+ dehydratedMatches: this.state.matches.map(d => ({
1972
+ ...pick(d, ['id', 'status', 'updatedAt', 'loaderData']),
1973
+ // If an error occurs server-side during SSRing,
1974
+ // send a small subset of the error to the client
1975
+ error: d.error ? {
1976
+ data: pickError(d.error),
1977
+ __isServerError: true
1978
+ } : undefined
1979
+ }))
2149
1980
  }
2150
- } catch (err) {
2151
- if (isRedirect(err)) {
2152
- if (!preload) this.navigate(err);
2153
- return matches;
2154
- }
2155
- throw err;
1981
+ };
1982
+ };
1983
+ hydrate = async __do_not_use_server_ctx => {
1984
+ let _ctx = __do_not_use_server_ctx;
1985
+ // Client hydrates from window
1986
+ if (typeof document !== 'undefined') {
1987
+ _ctx = window.__TSR_DEHYDRATED__;
2156
1988
  }
2157
- const validResolvedMatches = matches.slice(0, firstBadMatchIndex);
2158
- const matchPromises = [];
2159
- validResolvedMatches.forEach((match, index) => {
2160
- matchPromises.push(new Promise(async resolve => {
2161
- const parentMatchPromise = matchPromises[index - 1];
2162
- const route = this.looseRoutesById[match.routeId];
2163
- const handleErrorAndRedirect = err => {
2164
- if (isRedirect(err)) {
2165
- if (!preload) {
2166
- this.navigate(err);
2167
- }
2168
- return true;
2169
- }
2170
- return false;
2171
- };
2172
- let loadPromise;
2173
- matches[index] = match = {
1989
+ invariant(_ctx, 'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?');
1990
+ const ctx = _ctx;
1991
+ this.dehydratedData = ctx.payload;
1992
+ this.options.hydrate?.(ctx.payload);
1993
+ const dehydratedState = ctx.router.state;
1994
+ let matches = this.matchRoutes(this.state.location.pathname, this.state.location.search).map(match => {
1995
+ const dehydratedMatch = dehydratedState.dehydratedMatches.find(d => d.id === match.id);
1996
+ invariant(dehydratedMatch, `Could not find a client-side match for dehydrated match with id: ${match.id}!`);
1997
+ if (dehydratedMatch) {
1998
+ return {
2174
1999
  ...match,
2175
- showPending: false
2176
- };
2177
- let didShowPending = false;
2178
- const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs;
2179
- const pendingMinMs = route.options.pendingMinMs ?? this.options.defaultPendingMinMs;
2180
- const shouldPending = !preload && typeof pendingMs === 'number' && (route.options.pendingComponent ?? this.options.defaultPendingComponent);
2181
- const loaderContext = {
2182
- params: match.params,
2183
- deps: match.loaderDeps,
2184
- preload: !!preload,
2185
- parentMatchPromise,
2186
- abortController: match.abortController,
2187
- context: match.context,
2188
- location: this.state.location,
2189
- navigate: opts => this.navigate({
2190
- ...opts,
2191
- from: match.pathname
2192
- }),
2193
- cause: preload ? 'preload' : match.cause
2000
+ ...dehydratedMatch
2194
2001
  };
2195
- const fetch = async () => {
2196
- if (match.isFetching) {
2197
- loadPromise = getRouteMatch(this.state, match.id)?.loadPromise;
2198
- } else {
2199
- // If the user doesn't want the route to reload, just
2200
- // resolve with the existing loader data
2002
+ }
2003
+ return match;
2004
+ });
2005
+ this.__store.setState(s => {
2006
+ return {
2007
+ ...s,
2008
+ matches: matches
2009
+ };
2010
+ });
2011
+ };
2201
2012
 
2202
- if (match.fetchCount && match.status === 'success') {
2203
- resolve();
2204
- }
2013
+ // resolveMatchPromise = (matchId: string, key: string, value: any) => {
2014
+ // state.matches
2015
+ // .find((d) => d.id === matchId)
2016
+ // ?.__promisesByKey[key]?.resolve(value)
2017
+ // }
2018
+ }
2205
2019
 
2206
- // Otherwise, load the route
2207
- matches[index] = match = {
2208
- ...match,
2209
- isFetching: true,
2210
- fetchCount: match.fetchCount + 1
2211
- };
2212
- const componentsPromise = Promise.all(componentTypes.map(async type => {
2213
- const component = route.options[type];
2214
- if (component?.preload) {
2215
- await component.preload();
2216
- }
2217
- }));
2218
- const loaderPromise = route.options.loader?.(loaderContext);
2219
- loadPromise = Promise.all([componentsPromise, loaderPromise]).then(d => d[1]);
2220
- }
2221
- matches[index] = match = {
2222
- ...match,
2223
- loadPromise
2224
- };
2225
- updateMatch(match);
2226
- try {
2227
- const loaderData = await loadPromise;
2228
- if (latestPromise = checkLatest()) return await latestPromise;
2229
- if (isRedirect(loaderData)) {
2230
- if (handleErrorAndRedirect(loaderData)) return;
2231
- }
2232
- if (didShowPending && pendingMinMs) {
2233
- await new Promise(r => setTimeout(r, pendingMinMs));
2234
- }
2235
- if (latestPromise = checkLatest()) return await latestPromise;
2236
- matches[index] = match = {
2237
- ...match,
2238
- error: undefined,
2239
- status: 'success',
2240
- isFetching: false,
2241
- updatedAt: Date.now(),
2242
- loaderData,
2243
- loadPromise: undefined
2244
- };
2245
- } catch (error) {
2246
- if (latestPromise = checkLatest()) return await latestPromise;
2247
- if (handleErrorAndRedirect(error)) return;
2248
- try {
2249
- route.options.onError?.(error);
2250
- } catch (onErrorError) {
2251
- error = onErrorError;
2252
- if (handleErrorAndRedirect(onErrorError)) return;
2253
- }
2254
- matches[index] = match = {
2255
- ...match,
2256
- error,
2257
- status: 'error',
2258
- isFetching: false
2259
- };
2260
- }
2261
- updateMatch(match);
2262
- };
2020
+ // A function that takes an import() argument which is a function and returns a new function that will
2021
+ // proxy arguments from the caller to the imported function, retaining all type
2022
+ // information along the way
2023
+ function lazyFn(fn, key) {
2024
+ return async (...args) => {
2025
+ const imported = await fn();
2026
+ return imported[key || 'default'](...args);
2027
+ };
2028
+ }
2029
+ class SearchParamError extends Error {}
2030
+ class PathParamError extends Error {}
2031
+ function getInitialRouterState(location) {
2032
+ return {
2033
+ isLoading: false,
2034
+ isTransitioning: false,
2035
+ status: 'idle',
2036
+ resolvedLocation: {
2037
+ ...location
2038
+ },
2039
+ location,
2040
+ matches: [],
2041
+ pendingMatches: [],
2042
+ cachedMatches: [],
2043
+ lastUpdated: Date.now()
2044
+ };
2045
+ }
2046
+ function defaultSerializeError(err) {
2047
+ if (err instanceof Error) return {
2048
+ name: err.name,
2049
+ message: err.message
2050
+ };
2051
+ return {
2052
+ data: err
2053
+ };
2054
+ }
2263
2055
 
2264
- // This is where all of the stale-while-revalidate magic happens
2265
- const age = Date.now() - match.updatedAt;
2266
- let staleAge = preload ? route.options.preloadStaleTime ?? this.options.defaultPreloadStaleTime ?? 30_000 // 30 seconds for preloads by default
2267
- : route.options.staleTime ?? this.options.defaultStaleTime ?? 0;
2056
+ function defer(_promise, options) {
2057
+ const promise = _promise;
2058
+ if (!promise.__deferredState) {
2059
+ promise.__deferredState = {
2060
+ uid: Math.random().toString(36).slice(2),
2061
+ status: 'pending'
2062
+ };
2063
+ const state = promise.__deferredState;
2064
+ promise.then(data => {
2065
+ state.status = 'success';
2066
+ state.data = data;
2067
+ }).catch(error => {
2068
+ state.status = 'error';
2069
+ state.error = {
2070
+ data: (options?.serializeError ?? defaultSerializeError)(error),
2071
+ __isServerError: true
2072
+ };
2073
+ });
2074
+ }
2075
+ return promise;
2076
+ }
2077
+ function isDehydratedDeferred(obj) {
2078
+ return typeof obj === 'object' && obj !== null && !(obj instanceof Promise) && !obj.then && '__deferredState' in obj;
2079
+ }
2268
2080
 
2269
- // Default to reloading the route all the time
2270
- let shouldReload;
2271
- const shouldReloadOption = route.options.shouldReload;
2081
+ function useAwaited({
2082
+ promise
2083
+ }) {
2084
+ const router = useRouter();
2085
+ let state = promise.__deferredState;
2086
+ const key = `__TSR__DEFERRED__${state.uid}`;
2087
+ if (isDehydratedDeferred(promise)) {
2088
+ state = router.hydrateData(key);
2089
+ if (!state) throw new Error('Could not find dehydrated data');
2090
+ promise = Promise.resolve(state.data);
2091
+ promise.__deferredState = state;
2092
+ }
2093
+ if (state.status === 'pending') {
2094
+ throw promise;
2095
+ }
2096
+ if (state.status === 'error') {
2097
+ if (typeof document !== 'undefined') {
2098
+ if (isServerSideError(state.error)) {
2099
+ throw (router.options.errorSerializer?.deserialize ?? defaultDeserializeError)(state.error.data);
2100
+ } else {
2101
+ warning(false, "Encountered a server-side error that doesn't fit the expected shape");
2102
+ throw state.error;
2103
+ }
2104
+ } else {
2105
+ router.dehydrateData(key, state);
2106
+ throw {
2107
+ data: (router.options.errorSerializer?.serialize ?? defaultSerializeError)(state.error),
2108
+ __isServerError: true
2109
+ };
2110
+ }
2111
+ }
2112
+ router.dehydrateData(key, state);
2113
+ return [state.data];
2114
+ }
2115
+ function Await(props) {
2116
+ const awaited = useAwaited(props);
2117
+ return props.children(...awaited);
2118
+ }
2272
2119
 
2273
- // Allow shouldReload to get the last say,
2274
- // if provided.
2275
- shouldReload = typeof shouldReloadOption === 'function' ? shouldReloadOption(loaderContext) : shouldReloadOption;
2276
- matches[index] = match = {
2277
- ...match,
2278
- preload: !!preload && !this.state.matches.find(d => d.id === match.id)
2279
- };
2280
- if (match.status !== 'success') {
2281
- // If we need to potentially show the pending component,
2282
- // start a timer to show it after the pendingMs
2283
- if (shouldPending) {
2284
- match.pendingPromise?.then(async () => {
2285
- if (latestPromise = checkLatest()) return latestPromise;
2286
- didShowPending = true;
2287
- matches[index] = match = {
2288
- ...match,
2289
- showPending: true
2290
- };
2291
- updateMatch(match);
2292
- resolve();
2293
- });
2294
- }
2120
+ function useParams(opts) {
2121
+ return useRouterState({
2122
+ select: state => {
2123
+ const params = last(getRenderedMatches(state))?.params;
2124
+ return opts?.select ? opts.select(params) : params;
2125
+ }
2126
+ });
2127
+ }
2295
2128
 
2296
- // Critical Fetching, we need to await
2297
- await fetch();
2298
- } else if (match.invalid || (shouldReload ?? age > staleAge)) {
2299
- // Background Fetching, no need to wait
2300
- fetch();
2301
- }
2302
- resolve();
2303
- }));
2129
+ function useSearch(opts) {
2130
+ return useMatch({
2131
+ ...opts,
2132
+ select: match => {
2133
+ return opts?.select ? opts.select(match.search) : match.search;
2134
+ }
2135
+ });
2136
+ }
2137
+
2138
+ const rootRouteId = '__root__';
2139
+
2140
+ // The parse type here allows a zod schema to be passed directly to the validator
2141
+
2142
+ // TODO: This is part of a future APi to move away from classes and
2143
+ // towards a more functional API. It's not ready yet.
2144
+
2145
+ // type RouteApiInstance<
2146
+ // TId extends RouteIds<RegisteredRouter['routeTree']>,
2147
+ // TRoute extends AnyRoute = RouteById<RegisteredRouter['routeTree'], TId>,
2148
+ // TFullSearchSchema extends Record<
2149
+ // string,
2150
+ // any
2151
+ // > = TRoute['types']['fullSearchSchema'],
2152
+ // TAllParams extends AnyPathParams = TRoute['types']['allParams'],
2153
+ // TAllContext extends Record<string, any> = TRoute['types']['allContext'],
2154
+ // TLoaderDeps extends Record<string, any> = TRoute['types']['loaderDeps'],
2155
+ // TLoaderData extends any = TRoute['types']['loaderData'],
2156
+ // > = {
2157
+ // id: TId
2158
+ // useMatch: <TSelected = TAllContext>(opts?: {
2159
+ // select?: (s: TAllContext) => TSelected
2160
+ // }) => TSelected
2161
+
2162
+ // useRouteContext: <TSelected = TAllContext>(opts?: {
2163
+ // select?: (s: TAllContext) => TSelected
2164
+ // }) => TSelected
2165
+
2166
+ // useSearch: <TSelected = TFullSearchSchema>(opts?: {
2167
+ // select?: (s: TFullSearchSchema) => TSelected
2168
+ // }) => TSelected
2169
+
2170
+ // useParams: <TSelected = TAllParams>(opts?: {
2171
+ // select?: (s: TAllParams) => TSelected
2172
+ // }) => TSelected
2173
+
2174
+ // useLoaderDeps: <TSelected = TLoaderDeps>(opts?: {
2175
+ // select?: (s: TLoaderDeps) => TSelected
2176
+ // }) => TSelected
2177
+
2178
+ // useLoaderData: <TSelected = TLoaderData>(opts?: {
2179
+ // select?: (s: TLoaderData) => TSelected
2180
+ // }) => TSelected
2181
+ // }
2182
+
2183
+ // export function RouteApi_v2<
2184
+ // TId extends RouteIds<RegisteredRouter['routeTree']>,
2185
+ // TRoute extends AnyRoute = RouteById<RegisteredRouter['routeTree'], TId>,
2186
+ // TFullSearchSchema extends Record<
2187
+ // string,
2188
+ // any
2189
+ // > = TRoute['types']['fullSearchSchema'],
2190
+ // TAllParams extends AnyPathParams = TRoute['types']['allParams'],
2191
+ // TAllContext extends Record<string, any> = TRoute['types']['allContext'],
2192
+ // TLoaderDeps extends Record<string, any> = TRoute['types']['loaderDeps'],
2193
+ // TLoaderData extends any = TRoute['types']['loaderData'],
2194
+ // >({
2195
+ // id,
2196
+ // }: {
2197
+ // id: TId
2198
+ // }): RouteApiInstance<
2199
+ // TId,
2200
+ // TRoute,
2201
+ // TFullSearchSchema,
2202
+ // TAllParams,
2203
+ // TAllContext,
2204
+ // TLoaderDeps,
2205
+ // TLoaderData
2206
+ // > {
2207
+ // return {
2208
+ // id,
2209
+
2210
+ // useMatch: (opts) => {
2211
+ // return useMatch({ ...opts, from: id })
2212
+ // },
2213
+
2214
+ // useRouteContext: (opts) => {
2215
+ // return useMatch({
2216
+ // ...opts,
2217
+ // from: id,
2218
+ // select: (d: any) => (opts?.select ? opts.select(d.context) : d.context),
2219
+ // } as any)
2220
+ // },
2221
+
2222
+ // useSearch: (opts) => {
2223
+ // return useSearch({ ...opts, from: id } as any)
2224
+ // },
2225
+
2226
+ // useParams: (opts) => {
2227
+ // return useParams({ ...opts, from: id } as any)
2228
+ // },
2229
+
2230
+ // useLoaderDeps: (opts) => {
2231
+ // return useLoaderDeps({ ...opts, from: id } as any) as any
2232
+ // },
2233
+
2234
+ // useLoaderData: (opts) => {
2235
+ // return useLoaderData({ ...opts, from: id } as any) as any
2236
+ // },
2237
+ // }
2238
+ // }
2239
+
2240
+ class RouteApi {
2241
+ constructor({
2242
+ id
2243
+ }) {
2244
+ this.id = id;
2245
+ }
2246
+ useMatch = opts => {
2247
+ return useMatch({
2248
+ select: opts?.select,
2249
+ from: this.id
2250
+ });
2251
+ };
2252
+ useRouteContext = opts => {
2253
+ return useMatch({
2254
+ from: this.id,
2255
+ select: d => opts?.select ? opts.select(d.context) : d.context
2256
+ });
2257
+ };
2258
+ useSearch = opts => {
2259
+ return useSearch({
2260
+ ...opts,
2261
+ from: this.id
2262
+ });
2263
+ };
2264
+ useParams = opts => {
2265
+ return useParams({
2266
+ ...opts,
2267
+ from: this.id
2268
+ });
2269
+ };
2270
+ useLoaderDeps = opts => {
2271
+ return useLoaderDeps({
2272
+ ...opts,
2273
+ from: this.id
2274
+ });
2275
+ };
2276
+ useLoaderData = opts => {
2277
+ return useLoaderData({
2278
+ ...opts,
2279
+ from: this.id
2280
+ });
2281
+ };
2282
+ }
2283
+ class Route {
2284
+ // Set up in this.init()
2285
+
2286
+ // customId!: TCustomId
2287
+
2288
+ // Optional
2289
+
2290
+ constructor(options) {
2291
+ this.options = options || {};
2292
+ this.isRoot = !options?.getParentRoute;
2293
+ invariant(!(options?.id && options?.path), `Route cannot have both an 'id' and a 'path' option.`);
2294
+ this.$$typeof = Symbol.for('react.memo');
2295
+ }
2296
+ init = opts => {
2297
+ this.originalIndex = opts.originalIndex;
2298
+ const options = this.options;
2299
+ const isRoot = !options?.path && !options?.id;
2300
+ this.parentRoute = this.options?.getParentRoute?.();
2301
+ if (isRoot) {
2302
+ this.path = rootRouteId;
2303
+ } else {
2304
+ invariant(this.parentRoute, `Child Route instances must pass a 'getParentRoute: () => ParentRoute' option that returns a Route instance.`);
2305
+ }
2306
+ let path = isRoot ? rootRouteId : options.path;
2307
+
2308
+ // If the path is anything other than an index path, trim it up
2309
+ if (path && path !== '/') {
2310
+ path = trimPath(path);
2311
+ }
2312
+ const customId = options?.id || path;
2313
+
2314
+ // Strip the parentId prefix from the first level of children
2315
+ let id = isRoot ? rootRouteId : joinPaths([this.parentRoute.id === rootRouteId ? '' : this.parentRoute.id, customId]);
2316
+ if (path === rootRouteId) {
2317
+ path = '/';
2318
+ }
2319
+ if (id !== rootRouteId) {
2320
+ id = joinPaths(['/', id]);
2321
+ }
2322
+ const fullPath = id === rootRouteId ? '/' : joinPaths([this.parentRoute.fullPath, path]);
2323
+ this.path = path;
2324
+ this.id = id;
2325
+ // this.customId = customId as TCustomId
2326
+ this.fullPath = fullPath;
2327
+ this.to = fullPath;
2328
+ };
2329
+ addChildren = children => {
2330
+ this.children = children;
2331
+ return this;
2332
+ };
2333
+ updateLoader = options => {
2334
+ Object.assign(this.options, options);
2335
+ return this;
2336
+ };
2337
+ update = options => {
2338
+ Object.assign(this.options, options);
2339
+ return this;
2340
+ };
2341
+ useMatch = opts => {
2342
+ return useMatch({
2343
+ ...opts,
2344
+ from: this.id
2304
2345
  });
2305
- await Promise.all(matchPromises);
2306
- return matches;
2307
2346
  };
2308
- invalidate = () => {
2309
- const invalidate = d => ({
2310
- ...d,
2311
- invalid: true
2347
+ useRouteContext = opts => {
2348
+ return useMatch({
2349
+ ...opts,
2350
+ from: this.id,
2351
+ select: d => opts?.select ? opts.select(d.context) : d.context
2312
2352
  });
2313
- this.__store.setState(s => ({
2314
- ...s,
2315
- matches: s.matches.map(invalidate),
2316
- cachedMatches: s.cachedMatches.map(invalidate),
2317
- pendingMatches: s.pendingMatches?.map(invalidate)
2318
- }));
2319
- this.load();
2320
2353
  };
2321
- load = async () => {
2322
- const promise = new Promise(async (resolve, reject) => {
2323
- const next = this.latestLocation;
2324
- const prevLocation = this.state.resolvedLocation;
2325
- const pathDidChange = prevLocation.href !== next.href;
2326
- let latestPromise;
2327
-
2328
- // Cancel any pending matches
2329
- this.cancelMatches();
2330
- this.emit({
2331
- type: 'onBeforeLoad',
2332
- fromLocation: prevLocation,
2333
- toLocation: next,
2334
- pathChanged: pathDidChange
2335
- });
2336
- let pendingMatches;
2337
- const previousMatches = this.state.matches;
2338
- this.__store.batch(() => {
2339
- this.cleanCache();
2354
+ useSearch = opts => {
2355
+ return useSearch({
2356
+ ...opts,
2357
+ from: this.id
2358
+ });
2359
+ };
2360
+ useParams = opts => {
2361
+ return useParams({
2362
+ ...opts,
2363
+ from: this.id
2364
+ });
2365
+ };
2366
+ useLoaderDeps = opts => {
2367
+ return useLoaderDeps({
2368
+ ...opts,
2369
+ from: this.id
2370
+ });
2371
+ };
2372
+ useLoaderData = opts => {
2373
+ return useLoaderData({
2374
+ ...opts,
2375
+ from: this.id
2376
+ });
2377
+ };
2378
+ }
2379
+ function rootRouteWithContext() {
2380
+ return options => {
2381
+ return new RootRoute(options);
2382
+ };
2383
+ }
2384
+ class RootRoute extends Route {
2385
+ constructor(options) {
2386
+ super(options);
2387
+ }
2388
+ }
2389
+ function createRouteMask(opts) {
2390
+ return opts;
2391
+ }
2340
2392
 
2341
- // Match the routes
2342
- pendingMatches = this.matchRoutes(next.pathname, next.search, {
2343
- debug: true
2344
- });
2393
+ //
2345
2394
 
2346
- // Ingest the new matches
2347
- // If a cached moved to pendingMatches, remove it from cachedMatches
2348
- this.__store.setState(s => ({
2349
- ...s,
2350
- isLoading: true,
2351
- location: next,
2352
- pendingMatches,
2353
- cachedMatches: s.cachedMatches.filter(d => {
2354
- return !pendingMatches.find(e => e.id === d.id);
2355
- })
2356
- }));
2357
- });
2358
- try {
2359
- try {
2360
- // Load the matches
2361
- await this.loadMatches({
2362
- matches: pendingMatches,
2363
- checkLatest: () => this.checkLatest(promise)
2364
- });
2365
- } catch (err) {
2366
- // swallow this error, since we'll display the
2367
- // errors on the route components
2368
- }
2395
+ class NotFoundRoute extends Route {
2396
+ constructor(options) {
2397
+ super({
2398
+ ...options,
2399
+ id: '404'
2400
+ });
2401
+ }
2402
+ }
2369
2403
 
2370
- // Only apply the latest transition
2371
- if (latestPromise = this.checkLatest(promise)) {
2372
- return latestPromise;
2373
- }
2374
- const exitingMatches = previousMatches.filter(match => !pendingMatches.find(d => d.id === match.id));
2375
- const enteringMatches = pendingMatches.filter(match => !previousMatches.find(d => d.id === match.id));
2376
- const stayingMatches = previousMatches.filter(match => pendingMatches.find(d => d.id === match.id));
2404
+ class FileRoute {
2405
+ constructor(path) {
2406
+ this.path = path;
2407
+ }
2408
+ createRoute = options => {
2409
+ const route = new Route(options);
2410
+ route.isRoot = false;
2411
+ return route;
2412
+ };
2413
+ }
2414
+ function FileRouteLoader(_path) {
2415
+ return loaderFn => loaderFn;
2416
+ }
2377
2417
 
2378
- // Commit the pending matches. If a previous match was
2379
- // removed, place it in the cachedMatches
2380
- this.__store.batch(() => {
2381
- this.__store.setState(s => ({
2382
- ...s,
2383
- isLoading: false,
2384
- matches: s.pendingMatches,
2385
- pendingMatches: undefined,
2386
- cachedMatches: [...s.cachedMatches, ...exitingMatches.filter(d => d.status !== 'error')]
2387
- }));
2388
- this.cleanCache();
2389
- })
2418
+ function lazyRouteComponent(importer, exportName) {
2419
+ let loadPromise;
2420
+ const load = () => {
2421
+ if (!loadPromise) {
2422
+ loadPromise = importer();
2423
+ }
2424
+ return loadPromise;
2425
+ };
2426
+ const lazyComp = /*#__PURE__*/React.lazy(async () => {
2427
+ const moduleExports = await load();
2428
+ const comp = moduleExports[exportName ?? 'default'];
2429
+ return {
2430
+ default: comp
2431
+ };
2432
+ });
2433
+ lazyComp.preload = load;
2434
+ return lazyComp;
2435
+ }
2390
2436
 
2391
- //
2392
- ;
2393
- [[exitingMatches, 'onLeave'], [enteringMatches, 'onEnter'], [stayingMatches, 'onStay']].forEach(([matches, hook]) => {
2394
- matches.forEach(match => {
2395
- this.looseRoutesById[match.routeId].options[hook]?.(match);
2396
- });
2397
- });
2398
- this.emit({
2399
- type: 'onLoad',
2400
- fromLocation: prevLocation,
2401
- toLocation: next,
2402
- pathChanged: pathDidChange
2403
- });
2404
- resolve();
2405
- } catch (err) {
2406
- // Only apply the latest transition
2407
- if (latestPromise = this.checkLatest(promise)) {
2408
- return latestPromise;
2437
+ function _extends() {
2438
+ _extends = Object.assign ? Object.assign.bind() : function (target) {
2439
+ for (var i = 1; i < arguments.length; i++) {
2440
+ var source = arguments[i];
2441
+ for (var key in source) {
2442
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
2443
+ target[key] = source[key];
2409
2444
  }
2410
- reject(err);
2411
2445
  }
2412
- });
2413
- this.latestLoadPromise = promise;
2414
- return this.latestLoadPromise;
2446
+ }
2447
+ return target;
2415
2448
  };
2416
- cleanCache = () => {
2417
- // This is where all of the garbage collection magic happens
2418
- this.__store.setState(s => {
2419
- return {
2420
- ...s,
2421
- cachedMatches: s.cachedMatches.filter(d => {
2422
- const route = this.looseRoutesById[d.routeId];
2423
- if (!route.options.loader) {
2424
- return false;
2425
- }
2449
+ return _extends.apply(this, arguments);
2450
+ }
2451
+
2452
+ const preloadWarning = 'Error preloading route! ☝️';
2453
+ function useLinkProps(options) {
2454
+ const router = useRouter();
2455
+ const matchPathname = useMatch({
2456
+ strict: false,
2457
+ select: s => s.pathname
2458
+ });
2459
+ const {
2460
+ // custom props
2461
+ children,
2462
+ target,
2463
+ activeProps = () => ({
2464
+ className: 'active'
2465
+ }),
2466
+ inactiveProps = () => ({}),
2467
+ activeOptions,
2468
+ disabled,
2469
+ hash,
2470
+ search,
2471
+ params,
2472
+ to,
2473
+ state,
2474
+ mask,
2475
+ preload: userPreload,
2476
+ preloadDelay: userPreloadDelay,
2477
+ replace,
2478
+ startTransition,
2479
+ resetScroll,
2480
+ // element props
2481
+ style,
2482
+ className,
2483
+ onClick,
2484
+ onFocus,
2485
+ onMouseEnter,
2486
+ onMouseLeave,
2487
+ onTouchStart,
2488
+ ...rest
2489
+ } = options;
2426
2490
 
2427
- // If the route was preloaded, use the preloadGcTime
2428
- // otherwise, use the gcTime
2429
- const gcTime = (d.preload ? route.options.preloadGcTime ?? this.options.defaultPreloadGcTime : route.options.gcTime ?? this.options.defaultGcTime) ?? 5 * 60 * 1000;
2430
- return d.status !== 'error' && Date.now() - d.updatedAt < gcTime;
2431
- })
2432
- };
2433
- });
2434
- };
2435
- preloadRoute = async (navigateOpts = this.state.location) => {
2436
- let next = this.buildLocation(navigateOpts);
2437
- let matches = this.matchRoutes(next.pathname, next.search, {
2438
- throwOnError: true
2439
- });
2440
- const loadedMatchIds = Object.fromEntries([...this.state.matches, ...(this.state.pendingMatches ?? []), ...this.state.cachedMatches]?.map(d => [d.id, true]));
2441
- this.__store.batch(() => {
2442
- matches.forEach(match => {
2443
- if (!loadedMatchIds[match.id]) {
2444
- this.__store.setState(s => ({
2445
- ...s,
2446
- cachedMatches: [...s.cachedMatches, match]
2447
- }));
2448
- }
2449
- });
2450
- });
2451
- matches = await this.loadMatches({
2452
- matches,
2453
- preload: true,
2454
- checkLatest: () => undefined
2455
- });
2456
- return matches;
2491
+ // If this link simply reloads the current route,
2492
+ // make sure it has a new key so it will trigger a data refresh
2493
+
2494
+ // If this `to` is a valid external URL, return
2495
+ // null for LinkUtils
2496
+
2497
+ const dest = {
2498
+ from: options.to ? matchPathname : undefined,
2499
+ ...options
2457
2500
  };
2458
- matchRoute = (location, opts) => {
2459
- location = {
2460
- ...location,
2461
- to: location.to ? this.resolvePathWithBase(location.from || '', location.to) : undefined
2501
+ let type = 'internal';
2502
+ try {
2503
+ new URL(`${to}`);
2504
+ type = 'external';
2505
+ } catch {}
2506
+ if (type === 'external') {
2507
+ return {
2508
+ href: to
2462
2509
  };
2463
- const next = this.buildLocation(location);
2464
- if (opts?.pending && this.state.status !== 'pending') {
2465
- return false;
2466
- }
2467
- const baseLocation = opts?.pending ? this.latestLocation : this.state.resolvedLocation;
2468
- if (!baseLocation) {
2469
- return false;
2470
- }
2471
- const match = matchPathname(this.basepath, baseLocation.pathname, {
2472
- ...opts,
2473
- to: next.pathname
2474
- });
2475
- if (!match) {
2476
- return false;
2510
+ }
2511
+ const next = router.buildLocation(dest);
2512
+ const preload = userPreload ?? router.options.defaultPreload;
2513
+ const preloadDelay = userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0;
2514
+ const isActive = useRouterState({
2515
+ select: s => {
2516
+ // Compare path/hash for matches
2517
+ const currentPathSplit = s.location.pathname.split('/');
2518
+ const nextPathSplit = next.pathname.split('/');
2519
+ const pathIsFuzzyEqual = nextPathSplit.every((d, i) => d === currentPathSplit[i]);
2520
+ // Combine the matches based on user router.options
2521
+ const pathTest = activeOptions?.exact ? s.location.pathname === next.pathname : pathIsFuzzyEqual;
2522
+ const hashTest = activeOptions?.includeHash ? s.location.hash === next.hash : true;
2523
+ const searchTest = activeOptions?.includeSearch ?? true ? deepEqual(s.location.search, next.search, !activeOptions?.exact) : true;
2524
+
2525
+ // The final "active" test
2526
+ return pathTest && hashTest && searchTest;
2477
2527
  }
2478
- if (match && (opts?.includeSearch ?? true)) {
2479
- return deepEqual(baseLocation.search, next.search, true) ? match : false;
2528
+ });
2529
+
2530
+ // The click handler
2531
+ const handleClick = e => {
2532
+ if (!disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!target || target === '_self') && e.button === 0) {
2533
+ e.preventDefault();
2534
+
2535
+ // All is well? Navigate!
2536
+ router.commitLocation({
2537
+ ...next,
2538
+ replace,
2539
+ resetScroll,
2540
+ startTransition
2541
+ });
2480
2542
  }
2481
- return match;
2482
- };
2483
- injectHtml = async html => {
2484
- this.injectedHtml.push(html);
2485
2543
  };
2486
- dehydrateData = (key, getData) => {
2487
- if (typeof document === 'undefined') {
2488
- const strKey = typeof key === 'string' ? key : JSON.stringify(key);
2489
- this.injectHtml(async () => {
2490
- const id = `__TSR_DEHYDRATED__${strKey}`;
2491
- const data = typeof getData === 'function' ? await getData() : getData;
2492
- return `<script id='${id}' suppressHydrationWarning>window["__TSR_DEHYDRATED__${escapeJSON(strKey)}"] = ${JSON.stringify(data)}
2493
- ;(() => {
2494
- var el = document.getElementById('${id}')
2495
- el.parentElement.removeChild(el)
2496
- })()
2497
- </script>`;
2544
+
2545
+ // The click handler
2546
+ const handleFocus = e => {
2547
+ if (preload) {
2548
+ router.preloadRoute(dest).catch(err => {
2549
+ console.warn(err);
2550
+ console.warn(preloadWarning);
2498
2551
  });
2499
- return () => this.hydrateData(key);
2500
2552
  }
2501
- return () => undefined;
2502
2553
  };
2503
- hydrateData = key => {
2504
- if (typeof document !== 'undefined') {
2505
- const strKey = typeof key === 'string' ? key : JSON.stringify(key);
2506
- return window[`__TSR_DEHYDRATED__${strKey}`];
2554
+ const handleTouchStart = e => {
2555
+ if (preload) {
2556
+ router.preloadRoute(dest).catch(err => {
2557
+ console.warn(err);
2558
+ console.warn(preloadWarning);
2559
+ });
2507
2560
  }
2508
- return undefined;
2509
2561
  };
2510
- dehydrate = () => {
2511
- return {
2512
- state: {
2513
- dehydratedMatches: this.state.matches.map(d => pick(d, ['id', 'status', 'updatedAt', 'loaderData']))
2562
+ const handleEnter = e => {
2563
+ const target = e.target || {};
2564
+ if (preload) {
2565
+ if (target.preloadTimeout) {
2566
+ return;
2514
2567
  }
2515
- };
2568
+ target.preloadTimeout = setTimeout(() => {
2569
+ target.preloadTimeout = null;
2570
+ router.preloadRoute(dest).catch(err => {
2571
+ console.warn(err);
2572
+ console.warn(preloadWarning);
2573
+ });
2574
+ }, preloadDelay);
2575
+ }
2516
2576
  };
2517
- hydrate = async __do_not_use_server_ctx => {
2518
- let _ctx = __do_not_use_server_ctx;
2519
- // Client hydrates from window
2520
- if (typeof document !== 'undefined') {
2521
- _ctx = window.__TSR_DEHYDRATED__;
2577
+ const handleLeave = e => {
2578
+ const target = e.target || {};
2579
+ if (target.preloadTimeout) {
2580
+ clearTimeout(target.preloadTimeout);
2581
+ target.preloadTimeout = null;
2522
2582
  }
2523
- invariant(_ctx, 'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?');
2524
- const ctx = _ctx;
2525
- this.dehydratedData = ctx.payload;
2526
- this.options.hydrate?.(ctx.payload);
2527
- const dehydratedState = ctx.router.state;
2528
- let matches = this.matchRoutes(this.state.location.pathname, this.state.location.search).map(match => {
2529
- const dehydratedMatch = dehydratedState.dehydratedMatches.find(d => d.id === match.id);
2530
- invariant(dehydratedMatch, `Could not find a client-side match for dehydrated match with id: ${match.id}!`);
2531
- if (dehydratedMatch) {
2532
- return {
2533
- ...match,
2534
- ...dehydratedMatch
2535
- };
2536
- }
2537
- return match;
2538
- });
2539
- this.__store.setState(s => {
2540
- return {
2541
- ...s,
2542
- matches: matches
2543
- };
2583
+ };
2584
+ const composeHandlers = handlers => e => {
2585
+ if (e.persist) e.persist();
2586
+ handlers.filter(Boolean).forEach(handler => {
2587
+ if (e.defaultPrevented) return;
2588
+ handler(e);
2544
2589
  });
2545
2590
  };
2546
2591
 
2547
- // resolveMatchPromise = (matchId: string, key: string, value: any) => {
2548
- // state.matches
2549
- // .find((d) => d.id === matchId)
2550
- // ?.__promisesByKey[key]?.resolve(value)
2551
- // }
2552
- }
2592
+ // Get the active props
2593
+ const resolvedActiveProps = isActive ? functionalUpdate(activeProps, {}) ?? {} : {};
2553
2594
 
2554
- // A function that takes an import() argument which is a function and returns a new function that will
2555
- // proxy arguments from the caller to the imported function, retaining all type
2556
- // information along the way
2557
- function lazyFn(fn, key) {
2558
- return async (...args) => {
2559
- const imported = await fn();
2560
- return imported[key || 'default'](...args);
2561
- };
2562
- }
2563
- class SearchParamError extends Error {}
2564
- class PathParamError extends Error {}
2565
- function getInitialRouterState(location) {
2595
+ // Get the inactive props
2596
+ const resolvedInactiveProps = isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {};
2566
2597
  return {
2567
- isLoading: false,
2568
- isTransitioning: false,
2569
- status: 'idle',
2570
- resolvedLocation: {
2571
- ...location
2598
+ ...resolvedActiveProps,
2599
+ ...resolvedInactiveProps,
2600
+ ...rest,
2601
+ href: disabled ? undefined : next.maskedLocation ? next.maskedLocation.href : next.href,
2602
+ onClick: composeHandlers([onClick, handleClick]),
2603
+ onFocus: composeHandlers([onFocus, handleFocus]),
2604
+ onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
2605
+ onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
2606
+ onTouchStart: composeHandlers([onTouchStart, handleTouchStart]),
2607
+ target,
2608
+ style: {
2609
+ ...style,
2610
+ ...resolvedActiveProps.style,
2611
+ ...resolvedInactiveProps.style
2572
2612
  },
2573
- location,
2574
- matches: [],
2575
- pendingMatches: [],
2576
- cachedMatches: [],
2577
- lastUpdated: Date.now()
2613
+ className: [className, resolvedActiveProps.className, resolvedInactiveProps.className].filter(Boolean).join(' ') || undefined,
2614
+ ...(disabled ? {
2615
+ role: 'link',
2616
+ 'aria-disabled': true
2617
+ } : undefined),
2618
+ ['data-status']: isActive ? 'active' : undefined
2578
2619
  };
2579
2620
  }
2621
+ const Link = /*#__PURE__*/React.forwardRef((props, ref) => {
2622
+ const linkProps = useLinkProps(props);
2623
+ return /*#__PURE__*/React.createElement("a", _extends({
2624
+ ref: ref
2625
+ }, linkProps, {
2626
+ children: typeof props.children === 'function' ? props.children({
2627
+ isActive: linkProps['data-status'] === 'active'
2628
+ }) : props.children
2629
+ }));
2630
+ });
2631
+ function isCtrlEvent(e) {
2632
+ return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
2633
+ }
2580
2634
 
2581
2635
  const useLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
2582
2636
  const windowKey = 'window';
@@ -2816,5 +2870,5 @@ function useRouteContext(opts) {
2816
2870
  });
2817
2871
  }
2818
2872
 
2819
- export { Await, Block, CatchBoundary, CatchBoundaryImpl, ErrorComponent, FileRoute, FileRouteLoader, Link, Match, MatchRoute, Matches, Navigate, NotFoundRoute, Outlet, PathParamError, RootRoute, Route, RouteApi, Router, RouterProvider, ScrollRestoration, SearchParamError, cleanPath, componentTypes, createRouteMask, decode, deepEqual, defaultParseSearch, defaultStringifySearch, defer, encode, escapeJSON, functionalUpdate, getInitialRouterState, getRenderedMatches, getRouteMatch, interpolatePath, isDehydratedDeferred, isPlainArray, isPlainObject, isRedirect, isServer, joinPaths, last, lazyFn, lazyRouteComponent, matchByPath, matchContext, matchPathname, parsePathname, parseSearchWith, pick, redirect, removeBasepath, replaceEqualDeep, resolvePath, rootRouteId, rootRouteWithContext, routerContext, shallow, stringifySearchWith, trimPath, trimPathLeft, trimPathRight, useAwaited, useBlocker, useElementScrollRestoration, useLayoutEffect$1 as useLayoutEffect, useLinkProps, useLoaderData, useLoaderDeps, useMatch, useMatchRoute, useMatches, useNavigate, useParams, useParentMatches, useRouteContext, useRouter, useRouterState, useScrollRestoration, useSearch, useStableCallback };
2873
+ export { Await, Block, CatchBoundary, CatchBoundaryImpl, ErrorComponent, FileRoute, FileRouteLoader, Link, Match, MatchRoute, Matches, Navigate, NotFoundRoute, Outlet, PathParamError, RootRoute, Route, RouteApi, Router, RouterProvider, ScrollRestoration, SearchParamError, cleanPath, componentTypes, createRouteMask, decode, deepEqual, defaultDeserializeError, defaultParseSearch, defaultSerializeError, defaultStringifySearch, defer, encode, escapeJSON, functionalUpdate, getInitialRouterState, getRenderedMatches, getRouteMatch, interpolatePath, isDehydratedDeferred, isPlainArray, isPlainObject, isRedirect, isServer, isServerSideError, joinPaths, last, lazyFn, lazyRouteComponent, matchByPath, matchContext, matchPathname, parsePathname, parseSearchWith, pick, redirect, removeBasepath, replaceEqualDeep, resolvePath, rootRouteId, rootRouteWithContext, routerContext, shallow, stringifySearchWith, trimPath, trimPathLeft, trimPathRight, useAwaited, useBlocker, useElementScrollRestoration, useLayoutEffect$1 as useLayoutEffect, useLinkProps, useLoaderData, useLoaderDeps, useMatch, useMatchRoute, useMatches, useNavigate, useParams, useParentMatches, useRouteContext, useRouter, useRouterState, useScrollRestoration, useSearch, useStableCallback };
2820
2874
  //# sourceMappingURL=index.js.map