@tanstack/router-core 0.0.1-beta.9 → 1.20.3-alpha.1
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/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/cjs/Matches.cjs +13 -0
- package/dist/cjs/Matches.cjs.map +1 -0
- package/dist/cjs/Matches.d.cts +109 -0
- package/dist/cjs/RouterProvider.d.cts +26 -0
- package/dist/cjs/defer.cjs +25 -0
- package/dist/cjs/defer.cjs.map +1 -0
- package/dist/cjs/defer.d.cts +20 -0
- package/dist/cjs/fileRoute.d.cts +23 -0
- package/dist/cjs/history.d.cts +8 -0
- package/dist/cjs/index.cjs +80 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +41 -0
- package/dist/cjs/link.cjs +5 -0
- package/dist/cjs/link.cjs.map +1 -0
- package/dist/cjs/link.d.cts +200 -0
- package/dist/cjs/location.d.cts +12 -0
- package/dist/cjs/manifest.d.cts +24 -0
- package/dist/cjs/not-found.cjs +13 -0
- package/dist/cjs/not-found.cjs.map +1 -0
- package/dist/cjs/not-found.d.cts +20 -0
- package/dist/cjs/path.cjs +412 -0
- package/dist/cjs/path.cjs.map +1 -0
- package/dist/cjs/path.d.cts +56 -0
- package/dist/cjs/qss.cjs +38 -0
- package/dist/cjs/qss.cjs.map +1 -0
- package/dist/cjs/qss.d.cts +22 -0
- package/dist/cjs/redirect.cjs +34 -0
- package/dist/cjs/redirect.cjs.map +1 -0
- package/dist/cjs/redirect.d.cts +38 -0
- package/dist/cjs/root.cjs +5 -0
- package/dist/cjs/root.cjs.map +1 -0
- package/dist/cjs/root.d.cts +2 -0
- package/dist/cjs/route.cjs +119 -0
- package/dist/cjs/route.cjs.map +1 -0
- package/dist/cjs/route.d.cts +422 -0
- package/dist/cjs/routeInfo.d.cts +54 -0
- package/dist/cjs/router.cjs +1800 -0
- package/dist/cjs/router.cjs.map +1 -0
- package/dist/cjs/router.d.cts +630 -0
- package/dist/cjs/scroll-restoration.cjs +196 -0
- package/dist/cjs/scroll-restoration.cjs.map +1 -0
- package/dist/cjs/scroll-restoration.d.cts +38 -0
- package/dist/cjs/searchMiddleware.cjs +42 -0
- package/dist/cjs/searchMiddleware.cjs.map +1 -0
- package/dist/cjs/searchMiddleware.d.cts +5 -0
- package/dist/cjs/searchParams.cjs +61 -0
- package/dist/cjs/searchParams.cjs.map +1 -0
- package/dist/cjs/searchParams.d.cts +7 -0
- package/dist/cjs/serializer.d.cts +22 -0
- package/dist/cjs/structuralSharing.d.cts +4 -0
- package/dist/cjs/typePrimitives.d.cts +65 -0
- package/dist/cjs/useLoaderData.d.cts +5 -0
- package/dist/cjs/useLoaderDeps.d.cts +5 -0
- package/dist/cjs/useNavigate.d.cts +3 -0
- package/dist/cjs/useParams.d.cts +5 -0
- package/dist/cjs/useRouteContext.d.cts +9 -0
- package/dist/cjs/useSearch.d.cts +5 -0
- package/dist/cjs/utils.cjs +160 -0
- package/dist/cjs/utils.cjs.map +1 -0
- package/dist/cjs/utils.d.cts +105 -0
- package/dist/cjs/validators.d.cts +51 -0
- package/dist/esm/Matches.d.ts +109 -0
- package/dist/esm/Matches.js +13 -0
- package/dist/esm/Matches.js.map +1 -0
- package/dist/esm/RouterProvider.d.ts +26 -0
- package/dist/esm/defer.d.ts +20 -0
- package/dist/esm/defer.js +25 -0
- package/dist/esm/defer.js.map +1 -0
- package/dist/esm/fileRoute.d.ts +23 -0
- package/dist/esm/history.d.ts +8 -0
- package/dist/esm/index.d.ts +41 -0
- package/dist/esm/index.js +80 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/link.d.ts +200 -0
- package/dist/esm/link.js +5 -0
- package/dist/esm/link.js.map +1 -0
- package/dist/esm/location.d.ts +12 -0
- package/dist/esm/manifest.d.ts +24 -0
- package/dist/esm/not-found.d.ts +20 -0
- package/dist/esm/not-found.js +13 -0
- package/dist/esm/not-found.js.map +1 -0
- package/dist/esm/path.d.ts +56 -0
- package/dist/esm/path.js +412 -0
- package/dist/esm/path.js.map +1 -0
- package/dist/esm/qss.d.ts +22 -0
- package/dist/esm/qss.js +38 -0
- package/dist/esm/qss.js.map +1 -0
- package/dist/esm/redirect.d.ts +38 -0
- package/dist/esm/redirect.js +34 -0
- package/dist/esm/redirect.js.map +1 -0
- package/dist/esm/root.d.ts +2 -0
- package/dist/esm/root.js +5 -0
- package/dist/esm/root.js.map +1 -0
- package/dist/esm/route.d.ts +422 -0
- package/dist/esm/route.js +119 -0
- package/dist/esm/route.js.map +1 -0
- package/dist/esm/routeInfo.d.ts +54 -0
- package/dist/esm/router.d.ts +630 -0
- package/dist/esm/router.js +1800 -0
- package/dist/esm/router.js.map +1 -0
- package/dist/esm/scroll-restoration.d.ts +38 -0
- package/dist/esm/scroll-restoration.js +196 -0
- package/dist/esm/scroll-restoration.js.map +1 -0
- package/dist/esm/searchMiddleware.d.ts +5 -0
- package/dist/esm/searchMiddleware.js +42 -0
- package/dist/esm/searchMiddleware.js.map +1 -0
- package/dist/esm/searchParams.d.ts +7 -0
- package/dist/esm/searchParams.js +61 -0
- package/dist/esm/searchParams.js.map +1 -0
- package/dist/esm/serializer.d.ts +22 -0
- package/dist/esm/structuralSharing.d.ts +4 -0
- package/dist/esm/typePrimitives.d.ts +65 -0
- package/dist/esm/useLoaderData.d.ts +5 -0
- package/dist/esm/useLoaderDeps.d.ts +5 -0
- package/dist/esm/useNavigate.d.ts +3 -0
- package/dist/esm/useParams.d.ts +5 -0
- package/dist/esm/useRouteContext.d.ts +9 -0
- package/dist/esm/useSearch.d.ts +5 -0
- package/dist/esm/utils.d.ts +105 -0
- package/dist/esm/utils.js +160 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/validators.d.ts +51 -0
- package/package.json +36 -32
- package/src/Matches.ts +239 -0
- package/src/RouterProvider.ts +50 -0
- package/src/defer.ts +52 -0
- package/src/fileRoute.ts +140 -0
- package/src/history.ts +9 -0
- package/src/index.ts +421 -19
- package/src/link.ts +580 -286
- package/src/location.ts +13 -0
- package/src/manifest.ts +32 -0
- package/src/not-found.ts +29 -0
- package/src/path.ts +425 -49
- package/src/qss.ts +70 -41
- package/src/redirect.ts +100 -0
- package/src/root.ts +2 -0
- package/src/route.ts +1682 -218
- package/src/routeInfo.ts +224 -217
- package/src/router.ts +3100 -1073
- package/src/scroll-restoration.ts +340 -0
- package/src/searchMiddleware.ts +54 -0
- package/src/searchParams.ts +43 -20
- package/src/serializer.ts +32 -0
- package/src/structuralSharing.ts +7 -0
- package/src/typePrimitives.ts +181 -0
- package/src/useLoaderData.ts +20 -0
- package/src/useLoaderDeps.ts +13 -0
- package/src/useNavigate.ts +13 -0
- package/src/useParams.ts +20 -0
- package/src/useRouteContext.ts +39 -0
- package/src/useSearch.ts +20 -0
- package/src/utils.ts +369 -75
- package/src/validators.ts +121 -0
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +0 -33
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +0 -1
- package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js +0 -33
- package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js.map +0 -1
- package/build/cjs/node_modules/history/index.js +0 -815
- package/build/cjs/node_modules/history/index.js.map +0 -1
- package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js +0 -30
- package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js.map +0 -1
- package/build/cjs/packages/router-core/src/index.js +0 -58
- package/build/cjs/packages/router-core/src/index.js.map +0 -1
- package/build/cjs/packages/router-core/src/path.js +0 -222
- package/build/cjs/packages/router-core/src/path.js.map +0 -1
- package/build/cjs/packages/router-core/src/qss.js +0 -71
- package/build/cjs/packages/router-core/src/qss.js.map +0 -1
- package/build/cjs/packages/router-core/src/route.js +0 -150
- package/build/cjs/packages/router-core/src/route.js.map +0 -1
- package/build/cjs/packages/router-core/src/routeConfig.js +0 -69
- package/build/cjs/packages/router-core/src/routeConfig.js.map +0 -1
- package/build/cjs/packages/router-core/src/routeMatch.js +0 -266
- package/build/cjs/packages/router-core/src/routeMatch.js.map +0 -1
- package/build/cjs/packages/router-core/src/router.js +0 -822
- package/build/cjs/packages/router-core/src/router.js.map +0 -1
- package/build/cjs/packages/router-core/src/searchParams.js +0 -70
- package/build/cjs/packages/router-core/src/searchParams.js.map +0 -1
- package/build/cjs/packages/router-core/src/utils.js +0 -125
- package/build/cjs/packages/router-core/src/utils.js.map +0 -1
- package/build/esm/index.js +0 -2481
- package/build/esm/index.js.map +0 -1
- package/build/stats-html.html +0 -4034
- package/build/stats-react.json +0 -493
- package/build/types/index.d.ts +0 -618
- package/build/umd/index.development.js +0 -2514
- package/build/umd/index.development.js.map +0 -1
- package/build/umd/index.production.js +0 -12
- package/build/umd/index.production.js.map +0 -1
- package/src/frameworks.ts +0 -12
- package/src/routeConfig.ts +0 -495
- package/src/routeMatch.ts +0 -374
package/src/router.ts
CHANGED
|
@@ -1,1297 +1,3324 @@
|
|
|
1
|
+
import { Store, batch } from '@tanstack/store'
|
|
1
2
|
import {
|
|
2
|
-
BrowserHistory,
|
|
3
3
|
createBrowserHistory,
|
|
4
4
|
createMemoryHistory,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
MemoryHistory,
|
|
8
|
-
} from 'history'
|
|
5
|
+
parseHref,
|
|
6
|
+
} from '@tanstack/history'
|
|
9
7
|
import invariant from 'tiny-invariant'
|
|
10
|
-
import { GetFrameworkGeneric } from './frameworks'
|
|
11
|
-
|
|
12
8
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
createControlledPromise,
|
|
10
|
+
deepEqual,
|
|
11
|
+
functionalUpdate,
|
|
12
|
+
last,
|
|
13
|
+
pick,
|
|
14
|
+
replaceEqualDeep,
|
|
15
|
+
} from './utils'
|
|
19
16
|
import {
|
|
20
17
|
cleanPath,
|
|
21
18
|
interpolatePath,
|
|
22
19
|
joinPaths,
|
|
23
20
|
matchPathname,
|
|
21
|
+
parsePathname,
|
|
24
22
|
resolvePath,
|
|
23
|
+
trimPath,
|
|
24
|
+
trimPathLeft,
|
|
25
|
+
trimPathRight,
|
|
25
26
|
} from './path'
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
AnyLoaderData,
|
|
29
|
-
AnyPathParams,
|
|
30
|
-
AnyRouteConfig,
|
|
31
|
-
AnySearchSchema,
|
|
32
|
-
LoaderContext,
|
|
33
|
-
RouteConfig,
|
|
34
|
-
SearchFilter,
|
|
35
|
-
} from './routeConfig'
|
|
36
|
-
import {
|
|
37
|
-
AllRouteInfo,
|
|
38
|
-
AnyAllRouteInfo,
|
|
39
|
-
AnyRouteInfo,
|
|
40
|
-
RouteInfo,
|
|
41
|
-
RoutesById,
|
|
42
|
-
} from './routeInfo'
|
|
43
|
-
import { createRouteMatch, RouteMatch } from './routeMatch'
|
|
27
|
+
import { isNotFound } from './not-found'
|
|
28
|
+
import { setupScrollRestoration } from './scroll-restoration'
|
|
44
29
|
import { defaultParseSearch, defaultStringifySearch } from './searchParams'
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
import { rootRouteId } from './root'
|
|
31
|
+
import { isRedirect } 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'
|
|
40
|
+
import type {
|
|
41
|
+
ControlledPromise,
|
|
42
|
+
NoInfer,
|
|
43
|
+
NonNullableUpdater,
|
|
49
44
|
PickAsRequired,
|
|
50
|
-
PickRequired,
|
|
51
|
-
replaceEqualDeep,
|
|
52
|
-
Timeout,
|
|
53
45
|
Updater,
|
|
54
46
|
} from './utils'
|
|
47
|
+
import type { ParsedLocation } from './location'
|
|
48
|
+
import type { DeferredPromiseState } from './defer'
|
|
49
|
+
import type {
|
|
50
|
+
AnyContext,
|
|
51
|
+
AnyRoute,
|
|
52
|
+
AnyRouteWithContext,
|
|
53
|
+
BeforeLoadContextOptions,
|
|
54
|
+
LoaderFnContext,
|
|
55
|
+
MakeRemountDepsOptionsUnion,
|
|
56
|
+
RouteContextOptions,
|
|
57
|
+
RouteMask,
|
|
58
|
+
SearchMiddleware,
|
|
59
|
+
} from './route'
|
|
60
|
+
import type {
|
|
61
|
+
FullSearchSchema,
|
|
62
|
+
RouteById,
|
|
63
|
+
RoutePaths,
|
|
64
|
+
RoutesById,
|
|
65
|
+
RoutesByPath,
|
|
66
|
+
} from './routeInfo'
|
|
67
|
+
import type {
|
|
68
|
+
AnyRouteMatch,
|
|
69
|
+
MakeRouteMatch,
|
|
70
|
+
MakeRouteMatchUnion,
|
|
71
|
+
MatchRouteOptions,
|
|
72
|
+
} from './Matches'
|
|
73
|
+
import type {
|
|
74
|
+
BuildLocationFn,
|
|
75
|
+
CommitLocationOptions,
|
|
76
|
+
NavigateFn,
|
|
77
|
+
} from './RouterProvider'
|
|
78
|
+
import type { Manifest } from './manifest'
|
|
79
|
+
import type { StartSerializer } from './serializer'
|
|
80
|
+
import type { AnySchema, AnyValidator } from './validators'
|
|
81
|
+
import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
|
|
82
|
+
import type { NotFoundError } from './not-found'
|
|
83
|
+
|
|
84
|
+
declare global {
|
|
85
|
+
interface Window {
|
|
86
|
+
__TSR_ROUTER__?: AnyRouter
|
|
87
|
+
}
|
|
88
|
+
}
|
|
55
89
|
|
|
56
|
-
export
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
TSearchObj extends AnySearchSchema = {},
|
|
60
|
-
TState extends LocationState = LocationState,
|
|
61
|
-
> {
|
|
62
|
-
href: string
|
|
63
|
-
pathname: string
|
|
64
|
-
search: TSearchObj
|
|
65
|
-
searchStr: string
|
|
66
|
-
state: TState
|
|
67
|
-
hash: string
|
|
68
|
-
key?: string
|
|
90
|
+
export type ControllablePromise<T = any> = Promise<T> & {
|
|
91
|
+
resolve: (value: T) => void
|
|
92
|
+
reject: (value?: any) => void
|
|
69
93
|
}
|
|
70
94
|
|
|
71
|
-
export
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
hash?: string
|
|
95
|
+
export type InjectedHtmlEntry = Promise<string>
|
|
96
|
+
|
|
97
|
+
export interface DefaultRegister {
|
|
98
|
+
router: AnyRouter
|
|
76
99
|
}
|
|
77
100
|
|
|
78
|
-
export
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
routeConfigs: TRoute[],
|
|
82
|
-
) => TRoute[]
|
|
101
|
+
export interface Register extends DefaultRegister {
|
|
102
|
+
// router: Router
|
|
103
|
+
}
|
|
83
104
|
|
|
84
|
-
export
|
|
85
|
-
|
|
105
|
+
export type RegisteredRouter = Register['router']
|
|
106
|
+
|
|
107
|
+
export type DefaultRemountDepsFn<TRouteTree extends AnyRoute> = (
|
|
108
|
+
opts: MakeRemountDepsOptionsUnion<TRouteTree>,
|
|
109
|
+
) => any
|
|
110
|
+
|
|
111
|
+
export interface DefaultRouterOptionsExtensions {}
|
|
112
|
+
|
|
113
|
+
export interface RouterOptionsExtensions
|
|
114
|
+
extends DefaultRouterOptionsExtensions {}
|
|
115
|
+
|
|
116
|
+
export interface RouterOptions<
|
|
117
|
+
TRouteTree extends AnyRoute,
|
|
118
|
+
TTrailingSlashOption extends TrailingSlashOption,
|
|
119
|
+
TDefaultStructuralSharingOption extends boolean = false,
|
|
120
|
+
TRouterHistory extends RouterHistory = RouterHistory,
|
|
121
|
+
TDehydrated extends Record<string, any> = Record<string, any>,
|
|
122
|
+
> extends RouterOptionsExtensions {
|
|
123
|
+
/**
|
|
124
|
+
* The history object that will be used to manage the browser history.
|
|
125
|
+
*
|
|
126
|
+
* If not provided, a new createBrowserHistory instance will be created and used.
|
|
127
|
+
*
|
|
128
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#history-property)
|
|
129
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/history-types)
|
|
130
|
+
*/
|
|
131
|
+
history?: TRouterHistory
|
|
132
|
+
/**
|
|
133
|
+
* A function that will be used to stringify search params when generating links.
|
|
134
|
+
*
|
|
135
|
+
* @default defaultStringifySearch
|
|
136
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#stringifysearch-method)
|
|
137
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization)
|
|
138
|
+
*/
|
|
86
139
|
stringifySearch?: SearchSerializer
|
|
140
|
+
/**
|
|
141
|
+
* A function that will be used to parse search params when parsing the current location.
|
|
142
|
+
*
|
|
143
|
+
* @default defaultParseSearch
|
|
144
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#parsesearch-method)
|
|
145
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/custom-search-param-serialization)
|
|
146
|
+
*/
|
|
87
147
|
parseSearch?: SearchParser
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
148
|
+
/**
|
|
149
|
+
* If `false`, routes will not be preloaded by default in any way.
|
|
150
|
+
*
|
|
151
|
+
* If `'intent'`, routes will be preloaded by default when the user hovers over a link or a `touchstart` event is detected on a `<Link>`.
|
|
152
|
+
*
|
|
153
|
+
* If `'viewport'`, routes will be preloaded by default when they are within the viewport.
|
|
154
|
+
*
|
|
155
|
+
* @default false
|
|
156
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreload-property)
|
|
157
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading)
|
|
158
|
+
*/
|
|
159
|
+
defaultPreload?: false | 'intent' | 'viewport' | 'render'
|
|
160
|
+
/**
|
|
161
|
+
* The delay in milliseconds that a route must be hovered over or touched before it is preloaded.
|
|
162
|
+
*
|
|
163
|
+
* @default 50
|
|
164
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloaddelay-property)
|
|
165
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-delay)
|
|
166
|
+
*/
|
|
92
167
|
defaultPreloadDelay?: number
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
168
|
+
/**
|
|
169
|
+
* The default `preloadIntentProximity` a route should use if no preloadIntentProximity is provided.
|
|
170
|
+
*
|
|
171
|
+
* @default 0
|
|
172
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadintentproximity-property)
|
|
173
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-intent-proximity)
|
|
174
|
+
*/
|
|
175
|
+
defaultPreloadIntentProximity?: number
|
|
176
|
+
/**
|
|
177
|
+
* The default `pendingMs` a route should use if no pendingMs is provided.
|
|
178
|
+
*
|
|
179
|
+
* @default 1000
|
|
180
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpendingms-property)
|
|
181
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#avoiding-pending-component-flash)
|
|
182
|
+
*/
|
|
98
183
|
defaultPendingMs?: number
|
|
184
|
+
/**
|
|
185
|
+
* The default `pendingMinMs` a route should use if no pendingMinMs is provided.
|
|
186
|
+
*
|
|
187
|
+
* @default 500
|
|
188
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpendingminms-property)
|
|
189
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#avoiding-pending-component-flash)
|
|
190
|
+
*/
|
|
99
191
|
defaultPendingMinMs?: number
|
|
100
|
-
|
|
101
|
-
|
|
192
|
+
/**
|
|
193
|
+
* The default `staleTime` a route should use if no staleTime is provided. This is the time in milliseconds that a route will be considered fresh.
|
|
194
|
+
*
|
|
195
|
+
* @default 0
|
|
196
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultstaletime-property)
|
|
197
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#key-options)
|
|
198
|
+
*/
|
|
199
|
+
defaultStaleTime?: number
|
|
200
|
+
/**
|
|
201
|
+
* The default `preloadStaleTime` a route should use if no preloadStaleTime is provided.
|
|
202
|
+
*
|
|
203
|
+
* @default 30_000 `(30 seconds)`
|
|
204
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadstaletime-property)
|
|
205
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading)
|
|
206
|
+
*/
|
|
207
|
+
defaultPreloadStaleTime?: number
|
|
208
|
+
/**
|
|
209
|
+
* The default `defaultPreloadGcTime` a route should use if no preloadGcTime is provided.
|
|
210
|
+
*
|
|
211
|
+
* @default 1_800_000 `(30 minutes)`
|
|
212
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadgctime-property)
|
|
213
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading)
|
|
214
|
+
*/
|
|
215
|
+
defaultPreloadGcTime?: number
|
|
216
|
+
/**
|
|
217
|
+
* If `true`, route navigations will called using `document.startViewTransition()`.
|
|
218
|
+
*
|
|
219
|
+
* If the browser does not support this api, this option will be ignored.
|
|
220
|
+
*
|
|
221
|
+
* See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) for more information on how this function works.
|
|
222
|
+
*
|
|
223
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultviewtransition-property)
|
|
224
|
+
*/
|
|
225
|
+
defaultViewTransition?: boolean | ViewTransitionOptions
|
|
226
|
+
/**
|
|
227
|
+
* The default `hashScrollIntoView` a route should use if no hashScrollIntoView is provided while navigating
|
|
228
|
+
*
|
|
229
|
+
* See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) for more information on `ScrollIntoViewOptions`.
|
|
230
|
+
*
|
|
231
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaulthashscrollintoview-property)
|
|
232
|
+
*/
|
|
233
|
+
defaultHashScrollIntoView?: boolean | ScrollIntoViewOptions
|
|
234
|
+
/**
|
|
235
|
+
* @default 'fuzzy'
|
|
236
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#notfoundmode-property)
|
|
237
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/not-found-errors#the-notfoundmode-option)
|
|
238
|
+
*/
|
|
239
|
+
notFoundMode?: 'root' | 'fuzzy'
|
|
240
|
+
/**
|
|
241
|
+
* The default `gcTime` a route should use if no gcTime is provided.
|
|
242
|
+
*
|
|
243
|
+
* @default 1_800_000 `(30 minutes)`
|
|
244
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultgctime-property)
|
|
245
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#key-options)
|
|
246
|
+
*/
|
|
247
|
+
defaultGcTime?: number
|
|
248
|
+
/**
|
|
249
|
+
* If `true`, all routes will be matched as case-sensitive.
|
|
250
|
+
*
|
|
251
|
+
* @default false
|
|
252
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#casesensitive-property)
|
|
253
|
+
*/
|
|
102
254
|
caseSensitive?: boolean
|
|
103
|
-
|
|
255
|
+
/**
|
|
256
|
+
*
|
|
257
|
+
* The route tree that will be used to configure the router instance.
|
|
258
|
+
*
|
|
259
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#routetree-property)
|
|
260
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/routing/route-trees)
|
|
261
|
+
*/
|
|
262
|
+
routeTree?: TRouteTree
|
|
263
|
+
/**
|
|
264
|
+
* The basepath for then entire router. This is useful for mounting a router instance at a subpath.
|
|
265
|
+
*
|
|
266
|
+
* @default '/'
|
|
267
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#basepath-property)
|
|
268
|
+
*/
|
|
104
269
|
basepath?: string
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
270
|
+
/**
|
|
271
|
+
* The root context that will be provided to all routes in the route tree.
|
|
272
|
+
*
|
|
273
|
+
* This can be used to provide a context to all routes in the tree without having to provide it to each route individually.
|
|
274
|
+
*
|
|
275
|
+
* Optional or required if the root route was created with [`createRootRouteWithContext()`](https://tanstack.com/router/latest/docs/framework/react/api/router/createRootRouteWithContextFunction).
|
|
276
|
+
*
|
|
277
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#context-property)
|
|
278
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/router-context)
|
|
279
|
+
*/
|
|
280
|
+
context?: InferRouterContext<TRouteTree>
|
|
281
|
+
/**
|
|
282
|
+
* A function that will be called when the router is dehydrated.
|
|
283
|
+
*
|
|
284
|
+
* The return value of this function will be serialized and stored in the router's dehydrated state.
|
|
285
|
+
*
|
|
286
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#dehydrate-method)
|
|
287
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration)
|
|
288
|
+
*/
|
|
289
|
+
dehydrate?: () => TDehydrated
|
|
290
|
+
/**
|
|
291
|
+
* A function that will be called when the router is hydrated.
|
|
292
|
+
*
|
|
293
|
+
* The return value of this function will be serialized and stored in the router's dehydrated state.
|
|
294
|
+
*
|
|
295
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#hydrate-method)
|
|
296
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration)
|
|
297
|
+
*/
|
|
298
|
+
hydrate?: (dehydrated: TDehydrated) => void
|
|
299
|
+
/**
|
|
300
|
+
* An array of route masks that will be used to mask routes in the route tree.
|
|
301
|
+
*
|
|
302
|
+
* Route masking is when you display a route at a different path than the one it is configured to match, like a modal popup that when shared will unmask to the modal's content instead of the modal's context.
|
|
303
|
+
*
|
|
304
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#routemasks-property)
|
|
305
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/route-masking)
|
|
306
|
+
*/
|
|
307
|
+
routeMasks?: Array<RouteMask<TRouteTree>>
|
|
308
|
+
/**
|
|
309
|
+
* If `true`, route masks will, by default, be removed when the page is reloaded.
|
|
310
|
+
*
|
|
311
|
+
* This can be overridden on a per-mask basis by setting the `unmaskOnReload` option on the mask, or on a per-navigation basis by setting the `unmaskOnReload` option in the `Navigate` options.
|
|
312
|
+
*
|
|
313
|
+
* @default false
|
|
314
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#unmaskonreload-property)
|
|
315
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/route-masking#unmasking-on-page-reload)
|
|
316
|
+
*/
|
|
317
|
+
unmaskOnReload?: boolean
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Use `notFoundComponent` instead.
|
|
321
|
+
*
|
|
322
|
+
* @deprecated
|
|
323
|
+
* See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info.
|
|
324
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#notfoundroute-property)
|
|
325
|
+
*/
|
|
326
|
+
notFoundRoute?: AnyRoute
|
|
327
|
+
/**
|
|
328
|
+
* Configures how trailing slashes are treated.
|
|
329
|
+
*
|
|
330
|
+
* - `'always'` will add a trailing slash if not present
|
|
331
|
+
* - `'never'` will remove the trailing slash if present
|
|
332
|
+
* - `'preserve'` will not modify the trailing slash.
|
|
333
|
+
*
|
|
334
|
+
* @default 'never'
|
|
335
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#trailingslash-property)
|
|
336
|
+
*/
|
|
337
|
+
trailingSlash?: TTrailingSlashOption
|
|
338
|
+
/**
|
|
339
|
+
* While usually automatic, sometimes it can be useful to force the router into a server-side state, e.g. when using the router in a non-browser environment that has access to a global.document object.
|
|
340
|
+
*
|
|
341
|
+
* @default typeof document !== 'undefined'
|
|
342
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#isserver-property)
|
|
343
|
+
*/
|
|
344
|
+
isServer?: boolean
|
|
345
|
+
|
|
346
|
+
defaultSsr?: boolean
|
|
347
|
+
|
|
348
|
+
search?: {
|
|
349
|
+
/**
|
|
350
|
+
* Configures how unknown search params (= not returned by any `validateSearch`) are treated.
|
|
351
|
+
*
|
|
352
|
+
* @default false
|
|
353
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#search.strict-property)
|
|
354
|
+
*/
|
|
355
|
+
strict?: boolean
|
|
356
|
+
}
|
|
122
357
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Configures whether structural sharing is enabled by default for fine-grained selectors.
|
|
360
|
+
*
|
|
361
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultstructuralsharing-property)
|
|
362
|
+
*/
|
|
363
|
+
defaultStructuralSharing?: TDefaultStructuralSharingOption
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Configures which URI characters are allowed in path params that would ordinarily be escaped by encodeURIComponent.
|
|
367
|
+
*
|
|
368
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#pathparamsallowedcharacters-property)
|
|
369
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/path-params#allowed-characters)
|
|
370
|
+
*/
|
|
371
|
+
pathParamsAllowedCharacters?: Array<
|
|
372
|
+
';' | ':' | '@' | '&' | '=' | '+' | '$' | ','
|
|
373
|
+
>
|
|
134
374
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
375
|
+
defaultRemountDeps?: DefaultRemountDepsFn<TRouteTree>
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* If `true`, scroll restoration will be enabled
|
|
379
|
+
*
|
|
380
|
+
* @default false
|
|
381
|
+
*/
|
|
382
|
+
scrollRestoration?: boolean
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* A function that will be called to get the key for the scroll restoration cache.
|
|
386
|
+
*
|
|
387
|
+
* @default (location) => location.href
|
|
388
|
+
*/
|
|
389
|
+
getScrollRestorationKey?: (location: ParsedLocation) => string
|
|
390
|
+
/**
|
|
391
|
+
* The default behavior for scroll restoration.
|
|
392
|
+
*
|
|
393
|
+
* @default 'auto'
|
|
394
|
+
*/
|
|
395
|
+
scrollRestorationBehavior?: ScrollBehavior
|
|
396
|
+
/**
|
|
397
|
+
* An array of selectors that will be used to scroll to the top of the page in addition to `window`
|
|
398
|
+
*
|
|
399
|
+
* @default ['window']
|
|
400
|
+
*/
|
|
401
|
+
scrollToTopSelectors?: Array<string>
|
|
161
402
|
}
|
|
162
403
|
|
|
163
|
-
export interface
|
|
164
|
-
|
|
165
|
-
|
|
404
|
+
export interface RouterState<
|
|
405
|
+
in out TRouteTree extends AnyRoute = AnyRoute,
|
|
406
|
+
in out TRouteMatch = MakeRouteMatchUnion,
|
|
166
407
|
> {
|
|
408
|
+
status: 'pending' | 'idle'
|
|
167
409
|
loadedAt: number
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
location:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
latestAction?: ActionState
|
|
178
|
-
actions: Record<string, Action>
|
|
179
|
-
loaders: Record<string, Loader>
|
|
180
|
-
pending?: PendingState
|
|
181
|
-
isFetching: boolean
|
|
182
|
-
isPreloading: boolean
|
|
410
|
+
isLoading: boolean
|
|
411
|
+
isTransitioning: boolean
|
|
412
|
+
matches: Array<TRouteMatch>
|
|
413
|
+
pendingMatches?: Array<TRouteMatch>
|
|
414
|
+
cachedMatches: Array<TRouteMatch>
|
|
415
|
+
location: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
416
|
+
resolvedLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
417
|
+
statusCode: number
|
|
418
|
+
redirect?: AnyRedirect
|
|
183
419
|
}
|
|
184
420
|
|
|
185
|
-
export interface PendingState {
|
|
186
|
-
location: Location
|
|
187
|
-
matches: RouteMatch[]
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
type Listener = (router: Router<any, any>) => void
|
|
191
|
-
|
|
192
|
-
export type ListenerFn = () => void
|
|
193
|
-
|
|
194
421
|
export interface BuildNextOptions {
|
|
195
422
|
to?: string | number | null
|
|
196
|
-
params?: true | Updater<
|
|
423
|
+
params?: true | Updater<unknown>
|
|
197
424
|
search?: true | Updater<unknown>
|
|
198
425
|
hash?: true | Updater<string>
|
|
199
|
-
|
|
426
|
+
state?: true | NonNullableUpdater<ParsedHistoryState, HistoryState>
|
|
427
|
+
mask?: {
|
|
428
|
+
to?: string | number | null
|
|
429
|
+
params?: true | Updater<unknown>
|
|
430
|
+
search?: true | Updater<unknown>
|
|
431
|
+
hash?: true | Updater<string>
|
|
432
|
+
state?: true | NonNullableUpdater<ParsedHistoryState, HistoryState>
|
|
433
|
+
unmaskOnReload?: boolean
|
|
434
|
+
}
|
|
200
435
|
from?: string
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
__postSearchFilters?: SearchFilter<any>[]
|
|
436
|
+
_fromLocation?: ParsedLocation
|
|
437
|
+
href?: string
|
|
204
438
|
}
|
|
205
439
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
440
|
+
type NavigationEventInfo = {
|
|
441
|
+
fromLocation?: ParsedLocation
|
|
442
|
+
toLocation: ParsedLocation
|
|
443
|
+
pathChanged: boolean
|
|
444
|
+
hrefChanged: boolean
|
|
445
|
+
hashChanged: boolean
|
|
209
446
|
}
|
|
210
447
|
|
|
211
|
-
export
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
448
|
+
export type RouterEvents = {
|
|
449
|
+
onBeforeNavigate: {
|
|
450
|
+
type: 'onBeforeNavigate'
|
|
451
|
+
} & NavigationEventInfo
|
|
452
|
+
onBeforeLoad: {
|
|
453
|
+
type: 'onBeforeLoad'
|
|
454
|
+
} & NavigationEventInfo
|
|
455
|
+
onLoad: {
|
|
456
|
+
type: 'onLoad'
|
|
457
|
+
} & NavigationEventInfo
|
|
458
|
+
onResolved: {
|
|
459
|
+
type: 'onResolved'
|
|
460
|
+
} & NavigationEventInfo
|
|
461
|
+
onBeforeRouteMount: {
|
|
462
|
+
type: 'onBeforeRouteMount'
|
|
463
|
+
} & NavigationEventInfo
|
|
464
|
+
onInjectedHtml: {
|
|
465
|
+
type: 'onInjectedHtml'
|
|
466
|
+
promise: Promise<string>
|
|
467
|
+
}
|
|
468
|
+
onRendered: {
|
|
469
|
+
type: 'onRendered'
|
|
470
|
+
} & NavigationEventInfo
|
|
217
471
|
}
|
|
218
472
|
|
|
219
|
-
export
|
|
220
|
-
pending: boolean
|
|
221
|
-
caseSensitive?: boolean
|
|
222
|
-
}
|
|
473
|
+
export type RouterEvent = RouterEvents[keyof RouterEvents]
|
|
223
474
|
|
|
224
|
-
type
|
|
225
|
-
preloadTimeout?: null | ReturnType<typeof setTimeout>
|
|
226
|
-
}
|
|
475
|
+
export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void
|
|
227
476
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
477
|
+
export type RouterListener<TRouterEvent extends RouterEvent> = {
|
|
478
|
+
eventType: TRouterEvent['type']
|
|
479
|
+
fn: ListenerFn<TRouterEvent>
|
|
231
480
|
}
|
|
232
481
|
|
|
233
|
-
interface
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
| 'routeLoaderData'
|
|
239
|
-
| 'loaderData'
|
|
240
|
-
| 'isInvalid'
|
|
241
|
-
| 'invalidAt'
|
|
242
|
-
> {}
|
|
243
|
-
|
|
244
|
-
export interface Router<
|
|
245
|
-
TRouteConfig extends AnyRouteConfig = RouteConfig,
|
|
246
|
-
TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
|
|
247
|
-
> {
|
|
248
|
-
history: BrowserHistory | MemoryHistory | HashHistory
|
|
249
|
-
options: PickAsRequired<
|
|
250
|
-
RouterOptions<TRouteConfig>,
|
|
251
|
-
'stringifySearch' | 'parseSearch'
|
|
252
|
-
>
|
|
253
|
-
// Computed in this.update()
|
|
254
|
-
basepath: string
|
|
255
|
-
// Internal:
|
|
256
|
-
allRouteInfo: TAllRouteInfo
|
|
257
|
-
listeners: Listener[]
|
|
258
|
-
location: Location
|
|
259
|
-
navigateTimeout?: Timeout
|
|
260
|
-
nextAction?: 'push' | 'replace'
|
|
261
|
-
state: RouterState
|
|
262
|
-
routeTree: Route<TAllRouteInfo, RouteInfo>
|
|
263
|
-
routesById: RoutesById<TAllRouteInfo>
|
|
264
|
-
navigationPromise: Promise<void>
|
|
265
|
-
removeActionQueue: { action: Action; actionState: ActionState }[]
|
|
266
|
-
startedLoadingAt: number
|
|
267
|
-
resolveNavigation: () => void
|
|
268
|
-
subscribe: (listener: Listener) => () => void
|
|
269
|
-
notify: () => void
|
|
270
|
-
mount: () => () => void
|
|
271
|
-
onFocus: () => void
|
|
272
|
-
update: <TRouteConfig extends RouteConfig = RouteConfig>(
|
|
273
|
-
opts?: RouterOptions<TRouteConfig>,
|
|
274
|
-
) => Router<TRouteConfig>
|
|
275
|
-
|
|
276
|
-
buildNext: (opts: BuildNextOptions) => Location
|
|
277
|
-
cancelMatches: () => void
|
|
278
|
-
loadLocation: (next?: Location) => Promise<void>
|
|
279
|
-
matchCache: Record<string, MatchCacheEntry>
|
|
280
|
-
cleanMatchCache: () => void
|
|
281
|
-
getRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
|
|
282
|
-
id: TId,
|
|
283
|
-
) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
|
|
284
|
-
loadRoute: (navigateOpts: BuildNextOptions) => Promise<RouteMatch[]>
|
|
285
|
-
preloadRoute: (
|
|
286
|
-
navigateOpts: BuildNextOptions,
|
|
287
|
-
loaderOpts: { maxAge?: number; gcMaxAge?: number },
|
|
288
|
-
) => Promise<RouteMatch[]>
|
|
289
|
-
matchRoutes: (
|
|
290
|
-
pathname: string,
|
|
291
|
-
opts?: { strictParseParams?: boolean },
|
|
292
|
-
) => RouteMatch[]
|
|
293
|
-
loadMatches: (
|
|
294
|
-
resolvedMatches: RouteMatch[],
|
|
295
|
-
loaderOpts?: { withPending?: boolean } & (
|
|
296
|
-
| { preload: true; maxAge: number; gcMaxAge: number }
|
|
297
|
-
| { preload?: false; maxAge?: never; gcMaxAge?: never }
|
|
298
|
-
),
|
|
299
|
-
) => Promise<void>
|
|
300
|
-
invalidateRoute: (opts: MatchLocation) => void
|
|
301
|
-
reload: () => Promise<void>
|
|
302
|
-
resolvePath: (from: string, path: string) => string
|
|
303
|
-
navigate: <
|
|
304
|
-
TFrom extends ValidFromPath<TAllRouteInfo> = '/',
|
|
305
|
-
TTo extends string = '.',
|
|
306
|
-
>(
|
|
307
|
-
opts: NavigateOptionsAbsolute<TAllRouteInfo, TFrom, TTo>,
|
|
308
|
-
) => Promise<void>
|
|
309
|
-
matchRoute: <
|
|
310
|
-
TFrom extends ValidFromPath<TAllRouteInfo> = '/',
|
|
311
|
-
TTo extends string = '.',
|
|
312
|
-
>(
|
|
313
|
-
matchLocation: ToOptions<TAllRouteInfo, TFrom, TTo>,
|
|
314
|
-
opts?: MatchRouteOptions,
|
|
315
|
-
) => boolean
|
|
316
|
-
buildLink: <
|
|
317
|
-
TFrom extends ValidFromPath<TAllRouteInfo> = '/',
|
|
318
|
-
TTo extends string = '.',
|
|
319
|
-
>(
|
|
320
|
-
opts: LinkOptions<TAllRouteInfo, TFrom, TTo>,
|
|
321
|
-
) => LinkInfo
|
|
322
|
-
dehydrateState: () => DehydratedRouterState
|
|
323
|
-
hydrateState: (state: DehydratedRouterState) => void
|
|
324
|
-
__: {
|
|
325
|
-
buildRouteTree: (
|
|
326
|
-
routeConfig: RouteConfig,
|
|
327
|
-
) => Route<TAllRouteInfo, AnyRouteInfo>
|
|
328
|
-
parseLocation: (
|
|
329
|
-
location: History['location'],
|
|
330
|
-
previousLocation?: Location,
|
|
331
|
-
) => Location
|
|
332
|
-
buildLocation: (dest: BuildNextOptions) => Location
|
|
333
|
-
commitLocation: (next: Location, replace?: boolean) => Promise<void>
|
|
334
|
-
navigate: (
|
|
335
|
-
location: BuildNextOptions & { replace?: boolean },
|
|
336
|
-
) => Promise<void>
|
|
337
|
-
}
|
|
482
|
+
export interface MatchRoutesOpts {
|
|
483
|
+
preload?: boolean
|
|
484
|
+
throwOnError?: boolean
|
|
485
|
+
_buildLocation?: boolean
|
|
486
|
+
dest?: BuildNextOptions
|
|
338
487
|
}
|
|
339
488
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
typeof window === 'undefined' || !window.document?.createElement
|
|
489
|
+
export type InferRouterContext<TRouteTree extends AnyRoute> =
|
|
490
|
+
TRouteTree['types']['routerContext']
|
|
343
491
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
export function createRouter<
|
|
349
|
-
TRouteConfig extends AnyRouteConfig = RouteConfig,
|
|
350
|
-
TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
|
|
351
|
-
>(
|
|
352
|
-
userOptions?: RouterOptions<TRouteConfig>,
|
|
353
|
-
): Router<TRouteConfig, TAllRouteInfo> {
|
|
354
|
-
const history = userOptions?.history || createDefaultHistory()
|
|
355
|
-
|
|
356
|
-
const originalOptions = {
|
|
357
|
-
defaultLoaderGcMaxAge: 5 * 60 * 1000,
|
|
358
|
-
defaultLoaderMaxAge: 0,
|
|
359
|
-
defaultPreloadMaxAge: 2000,
|
|
360
|
-
defaultPreloadDelay: 50,
|
|
361
|
-
...userOptions,
|
|
362
|
-
stringifySearch: userOptions?.stringifySearch ?? defaultStringifySearch,
|
|
363
|
-
parseSearch: userOptions?.parseSearch ?? defaultParseSearch,
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
let router: Router<TRouteConfig, TAllRouteInfo> = {
|
|
367
|
-
history,
|
|
368
|
-
options: originalOptions,
|
|
369
|
-
listeners: [],
|
|
370
|
-
removeActionQueue: [],
|
|
371
|
-
// Resolved after construction
|
|
372
|
-
basepath: '',
|
|
373
|
-
routeTree: undefined!,
|
|
374
|
-
routesById: {} as any,
|
|
375
|
-
location: undefined!,
|
|
376
|
-
allRouteInfo: undefined!,
|
|
377
|
-
//
|
|
378
|
-
navigationPromise: Promise.resolve(),
|
|
379
|
-
resolveNavigation: () => {},
|
|
380
|
-
matchCache: {},
|
|
381
|
-
state: {
|
|
382
|
-
status: 'idle',
|
|
383
|
-
location: null!,
|
|
384
|
-
matches: [],
|
|
385
|
-
actions: {},
|
|
386
|
-
loaders: {},
|
|
387
|
-
lastUpdated: Date.now(),
|
|
388
|
-
isFetching: false,
|
|
389
|
-
isPreloading: false,
|
|
390
|
-
},
|
|
391
|
-
startedLoadingAt: Date.now(),
|
|
392
|
-
subscribe: (listener: Listener): (() => void) => {
|
|
393
|
-
router.listeners.push(listener as Listener)
|
|
394
|
-
return () => {
|
|
395
|
-
router.listeners = router.listeners.filter((x) => x !== listener)
|
|
396
|
-
}
|
|
397
|
-
},
|
|
398
|
-
getRoute: (id) => {
|
|
399
|
-
return router.routesById[id]
|
|
400
|
-
},
|
|
401
|
-
notify: (): void => {
|
|
402
|
-
router.state = {
|
|
403
|
-
...router.state,
|
|
404
|
-
isFetching:
|
|
405
|
-
router.state.status === 'loading' ||
|
|
406
|
-
router.state.matches.some((d) => d.isFetching),
|
|
407
|
-
isPreloading: Object.values(router.matchCache).some(
|
|
408
|
-
(d) =>
|
|
409
|
-
d.match.isFetching &&
|
|
410
|
-
!router.state.matches.find((dd) => dd.matchId === d.match.matchId),
|
|
411
|
-
),
|
|
492
|
+
export type RouterContextOptions<TRouteTree extends AnyRoute> =
|
|
493
|
+
AnyContext extends InferRouterContext<TRouteTree>
|
|
494
|
+
? {
|
|
495
|
+
context?: InferRouterContext<TRouteTree>
|
|
412
496
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
router.listeners.forEach((listener) => listener(router))
|
|
416
|
-
},
|
|
417
|
-
|
|
418
|
-
dehydrateState: () => {
|
|
419
|
-
return {
|
|
420
|
-
...pick(router.state, ['status', 'location', 'lastUpdated']),
|
|
421
|
-
matches: router.state.matches.map((match) =>
|
|
422
|
-
pick(match, [
|
|
423
|
-
'matchId',
|
|
424
|
-
'status',
|
|
425
|
-
'routeLoaderData',
|
|
426
|
-
'loaderData',
|
|
427
|
-
'isInvalid',
|
|
428
|
-
'invalidAt',
|
|
429
|
-
]),
|
|
430
|
-
),
|
|
497
|
+
: {
|
|
498
|
+
context: InferRouterContext<TRouteTree>
|
|
431
499
|
}
|
|
432
|
-
},
|
|
433
500
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
501
|
+
export type RouterConstructorOptions<
|
|
502
|
+
TRouteTree extends AnyRoute,
|
|
503
|
+
TTrailingSlashOption extends TrailingSlashOption,
|
|
504
|
+
TDefaultStructuralSharingOption extends boolean,
|
|
505
|
+
TRouterHistory extends RouterHistory,
|
|
506
|
+
TDehydrated extends Record<string, any>,
|
|
507
|
+
> = Omit<
|
|
508
|
+
RouterOptions<
|
|
509
|
+
TRouteTree,
|
|
510
|
+
TTrailingSlashOption,
|
|
511
|
+
TDefaultStructuralSharingOption,
|
|
512
|
+
TRouterHistory,
|
|
513
|
+
TDehydrated
|
|
514
|
+
>,
|
|
515
|
+
'context'
|
|
516
|
+
> &
|
|
517
|
+
RouterContextOptions<TRouteTree>
|
|
518
|
+
|
|
519
|
+
export interface RouterErrorSerializer<TSerializedError> {
|
|
520
|
+
serialize: (err: unknown) => TSerializedError
|
|
521
|
+
deserialize: (err: TSerializedError) => unknown
|
|
522
|
+
}
|
|
439
523
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const dehydratedMatch = dehydratedState.matches.find(
|
|
445
|
-
(d: any) => d.matchId === match.matchId,
|
|
446
|
-
)
|
|
447
|
-
invariant(
|
|
448
|
-
dehydratedMatch,
|
|
449
|
-
'Oh no! Dehydrated route matches did not match the active state of the router 😬',
|
|
450
|
-
)
|
|
451
|
-
Object.assign(match, dehydratedMatch)
|
|
452
|
-
return match
|
|
453
|
-
}),
|
|
454
|
-
}
|
|
455
|
-
},
|
|
524
|
+
export interface MatchedRoutesResult {
|
|
525
|
+
matchedRoutes: Array<AnyRoute>
|
|
526
|
+
routeParams: Record<string, string>
|
|
527
|
+
}
|
|
456
528
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
529
|
+
export type PreloadRouteFn<
|
|
530
|
+
TRouteTree extends AnyRoute,
|
|
531
|
+
TTrailingSlashOption extends TrailingSlashOption,
|
|
532
|
+
TDefaultStructuralSharingOption extends boolean,
|
|
533
|
+
TRouterHistory extends RouterHistory,
|
|
534
|
+
> = <
|
|
535
|
+
TFrom extends RoutePaths<TRouteTree> | string = string,
|
|
536
|
+
TTo extends string | undefined = undefined,
|
|
537
|
+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
|
|
538
|
+
TMaskTo extends string = '',
|
|
539
|
+
>(
|
|
540
|
+
opts: NavigateOptions<
|
|
541
|
+
RouterCore<
|
|
542
|
+
TRouteTree,
|
|
543
|
+
TTrailingSlashOption,
|
|
544
|
+
TDefaultStructuralSharingOption,
|
|
545
|
+
TRouterHistory
|
|
546
|
+
>,
|
|
547
|
+
TFrom,
|
|
548
|
+
TTo,
|
|
549
|
+
TMaskFrom,
|
|
550
|
+
TMaskTo
|
|
551
|
+
>,
|
|
552
|
+
) => Promise<Array<AnyRouteMatch> | undefined>
|
|
553
|
+
|
|
554
|
+
export type MatchRouteFn<
|
|
555
|
+
TRouteTree extends AnyRoute,
|
|
556
|
+
TTrailingSlashOption extends TrailingSlashOption,
|
|
557
|
+
TDefaultStructuralSharingOption extends boolean,
|
|
558
|
+
TRouterHistory extends RouterHistory,
|
|
559
|
+
> = <
|
|
560
|
+
TFrom extends RoutePaths<TRouteTree> = '/',
|
|
561
|
+
TTo extends string | undefined = undefined,
|
|
562
|
+
TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
|
|
563
|
+
>(
|
|
564
|
+
location: ToOptions<
|
|
565
|
+
RouterCore<
|
|
566
|
+
TRouteTree,
|
|
567
|
+
TTrailingSlashOption,
|
|
568
|
+
TDefaultStructuralSharingOption,
|
|
569
|
+
TRouterHistory
|
|
570
|
+
>,
|
|
571
|
+
TFrom,
|
|
572
|
+
TTo
|
|
573
|
+
>,
|
|
574
|
+
opts?: MatchRouteOptions,
|
|
575
|
+
) => false | RouteById<TRouteTree, TResolved>['types']['allParams']
|
|
576
|
+
|
|
577
|
+
export type UpdateFn<
|
|
578
|
+
TRouteTree extends AnyRoute,
|
|
579
|
+
TTrailingSlashOption extends TrailingSlashOption,
|
|
580
|
+
TDefaultStructuralSharingOption extends boolean,
|
|
581
|
+
TRouterHistory extends RouterHistory,
|
|
582
|
+
TDehydrated extends Record<string, any>,
|
|
583
|
+
> = (
|
|
584
|
+
newOptions: RouterConstructorOptions<
|
|
585
|
+
TRouteTree,
|
|
586
|
+
TTrailingSlashOption,
|
|
587
|
+
TDefaultStructuralSharingOption,
|
|
588
|
+
TRouterHistory,
|
|
589
|
+
TDehydrated
|
|
590
|
+
>,
|
|
591
|
+
) => void
|
|
592
|
+
|
|
593
|
+
export type InvalidateFn<TRouter extends AnyRouter> = (opts?: {
|
|
594
|
+
filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean
|
|
595
|
+
sync?: boolean
|
|
596
|
+
}) => Promise<void>
|
|
597
|
+
|
|
598
|
+
export type ParseLocationFn<TRouteTree extends AnyRoute> = (
|
|
599
|
+
previousLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>,
|
|
600
|
+
locationToParse?: HistoryLocation,
|
|
601
|
+
) => ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
602
|
+
|
|
603
|
+
export type GetMatchRoutesFn = (
|
|
604
|
+
pathname: string,
|
|
605
|
+
routePathname: string | undefined,
|
|
606
|
+
) => {
|
|
607
|
+
matchedRoutes: Array<AnyRoute>
|
|
608
|
+
routeParams: Record<string, string>
|
|
609
|
+
foundRoute: AnyRoute | undefined
|
|
610
|
+
}
|
|
463
611
|
|
|
464
|
-
|
|
465
|
-
// to the current location. Otherwise, load the current location.
|
|
466
|
-
if (next.href !== router.location.href) {
|
|
467
|
-
router.__.commitLocation(next, true)
|
|
468
|
-
}
|
|
612
|
+
export type EmitFn = (routerEvent: RouterEvent) => void
|
|
469
613
|
|
|
470
|
-
|
|
614
|
+
export type LoadFn = (opts?: { sync?: boolean }) => Promise<void>
|
|
471
615
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
})
|
|
616
|
+
export type CommitLocationFn = ({
|
|
617
|
+
viewTransition,
|
|
618
|
+
ignoreBlocker,
|
|
619
|
+
...next
|
|
620
|
+
}: ParsedLocation & CommitLocationOptions) => Promise<void>
|
|
478
621
|
|
|
479
|
-
|
|
480
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
481
|
-
if (!isServer && window.addEventListener) {
|
|
482
|
-
// Listen to visibillitychange and focus
|
|
483
|
-
window.addEventListener('visibilitychange', router.onFocus, false)
|
|
484
|
-
window.addEventListener('focus', router.onFocus, false)
|
|
485
|
-
}
|
|
622
|
+
export type StartTransitionFn = (fn: () => void) => void
|
|
486
623
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
window.removeEventListener('focus', router.onFocus)
|
|
492
|
-
}
|
|
493
|
-
},
|
|
624
|
+
export type SubscribeFn = <TType extends keyof RouterEvents>(
|
|
625
|
+
eventType: TType,
|
|
626
|
+
fn: ListenerFn<RouterEvents[TType]>,
|
|
627
|
+
) => () => void
|
|
494
628
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
629
|
+
export interface MatchRoutesFn {
|
|
630
|
+
(
|
|
631
|
+
pathname: string,
|
|
632
|
+
locationSearch: AnySchema,
|
|
633
|
+
opts?: MatchRoutesOpts,
|
|
634
|
+
): Array<AnyRouteMatch>
|
|
635
|
+
(next: ParsedLocation, opts?: MatchRoutesOpts): Array<AnyRouteMatch>
|
|
636
|
+
(
|
|
637
|
+
pathnameOrNext: string | ParsedLocation,
|
|
638
|
+
locationSearchOrOpts?: AnySchema | MatchRoutesOpts,
|
|
639
|
+
opts?: MatchRoutesOpts,
|
|
640
|
+
): Array<AnyRouteMatch>
|
|
641
|
+
}
|
|
498
642
|
|
|
499
|
-
|
|
500
|
-
const newHistory = opts?.history !== router.history
|
|
501
|
-
if (!router.location || newHistory) {
|
|
502
|
-
if (opts?.history) {
|
|
503
|
-
router.history = opts.history
|
|
504
|
-
}
|
|
505
|
-
router.location = router.__.parseLocation(router.history.location)
|
|
506
|
-
router.state.location = router.location
|
|
507
|
-
}
|
|
643
|
+
export type GetMatchFn = (matchId: string) => AnyRouteMatch | undefined
|
|
508
644
|
|
|
509
|
-
|
|
645
|
+
export type UpdateMatchFn = (
|
|
646
|
+
id: string,
|
|
647
|
+
updater: (match: AnyRouteMatch) => AnyRouteMatch,
|
|
648
|
+
) => AnyRouteMatch
|
|
510
649
|
|
|
511
|
-
|
|
650
|
+
export type LoadRouteChunkFn = (route: AnyRoute) => Promise<Array<void>>
|
|
512
651
|
|
|
513
|
-
|
|
652
|
+
export type ResolveRedirect = (err: AnyRedirect) => ResolvedRedirect
|
|
514
653
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
654
|
+
export type ClearCacheFn<TRouter extends AnyRouter> = (opts?: {
|
|
655
|
+
filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean
|
|
656
|
+
}) => void
|
|
519
657
|
|
|
520
|
-
|
|
521
|
-
|
|
658
|
+
export interface ServerSrr {
|
|
659
|
+
injectedHtml: Array<InjectedHtmlEntry>
|
|
660
|
+
injectHtml: (getHtml: () => string | Promise<string>) => Promise<void>
|
|
661
|
+
injectScript: (
|
|
662
|
+
getScript: () => string | Promise<string>,
|
|
663
|
+
opts?: { logScript?: boolean },
|
|
664
|
+
) => Promise<void>
|
|
665
|
+
streamValue: (key: string, value: any) => void
|
|
666
|
+
streamedKeys: Set<string>
|
|
667
|
+
onMatchSettled: (opts: { router: AnyRouter; match: AnyRouteMatch }) => any
|
|
668
|
+
}
|
|
522
669
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
670
|
+
export type AnyRouterWithContext<TContext> = RouterCore<
|
|
671
|
+
AnyRouteWithContext<TContext>,
|
|
672
|
+
any,
|
|
673
|
+
any,
|
|
674
|
+
any,
|
|
675
|
+
any
|
|
676
|
+
>
|
|
677
|
+
|
|
678
|
+
export type AnyRouter = RouterCore<any, any, any, any, any>
|
|
679
|
+
|
|
680
|
+
export interface ViewTransitionOptions {
|
|
681
|
+
types:
|
|
682
|
+
| Array<string>
|
|
683
|
+
| ((locationChangeInfo: {
|
|
684
|
+
fromLocation?: ParsedLocation
|
|
685
|
+
toLocation: ParsedLocation
|
|
686
|
+
pathChanged: boolean
|
|
687
|
+
hrefChanged: boolean
|
|
688
|
+
hashChanged: boolean
|
|
689
|
+
}) => Array<string>)
|
|
690
|
+
}
|
|
531
691
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
692
|
+
export function defaultSerializeError(err: unknown) {
|
|
693
|
+
if (err instanceof Error) {
|
|
694
|
+
const obj = {
|
|
695
|
+
name: err.name,
|
|
696
|
+
message: err.message,
|
|
697
|
+
}
|
|
535
698
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
699
|
+
if (process.env.NODE_ENV === 'development') {
|
|
700
|
+
;(obj as any).stack = err.stack
|
|
701
|
+
}
|
|
540
702
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (router.state.currentAction === actionState) {
|
|
544
|
-
router.state.currentAction = undefined
|
|
545
|
-
}
|
|
546
|
-
if (action.current === actionState) {
|
|
547
|
-
action.current = undefined
|
|
548
|
-
}
|
|
549
|
-
})
|
|
550
|
-
router.removeActionQueue = []
|
|
703
|
+
return obj
|
|
704
|
+
}
|
|
551
705
|
|
|
552
|
-
|
|
553
|
-
|
|
706
|
+
return {
|
|
707
|
+
data: err,
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
export interface ExtractedBaseEntry {
|
|
711
|
+
dataType: '__beforeLoadContext' | 'loaderData'
|
|
712
|
+
type: string
|
|
713
|
+
path: Array<string>
|
|
714
|
+
id: number
|
|
715
|
+
matchIndex: number
|
|
716
|
+
}
|
|
554
717
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
718
|
+
export interface ExtractedStream extends ExtractedBaseEntry {
|
|
719
|
+
type: 'stream'
|
|
720
|
+
streamState: StreamState
|
|
721
|
+
}
|
|
559
722
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
location: router.location,
|
|
565
|
-
},
|
|
566
|
-
status: 'loading',
|
|
567
|
-
}
|
|
723
|
+
export interface ExtractedPromise extends ExtractedBaseEntry {
|
|
724
|
+
type: 'promise'
|
|
725
|
+
promiseState: DeferredPromiseState<any>
|
|
726
|
+
}
|
|
568
727
|
|
|
569
|
-
|
|
728
|
+
export type ExtractedEntry = ExtractedStream | ExtractedPromise
|
|
570
729
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
})
|
|
730
|
+
export type StreamState = {
|
|
731
|
+
promises: Array<ControlledPromise<string | null>>
|
|
732
|
+
}
|
|
575
733
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
734
|
+
export type TrailingSlashOption = 'always' | 'never' | 'preserve'
|
|
735
|
+
|
|
736
|
+
export function getLocationChangeInfo(routerState: {
|
|
737
|
+
resolvedLocation?: ParsedLocation
|
|
738
|
+
location: ParsedLocation
|
|
739
|
+
}) {
|
|
740
|
+
const fromLocation = routerState.resolvedLocation
|
|
741
|
+
const toLocation = routerState.location
|
|
742
|
+
const pathChanged = fromLocation?.pathname !== toLocation.pathname
|
|
743
|
+
const hrefChanged = fromLocation?.href !== toLocation.href
|
|
744
|
+
const hashChanged = fromLocation?.hash !== toLocation.hash
|
|
745
|
+
return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
|
|
746
|
+
}
|
|
580
747
|
|
|
581
|
-
|
|
748
|
+
export type CreateRouterFn = <
|
|
749
|
+
TRouteTree extends AnyRoute,
|
|
750
|
+
TTrailingSlashOption extends TrailingSlashOption = 'never',
|
|
751
|
+
TDefaultStructuralSharingOption extends boolean = false,
|
|
752
|
+
TRouterHistory extends RouterHistory = RouterHistory,
|
|
753
|
+
TDehydrated extends Record<string, any> = Record<string, any>,
|
|
754
|
+
>(
|
|
755
|
+
options: undefined extends number
|
|
756
|
+
? 'strictNullChecks must be enabled in tsconfig.json'
|
|
757
|
+
: RouterConstructorOptions<
|
|
758
|
+
TRouteTree,
|
|
759
|
+
TTrailingSlashOption,
|
|
760
|
+
TDefaultStructuralSharingOption,
|
|
761
|
+
TRouterHistory,
|
|
762
|
+
TDehydrated
|
|
763
|
+
>,
|
|
764
|
+
) => RouterCore<
|
|
765
|
+
TRouteTree,
|
|
766
|
+
TTrailingSlashOption,
|
|
767
|
+
TDefaultStructuralSharingOption,
|
|
768
|
+
TRouterHistory,
|
|
769
|
+
TDehydrated
|
|
770
|
+
>
|
|
771
|
+
|
|
772
|
+
export class RouterCore<
|
|
773
|
+
in out TRouteTree extends AnyRoute,
|
|
774
|
+
in out TTrailingSlashOption extends TrailingSlashOption,
|
|
775
|
+
in out TDefaultStructuralSharingOption extends boolean,
|
|
776
|
+
in out TRouterHistory extends RouterHistory = RouterHistory,
|
|
777
|
+
in out TDehydrated extends Record<string, any> = Record<string, any>,
|
|
778
|
+
> {
|
|
779
|
+
// Option-independent properties
|
|
780
|
+
tempLocationKey: string | undefined = `${Math.round(
|
|
781
|
+
Math.random() * 10000000,
|
|
782
|
+
)}`
|
|
783
|
+
resetNextScroll = true
|
|
784
|
+
shouldViewTransition?: boolean | ViewTransitionOptions = undefined
|
|
785
|
+
isViewTransitionTypesSupported?: boolean = undefined
|
|
786
|
+
subscribers = new Set<RouterListener<RouterEvent>>()
|
|
787
|
+
viewTransitionPromise?: ControlledPromise<true>
|
|
788
|
+
isScrollRestoring = false
|
|
789
|
+
isScrollRestorationSetup = false
|
|
790
|
+
|
|
791
|
+
// Must build in constructor
|
|
792
|
+
__store!: Store<RouterState<TRouteTree>>
|
|
793
|
+
options!: PickAsRequired<
|
|
794
|
+
RouterOptions<
|
|
795
|
+
TRouteTree,
|
|
796
|
+
TTrailingSlashOption,
|
|
797
|
+
TDefaultStructuralSharingOption,
|
|
798
|
+
TRouterHistory,
|
|
799
|
+
TDehydrated
|
|
800
|
+
>,
|
|
801
|
+
'stringifySearch' | 'parseSearch' | 'context'
|
|
802
|
+
>
|
|
803
|
+
history!: TRouterHistory
|
|
804
|
+
latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
805
|
+
basepath!: string
|
|
806
|
+
routeTree!: TRouteTree
|
|
807
|
+
routesById!: RoutesById<TRouteTree>
|
|
808
|
+
routesByPath!: RoutesByPath<TRouteTree>
|
|
809
|
+
flatRoutes!: Array<AnyRoute>
|
|
810
|
+
isServer!: boolean
|
|
811
|
+
pathParamsDecodeCharMap?: Map<string, string>
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* @deprecated Use the `createRouter` function instead
|
|
815
|
+
*/
|
|
816
|
+
constructor(
|
|
817
|
+
options: RouterConstructorOptions<
|
|
818
|
+
TRouteTree,
|
|
819
|
+
TTrailingSlashOption,
|
|
820
|
+
TDefaultStructuralSharingOption,
|
|
821
|
+
TRouterHistory,
|
|
822
|
+
TDehydrated
|
|
823
|
+
>,
|
|
824
|
+
) {
|
|
825
|
+
this.update({
|
|
826
|
+
defaultPreloadDelay: 50,
|
|
827
|
+
defaultPendingMs: 1000,
|
|
828
|
+
defaultPendingMinMs: 500,
|
|
829
|
+
context: undefined!,
|
|
830
|
+
...options,
|
|
831
|
+
caseSensitive: options.caseSensitive ?? false,
|
|
832
|
+
notFoundMode: options.notFoundMode ?? 'fuzzy',
|
|
833
|
+
stringifySearch: options.stringifySearch ?? defaultStringifySearch,
|
|
834
|
+
parseSearch: options.parseSearch ?? defaultParseSearch,
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
if (typeof document !== 'undefined') {
|
|
838
|
+
;(window as any).__TSR_ROUTER__ = this
|
|
839
|
+
}
|
|
840
|
+
}
|
|
582
841
|
|
|
583
|
-
|
|
584
|
-
|
|
842
|
+
// These are default implementations that can optionally be overridden
|
|
843
|
+
// by the router provider once rendered. We provide these so that the
|
|
844
|
+
// router can be used in a non-react environment if necessary
|
|
845
|
+
startTransition: StartTransitionFn = (fn) => fn()
|
|
846
|
+
|
|
847
|
+
update: UpdateFn<
|
|
848
|
+
TRouteTree,
|
|
849
|
+
TTrailingSlashOption,
|
|
850
|
+
TDefaultStructuralSharingOption,
|
|
851
|
+
TRouterHistory,
|
|
852
|
+
TDehydrated
|
|
853
|
+
> = (newOptions) => {
|
|
854
|
+
if (newOptions.notFoundRoute) {
|
|
855
|
+
console.warn(
|
|
856
|
+
'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.',
|
|
857
|
+
)
|
|
858
|
+
}
|
|
585
859
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
})
|
|
860
|
+
const previousOptions = this.options
|
|
861
|
+
this.options = {
|
|
862
|
+
...this.options,
|
|
863
|
+
...newOptions,
|
|
864
|
+
}
|
|
593
865
|
|
|
594
|
-
|
|
866
|
+
this.isServer = this.options.isServer ?? typeof document === 'undefined'
|
|
595
867
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (d.status === 'error' && !d.isFetching) {
|
|
603
|
-
d.status = 'idle'
|
|
604
|
-
d.error = undefined
|
|
605
|
-
}
|
|
606
|
-
const gc = Math.max(
|
|
607
|
-
d.options.loaderGcMaxAge ?? router.options.defaultLoaderGcMaxAge ?? 0,
|
|
608
|
-
d.options.loaderMaxAge ?? router.options.defaultLoaderMaxAge ?? 0,
|
|
868
|
+
this.pathParamsDecodeCharMap = this.options.pathParamsAllowedCharacters
|
|
869
|
+
? new Map(
|
|
870
|
+
this.options.pathParamsAllowedCharacters.map((char) => [
|
|
871
|
+
encodeURIComponent(char),
|
|
872
|
+
char,
|
|
873
|
+
]),
|
|
609
874
|
)
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
875
|
+
: undefined
|
|
876
|
+
|
|
877
|
+
if (
|
|
878
|
+
!this.basepath ||
|
|
879
|
+
(newOptions.basepath && newOptions.basepath !== previousOptions.basepath)
|
|
880
|
+
) {
|
|
881
|
+
if (
|
|
882
|
+
newOptions.basepath === undefined ||
|
|
883
|
+
newOptions.basepath === '' ||
|
|
884
|
+
newOptions.basepath === '/'
|
|
885
|
+
) {
|
|
886
|
+
this.basepath = '/'
|
|
887
|
+
} else {
|
|
888
|
+
this.basepath = `/${trimPath(newOptions.basepath)}`
|
|
889
|
+
}
|
|
890
|
+
}
|
|
617
891
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
892
|
+
if (
|
|
893
|
+
!this.history ||
|
|
894
|
+
(this.options.history && this.options.history !== this.history)
|
|
895
|
+
) {
|
|
896
|
+
this.history =
|
|
897
|
+
this.options.history ??
|
|
898
|
+
((this.isServer
|
|
899
|
+
? createMemoryHistory({
|
|
900
|
+
initialEntries: [this.basepath || '/'],
|
|
901
|
+
})
|
|
902
|
+
: createBrowserHistory()) as TRouterHistory)
|
|
903
|
+
this.latestLocation = this.parseLocation()
|
|
904
|
+
}
|
|
624
905
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
906
|
+
if (this.options.routeTree !== this.routeTree) {
|
|
907
|
+
this.routeTree = this.options.routeTree as TRouteTree
|
|
908
|
+
this.buildRouteTree()
|
|
909
|
+
}
|
|
628
910
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
911
|
+
if (!this.__store) {
|
|
912
|
+
this.__store = new Store(getInitialRouterState(this.latestLocation), {
|
|
913
|
+
onUpdate: () => {
|
|
914
|
+
this.__store.state = {
|
|
915
|
+
...this.state,
|
|
916
|
+
cachedMatches: this.state.cachedMatches.filter(
|
|
917
|
+
(d) => !['redirected'].includes(d.status),
|
|
918
|
+
),
|
|
919
|
+
}
|
|
920
|
+
},
|
|
635
921
|
})
|
|
636
922
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
await Promise.all(
|
|
640
|
-
matches.map((d) => d.__.loaderPromise || Promise.resolve()),
|
|
641
|
-
)
|
|
642
|
-
}
|
|
643
|
-
if (router.startedLoadingAt !== id) {
|
|
644
|
-
// Ignore side-effects of match loading
|
|
645
|
-
return
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
router.state = {
|
|
649
|
-
...router.state,
|
|
650
|
-
location: router.location,
|
|
651
|
-
matches,
|
|
652
|
-
pending: undefined,
|
|
653
|
-
status: 'idle',
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
router.notify()
|
|
657
|
-
router.resolveNavigation()
|
|
658
|
-
},
|
|
923
|
+
setupScrollRestoration(this)
|
|
924
|
+
}
|
|
659
925
|
|
|
660
|
-
|
|
661
|
-
|
|
926
|
+
if (
|
|
927
|
+
typeof window !== 'undefined' &&
|
|
928
|
+
'CSS' in window &&
|
|
929
|
+
typeof window.CSS?.supports === 'function'
|
|
930
|
+
) {
|
|
931
|
+
this.isViewTransitionTypesSupported = window.CSS.supports(
|
|
932
|
+
'selector(:active-view-transition-type(a)',
|
|
933
|
+
)
|
|
934
|
+
}
|
|
935
|
+
}
|
|
662
936
|
|
|
663
|
-
|
|
664
|
-
|
|
937
|
+
get state() {
|
|
938
|
+
return this.__store.state
|
|
939
|
+
}
|
|
665
940
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
941
|
+
buildRouteTree = () => {
|
|
942
|
+
const { routesById, routesByPath, flatRoutes } = processRouteTree({
|
|
943
|
+
routeTree: this.routeTree,
|
|
944
|
+
initRoute: (route, i) => {
|
|
945
|
+
route.init({
|
|
946
|
+
originalIndex: i,
|
|
947
|
+
defaultSsr: this.options.defaultSsr,
|
|
948
|
+
})
|
|
949
|
+
},
|
|
950
|
+
})
|
|
670
951
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
}
|
|
952
|
+
this.routesById = routesById as RoutesById<TRouteTree>
|
|
953
|
+
this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
|
|
954
|
+
this.flatRoutes = flatRoutes as Array<AnyRoute>
|
|
675
955
|
|
|
676
|
-
|
|
677
|
-
delete router.matchCache[matchId]
|
|
678
|
-
})
|
|
679
|
-
},
|
|
956
|
+
const notFoundRoute = this.options.notFoundRoute
|
|
680
957
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
958
|
+
if (notFoundRoute) {
|
|
959
|
+
notFoundRoute.init({
|
|
960
|
+
originalIndex: 99999999999,
|
|
961
|
+
defaultSsr: this.options.defaultSsr,
|
|
685
962
|
})
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
963
|
+
this.routesById[notFoundRoute.id] = notFoundRoute
|
|
964
|
+
}
|
|
965
|
+
}
|
|
689
966
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
await router.loadMatches(matches, {
|
|
696
|
-
preload: true,
|
|
697
|
-
maxAge:
|
|
698
|
-
loaderOpts.maxAge ??
|
|
699
|
-
router.options.defaultPreloadMaxAge ??
|
|
700
|
-
router.options.defaultLoaderMaxAge ??
|
|
701
|
-
0,
|
|
702
|
-
gcMaxAge:
|
|
703
|
-
loaderOpts.gcMaxAge ??
|
|
704
|
-
router.options.defaultPreloadGcMaxAge ??
|
|
705
|
-
router.options.defaultLoaderGcMaxAge ??
|
|
706
|
-
0,
|
|
707
|
-
})
|
|
708
|
-
return matches
|
|
709
|
-
},
|
|
967
|
+
subscribe: SubscribeFn = (eventType, fn) => {
|
|
968
|
+
const listener: RouterListener<any> = {
|
|
969
|
+
eventType,
|
|
970
|
+
fn,
|
|
971
|
+
}
|
|
710
972
|
|
|
711
|
-
|
|
712
|
-
router.cleanMatchCache()
|
|
973
|
+
this.subscribers.add(listener)
|
|
713
974
|
|
|
714
|
-
|
|
975
|
+
return () => {
|
|
976
|
+
this.subscribers.delete(listener)
|
|
977
|
+
}
|
|
978
|
+
}
|
|
715
979
|
|
|
716
|
-
|
|
717
|
-
|
|
980
|
+
emit: EmitFn = (routerEvent) => {
|
|
981
|
+
this.subscribers.forEach((listener) => {
|
|
982
|
+
if (listener.eventType === routerEvent.type) {
|
|
983
|
+
listener.fn(routerEvent)
|
|
718
984
|
}
|
|
985
|
+
})
|
|
986
|
+
}
|
|
719
987
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
988
|
+
parseLocation: ParseLocationFn<TRouteTree> = (
|
|
989
|
+
previousLocation,
|
|
990
|
+
locationToParse,
|
|
991
|
+
) => {
|
|
992
|
+
const parse = ({
|
|
993
|
+
pathname,
|
|
994
|
+
search,
|
|
995
|
+
hash,
|
|
996
|
+
state,
|
|
997
|
+
}: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
|
|
998
|
+
const parsedSearch = this.options.parseSearch(search)
|
|
999
|
+
const searchStr = this.options.stringifySearch(parsedSearch)
|
|
724
1000
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1001
|
+
return {
|
|
1002
|
+
pathname,
|
|
1003
|
+
searchStr,
|
|
1004
|
+
search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
|
|
1005
|
+
hash: hash.split('#').reverse()[0] ?? '',
|
|
1006
|
+
href: `${pathname}${searchStr}${hash}`,
|
|
1007
|
+
state: replaceEqualDeep(previousLocation?.state, state),
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
728
1010
|
|
|
729
|
-
|
|
1011
|
+
const location = parse(locationToParse ?? this.history.location)
|
|
730
1012
|
|
|
731
|
-
|
|
1013
|
+
const { __tempLocation, __tempKey } = location.state
|
|
732
1014
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
[...foundRoutes, route],
|
|
738
|
-
route.childRoutes,
|
|
739
|
-
)
|
|
740
|
-
}
|
|
1015
|
+
if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
|
|
1016
|
+
// Sync up the location keys
|
|
1017
|
+
const parsedTempLocation = parse(__tempLocation) as any
|
|
1018
|
+
parsedTempLocation.state.key = location.state.key
|
|
741
1019
|
|
|
742
|
-
|
|
743
|
-
route.routePath !== '/' || route.childRoutes?.length
|
|
744
|
-
)
|
|
1020
|
+
delete parsedTempLocation.state.__tempLocation
|
|
745
1021
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
})
|
|
1022
|
+
return {
|
|
1023
|
+
...parsedTempLocation,
|
|
1024
|
+
maskedLocation: location,
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
752
1027
|
|
|
753
|
-
|
|
754
|
-
|
|
1028
|
+
return location
|
|
1029
|
+
}
|
|
755
1030
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1031
|
+
resolvePathWithBase = (from: string, path: string) => {
|
|
1032
|
+
const resolvedPath = resolvePath({
|
|
1033
|
+
basepath: this.basepath,
|
|
1034
|
+
base: from,
|
|
1035
|
+
to: cleanPath(path),
|
|
1036
|
+
trailingSlash: this.options.trailingSlash,
|
|
1037
|
+
caseSensitive: this.options.caseSensitive,
|
|
1038
|
+
})
|
|
1039
|
+
return resolvedPath
|
|
1040
|
+
}
|
|
764
1041
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
}
|
|
1042
|
+
get looseRoutesById() {
|
|
1043
|
+
return this.routesById as Record<string, AnyRoute>
|
|
1044
|
+
}
|
|
770
1045
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1046
|
+
/**
|
|
1047
|
+
@deprecated use the following signature instead
|
|
1048
|
+
```ts
|
|
1049
|
+
matchRoutes (
|
|
1050
|
+
next: ParsedLocation,
|
|
1051
|
+
opts?: { preload?: boolean; throwOnError?: boolean },
|
|
1052
|
+
): Array<AnyRouteMatch>;
|
|
1053
|
+
```
|
|
1054
|
+
*/
|
|
1055
|
+
matchRoutes: MatchRoutesFn = (
|
|
1056
|
+
pathnameOrNext: string | ParsedLocation,
|
|
1057
|
+
locationSearchOrOpts?: AnySchema | MatchRoutesOpts,
|
|
1058
|
+
opts?: MatchRoutesOpts,
|
|
1059
|
+
) => {
|
|
1060
|
+
if (typeof pathnameOrNext === 'string') {
|
|
1061
|
+
return this.matchRoutesInternal(
|
|
1062
|
+
{
|
|
1063
|
+
pathname: pathnameOrNext,
|
|
1064
|
+
search: locationSearchOrOpts,
|
|
1065
|
+
} as ParsedLocation,
|
|
1066
|
+
opts,
|
|
1067
|
+
)
|
|
1068
|
+
} else {
|
|
1069
|
+
return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
774
1072
|
|
|
775
|
-
|
|
776
|
-
|
|
1073
|
+
private matchRoutesInternal(
|
|
1074
|
+
next: ParsedLocation,
|
|
1075
|
+
opts?: MatchRoutesOpts,
|
|
1076
|
+
): Array<AnyRouteMatch> {
|
|
1077
|
+
const { foundRoute, matchedRoutes, routeParams } = this.getMatchedRoutes(
|
|
1078
|
+
next.pathname,
|
|
1079
|
+
opts?.dest?.to as string,
|
|
1080
|
+
)
|
|
1081
|
+
let isGlobalNotFound = false
|
|
1082
|
+
|
|
1083
|
+
// Check to see if the route needs a 404 entry
|
|
1084
|
+
if (
|
|
1085
|
+
// If we found a route, and it's not an index route and we have left over path
|
|
1086
|
+
foundRoute
|
|
1087
|
+
? foundRoute.path !== '/' && routeParams['**']
|
|
1088
|
+
: // Or if we didn't find a route and we have left over path
|
|
1089
|
+
trimPathRight(next.pathname)
|
|
1090
|
+
) {
|
|
1091
|
+
// If the user has defined an (old) 404 route, use it
|
|
1092
|
+
if (this.options.notFoundRoute) {
|
|
1093
|
+
matchedRoutes.push(this.options.notFoundRoute)
|
|
1094
|
+
} else {
|
|
1095
|
+
// If there is no routes found during path matching
|
|
1096
|
+
isGlobalNotFound = true
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const globalNotFoundRouteId = (() => {
|
|
1101
|
+
if (!isGlobalNotFound) {
|
|
1102
|
+
return undefined
|
|
1103
|
+
}
|
|
777
1104
|
|
|
778
|
-
|
|
1105
|
+
if (this.options.notFoundMode !== 'root') {
|
|
1106
|
+
for (let i = matchedRoutes.length - 1; i >= 0; i--) {
|
|
1107
|
+
const route = matchedRoutes[i]!
|
|
1108
|
+
if (route.children) {
|
|
1109
|
+
return route.id
|
|
1110
|
+
}
|
|
779
1111
|
}
|
|
1112
|
+
}
|
|
780
1113
|
|
|
781
|
-
|
|
1114
|
+
return rootRouteId
|
|
1115
|
+
})()
|
|
782
1116
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
}
|
|
1117
|
+
const parseErrors = matchedRoutes.map((route) => {
|
|
1118
|
+
let parsedParamsError
|
|
786
1119
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const matchId = interpolatePath(foundRoute.routeId, params, true)
|
|
790
|
-
|
|
791
|
-
const match =
|
|
792
|
-
existingMatches.find((d) => d.matchId === matchId) ||
|
|
793
|
-
router.matchCache[matchId]?.match ||
|
|
794
|
-
createRouteMatch(router, foundRoute, {
|
|
795
|
-
matchId,
|
|
796
|
-
params,
|
|
797
|
-
pathname: joinPaths([pathname, interpolatedPath]),
|
|
798
|
-
})
|
|
1120
|
+
const parseParams =
|
|
1121
|
+
route.options.params?.parse ?? route.options.parseParams
|
|
799
1122
|
|
|
800
|
-
|
|
801
|
-
|
|
1123
|
+
if (parseParams) {
|
|
1124
|
+
try {
|
|
1125
|
+
const parsedParams = parseParams(routeParams)
|
|
1126
|
+
// Add the parsed params to the accumulated params bag
|
|
1127
|
+
Object.assign(routeParams, parsedParams)
|
|
1128
|
+
} catch (err: any) {
|
|
1129
|
+
parsedParamsError = new PathParamError(err.message, {
|
|
1130
|
+
cause: err,
|
|
1131
|
+
})
|
|
802
1132
|
|
|
803
|
-
|
|
1133
|
+
if (opts?.throwOnError) {
|
|
1134
|
+
throw parsedParamsError
|
|
1135
|
+
}
|
|
804
1136
|
|
|
805
|
-
|
|
806
|
-
recurse(foundRoute.childRoutes)
|
|
1137
|
+
return parsedParamsError
|
|
807
1138
|
}
|
|
808
1139
|
}
|
|
809
1140
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
cascadeLoaderData(matches)
|
|
813
|
-
|
|
814
|
-
return matches
|
|
815
|
-
},
|
|
1141
|
+
return
|
|
1142
|
+
})
|
|
816
1143
|
|
|
817
|
-
|
|
818
|
-
const matchPromises = resolvedMatches.map(async (match) => {
|
|
819
|
-
// Validate the match (loads search params etc)
|
|
820
|
-
match.__.validate()
|
|
821
|
-
match.load(loaderOpts)
|
|
1144
|
+
const matches: Array<AnyRouteMatch> = []
|
|
822
1145
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
if (loaderOpts?.withPending) match.__.startPending()
|
|
1146
|
+
const getParentContext = (parentMatch?: AnyRouteMatch) => {
|
|
1147
|
+
const parentMatchId = parentMatch?.id
|
|
826
1148
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
})
|
|
1149
|
+
const parentContext = !parentMatchId
|
|
1150
|
+
? ((this.options.context as any) ?? {})
|
|
1151
|
+
: (parentMatch.context ?? this.options.context ?? {})
|
|
832
1152
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
await Promise.all(matchPromises)
|
|
836
|
-
},
|
|
837
|
-
|
|
838
|
-
invalidateRoute: (opts: MatchLocation) => {
|
|
839
|
-
const next = router.buildNext(opts)
|
|
840
|
-
const unloadedMatchIds = router
|
|
841
|
-
.matchRoutes(next.pathname)
|
|
842
|
-
.map((d) => d.matchId)
|
|
843
|
-
;[
|
|
844
|
-
...router.state.matches,
|
|
845
|
-
...(router.state.pending?.matches ?? []),
|
|
846
|
-
].forEach((match) => {
|
|
847
|
-
if (unloadedMatchIds.includes(match.matchId)) {
|
|
848
|
-
match.invalidate()
|
|
849
|
-
}
|
|
850
|
-
})
|
|
851
|
-
},
|
|
1153
|
+
return parentContext
|
|
1154
|
+
}
|
|
852
1155
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1156
|
+
matchedRoutes.forEach((route, index) => {
|
|
1157
|
+
// Take each matched route and resolve + validate its search params
|
|
1158
|
+
// This has to happen serially because each route's search params
|
|
1159
|
+
// can depend on the parent route's search params
|
|
1160
|
+
// It must also happen before we create the match so that we can
|
|
1161
|
+
// pass the search params to the route's potential key function
|
|
1162
|
+
// which is used to uniquely identify the route match in state
|
|
1163
|
+
|
|
1164
|
+
const parentMatch = matches[index - 1]
|
|
1165
|
+
|
|
1166
|
+
const [preMatchSearch, strictMatchSearch, searchError]: [
|
|
1167
|
+
Record<string, any>,
|
|
1168
|
+
Record<string, any>,
|
|
1169
|
+
any,
|
|
1170
|
+
] = (() => {
|
|
1171
|
+
// Validate the search params and stabilize them
|
|
1172
|
+
const parentSearch = parentMatch?.search ?? next.search
|
|
1173
|
+
const parentStrictSearch = parentMatch?._strictSearch ?? {}
|
|
1174
|
+
|
|
1175
|
+
try {
|
|
1176
|
+
const strictSearch =
|
|
1177
|
+
validateSearch(route.options.validateSearch, { ...parentSearch }) ??
|
|
1178
|
+
{}
|
|
1179
|
+
|
|
1180
|
+
return [
|
|
1181
|
+
{
|
|
1182
|
+
...parentSearch,
|
|
1183
|
+
...strictSearch,
|
|
1184
|
+
},
|
|
1185
|
+
{ ...parentStrictSearch, ...strictSearch },
|
|
1186
|
+
undefined,
|
|
1187
|
+
]
|
|
1188
|
+
} catch (err: any) {
|
|
1189
|
+
let searchParamError = err
|
|
1190
|
+
if (!(err instanceof SearchParamError)) {
|
|
1191
|
+
searchParamError = new SearchParamError(err.message, {
|
|
1192
|
+
cause: err,
|
|
1193
|
+
})
|
|
1194
|
+
}
|
|
859
1195
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1196
|
+
if (opts?.throwOnError) {
|
|
1197
|
+
throw searchParamError
|
|
1198
|
+
}
|
|
863
1199
|
|
|
864
|
-
|
|
865
|
-
|
|
1200
|
+
return [parentSearch, {}, searchParamError]
|
|
1201
|
+
}
|
|
1202
|
+
})()
|
|
866
1203
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
: undefined,
|
|
872
|
-
}
|
|
1204
|
+
// This is where we need to call route.options.loaderDeps() to get any additional
|
|
1205
|
+
// deps that the route's loader function might need to run. We need to do this
|
|
1206
|
+
// before we create the match so that we can pass the deps to the route's
|
|
1207
|
+
// potential key function which is used to uniquely identify the route match in state
|
|
873
1208
|
|
|
874
|
-
const
|
|
1209
|
+
const loaderDeps =
|
|
1210
|
+
route.options.loaderDeps?.({
|
|
1211
|
+
search: preMatchSearch,
|
|
1212
|
+
}) ?? ''
|
|
875
1213
|
|
|
876
|
-
|
|
877
|
-
if (!router.state.pending?.location) {
|
|
878
|
-
return false
|
|
879
|
-
}
|
|
880
|
-
return !!matchPathname(router.state.pending.location.pathname, {
|
|
881
|
-
...opts,
|
|
882
|
-
to: next.pathname,
|
|
883
|
-
})
|
|
884
|
-
}
|
|
1214
|
+
const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : ''
|
|
885
1215
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1216
|
+
const { usedParams, interpolatedPath } = interpolatePath({
|
|
1217
|
+
path: route.fullPath,
|
|
1218
|
+
params: routeParams,
|
|
1219
|
+
decodeCharMap: this.pathParamsDecodeCharMap,
|
|
889
1220
|
})
|
|
890
|
-
},
|
|
891
1221
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1222
|
+
const matchId =
|
|
1223
|
+
interpolatePath({
|
|
1224
|
+
path: route.id,
|
|
1225
|
+
params: routeParams,
|
|
1226
|
+
leaveWildcards: true,
|
|
1227
|
+
decodeCharMap: this.pathParamsDecodeCharMap,
|
|
1228
|
+
}).interpolatedPath + loaderDepsHash
|
|
895
1229
|
|
|
896
|
-
// If
|
|
897
|
-
//
|
|
898
|
-
|
|
899
|
-
const fromString = String(from)
|
|
1230
|
+
// Waste not, want not. If we already have a match for this route,
|
|
1231
|
+
// reuse it. This is important for layout routes, which might stick
|
|
1232
|
+
// around between navigation actions that only change leaf routes.
|
|
900
1233
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
new URL(`${toString}`)
|
|
905
|
-
isExternal = true
|
|
906
|
-
} catch (e) {}
|
|
1234
|
+
// Existing matches are matches that are already loaded along with
|
|
1235
|
+
// pending matches that are still loading
|
|
1236
|
+
const existingMatch = this.getMatch(matchId)
|
|
907
1237
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
'Attempting to navigate to external url with router.navigate!',
|
|
1238
|
+
const previousMatch = this.state.matches.find(
|
|
1239
|
+
(d) => d.routeId === route.id,
|
|
911
1240
|
)
|
|
912
1241
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
target,
|
|
930
|
-
replace,
|
|
931
|
-
activeOptions,
|
|
932
|
-
preload,
|
|
933
|
-
preloadMaxAge: userPreloadMaxAge,
|
|
934
|
-
preloadGcMaxAge: userPreloadGcMaxAge,
|
|
935
|
-
preloadDelay: userPreloadDelay,
|
|
936
|
-
disabled,
|
|
937
|
-
}) => {
|
|
938
|
-
// If this link simply reloads the current route,
|
|
939
|
-
// make sure it has a new key so it will trigger a data refresh
|
|
940
|
-
|
|
941
|
-
// If this `to` is a valid external URL, return
|
|
942
|
-
// null for LinkUtils
|
|
943
|
-
|
|
944
|
-
try {
|
|
945
|
-
new URL(`${to}`)
|
|
946
|
-
return {
|
|
947
|
-
type: 'external',
|
|
948
|
-
href: to,
|
|
1242
|
+
const cause = previousMatch ? 'stay' : 'enter'
|
|
1243
|
+
|
|
1244
|
+
let match: AnyRouteMatch
|
|
1245
|
+
|
|
1246
|
+
if (existingMatch) {
|
|
1247
|
+
match = {
|
|
1248
|
+
...existingMatch,
|
|
1249
|
+
cause,
|
|
1250
|
+
params: previousMatch
|
|
1251
|
+
? replaceEqualDeep(previousMatch.params, routeParams)
|
|
1252
|
+
: routeParams,
|
|
1253
|
+
_strictParams: usedParams,
|
|
1254
|
+
search: previousMatch
|
|
1255
|
+
? replaceEqualDeep(previousMatch.search, preMatchSearch)
|
|
1256
|
+
: replaceEqualDeep(existingMatch.search, preMatchSearch),
|
|
1257
|
+
_strictSearch: strictMatchSearch,
|
|
949
1258
|
}
|
|
950
|
-
}
|
|
1259
|
+
} else {
|
|
1260
|
+
const status =
|
|
1261
|
+
route.options.loader ||
|
|
1262
|
+
route.options.beforeLoad ||
|
|
1263
|
+
route.lazyFn ||
|
|
1264
|
+
routeNeedsPreload(route)
|
|
1265
|
+
? 'pending'
|
|
1266
|
+
: 'success'
|
|
1267
|
+
|
|
1268
|
+
match = {
|
|
1269
|
+
id: matchId,
|
|
1270
|
+
index,
|
|
1271
|
+
routeId: route.id,
|
|
1272
|
+
params: previousMatch
|
|
1273
|
+
? replaceEqualDeep(previousMatch.params, routeParams)
|
|
1274
|
+
: routeParams,
|
|
1275
|
+
_strictParams: usedParams,
|
|
1276
|
+
pathname: joinPaths([this.basepath, interpolatedPath]),
|
|
1277
|
+
updatedAt: Date.now(),
|
|
1278
|
+
search: previousMatch
|
|
1279
|
+
? replaceEqualDeep(previousMatch.search, preMatchSearch)
|
|
1280
|
+
: preMatchSearch,
|
|
1281
|
+
_strictSearch: strictMatchSearch,
|
|
1282
|
+
searchError: undefined,
|
|
1283
|
+
status,
|
|
1284
|
+
isFetching: false,
|
|
1285
|
+
error: undefined,
|
|
1286
|
+
paramsError: parseErrors[index],
|
|
1287
|
+
__routeContext: {},
|
|
1288
|
+
__beforeLoadContext: {},
|
|
1289
|
+
context: {},
|
|
1290
|
+
abortController: new AbortController(),
|
|
1291
|
+
fetchCount: 0,
|
|
1292
|
+
cause,
|
|
1293
|
+
loaderDeps: previousMatch
|
|
1294
|
+
? replaceEqualDeep(previousMatch.loaderDeps, loaderDeps)
|
|
1295
|
+
: loaderDeps,
|
|
1296
|
+
invalid: false,
|
|
1297
|
+
preload: false,
|
|
1298
|
+
links: undefined,
|
|
1299
|
+
scripts: undefined,
|
|
1300
|
+
headScripts: undefined,
|
|
1301
|
+
meta: undefined,
|
|
1302
|
+
staticData: route.options.staticData || {},
|
|
1303
|
+
loadPromise: createControlledPromise(),
|
|
1304
|
+
fullPath: route.fullPath,
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
951
1307
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
search,
|
|
956
|
-
params,
|
|
957
|
-
hash,
|
|
958
|
-
replace,
|
|
1308
|
+
if (!opts?.preload) {
|
|
1309
|
+
// If we have a global not found, mark the right match as global not found
|
|
1310
|
+
match.globalNotFound = globalNotFoundRouteId === route.id
|
|
959
1311
|
}
|
|
960
1312
|
|
|
961
|
-
|
|
1313
|
+
// update the searchError if there is one
|
|
1314
|
+
match.searchError = searchError
|
|
962
1315
|
|
|
963
|
-
|
|
964
|
-
const preloadDelay =
|
|
965
|
-
userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0
|
|
1316
|
+
const parentContext = getParentContext(parentMatch)
|
|
966
1317
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
(d, i) => d === currentPathSplit[i],
|
|
973
|
-
)
|
|
974
|
-
const hashIsEqual = router.state.location.hash === next.hash
|
|
975
|
-
// Combine the matches based on user options
|
|
976
|
-
const pathTest = activeOptions?.exact ? pathIsEqual : pathIsFuzzyEqual
|
|
977
|
-
const hashTest = activeOptions?.includeHash ? hashIsEqual : true
|
|
1318
|
+
match.context = {
|
|
1319
|
+
...parentContext,
|
|
1320
|
+
...match.__routeContext,
|
|
1321
|
+
...match.__beforeLoadContext,
|
|
1322
|
+
}
|
|
978
1323
|
|
|
979
|
-
|
|
980
|
-
|
|
1324
|
+
matches.push(match)
|
|
1325
|
+
})
|
|
1326
|
+
|
|
1327
|
+
matches.forEach((match, index) => {
|
|
1328
|
+
const route = this.looseRoutesById[match.routeId]!
|
|
1329
|
+
const existingMatch = this.getMatch(match.id)
|
|
1330
|
+
|
|
1331
|
+
// only execute `context` if we are not just building a location
|
|
1332
|
+
if (!existingMatch && opts?._buildLocation !== true) {
|
|
1333
|
+
const parentMatch = matches[index - 1]
|
|
1334
|
+
const parentContext = getParentContext(parentMatch)
|
|
1335
|
+
|
|
1336
|
+
// Update the match's context
|
|
1337
|
+
const contextFnContext: RouteContextOptions<any, any, any, any> = {
|
|
1338
|
+
deps: match.loaderDeps,
|
|
1339
|
+
params: match.params,
|
|
1340
|
+
context: parentContext,
|
|
1341
|
+
location: next,
|
|
1342
|
+
navigate: (opts: any) =>
|
|
1343
|
+
this.navigate({ ...opts, _fromLocation: next }),
|
|
1344
|
+
buildLocation: this.buildLocation,
|
|
1345
|
+
cause: match.cause,
|
|
1346
|
+
abortController: match.abortController,
|
|
1347
|
+
preload: !!match.preload,
|
|
1348
|
+
matches,
|
|
1349
|
+
}
|
|
981
1350
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if (
|
|
985
|
-
!disabled &&
|
|
986
|
-
!isCtrlEvent(e) &&
|
|
987
|
-
!e.defaultPrevented &&
|
|
988
|
-
(!target || target === '_self') &&
|
|
989
|
-
e.button === 0
|
|
990
|
-
) {
|
|
991
|
-
e.preventDefault()
|
|
992
|
-
if (pathIsEqual && !search && !hash) {
|
|
993
|
-
router.invalidateRoute(nextOpts)
|
|
994
|
-
}
|
|
1351
|
+
// Get the route context
|
|
1352
|
+
match.__routeContext = route.options.context?.(contextFnContext) ?? {}
|
|
995
1353
|
|
|
996
|
-
|
|
997
|
-
|
|
1354
|
+
match.context = {
|
|
1355
|
+
...parentContext,
|
|
1356
|
+
...match.__routeContext,
|
|
1357
|
+
...match.__beforeLoadContext,
|
|
998
1358
|
}
|
|
999
1359
|
}
|
|
1000
1360
|
|
|
1001
|
-
//
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1361
|
+
// If it's already a success, update headers and head content
|
|
1362
|
+
// These may get updated again if the match is refreshed
|
|
1363
|
+
// due to being stale
|
|
1364
|
+
if (match.status === 'success') {
|
|
1365
|
+
match.headers = route.options.headers?.({
|
|
1366
|
+
loaderData: match.loaderData,
|
|
1367
|
+
})
|
|
1368
|
+
const assetContext = {
|
|
1369
|
+
matches,
|
|
1370
|
+
match,
|
|
1371
|
+
params: match.params,
|
|
1372
|
+
loaderData: match.loaderData,
|
|
1008
1373
|
}
|
|
1374
|
+
const headFnContent = route.options.head?.(assetContext)
|
|
1375
|
+
match.links = headFnContent?.links
|
|
1376
|
+
match.headScripts = headFnContent?.scripts
|
|
1377
|
+
match.meta = headFnContent?.meta
|
|
1378
|
+
match.scripts = route.options.scripts?.(assetContext)
|
|
1009
1379
|
}
|
|
1380
|
+
})
|
|
1010
1381
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1382
|
+
return matches
|
|
1383
|
+
}
|
|
1013
1384
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1385
|
+
getMatchedRoutes: GetMatchRoutesFn = (
|
|
1386
|
+
pathname: string,
|
|
1387
|
+
routePathname: string | undefined,
|
|
1388
|
+
) => {
|
|
1389
|
+
return getMatchedRoutes({
|
|
1390
|
+
pathname,
|
|
1391
|
+
routePathname,
|
|
1392
|
+
basepath: this.basepath,
|
|
1393
|
+
caseSensitive: this.options.caseSensitive,
|
|
1394
|
+
routesByPath: this.routesByPath,
|
|
1395
|
+
routesById: this.routesById,
|
|
1396
|
+
flatRoutes: this.flatRoutes,
|
|
1397
|
+
})
|
|
1398
|
+
}
|
|
1018
1399
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
router.preloadRoute(nextOpts, {
|
|
1022
|
-
maxAge: userPreloadMaxAge,
|
|
1023
|
-
gcMaxAge: userPreloadGcMaxAge,
|
|
1024
|
-
})
|
|
1025
|
-
}, preloadDelay)
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1400
|
+
cancelMatch = (id: string) => {
|
|
1401
|
+
const match = this.getMatch(id)
|
|
1028
1402
|
|
|
1029
|
-
|
|
1030
|
-
const target = (e.target || {}) as LinkCurrentTargetElement
|
|
1403
|
+
if (!match) return
|
|
1031
1404
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1405
|
+
match.abortController.abort()
|
|
1406
|
+
clearTimeout(match.pendingTimeout)
|
|
1407
|
+
}
|
|
1037
1408
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1409
|
+
cancelMatches = () => {
|
|
1410
|
+
this.state.pendingMatches?.forEach((match) => {
|
|
1411
|
+
this.cancelMatch(match.id)
|
|
1412
|
+
})
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
buildLocation: BuildLocationFn = (opts) => {
|
|
1416
|
+
const build = (
|
|
1417
|
+
dest: BuildNextOptions & {
|
|
1418
|
+
unmaskOnReload?: boolean
|
|
1419
|
+
} = {},
|
|
1420
|
+
matchedRoutesResult?: MatchedRoutesResult,
|
|
1421
|
+
): ParsedLocation => {
|
|
1422
|
+
const fromMatches = dest._fromLocation
|
|
1423
|
+
? this.matchRoutes(dest._fromLocation, { _buildLocation: true })
|
|
1424
|
+
: this.state.matches
|
|
1425
|
+
|
|
1426
|
+
const fromMatch =
|
|
1427
|
+
dest.from != null
|
|
1428
|
+
? fromMatches.find((d) =>
|
|
1429
|
+
matchPathname(this.basepath, trimPathRight(d.pathname), {
|
|
1430
|
+
to: dest.from,
|
|
1431
|
+
caseSensitive: false,
|
|
1432
|
+
fuzzy: false,
|
|
1433
|
+
}),
|
|
1434
|
+
)
|
|
1435
|
+
: undefined
|
|
1436
|
+
|
|
1437
|
+
const fromPath = fromMatch?.pathname || this.latestLocation.pathname
|
|
1438
|
+
|
|
1439
|
+
invariant(
|
|
1440
|
+
dest.from == null || fromMatch != null,
|
|
1441
|
+
'Could not find match for from: ' + dest.from,
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
const fromSearch = this.state.pendingMatches?.length
|
|
1445
|
+
? last(this.state.pendingMatches)?.search
|
|
1446
|
+
: last(fromMatches)?.search || this.latestLocation.search
|
|
1447
|
+
|
|
1448
|
+
const stayingMatches = matchedRoutesResult?.matchedRoutes.filter((d) =>
|
|
1449
|
+
fromMatches.find((e) => e.routeId === d.id),
|
|
1450
|
+
)
|
|
1451
|
+
let pathname: string
|
|
1452
|
+
if (dest.to) {
|
|
1453
|
+
const resolvePathTo =
|
|
1454
|
+
fromMatch?.fullPath ||
|
|
1455
|
+
last(fromMatches)?.fullPath ||
|
|
1456
|
+
this.latestLocation.pathname
|
|
1457
|
+
pathname = this.resolvePathWithBase(resolvePathTo, `${dest.to}`)
|
|
1458
|
+
} else {
|
|
1459
|
+
const fromRouteByFromPathRouteId =
|
|
1460
|
+
this.routesById[
|
|
1461
|
+
stayingMatches?.find((route) => {
|
|
1462
|
+
const interpolatedPath = interpolatePath({
|
|
1463
|
+
path: route.fullPath,
|
|
1464
|
+
params: matchedRoutesResult?.routeParams ?? {},
|
|
1465
|
+
decodeCharMap: this.pathParamsDecodeCharMap,
|
|
1466
|
+
}).interpolatedPath
|
|
1467
|
+
const pathname = joinPaths([this.basepath, interpolatedPath])
|
|
1468
|
+
return pathname === fromPath
|
|
1469
|
+
})?.id as keyof this['routesById']
|
|
1470
|
+
]
|
|
1471
|
+
pathname = this.resolvePathWithBase(
|
|
1472
|
+
fromPath,
|
|
1473
|
+
fromRouteByFromPathRouteId?.to ?? fromPath,
|
|
1474
|
+
)
|
|
1047
1475
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1476
|
+
|
|
1477
|
+
const prevParams = { ...last(fromMatches)?.params }
|
|
1478
|
+
|
|
1479
|
+
let nextParams =
|
|
1480
|
+
(dest.params ?? true) === true
|
|
1481
|
+
? prevParams
|
|
1482
|
+
: {
|
|
1483
|
+
...prevParams,
|
|
1484
|
+
...functionalUpdate(dest.params as any, prevParams),
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (Object.keys(nextParams).length > 0) {
|
|
1488
|
+
matchedRoutesResult?.matchedRoutes
|
|
1489
|
+
.map((route) => {
|
|
1490
|
+
return (
|
|
1491
|
+
route.options.params?.stringify ?? route.options.stringifyParams
|
|
1492
|
+
)
|
|
1493
|
+
})
|
|
1494
|
+
.filter(Boolean)
|
|
1495
|
+
.forEach((fn) => {
|
|
1496
|
+
nextParams = { ...nextParams!, ...fn!(nextParams) }
|
|
1497
|
+
})
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
pathname = interpolatePath({
|
|
1501
|
+
path: pathname,
|
|
1502
|
+
params: nextParams ?? {},
|
|
1503
|
+
leaveWildcards: false,
|
|
1504
|
+
leaveParams: opts.leaveParams,
|
|
1505
|
+
decodeCharMap: this.pathParamsDecodeCharMap,
|
|
1506
|
+
}).interpolatedPath
|
|
1507
|
+
|
|
1508
|
+
let search = fromSearch
|
|
1509
|
+
if (opts._includeValidateSearch && this.options.search?.strict) {
|
|
1510
|
+
let validatedSearch = {}
|
|
1511
|
+
matchedRoutesResult?.matchedRoutes.forEach((route) => {
|
|
1512
|
+
try {
|
|
1513
|
+
if (route.options.validateSearch) {
|
|
1514
|
+
validatedSearch = {
|
|
1515
|
+
...validatedSearch,
|
|
1516
|
+
...(validateSearch(route.options.validateSearch, {
|
|
1517
|
+
...validatedSearch,
|
|
1518
|
+
...search,
|
|
1519
|
+
}) ?? {}),
|
|
1089
1520
|
}
|
|
1090
|
-
throw new Error()
|
|
1091
1521
|
}
|
|
1522
|
+
} catch {
|
|
1523
|
+
// ignore errors here because they are already handled in matchRoutes
|
|
1524
|
+
}
|
|
1525
|
+
})
|
|
1526
|
+
search = validatedSearch
|
|
1527
|
+
}
|
|
1092
1528
|
|
|
1093
|
-
|
|
1529
|
+
const applyMiddlewares = (search: any) => {
|
|
1530
|
+
const allMiddlewares =
|
|
1531
|
+
matchedRoutesResult?.matchedRoutes.reduce(
|
|
1532
|
+
(acc, route) => {
|
|
1533
|
+
const middlewares: Array<SearchMiddleware<any>> = []
|
|
1534
|
+
if ('search' in route.options) {
|
|
1535
|
+
if (route.options.search?.middlewares) {
|
|
1536
|
+
middlewares.push(...route.options.search.middlewares)
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
// TODO remove preSearchFilters and postSearchFilters in v2
|
|
1540
|
+
else if (
|
|
1541
|
+
route.options.preSearchFilters ||
|
|
1542
|
+
route.options.postSearchFilters
|
|
1543
|
+
) {
|
|
1544
|
+
const legacyMiddleware: SearchMiddleware<any> = ({
|
|
1545
|
+
search,
|
|
1546
|
+
next,
|
|
1547
|
+
}) => {
|
|
1548
|
+
let nextSearch = search
|
|
1549
|
+
if (
|
|
1550
|
+
'preSearchFilters' in route.options &&
|
|
1551
|
+
route.options.preSearchFilters
|
|
1552
|
+
) {
|
|
1553
|
+
nextSearch = route.options.preSearchFilters.reduce(
|
|
1554
|
+
(prev, next) => next(prev),
|
|
1555
|
+
search,
|
|
1556
|
+
)
|
|
1557
|
+
}
|
|
1558
|
+
const result = next(nextSearch)
|
|
1559
|
+
if (
|
|
1560
|
+
'postSearchFilters' in route.options &&
|
|
1561
|
+
route.options.postSearchFilters
|
|
1562
|
+
) {
|
|
1563
|
+
return route.options.postSearchFilters.reduce(
|
|
1564
|
+
(prev, next) => next(prev),
|
|
1565
|
+
result,
|
|
1566
|
+
)
|
|
1567
|
+
}
|
|
1568
|
+
return result
|
|
1569
|
+
}
|
|
1570
|
+
middlewares.push(legacyMiddleware)
|
|
1571
|
+
}
|
|
1572
|
+
if (opts._includeValidateSearch && route.options.validateSearch) {
|
|
1573
|
+
const validate: SearchMiddleware<any> = ({ search, next }) => {
|
|
1574
|
+
const result = next(search)
|
|
1575
|
+
try {
|
|
1576
|
+
const validatedSearch = {
|
|
1577
|
+
...result,
|
|
1578
|
+
...(validateSearch(
|
|
1579
|
+
route.options.validateSearch,
|
|
1580
|
+
result,
|
|
1581
|
+
) ?? {}),
|
|
1582
|
+
}
|
|
1583
|
+
return validatedSearch
|
|
1584
|
+
} catch {
|
|
1585
|
+
// ignore errors here because they are already handled in matchRoutes
|
|
1586
|
+
return result
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
middlewares.push(validate)
|
|
1590
|
+
}
|
|
1591
|
+
return acc.concat(middlewares)
|
|
1592
|
+
},
|
|
1593
|
+
[] as Array<SearchMiddleware<any>>,
|
|
1594
|
+
) ?? []
|
|
1094
1595
|
|
|
1095
|
-
|
|
1596
|
+
// the chain ends here since `next` is not called
|
|
1597
|
+
const final: SearchMiddleware<any> = ({ search }) => {
|
|
1598
|
+
if (!dest.search) {
|
|
1599
|
+
return {}
|
|
1600
|
+
}
|
|
1601
|
+
if (dest.search === true) {
|
|
1602
|
+
return search
|
|
1603
|
+
}
|
|
1604
|
+
return functionalUpdate(dest.search, search)
|
|
1605
|
+
}
|
|
1606
|
+
allMiddlewares.push(final)
|
|
1096
1607
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1608
|
+
const applyNext = (index: number, currentSearch: any): any => {
|
|
1609
|
+
// no more middlewares left, return the current search
|
|
1610
|
+
if (index >= allMiddlewares.length) {
|
|
1611
|
+
return currentSearch
|
|
1612
|
+
}
|
|
1100
1613
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1614
|
+
const middleware = allMiddlewares[index]!
|
|
1615
|
+
|
|
1616
|
+
const next = (newSearch: any): any => {
|
|
1617
|
+
return applyNext(index + 1, newSearch)
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
return middleware({ search: currentSearch, next })
|
|
1103
1621
|
}
|
|
1104
1622
|
|
|
1105
|
-
|
|
1623
|
+
// Start applying middlewares
|
|
1624
|
+
return applyNext(0, search)
|
|
1625
|
+
}
|
|
1106
1626
|
|
|
1107
|
-
|
|
1108
|
-
},
|
|
1627
|
+
search = applyMiddlewares(search)
|
|
1109
1628
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
previousLocation?: Location,
|
|
1113
|
-
): Location => {
|
|
1114
|
-
const parsedSearch = router.options.parseSearch(location.search)
|
|
1629
|
+
search = replaceEqualDeep(fromSearch, search)
|
|
1630
|
+
const searchStr = this.options.stringifySearch(search)
|
|
1115
1631
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1632
|
+
const hash =
|
|
1633
|
+
dest.hash === true
|
|
1634
|
+
? this.latestLocation.hash
|
|
1635
|
+
: dest.hash
|
|
1636
|
+
? functionalUpdate(dest.hash, this.latestLocation.hash)
|
|
1637
|
+
: undefined
|
|
1638
|
+
|
|
1639
|
+
const hashStr = hash ? `#${hash}` : ''
|
|
1640
|
+
|
|
1641
|
+
let nextState =
|
|
1642
|
+
dest.state === true
|
|
1643
|
+
? this.latestLocation.state
|
|
1644
|
+
: dest.state
|
|
1645
|
+
? functionalUpdate(dest.state, this.latestLocation.state)
|
|
1646
|
+
: {}
|
|
1647
|
+
|
|
1648
|
+
nextState = replaceEqualDeep(this.latestLocation.state, nextState)
|
|
1649
|
+
|
|
1650
|
+
return {
|
|
1651
|
+
pathname,
|
|
1652
|
+
search,
|
|
1653
|
+
searchStr,
|
|
1654
|
+
state: nextState as any,
|
|
1655
|
+
hash: hash ?? '',
|
|
1656
|
+
href: `${pathname}${searchStr}${hashStr}`,
|
|
1657
|
+
unmaskOnReload: dest.unmaskOnReload,
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const buildWithMatches = (
|
|
1662
|
+
dest: BuildNextOptions = {},
|
|
1663
|
+
maskedDest?: BuildNextOptions,
|
|
1664
|
+
) => {
|
|
1665
|
+
const next = build(dest)
|
|
1666
|
+
let maskedNext = maskedDest ? build(maskedDest) : undefined
|
|
1667
|
+
|
|
1668
|
+
if (!maskedNext) {
|
|
1669
|
+
let params = {}
|
|
1670
|
+
|
|
1671
|
+
const foundMask = this.options.routeMasks?.find((d) => {
|
|
1672
|
+
const match = matchPathname(this.basepath, next.pathname, {
|
|
1673
|
+
to: d.from,
|
|
1674
|
+
caseSensitive: false,
|
|
1675
|
+
fuzzy: false,
|
|
1676
|
+
})
|
|
1677
|
+
|
|
1678
|
+
if (match) {
|
|
1679
|
+
params = match
|
|
1680
|
+
return true
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
return false
|
|
1684
|
+
})
|
|
1685
|
+
|
|
1686
|
+
if (foundMask) {
|
|
1687
|
+
const { from: _from, ...maskProps } = foundMask
|
|
1688
|
+
maskedDest = {
|
|
1689
|
+
...pick(opts, ['from']),
|
|
1690
|
+
...maskProps,
|
|
1691
|
+
params,
|
|
1692
|
+
}
|
|
1693
|
+
maskedNext = build(maskedDest)
|
|
1124
1694
|
}
|
|
1125
|
-
}
|
|
1695
|
+
}
|
|
1126
1696
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1697
|
+
const nextMatches = this.getMatchedRoutes(
|
|
1698
|
+
next.pathname,
|
|
1699
|
+
dest.to as string,
|
|
1700
|
+
)
|
|
1701
|
+
const final = build(dest, nextMatches)
|
|
1131
1702
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
? router.location.pathname
|
|
1137
|
-
: dest.from ?? router.location.pathname
|
|
1138
|
-
|
|
1139
|
-
let pathname = resolvePath(
|
|
1140
|
-
router.basepath ?? '/',
|
|
1141
|
-
fromPathname,
|
|
1142
|
-
`${dest.to ?? '.'}`,
|
|
1703
|
+
if (maskedNext) {
|
|
1704
|
+
const maskedMatches = this.getMatchedRoutes(
|
|
1705
|
+
maskedNext.pathname,
|
|
1706
|
+
maskedDest?.to as string,
|
|
1143
1707
|
)
|
|
1708
|
+
const maskedFinal = build(maskedDest, maskedMatches)
|
|
1709
|
+
final.maskedLocation = maskedFinal
|
|
1710
|
+
}
|
|
1144
1711
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1712
|
+
return final
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
if (opts.mask) {
|
|
1716
|
+
return buildWithMatches(opts, {
|
|
1717
|
+
...pick(opts, ['from']),
|
|
1718
|
+
...opts.mask,
|
|
1719
|
+
})
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
return buildWithMatches(opts)
|
|
1723
|
+
}
|
|
1148
1724
|
|
|
1149
|
-
|
|
1725
|
+
commitLocationPromise: undefined | ControlledPromise<void>
|
|
1726
|
+
|
|
1727
|
+
commitLocation: CommitLocationFn = ({
|
|
1728
|
+
viewTransition,
|
|
1729
|
+
ignoreBlocker,
|
|
1730
|
+
...next
|
|
1731
|
+
}) => {
|
|
1732
|
+
const isSameState = () => {
|
|
1733
|
+
// the following props are ignored but may still be provided when navigating,
|
|
1734
|
+
// temporarily add the previous values to the next state so they don't affect
|
|
1735
|
+
// the comparison
|
|
1736
|
+
const ignoredProps = [
|
|
1737
|
+
'key',
|
|
1738
|
+
'__TSR_index',
|
|
1739
|
+
'__hashScrollIntoViewOptions',
|
|
1740
|
+
] as const
|
|
1741
|
+
ignoredProps.forEach((prop) => {
|
|
1742
|
+
;(next.state as any)[prop] = this.latestLocation.state[prop]
|
|
1743
|
+
})
|
|
1744
|
+
const isEqual = deepEqual(next.state, this.latestLocation.state)
|
|
1745
|
+
ignoredProps.forEach((prop) => {
|
|
1746
|
+
delete next.state[prop]
|
|
1747
|
+
})
|
|
1748
|
+
return isEqual
|
|
1749
|
+
}
|
|
1150
1750
|
|
|
1151
|
-
|
|
1751
|
+
const isSameUrl = this.latestLocation.href === next.href
|
|
1752
|
+
|
|
1753
|
+
const previousCommitPromise = this.commitLocationPromise
|
|
1754
|
+
this.commitLocationPromise = createControlledPromise<void>(() => {
|
|
1755
|
+
previousCommitPromise?.resolve()
|
|
1756
|
+
})
|
|
1757
|
+
|
|
1758
|
+
// Don't commit to history if nothing changed
|
|
1759
|
+
if (isSameUrl && isSameState()) {
|
|
1760
|
+
this.load()
|
|
1761
|
+
} else {
|
|
1762
|
+
// eslint-disable-next-line prefer-const
|
|
1763
|
+
let { maskedLocation, hashScrollIntoView, ...nextHistory } = next
|
|
1764
|
+
|
|
1765
|
+
if (maskedLocation) {
|
|
1766
|
+
nextHistory = {
|
|
1767
|
+
...maskedLocation,
|
|
1768
|
+
state: {
|
|
1769
|
+
...maskedLocation.state,
|
|
1770
|
+
__tempKey: undefined,
|
|
1771
|
+
__tempLocation: {
|
|
1772
|
+
...nextHistory,
|
|
1773
|
+
search: nextHistory.searchStr,
|
|
1774
|
+
state: {
|
|
1775
|
+
...nextHistory.state,
|
|
1776
|
+
__tempKey: undefined!,
|
|
1777
|
+
__tempLocation: undefined!,
|
|
1778
|
+
key: undefined!,
|
|
1779
|
+
},
|
|
1780
|
+
},
|
|
1781
|
+
},
|
|
1782
|
+
}
|
|
1152
1783
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1784
|
+
if (
|
|
1785
|
+
nextHistory.unmaskOnReload ??
|
|
1786
|
+
this.options.unmaskOnReload ??
|
|
1787
|
+
false
|
|
1788
|
+
) {
|
|
1789
|
+
nextHistory.state.__tempKey = this.tempLocationKey
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1157
1792
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1793
|
+
nextHistory.state.__hashScrollIntoViewOptions =
|
|
1794
|
+
hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true
|
|
1795
|
+
|
|
1796
|
+
this.shouldViewTransition = viewTransition
|
|
1797
|
+
|
|
1798
|
+
this.history[next.replace ? 'replace' : 'push'](
|
|
1799
|
+
nextHistory.href,
|
|
1800
|
+
nextHistory.state,
|
|
1801
|
+
{ ignoreBlocker },
|
|
1802
|
+
)
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
this.resetNextScroll = next.resetScroll ?? true
|
|
1806
|
+
|
|
1807
|
+
if (!this.history.subscribers.size) {
|
|
1808
|
+
this.load()
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
return this.commitLocationPromise
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
buildAndCommitLocation = ({
|
|
1815
|
+
replace,
|
|
1816
|
+
resetScroll,
|
|
1817
|
+
hashScrollIntoView,
|
|
1818
|
+
viewTransition,
|
|
1819
|
+
ignoreBlocker,
|
|
1820
|
+
href,
|
|
1821
|
+
...rest
|
|
1822
|
+
}: BuildNextOptions & CommitLocationOptions = {}) => {
|
|
1823
|
+
if (href) {
|
|
1824
|
+
const currentIndex = this.history.location.state.__TSR_index
|
|
1825
|
+
const parsed = parseHref(href, {
|
|
1826
|
+
__TSR_index: replace ? currentIndex : currentIndex + 1,
|
|
1827
|
+
})
|
|
1828
|
+
rest.to = parsed.pathname
|
|
1829
|
+
rest.search = this.options.parseSearch(parsed.search)
|
|
1830
|
+
// remove the leading `#` from the hash
|
|
1831
|
+
rest.hash = parsed.hash.slice(1)
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
const location = this.buildLocation({
|
|
1835
|
+
...(rest as any),
|
|
1836
|
+
_includeValidateSearch: true,
|
|
1837
|
+
})
|
|
1838
|
+
return this.commitLocation({
|
|
1839
|
+
...location,
|
|
1840
|
+
viewTransition,
|
|
1841
|
+
replace,
|
|
1842
|
+
resetScroll,
|
|
1843
|
+
hashScrollIntoView,
|
|
1844
|
+
ignoreBlocker,
|
|
1845
|
+
})
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
|
|
1849
|
+
if (!reloadDocument && href) {
|
|
1850
|
+
try {
|
|
1851
|
+
new URL(`${href}`)
|
|
1852
|
+
reloadDocument = true
|
|
1853
|
+
} catch {}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
if (reloadDocument) {
|
|
1857
|
+
if (!href) {
|
|
1858
|
+
const location = this.buildLocation({ to, ...rest } as any)
|
|
1859
|
+
href = this.history.createHref(location.href)
|
|
1860
|
+
}
|
|
1861
|
+
if (rest.replace) {
|
|
1862
|
+
window.location.replace(href)
|
|
1863
|
+
} else {
|
|
1864
|
+
window.location.href = href
|
|
1865
|
+
}
|
|
1866
|
+
return
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
return this.buildAndCommitLocation({
|
|
1870
|
+
...rest,
|
|
1871
|
+
href,
|
|
1872
|
+
to: to as string,
|
|
1873
|
+
})
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
latestLoadPromise: undefined | Promise<void>
|
|
1877
|
+
|
|
1878
|
+
beforeLoad = () => {
|
|
1879
|
+
// Cancel any pending matches
|
|
1880
|
+
this.cancelMatches()
|
|
1881
|
+
this.latestLocation = this.parseLocation(this.latestLocation)
|
|
1882
|
+
|
|
1883
|
+
// Match the routes
|
|
1884
|
+
const pendingMatches = this.matchRoutes(this.latestLocation)
|
|
1885
|
+
|
|
1886
|
+
// Ingest the new matches
|
|
1887
|
+
this.__store.setState((s) => ({
|
|
1888
|
+
...s,
|
|
1889
|
+
status: 'pending',
|
|
1890
|
+
isLoading: true,
|
|
1891
|
+
location: this.latestLocation,
|
|
1892
|
+
pendingMatches,
|
|
1893
|
+
// If a cached moved to pendingMatches, remove it from cachedMatches
|
|
1894
|
+
cachedMatches: s.cachedMatches.filter((d) => {
|
|
1895
|
+
return !pendingMatches.find((e) => e.id === d.id)
|
|
1896
|
+
}),
|
|
1897
|
+
}))
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
|
|
1901
|
+
let redirect: AnyRedirect | undefined
|
|
1902
|
+
let notFound: NotFoundError | undefined
|
|
1903
|
+
|
|
1904
|
+
let loadPromise: Promise<void>
|
|
1905
|
+
|
|
1906
|
+
// eslint-disable-next-line prefer-const
|
|
1907
|
+
loadPromise = new Promise<void>((resolve) => {
|
|
1908
|
+
this.startTransition(async () => {
|
|
1909
|
+
try {
|
|
1910
|
+
this.beforeLoad()
|
|
1911
|
+
const next = this.latestLocation
|
|
1912
|
+
const prevLocation = this.state.resolvedLocation
|
|
1913
|
+
|
|
1914
|
+
if (!this.state.redirect) {
|
|
1915
|
+
this.emit({
|
|
1916
|
+
type: 'onBeforeNavigate',
|
|
1917
|
+
...getLocationChangeInfo({
|
|
1918
|
+
resolvedLocation: prevLocation,
|
|
1919
|
+
location: next,
|
|
1920
|
+
}),
|
|
1164
1921
|
})
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
this.emit({
|
|
1925
|
+
type: 'onBeforeLoad',
|
|
1926
|
+
...getLocationChangeInfo({
|
|
1927
|
+
resolvedLocation: prevLocation,
|
|
1928
|
+
location: next,
|
|
1929
|
+
}),
|
|
1930
|
+
})
|
|
1931
|
+
|
|
1932
|
+
await this.loadMatches({
|
|
1933
|
+
sync: opts?.sync,
|
|
1934
|
+
matches: this.state.pendingMatches as Array<AnyRouteMatch>,
|
|
1935
|
+
location: next,
|
|
1936
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
1937
|
+
onReady: async () => {
|
|
1938
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
1939
|
+
this.startViewTransition(async () => {
|
|
1940
|
+
// this.viewTransitionPromise = createControlledPromise<true>()
|
|
1941
|
+
|
|
1942
|
+
// Commit the pending matches. If a previous match was
|
|
1943
|
+
// removed, place it in the cachedMatches
|
|
1944
|
+
let exitingMatches!: Array<AnyRouteMatch>
|
|
1945
|
+
let enteringMatches!: Array<AnyRouteMatch>
|
|
1946
|
+
let stayingMatches!: Array<AnyRouteMatch>
|
|
1947
|
+
|
|
1948
|
+
batch(() => {
|
|
1949
|
+
this.__store.setState((s) => {
|
|
1950
|
+
const previousMatches = s.matches
|
|
1951
|
+
const newMatches = s.pendingMatches || s.matches
|
|
1952
|
+
|
|
1953
|
+
exitingMatches = previousMatches.filter(
|
|
1954
|
+
(match) => !newMatches.find((d) => d.id === match.id),
|
|
1955
|
+
)
|
|
1956
|
+
enteringMatches = newMatches.filter(
|
|
1957
|
+
(match) =>
|
|
1958
|
+
!previousMatches.find((d) => d.id === match.id),
|
|
1959
|
+
)
|
|
1960
|
+
stayingMatches = previousMatches.filter((match) =>
|
|
1961
|
+
newMatches.find((d) => d.id === match.id),
|
|
1962
|
+
)
|
|
1963
|
+
|
|
1964
|
+
return {
|
|
1965
|
+
...s,
|
|
1966
|
+
isLoading: false,
|
|
1967
|
+
loadedAt: Date.now(),
|
|
1968
|
+
matches: newMatches,
|
|
1969
|
+
pendingMatches: undefined,
|
|
1970
|
+
cachedMatches: [
|
|
1971
|
+
...s.cachedMatches,
|
|
1972
|
+
...exitingMatches.filter((d) => d.status !== 'error'),
|
|
1973
|
+
],
|
|
1974
|
+
}
|
|
1975
|
+
})
|
|
1976
|
+
this.clearExpiredCache()
|
|
1977
|
+
})
|
|
1978
|
+
|
|
1979
|
+
//
|
|
1980
|
+
;(
|
|
1981
|
+
[
|
|
1982
|
+
[exitingMatches, 'onLeave'],
|
|
1983
|
+
[enteringMatches, 'onEnter'],
|
|
1984
|
+
[stayingMatches, 'onStay'],
|
|
1985
|
+
] as const
|
|
1986
|
+
).forEach(([matches, hook]) => {
|
|
1987
|
+
matches.forEach((match) => {
|
|
1988
|
+
this.looseRoutesById[match.routeId]!.options[hook]?.(match)
|
|
1989
|
+
})
|
|
1990
|
+
})
|
|
1991
|
+
})
|
|
1992
|
+
},
|
|
1993
|
+
})
|
|
1994
|
+
} catch (err) {
|
|
1995
|
+
if (isRedirect(err)) {
|
|
1996
|
+
redirect = err
|
|
1997
|
+
if (!this.isServer) {
|
|
1998
|
+
this.navigate({
|
|
1999
|
+
...redirect.options,
|
|
2000
|
+
replace: true,
|
|
2001
|
+
ignoreBlocker: true,
|
|
2002
|
+
})
|
|
2003
|
+
}
|
|
2004
|
+
} else if (isNotFound(err)) {
|
|
2005
|
+
notFound = err
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
this.__store.setState((s) => ({
|
|
2009
|
+
...s,
|
|
2010
|
+
statusCode: redirect
|
|
2011
|
+
? redirect.status
|
|
2012
|
+
: notFound
|
|
2013
|
+
? 404
|
|
2014
|
+
: s.matches.some((d) => d.status === 'error')
|
|
2015
|
+
? 500
|
|
2016
|
+
: 200,
|
|
2017
|
+
redirect,
|
|
2018
|
+
}))
|
|
1165
2019
|
}
|
|
1166
2020
|
|
|
1167
|
-
|
|
2021
|
+
if (this.latestLoadPromise === loadPromise) {
|
|
2022
|
+
this.commitLocationPromise?.resolve()
|
|
2023
|
+
this.latestLoadPromise = undefined
|
|
2024
|
+
this.commitLocationPromise = undefined
|
|
2025
|
+
}
|
|
2026
|
+
resolve()
|
|
2027
|
+
})
|
|
2028
|
+
})
|
|
1168
2029
|
|
|
1169
|
-
|
|
1170
|
-
const preFilteredSearch = dest.__preSearchFilters?.length
|
|
1171
|
-
? dest.__preSearchFilters.reduce(
|
|
1172
|
-
(prev, next) => next(prev),
|
|
1173
|
-
router.location.search,
|
|
1174
|
-
)
|
|
1175
|
-
: router.location.search
|
|
1176
|
-
|
|
1177
|
-
// Then the link/navigate function
|
|
1178
|
-
const destSearch =
|
|
1179
|
-
dest.search === true
|
|
1180
|
-
? preFilteredSearch // Preserve resolvedFrom true
|
|
1181
|
-
: dest.search
|
|
1182
|
-
? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
|
|
1183
|
-
: dest.__preSearchFilters?.length
|
|
1184
|
-
? preFilteredSearch // Preserve resolvedFrom filters
|
|
1185
|
-
: {}
|
|
2030
|
+
this.latestLoadPromise = loadPromise
|
|
1186
2031
|
|
|
1187
|
-
|
|
1188
|
-
const postFilteredSearch = dest.__postSearchFilters?.length
|
|
1189
|
-
? dest.__postSearchFilters.reduce(
|
|
1190
|
-
(prev, next) => next(prev),
|
|
1191
|
-
destSearch,
|
|
1192
|
-
)
|
|
1193
|
-
: destSearch
|
|
2032
|
+
await loadPromise
|
|
1194
2033
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
2034
|
+
while (
|
|
2035
|
+
(this.latestLoadPromise as any) &&
|
|
2036
|
+
loadPromise !== this.latestLoadPromise
|
|
2037
|
+
) {
|
|
2038
|
+
await this.latestLoadPromise
|
|
2039
|
+
}
|
|
1199
2040
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
2041
|
+
if (this.hasNotFoundMatch()) {
|
|
2042
|
+
this.__store.setState((s) => ({
|
|
2043
|
+
...s,
|
|
2044
|
+
statusCode: 404,
|
|
2045
|
+
}))
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
1206
2048
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
2049
|
+
startViewTransition = (fn: () => Promise<void>) => {
|
|
2050
|
+
// Determine if we should start a view transition from the navigation
|
|
2051
|
+
// or from the router default
|
|
2052
|
+
const shouldViewTransition =
|
|
2053
|
+
this.shouldViewTransition ?? this.options.defaultViewTransition
|
|
2054
|
+
|
|
2055
|
+
// Reset the view transition flag
|
|
2056
|
+
delete this.shouldViewTransition
|
|
2057
|
+
// Attempt to start a view transition (or just apply the changes if we can't)
|
|
2058
|
+
if (
|
|
2059
|
+
shouldViewTransition &&
|
|
2060
|
+
typeof document !== 'undefined' &&
|
|
2061
|
+
'startViewTransition' in document &&
|
|
2062
|
+
typeof document.startViewTransition === 'function'
|
|
2063
|
+
) {
|
|
2064
|
+
// lib.dom.ts doesn't support viewTransition types variant yet.
|
|
2065
|
+
// TODO: Fix this when dom types are updated
|
|
2066
|
+
let startViewTransitionParams: any
|
|
2067
|
+
|
|
2068
|
+
if (
|
|
2069
|
+
typeof shouldViewTransition === 'object' &&
|
|
2070
|
+
this.isViewTransitionTypesSupported
|
|
2071
|
+
) {
|
|
2072
|
+
const next = this.latestLocation
|
|
2073
|
+
const prevLocation = this.state.resolvedLocation
|
|
2074
|
+
|
|
2075
|
+
const resolvedViewTransitionTypes =
|
|
2076
|
+
typeof shouldViewTransition.types === 'function'
|
|
2077
|
+
? shouldViewTransition.types(
|
|
2078
|
+
getLocationChangeInfo({
|
|
2079
|
+
resolvedLocation: prevLocation,
|
|
2080
|
+
location: next,
|
|
2081
|
+
}),
|
|
2082
|
+
)
|
|
2083
|
+
: shouldViewTransition.types
|
|
2084
|
+
|
|
2085
|
+
startViewTransitionParams = {
|
|
2086
|
+
update: fn,
|
|
2087
|
+
types: resolvedViewTransitionTypes,
|
|
1215
2088
|
}
|
|
1216
|
-
}
|
|
2089
|
+
} else {
|
|
2090
|
+
startViewTransitionParams = fn
|
|
2091
|
+
}
|
|
1217
2092
|
|
|
1218
|
-
|
|
1219
|
-
|
|
2093
|
+
document.startViewTransition(startViewTransitionParams)
|
|
2094
|
+
} else {
|
|
2095
|
+
fn()
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
1220
2098
|
|
|
1221
|
-
|
|
2099
|
+
updateMatch: UpdateMatchFn = (id, updater) => {
|
|
2100
|
+
let updated!: AnyRouteMatch
|
|
2101
|
+
const isPending = this.state.pendingMatches?.find((d) => d.id === id)
|
|
2102
|
+
const isMatched = this.state.matches.find((d) => d.id === id)
|
|
2103
|
+
const isCached = this.state.cachedMatches.find((d) => d.id === id)
|
|
2104
|
+
|
|
2105
|
+
const matchesKey = isPending
|
|
2106
|
+
? 'pendingMatches'
|
|
2107
|
+
: isMatched
|
|
2108
|
+
? 'matches'
|
|
2109
|
+
: isCached
|
|
2110
|
+
? 'cachedMatches'
|
|
2111
|
+
: ''
|
|
2112
|
+
|
|
2113
|
+
if (matchesKey) {
|
|
2114
|
+
this.__store.setState((s) => ({
|
|
2115
|
+
...s,
|
|
2116
|
+
[matchesKey]: s[matchesKey]?.map((d) =>
|
|
2117
|
+
d.id === id ? (updated = updater(d)) : d,
|
|
2118
|
+
),
|
|
2119
|
+
}))
|
|
2120
|
+
}
|
|
1222
2121
|
|
|
1223
|
-
|
|
2122
|
+
return updated
|
|
2123
|
+
}
|
|
1224
2124
|
|
|
1225
|
-
|
|
1226
|
-
|
|
2125
|
+
getMatch: GetMatchFn = (matchId: string) => {
|
|
2126
|
+
return [
|
|
2127
|
+
...this.state.cachedMatches,
|
|
2128
|
+
...(this.state.pendingMatches ?? []),
|
|
2129
|
+
...this.state.matches,
|
|
2130
|
+
].find((d) => d.id === matchId)
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
loadMatches = async ({
|
|
2134
|
+
location,
|
|
2135
|
+
matches,
|
|
2136
|
+
preload: allPreload,
|
|
2137
|
+
onReady,
|
|
2138
|
+
updateMatch = this.updateMatch,
|
|
2139
|
+
sync,
|
|
2140
|
+
}: {
|
|
2141
|
+
location: ParsedLocation
|
|
2142
|
+
matches: Array<AnyRouteMatch>
|
|
2143
|
+
preload?: boolean
|
|
2144
|
+
onReady?: () => Promise<void>
|
|
2145
|
+
updateMatch?: (
|
|
2146
|
+
id: string,
|
|
2147
|
+
updater: (match: AnyRouteMatch) => AnyRouteMatch,
|
|
2148
|
+
) => void
|
|
2149
|
+
getMatch?: (matchId: string) => AnyRouteMatch | undefined
|
|
2150
|
+
sync?: boolean
|
|
2151
|
+
}): Promise<Array<MakeRouteMatch>> => {
|
|
2152
|
+
let firstBadMatchIndex: number | undefined
|
|
2153
|
+
let rendered = false
|
|
2154
|
+
|
|
2155
|
+
const triggerOnReady = async () => {
|
|
2156
|
+
if (!rendered) {
|
|
2157
|
+
rendered = true
|
|
2158
|
+
await onReady?.()
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
const resolvePreload = (matchId: string) => {
|
|
2163
|
+
return !!(allPreload && !this.state.matches.find((d) => d.id === matchId))
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
|
|
2167
|
+
if (isRedirect(err) || isNotFound(err)) {
|
|
2168
|
+
if (isRedirect(err)) {
|
|
2169
|
+
if (err.redirectHandled) {
|
|
2170
|
+
if (!err.options.reloadDocument) {
|
|
2171
|
+
throw err
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
1227
2174
|
}
|
|
1228
2175
|
|
|
1229
|
-
|
|
1230
|
-
|
|
2176
|
+
updateMatch(match.id, (prev) => ({
|
|
2177
|
+
...prev,
|
|
2178
|
+
status: isRedirect(err)
|
|
2179
|
+
? 'redirected'
|
|
2180
|
+
: isNotFound(err)
|
|
2181
|
+
? 'notFound'
|
|
2182
|
+
: 'error',
|
|
2183
|
+
isFetching: false,
|
|
2184
|
+
error: err,
|
|
2185
|
+
beforeLoadPromise: undefined,
|
|
2186
|
+
loaderPromise: undefined,
|
|
2187
|
+
}))
|
|
2188
|
+
|
|
2189
|
+
if (!(err as any).routeId) {
|
|
2190
|
+
;(err as any).routeId = match.routeId
|
|
2191
|
+
}
|
|
1231
2192
|
|
|
1232
|
-
|
|
1233
|
-
|
|
2193
|
+
match.beforeLoadPromise?.resolve()
|
|
2194
|
+
match.loaderPromise?.resolve()
|
|
2195
|
+
match.loadPromise?.resolve()
|
|
2196
|
+
|
|
2197
|
+
if (isRedirect(err)) {
|
|
2198
|
+
rendered = true
|
|
2199
|
+
err.options._fromLocation = location
|
|
2200
|
+
err.redirectHandled = true
|
|
2201
|
+
err = this.resolveRedirect(err)
|
|
2202
|
+
throw err
|
|
2203
|
+
} else if (isNotFound(err)) {
|
|
2204
|
+
this._handleNotFound(matches, err, {
|
|
2205
|
+
updateMatch,
|
|
2206
|
+
})
|
|
2207
|
+
this.serverSsr?.onMatchSettled({
|
|
2208
|
+
router: this,
|
|
2209
|
+
match: this.getMatch(match.id)!,
|
|
2210
|
+
})
|
|
2211
|
+
throw err
|
|
1234
2212
|
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
1235
2215
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
2216
|
+
try {
|
|
2217
|
+
await new Promise<void>((resolveAll, rejectAll) => {
|
|
2218
|
+
;(async () => {
|
|
2219
|
+
try {
|
|
2220
|
+
const handleSerialError = (
|
|
2221
|
+
index: number,
|
|
2222
|
+
err: any,
|
|
2223
|
+
routerCode: string,
|
|
2224
|
+
) => {
|
|
2225
|
+
const { id: matchId, routeId } = matches[index]!
|
|
2226
|
+
const route = this.looseRoutesById[routeId]!
|
|
2227
|
+
|
|
2228
|
+
// Much like suspense, we use a promise here to know if
|
|
2229
|
+
// we've been outdated by a new loadMatches call and
|
|
2230
|
+
// should abort the current async operation
|
|
2231
|
+
if (err instanceof Promise) {
|
|
2232
|
+
throw err
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
err.routerCode = routerCode
|
|
2236
|
+
firstBadMatchIndex = firstBadMatchIndex ?? index
|
|
2237
|
+
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
|
|
2238
|
+
|
|
2239
|
+
try {
|
|
2240
|
+
route.options.onError?.(err)
|
|
2241
|
+
} catch (errorHandlerErr) {
|
|
2242
|
+
err = errorHandlerErr
|
|
2243
|
+
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
updateMatch(matchId, (prev) => {
|
|
2247
|
+
prev.beforeLoadPromise?.resolve()
|
|
2248
|
+
prev.loadPromise?.resolve()
|
|
2249
|
+
|
|
2250
|
+
return {
|
|
2251
|
+
...prev,
|
|
2252
|
+
error: err,
|
|
2253
|
+
status: 'error',
|
|
2254
|
+
isFetching: false,
|
|
2255
|
+
updatedAt: Date.now(),
|
|
2256
|
+
abortController: new AbortController(),
|
|
2257
|
+
beforeLoadPromise: undefined,
|
|
2258
|
+
}
|
|
2259
|
+
})
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
for (const [index, { id: matchId, routeId }] of matches.entries()) {
|
|
2263
|
+
const existingMatch = this.getMatch(matchId)!
|
|
2264
|
+
const parentMatchId = matches[index - 1]?.id
|
|
2265
|
+
|
|
2266
|
+
const route = this.looseRoutesById[routeId]!
|
|
2267
|
+
|
|
2268
|
+
const pendingMs =
|
|
2269
|
+
route.options.pendingMs ?? this.options.defaultPendingMs
|
|
2270
|
+
|
|
2271
|
+
const shouldPending = !!(
|
|
2272
|
+
onReady &&
|
|
2273
|
+
!this.isServer &&
|
|
2274
|
+
!resolvePreload(matchId) &&
|
|
2275
|
+
(route.options.loader ||
|
|
2276
|
+
route.options.beforeLoad ||
|
|
2277
|
+
routeNeedsPreload(route)) &&
|
|
2278
|
+
typeof pendingMs === 'number' &&
|
|
2279
|
+
pendingMs !== Infinity &&
|
|
2280
|
+
(route.options.pendingComponent ??
|
|
2281
|
+
(this.options as any)?.defaultPendingComponent)
|
|
2282
|
+
)
|
|
2283
|
+
|
|
2284
|
+
let executeBeforeLoad = true
|
|
2285
|
+
if (
|
|
2286
|
+
// If we are in the middle of a load, either of these will be present
|
|
2287
|
+
// (not to be confused with `loadPromise`, which is always defined)
|
|
2288
|
+
existingMatch.beforeLoadPromise ||
|
|
2289
|
+
existingMatch.loaderPromise
|
|
2290
|
+
) {
|
|
2291
|
+
if (shouldPending) {
|
|
2292
|
+
setTimeout(() => {
|
|
2293
|
+
try {
|
|
2294
|
+
// Update the match and prematurely resolve the loadMatches promise so that
|
|
2295
|
+
// the pending component can start rendering
|
|
2296
|
+
triggerOnReady()
|
|
2297
|
+
} catch {}
|
|
2298
|
+
}, pendingMs)
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
// Wait for the beforeLoad to resolve before we continue
|
|
2302
|
+
await existingMatch.beforeLoadPromise
|
|
2303
|
+
executeBeforeLoad = this.getMatch(matchId)!.status !== 'success'
|
|
2304
|
+
}
|
|
2305
|
+
if (executeBeforeLoad) {
|
|
2306
|
+
// If we are not in the middle of a load OR the previous load failed, start it
|
|
2307
|
+
try {
|
|
2308
|
+
updateMatch(matchId, (prev) => {
|
|
2309
|
+
// explicitly capture the previous loadPromise
|
|
2310
|
+
const prevLoadPromise = prev.loadPromise
|
|
2311
|
+
return {
|
|
2312
|
+
...prev,
|
|
2313
|
+
loadPromise: createControlledPromise<void>(() => {
|
|
2314
|
+
prevLoadPromise?.resolve()
|
|
2315
|
+
}),
|
|
2316
|
+
beforeLoadPromise: createControlledPromise<void>(),
|
|
2317
|
+
}
|
|
2318
|
+
})
|
|
2319
|
+
const abortController = new AbortController()
|
|
2320
|
+
|
|
2321
|
+
let pendingTimeout: ReturnType<typeof setTimeout>
|
|
2322
|
+
|
|
2323
|
+
if (shouldPending) {
|
|
2324
|
+
// If we might show a pending component, we need to wait for the
|
|
2325
|
+
// pending promise to resolve before we start showing that state
|
|
2326
|
+
pendingTimeout = setTimeout(() => {
|
|
2327
|
+
try {
|
|
2328
|
+
// Update the match and prematurely resolve the loadMatches promise so that
|
|
2329
|
+
// the pending component can start rendering
|
|
2330
|
+
triggerOnReady()
|
|
2331
|
+
} catch {}
|
|
2332
|
+
}, pendingMs)
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
const { paramsError, searchError } = this.getMatch(matchId)!
|
|
2336
|
+
|
|
2337
|
+
if (paramsError) {
|
|
2338
|
+
handleSerialError(index, paramsError, 'PARSE_PARAMS')
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
if (searchError) {
|
|
2342
|
+
handleSerialError(index, searchError, 'VALIDATE_SEARCH')
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
const getParentMatchContext = () =>
|
|
2346
|
+
parentMatchId
|
|
2347
|
+
? this.getMatch(parentMatchId)!.context
|
|
2348
|
+
: (this.options.context ?? {})
|
|
2349
|
+
|
|
2350
|
+
updateMatch(matchId, (prev) => ({
|
|
2351
|
+
...prev,
|
|
2352
|
+
isFetching: 'beforeLoad',
|
|
2353
|
+
fetchCount: prev.fetchCount + 1,
|
|
2354
|
+
abortController,
|
|
2355
|
+
pendingTimeout,
|
|
2356
|
+
context: {
|
|
2357
|
+
...getParentMatchContext(),
|
|
2358
|
+
...prev.__routeContext,
|
|
2359
|
+
},
|
|
2360
|
+
}))
|
|
2361
|
+
|
|
2362
|
+
const { search, params, context, cause } =
|
|
2363
|
+
this.getMatch(matchId)!
|
|
2364
|
+
|
|
2365
|
+
const preload = resolvePreload(matchId)
|
|
2366
|
+
|
|
2367
|
+
const beforeLoadFnContext: BeforeLoadContextOptions<
|
|
2368
|
+
any,
|
|
2369
|
+
any,
|
|
2370
|
+
any,
|
|
2371
|
+
any,
|
|
2372
|
+
any
|
|
2373
|
+
> = {
|
|
2374
|
+
search,
|
|
2375
|
+
abortController,
|
|
2376
|
+
params,
|
|
2377
|
+
preload,
|
|
2378
|
+
context,
|
|
2379
|
+
location,
|
|
2380
|
+
navigate: (opts: any) =>
|
|
2381
|
+
this.navigate({ ...opts, _fromLocation: location }),
|
|
2382
|
+
buildLocation: this.buildLocation,
|
|
2383
|
+
cause: preload ? 'preload' : cause,
|
|
2384
|
+
matches,
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
const beforeLoadContext =
|
|
2388
|
+
(await route.options.beforeLoad?.(beforeLoadFnContext)) ??
|
|
2389
|
+
{}
|
|
2390
|
+
|
|
2391
|
+
if (
|
|
2392
|
+
isRedirect(beforeLoadContext) ||
|
|
2393
|
+
isNotFound(beforeLoadContext)
|
|
2394
|
+
) {
|
|
2395
|
+
handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
updateMatch(matchId, (prev) => {
|
|
2399
|
+
return {
|
|
2400
|
+
...prev,
|
|
2401
|
+
__beforeLoadContext: beforeLoadContext,
|
|
2402
|
+
context: {
|
|
2403
|
+
...getParentMatchContext(),
|
|
2404
|
+
...prev.__routeContext,
|
|
2405
|
+
...beforeLoadContext,
|
|
2406
|
+
},
|
|
2407
|
+
abortController,
|
|
2408
|
+
}
|
|
2409
|
+
})
|
|
2410
|
+
} catch (err) {
|
|
2411
|
+
handleSerialError(index, err, 'BEFORE_LOAD')
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
updateMatch(matchId, (prev) => {
|
|
2415
|
+
prev.beforeLoadPromise?.resolve()
|
|
2416
|
+
|
|
2417
|
+
return {
|
|
2418
|
+
...prev,
|
|
2419
|
+
beforeLoadPromise: undefined,
|
|
2420
|
+
isFetching: false,
|
|
2421
|
+
}
|
|
2422
|
+
})
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
|
|
2427
|
+
const matchPromises: Array<Promise<AnyRouteMatch>> = []
|
|
2428
|
+
|
|
2429
|
+
validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
|
|
2430
|
+
matchPromises.push(
|
|
2431
|
+
(async () => {
|
|
2432
|
+
const { loaderPromise: prevLoaderPromise } =
|
|
2433
|
+
this.getMatch(matchId)!
|
|
2434
|
+
|
|
2435
|
+
let loaderShouldRunAsync = false
|
|
2436
|
+
let loaderIsRunningAsync = false
|
|
2437
|
+
|
|
2438
|
+
if (prevLoaderPromise) {
|
|
2439
|
+
await prevLoaderPromise
|
|
2440
|
+
const match = this.getMatch(matchId)!
|
|
2441
|
+
if (match.error) {
|
|
2442
|
+
handleRedirectAndNotFound(match, match.error)
|
|
2443
|
+
}
|
|
2444
|
+
} else {
|
|
2445
|
+
const parentMatchPromise = matchPromises[index - 1] as any
|
|
2446
|
+
const route = this.looseRoutesById[routeId]!
|
|
2447
|
+
|
|
2448
|
+
const getLoaderContext = (): LoaderFnContext => {
|
|
2449
|
+
const {
|
|
2450
|
+
params,
|
|
2451
|
+
loaderDeps,
|
|
2452
|
+
abortController,
|
|
2453
|
+
context,
|
|
2454
|
+
cause,
|
|
2455
|
+
} = this.getMatch(matchId)!
|
|
2456
|
+
|
|
2457
|
+
const preload = resolvePreload(matchId)
|
|
2458
|
+
|
|
2459
|
+
return {
|
|
2460
|
+
params,
|
|
2461
|
+
deps: loaderDeps,
|
|
2462
|
+
preload: !!preload,
|
|
2463
|
+
parentMatchPromise,
|
|
2464
|
+
abortController: abortController,
|
|
2465
|
+
context,
|
|
2466
|
+
location,
|
|
2467
|
+
navigate: (opts) =>
|
|
2468
|
+
this.navigate({ ...opts, _fromLocation: location }),
|
|
2469
|
+
cause: preload ? 'preload' : cause,
|
|
2470
|
+
route,
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// This is where all of the stale-while-revalidate magic happens
|
|
2475
|
+
const age = Date.now() - this.getMatch(matchId)!.updatedAt
|
|
2476
|
+
|
|
2477
|
+
const preload = resolvePreload(matchId)
|
|
2478
|
+
|
|
2479
|
+
const staleAge = preload
|
|
2480
|
+
? (route.options.preloadStaleTime ??
|
|
2481
|
+
this.options.defaultPreloadStaleTime ??
|
|
2482
|
+
30_000) // 30 seconds for preloads by default
|
|
2483
|
+
: (route.options.staleTime ??
|
|
2484
|
+
this.options.defaultStaleTime ??
|
|
2485
|
+
0)
|
|
2486
|
+
|
|
2487
|
+
const shouldReloadOption = route.options.shouldReload
|
|
2488
|
+
|
|
2489
|
+
// Default to reloading the route all the time
|
|
2490
|
+
// Allow shouldReload to get the last say,
|
|
2491
|
+
// if provided.
|
|
2492
|
+
const shouldReload =
|
|
2493
|
+
typeof shouldReloadOption === 'function'
|
|
2494
|
+
? shouldReloadOption(getLoaderContext())
|
|
2495
|
+
: shouldReloadOption
|
|
2496
|
+
|
|
2497
|
+
updateMatch(matchId, (prev) => ({
|
|
2498
|
+
...prev,
|
|
2499
|
+
loaderPromise: createControlledPromise<void>(),
|
|
2500
|
+
preload:
|
|
2501
|
+
!!preload &&
|
|
2502
|
+
!this.state.matches.find((d) => d.id === matchId),
|
|
2503
|
+
}))
|
|
2504
|
+
|
|
2505
|
+
const runLoader = async () => {
|
|
2506
|
+
try {
|
|
2507
|
+
// If the Matches component rendered
|
|
2508
|
+
// the pending component and needs to show it for
|
|
2509
|
+
// a minimum duration, we''ll wait for it to resolve
|
|
2510
|
+
// before committing to the match and resolving
|
|
2511
|
+
// the loadPromise
|
|
2512
|
+
const potentialPendingMinPromise = async () => {
|
|
2513
|
+
const latestMatch = this.getMatch(matchId)!
|
|
2514
|
+
|
|
2515
|
+
if (latestMatch.minPendingPromise) {
|
|
2516
|
+
await latestMatch.minPendingPromise
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// Actually run the loader and handle the result
|
|
2521
|
+
try {
|
|
2522
|
+
this.loadRouteChunk(route)
|
|
2523
|
+
|
|
2524
|
+
updateMatch(matchId, (prev) => ({
|
|
2525
|
+
...prev,
|
|
2526
|
+
isFetching: 'loader',
|
|
2527
|
+
}))
|
|
2528
|
+
|
|
2529
|
+
// Kick off the loader!
|
|
2530
|
+
const loaderData =
|
|
2531
|
+
await route.options.loader?.(getLoaderContext())
|
|
2532
|
+
|
|
2533
|
+
handleRedirectAndNotFound(
|
|
2534
|
+
this.getMatch(matchId)!,
|
|
2535
|
+
loaderData,
|
|
2536
|
+
)
|
|
2537
|
+
|
|
2538
|
+
// Lazy option can modify the route options,
|
|
2539
|
+
// so we need to wait for it to resolve before
|
|
2540
|
+
// we can use the options
|
|
2541
|
+
await route._lazyPromise
|
|
2542
|
+
|
|
2543
|
+
await potentialPendingMinPromise()
|
|
2544
|
+
|
|
2545
|
+
const assetContext = {
|
|
2546
|
+
matches,
|
|
2547
|
+
match: this.getMatch(matchId)!,
|
|
2548
|
+
params: this.getMatch(matchId)!.params,
|
|
2549
|
+
loaderData,
|
|
2550
|
+
}
|
|
2551
|
+
const headFnContent =
|
|
2552
|
+
route.options.head?.(assetContext)
|
|
2553
|
+
const meta = headFnContent?.meta
|
|
2554
|
+
const links = headFnContent?.links
|
|
2555
|
+
const headScripts = headFnContent?.scripts
|
|
2556
|
+
|
|
2557
|
+
const scripts = route.options.scripts?.(assetContext)
|
|
2558
|
+
const headers = route.options.headers?.({
|
|
2559
|
+
loaderData,
|
|
2560
|
+
})
|
|
2561
|
+
|
|
2562
|
+
// Last but not least, wait for the the components
|
|
2563
|
+
// to be preloaded before we resolve the match
|
|
2564
|
+
await route._componentsPromise
|
|
2565
|
+
|
|
2566
|
+
updateMatch(matchId, (prev) => ({
|
|
2567
|
+
...prev,
|
|
2568
|
+
error: undefined,
|
|
2569
|
+
status: 'success',
|
|
2570
|
+
isFetching: false,
|
|
2571
|
+
updatedAt: Date.now(),
|
|
2572
|
+
loaderData,
|
|
2573
|
+
meta,
|
|
2574
|
+
links,
|
|
2575
|
+
headScripts,
|
|
2576
|
+
headers,
|
|
2577
|
+
scripts,
|
|
2578
|
+
}))
|
|
2579
|
+
} catch (e) {
|
|
2580
|
+
let error = e
|
|
2581
|
+
|
|
2582
|
+
await potentialPendingMinPromise()
|
|
2583
|
+
|
|
2584
|
+
handleRedirectAndNotFound(this.getMatch(matchId)!, e)
|
|
2585
|
+
|
|
2586
|
+
try {
|
|
2587
|
+
route.options.onError?.(e)
|
|
2588
|
+
} catch (onErrorError) {
|
|
2589
|
+
error = onErrorError
|
|
2590
|
+
handleRedirectAndNotFound(
|
|
2591
|
+
this.getMatch(matchId)!,
|
|
2592
|
+
onErrorError,
|
|
2593
|
+
)
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
updateMatch(matchId, (prev) => ({
|
|
2597
|
+
...prev,
|
|
2598
|
+
error,
|
|
2599
|
+
status: 'error',
|
|
2600
|
+
isFetching: false,
|
|
2601
|
+
}))
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
this.serverSsr?.onMatchSettled({
|
|
2605
|
+
router: this,
|
|
2606
|
+
match: this.getMatch(matchId)!,
|
|
2607
|
+
})
|
|
2608
|
+
} catch (err) {
|
|
2609
|
+
updateMatch(matchId, (prev) => ({
|
|
2610
|
+
...prev,
|
|
2611
|
+
loaderPromise: undefined,
|
|
2612
|
+
}))
|
|
2613
|
+
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// If the route is successful and still fresh, just resolve
|
|
2618
|
+
const { status, invalid } = this.getMatch(matchId)!
|
|
2619
|
+
loaderShouldRunAsync =
|
|
2620
|
+
status === 'success' &&
|
|
2621
|
+
(invalid || (shouldReload ?? age > staleAge))
|
|
2622
|
+
if (preload && route.options.preload === false) {
|
|
2623
|
+
// Do nothing
|
|
2624
|
+
} else if (loaderShouldRunAsync && !sync) {
|
|
2625
|
+
loaderIsRunningAsync = true
|
|
2626
|
+
;(async () => {
|
|
2627
|
+
try {
|
|
2628
|
+
await runLoader()
|
|
2629
|
+
const { loaderPromise, loadPromise } =
|
|
2630
|
+
this.getMatch(matchId)!
|
|
2631
|
+
loaderPromise?.resolve()
|
|
2632
|
+
loadPromise?.resolve()
|
|
2633
|
+
updateMatch(matchId, (prev) => ({
|
|
2634
|
+
...prev,
|
|
2635
|
+
loaderPromise: undefined,
|
|
2636
|
+
}))
|
|
2637
|
+
} catch (err) {
|
|
2638
|
+
if (isRedirect(err)) {
|
|
2639
|
+
await this.navigate(err.options)
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
})()
|
|
2643
|
+
} else if (
|
|
2644
|
+
status !== 'success' ||
|
|
2645
|
+
(loaderShouldRunAsync && sync)
|
|
2646
|
+
) {
|
|
2647
|
+
await runLoader()
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (!loaderIsRunningAsync) {
|
|
2651
|
+
const { loaderPromise, loadPromise } =
|
|
2652
|
+
this.getMatch(matchId)!
|
|
2653
|
+
loaderPromise?.resolve()
|
|
2654
|
+
loadPromise?.resolve()
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
updateMatch(matchId, (prev) => ({
|
|
2658
|
+
...prev,
|
|
2659
|
+
isFetching: loaderIsRunningAsync ? prev.isFetching : false,
|
|
2660
|
+
loaderPromise: loaderIsRunningAsync
|
|
2661
|
+
? prev.loaderPromise
|
|
2662
|
+
: undefined,
|
|
2663
|
+
invalid: false,
|
|
2664
|
+
}))
|
|
2665
|
+
return this.getMatch(matchId)!
|
|
2666
|
+
})(),
|
|
2667
|
+
)
|
|
2668
|
+
})
|
|
2669
|
+
|
|
2670
|
+
await Promise.all(matchPromises)
|
|
2671
|
+
|
|
2672
|
+
resolveAll()
|
|
2673
|
+
} catch (err) {
|
|
2674
|
+
rejectAll(err)
|
|
2675
|
+
}
|
|
2676
|
+
})()
|
|
2677
|
+
})
|
|
2678
|
+
await triggerOnReady()
|
|
2679
|
+
} catch (err) {
|
|
2680
|
+
if (isRedirect(err) || isNotFound(err)) {
|
|
2681
|
+
if (isNotFound(err) && !allPreload) {
|
|
2682
|
+
await triggerOnReady()
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
throw err
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
return matches
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
invalidate: InvalidateFn<
|
|
2693
|
+
RouterCore<
|
|
2694
|
+
TRouteTree,
|
|
2695
|
+
TTrailingSlashOption,
|
|
2696
|
+
TDefaultStructuralSharingOption,
|
|
2697
|
+
TRouterHistory,
|
|
2698
|
+
TDehydrated
|
|
2699
|
+
>
|
|
2700
|
+
> = (opts) => {
|
|
2701
|
+
const invalidate = (d: MakeRouteMatch<TRouteTree>) => {
|
|
2702
|
+
if (opts?.filter?.(d as MakeRouteMatchUnion<this>) ?? true) {
|
|
2703
|
+
return {
|
|
2704
|
+
...d,
|
|
2705
|
+
invalid: true,
|
|
2706
|
+
...(d.status === 'error'
|
|
2707
|
+
? ({ status: 'pending', error: undefined } as const)
|
|
2708
|
+
: {}),
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
return d
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
this.__store.setState((s) => ({
|
|
2715
|
+
...s,
|
|
2716
|
+
matches: s.matches.map(invalidate),
|
|
2717
|
+
cachedMatches: s.cachedMatches.map(invalidate),
|
|
2718
|
+
pendingMatches: s.pendingMatches?.map(invalidate),
|
|
2719
|
+
}))
|
|
2720
|
+
|
|
2721
|
+
return this.load({ sync: opts?.sync })
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
|
|
2725
|
+
if (!redirect.options.href) {
|
|
2726
|
+
redirect.options.href = this.buildLocation(redirect.options).href
|
|
2727
|
+
redirect.headers.set('Location', redirect.options.href)
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
if (!redirect.headers.get('Location')) {
|
|
2731
|
+
redirect.headers.set('Location', redirect.options.href)
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
return redirect
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
clearCache: ClearCacheFn<this> = (opts) => {
|
|
2738
|
+
const filter = opts?.filter
|
|
2739
|
+
if (filter !== undefined) {
|
|
2740
|
+
this.__store.setState((s) => {
|
|
2741
|
+
return {
|
|
2742
|
+
...s,
|
|
2743
|
+
cachedMatches: s.cachedMatches.filter(
|
|
2744
|
+
(m) => !filter(m as MakeRouteMatchUnion<this>),
|
|
2745
|
+
),
|
|
2746
|
+
}
|
|
2747
|
+
})
|
|
2748
|
+
} else {
|
|
2749
|
+
this.__store.setState((s) => {
|
|
2750
|
+
return {
|
|
2751
|
+
...s,
|
|
2752
|
+
cachedMatches: [],
|
|
1258
2753
|
}
|
|
2754
|
+
})
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
clearExpiredCache = () => {
|
|
2759
|
+
// This is where all of the garbage collection magic happens
|
|
2760
|
+
const filter = (d: MakeRouteMatch<TRouteTree>) => {
|
|
2761
|
+
const route = this.looseRoutesById[d.routeId]!
|
|
2762
|
+
|
|
2763
|
+
if (!route.options.loader) {
|
|
2764
|
+
return true
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// If the route was preloaded, use the preloadGcTime
|
|
2768
|
+
// otherwise, use the gcTime
|
|
2769
|
+
const gcTime =
|
|
2770
|
+
(d.preload
|
|
2771
|
+
? (route.options.preloadGcTime ?? this.options.defaultPreloadGcTime)
|
|
2772
|
+
: (route.options.gcTime ?? this.options.defaultGcTime)) ??
|
|
2773
|
+
5 * 60 * 1000
|
|
2774
|
+
|
|
2775
|
+
return !(d.status !== 'error' && Date.now() - d.updatedAt < gcTime)
|
|
2776
|
+
}
|
|
2777
|
+
this.clearCache({ filter })
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
loadRouteChunk = (route: AnyRoute) => {
|
|
2781
|
+
if (route._lazyPromise === undefined) {
|
|
2782
|
+
if (route.lazyFn) {
|
|
2783
|
+
route._lazyPromise = route.lazyFn().then((lazyRoute) => {
|
|
2784
|
+
// explicitly don't copy over the lazy route's id
|
|
2785
|
+
const { id: _id, ...options } = lazyRoute.options
|
|
2786
|
+
Object.assign(route.options, options)
|
|
2787
|
+
})
|
|
2788
|
+
} else {
|
|
2789
|
+
route._lazyPromise = Promise.resolve()
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
// If for some reason lazy resolves more lazy components...
|
|
2794
|
+
// We'll wait for that before pre attempt to preload any
|
|
2795
|
+
// components themselves.
|
|
2796
|
+
if (route._componentsPromise === undefined) {
|
|
2797
|
+
route._componentsPromise = route._lazyPromise.then(() =>
|
|
2798
|
+
Promise.all(
|
|
2799
|
+
componentTypes.map(async (type) => {
|
|
2800
|
+
const component = route.options[type]
|
|
2801
|
+
if ((component as any)?.preload) {
|
|
2802
|
+
await (component as any).preload()
|
|
2803
|
+
}
|
|
2804
|
+
}),
|
|
2805
|
+
),
|
|
2806
|
+
)
|
|
2807
|
+
}
|
|
2808
|
+
return route._componentsPromise
|
|
2809
|
+
}
|
|
1259
2810
|
|
|
1260
|
-
|
|
1261
|
-
|
|
2811
|
+
preloadRoute: PreloadRouteFn<
|
|
2812
|
+
TRouteTree,
|
|
2813
|
+
TTrailingSlashOption,
|
|
2814
|
+
TDefaultStructuralSharingOption,
|
|
2815
|
+
TRouterHistory
|
|
2816
|
+
> = async (opts) => {
|
|
2817
|
+
const next = this.buildLocation(opts as any)
|
|
2818
|
+
|
|
2819
|
+
let matches = this.matchRoutes(next, {
|
|
2820
|
+
throwOnError: true,
|
|
2821
|
+
preload: true,
|
|
2822
|
+
dest: opts,
|
|
2823
|
+
})
|
|
2824
|
+
|
|
2825
|
+
const activeMatchIds = new Set(
|
|
2826
|
+
[...this.state.matches, ...(this.state.pendingMatches ?? [])].map(
|
|
2827
|
+
(d) => d.id,
|
|
2828
|
+
),
|
|
2829
|
+
)
|
|
2830
|
+
|
|
2831
|
+
const loadedMatchIds = new Set([
|
|
2832
|
+
...activeMatchIds,
|
|
2833
|
+
...this.state.cachedMatches.map((d) => d.id),
|
|
2834
|
+
])
|
|
2835
|
+
|
|
2836
|
+
// If the matches are already loaded, we need to add them to the cachedMatches
|
|
2837
|
+
batch(() => {
|
|
2838
|
+
matches.forEach((match) => {
|
|
2839
|
+
if (!loadedMatchIds.has(match.id)) {
|
|
2840
|
+
this.__store.setState((s) => ({
|
|
2841
|
+
...s,
|
|
2842
|
+
cachedMatches: [...(s.cachedMatches as any), match],
|
|
2843
|
+
}))
|
|
2844
|
+
}
|
|
2845
|
+
})
|
|
2846
|
+
})
|
|
1262
2847
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
2848
|
+
try {
|
|
2849
|
+
matches = await this.loadMatches({
|
|
2850
|
+
matches,
|
|
2851
|
+
location: next,
|
|
2852
|
+
preload: true,
|
|
2853
|
+
updateMatch: (id, updater) => {
|
|
2854
|
+
// Don't update the match if it's currently loaded
|
|
2855
|
+
if (activeMatchIds.has(id)) {
|
|
2856
|
+
matches = matches.map((d) => (d.id === id ? updater(d) : d))
|
|
2857
|
+
} else {
|
|
2858
|
+
this.updateMatch(id, updater)
|
|
1266
2859
|
}
|
|
2860
|
+
},
|
|
2861
|
+
})
|
|
2862
|
+
|
|
2863
|
+
return matches
|
|
2864
|
+
} catch (err) {
|
|
2865
|
+
if (isRedirect(err)) {
|
|
2866
|
+
if (err.options.reloadDocument) {
|
|
2867
|
+
return undefined
|
|
2868
|
+
}
|
|
2869
|
+
return await this.preloadRoute({
|
|
2870
|
+
...err.options,
|
|
2871
|
+
_fromLocation: next,
|
|
1267
2872
|
})
|
|
2873
|
+
}
|
|
2874
|
+
if (!isNotFound(err)) {
|
|
2875
|
+
// Preload errors are not fatal, but we should still log them
|
|
2876
|
+
console.error(err)
|
|
2877
|
+
}
|
|
2878
|
+
return undefined
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
1268
2881
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
2882
|
+
matchRoute: MatchRouteFn<
|
|
2883
|
+
TRouteTree,
|
|
2884
|
+
TTrailingSlashOption,
|
|
2885
|
+
TDefaultStructuralSharingOption,
|
|
2886
|
+
TRouterHistory
|
|
2887
|
+
> = (location, opts) => {
|
|
2888
|
+
const matchLocation = {
|
|
2889
|
+
...location,
|
|
2890
|
+
to: location.to
|
|
2891
|
+
? this.resolvePathWithBase(
|
|
2892
|
+
(location.from || '') as string,
|
|
2893
|
+
location.to as string,
|
|
2894
|
+
)
|
|
2895
|
+
: undefined,
|
|
2896
|
+
params: location.params || {},
|
|
2897
|
+
leaveParams: true,
|
|
2898
|
+
}
|
|
2899
|
+
const next = this.buildLocation(matchLocation as any)
|
|
2900
|
+
|
|
2901
|
+
if (opts?.pending && this.state.status !== 'pending') {
|
|
2902
|
+
return false
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
const pending =
|
|
2906
|
+
opts?.pending === undefined ? !this.state.isLoading : opts.pending
|
|
2907
|
+
|
|
2908
|
+
const baseLocation = pending
|
|
2909
|
+
? this.latestLocation
|
|
2910
|
+
: this.state.resolvedLocation || this.state.location
|
|
2911
|
+
|
|
2912
|
+
const match = matchPathname(this.basepath, baseLocation.pathname, {
|
|
2913
|
+
...opts,
|
|
2914
|
+
to: next.pathname,
|
|
2915
|
+
}) as any
|
|
2916
|
+
|
|
2917
|
+
if (!match) {
|
|
2918
|
+
return false
|
|
2919
|
+
}
|
|
2920
|
+
if (location.params) {
|
|
2921
|
+
if (!deepEqual(match, location.params, { partial: true })) {
|
|
2922
|
+
return false
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
if (match && (opts?.includeSearch ?? true)) {
|
|
2927
|
+
return deepEqual(baseLocation.search, next.search, { partial: true })
|
|
2928
|
+
? match
|
|
2929
|
+
: false
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
return match
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
ssr?: {
|
|
2936
|
+
manifest: Manifest | undefined
|
|
2937
|
+
serializer: StartSerializer
|
|
1272
2938
|
}
|
|
1273
2939
|
|
|
1274
|
-
|
|
2940
|
+
serverSsr?: {
|
|
2941
|
+
injectedHtml: Array<InjectedHtmlEntry>
|
|
2942
|
+
injectHtml: (getHtml: () => string | Promise<string>) => Promise<void>
|
|
2943
|
+
injectScript: (
|
|
2944
|
+
getScript: () => string | Promise<string>,
|
|
2945
|
+
opts?: { logScript?: boolean },
|
|
2946
|
+
) => Promise<void>
|
|
2947
|
+
streamValue: (key: string, value: any) => void
|
|
2948
|
+
streamedKeys: Set<string>
|
|
2949
|
+
onMatchSettled: (opts: { router: AnyRouter; match: AnyRouteMatch }) => any
|
|
2950
|
+
}
|
|
1275
2951
|
|
|
1276
|
-
|
|
1277
|
-
|
|
2952
|
+
clientSsr?: {
|
|
2953
|
+
getStreamedValue: <T>(key: string) => T | undefined
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
_handleNotFound = (
|
|
2957
|
+
matches: Array<AnyRouteMatch>,
|
|
2958
|
+
err: NotFoundError,
|
|
2959
|
+
{
|
|
2960
|
+
updateMatch = this.updateMatch,
|
|
2961
|
+
}: {
|
|
2962
|
+
updateMatch?: (
|
|
2963
|
+
id: string,
|
|
2964
|
+
updater: (match: AnyRouteMatch) => AnyRouteMatch,
|
|
2965
|
+
) => void
|
|
2966
|
+
} = {},
|
|
2967
|
+
) => {
|
|
2968
|
+
// Find the route that should handle the not found error
|
|
2969
|
+
// First check if a specific route is requested to show the error
|
|
2970
|
+
const routeCursor = this.routesById[err.routeId ?? ''] ?? this.routeTree
|
|
2971
|
+
const matchesByRouteId: Record<string, AnyRouteMatch> = {}
|
|
2972
|
+
|
|
2973
|
+
// Setup routesByRouteId object for quick access
|
|
2974
|
+
for (const match of matches) {
|
|
2975
|
+
matchesByRouteId[match.routeId] = match
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
// Ensure a NotFoundComponent exists on the route
|
|
2979
|
+
if (
|
|
2980
|
+
!routeCursor.options.notFoundComponent &&
|
|
2981
|
+
(this.options as any)?.defaultNotFoundComponent
|
|
2982
|
+
) {
|
|
2983
|
+
routeCursor.options.notFoundComponent = (
|
|
2984
|
+
this.options as any
|
|
2985
|
+
).defaultNotFoundComponent
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// Ensure we have a notFoundComponent
|
|
2989
|
+
invariant(
|
|
2990
|
+
routeCursor.options.notFoundComponent,
|
|
2991
|
+
'No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router.',
|
|
2992
|
+
)
|
|
2993
|
+
|
|
2994
|
+
// Find the match for this route
|
|
2995
|
+
const matchForRoute = matchesByRouteId[routeCursor.id]
|
|
2996
|
+
|
|
2997
|
+
invariant(
|
|
2998
|
+
matchForRoute,
|
|
2999
|
+
'Could not find match for route: ' + routeCursor.id,
|
|
3000
|
+
)
|
|
3001
|
+
|
|
3002
|
+
// Assign the error to the match - using non-null assertion since we've checked with invariant
|
|
3003
|
+
updateMatch(matchForRoute.id, (prev) => ({
|
|
3004
|
+
...prev,
|
|
3005
|
+
status: 'notFound',
|
|
3006
|
+
error: err,
|
|
3007
|
+
isFetching: false,
|
|
3008
|
+
}))
|
|
3009
|
+
|
|
3010
|
+
if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
|
|
3011
|
+
err.routeId = routeCursor.parentRoute.id
|
|
3012
|
+
this._handleNotFound(matches, err, {
|
|
3013
|
+
updateMatch,
|
|
3014
|
+
})
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
1278
3017
|
|
|
1279
|
-
|
|
3018
|
+
hasNotFoundMatch = () => {
|
|
3019
|
+
return this.__store.state.matches.some(
|
|
3020
|
+
(d) => d.status === 'notFound' || d.globalNotFound,
|
|
3021
|
+
)
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
export class SearchParamError extends Error {}
|
|
3026
|
+
|
|
3027
|
+
export class PathParamError extends Error {}
|
|
3028
|
+
|
|
3029
|
+
// A function that takes an import() argument which is a function and returns a new function that will
|
|
3030
|
+
// proxy arguments from the caller to the imported function, retaining all type
|
|
3031
|
+
// information along the way
|
|
3032
|
+
export function lazyFn<
|
|
3033
|
+
T extends Record<string, (...args: Array<any>) => any>,
|
|
3034
|
+
TKey extends keyof T = 'default',
|
|
3035
|
+
>(fn: () => Promise<T>, key?: TKey) {
|
|
3036
|
+
return async (
|
|
3037
|
+
...args: Parameters<T[TKey]>
|
|
3038
|
+
): Promise<Awaited<ReturnType<T[TKey]>>> => {
|
|
3039
|
+
const imported = await fn()
|
|
3040
|
+
return imported[key || 'default'](...args)
|
|
3041
|
+
}
|
|
1280
3042
|
}
|
|
1281
3043
|
|
|
1282
|
-
function
|
|
1283
|
-
|
|
3044
|
+
export function getInitialRouterState(
|
|
3045
|
+
location: ParsedLocation,
|
|
3046
|
+
): RouterState<any> {
|
|
3047
|
+
return {
|
|
3048
|
+
loadedAt: 0,
|
|
3049
|
+
isLoading: false,
|
|
3050
|
+
isTransitioning: false,
|
|
3051
|
+
status: 'idle',
|
|
3052
|
+
resolvedLocation: undefined,
|
|
3053
|
+
location,
|
|
3054
|
+
matches: [],
|
|
3055
|
+
pendingMatches: [],
|
|
3056
|
+
cachedMatches: [],
|
|
3057
|
+
statusCode: 200,
|
|
3058
|
+
}
|
|
1284
3059
|
}
|
|
1285
3060
|
|
|
1286
|
-
function
|
|
1287
|
-
|
|
1288
|
-
|
|
3061
|
+
function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
|
|
3062
|
+
if (validateSearch == null) return {}
|
|
3063
|
+
|
|
3064
|
+
if ('~standard' in validateSearch) {
|
|
3065
|
+
const result = validateSearch['~standard'].validate(input)
|
|
1289
3066
|
|
|
1290
|
-
if (
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
3067
|
+
if (result instanceof Promise)
|
|
3068
|
+
throw new SearchParamError('Async validation not supported')
|
|
3069
|
+
|
|
3070
|
+
if (result.issues)
|
|
3071
|
+
throw new SearchParamError(JSON.stringify(result.issues, undefined, 2), {
|
|
3072
|
+
cause: result,
|
|
1294
3073
|
})
|
|
3074
|
+
|
|
3075
|
+
return result.value
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
if ('parse' in validateSearch) {
|
|
3079
|
+
return validateSearch.parse(input)
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
if (typeof validateSearch === 'function') {
|
|
3083
|
+
return validateSearch(input)
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
return {}
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
export const componentTypes = [
|
|
3090
|
+
'component',
|
|
3091
|
+
'errorComponent',
|
|
3092
|
+
'pendingComponent',
|
|
3093
|
+
'notFoundComponent',
|
|
3094
|
+
] as const
|
|
3095
|
+
|
|
3096
|
+
function routeNeedsPreload(route: AnyRoute) {
|
|
3097
|
+
for (const componentType of componentTypes) {
|
|
3098
|
+
if ((route.options[componentType] as any)?.preload) {
|
|
3099
|
+
return true
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
return false
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
interface RouteLike {
|
|
3106
|
+
id: string
|
|
3107
|
+
isRoot?: boolean
|
|
3108
|
+
path?: string
|
|
3109
|
+
fullPath: string
|
|
3110
|
+
rank?: number
|
|
3111
|
+
parentRoute?: RouteLike
|
|
3112
|
+
children?: Array<RouteLike>
|
|
3113
|
+
options?: {
|
|
3114
|
+
caseSensitive?: boolean
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
export function processRouteTree<TRouteLike extends RouteLike>({
|
|
3119
|
+
routeTree,
|
|
3120
|
+
initRoute,
|
|
3121
|
+
}: {
|
|
3122
|
+
routeTree: TRouteLike
|
|
3123
|
+
initRoute?: (route: TRouteLike, index: number) => void
|
|
3124
|
+
}) {
|
|
3125
|
+
const routesById = {} as Record<string, TRouteLike>
|
|
3126
|
+
const routesByPath = {} as Record<string, TRouteLike>
|
|
3127
|
+
|
|
3128
|
+
const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
|
|
3129
|
+
childRoutes.forEach((childRoute, i) => {
|
|
3130
|
+
initRoute?.(childRoute, i)
|
|
3131
|
+
|
|
3132
|
+
const existingRoute = routesById[childRoute.id]
|
|
3133
|
+
|
|
3134
|
+
invariant(
|
|
3135
|
+
!existingRoute,
|
|
3136
|
+
`Duplicate routes found with id: ${String(childRoute.id)}`,
|
|
3137
|
+
)
|
|
3138
|
+
|
|
3139
|
+
routesById[childRoute.id] = childRoute
|
|
3140
|
+
|
|
3141
|
+
if (!childRoute.isRoot && childRoute.path) {
|
|
3142
|
+
const trimmedFullPath = trimPathRight(childRoute.fullPath)
|
|
3143
|
+
if (
|
|
3144
|
+
!routesByPath[trimmedFullPath] ||
|
|
3145
|
+
childRoute.fullPath.endsWith('/')
|
|
3146
|
+
) {
|
|
3147
|
+
routesByPath[trimmedFullPath] = childRoute
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
const children = childRoute.children as Array<TRouteLike>
|
|
3152
|
+
|
|
3153
|
+
if (children?.length) {
|
|
3154
|
+
recurseRoutes(children)
|
|
3155
|
+
}
|
|
3156
|
+
})
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
recurseRoutes([routeTree])
|
|
3160
|
+
|
|
3161
|
+
const scoredRoutes: Array<{
|
|
3162
|
+
child: TRouteLike
|
|
3163
|
+
trimmed: string
|
|
3164
|
+
parsed: ReturnType<typeof parsePathname>
|
|
3165
|
+
index: number
|
|
3166
|
+
scores: Array<number>
|
|
3167
|
+
}> = []
|
|
3168
|
+
|
|
3169
|
+
const routes: Array<TRouteLike> = Object.values(routesById)
|
|
3170
|
+
|
|
3171
|
+
routes.forEach((d, i) => {
|
|
3172
|
+
if (d.isRoot || !d.path) {
|
|
3173
|
+
return
|
|
1295
3174
|
}
|
|
3175
|
+
|
|
3176
|
+
const trimmed = trimPathLeft(d.fullPath)
|
|
3177
|
+
const parsed = parsePathname(trimmed)
|
|
3178
|
+
|
|
3179
|
+
// Removes the leading slash if it is not the only remaining segment
|
|
3180
|
+
while (parsed.length > 1 && parsed[0]?.value === '/') {
|
|
3181
|
+
parsed.shift()
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
const scores = parsed.map((segment) => {
|
|
3185
|
+
if (segment.value === '/') {
|
|
3186
|
+
return 0.75
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
if (
|
|
3190
|
+
segment.type === 'param' &&
|
|
3191
|
+
segment.prefixSegment &&
|
|
3192
|
+
segment.suffixSegment
|
|
3193
|
+
) {
|
|
3194
|
+
return 0.55
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
if (segment.type === 'param' && segment.prefixSegment) {
|
|
3198
|
+
return 0.52
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
if (segment.type === 'param' && segment.suffixSegment) {
|
|
3202
|
+
return 0.51
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
if (segment.type === 'param') {
|
|
3206
|
+
return 0.5
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
if (
|
|
3210
|
+
segment.type === 'wildcard' &&
|
|
3211
|
+
segment.prefixSegment &&
|
|
3212
|
+
segment.suffixSegment
|
|
3213
|
+
) {
|
|
3214
|
+
return 0.3
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
if (segment.type === 'wildcard' && segment.prefixSegment) {
|
|
3218
|
+
return 0.27
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
if (segment.type === 'wildcard' && segment.suffixSegment) {
|
|
3222
|
+
return 0.26
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
if (segment.type === 'wildcard') {
|
|
3226
|
+
return 0.25
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
return 1
|
|
3230
|
+
})
|
|
3231
|
+
|
|
3232
|
+
scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
|
|
1296
3233
|
})
|
|
3234
|
+
|
|
3235
|
+
const flatRoutes = scoredRoutes
|
|
3236
|
+
.sort((a, b) => {
|
|
3237
|
+
const minLength = Math.min(a.scores.length, b.scores.length)
|
|
3238
|
+
|
|
3239
|
+
// Sort by min available score
|
|
3240
|
+
for (let i = 0; i < minLength; i++) {
|
|
3241
|
+
if (a.scores[i] !== b.scores[i]) {
|
|
3242
|
+
return b.scores[i]! - a.scores[i]!
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// Sort by length of score
|
|
3247
|
+
if (a.scores.length !== b.scores.length) {
|
|
3248
|
+
return b.scores.length - a.scores.length
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
// Sort by min available parsed value
|
|
3252
|
+
for (let i = 0; i < minLength; i++) {
|
|
3253
|
+
if (a.parsed[i]!.value !== b.parsed[i]!.value) {
|
|
3254
|
+
return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
// Sort by original index
|
|
3259
|
+
return a.index - b.index
|
|
3260
|
+
})
|
|
3261
|
+
.map((d, i) => {
|
|
3262
|
+
d.child.rank = i
|
|
3263
|
+
return d.child
|
|
3264
|
+
})
|
|
3265
|
+
|
|
3266
|
+
return { routesById, routesByPath, flatRoutes }
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
export function getMatchedRoutes<TRouteLike extends RouteLike>({
|
|
3270
|
+
pathname,
|
|
3271
|
+
routePathname,
|
|
3272
|
+
basepath,
|
|
3273
|
+
caseSensitive,
|
|
3274
|
+
routesByPath,
|
|
3275
|
+
routesById,
|
|
3276
|
+
flatRoutes,
|
|
3277
|
+
}: {
|
|
3278
|
+
pathname: string
|
|
3279
|
+
routePathname?: string
|
|
3280
|
+
basepath: string
|
|
3281
|
+
caseSensitive?: boolean
|
|
3282
|
+
routesByPath: Record<string, TRouteLike>
|
|
3283
|
+
routesById: Record<string, TRouteLike>
|
|
3284
|
+
flatRoutes: Array<TRouteLike>
|
|
3285
|
+
}) {
|
|
3286
|
+
let routeParams: Record<string, string> = {}
|
|
3287
|
+
const trimmedPath = trimPathRight(pathname)
|
|
3288
|
+
const getMatchedParams = (route: TRouteLike) => {
|
|
3289
|
+
const result = matchPathname(basepath, trimmedPath, {
|
|
3290
|
+
to: route.fullPath,
|
|
3291
|
+
caseSensitive: route.options?.caseSensitive ?? caseSensitive,
|
|
3292
|
+
fuzzy: true,
|
|
3293
|
+
})
|
|
3294
|
+
return result
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
let foundRoute: TRouteLike | undefined =
|
|
3298
|
+
routePathname !== undefined ? routesByPath[routePathname] : undefined
|
|
3299
|
+
if (foundRoute) {
|
|
3300
|
+
routeParams = getMatchedParams(foundRoute)!
|
|
3301
|
+
} else {
|
|
3302
|
+
foundRoute = flatRoutes.find((route) => {
|
|
3303
|
+
const matchedParams = getMatchedParams(route)
|
|
3304
|
+
|
|
3305
|
+
if (matchedParams) {
|
|
3306
|
+
routeParams = matchedParams
|
|
3307
|
+
return true
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
return false
|
|
3311
|
+
})
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
let routeCursor: TRouteLike = foundRoute || routesById[rootRouteId]!
|
|
3315
|
+
|
|
3316
|
+
const matchedRoutes: Array<TRouteLike> = [routeCursor]
|
|
3317
|
+
|
|
3318
|
+
while (routeCursor.parentRoute) {
|
|
3319
|
+
routeCursor = routeCursor.parentRoute as TRouteLike
|
|
3320
|
+
matchedRoutes.unshift(routeCursor)
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
return { matchedRoutes, routeParams, foundRoute }
|
|
1297
3324
|
}
|