@tanstack/router-core 1.114.24 → 1.114.25
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.
- package/dist/cjs/history.d.cts +8 -0
- package/dist/cjs/index.cjs +6 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -2
- package/dist/cjs/router.cjs +1695 -0
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +117 -55
- package/dist/esm/history.d.ts +8 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +7 -1
- package/dist/esm/router.d.ts +117 -55
- package/dist/esm/router.js +1696 -1
- package/dist/esm/router.js.map +1 -1
- package/package.json +2 -1
- package/src/history.ts +9 -0
- package/src/index.ts +11 -2
- package/src/router.ts +2511 -106
package/src/router.ts
CHANGED
|
@@ -1,19 +1,62 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { Store, batch } from '@tanstack/store'
|
|
2
|
+
import {
|
|
3
|
+
createBrowserHistory,
|
|
4
|
+
createMemoryHistory,
|
|
5
|
+
parseHref,
|
|
6
|
+
} from '@tanstack/history'
|
|
7
|
+
import invariant from 'tiny-invariant'
|
|
8
|
+
import {
|
|
9
|
+
createControlledPromise,
|
|
10
|
+
deepEqual,
|
|
11
|
+
functionalUpdate,
|
|
12
|
+
last,
|
|
13
|
+
pick,
|
|
14
|
+
replaceEqualDeep,
|
|
15
|
+
} from './utils'
|
|
16
|
+
import {
|
|
17
|
+
cleanPath,
|
|
18
|
+
interpolatePath,
|
|
19
|
+
joinPaths,
|
|
20
|
+
matchPathname,
|
|
21
|
+
parsePathname,
|
|
22
|
+
resolvePath,
|
|
23
|
+
trimPath,
|
|
24
|
+
trimPathLeft,
|
|
25
|
+
trimPathRight,
|
|
26
|
+
} from './path'
|
|
27
|
+
import { isNotFound } from './not-found'
|
|
28
|
+
import { setupScrollRestoration } from './scroll-restoration'
|
|
29
|
+
import { defaultParseSearch, defaultStringifySearch } from './searchParams'
|
|
30
|
+
import { rootRouteId } from './root'
|
|
31
|
+
import { isRedirect, isResolvedRedirect } from './redirect'
|
|
32
|
+
import type { SearchParser, SearchSerializer } from './searchParams'
|
|
33
|
+
import type { AnyRedirect, ResolvedRedirect } from './redirect'
|
|
34
|
+
import type {
|
|
35
|
+
HistoryLocation,
|
|
36
|
+
HistoryState,
|
|
37
|
+
ParsedHistoryState,
|
|
38
|
+
RouterHistory,
|
|
39
|
+
} from '@tanstack/history'
|
|
3
40
|
import type {
|
|
4
41
|
ControlledPromise,
|
|
5
42
|
NoInfer,
|
|
6
43
|
NonNullableUpdater,
|
|
44
|
+
PickAsRequired,
|
|
7
45
|
Updater,
|
|
8
46
|
} from './utils'
|
|
47
|
+
import type { ParsedLocation } from './location'
|
|
48
|
+
import type { DeferredPromiseState } from './defer'
|
|
9
49
|
import type {
|
|
10
50
|
AnyContext,
|
|
11
51
|
AnyRoute,
|
|
12
52
|
AnyRouteWithContext,
|
|
53
|
+
BeforeLoadContextOptions,
|
|
54
|
+
LoaderFnContext,
|
|
13
55
|
MakeRemountDepsOptionsUnion,
|
|
56
|
+
RouteContextOptions,
|
|
14
57
|
RouteMask,
|
|
58
|
+
SearchMiddleware,
|
|
15
59
|
} from './route'
|
|
16
|
-
import type { Store } from '@tanstack/store'
|
|
17
60
|
import type {
|
|
18
61
|
FullSearchSchema,
|
|
19
62
|
RouteById,
|
|
@@ -23,26 +66,20 @@ import type {
|
|
|
23
66
|
} from './routeInfo'
|
|
24
67
|
import type {
|
|
25
68
|
AnyRouteMatch,
|
|
69
|
+
MakeRouteMatch,
|
|
26
70
|
MakeRouteMatchUnion,
|
|
27
71
|
MatchRouteOptions,
|
|
28
72
|
} from './Matches'
|
|
29
|
-
import type { AnyRedirect, ResolvedRedirect } from './redirect'
|
|
30
73
|
import type {
|
|
31
74
|
BuildLocationFn,
|
|
32
75
|
CommitLocationOptions,
|
|
33
76
|
NavigateFn,
|
|
34
77
|
} from './RouterProvider'
|
|
35
|
-
import type {
|
|
36
|
-
HistoryLocation,
|
|
37
|
-
HistoryState,
|
|
38
|
-
ParsedHistoryState,
|
|
39
|
-
RouterHistory,
|
|
40
|
-
} from '@tanstack/history'
|
|
41
78
|
import type { Manifest } from './manifest'
|
|
42
79
|
import type { StartSerializer } from './serializer'
|
|
43
|
-
import type { AnySchema } from './validators'
|
|
80
|
+
import type { AnySchema, AnyValidator } from './validators'
|
|
44
81
|
import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
|
|
45
|
-
import type {
|
|
82
|
+
import type { NotFoundError } from './not-found'
|
|
46
83
|
|
|
47
84
|
declare global {
|
|
48
85
|
interface Window {
|
|
@@ -493,7 +530,7 @@ export type PreloadRouteFn<
|
|
|
493
530
|
TMaskTo extends string = '',
|
|
494
531
|
>(
|
|
495
532
|
opts: NavigateOptions<
|
|
496
|
-
|
|
533
|
+
RouterCore<
|
|
497
534
|
TRouteTree,
|
|
498
535
|
TTrailingSlashOption,
|
|
499
536
|
TDefaultStructuralSharingOption,
|
|
@@ -517,7 +554,7 @@ export type MatchRouteFn<
|
|
|
517
554
|
TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
|
|
518
555
|
>(
|
|
519
556
|
location: ToOptions<
|
|
520
|
-
|
|
557
|
+
RouterCore<
|
|
521
558
|
TRouteTree,
|
|
522
559
|
TTrailingSlashOption,
|
|
523
560
|
TDefaultStructuralSharingOption,
|
|
@@ -622,97 +659,7 @@ export interface ServerSrr {
|
|
|
622
659
|
onMatchSettled: (opts: { router: AnyRouter; match: AnyRouteMatch }) => any
|
|
623
660
|
}
|
|
624
661
|
|
|
625
|
-
export
|
|
626
|
-
in out TRouteTree extends AnyRoute,
|
|
627
|
-
in out TTrailingSlashOption extends TrailingSlashOption,
|
|
628
|
-
in out TDefaultStructuralSharingOption extends boolean,
|
|
629
|
-
in out TRouterHistory extends RouterHistory = RouterHistory,
|
|
630
|
-
in out TDehydrated extends Record<string, any> = Record<string, any>,
|
|
631
|
-
> {
|
|
632
|
-
routeTree: TRouteTree
|
|
633
|
-
options: RouterOptions<
|
|
634
|
-
TRouteTree,
|
|
635
|
-
TTrailingSlashOption,
|
|
636
|
-
TDefaultStructuralSharingOption,
|
|
637
|
-
TRouterHistory,
|
|
638
|
-
TDehydrated
|
|
639
|
-
>
|
|
640
|
-
__store: Store<RouterState<TRouteTree>>
|
|
641
|
-
navigate: NavigateFn
|
|
642
|
-
history: TRouterHistory
|
|
643
|
-
state: RouterState<TRouteTree>
|
|
644
|
-
isServer: boolean
|
|
645
|
-
clientSsr?: {
|
|
646
|
-
getStreamedValue: <T>(key: string) => T | undefined
|
|
647
|
-
}
|
|
648
|
-
looseRoutesById: Record<string, AnyRoute>
|
|
649
|
-
latestLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
650
|
-
isScrollRestoring: boolean
|
|
651
|
-
resetNextScroll: boolean
|
|
652
|
-
isScrollRestorationSetup: boolean
|
|
653
|
-
ssr?: {
|
|
654
|
-
manifest: Manifest | undefined
|
|
655
|
-
serializer: StartSerializer
|
|
656
|
-
}
|
|
657
|
-
serverSsr?: ServerSrr
|
|
658
|
-
basepath: string
|
|
659
|
-
routesById: RoutesById<TRouteTree>
|
|
660
|
-
routesByPath: RoutesByPath<TRouteTree>
|
|
661
|
-
flatRoutes: Array<AnyRoute>
|
|
662
|
-
parseLocation: ParseLocationFn<TRouteTree>
|
|
663
|
-
getMatchedRoutes: GetMatchRoutesFn
|
|
664
|
-
emit: EmitFn
|
|
665
|
-
load: LoadFn
|
|
666
|
-
commitLocation: CommitLocationFn
|
|
667
|
-
buildLocation: BuildLocationFn
|
|
668
|
-
startTransition: StartTransitionFn
|
|
669
|
-
subscribe: SubscribeFn
|
|
670
|
-
matchRoutes: MatchRoutesFn
|
|
671
|
-
preloadRoute: PreloadRouteFn<
|
|
672
|
-
TRouteTree,
|
|
673
|
-
TTrailingSlashOption,
|
|
674
|
-
TDefaultStructuralSharingOption,
|
|
675
|
-
TRouterHistory
|
|
676
|
-
>
|
|
677
|
-
getMatch: GetMatchFn
|
|
678
|
-
updateMatch: UpdateMatchFn
|
|
679
|
-
matchRoute: MatchRouteFn<
|
|
680
|
-
TRouteTree,
|
|
681
|
-
TTrailingSlashOption,
|
|
682
|
-
TDefaultStructuralSharingOption,
|
|
683
|
-
TRouterHistory
|
|
684
|
-
>
|
|
685
|
-
update: UpdateFn<
|
|
686
|
-
TRouteTree,
|
|
687
|
-
TTrailingSlashOption,
|
|
688
|
-
TDefaultStructuralSharingOption,
|
|
689
|
-
TRouterHistory,
|
|
690
|
-
TDehydrated
|
|
691
|
-
>
|
|
692
|
-
invalidate: InvalidateFn<
|
|
693
|
-
Router<
|
|
694
|
-
TRouteTree,
|
|
695
|
-
TTrailingSlashOption,
|
|
696
|
-
TDefaultStructuralSharingOption,
|
|
697
|
-
TRouterHistory,
|
|
698
|
-
TDehydrated
|
|
699
|
-
>
|
|
700
|
-
>
|
|
701
|
-
loadRouteChunk: LoadRouteChunkFn
|
|
702
|
-
resolveRedirect: ResolveRedirect
|
|
703
|
-
buildRouteTree: () => void
|
|
704
|
-
clearCache: ClearCacheFn<
|
|
705
|
-
Router<
|
|
706
|
-
TRouteTree,
|
|
707
|
-
TTrailingSlashOption,
|
|
708
|
-
TDefaultStructuralSharingOption,
|
|
709
|
-
TRouterHistory,
|
|
710
|
-
TDehydrated
|
|
711
|
-
>
|
|
712
|
-
>
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
export type AnyRouterWithContext<TContext> = Router<
|
|
662
|
+
export type AnyRouterWithContext<TContext> = RouterCore<
|
|
716
663
|
AnyRouteWithContext<TContext>,
|
|
717
664
|
any,
|
|
718
665
|
any,
|
|
@@ -720,7 +667,7 @@ export type AnyRouterWithContext<TContext> = Router<
|
|
|
720
667
|
any
|
|
721
668
|
>
|
|
722
669
|
|
|
723
|
-
export type AnyRouter =
|
|
670
|
+
export type AnyRouter = RouterCore<any, any, any, any, any>
|
|
724
671
|
|
|
725
672
|
export interface ViewTransitionOptions {
|
|
726
673
|
types: Array<string>
|
|
@@ -781,3 +728,2461 @@ export function getLocationChangeInfo(routerState: {
|
|
|
781
728
|
const hashChanged = fromLocation?.hash !== toLocation.hash
|
|
782
729
|
return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
|
|
783
730
|
}
|
|
731
|
+
|
|
732
|
+
export type CreateRouterFn = <
|
|
733
|
+
TRouteTree extends AnyRoute,
|
|
734
|
+
TTrailingSlashOption extends TrailingSlashOption = 'never',
|
|
735
|
+
TDefaultStructuralSharingOption extends boolean = false,
|
|
736
|
+
TRouterHistory extends RouterHistory = RouterHistory,
|
|
737
|
+
TDehydrated extends Record<string, any> = Record<string, any>,
|
|
738
|
+
>(
|
|
739
|
+
options: undefined extends number
|
|
740
|
+
? 'strictNullChecks must be enabled in tsconfig.json'
|
|
741
|
+
: RouterConstructorOptions<
|
|
742
|
+
TRouteTree,
|
|
743
|
+
TTrailingSlashOption,
|
|
744
|
+
TDefaultStructuralSharingOption,
|
|
745
|
+
TRouterHistory,
|
|
746
|
+
TDehydrated
|
|
747
|
+
>,
|
|
748
|
+
) => RouterCore<
|
|
749
|
+
TRouteTree,
|
|
750
|
+
TTrailingSlashOption,
|
|
751
|
+
TDefaultStructuralSharingOption,
|
|
752
|
+
TRouterHistory,
|
|
753
|
+
TDehydrated
|
|
754
|
+
>
|
|
755
|
+
|
|
756
|
+
export class RouterCore<
|
|
757
|
+
in out TRouteTree extends AnyRoute,
|
|
758
|
+
in out TTrailingSlashOption extends TrailingSlashOption,
|
|
759
|
+
in out TDefaultStructuralSharingOption extends boolean,
|
|
760
|
+
in out TRouterHistory extends RouterHistory = RouterHistory,
|
|
761
|
+
in out TDehydrated extends Record<string, any> = Record<string, any>,
|
|
762
|
+
> {
|
|
763
|
+
// Option-independent properties
|
|
764
|
+
tempLocationKey: string | undefined = `${Math.round(
|
|
765
|
+
Math.random() * 10000000,
|
|
766
|
+
)}`
|
|
767
|
+
resetNextScroll = true
|
|
768
|
+
shouldViewTransition?: boolean | ViewTransitionOptions = undefined
|
|
769
|
+
isViewTransitionTypesSupported?: boolean = undefined
|
|
770
|
+
subscribers = new Set<RouterListener<RouterEvent>>()
|
|
771
|
+
viewTransitionPromise?: ControlledPromise<true>
|
|
772
|
+
isScrollRestoring = false
|
|
773
|
+
isScrollRestorationSetup = false
|
|
774
|
+
|
|
775
|
+
// Must build in constructor
|
|
776
|
+
__store!: Store<RouterState<TRouteTree>>
|
|
777
|
+
options!: PickAsRequired<
|
|
778
|
+
RouterOptions<
|
|
779
|
+
TRouteTree,
|
|
780
|
+
TTrailingSlashOption,
|
|
781
|
+
TDefaultStructuralSharingOption,
|
|
782
|
+
TRouterHistory,
|
|
783
|
+
TDehydrated
|
|
784
|
+
>,
|
|
785
|
+
'stringifySearch' | 'parseSearch' | 'context'
|
|
786
|
+
>
|
|
787
|
+
history!: TRouterHistory
|
|
788
|
+
latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
789
|
+
basepath!: string
|
|
790
|
+
routeTree!: TRouteTree
|
|
791
|
+
routesById!: RoutesById<TRouteTree>
|
|
792
|
+
routesByPath!: RoutesByPath<TRouteTree>
|
|
793
|
+
flatRoutes!: Array<AnyRoute>
|
|
794
|
+
isServer!: boolean
|
|
795
|
+
pathParamsDecodeCharMap?: Map<string, string>
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* @deprecated Use the `createRouter` function instead
|
|
799
|
+
*/
|
|
800
|
+
constructor(
|
|
801
|
+
options: RouterConstructorOptions<
|
|
802
|
+
TRouteTree,
|
|
803
|
+
TTrailingSlashOption,
|
|
804
|
+
TDefaultStructuralSharingOption,
|
|
805
|
+
TRouterHistory,
|
|
806
|
+
TDehydrated
|
|
807
|
+
>,
|
|
808
|
+
) {
|
|
809
|
+
this.update({
|
|
810
|
+
defaultPreloadDelay: 50,
|
|
811
|
+
defaultPendingMs: 1000,
|
|
812
|
+
defaultPendingMinMs: 500,
|
|
813
|
+
context: undefined!,
|
|
814
|
+
...options,
|
|
815
|
+
caseSensitive: options.caseSensitive ?? false,
|
|
816
|
+
notFoundMode: options.notFoundMode ?? 'fuzzy',
|
|
817
|
+
stringifySearch: options.stringifySearch ?? defaultStringifySearch,
|
|
818
|
+
parseSearch: options.parseSearch ?? defaultParseSearch,
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
if (typeof document !== 'undefined') {
|
|
822
|
+
;(window as any).__TSR_ROUTER__ = this
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// These are default implementations that can optionally be overridden
|
|
827
|
+
// by the router provider once rendered. We provide these so that the
|
|
828
|
+
// router can be used in a non-react environment if necessary
|
|
829
|
+
startTransition: StartTransitionFn = (fn) => fn()
|
|
830
|
+
|
|
831
|
+
update: UpdateFn<
|
|
832
|
+
TRouteTree,
|
|
833
|
+
TTrailingSlashOption,
|
|
834
|
+
TDefaultStructuralSharingOption,
|
|
835
|
+
TRouterHistory,
|
|
836
|
+
TDehydrated
|
|
837
|
+
> = (newOptions) => {
|
|
838
|
+
if (newOptions.notFoundRoute) {
|
|
839
|
+
console.warn(
|
|
840
|
+
'The notFoundRoute API is deprecated and will be removed in the next major version. See https://tanstack.com/router/v1/docs/framework/react/guide/not-found-errors#migrating-from-notfoundroute for more info.',
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const previousOptions = this.options
|
|
845
|
+
this.options = {
|
|
846
|
+
...this.options,
|
|
847
|
+
...newOptions,
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
this.isServer = this.options.isServer ?? typeof document === 'undefined'
|
|
851
|
+
|
|
852
|
+
this.pathParamsDecodeCharMap = this.options.pathParamsAllowedCharacters
|
|
853
|
+
? new Map(
|
|
854
|
+
this.options.pathParamsAllowedCharacters.map((char) => [
|
|
855
|
+
encodeURIComponent(char),
|
|
856
|
+
char,
|
|
857
|
+
]),
|
|
858
|
+
)
|
|
859
|
+
: undefined
|
|
860
|
+
|
|
861
|
+
if (
|
|
862
|
+
!this.basepath ||
|
|
863
|
+
(newOptions.basepath && newOptions.basepath !== previousOptions.basepath)
|
|
864
|
+
) {
|
|
865
|
+
if (
|
|
866
|
+
newOptions.basepath === undefined ||
|
|
867
|
+
newOptions.basepath === '' ||
|
|
868
|
+
newOptions.basepath === '/'
|
|
869
|
+
) {
|
|
870
|
+
this.basepath = '/'
|
|
871
|
+
} else {
|
|
872
|
+
this.basepath = `/${trimPath(newOptions.basepath)}`
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (
|
|
877
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
878
|
+
!this.history ||
|
|
879
|
+
(this.options.history && this.options.history !== this.history)
|
|
880
|
+
) {
|
|
881
|
+
this.history =
|
|
882
|
+
this.options.history ??
|
|
883
|
+
((this.isServer
|
|
884
|
+
? createMemoryHistory({
|
|
885
|
+
initialEntries: [this.basepath || '/'],
|
|
886
|
+
})
|
|
887
|
+
: createBrowserHistory()) as TRouterHistory)
|
|
888
|
+
this.latestLocation = this.parseLocation()
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (this.options.routeTree !== this.routeTree) {
|
|
892
|
+
this.routeTree = this.options.routeTree as TRouteTree
|
|
893
|
+
this.buildRouteTree()
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
897
|
+
if (!this.__store) {
|
|
898
|
+
this.__store = new Store(getInitialRouterState(this.latestLocation), {
|
|
899
|
+
onUpdate: () => {
|
|
900
|
+
this.__store.state = {
|
|
901
|
+
...this.state,
|
|
902
|
+
cachedMatches: this.state.cachedMatches.filter(
|
|
903
|
+
(d) => !['redirected'].includes(d.status),
|
|
904
|
+
),
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
setupScrollRestoration(this)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (
|
|
913
|
+
typeof window !== 'undefined' &&
|
|
914
|
+
'CSS' in window &&
|
|
915
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
916
|
+
typeof window.CSS?.supports === 'function'
|
|
917
|
+
) {
|
|
918
|
+
this.isViewTransitionTypesSupported = window.CSS.supports(
|
|
919
|
+
'selector(:active-view-transition-type(a)',
|
|
920
|
+
)
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
get state() {
|
|
925
|
+
return this.__store.state
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
buildRouteTree = () => {
|
|
929
|
+
this.routesById = {} as RoutesById<TRouteTree>
|
|
930
|
+
this.routesByPath = {} as RoutesByPath<TRouteTree>
|
|
931
|
+
|
|
932
|
+
const notFoundRoute = this.options.notFoundRoute
|
|
933
|
+
if (notFoundRoute) {
|
|
934
|
+
notFoundRoute.init({
|
|
935
|
+
originalIndex: 99999999999,
|
|
936
|
+
defaultSsr: this.options.defaultSsr,
|
|
937
|
+
})
|
|
938
|
+
;(this.routesById as any)[notFoundRoute.id] = notFoundRoute
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const recurseRoutes = (childRoutes: Array<AnyRoute>) => {
|
|
942
|
+
childRoutes.forEach((childRoute, i) => {
|
|
943
|
+
childRoute.init({
|
|
944
|
+
originalIndex: i,
|
|
945
|
+
defaultSsr: this.options.defaultSsr,
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
const existingRoute = (this.routesById as any)[childRoute.id]
|
|
949
|
+
|
|
950
|
+
invariant(
|
|
951
|
+
!existingRoute,
|
|
952
|
+
`Duplicate routes found with id: ${String(childRoute.id)}`,
|
|
953
|
+
)
|
|
954
|
+
;(this.routesById as any)[childRoute.id] = childRoute
|
|
955
|
+
|
|
956
|
+
if (!childRoute.isRoot && childRoute.path) {
|
|
957
|
+
const trimmedFullPath = trimPathRight(childRoute.fullPath)
|
|
958
|
+
if (
|
|
959
|
+
!(this.routesByPath as any)[trimmedFullPath] ||
|
|
960
|
+
childRoute.fullPath.endsWith('/')
|
|
961
|
+
) {
|
|
962
|
+
;(this.routesByPath as any)[trimmedFullPath] = childRoute
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const children = childRoute.children
|
|
967
|
+
|
|
968
|
+
if (children?.length) {
|
|
969
|
+
recurseRoutes(children)
|
|
970
|
+
}
|
|
971
|
+
})
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
recurseRoutes([this.routeTree])
|
|
975
|
+
|
|
976
|
+
const scoredRoutes: Array<{
|
|
977
|
+
child: AnyRoute
|
|
978
|
+
trimmed: string
|
|
979
|
+
parsed: ReturnType<typeof parsePathname>
|
|
980
|
+
index: number
|
|
981
|
+
scores: Array<number>
|
|
982
|
+
}> = []
|
|
983
|
+
|
|
984
|
+
const routes: Array<AnyRoute> = Object.values(this.routesById)
|
|
985
|
+
|
|
986
|
+
routes.forEach((d, i) => {
|
|
987
|
+
if (d.isRoot || !d.path) {
|
|
988
|
+
return
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const trimmed = trimPathLeft(d.fullPath)
|
|
992
|
+
const parsed = parsePathname(trimmed)
|
|
993
|
+
|
|
994
|
+
while (parsed.length > 1 && parsed[0]?.value === '/') {
|
|
995
|
+
parsed.shift()
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const scores = parsed.map((segment) => {
|
|
999
|
+
if (segment.value === '/') {
|
|
1000
|
+
return 0.75
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (segment.type === 'param') {
|
|
1004
|
+
return 0.5
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (segment.type === 'wildcard') {
|
|
1008
|
+
return 0.25
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return 1
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
this.flatRoutes = scoredRoutes
|
|
1018
|
+
.sort((a, b) => {
|
|
1019
|
+
const minLength = Math.min(a.scores.length, b.scores.length)
|
|
1020
|
+
|
|
1021
|
+
// Sort by min available score
|
|
1022
|
+
for (let i = 0; i < minLength; i++) {
|
|
1023
|
+
if (a.scores[i] !== b.scores[i]) {
|
|
1024
|
+
return b.scores[i]! - a.scores[i]!
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Sort by length of score
|
|
1029
|
+
if (a.scores.length !== b.scores.length) {
|
|
1030
|
+
return b.scores.length - a.scores.length
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Sort by min available parsed value
|
|
1034
|
+
for (let i = 0; i < minLength; i++) {
|
|
1035
|
+
if (a.parsed[i]!.value !== b.parsed[i]!.value) {
|
|
1036
|
+
return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Sort by original index
|
|
1041
|
+
return a.index - b.index
|
|
1042
|
+
})
|
|
1043
|
+
.map((d, i) => {
|
|
1044
|
+
d.child.rank = i
|
|
1045
|
+
return d.child
|
|
1046
|
+
})
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
subscribe: SubscribeFn = (eventType, fn) => {
|
|
1050
|
+
const listener: RouterListener<any> = {
|
|
1051
|
+
eventType,
|
|
1052
|
+
fn,
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
this.subscribers.add(listener)
|
|
1056
|
+
|
|
1057
|
+
return () => {
|
|
1058
|
+
this.subscribers.delete(listener)
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
emit: EmitFn = (routerEvent) => {
|
|
1063
|
+
this.subscribers.forEach((listener) => {
|
|
1064
|
+
if (listener.eventType === routerEvent.type) {
|
|
1065
|
+
listener.fn(routerEvent)
|
|
1066
|
+
}
|
|
1067
|
+
})
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
parseLocation: ParseLocationFn<TRouteTree> = (
|
|
1071
|
+
previousLocation,
|
|
1072
|
+
locationToParse,
|
|
1073
|
+
) => {
|
|
1074
|
+
const parse = ({
|
|
1075
|
+
pathname,
|
|
1076
|
+
search,
|
|
1077
|
+
hash,
|
|
1078
|
+
state,
|
|
1079
|
+
}: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
|
|
1080
|
+
const parsedSearch = this.options.parseSearch(search)
|
|
1081
|
+
const searchStr = this.options.stringifySearch(parsedSearch)
|
|
1082
|
+
|
|
1083
|
+
return {
|
|
1084
|
+
pathname,
|
|
1085
|
+
searchStr,
|
|
1086
|
+
search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
|
|
1087
|
+
hash: hash.split('#').reverse()[0] ?? '',
|
|
1088
|
+
href: `${pathname}${searchStr}${hash}`,
|
|
1089
|
+
state: replaceEqualDeep(previousLocation?.state, state),
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const location = parse(locationToParse ?? this.history.location)
|
|
1094
|
+
|
|
1095
|
+
const { __tempLocation, __tempKey } = location.state
|
|
1096
|
+
|
|
1097
|
+
if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
|
|
1098
|
+
// Sync up the location keys
|
|
1099
|
+
const parsedTempLocation = parse(__tempLocation) as any
|
|
1100
|
+
parsedTempLocation.state.key = location.state.key
|
|
1101
|
+
|
|
1102
|
+
delete parsedTempLocation.state.__tempLocation
|
|
1103
|
+
|
|
1104
|
+
return {
|
|
1105
|
+
...parsedTempLocation,
|
|
1106
|
+
maskedLocation: location,
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
return location
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
resolvePathWithBase = (from: string, path: string) => {
|
|
1114
|
+
const resolvedPath = resolvePath({
|
|
1115
|
+
basepath: this.basepath,
|
|
1116
|
+
base: from,
|
|
1117
|
+
to: cleanPath(path),
|
|
1118
|
+
trailingSlash: this.options.trailingSlash,
|
|
1119
|
+
caseSensitive: this.options.caseSensitive,
|
|
1120
|
+
})
|
|
1121
|
+
return resolvedPath
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
get looseRoutesById() {
|
|
1125
|
+
return this.routesById as Record<string, AnyRoute>
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
@deprecated use the following signature instead
|
|
1130
|
+
```ts
|
|
1131
|
+
matchRoutes (
|
|
1132
|
+
next: ParsedLocation,
|
|
1133
|
+
opts?: { preload?: boolean; throwOnError?: boolean },
|
|
1134
|
+
): Array<AnyRouteMatch>;
|
|
1135
|
+
```
|
|
1136
|
+
*/
|
|
1137
|
+
matchRoutes: MatchRoutesFn = (
|
|
1138
|
+
pathnameOrNext: string | ParsedLocation,
|
|
1139
|
+
locationSearchOrOpts?: AnySchema | MatchRoutesOpts,
|
|
1140
|
+
opts?: MatchRoutesOpts,
|
|
1141
|
+
) => {
|
|
1142
|
+
if (typeof pathnameOrNext === 'string') {
|
|
1143
|
+
return this.matchRoutesInternal(
|
|
1144
|
+
{
|
|
1145
|
+
pathname: pathnameOrNext,
|
|
1146
|
+
search: locationSearchOrOpts,
|
|
1147
|
+
} as ParsedLocation,
|
|
1148
|
+
opts,
|
|
1149
|
+
)
|
|
1150
|
+
} else {
|
|
1151
|
+
return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
private matchRoutesInternal(
|
|
1156
|
+
next: ParsedLocation,
|
|
1157
|
+
opts?: MatchRoutesOpts,
|
|
1158
|
+
): Array<AnyRouteMatch> {
|
|
1159
|
+
const { foundRoute, matchedRoutes, routeParams } = this.getMatchedRoutes(
|
|
1160
|
+
next,
|
|
1161
|
+
opts?.dest,
|
|
1162
|
+
)
|
|
1163
|
+
let isGlobalNotFound = false
|
|
1164
|
+
|
|
1165
|
+
// Check to see if the route needs a 404 entry
|
|
1166
|
+
if (
|
|
1167
|
+
// If we found a route, and it's not an index route and we have left over path
|
|
1168
|
+
foundRoute
|
|
1169
|
+
? foundRoute.path !== '/' && routeParams['**']
|
|
1170
|
+
: // Or if we didn't find a route and we have left over path
|
|
1171
|
+
trimPathRight(next.pathname)
|
|
1172
|
+
) {
|
|
1173
|
+
// If the user has defined an (old) 404 route, use it
|
|
1174
|
+
if (this.options.notFoundRoute) {
|
|
1175
|
+
matchedRoutes.push(this.options.notFoundRoute)
|
|
1176
|
+
} else {
|
|
1177
|
+
// If there is no routes found during path matching
|
|
1178
|
+
isGlobalNotFound = true
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const globalNotFoundRouteId = (() => {
|
|
1183
|
+
if (!isGlobalNotFound) {
|
|
1184
|
+
return undefined
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (this.options.notFoundMode !== 'root') {
|
|
1188
|
+
for (let i = matchedRoutes.length - 1; i >= 0; i--) {
|
|
1189
|
+
const route = matchedRoutes[i]!
|
|
1190
|
+
if (route.children) {
|
|
1191
|
+
return route.id
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return rootRouteId
|
|
1197
|
+
})()
|
|
1198
|
+
|
|
1199
|
+
const parseErrors = matchedRoutes.map((route) => {
|
|
1200
|
+
let parsedParamsError
|
|
1201
|
+
|
|
1202
|
+
const parseParams =
|
|
1203
|
+
route.options.params?.parse ?? route.options.parseParams
|
|
1204
|
+
|
|
1205
|
+
if (parseParams) {
|
|
1206
|
+
try {
|
|
1207
|
+
const parsedParams = parseParams(routeParams)
|
|
1208
|
+
// Add the parsed params to the accumulated params bag
|
|
1209
|
+
Object.assign(routeParams, parsedParams)
|
|
1210
|
+
} catch (err: any) {
|
|
1211
|
+
parsedParamsError = new PathParamError(err.message, {
|
|
1212
|
+
cause: err,
|
|
1213
|
+
})
|
|
1214
|
+
|
|
1215
|
+
if (opts?.throwOnError) {
|
|
1216
|
+
throw parsedParamsError
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
return parsedParamsError
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
return
|
|
1224
|
+
})
|
|
1225
|
+
|
|
1226
|
+
const matches: Array<AnyRouteMatch> = []
|
|
1227
|
+
|
|
1228
|
+
const getParentContext = (parentMatch?: AnyRouteMatch) => {
|
|
1229
|
+
const parentMatchId = parentMatch?.id
|
|
1230
|
+
|
|
1231
|
+
const parentContext = !parentMatchId
|
|
1232
|
+
? ((this.options.context as any) ?? {})
|
|
1233
|
+
: (parentMatch.context ?? this.options.context ?? {})
|
|
1234
|
+
|
|
1235
|
+
return parentContext
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
matchedRoutes.forEach((route, index) => {
|
|
1239
|
+
// Take each matched route and resolve + validate its search params
|
|
1240
|
+
// This has to happen serially because each route's search params
|
|
1241
|
+
// can depend on the parent route's search params
|
|
1242
|
+
// It must also happen before we create the match so that we can
|
|
1243
|
+
// pass the search params to the route's potential key function
|
|
1244
|
+
// which is used to uniquely identify the route match in state
|
|
1245
|
+
|
|
1246
|
+
const parentMatch = matches[index - 1]
|
|
1247
|
+
|
|
1248
|
+
const [preMatchSearch, strictMatchSearch, searchError]: [
|
|
1249
|
+
Record<string, any>,
|
|
1250
|
+
Record<string, any>,
|
|
1251
|
+
any,
|
|
1252
|
+
] = (() => {
|
|
1253
|
+
// Validate the search params and stabilize them
|
|
1254
|
+
const parentSearch = parentMatch?.search ?? next.search
|
|
1255
|
+
const parentStrictSearch = parentMatch?._strictSearch ?? {}
|
|
1256
|
+
|
|
1257
|
+
try {
|
|
1258
|
+
const strictSearch =
|
|
1259
|
+
validateSearch(route.options.validateSearch, { ...parentSearch }) ??
|
|
1260
|
+
{}
|
|
1261
|
+
|
|
1262
|
+
return [
|
|
1263
|
+
{
|
|
1264
|
+
...parentSearch,
|
|
1265
|
+
...strictSearch,
|
|
1266
|
+
},
|
|
1267
|
+
{ ...parentStrictSearch, ...strictSearch },
|
|
1268
|
+
undefined,
|
|
1269
|
+
]
|
|
1270
|
+
} catch (err: any) {
|
|
1271
|
+
let searchParamError = err
|
|
1272
|
+
if (!(err instanceof SearchParamError)) {
|
|
1273
|
+
searchParamError = new SearchParamError(err.message, {
|
|
1274
|
+
cause: err,
|
|
1275
|
+
})
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (opts?.throwOnError) {
|
|
1279
|
+
throw searchParamError
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
return [parentSearch, {}, searchParamError]
|
|
1283
|
+
}
|
|
1284
|
+
})()
|
|
1285
|
+
|
|
1286
|
+
// This is where we need to call route.options.loaderDeps() to get any additional
|
|
1287
|
+
// deps that the route's loader function might need to run. We need to do this
|
|
1288
|
+
// before we create the match so that we can pass the deps to the route's
|
|
1289
|
+
// potential key function which is used to uniquely identify the route match in state
|
|
1290
|
+
|
|
1291
|
+
const loaderDeps =
|
|
1292
|
+
route.options.loaderDeps?.({
|
|
1293
|
+
search: preMatchSearch,
|
|
1294
|
+
}) ?? ''
|
|
1295
|
+
|
|
1296
|
+
const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : ''
|
|
1297
|
+
|
|
1298
|
+
const { usedParams, interpolatedPath } = interpolatePath({
|
|
1299
|
+
path: route.fullPath,
|
|
1300
|
+
params: routeParams,
|
|
1301
|
+
decodeCharMap: this.pathParamsDecodeCharMap,
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
const matchId =
|
|
1305
|
+
interpolatePath({
|
|
1306
|
+
path: route.id,
|
|
1307
|
+
params: routeParams,
|
|
1308
|
+
leaveWildcards: true,
|
|
1309
|
+
decodeCharMap: this.pathParamsDecodeCharMap,
|
|
1310
|
+
}).interpolatedPath + loaderDepsHash
|
|
1311
|
+
|
|
1312
|
+
// Waste not, want not. If we already have a match for this route,
|
|
1313
|
+
// reuse it. This is important for layout routes, which might stick
|
|
1314
|
+
// around between navigation actions that only change leaf routes.
|
|
1315
|
+
|
|
1316
|
+
// Existing matches are matches that are already loaded along with
|
|
1317
|
+
// pending matches that are still loading
|
|
1318
|
+
const existingMatch = this.getMatch(matchId)
|
|
1319
|
+
|
|
1320
|
+
const previousMatch = this.state.matches.find(
|
|
1321
|
+
(d) => d.routeId === route.id,
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
const cause = previousMatch ? 'stay' : 'enter'
|
|
1325
|
+
|
|
1326
|
+
let match: AnyRouteMatch
|
|
1327
|
+
|
|
1328
|
+
if (existingMatch) {
|
|
1329
|
+
match = {
|
|
1330
|
+
...existingMatch,
|
|
1331
|
+
cause,
|
|
1332
|
+
params: previousMatch
|
|
1333
|
+
? replaceEqualDeep(previousMatch.params, routeParams)
|
|
1334
|
+
: routeParams,
|
|
1335
|
+
_strictParams: usedParams,
|
|
1336
|
+
search: previousMatch
|
|
1337
|
+
? replaceEqualDeep(previousMatch.search, preMatchSearch)
|
|
1338
|
+
: replaceEqualDeep(existingMatch.search, preMatchSearch),
|
|
1339
|
+
_strictSearch: strictMatchSearch,
|
|
1340
|
+
}
|
|
1341
|
+
} else {
|
|
1342
|
+
const status =
|
|
1343
|
+
route.options.loader ||
|
|
1344
|
+
route.options.beforeLoad ||
|
|
1345
|
+
route.lazyFn ||
|
|
1346
|
+
routeNeedsPreload(route)
|
|
1347
|
+
? 'pending'
|
|
1348
|
+
: 'success'
|
|
1349
|
+
|
|
1350
|
+
match = {
|
|
1351
|
+
id: matchId,
|
|
1352
|
+
index,
|
|
1353
|
+
routeId: route.id,
|
|
1354
|
+
params: previousMatch
|
|
1355
|
+
? replaceEqualDeep(previousMatch.params, routeParams)
|
|
1356
|
+
: routeParams,
|
|
1357
|
+
_strictParams: usedParams,
|
|
1358
|
+
pathname: joinPaths([this.basepath, interpolatedPath]),
|
|
1359
|
+
updatedAt: Date.now(),
|
|
1360
|
+
search: previousMatch
|
|
1361
|
+
? replaceEqualDeep(previousMatch.search, preMatchSearch)
|
|
1362
|
+
: preMatchSearch,
|
|
1363
|
+
_strictSearch: strictMatchSearch,
|
|
1364
|
+
searchError: undefined,
|
|
1365
|
+
status,
|
|
1366
|
+
isFetching: false,
|
|
1367
|
+
error: undefined,
|
|
1368
|
+
paramsError: parseErrors[index],
|
|
1369
|
+
__routeContext: {},
|
|
1370
|
+
__beforeLoadContext: {},
|
|
1371
|
+
context: {},
|
|
1372
|
+
abortController: new AbortController(),
|
|
1373
|
+
fetchCount: 0,
|
|
1374
|
+
cause,
|
|
1375
|
+
loaderDeps: previousMatch
|
|
1376
|
+
? replaceEqualDeep(previousMatch.loaderDeps, loaderDeps)
|
|
1377
|
+
: loaderDeps,
|
|
1378
|
+
invalid: false,
|
|
1379
|
+
preload: false,
|
|
1380
|
+
links: undefined,
|
|
1381
|
+
scripts: undefined,
|
|
1382
|
+
headScripts: undefined,
|
|
1383
|
+
meta: undefined,
|
|
1384
|
+
staticData: route.options.staticData || {},
|
|
1385
|
+
loadPromise: createControlledPromise(),
|
|
1386
|
+
fullPath: route.fullPath,
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (!opts?.preload) {
|
|
1391
|
+
// If we have a global not found, mark the right match as global not found
|
|
1392
|
+
match.globalNotFound = globalNotFoundRouteId === route.id
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// update the searchError if there is one
|
|
1396
|
+
match.searchError = searchError
|
|
1397
|
+
|
|
1398
|
+
const parentContext = getParentContext(parentMatch)
|
|
1399
|
+
|
|
1400
|
+
match.context = {
|
|
1401
|
+
...parentContext,
|
|
1402
|
+
...match.__routeContext,
|
|
1403
|
+
...match.__beforeLoadContext,
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
matches.push(match)
|
|
1407
|
+
})
|
|
1408
|
+
|
|
1409
|
+
matches.forEach((match, index) => {
|
|
1410
|
+
const route = this.looseRoutesById[match.routeId]!
|
|
1411
|
+
const existingMatch = this.getMatch(match.id)
|
|
1412
|
+
|
|
1413
|
+
// only execute `context` if we are not just building a location
|
|
1414
|
+
if (!existingMatch && opts?._buildLocation !== true) {
|
|
1415
|
+
const parentMatch = matches[index - 1]
|
|
1416
|
+
const parentContext = getParentContext(parentMatch)
|
|
1417
|
+
|
|
1418
|
+
// Update the match's context
|
|
1419
|
+
const contextFnContext: RouteContextOptions<any, any, any, any> = {
|
|
1420
|
+
deps: match.loaderDeps,
|
|
1421
|
+
params: match.params,
|
|
1422
|
+
context: parentContext,
|
|
1423
|
+
location: next,
|
|
1424
|
+
navigate: (opts: any) =>
|
|
1425
|
+
this.navigate({ ...opts, _fromLocation: next }),
|
|
1426
|
+
buildLocation: this.buildLocation,
|
|
1427
|
+
cause: match.cause,
|
|
1428
|
+
abortController: match.abortController,
|
|
1429
|
+
preload: !!match.preload,
|
|
1430
|
+
matches,
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Get the route context
|
|
1434
|
+
match.__routeContext = route.options.context?.(contextFnContext) ?? {}
|
|
1435
|
+
|
|
1436
|
+
match.context = {
|
|
1437
|
+
...parentContext,
|
|
1438
|
+
...match.__routeContext,
|
|
1439
|
+
...match.__beforeLoadContext,
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// If it's already a success, update headers and head content
|
|
1444
|
+
// These may get updated again if the match is refreshed
|
|
1445
|
+
// due to being stale
|
|
1446
|
+
if (match.status === 'success') {
|
|
1447
|
+
match.headers = route.options.headers?.({
|
|
1448
|
+
loaderData: match.loaderData,
|
|
1449
|
+
})
|
|
1450
|
+
const assetContext = {
|
|
1451
|
+
matches,
|
|
1452
|
+
match,
|
|
1453
|
+
params: match.params,
|
|
1454
|
+
loaderData: match.loaderData,
|
|
1455
|
+
}
|
|
1456
|
+
const headFnContent = route.options.head?.(assetContext)
|
|
1457
|
+
match.links = headFnContent?.links
|
|
1458
|
+
match.headScripts = headFnContent?.scripts
|
|
1459
|
+
match.meta = headFnContent?.meta
|
|
1460
|
+
match.scripts = route.options.scripts?.(assetContext)
|
|
1461
|
+
}
|
|
1462
|
+
})
|
|
1463
|
+
|
|
1464
|
+
return matches
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
getMatchedRoutes: GetMatchRoutesFn = (next, dest) => {
|
|
1468
|
+
let routeParams: Record<string, string> = {}
|
|
1469
|
+
const trimmedPath = trimPathRight(next.pathname)
|
|
1470
|
+
const getMatchedParams = (route: AnyRoute) => {
|
|
1471
|
+
const result = matchPathname(this.basepath, trimmedPath, {
|
|
1472
|
+
to: route.fullPath,
|
|
1473
|
+
caseSensitive:
|
|
1474
|
+
route.options.caseSensitive ?? this.options.caseSensitive,
|
|
1475
|
+
fuzzy: true,
|
|
1476
|
+
})
|
|
1477
|
+
return result
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
let foundRoute: AnyRoute | undefined =
|
|
1481
|
+
dest?.to !== undefined ? this.routesByPath[dest.to!] : undefined
|
|
1482
|
+
if (foundRoute) {
|
|
1483
|
+
routeParams = getMatchedParams(foundRoute)!
|
|
1484
|
+
} else {
|
|
1485
|
+
foundRoute = this.flatRoutes.find((route) => {
|
|
1486
|
+
const matchedParams = getMatchedParams(route)
|
|
1487
|
+
|
|
1488
|
+
if (matchedParams) {
|
|
1489
|
+
routeParams = matchedParams
|
|
1490
|
+
return true
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return false
|
|
1494
|
+
})
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
let routeCursor: AnyRoute =
|
|
1498
|
+
foundRoute || (this.routesById as any)[rootRouteId]
|
|
1499
|
+
|
|
1500
|
+
const matchedRoutes: Array<AnyRoute> = [routeCursor]
|
|
1501
|
+
|
|
1502
|
+
while (routeCursor.parentRoute) {
|
|
1503
|
+
routeCursor = routeCursor.parentRoute
|
|
1504
|
+
matchedRoutes.unshift(routeCursor)
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
return { matchedRoutes, routeParams, foundRoute }
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
cancelMatch = (id: string) => {
|
|
1511
|
+
const match = this.getMatch(id)
|
|
1512
|
+
|
|
1513
|
+
if (!match) return
|
|
1514
|
+
|
|
1515
|
+
match.abortController.abort()
|
|
1516
|
+
clearTimeout(match.pendingTimeout)
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
cancelMatches = () => {
|
|
1520
|
+
this.state.pendingMatches?.forEach((match) => {
|
|
1521
|
+
this.cancelMatch(match.id)
|
|
1522
|
+
})
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
buildLocation: BuildLocationFn = (opts) => {
|
|
1526
|
+
const build = (
|
|
1527
|
+
dest: BuildNextOptions & {
|
|
1528
|
+
unmaskOnReload?: boolean
|
|
1529
|
+
} = {},
|
|
1530
|
+
matchedRoutesResult?: MatchedRoutesResult,
|
|
1531
|
+
): ParsedLocation => {
|
|
1532
|
+
const fromMatches = dest._fromLocation
|
|
1533
|
+
? this.matchRoutes(dest._fromLocation, { _buildLocation: true })
|
|
1534
|
+
: this.state.matches
|
|
1535
|
+
|
|
1536
|
+
const fromMatch =
|
|
1537
|
+
dest.from != null
|
|
1538
|
+
? fromMatches.find((d) =>
|
|
1539
|
+
matchPathname(this.basepath, trimPathRight(d.pathname), {
|
|
1540
|
+
to: dest.from,
|
|
1541
|
+
caseSensitive: false,
|
|
1542
|
+
fuzzy: false,
|
|
1543
|
+
}),
|
|
1544
|
+
)
|
|
1545
|
+
: undefined
|
|
1546
|
+
|
|
1547
|
+
const fromPath = fromMatch?.pathname || this.latestLocation.pathname
|
|
1548
|
+
|
|
1549
|
+
invariant(
|
|
1550
|
+
dest.from == null || fromMatch != null,
|
|
1551
|
+
'Could not find match for from: ' + dest.from,
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
const fromSearch = this.state.pendingMatches?.length
|
|
1555
|
+
? last(this.state.pendingMatches)?.search
|
|
1556
|
+
: last(fromMatches)?.search || this.latestLocation.search
|
|
1557
|
+
|
|
1558
|
+
const stayingMatches = matchedRoutesResult?.matchedRoutes.filter((d) =>
|
|
1559
|
+
fromMatches.find((e) => e.routeId === d.id),
|
|
1560
|
+
)
|
|
1561
|
+
let pathname: string
|
|
1562
|
+
if (dest.to) {
|
|
1563
|
+
const resolvePathTo =
|
|
1564
|
+
fromMatch?.fullPath ||
|
|
1565
|
+
last(fromMatches)?.fullPath ||
|
|
1566
|
+
this.latestLocation.pathname
|
|
1567
|
+
pathname = this.resolvePathWithBase(resolvePathTo, `${dest.to}`)
|
|
1568
|
+
} else {
|
|
1569
|
+
const fromRouteByFromPathRouteId =
|
|
1570
|
+
this.routesById[
|
|
1571
|
+
stayingMatches?.find((route) => {
|
|
1572
|
+
const interpolatedPath = interpolatePath({
|
|
1573
|
+
path: route.fullPath,
|
|
1574
|
+
params: matchedRoutesResult?.routeParams ?? {},
|
|
1575
|
+
decodeCharMap: this.pathParamsDecodeCharMap,
|
|
1576
|
+
}).interpolatedPath
|
|
1577
|
+
const pathname = joinPaths([this.basepath, interpolatedPath])
|
|
1578
|
+
return pathname === fromPath
|
|
1579
|
+
})?.id as keyof this['routesById']
|
|
1580
|
+
]
|
|
1581
|
+
pathname = this.resolvePathWithBase(
|
|
1582
|
+
fromPath,
|
|
1583
|
+
fromRouteByFromPathRouteId?.to ?? fromPath,
|
|
1584
|
+
)
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const prevParams = { ...last(fromMatches)?.params }
|
|
1588
|
+
|
|
1589
|
+
let nextParams =
|
|
1590
|
+
(dest.params ?? true) === true
|
|
1591
|
+
? prevParams
|
|
1592
|
+
: {
|
|
1593
|
+
...prevParams,
|
|
1594
|
+
...functionalUpdate(dest.params as any, prevParams),
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (Object.keys(nextParams).length > 0) {
|
|
1598
|
+
matchedRoutesResult?.matchedRoutes
|
|
1599
|
+
.map((route) => {
|
|
1600
|
+
return (
|
|
1601
|
+
route.options.params?.stringify ?? route.options.stringifyParams
|
|
1602
|
+
)
|
|
1603
|
+
})
|
|
1604
|
+
.filter(Boolean)
|
|
1605
|
+
.forEach((fn) => {
|
|
1606
|
+
nextParams = { ...nextParams!, ...fn!(nextParams) }
|
|
1607
|
+
})
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
pathname = interpolatePath({
|
|
1611
|
+
path: pathname,
|
|
1612
|
+
params: nextParams ?? {},
|
|
1613
|
+
leaveWildcards: false,
|
|
1614
|
+
leaveParams: opts.leaveParams,
|
|
1615
|
+
decodeCharMap: this.pathParamsDecodeCharMap,
|
|
1616
|
+
}).interpolatedPath
|
|
1617
|
+
|
|
1618
|
+
let search = fromSearch
|
|
1619
|
+
if (opts._includeValidateSearch && this.options.search?.strict) {
|
|
1620
|
+
let validatedSearch = {}
|
|
1621
|
+
matchedRoutesResult?.matchedRoutes.forEach((route) => {
|
|
1622
|
+
try {
|
|
1623
|
+
if (route.options.validateSearch) {
|
|
1624
|
+
validatedSearch = {
|
|
1625
|
+
...validatedSearch,
|
|
1626
|
+
...(validateSearch(route.options.validateSearch, {
|
|
1627
|
+
...validatedSearch,
|
|
1628
|
+
...search,
|
|
1629
|
+
}) ?? {}),
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
} catch {
|
|
1633
|
+
// ignore errors here because they are already handled in matchRoutes
|
|
1634
|
+
}
|
|
1635
|
+
})
|
|
1636
|
+
search = validatedSearch
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const applyMiddlewares = (search: any) => {
|
|
1640
|
+
const allMiddlewares =
|
|
1641
|
+
matchedRoutesResult?.matchedRoutes.reduce(
|
|
1642
|
+
(acc, route) => {
|
|
1643
|
+
const middlewares: Array<SearchMiddleware<any>> = []
|
|
1644
|
+
if ('search' in route.options) {
|
|
1645
|
+
if (route.options.search?.middlewares) {
|
|
1646
|
+
middlewares.push(...route.options.search.middlewares)
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
// TODO remove preSearchFilters and postSearchFilters in v2
|
|
1650
|
+
else if (
|
|
1651
|
+
route.options.preSearchFilters ||
|
|
1652
|
+
route.options.postSearchFilters
|
|
1653
|
+
) {
|
|
1654
|
+
const legacyMiddleware: SearchMiddleware<any> = ({
|
|
1655
|
+
search,
|
|
1656
|
+
next,
|
|
1657
|
+
}) => {
|
|
1658
|
+
let nextSearch = search
|
|
1659
|
+
if (
|
|
1660
|
+
'preSearchFilters' in route.options &&
|
|
1661
|
+
route.options.preSearchFilters
|
|
1662
|
+
) {
|
|
1663
|
+
nextSearch = route.options.preSearchFilters.reduce(
|
|
1664
|
+
(prev, next) => next(prev),
|
|
1665
|
+
search,
|
|
1666
|
+
)
|
|
1667
|
+
}
|
|
1668
|
+
const result = next(nextSearch)
|
|
1669
|
+
if (
|
|
1670
|
+
'postSearchFilters' in route.options &&
|
|
1671
|
+
route.options.postSearchFilters
|
|
1672
|
+
) {
|
|
1673
|
+
return route.options.postSearchFilters.reduce(
|
|
1674
|
+
(prev, next) => next(prev),
|
|
1675
|
+
result,
|
|
1676
|
+
)
|
|
1677
|
+
}
|
|
1678
|
+
return result
|
|
1679
|
+
}
|
|
1680
|
+
middlewares.push(legacyMiddleware)
|
|
1681
|
+
}
|
|
1682
|
+
if (opts._includeValidateSearch && route.options.validateSearch) {
|
|
1683
|
+
const validate: SearchMiddleware<any> = ({ search, next }) => {
|
|
1684
|
+
const result = next(search)
|
|
1685
|
+
try {
|
|
1686
|
+
const validatedSearch = {
|
|
1687
|
+
...result,
|
|
1688
|
+
...(validateSearch(
|
|
1689
|
+
route.options.validateSearch,
|
|
1690
|
+
result,
|
|
1691
|
+
) ?? {}),
|
|
1692
|
+
}
|
|
1693
|
+
return validatedSearch
|
|
1694
|
+
} catch {
|
|
1695
|
+
// ignore errors here because they are already handled in matchRoutes
|
|
1696
|
+
return result
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
middlewares.push(validate)
|
|
1700
|
+
}
|
|
1701
|
+
return acc.concat(middlewares)
|
|
1702
|
+
},
|
|
1703
|
+
[] as Array<SearchMiddleware<any>>,
|
|
1704
|
+
) ?? []
|
|
1705
|
+
|
|
1706
|
+
// the chain ends here since `next` is not called
|
|
1707
|
+
const final: SearchMiddleware<any> = ({ search }) => {
|
|
1708
|
+
if (!dest.search) {
|
|
1709
|
+
return {}
|
|
1710
|
+
}
|
|
1711
|
+
if (dest.search === true) {
|
|
1712
|
+
return search
|
|
1713
|
+
}
|
|
1714
|
+
return functionalUpdate(dest.search, search)
|
|
1715
|
+
}
|
|
1716
|
+
allMiddlewares.push(final)
|
|
1717
|
+
|
|
1718
|
+
const applyNext = (index: number, currentSearch: any): any => {
|
|
1719
|
+
// no more middlewares left, return the current search
|
|
1720
|
+
if (index >= allMiddlewares.length) {
|
|
1721
|
+
return currentSearch
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const middleware = allMiddlewares[index]!
|
|
1725
|
+
|
|
1726
|
+
const next = (newSearch: any): any => {
|
|
1727
|
+
return applyNext(index + 1, newSearch)
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
return middleware({ search: currentSearch, next })
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Start applying middlewares
|
|
1734
|
+
return applyNext(0, search)
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
search = applyMiddlewares(search)
|
|
1738
|
+
|
|
1739
|
+
search = replaceEqualDeep(fromSearch, search)
|
|
1740
|
+
const searchStr = this.options.stringifySearch(search)
|
|
1741
|
+
|
|
1742
|
+
const hash =
|
|
1743
|
+
dest.hash === true
|
|
1744
|
+
? this.latestLocation.hash
|
|
1745
|
+
: dest.hash
|
|
1746
|
+
? functionalUpdate(dest.hash, this.latestLocation.hash)
|
|
1747
|
+
: undefined
|
|
1748
|
+
|
|
1749
|
+
const hashStr = hash ? `#${hash}` : ''
|
|
1750
|
+
|
|
1751
|
+
let nextState =
|
|
1752
|
+
dest.state === true
|
|
1753
|
+
? this.latestLocation.state
|
|
1754
|
+
: dest.state
|
|
1755
|
+
? functionalUpdate(dest.state, this.latestLocation.state)
|
|
1756
|
+
: {}
|
|
1757
|
+
|
|
1758
|
+
nextState = replaceEqualDeep(this.latestLocation.state, nextState)
|
|
1759
|
+
|
|
1760
|
+
return {
|
|
1761
|
+
pathname,
|
|
1762
|
+
search,
|
|
1763
|
+
searchStr,
|
|
1764
|
+
state: nextState as any,
|
|
1765
|
+
hash: hash ?? '',
|
|
1766
|
+
href: `${pathname}${searchStr}${hashStr}`,
|
|
1767
|
+
unmaskOnReload: dest.unmaskOnReload,
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const buildWithMatches = (
|
|
1772
|
+
dest: BuildNextOptions = {},
|
|
1773
|
+
maskedDest?: BuildNextOptions,
|
|
1774
|
+
) => {
|
|
1775
|
+
const next = build(dest)
|
|
1776
|
+
let maskedNext = maskedDest ? build(maskedDest) : undefined
|
|
1777
|
+
|
|
1778
|
+
if (!maskedNext) {
|
|
1779
|
+
let params = {}
|
|
1780
|
+
|
|
1781
|
+
const foundMask = this.options.routeMasks?.find((d) => {
|
|
1782
|
+
const match = matchPathname(this.basepath, next.pathname, {
|
|
1783
|
+
to: d.from,
|
|
1784
|
+
caseSensitive: false,
|
|
1785
|
+
fuzzy: false,
|
|
1786
|
+
})
|
|
1787
|
+
|
|
1788
|
+
if (match) {
|
|
1789
|
+
params = match
|
|
1790
|
+
return true
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
return false
|
|
1794
|
+
})
|
|
1795
|
+
|
|
1796
|
+
if (foundMask) {
|
|
1797
|
+
const { from: _from, ...maskProps } = foundMask
|
|
1798
|
+
maskedDest = {
|
|
1799
|
+
...pick(opts, ['from']),
|
|
1800
|
+
...maskProps,
|
|
1801
|
+
params,
|
|
1802
|
+
}
|
|
1803
|
+
maskedNext = build(maskedDest)
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
const nextMatches = this.getMatchedRoutes(next, dest)
|
|
1808
|
+
const final = build(dest, nextMatches)
|
|
1809
|
+
|
|
1810
|
+
if (maskedNext) {
|
|
1811
|
+
const maskedMatches = this.getMatchedRoutes(maskedNext, maskedDest)
|
|
1812
|
+
const maskedFinal = build(maskedDest, maskedMatches)
|
|
1813
|
+
final.maskedLocation = maskedFinal
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
return final
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
if (opts.mask) {
|
|
1820
|
+
return buildWithMatches(opts, {
|
|
1821
|
+
...pick(opts, ['from']),
|
|
1822
|
+
...opts.mask,
|
|
1823
|
+
})
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
return buildWithMatches(opts)
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
commitLocationPromise: undefined | ControlledPromise<void>
|
|
1830
|
+
|
|
1831
|
+
commitLocation: CommitLocationFn = ({
|
|
1832
|
+
viewTransition,
|
|
1833
|
+
ignoreBlocker,
|
|
1834
|
+
...next
|
|
1835
|
+
}) => {
|
|
1836
|
+
const isSameState = () => {
|
|
1837
|
+
// the following props are ignored but may still be provided when navigating,
|
|
1838
|
+
// temporarily add the previous values to the next state so they don't affect
|
|
1839
|
+
// the comparison
|
|
1840
|
+
const ignoredProps = [
|
|
1841
|
+
'key',
|
|
1842
|
+
'__TSR_index',
|
|
1843
|
+
'__hashScrollIntoViewOptions',
|
|
1844
|
+
] as const
|
|
1845
|
+
ignoredProps.forEach((prop) => {
|
|
1846
|
+
;(next.state as any)[prop] = this.latestLocation.state[prop]
|
|
1847
|
+
})
|
|
1848
|
+
const isEqual = deepEqual(next.state, this.latestLocation.state)
|
|
1849
|
+
ignoredProps.forEach((prop) => {
|
|
1850
|
+
delete next.state[prop]
|
|
1851
|
+
})
|
|
1852
|
+
return isEqual
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const isSameUrl = this.latestLocation.href === next.href
|
|
1856
|
+
|
|
1857
|
+
const previousCommitPromise = this.commitLocationPromise
|
|
1858
|
+
this.commitLocationPromise = createControlledPromise<void>(() => {
|
|
1859
|
+
previousCommitPromise?.resolve()
|
|
1860
|
+
})
|
|
1861
|
+
|
|
1862
|
+
// Don't commit to history if nothing changed
|
|
1863
|
+
if (isSameUrl && isSameState()) {
|
|
1864
|
+
this.load()
|
|
1865
|
+
} else {
|
|
1866
|
+
// eslint-disable-next-line prefer-const
|
|
1867
|
+
let { maskedLocation, hashScrollIntoView, ...nextHistory } = next
|
|
1868
|
+
|
|
1869
|
+
if (maskedLocation) {
|
|
1870
|
+
nextHistory = {
|
|
1871
|
+
...maskedLocation,
|
|
1872
|
+
state: {
|
|
1873
|
+
...maskedLocation.state,
|
|
1874
|
+
__tempKey: undefined,
|
|
1875
|
+
__tempLocation: {
|
|
1876
|
+
...nextHistory,
|
|
1877
|
+
search: nextHistory.searchStr,
|
|
1878
|
+
state: {
|
|
1879
|
+
...nextHistory.state,
|
|
1880
|
+
__tempKey: undefined!,
|
|
1881
|
+
__tempLocation: undefined!,
|
|
1882
|
+
key: undefined!,
|
|
1883
|
+
},
|
|
1884
|
+
},
|
|
1885
|
+
},
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
if (
|
|
1889
|
+
nextHistory.unmaskOnReload ??
|
|
1890
|
+
this.options.unmaskOnReload ??
|
|
1891
|
+
false
|
|
1892
|
+
) {
|
|
1893
|
+
nextHistory.state.__tempKey = this.tempLocationKey
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
nextHistory.state.__hashScrollIntoViewOptions =
|
|
1898
|
+
hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true
|
|
1899
|
+
|
|
1900
|
+
this.shouldViewTransition = viewTransition
|
|
1901
|
+
|
|
1902
|
+
this.history[next.replace ? 'replace' : 'push'](
|
|
1903
|
+
nextHistory.href,
|
|
1904
|
+
nextHistory.state,
|
|
1905
|
+
{ ignoreBlocker },
|
|
1906
|
+
)
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
this.resetNextScroll = next.resetScroll ?? true
|
|
1910
|
+
|
|
1911
|
+
if (!this.history.subscribers.size) {
|
|
1912
|
+
this.load()
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
return this.commitLocationPromise
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
buildAndCommitLocation = ({
|
|
1919
|
+
replace,
|
|
1920
|
+
resetScroll,
|
|
1921
|
+
hashScrollIntoView,
|
|
1922
|
+
viewTransition,
|
|
1923
|
+
ignoreBlocker,
|
|
1924
|
+
href,
|
|
1925
|
+
...rest
|
|
1926
|
+
}: BuildNextOptions & CommitLocationOptions = {}) => {
|
|
1927
|
+
if (href) {
|
|
1928
|
+
const currentIndex = this.history.location.state.__TSR_index
|
|
1929
|
+
const parsed = parseHref(href, {
|
|
1930
|
+
__TSR_index: replace ? currentIndex : currentIndex + 1,
|
|
1931
|
+
})
|
|
1932
|
+
rest.to = parsed.pathname
|
|
1933
|
+
rest.search = this.options.parseSearch(parsed.search)
|
|
1934
|
+
// remove the leading `#` from the hash
|
|
1935
|
+
rest.hash = parsed.hash.slice(1)
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
const location = this.buildLocation({
|
|
1939
|
+
...(rest as any),
|
|
1940
|
+
_includeValidateSearch: true,
|
|
1941
|
+
})
|
|
1942
|
+
return this.commitLocation({
|
|
1943
|
+
...location,
|
|
1944
|
+
viewTransition,
|
|
1945
|
+
replace,
|
|
1946
|
+
resetScroll,
|
|
1947
|
+
hashScrollIntoView,
|
|
1948
|
+
ignoreBlocker,
|
|
1949
|
+
})
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
|
|
1953
|
+
if (reloadDocument) {
|
|
1954
|
+
if (!href) {
|
|
1955
|
+
const location = this.buildLocation({ to, ...rest } as any)
|
|
1956
|
+
href = this.history.createHref(location.href)
|
|
1957
|
+
}
|
|
1958
|
+
if (rest.replace) {
|
|
1959
|
+
window.location.replace(href)
|
|
1960
|
+
} else {
|
|
1961
|
+
window.location.href = href
|
|
1962
|
+
}
|
|
1963
|
+
return
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
return this.buildAndCommitLocation({
|
|
1967
|
+
...rest,
|
|
1968
|
+
href,
|
|
1969
|
+
to: to as string,
|
|
1970
|
+
})
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
latestLoadPromise: undefined | Promise<void>
|
|
1974
|
+
|
|
1975
|
+
load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
|
|
1976
|
+
this.latestLocation = this.parseLocation(this.latestLocation)
|
|
1977
|
+
|
|
1978
|
+
let redirect: ResolvedRedirect | undefined
|
|
1979
|
+
let notFound: NotFoundError | undefined
|
|
1980
|
+
|
|
1981
|
+
let loadPromise: Promise<void>
|
|
1982
|
+
|
|
1983
|
+
// eslint-disable-next-line prefer-const
|
|
1984
|
+
loadPromise = new Promise<void>((resolve) => {
|
|
1985
|
+
this.startTransition(async () => {
|
|
1986
|
+
try {
|
|
1987
|
+
const next = this.latestLocation
|
|
1988
|
+
const prevLocation = this.state.resolvedLocation
|
|
1989
|
+
|
|
1990
|
+
// Cancel any pending matches
|
|
1991
|
+
this.cancelMatches()
|
|
1992
|
+
|
|
1993
|
+
let pendingMatches!: Array<AnyRouteMatch>
|
|
1994
|
+
|
|
1995
|
+
batch(() => {
|
|
1996
|
+
// this call breaks a route context of destination route after a redirect
|
|
1997
|
+
// we should be fine not eagerly calling this since we call it later
|
|
1998
|
+
// this.clearExpiredCache()
|
|
1999
|
+
|
|
2000
|
+
// Match the routes
|
|
2001
|
+
pendingMatches = this.matchRoutes(next)
|
|
2002
|
+
|
|
2003
|
+
// Ingest the new matches
|
|
2004
|
+
this.__store.setState((s) => ({
|
|
2005
|
+
...s,
|
|
2006
|
+
status: 'pending',
|
|
2007
|
+
isLoading: true,
|
|
2008
|
+
location: next,
|
|
2009
|
+
pendingMatches,
|
|
2010
|
+
// If a cached moved to pendingMatches, remove it from cachedMatches
|
|
2011
|
+
cachedMatches: s.cachedMatches.filter((d) => {
|
|
2012
|
+
return !pendingMatches.find((e) => e.id === d.id)
|
|
2013
|
+
}),
|
|
2014
|
+
}))
|
|
2015
|
+
})
|
|
2016
|
+
|
|
2017
|
+
if (!this.state.redirect) {
|
|
2018
|
+
this.emit({
|
|
2019
|
+
type: 'onBeforeNavigate',
|
|
2020
|
+
...getLocationChangeInfo({
|
|
2021
|
+
resolvedLocation: prevLocation,
|
|
2022
|
+
location: next,
|
|
2023
|
+
}),
|
|
2024
|
+
})
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
this.emit({
|
|
2028
|
+
type: 'onBeforeLoad',
|
|
2029
|
+
...getLocationChangeInfo({
|
|
2030
|
+
resolvedLocation: prevLocation,
|
|
2031
|
+
location: next,
|
|
2032
|
+
}),
|
|
2033
|
+
})
|
|
2034
|
+
|
|
2035
|
+
await this.loadMatches({
|
|
2036
|
+
sync: opts?.sync,
|
|
2037
|
+
matches: pendingMatches,
|
|
2038
|
+
location: next,
|
|
2039
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
2040
|
+
onReady: async () => {
|
|
2041
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
2042
|
+
this.startViewTransition(async () => {
|
|
2043
|
+
// this.viewTransitionPromise = createControlledPromise<true>()
|
|
2044
|
+
|
|
2045
|
+
// Commit the pending matches. If a previous match was
|
|
2046
|
+
// removed, place it in the cachedMatches
|
|
2047
|
+
let exitingMatches!: Array<AnyRouteMatch>
|
|
2048
|
+
let enteringMatches!: Array<AnyRouteMatch>
|
|
2049
|
+
let stayingMatches!: Array<AnyRouteMatch>
|
|
2050
|
+
|
|
2051
|
+
batch(() => {
|
|
2052
|
+
this.__store.setState((s) => {
|
|
2053
|
+
const previousMatches = s.matches
|
|
2054
|
+
const newMatches = s.pendingMatches || s.matches
|
|
2055
|
+
|
|
2056
|
+
exitingMatches = previousMatches.filter(
|
|
2057
|
+
(match) => !newMatches.find((d) => d.id === match.id),
|
|
2058
|
+
)
|
|
2059
|
+
enteringMatches = newMatches.filter(
|
|
2060
|
+
(match) =>
|
|
2061
|
+
!previousMatches.find((d) => d.id === match.id),
|
|
2062
|
+
)
|
|
2063
|
+
stayingMatches = previousMatches.filter((match) =>
|
|
2064
|
+
newMatches.find((d) => d.id === match.id),
|
|
2065
|
+
)
|
|
2066
|
+
|
|
2067
|
+
return {
|
|
2068
|
+
...s,
|
|
2069
|
+
isLoading: false,
|
|
2070
|
+
loadedAt: Date.now(),
|
|
2071
|
+
matches: newMatches,
|
|
2072
|
+
pendingMatches: undefined,
|
|
2073
|
+
cachedMatches: [
|
|
2074
|
+
...s.cachedMatches,
|
|
2075
|
+
...exitingMatches.filter((d) => d.status !== 'error'),
|
|
2076
|
+
],
|
|
2077
|
+
}
|
|
2078
|
+
})
|
|
2079
|
+
this.clearExpiredCache()
|
|
2080
|
+
})
|
|
2081
|
+
|
|
2082
|
+
//
|
|
2083
|
+
;(
|
|
2084
|
+
[
|
|
2085
|
+
[exitingMatches, 'onLeave'],
|
|
2086
|
+
[enteringMatches, 'onEnter'],
|
|
2087
|
+
[stayingMatches, 'onStay'],
|
|
2088
|
+
] as const
|
|
2089
|
+
).forEach(([matches, hook]) => {
|
|
2090
|
+
matches.forEach((match) => {
|
|
2091
|
+
this.looseRoutesById[match.routeId]!.options[hook]?.(match)
|
|
2092
|
+
})
|
|
2093
|
+
})
|
|
2094
|
+
})
|
|
2095
|
+
},
|
|
2096
|
+
})
|
|
2097
|
+
} catch (err) {
|
|
2098
|
+
if (isResolvedRedirect(err)) {
|
|
2099
|
+
redirect = err
|
|
2100
|
+
if (!this.isServer) {
|
|
2101
|
+
this.navigate({
|
|
2102
|
+
...redirect,
|
|
2103
|
+
replace: true,
|
|
2104
|
+
ignoreBlocker: true,
|
|
2105
|
+
})
|
|
2106
|
+
}
|
|
2107
|
+
} else if (isNotFound(err)) {
|
|
2108
|
+
notFound = err
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
this.__store.setState((s) => ({
|
|
2112
|
+
...s,
|
|
2113
|
+
statusCode: redirect
|
|
2114
|
+
? redirect.statusCode
|
|
2115
|
+
: notFound
|
|
2116
|
+
? 404
|
|
2117
|
+
: s.matches.some((d) => d.status === 'error')
|
|
2118
|
+
? 500
|
|
2119
|
+
: 200,
|
|
2120
|
+
redirect,
|
|
2121
|
+
}))
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
if (this.latestLoadPromise === loadPromise) {
|
|
2125
|
+
this.commitLocationPromise?.resolve()
|
|
2126
|
+
this.latestLoadPromise = undefined
|
|
2127
|
+
this.commitLocationPromise = undefined
|
|
2128
|
+
}
|
|
2129
|
+
resolve()
|
|
2130
|
+
})
|
|
2131
|
+
})
|
|
2132
|
+
|
|
2133
|
+
this.latestLoadPromise = loadPromise
|
|
2134
|
+
|
|
2135
|
+
await loadPromise
|
|
2136
|
+
|
|
2137
|
+
while (
|
|
2138
|
+
(this.latestLoadPromise as any) &&
|
|
2139
|
+
loadPromise !== this.latestLoadPromise
|
|
2140
|
+
) {
|
|
2141
|
+
await this.latestLoadPromise
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
if (this.hasNotFoundMatch()) {
|
|
2145
|
+
this.__store.setState((s) => ({
|
|
2146
|
+
...s,
|
|
2147
|
+
statusCode: 404,
|
|
2148
|
+
}))
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
startViewTransition = (fn: () => Promise<void>) => {
|
|
2153
|
+
// Determine if we should start a view transition from the navigation
|
|
2154
|
+
// or from the router default
|
|
2155
|
+
const shouldViewTransition =
|
|
2156
|
+
this.shouldViewTransition ?? this.options.defaultViewTransition
|
|
2157
|
+
|
|
2158
|
+
// Reset the view transition flag
|
|
2159
|
+
delete this.shouldViewTransition
|
|
2160
|
+
// Attempt to start a view transition (or just apply the changes if we can't)
|
|
2161
|
+
if (
|
|
2162
|
+
shouldViewTransition &&
|
|
2163
|
+
typeof document !== 'undefined' &&
|
|
2164
|
+
'startViewTransition' in document &&
|
|
2165
|
+
typeof document.startViewTransition === 'function'
|
|
2166
|
+
) {
|
|
2167
|
+
// lib.dom.ts doesn't support viewTransition types variant yet.
|
|
2168
|
+
// TODO: Fix this when dom types are updated
|
|
2169
|
+
let startViewTransitionParams: any
|
|
2170
|
+
|
|
2171
|
+
if (
|
|
2172
|
+
typeof shouldViewTransition === 'object' &&
|
|
2173
|
+
this.isViewTransitionTypesSupported
|
|
2174
|
+
) {
|
|
2175
|
+
startViewTransitionParams = {
|
|
2176
|
+
update: fn,
|
|
2177
|
+
types: shouldViewTransition.types,
|
|
2178
|
+
}
|
|
2179
|
+
} else {
|
|
2180
|
+
startViewTransitionParams = fn
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
document.startViewTransition(startViewTransitionParams)
|
|
2184
|
+
} else {
|
|
2185
|
+
fn()
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
updateMatch: UpdateMatchFn = (id, updater) => {
|
|
2190
|
+
let updated!: AnyRouteMatch
|
|
2191
|
+
const isPending = this.state.pendingMatches?.find((d) => d.id === id)
|
|
2192
|
+
const isMatched = this.state.matches.find((d) => d.id === id)
|
|
2193
|
+
const isCached = this.state.cachedMatches.find((d) => d.id === id)
|
|
2194
|
+
|
|
2195
|
+
const matchesKey = isPending
|
|
2196
|
+
? 'pendingMatches'
|
|
2197
|
+
: isMatched
|
|
2198
|
+
? 'matches'
|
|
2199
|
+
: isCached
|
|
2200
|
+
? 'cachedMatches'
|
|
2201
|
+
: ''
|
|
2202
|
+
|
|
2203
|
+
if (matchesKey) {
|
|
2204
|
+
this.__store.setState((s) => ({
|
|
2205
|
+
...s,
|
|
2206
|
+
[matchesKey]: s[matchesKey]?.map((d) =>
|
|
2207
|
+
d.id === id ? (updated = updater(d)) : d,
|
|
2208
|
+
),
|
|
2209
|
+
}))
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
return updated
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
getMatch: GetMatchFn = (matchId: string) => {
|
|
2216
|
+
return [
|
|
2217
|
+
...this.state.cachedMatches,
|
|
2218
|
+
...(this.state.pendingMatches ?? []),
|
|
2219
|
+
...this.state.matches,
|
|
2220
|
+
].find((d) => d.id === matchId)
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
loadMatches = async ({
|
|
2224
|
+
location,
|
|
2225
|
+
matches,
|
|
2226
|
+
preload: allPreload,
|
|
2227
|
+
onReady,
|
|
2228
|
+
updateMatch = this.updateMatch,
|
|
2229
|
+
sync,
|
|
2230
|
+
}: {
|
|
2231
|
+
location: ParsedLocation
|
|
2232
|
+
matches: Array<AnyRouteMatch>
|
|
2233
|
+
preload?: boolean
|
|
2234
|
+
onReady?: () => Promise<void>
|
|
2235
|
+
updateMatch?: (
|
|
2236
|
+
id: string,
|
|
2237
|
+
updater: (match: AnyRouteMatch) => AnyRouteMatch,
|
|
2238
|
+
) => void
|
|
2239
|
+
getMatch?: (matchId: string) => AnyRouteMatch | undefined
|
|
2240
|
+
sync?: boolean
|
|
2241
|
+
}): Promise<Array<MakeRouteMatch>> => {
|
|
2242
|
+
let firstBadMatchIndex: number | undefined
|
|
2243
|
+
let rendered = false
|
|
2244
|
+
|
|
2245
|
+
const triggerOnReady = async () => {
|
|
2246
|
+
if (!rendered) {
|
|
2247
|
+
rendered = true
|
|
2248
|
+
await onReady?.()
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
const resolvePreload = (matchId: string) => {
|
|
2253
|
+
return !!(allPreload && !this.state.matches.find((d) => d.id === matchId))
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
if (!this.isServer && !this.state.matches.length) {
|
|
2257
|
+
triggerOnReady()
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
|
|
2261
|
+
if (isResolvedRedirect(err)) {
|
|
2262
|
+
if (!err.reloadDocument) {
|
|
2263
|
+
throw err
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
if (isRedirect(err) || isNotFound(err)) {
|
|
2268
|
+
updateMatch(match.id, (prev) => ({
|
|
2269
|
+
...prev,
|
|
2270
|
+
status: isRedirect(err)
|
|
2271
|
+
? 'redirected'
|
|
2272
|
+
: isNotFound(err)
|
|
2273
|
+
? 'notFound'
|
|
2274
|
+
: 'error',
|
|
2275
|
+
isFetching: false,
|
|
2276
|
+
error: err,
|
|
2277
|
+
beforeLoadPromise: undefined,
|
|
2278
|
+
loaderPromise: undefined,
|
|
2279
|
+
}))
|
|
2280
|
+
|
|
2281
|
+
if (!(err as any).routeId) {
|
|
2282
|
+
;(err as any).routeId = match.routeId
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
match.beforeLoadPromise?.resolve()
|
|
2286
|
+
match.loaderPromise?.resolve()
|
|
2287
|
+
match.loadPromise?.resolve()
|
|
2288
|
+
|
|
2289
|
+
if (isRedirect(err)) {
|
|
2290
|
+
rendered = true
|
|
2291
|
+
err = this.resolveRedirect({ ...err, _fromLocation: location })
|
|
2292
|
+
throw err
|
|
2293
|
+
} else if (isNotFound(err)) {
|
|
2294
|
+
this._handleNotFound(matches, err, {
|
|
2295
|
+
updateMatch,
|
|
2296
|
+
})
|
|
2297
|
+
this.serverSsr?.onMatchSettled({
|
|
2298
|
+
router: this,
|
|
2299
|
+
match: this.getMatch(match.id)!,
|
|
2300
|
+
})
|
|
2301
|
+
throw err
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
try {
|
|
2307
|
+
await new Promise<void>((resolveAll, rejectAll) => {
|
|
2308
|
+
;(async () => {
|
|
2309
|
+
try {
|
|
2310
|
+
const handleSerialError = (
|
|
2311
|
+
index: number,
|
|
2312
|
+
err: any,
|
|
2313
|
+
routerCode: string,
|
|
2314
|
+
) => {
|
|
2315
|
+
const { id: matchId, routeId } = matches[index]!
|
|
2316
|
+
const route = this.looseRoutesById[routeId]!
|
|
2317
|
+
|
|
2318
|
+
// Much like suspense, we use a promise here to know if
|
|
2319
|
+
// we've been outdated by a new loadMatches call and
|
|
2320
|
+
// should abort the current async operation
|
|
2321
|
+
if (err instanceof Promise) {
|
|
2322
|
+
throw err
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
err.routerCode = routerCode
|
|
2326
|
+
firstBadMatchIndex = firstBadMatchIndex ?? index
|
|
2327
|
+
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
|
|
2328
|
+
|
|
2329
|
+
try {
|
|
2330
|
+
route.options.onError?.(err)
|
|
2331
|
+
} catch (errorHandlerErr) {
|
|
2332
|
+
err = errorHandlerErr
|
|
2333
|
+
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
updateMatch(matchId, (prev) => {
|
|
2337
|
+
prev.beforeLoadPromise?.resolve()
|
|
2338
|
+
prev.loadPromise?.resolve()
|
|
2339
|
+
|
|
2340
|
+
return {
|
|
2341
|
+
...prev,
|
|
2342
|
+
error: err,
|
|
2343
|
+
status: 'error',
|
|
2344
|
+
isFetching: false,
|
|
2345
|
+
updatedAt: Date.now(),
|
|
2346
|
+
abortController: new AbortController(),
|
|
2347
|
+
beforeLoadPromise: undefined,
|
|
2348
|
+
}
|
|
2349
|
+
})
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
for (const [index, { id: matchId, routeId }] of matches.entries()) {
|
|
2353
|
+
const existingMatch = this.getMatch(matchId)!
|
|
2354
|
+
const parentMatchId = matches[index - 1]?.id
|
|
2355
|
+
|
|
2356
|
+
const route = this.looseRoutesById[routeId]!
|
|
2357
|
+
|
|
2358
|
+
const pendingMs =
|
|
2359
|
+
route.options.pendingMs ?? this.options.defaultPendingMs
|
|
2360
|
+
|
|
2361
|
+
const shouldPending = !!(
|
|
2362
|
+
onReady &&
|
|
2363
|
+
!this.isServer &&
|
|
2364
|
+
!resolvePreload(matchId) &&
|
|
2365
|
+
(route.options.loader || route.options.beforeLoad) &&
|
|
2366
|
+
typeof pendingMs === 'number' &&
|
|
2367
|
+
pendingMs !== Infinity &&
|
|
2368
|
+
(route.options.pendingComponent ??
|
|
2369
|
+
(this.options as any)?.defaultPendingComponent)
|
|
2370
|
+
)
|
|
2371
|
+
|
|
2372
|
+
let executeBeforeLoad = true
|
|
2373
|
+
if (
|
|
2374
|
+
// If we are in the middle of a load, either of these will be present
|
|
2375
|
+
// (not to be confused with `loadPromise`, which is always defined)
|
|
2376
|
+
existingMatch.beforeLoadPromise ||
|
|
2377
|
+
existingMatch.loaderPromise
|
|
2378
|
+
) {
|
|
2379
|
+
if (shouldPending) {
|
|
2380
|
+
setTimeout(() => {
|
|
2381
|
+
try {
|
|
2382
|
+
// Update the match and prematurely resolve the loadMatches promise so that
|
|
2383
|
+
// the pending component can start rendering
|
|
2384
|
+
triggerOnReady()
|
|
2385
|
+
} catch {}
|
|
2386
|
+
}, pendingMs)
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// Wait for the beforeLoad to resolve before we continue
|
|
2390
|
+
await existingMatch.beforeLoadPromise
|
|
2391
|
+
executeBeforeLoad = this.getMatch(matchId)!.status !== 'success'
|
|
2392
|
+
}
|
|
2393
|
+
if (executeBeforeLoad) {
|
|
2394
|
+
// If we are not in the middle of a load OR the previous load failed, start it
|
|
2395
|
+
try {
|
|
2396
|
+
updateMatch(matchId, (prev) => {
|
|
2397
|
+
// explicitly capture the previous loadPromise
|
|
2398
|
+
const prevLoadPromise = prev.loadPromise
|
|
2399
|
+
return {
|
|
2400
|
+
...prev,
|
|
2401
|
+
loadPromise: createControlledPromise<void>(() => {
|
|
2402
|
+
prevLoadPromise?.resolve()
|
|
2403
|
+
}),
|
|
2404
|
+
beforeLoadPromise: createControlledPromise<void>(),
|
|
2405
|
+
}
|
|
2406
|
+
})
|
|
2407
|
+
const abortController = new AbortController()
|
|
2408
|
+
|
|
2409
|
+
let pendingTimeout: ReturnType<typeof setTimeout>
|
|
2410
|
+
|
|
2411
|
+
if (shouldPending) {
|
|
2412
|
+
// If we might show a pending component, we need to wait for the
|
|
2413
|
+
// pending promise to resolve before we start showing that state
|
|
2414
|
+
pendingTimeout = setTimeout(() => {
|
|
2415
|
+
try {
|
|
2416
|
+
// Update the match and prematurely resolve the loadMatches promise so that
|
|
2417
|
+
// the pending component can start rendering
|
|
2418
|
+
triggerOnReady()
|
|
2419
|
+
} catch {}
|
|
2420
|
+
}, pendingMs)
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
const { paramsError, searchError } = this.getMatch(matchId)!
|
|
2424
|
+
|
|
2425
|
+
if (paramsError) {
|
|
2426
|
+
handleSerialError(index, paramsError, 'PARSE_PARAMS')
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
if (searchError) {
|
|
2430
|
+
handleSerialError(index, searchError, 'VALIDATE_SEARCH')
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
const getParentMatchContext = () =>
|
|
2434
|
+
parentMatchId
|
|
2435
|
+
? this.getMatch(parentMatchId)!.context
|
|
2436
|
+
: (this.options.context ?? {})
|
|
2437
|
+
|
|
2438
|
+
updateMatch(matchId, (prev) => ({
|
|
2439
|
+
...prev,
|
|
2440
|
+
isFetching: 'beforeLoad',
|
|
2441
|
+
fetchCount: prev.fetchCount + 1,
|
|
2442
|
+
abortController,
|
|
2443
|
+
pendingTimeout,
|
|
2444
|
+
context: {
|
|
2445
|
+
...getParentMatchContext(),
|
|
2446
|
+
...prev.__routeContext,
|
|
2447
|
+
},
|
|
2448
|
+
}))
|
|
2449
|
+
|
|
2450
|
+
const { search, params, context, cause } =
|
|
2451
|
+
this.getMatch(matchId)!
|
|
2452
|
+
|
|
2453
|
+
const preload = resolvePreload(matchId)
|
|
2454
|
+
|
|
2455
|
+
const beforeLoadFnContext: BeforeLoadContextOptions<
|
|
2456
|
+
any,
|
|
2457
|
+
any,
|
|
2458
|
+
any,
|
|
2459
|
+
any,
|
|
2460
|
+
any
|
|
2461
|
+
> = {
|
|
2462
|
+
search,
|
|
2463
|
+
abortController,
|
|
2464
|
+
params,
|
|
2465
|
+
preload,
|
|
2466
|
+
context,
|
|
2467
|
+
location,
|
|
2468
|
+
navigate: (opts: any) =>
|
|
2469
|
+
this.navigate({ ...opts, _fromLocation: location }),
|
|
2470
|
+
buildLocation: this.buildLocation,
|
|
2471
|
+
cause: preload ? 'preload' : cause,
|
|
2472
|
+
matches,
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
const beforeLoadContext =
|
|
2476
|
+
(await route.options.beforeLoad?.(beforeLoadFnContext)) ??
|
|
2477
|
+
{}
|
|
2478
|
+
|
|
2479
|
+
if (
|
|
2480
|
+
isRedirect(beforeLoadContext) ||
|
|
2481
|
+
isNotFound(beforeLoadContext)
|
|
2482
|
+
) {
|
|
2483
|
+
handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
updateMatch(matchId, (prev) => {
|
|
2487
|
+
return {
|
|
2488
|
+
...prev,
|
|
2489
|
+
__beforeLoadContext: beforeLoadContext,
|
|
2490
|
+
context: {
|
|
2491
|
+
...getParentMatchContext(),
|
|
2492
|
+
...prev.__routeContext,
|
|
2493
|
+
...beforeLoadContext,
|
|
2494
|
+
},
|
|
2495
|
+
abortController,
|
|
2496
|
+
}
|
|
2497
|
+
})
|
|
2498
|
+
} catch (err) {
|
|
2499
|
+
handleSerialError(index, err, 'BEFORE_LOAD')
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
updateMatch(matchId, (prev) => {
|
|
2503
|
+
prev.beforeLoadPromise?.resolve()
|
|
2504
|
+
|
|
2505
|
+
return {
|
|
2506
|
+
...prev,
|
|
2507
|
+
beforeLoadPromise: undefined,
|
|
2508
|
+
isFetching: false,
|
|
2509
|
+
}
|
|
2510
|
+
})
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
|
|
2515
|
+
const matchPromises: Array<Promise<AnyRouteMatch>> = []
|
|
2516
|
+
|
|
2517
|
+
validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
|
|
2518
|
+
matchPromises.push(
|
|
2519
|
+
(async () => {
|
|
2520
|
+
const { loaderPromise: prevLoaderPromise } =
|
|
2521
|
+
this.getMatch(matchId)!
|
|
2522
|
+
|
|
2523
|
+
let loaderShouldRunAsync = false
|
|
2524
|
+
let loaderIsRunningAsync = false
|
|
2525
|
+
|
|
2526
|
+
if (prevLoaderPromise) {
|
|
2527
|
+
await prevLoaderPromise
|
|
2528
|
+
const match = this.getMatch(matchId)!
|
|
2529
|
+
if (match.error) {
|
|
2530
|
+
handleRedirectAndNotFound(match, match.error)
|
|
2531
|
+
}
|
|
2532
|
+
} else {
|
|
2533
|
+
const parentMatchPromise = matchPromises[index - 1] as any
|
|
2534
|
+
const route = this.looseRoutesById[routeId]!
|
|
2535
|
+
|
|
2536
|
+
const getLoaderContext = (): LoaderFnContext => {
|
|
2537
|
+
const {
|
|
2538
|
+
params,
|
|
2539
|
+
loaderDeps,
|
|
2540
|
+
abortController,
|
|
2541
|
+
context,
|
|
2542
|
+
cause,
|
|
2543
|
+
} = this.getMatch(matchId)!
|
|
2544
|
+
|
|
2545
|
+
const preload = resolvePreload(matchId)
|
|
2546
|
+
|
|
2547
|
+
return {
|
|
2548
|
+
params,
|
|
2549
|
+
deps: loaderDeps,
|
|
2550
|
+
preload: !!preload,
|
|
2551
|
+
parentMatchPromise,
|
|
2552
|
+
abortController: abortController,
|
|
2553
|
+
context,
|
|
2554
|
+
location,
|
|
2555
|
+
navigate: (opts) =>
|
|
2556
|
+
this.navigate({ ...opts, _fromLocation: location }),
|
|
2557
|
+
cause: preload ? 'preload' : cause,
|
|
2558
|
+
route,
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// This is where all of the stale-while-revalidate magic happens
|
|
2563
|
+
const age = Date.now() - this.getMatch(matchId)!.updatedAt
|
|
2564
|
+
|
|
2565
|
+
const preload = resolvePreload(matchId)
|
|
2566
|
+
|
|
2567
|
+
const staleAge = preload
|
|
2568
|
+
? (route.options.preloadStaleTime ??
|
|
2569
|
+
this.options.defaultPreloadStaleTime ??
|
|
2570
|
+
30_000) // 30 seconds for preloads by default
|
|
2571
|
+
: (route.options.staleTime ??
|
|
2572
|
+
this.options.defaultStaleTime ??
|
|
2573
|
+
0)
|
|
2574
|
+
|
|
2575
|
+
const shouldReloadOption = route.options.shouldReload
|
|
2576
|
+
|
|
2577
|
+
// Default to reloading the route all the time
|
|
2578
|
+
// Allow shouldReload to get the last say,
|
|
2579
|
+
// if provided.
|
|
2580
|
+
const shouldReload =
|
|
2581
|
+
typeof shouldReloadOption === 'function'
|
|
2582
|
+
? shouldReloadOption(getLoaderContext())
|
|
2583
|
+
: shouldReloadOption
|
|
2584
|
+
|
|
2585
|
+
updateMatch(matchId, (prev) => ({
|
|
2586
|
+
...prev,
|
|
2587
|
+
loaderPromise: createControlledPromise<void>(),
|
|
2588
|
+
preload:
|
|
2589
|
+
!!preload &&
|
|
2590
|
+
!this.state.matches.find((d) => d.id === matchId),
|
|
2591
|
+
}))
|
|
2592
|
+
|
|
2593
|
+
const runLoader = async () => {
|
|
2594
|
+
try {
|
|
2595
|
+
// If the Matches component rendered
|
|
2596
|
+
// the pending component and needs to show it for
|
|
2597
|
+
// a minimum duration, we''ll wait for it to resolve
|
|
2598
|
+
// before committing to the match and resolving
|
|
2599
|
+
// the loadPromise
|
|
2600
|
+
const potentialPendingMinPromise = async () => {
|
|
2601
|
+
const latestMatch = this.getMatch(matchId)!
|
|
2602
|
+
|
|
2603
|
+
if (latestMatch.minPendingPromise) {
|
|
2604
|
+
await latestMatch.minPendingPromise
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
// Actually run the loader and handle the result
|
|
2609
|
+
try {
|
|
2610
|
+
this.loadRouteChunk(route)
|
|
2611
|
+
|
|
2612
|
+
updateMatch(matchId, (prev) => ({
|
|
2613
|
+
...prev,
|
|
2614
|
+
isFetching: 'loader',
|
|
2615
|
+
}))
|
|
2616
|
+
|
|
2617
|
+
// Kick off the loader!
|
|
2618
|
+
const loaderData =
|
|
2619
|
+
await route.options.loader?.(getLoaderContext())
|
|
2620
|
+
|
|
2621
|
+
handleRedirectAndNotFound(
|
|
2622
|
+
this.getMatch(matchId)!,
|
|
2623
|
+
loaderData,
|
|
2624
|
+
)
|
|
2625
|
+
|
|
2626
|
+
// Lazy option can modify the route options,
|
|
2627
|
+
// so we need to wait for it to resolve before
|
|
2628
|
+
// we can use the options
|
|
2629
|
+
await route._lazyPromise
|
|
2630
|
+
|
|
2631
|
+
await potentialPendingMinPromise()
|
|
2632
|
+
|
|
2633
|
+
const assetContext = {
|
|
2634
|
+
matches,
|
|
2635
|
+
match: this.getMatch(matchId)!,
|
|
2636
|
+
params: this.getMatch(matchId)!.params,
|
|
2637
|
+
loaderData,
|
|
2638
|
+
}
|
|
2639
|
+
const headFnContent =
|
|
2640
|
+
route.options.head?.(assetContext)
|
|
2641
|
+
const meta = headFnContent?.meta
|
|
2642
|
+
const links = headFnContent?.links
|
|
2643
|
+
const headScripts = headFnContent?.scripts
|
|
2644
|
+
|
|
2645
|
+
const scripts = route.options.scripts?.(assetContext)
|
|
2646
|
+
const headers = route.options.headers?.({
|
|
2647
|
+
loaderData,
|
|
2648
|
+
})
|
|
2649
|
+
|
|
2650
|
+
updateMatch(matchId, (prev) => ({
|
|
2651
|
+
...prev,
|
|
2652
|
+
error: undefined,
|
|
2653
|
+
status: 'success',
|
|
2654
|
+
isFetching: false,
|
|
2655
|
+
updatedAt: Date.now(),
|
|
2656
|
+
loaderData,
|
|
2657
|
+
meta,
|
|
2658
|
+
links,
|
|
2659
|
+
headScripts,
|
|
2660
|
+
headers,
|
|
2661
|
+
scripts,
|
|
2662
|
+
}))
|
|
2663
|
+
} catch (e) {
|
|
2664
|
+
let error = e
|
|
2665
|
+
|
|
2666
|
+
await potentialPendingMinPromise()
|
|
2667
|
+
|
|
2668
|
+
handleRedirectAndNotFound(this.getMatch(matchId)!, e)
|
|
2669
|
+
|
|
2670
|
+
try {
|
|
2671
|
+
route.options.onError?.(e)
|
|
2672
|
+
} catch (onErrorError) {
|
|
2673
|
+
error = onErrorError
|
|
2674
|
+
handleRedirectAndNotFound(
|
|
2675
|
+
this.getMatch(matchId)!,
|
|
2676
|
+
onErrorError,
|
|
2677
|
+
)
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
updateMatch(matchId, (prev) => ({
|
|
2681
|
+
...prev,
|
|
2682
|
+
error,
|
|
2683
|
+
status: 'error',
|
|
2684
|
+
isFetching: false,
|
|
2685
|
+
}))
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
this.serverSsr?.onMatchSettled({
|
|
2689
|
+
router: this,
|
|
2690
|
+
match: this.getMatch(matchId)!,
|
|
2691
|
+
})
|
|
2692
|
+
|
|
2693
|
+
// Last but not least, wait for the the components
|
|
2694
|
+
// to be preloaded before we resolve the match
|
|
2695
|
+
await route._componentsPromise
|
|
2696
|
+
} catch (err) {
|
|
2697
|
+
updateMatch(matchId, (prev) => ({
|
|
2698
|
+
...prev,
|
|
2699
|
+
loaderPromise: undefined,
|
|
2700
|
+
}))
|
|
2701
|
+
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// If the route is successful and still fresh, just resolve
|
|
2706
|
+
const { status, invalid } = this.getMatch(matchId)!
|
|
2707
|
+
loaderShouldRunAsync =
|
|
2708
|
+
status === 'success' &&
|
|
2709
|
+
(invalid || (shouldReload ?? age > staleAge))
|
|
2710
|
+
if (preload && route.options.preload === false) {
|
|
2711
|
+
// Do nothing
|
|
2712
|
+
} else if (loaderShouldRunAsync && !sync) {
|
|
2713
|
+
loaderIsRunningAsync = true
|
|
2714
|
+
;(async () => {
|
|
2715
|
+
try {
|
|
2716
|
+
await runLoader()
|
|
2717
|
+
const { loaderPromise, loadPromise } =
|
|
2718
|
+
this.getMatch(matchId)!
|
|
2719
|
+
loaderPromise?.resolve()
|
|
2720
|
+
loadPromise?.resolve()
|
|
2721
|
+
updateMatch(matchId, (prev) => ({
|
|
2722
|
+
...prev,
|
|
2723
|
+
loaderPromise: undefined,
|
|
2724
|
+
}))
|
|
2725
|
+
} catch (err) {
|
|
2726
|
+
if (isResolvedRedirect(err)) {
|
|
2727
|
+
await this.navigate(err)
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
})()
|
|
2731
|
+
} else if (
|
|
2732
|
+
status !== 'success' ||
|
|
2733
|
+
(loaderShouldRunAsync && sync)
|
|
2734
|
+
) {
|
|
2735
|
+
await runLoader()
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
if (!loaderIsRunningAsync) {
|
|
2739
|
+
const { loaderPromise, loadPromise } =
|
|
2740
|
+
this.getMatch(matchId)!
|
|
2741
|
+
loaderPromise?.resolve()
|
|
2742
|
+
loadPromise?.resolve()
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
updateMatch(matchId, (prev) => ({
|
|
2746
|
+
...prev,
|
|
2747
|
+
isFetching: loaderIsRunningAsync ? prev.isFetching : false,
|
|
2748
|
+
loaderPromise: loaderIsRunningAsync
|
|
2749
|
+
? prev.loaderPromise
|
|
2750
|
+
: undefined,
|
|
2751
|
+
invalid: false,
|
|
2752
|
+
}))
|
|
2753
|
+
return this.getMatch(matchId)!
|
|
2754
|
+
})(),
|
|
2755
|
+
)
|
|
2756
|
+
})
|
|
2757
|
+
|
|
2758
|
+
await Promise.all(matchPromises)
|
|
2759
|
+
|
|
2760
|
+
resolveAll()
|
|
2761
|
+
} catch (err) {
|
|
2762
|
+
rejectAll(err)
|
|
2763
|
+
}
|
|
2764
|
+
})()
|
|
2765
|
+
})
|
|
2766
|
+
await triggerOnReady()
|
|
2767
|
+
} catch (err) {
|
|
2768
|
+
if (isRedirect(err) || isNotFound(err)) {
|
|
2769
|
+
if (isNotFound(err) && !allPreload) {
|
|
2770
|
+
await triggerOnReady()
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
throw err
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
return matches
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
invalidate: InvalidateFn<
|
|
2781
|
+
RouterCore<
|
|
2782
|
+
TRouteTree,
|
|
2783
|
+
TTrailingSlashOption,
|
|
2784
|
+
TDefaultStructuralSharingOption,
|
|
2785
|
+
TRouterHistory,
|
|
2786
|
+
TDehydrated
|
|
2787
|
+
>
|
|
2788
|
+
> = (opts) => {
|
|
2789
|
+
const invalidate = (d: MakeRouteMatch<TRouteTree>) => {
|
|
2790
|
+
if (opts?.filter?.(d as MakeRouteMatchUnion<this>) ?? true) {
|
|
2791
|
+
return {
|
|
2792
|
+
...d,
|
|
2793
|
+
invalid: true,
|
|
2794
|
+
...(d.status === 'error'
|
|
2795
|
+
? ({ status: 'pending', error: undefined } as const)
|
|
2796
|
+
: {}),
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
return d
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
this.__store.setState((s) => ({
|
|
2803
|
+
...s,
|
|
2804
|
+
matches: s.matches.map(invalidate),
|
|
2805
|
+
cachedMatches: s.cachedMatches.map(invalidate),
|
|
2806
|
+
pendingMatches: s.pendingMatches?.map(invalidate),
|
|
2807
|
+
}))
|
|
2808
|
+
|
|
2809
|
+
return this.load({ sync: opts?.sync })
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
resolveRedirect = (err: AnyRedirect): ResolvedRedirect => {
|
|
2813
|
+
const redirect = err as ResolvedRedirect
|
|
2814
|
+
|
|
2815
|
+
if (!redirect.href) {
|
|
2816
|
+
redirect.href = this.buildLocation(redirect as any).href
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
return redirect
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
clearCache: ClearCacheFn<this> = (opts) => {
|
|
2823
|
+
const filter = opts?.filter
|
|
2824
|
+
if (filter !== undefined) {
|
|
2825
|
+
this.__store.setState((s) => {
|
|
2826
|
+
return {
|
|
2827
|
+
...s,
|
|
2828
|
+
cachedMatches: s.cachedMatches.filter(
|
|
2829
|
+
(m) => !filter(m as MakeRouteMatchUnion<this>),
|
|
2830
|
+
),
|
|
2831
|
+
}
|
|
2832
|
+
})
|
|
2833
|
+
} else {
|
|
2834
|
+
this.__store.setState((s) => {
|
|
2835
|
+
return {
|
|
2836
|
+
...s,
|
|
2837
|
+
cachedMatches: [],
|
|
2838
|
+
}
|
|
2839
|
+
})
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
clearExpiredCache = () => {
|
|
2844
|
+
// This is where all of the garbage collection magic happens
|
|
2845
|
+
const filter = (d: MakeRouteMatch<TRouteTree>) => {
|
|
2846
|
+
const route = this.looseRoutesById[d.routeId]!
|
|
2847
|
+
|
|
2848
|
+
if (!route.options.loader) {
|
|
2849
|
+
return true
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// If the route was preloaded, use the preloadGcTime
|
|
2853
|
+
// otherwise, use the gcTime
|
|
2854
|
+
const gcTime =
|
|
2855
|
+
(d.preload
|
|
2856
|
+
? (route.options.preloadGcTime ?? this.options.defaultPreloadGcTime)
|
|
2857
|
+
: (route.options.gcTime ?? this.options.defaultGcTime)) ??
|
|
2858
|
+
5 * 60 * 1000
|
|
2859
|
+
|
|
2860
|
+
return !(d.status !== 'error' && Date.now() - d.updatedAt < gcTime)
|
|
2861
|
+
}
|
|
2862
|
+
this.clearCache({ filter })
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
loadRouteChunk = (route: AnyRoute) => {
|
|
2866
|
+
if (route._lazyPromise === undefined) {
|
|
2867
|
+
if (route.lazyFn) {
|
|
2868
|
+
route._lazyPromise = route.lazyFn().then((lazyRoute) => {
|
|
2869
|
+
// explicitly don't copy over the lazy route's id
|
|
2870
|
+
const { id: _id, ...options } = lazyRoute.options
|
|
2871
|
+
Object.assign(route.options, options)
|
|
2872
|
+
})
|
|
2873
|
+
} else {
|
|
2874
|
+
route._lazyPromise = Promise.resolve()
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// If for some reason lazy resolves more lazy components...
|
|
2879
|
+
// We'll wait for that before pre attempt to preload any
|
|
2880
|
+
// components themselves.
|
|
2881
|
+
if (route._componentsPromise === undefined) {
|
|
2882
|
+
route._componentsPromise = route._lazyPromise.then(() =>
|
|
2883
|
+
Promise.all(
|
|
2884
|
+
componentTypes.map(async (type) => {
|
|
2885
|
+
const component = route.options[type]
|
|
2886
|
+
if ((component as any)?.preload) {
|
|
2887
|
+
await (component as any).preload()
|
|
2888
|
+
}
|
|
2889
|
+
}),
|
|
2890
|
+
),
|
|
2891
|
+
)
|
|
2892
|
+
}
|
|
2893
|
+
return route._componentsPromise
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
preloadRoute: PreloadRouteFn<
|
|
2897
|
+
TRouteTree,
|
|
2898
|
+
TTrailingSlashOption,
|
|
2899
|
+
TDefaultStructuralSharingOption,
|
|
2900
|
+
TRouterHistory
|
|
2901
|
+
> = async (opts) => {
|
|
2902
|
+
const next = this.buildLocation(opts as any)
|
|
2903
|
+
|
|
2904
|
+
let matches = this.matchRoutes(next, {
|
|
2905
|
+
throwOnError: true,
|
|
2906
|
+
preload: true,
|
|
2907
|
+
dest: opts,
|
|
2908
|
+
})
|
|
2909
|
+
|
|
2910
|
+
const activeMatchIds = new Set(
|
|
2911
|
+
[...this.state.matches, ...(this.state.pendingMatches ?? [])].map(
|
|
2912
|
+
(d) => d.id,
|
|
2913
|
+
),
|
|
2914
|
+
)
|
|
2915
|
+
|
|
2916
|
+
const loadedMatchIds = new Set([
|
|
2917
|
+
...activeMatchIds,
|
|
2918
|
+
...this.state.cachedMatches.map((d) => d.id),
|
|
2919
|
+
])
|
|
2920
|
+
|
|
2921
|
+
// If the matches are already loaded, we need to add them to the cachedMatches
|
|
2922
|
+
batch(() => {
|
|
2923
|
+
matches.forEach((match) => {
|
|
2924
|
+
if (!loadedMatchIds.has(match.id)) {
|
|
2925
|
+
this.__store.setState((s) => ({
|
|
2926
|
+
...s,
|
|
2927
|
+
cachedMatches: [...(s.cachedMatches as any), match],
|
|
2928
|
+
}))
|
|
2929
|
+
}
|
|
2930
|
+
})
|
|
2931
|
+
})
|
|
2932
|
+
|
|
2933
|
+
try {
|
|
2934
|
+
matches = await this.loadMatches({
|
|
2935
|
+
matches,
|
|
2936
|
+
location: next,
|
|
2937
|
+
preload: true,
|
|
2938
|
+
updateMatch: (id, updater) => {
|
|
2939
|
+
// Don't update the match if it's currently loaded
|
|
2940
|
+
if (activeMatchIds.has(id)) {
|
|
2941
|
+
matches = matches.map((d) => (d.id === id ? updater(d) : d))
|
|
2942
|
+
} else {
|
|
2943
|
+
this.updateMatch(id, updater)
|
|
2944
|
+
}
|
|
2945
|
+
},
|
|
2946
|
+
})
|
|
2947
|
+
|
|
2948
|
+
return matches
|
|
2949
|
+
} catch (err) {
|
|
2950
|
+
if (isRedirect(err)) {
|
|
2951
|
+
if (err.reloadDocument) {
|
|
2952
|
+
return undefined
|
|
2953
|
+
}
|
|
2954
|
+
return await this.preloadRoute({
|
|
2955
|
+
...(err as any),
|
|
2956
|
+
_fromLocation: next,
|
|
2957
|
+
})
|
|
2958
|
+
}
|
|
2959
|
+
if (!isNotFound(err)) {
|
|
2960
|
+
// Preload errors are not fatal, but we should still log them
|
|
2961
|
+
console.error(err)
|
|
2962
|
+
}
|
|
2963
|
+
return undefined
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
matchRoute: MatchRouteFn<
|
|
2968
|
+
TRouteTree,
|
|
2969
|
+
TTrailingSlashOption,
|
|
2970
|
+
TDefaultStructuralSharingOption,
|
|
2971
|
+
TRouterHistory
|
|
2972
|
+
> = (location, opts) => {
|
|
2973
|
+
const matchLocation = {
|
|
2974
|
+
...location,
|
|
2975
|
+
to: location.to
|
|
2976
|
+
? this.resolvePathWithBase(
|
|
2977
|
+
(location.from || '') as string,
|
|
2978
|
+
location.to as string,
|
|
2979
|
+
)
|
|
2980
|
+
: undefined,
|
|
2981
|
+
params: location.params || {},
|
|
2982
|
+
leaveParams: true,
|
|
2983
|
+
}
|
|
2984
|
+
const next = this.buildLocation(matchLocation as any)
|
|
2985
|
+
|
|
2986
|
+
if (opts?.pending && this.state.status !== 'pending') {
|
|
2987
|
+
return false
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
const pending =
|
|
2991
|
+
opts?.pending === undefined ? !this.state.isLoading : opts.pending
|
|
2992
|
+
|
|
2993
|
+
const baseLocation = pending
|
|
2994
|
+
? this.latestLocation
|
|
2995
|
+
: this.state.resolvedLocation || this.state.location
|
|
2996
|
+
|
|
2997
|
+
const match = matchPathname(this.basepath, baseLocation.pathname, {
|
|
2998
|
+
...opts,
|
|
2999
|
+
to: next.pathname,
|
|
3000
|
+
}) as any
|
|
3001
|
+
|
|
3002
|
+
if (!match) {
|
|
3003
|
+
return false
|
|
3004
|
+
}
|
|
3005
|
+
if (location.params) {
|
|
3006
|
+
if (!deepEqual(match, location.params, { partial: true })) {
|
|
3007
|
+
return false
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
if (match && (opts?.includeSearch ?? true)) {
|
|
3012
|
+
return deepEqual(baseLocation.search, next.search, { partial: true })
|
|
3013
|
+
? match
|
|
3014
|
+
: false
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
return match
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
ssr?: {
|
|
3021
|
+
manifest: Manifest | undefined
|
|
3022
|
+
serializer: StartSerializer
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
serverSsr?: {
|
|
3026
|
+
injectedHtml: Array<InjectedHtmlEntry>
|
|
3027
|
+
injectHtml: (getHtml: () => string | Promise<string>) => Promise<void>
|
|
3028
|
+
injectScript: (
|
|
3029
|
+
getScript: () => string | Promise<string>,
|
|
3030
|
+
opts?: { logScript?: boolean },
|
|
3031
|
+
) => Promise<void>
|
|
3032
|
+
streamValue: (key: string, value: any) => void
|
|
3033
|
+
streamedKeys: Set<string>
|
|
3034
|
+
onMatchSettled: (opts: { router: AnyRouter; match: AnyRouteMatch }) => any
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
clientSsr?: {
|
|
3038
|
+
getStreamedValue: <T>(key: string) => T | undefined
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
_handleNotFound = (
|
|
3042
|
+
matches: Array<AnyRouteMatch>,
|
|
3043
|
+
err: NotFoundError,
|
|
3044
|
+
{
|
|
3045
|
+
updateMatch = this.updateMatch,
|
|
3046
|
+
}: {
|
|
3047
|
+
updateMatch?: (
|
|
3048
|
+
id: string,
|
|
3049
|
+
updater: (match: AnyRouteMatch) => AnyRouteMatch,
|
|
3050
|
+
) => void
|
|
3051
|
+
} = {},
|
|
3052
|
+
) => {
|
|
3053
|
+
// Find the route that should handle the not found error
|
|
3054
|
+
// First check if a specific route is requested to show the error
|
|
3055
|
+
const routeCursor = this.routesById[err.routeId ?? ''] ?? this.routeTree
|
|
3056
|
+
const matchesByRouteId: Record<string, AnyRouteMatch> = {}
|
|
3057
|
+
|
|
3058
|
+
// Setup routesByRouteId object for quick access
|
|
3059
|
+
for (const match of matches) {
|
|
3060
|
+
matchesByRouteId[match.routeId] = match
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// Ensure a NotFoundComponent exists on the route
|
|
3064
|
+
if (
|
|
3065
|
+
!routeCursor.options.notFoundComponent &&
|
|
3066
|
+
(this.options as any)?.defaultNotFoundComponent
|
|
3067
|
+
) {
|
|
3068
|
+
routeCursor.options.notFoundComponent = (
|
|
3069
|
+
this.options as any
|
|
3070
|
+
).defaultNotFoundComponent
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// Ensure we have a notFoundComponent
|
|
3074
|
+
invariant(
|
|
3075
|
+
routeCursor.options.notFoundComponent,
|
|
3076
|
+
'No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router.',
|
|
3077
|
+
)
|
|
3078
|
+
|
|
3079
|
+
// Find the match for this route
|
|
3080
|
+
const matchForRoute = matchesByRouteId[routeCursor.id]
|
|
3081
|
+
|
|
3082
|
+
invariant(
|
|
3083
|
+
matchForRoute,
|
|
3084
|
+
'Could not find match for route: ' + routeCursor.id,
|
|
3085
|
+
)
|
|
3086
|
+
|
|
3087
|
+
// Assign the error to the match - using non-null assertion since we've checked with invariant
|
|
3088
|
+
updateMatch(matchForRoute.id, (prev) => ({
|
|
3089
|
+
...prev,
|
|
3090
|
+
status: 'notFound',
|
|
3091
|
+
error: err,
|
|
3092
|
+
isFetching: false,
|
|
3093
|
+
}))
|
|
3094
|
+
|
|
3095
|
+
if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
|
|
3096
|
+
err.routeId = routeCursor.parentRoute.id
|
|
3097
|
+
this._handleNotFound(matches, err, {
|
|
3098
|
+
updateMatch,
|
|
3099
|
+
})
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
hasNotFoundMatch = () => {
|
|
3104
|
+
return this.__store.state.matches.some(
|
|
3105
|
+
(d) => d.status === 'notFound' || d.globalNotFound,
|
|
3106
|
+
)
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
export class SearchParamError extends Error {}
|
|
3111
|
+
|
|
3112
|
+
export class PathParamError extends Error {}
|
|
3113
|
+
|
|
3114
|
+
// A function that takes an import() argument which is a function and returns a new function that will
|
|
3115
|
+
// proxy arguments from the caller to the imported function, retaining all type
|
|
3116
|
+
// information along the way
|
|
3117
|
+
export function lazyFn<
|
|
3118
|
+
T extends Record<string, (...args: Array<any>) => any>,
|
|
3119
|
+
TKey extends keyof T = 'default',
|
|
3120
|
+
>(fn: () => Promise<T>, key?: TKey) {
|
|
3121
|
+
return async (
|
|
3122
|
+
...args: Parameters<T[TKey]>
|
|
3123
|
+
): Promise<Awaited<ReturnType<T[TKey]>>> => {
|
|
3124
|
+
const imported = await fn()
|
|
3125
|
+
return imported[key || 'default'](...args)
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
export function getInitialRouterState(
|
|
3130
|
+
location: ParsedLocation,
|
|
3131
|
+
): RouterState<any> {
|
|
3132
|
+
return {
|
|
3133
|
+
loadedAt: 0,
|
|
3134
|
+
isLoading: false,
|
|
3135
|
+
isTransitioning: false,
|
|
3136
|
+
status: 'idle',
|
|
3137
|
+
resolvedLocation: undefined,
|
|
3138
|
+
location,
|
|
3139
|
+
matches: [],
|
|
3140
|
+
pendingMatches: [],
|
|
3141
|
+
cachedMatches: [],
|
|
3142
|
+
statusCode: 200,
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
|
|
3147
|
+
if (validateSearch == null) return {}
|
|
3148
|
+
|
|
3149
|
+
if ('~standard' in validateSearch) {
|
|
3150
|
+
const result = validateSearch['~standard'].validate(input)
|
|
3151
|
+
|
|
3152
|
+
if (result instanceof Promise)
|
|
3153
|
+
throw new SearchParamError('Async validation not supported')
|
|
3154
|
+
|
|
3155
|
+
if (result.issues)
|
|
3156
|
+
throw new SearchParamError(JSON.stringify(result.issues, undefined, 2), {
|
|
3157
|
+
cause: result,
|
|
3158
|
+
})
|
|
3159
|
+
|
|
3160
|
+
return result.value
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
if ('parse' in validateSearch) {
|
|
3164
|
+
return validateSearch.parse(input)
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
if (typeof validateSearch === 'function') {
|
|
3168
|
+
return validateSearch(input)
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
return {}
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
export const componentTypes = [
|
|
3175
|
+
'component',
|
|
3176
|
+
'errorComponent',
|
|
3177
|
+
'pendingComponent',
|
|
3178
|
+
'notFoundComponent',
|
|
3179
|
+
] as const
|
|
3180
|
+
|
|
3181
|
+
function routeNeedsPreload(route: AnyRoute) {
|
|
3182
|
+
for (const componentType of componentTypes) {
|
|
3183
|
+
if ((route.options[componentType] as any)?.preload) {
|
|
3184
|
+
return true
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
return false
|
|
3188
|
+
}
|