@tanstack/router-core 0.0.1-beta.2 → 0.0.1-beta.201
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/build/cjs/defer.js +39 -0
- package/build/cjs/defer.js.map +1 -0
- package/build/cjs/fileRoute.js +29 -0
- package/build/cjs/fileRoute.js.map +1 -0
- package/build/cjs/index.js +89 -0
- package/build/cjs/{packages/router-core/src/index.js.map → index.js.map} +1 -1
- package/build/cjs/{packages/router-core/src/path.js → path.js} +45 -56
- package/build/cjs/path.js.map +1 -0
- package/build/cjs/{packages/router-core/src/qss.js → qss.js} +10 -15
- package/build/cjs/qss.js.map +1 -0
- package/build/cjs/route.js +114 -0
- package/build/cjs/route.js.map +1 -0
- package/build/cjs/router.js +1277 -0
- package/build/cjs/router.js.map +1 -0
- package/build/cjs/scroll-restoration.js +139 -0
- package/build/cjs/scroll-restoration.js.map +1 -0
- package/build/cjs/{packages/router-core/src/searchParams.js → searchParams.js} +32 -19
- package/build/cjs/searchParams.js.map +1 -0
- package/build/cjs/utils.js +132 -0
- package/build/cjs/utils.js.map +1 -0
- package/build/esm/index.js +1565 -2106
- package/build/esm/index.js.map +1 -1
- package/build/stats-html.html +59 -49
- package/build/stats-react.json +219 -241
- package/build/types/defer.d.ts +19 -0
- package/build/types/fileRoute.d.ts +35 -0
- package/build/types/index.d.ts +13 -611
- package/build/types/link.d.ts +96 -0
- package/build/types/path.d.ts +16 -0
- package/build/types/qss.d.ts +2 -0
- package/build/types/route.d.ts +261 -0
- package/build/types/routeInfo.d.ts +22 -0
- package/build/types/router.d.ts +265 -0
- package/build/types/scroll-restoration.d.ts +6 -0
- package/build/types/searchParams.d.ts +5 -0
- package/build/types/utils.d.ts +51 -0
- package/build/umd/index.development.js +1917 -2070
- package/build/umd/index.development.js.map +1 -1
- package/build/umd/index.production.js +23 -2
- package/build/umd/index.production.js.map +1 -1
- package/package.json +13 -7
- package/src/defer.ts +55 -0
- package/src/fileRoute.ts +156 -0
- package/src/index.ts +5 -11
- package/src/link.ts +149 -123
- package/src/path.ts +37 -17
- package/src/qss.ts +1 -1
- package/src/route.ts +861 -231
- package/src/routeInfo.ts +47 -205
- package/src/router.ts +1818 -952
- package/src/scroll-restoration.ts +179 -0
- package/src/searchParams.ts +31 -9
- package/src/utils.ts +106 -43
- 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/path.js.map +0 -1
- package/build/cjs/packages/router-core/src/qss.js.map +0 -1
- package/build/cjs/packages/router-core/src/route.js +0 -161
- 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 -789
- package/build/cjs/packages/router-core/src/router.js.map +0 -1
- package/build/cjs/packages/router-core/src/searchParams.js.map +0 -1
- package/build/cjs/packages/router-core/src/utils.js +0 -118
- package/build/cjs/packages/router-core/src/utils.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,211 +1,238 @@
|
|
|
1
|
+
import { Store } from '@tanstack/store'
|
|
2
|
+
import invariant from 'tiny-invariant'
|
|
1
3
|
import {
|
|
2
|
-
BrowserHistory,
|
|
3
4
|
createBrowserHistory,
|
|
4
5
|
createMemoryHistory,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from 'history'
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
HistoryLocation,
|
|
7
|
+
HistoryState,
|
|
8
|
+
RouterHistory,
|
|
9
|
+
} from '@tanstack/history'
|
|
10
|
+
|
|
11
|
+
//
|
|
11
12
|
|
|
12
13
|
import {
|
|
13
14
|
LinkInfo,
|
|
14
15
|
LinkOptions,
|
|
15
|
-
|
|
16
|
+
NavigateOptions,
|
|
16
17
|
ToOptions,
|
|
17
|
-
|
|
18
|
+
ResolveRelativePath,
|
|
18
19
|
} from './link'
|
|
19
20
|
import {
|
|
20
21
|
cleanPath,
|
|
21
22
|
interpolatePath,
|
|
22
23
|
joinPaths,
|
|
23
24
|
matchPathname,
|
|
25
|
+
parsePathname,
|
|
24
26
|
resolvePath,
|
|
27
|
+
trimPath,
|
|
28
|
+
trimPathRight,
|
|
25
29
|
} from './path'
|
|
26
|
-
import { AnyRoute, cascadeLoaderData, createRoute, Route } from './route'
|
|
27
30
|
import {
|
|
28
|
-
|
|
29
|
-
AnyPathParams,
|
|
30
|
-
AnyRouteConfig,
|
|
31
|
+
Route,
|
|
31
32
|
AnySearchSchema,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
AnyRoute,
|
|
34
|
+
AnyContext,
|
|
35
|
+
AnyPathParams,
|
|
36
|
+
RegisteredRouteComponent,
|
|
37
|
+
RegisteredErrorRouteComponent,
|
|
38
|
+
RegisteredPendingRouteComponent,
|
|
39
|
+
RouteMask,
|
|
40
|
+
} from './route'
|
|
36
41
|
import {
|
|
37
|
-
AllRouteInfo,
|
|
38
|
-
AnyAllRouteInfo,
|
|
39
|
-
AnyRouteInfo,
|
|
40
|
-
RouteInfo,
|
|
41
42
|
RoutesById,
|
|
43
|
+
RoutesByPath,
|
|
44
|
+
ParseRoute,
|
|
45
|
+
FullSearchSchema,
|
|
46
|
+
RouteById,
|
|
47
|
+
RoutePaths,
|
|
48
|
+
RouteIds,
|
|
42
49
|
} from './routeInfo'
|
|
43
|
-
import { createRouteMatch, RouteMatch } from './routeMatch'
|
|
44
50
|
import { defaultParseSearch, defaultStringifySearch } from './searchParams'
|
|
45
51
|
import {
|
|
46
52
|
functionalUpdate,
|
|
47
53
|
last,
|
|
54
|
+
NoInfer,
|
|
55
|
+
pick,
|
|
48
56
|
PickAsRequired,
|
|
49
|
-
PickRequired,
|
|
50
|
-
replaceEqualDeep,
|
|
51
57
|
Timeout,
|
|
52
58
|
Updater,
|
|
59
|
+
replaceEqualDeep,
|
|
60
|
+
partialDeepEqual,
|
|
61
|
+
NonNullableUpdater,
|
|
53
62
|
} from './utils'
|
|
54
63
|
|
|
64
|
+
//
|
|
65
|
+
|
|
66
|
+
declare global {
|
|
67
|
+
interface Window {
|
|
68
|
+
__TSR_DEHYDRATED__?: HydrationCtx
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
declare module '@tanstack/history' {
|
|
73
|
+
interface HistoryState {
|
|
74
|
+
__tempLocation?: HistoryLocation
|
|
75
|
+
__tempKey?: string
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface Register {
|
|
80
|
+
// router: Router
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
export interface LocationState {}
|
|
56
84
|
|
|
57
|
-
export
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
85
|
+
export type AnyRouter = Router<any, any>
|
|
86
|
+
|
|
87
|
+
export type RegisteredRouter = Register extends {
|
|
88
|
+
router: infer TRouter extends AnyRouter
|
|
89
|
+
}
|
|
90
|
+
? TRouter
|
|
91
|
+
: AnyRouter
|
|
92
|
+
|
|
93
|
+
export interface ParsedLocation<TSearchObj extends AnySearchSchema = {}> {
|
|
61
94
|
href: string
|
|
62
95
|
pathname: string
|
|
63
96
|
search: TSearchObj
|
|
64
97
|
searchStr: string
|
|
65
|
-
state:
|
|
98
|
+
state: HistoryState
|
|
66
99
|
hash: string
|
|
67
|
-
|
|
100
|
+
maskedLocation?: ParsedLocation<TSearchObj>
|
|
101
|
+
unmaskOnReload?: boolean
|
|
68
102
|
}
|
|
69
103
|
|
|
70
104
|
export interface FromLocation {
|
|
71
105
|
pathname: string
|
|
72
106
|
search?: unknown
|
|
73
|
-
key?: string
|
|
74
107
|
hash?: string
|
|
75
108
|
}
|
|
76
109
|
|
|
77
110
|
export type SearchSerializer = (searchObj: Record<string, any>) => string
|
|
78
111
|
export type SearchParser = (searchStr: string) => Record<string, any>
|
|
79
|
-
export type FilterRoutesFn = <TRoute extends Route<any, RouteInfo>>(
|
|
80
|
-
routeConfigs: TRoute[],
|
|
81
|
-
) => TRoute[]
|
|
82
112
|
|
|
83
|
-
export
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
parseSearch?: SearchParser
|
|
87
|
-
filterRoutes?: FilterRoutesFn
|
|
88
|
-
defaultPreload?: false | 'intent'
|
|
89
|
-
defaultPreloadMaxAge?: number
|
|
90
|
-
defaultPreloadGcMaxAge?: number
|
|
91
|
-
defaultPreloadDelay?: number
|
|
92
|
-
useErrorBoundary?: boolean
|
|
93
|
-
defaultElement?: GetFrameworkGeneric<'Element'>
|
|
94
|
-
defaultErrorElement?: GetFrameworkGeneric<'Element'>
|
|
95
|
-
defaultCatchElement?: GetFrameworkGeneric<'Element'>
|
|
96
|
-
defaultPendingElement?: GetFrameworkGeneric<'Element'>
|
|
97
|
-
defaultPendingMs?: number
|
|
98
|
-
defaultPendingMinMs?: number
|
|
99
|
-
defaultLoaderMaxAge?: number
|
|
100
|
-
defaultLoaderGcMaxAge?: number
|
|
101
|
-
caseSensitive?: boolean
|
|
102
|
-
routeConfig?: TRouteConfig
|
|
103
|
-
basepath?: string
|
|
104
|
-
createRouter?: (router: Router<any, any>) => void
|
|
105
|
-
createRoute?: (opts: { route: AnyRoute; router: Router<any, any> }) => void
|
|
106
|
-
createElement?: (
|
|
107
|
-
element: GetFrameworkGeneric<'SyncOrAsyncElement'>,
|
|
108
|
-
) => Promise<GetFrameworkGeneric<'Element'>>
|
|
113
|
+
export type HydrationCtx = {
|
|
114
|
+
router: DehydratedRouter
|
|
115
|
+
payload: Record<string, any>
|
|
109
116
|
}
|
|
110
117
|
|
|
111
|
-
export interface
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// TError = unknown,
|
|
118
|
+
export interface RouteMatch<
|
|
119
|
+
TRouteTree extends AnyRoute = AnyRoute,
|
|
120
|
+
TRouteId extends RouteIds<TRouteTree> = ParseRoute<TRouteTree>['id'],
|
|
115
121
|
> {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
id: string
|
|
123
|
+
// loaderContext?: RouteById<TRouteTree, TRouteId>['types']['loaderContext']
|
|
124
|
+
loaderContext?: any
|
|
125
|
+
routeId: TRouteId
|
|
126
|
+
pathname: string
|
|
127
|
+
params: RouteById<TRouteTree, TRouteId>['types']['allParams']
|
|
128
|
+
status: 'pending' | 'success' | 'error'
|
|
129
|
+
isFetching: boolean
|
|
130
|
+
invalid: boolean
|
|
131
|
+
error: unknown
|
|
132
|
+
paramsError: unknown
|
|
133
|
+
searchError: unknown
|
|
134
|
+
updatedAt: number
|
|
135
|
+
maxAge: number
|
|
136
|
+
preloadMaxAge: number
|
|
137
|
+
loaderData: RouteById<TRouteTree, TRouteId>['types']['loader']
|
|
138
|
+
loadPromise?: Promise<void>
|
|
139
|
+
__resolveLoadPromise?: () => void
|
|
140
|
+
context: RouteById<TRouteTree, TRouteId>['types']['context']
|
|
141
|
+
routeSearch: RouteById<TRouteTree, TRouteId>['types']['searchSchema']
|
|
142
|
+
search: FullSearchSchema<TRouteTree> &
|
|
143
|
+
RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema']
|
|
144
|
+
fetchedAt: number
|
|
145
|
+
abortController: AbortController
|
|
120
146
|
}
|
|
121
147
|
|
|
122
|
-
export
|
|
123
|
-
TPayload = unknown,
|
|
124
|
-
TResponse = unknown,
|
|
125
|
-
// TError = unknown,
|
|
126
|
-
> {
|
|
127
|
-
submittedAt: number
|
|
128
|
-
status: 'idle' | 'pending' | 'success' | 'error'
|
|
129
|
-
submission: TPayload
|
|
130
|
-
data?: TResponse
|
|
131
|
-
error?: unknown
|
|
132
|
-
}
|
|
148
|
+
export type AnyRouteMatch = RouteMatch<any>
|
|
133
149
|
|
|
134
|
-
export
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
: (loaderContext: {
|
|
143
|
-
params: TAllParams
|
|
144
|
-
search?: TFullSearchSchema
|
|
145
|
-
signal?: AbortSignal
|
|
146
|
-
}) => Promise<TRouteLoaderData>
|
|
147
|
-
: keyof TAllParams extends never
|
|
148
|
-
? (loaderContext: {
|
|
149
|
-
search: TFullSearchSchema
|
|
150
|
-
params: TAllParams
|
|
151
|
-
signal?: AbortSignal
|
|
152
|
-
}) => Promise<TRouteLoaderData>
|
|
153
|
-
: (loaderContext: {
|
|
154
|
-
search: TFullSearchSchema
|
|
155
|
-
signal?: AbortSignal
|
|
156
|
-
}) => Promise<TRouteLoaderData>
|
|
157
|
-
current?: LoaderState<TFullSearchSchema, TAllParams>
|
|
158
|
-
latest?: LoaderState<TFullSearchSchema, TAllParams>
|
|
159
|
-
pending: LoaderState<TFullSearchSchema, TAllParams>[]
|
|
160
|
-
}
|
|
150
|
+
export type RouterContextOptions<TRouteTree extends AnyRoute> =
|
|
151
|
+
AnyContext extends TRouteTree['types']['routerContext']
|
|
152
|
+
? {
|
|
153
|
+
context?: TRouteTree['types']['routerContext']
|
|
154
|
+
}
|
|
155
|
+
: {
|
|
156
|
+
context: TRouteTree['types']['routerContext']
|
|
157
|
+
}
|
|
161
158
|
|
|
162
|
-
export interface
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
export interface RouterOptions<
|
|
160
|
+
TRouteTree extends AnyRoute,
|
|
161
|
+
TDehydrated extends Record<string, any>,
|
|
165
162
|
> {
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
history?: RouterHistory
|
|
164
|
+
stringifySearch?: SearchSerializer
|
|
165
|
+
parseSearch?: SearchParser
|
|
166
|
+
defaultPreload?: false | 'intent'
|
|
167
|
+
defaultPreloadDelay?: number
|
|
168
|
+
reloadOnWindowFocus?: boolean
|
|
169
|
+
defaultComponent?: RegisteredRouteComponent<
|
|
170
|
+
unknown,
|
|
171
|
+
AnySearchSchema,
|
|
172
|
+
AnyPathParams,
|
|
173
|
+
AnyContext
|
|
174
|
+
>
|
|
175
|
+
defaultErrorComponent?: RegisteredErrorRouteComponent<
|
|
176
|
+
AnySearchSchema,
|
|
177
|
+
AnyPathParams,
|
|
178
|
+
AnyContext
|
|
179
|
+
>
|
|
180
|
+
defaultPendingComponent?: RegisteredPendingRouteComponent<
|
|
181
|
+
AnySearchSchema,
|
|
182
|
+
AnyPathParams,
|
|
183
|
+
AnyContext
|
|
184
|
+
>
|
|
185
|
+
defaultMaxAge?: number
|
|
186
|
+
defaultGcMaxAge?: number
|
|
187
|
+
defaultPreloadMaxAge?: number
|
|
188
|
+
caseSensitive?: boolean
|
|
189
|
+
routeTree?: TRouteTree
|
|
190
|
+
basepath?: string
|
|
191
|
+
createRoute?: (opts: { route: AnyRoute; router: AnyRouter }) => void
|
|
192
|
+
context?: TRouteTree['types']['routerContext']
|
|
193
|
+
dehydrate?: () => TDehydrated
|
|
194
|
+
hydrate?: (dehydrated: TDehydrated) => void
|
|
195
|
+
routeMasks?: RouteMask<TRouteTree>[]
|
|
196
|
+
unmaskOnReload?: boolean
|
|
168
197
|
}
|
|
169
198
|
|
|
170
|
-
export interface RouterState {
|
|
171
|
-
status: 'idle' | '
|
|
172
|
-
location: Location
|
|
173
|
-
matches: RouteMatch[]
|
|
174
|
-
lastUpdated: number
|
|
175
|
-
loaderData: unknown
|
|
176
|
-
currentAction?: ActionState
|
|
177
|
-
latestAction?: ActionState
|
|
178
|
-
actions: Record<string, Action>
|
|
179
|
-
loaders: Record<string, Loader>
|
|
180
|
-
pending?: PendingState
|
|
199
|
+
export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
|
|
200
|
+
status: 'idle' | 'pending'
|
|
181
201
|
isFetching: boolean
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
202
|
+
matchesById: Record<string, RouteMatch<TRouteTree>>
|
|
203
|
+
matchIds: string[]
|
|
204
|
+
pendingMatchIds: string[]
|
|
205
|
+
matches: RouteMatch<TRouteTree>[]
|
|
206
|
+
pendingMatches: RouteMatch<TRouteTree>[]
|
|
207
|
+
renderedMatchIds: string[]
|
|
208
|
+
renderedMatches: RouteMatch<TRouteTree>[]
|
|
209
|
+
location: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
210
|
+
resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
211
|
+
lastUpdated: number
|
|
188
212
|
}
|
|
189
213
|
|
|
190
|
-
type
|
|
191
|
-
|
|
192
|
-
export type ListenerFn = () => void
|
|
214
|
+
export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void
|
|
193
215
|
|
|
194
216
|
export interface BuildNextOptions {
|
|
195
217
|
to?: string | number | null
|
|
196
|
-
params?: true | Updater<
|
|
218
|
+
params?: true | Updater<unknown>
|
|
197
219
|
search?: true | Updater<unknown>
|
|
198
220
|
hash?: true | Updater<string>
|
|
199
|
-
|
|
221
|
+
state?: true | NonNullableUpdater<LocationState>
|
|
222
|
+
mask?: {
|
|
223
|
+
to?: string | number | null
|
|
224
|
+
params?: true | Updater<unknown>
|
|
225
|
+
search?: true | Updater<unknown>
|
|
226
|
+
hash?: true | Updater<string>
|
|
227
|
+
state?: true | NonNullableUpdater<LocationState>
|
|
228
|
+
unmaskOnReload?: boolean
|
|
229
|
+
}
|
|
200
230
|
from?: string
|
|
201
|
-
fromCurrent?: boolean
|
|
202
|
-
__preSearchFilters?: SearchFilter<any>[]
|
|
203
|
-
__postSearchFilters?: SearchFilter<any>[]
|
|
204
231
|
}
|
|
205
232
|
|
|
206
|
-
export
|
|
207
|
-
|
|
208
|
-
|
|
233
|
+
export interface CommitLocationOptions {
|
|
234
|
+
replace?: boolean
|
|
235
|
+
resetScroll?: boolean
|
|
209
236
|
}
|
|
210
237
|
|
|
211
238
|
export interface MatchLocation {
|
|
@@ -213,1016 +240,1855 @@ export interface MatchLocation {
|
|
|
213
240
|
fuzzy?: boolean
|
|
214
241
|
caseSensitive?: boolean
|
|
215
242
|
from?: string
|
|
216
|
-
fromCurrent?: boolean
|
|
217
243
|
}
|
|
218
244
|
|
|
219
245
|
export interface MatchRouteOptions {
|
|
220
|
-
pending
|
|
246
|
+
pending?: boolean
|
|
221
247
|
caseSensitive?: boolean
|
|
248
|
+
includeSearch?: boolean
|
|
249
|
+
fuzzy?: boolean
|
|
222
250
|
}
|
|
223
251
|
|
|
224
252
|
type LinkCurrentTargetElement = {
|
|
225
253
|
preloadTimeout?: null | ReturnType<typeof setTimeout>
|
|
226
254
|
}
|
|
227
255
|
|
|
228
|
-
export interface
|
|
229
|
-
|
|
230
|
-
|
|
256
|
+
export interface DehydratedRouterState {
|
|
257
|
+
matchIds: string[]
|
|
258
|
+
dehydratedMatches: DehydratedRouteMatch[]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export type DehydratedRouteMatch = Pick<
|
|
262
|
+
RouteMatch,
|
|
263
|
+
| 'fetchedAt'
|
|
264
|
+
| 'invalid'
|
|
265
|
+
| 'maxAge'
|
|
266
|
+
| 'preloadMaxAge'
|
|
267
|
+
| 'id'
|
|
268
|
+
| 'loaderData'
|
|
269
|
+
| 'status'
|
|
270
|
+
| 'updatedAt'
|
|
271
|
+
>
|
|
272
|
+
|
|
273
|
+
export interface DehydratedRouter {
|
|
274
|
+
state: DehydratedRouterState
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export type RouterConstructorOptions<
|
|
278
|
+
TRouteTree extends AnyRoute,
|
|
279
|
+
TDehydrated extends Record<string, any>,
|
|
280
|
+
> = Omit<RouterOptions<TRouteTree, TDehydrated>, 'context'> &
|
|
281
|
+
RouterContextOptions<TRouteTree>
|
|
282
|
+
|
|
283
|
+
export const componentTypes = [
|
|
284
|
+
'component',
|
|
285
|
+
'errorComponent',
|
|
286
|
+
'pendingComponent',
|
|
287
|
+
] as const
|
|
288
|
+
|
|
289
|
+
export type RouterEvents = {
|
|
290
|
+
onBeforeLoad: {
|
|
291
|
+
type: 'onBeforeLoad'
|
|
292
|
+
from: ParsedLocation
|
|
293
|
+
to: ParsedLocation
|
|
294
|
+
pathChanged: boolean
|
|
295
|
+
}
|
|
296
|
+
onLoad: {
|
|
297
|
+
type: 'onLoad'
|
|
298
|
+
from: ParsedLocation
|
|
299
|
+
to: ParsedLocation
|
|
300
|
+
pathChanged: boolean
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export type RouterEvent = RouterEvents[keyof RouterEvents]
|
|
305
|
+
|
|
306
|
+
export type RouterListener<TRouterEvent extends RouterEvent> = {
|
|
307
|
+
eventType: TRouterEvent['type']
|
|
308
|
+
fn: ListenerFn<TRouterEvent>
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const visibilityChangeEvent = 'visibilitychange'
|
|
312
|
+
const focusEvent = 'focus'
|
|
313
|
+
const preloadWarning = 'Error preloading route! ☝️'
|
|
314
|
+
|
|
315
|
+
export class Router<
|
|
316
|
+
TRouteTree extends AnyRoute = AnyRoute,
|
|
317
|
+
TDehydrated extends Record<string, any> = Record<string, any>,
|
|
231
318
|
> {
|
|
232
|
-
|
|
319
|
+
types!: {
|
|
320
|
+
RootRoute: TRouteTree
|
|
321
|
+
}
|
|
322
|
+
|
|
233
323
|
options: PickAsRequired<
|
|
234
|
-
RouterOptions<
|
|
235
|
-
'stringifySearch' | 'parseSearch'
|
|
324
|
+
RouterOptions<TRouteTree, TDehydrated>,
|
|
325
|
+
'stringifySearch' | 'parseSearch' | 'context'
|
|
236
326
|
>
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
327
|
+
history!: RouterHistory
|
|
328
|
+
#unsubHistory?: () => void
|
|
329
|
+
basepath!: string
|
|
330
|
+
routeTree!: TRouteTree
|
|
331
|
+
routesById!: RoutesById<TRouteTree>
|
|
332
|
+
routesByPath!: RoutesByPath<TRouteTree>
|
|
333
|
+
flatRoutes!: ParseRoute<TRouteTree>[]
|
|
334
|
+
navigateTimeout: undefined | Timeout
|
|
335
|
+
nextAction: undefined | 'push' | 'replace'
|
|
336
|
+
navigationPromise: undefined | Promise<void>
|
|
337
|
+
|
|
338
|
+
__store: Store<RouterState<TRouteTree>>
|
|
339
|
+
state: RouterState<TRouteTree>
|
|
340
|
+
dehydratedData?: TDehydrated
|
|
341
|
+
resetNextScroll = false
|
|
342
|
+
tempLocationKey = `${Math.round(Math.random() * 10000000)}`
|
|
343
|
+
// nextTemporaryLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
344
|
+
|
|
345
|
+
constructor(options: RouterConstructorOptions<TRouteTree, TDehydrated>) {
|
|
346
|
+
this.options = {
|
|
347
|
+
defaultPreloadDelay: 50,
|
|
348
|
+
context: undefined!,
|
|
349
|
+
...options,
|
|
350
|
+
stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
|
|
351
|
+
parseSearch: options?.parseSearch ?? defaultParseSearch,
|
|
352
|
+
// fetchServerDataFn: options?.fetchServerDataFn ?? defaultFetchServerDataFn,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this.__store = new Store<RouterState<TRouteTree>>(getInitialRouterState(), {
|
|
356
|
+
onUpdate: () => {
|
|
357
|
+
const prev = this.state
|
|
358
|
+
|
|
359
|
+
const next = this.__store.state
|
|
360
|
+
|
|
361
|
+
const matchesByIdChanged = prev.matchesById !== next.matchesById
|
|
362
|
+
let matchesChanged
|
|
363
|
+
let pendingMatchesChanged
|
|
364
|
+
|
|
365
|
+
if (!matchesByIdChanged) {
|
|
366
|
+
matchesChanged =
|
|
367
|
+
prev.matchIds.length !== next.matchIds.length ||
|
|
368
|
+
prev.matchIds.some((d, i) => d !== next.matchIds[i])
|
|
369
|
+
|
|
370
|
+
pendingMatchesChanged =
|
|
371
|
+
prev.pendingMatchIds.length !== next.pendingMatchIds.length ||
|
|
372
|
+
prev.pendingMatchIds.some((d, i) => d !== next.pendingMatchIds[i])
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (matchesByIdChanged || matchesChanged) {
|
|
376
|
+
next.matches = next.matchIds.map((id) => {
|
|
377
|
+
return next.matchesById[id] as any
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (matchesByIdChanged || pendingMatchesChanged) {
|
|
382
|
+
next.pendingMatches = next.pendingMatchIds.map((id) => {
|
|
383
|
+
return next.matchesById[id] as any
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (matchesByIdChanged || matchesChanged || pendingMatchesChanged) {
|
|
388
|
+
const hasPendingComponent = next.pendingMatches.some((d) => {
|
|
389
|
+
const route = this.getRoute(d.routeId as any)
|
|
390
|
+
return !!route?.options.pendingComponent
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
next.renderedMatchIds = hasPendingComponent
|
|
394
|
+
? next.pendingMatchIds
|
|
395
|
+
: next.matchIds
|
|
396
|
+
|
|
397
|
+
next.renderedMatches = next.renderedMatchIds.map((id) => {
|
|
398
|
+
return next.matchesById[id] as any
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
next.isFetching = [...next.matches, ...next.pendingMatches].some(
|
|
403
|
+
(d) => d.isFetching,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
this.state = next
|
|
407
|
+
},
|
|
408
|
+
defaultPriority: 'low',
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
this.state = this.__store.state
|
|
412
|
+
|
|
413
|
+
this.update(options)
|
|
414
|
+
|
|
415
|
+
const nextLocation = this.buildLocation({
|
|
416
|
+
search: true,
|
|
417
|
+
params: true,
|
|
418
|
+
hash: true,
|
|
419
|
+
state: true,
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
if (this.state.location.href !== nextLocation.href) {
|
|
423
|
+
this.#commitLocation({ ...nextLocation, replace: true })
|
|
424
|
+
}
|
|
319
425
|
}
|
|
320
|
-
}
|
|
321
426
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const originalOptions = {
|
|
340
|
-
defaultLoaderGcMaxAge: 5 * 60 * 1000,
|
|
341
|
-
defaultLoaderMaxAge: 0,
|
|
342
|
-
defaultPreloadMaxAge: 2000,
|
|
343
|
-
defaultPreloadDelay: 50,
|
|
344
|
-
...userOptions,
|
|
345
|
-
stringifySearch: userOptions?.stringifySearch ?? defaultStringifySearch,
|
|
346
|
-
parseSearch: userOptions?.parseSearch ?? defaultParseSearch,
|
|
427
|
+
subscribers = new Set<RouterListener<RouterEvent>>()
|
|
428
|
+
|
|
429
|
+
subscribe = <TType extends keyof RouterEvents>(
|
|
430
|
+
eventType: TType,
|
|
431
|
+
fn: ListenerFn<RouterEvents[TType]>,
|
|
432
|
+
) => {
|
|
433
|
+
const listener: RouterListener<any> = {
|
|
434
|
+
eventType,
|
|
435
|
+
fn,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
this.subscribers.add(listener)
|
|
439
|
+
|
|
440
|
+
return () => {
|
|
441
|
+
this.subscribers.delete(listener)
|
|
442
|
+
}
|
|
347
443
|
}
|
|
348
444
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
removeActionQueue: [],
|
|
354
|
-
// Resolved after construction
|
|
355
|
-
basepath: '',
|
|
356
|
-
routeTree: undefined!,
|
|
357
|
-
routesById: {} as any,
|
|
358
|
-
location: undefined!,
|
|
359
|
-
allRouteInfo: undefined!,
|
|
360
|
-
//
|
|
361
|
-
navigationPromise: Promise.resolve(),
|
|
362
|
-
resolveNavigation: () => {},
|
|
363
|
-
matchCache: {},
|
|
364
|
-
state: {
|
|
365
|
-
status: 'idle',
|
|
366
|
-
location: null!,
|
|
367
|
-
matches: [],
|
|
368
|
-
actions: {},
|
|
369
|
-
loaders: {},
|
|
370
|
-
loaderData: {} as any,
|
|
371
|
-
lastUpdated: Date.now(),
|
|
372
|
-
isFetching: false,
|
|
373
|
-
isPreloading: false,
|
|
374
|
-
},
|
|
375
|
-
startedLoadingAt: Date.now(),
|
|
376
|
-
subscribe: (listener: Listener): (() => void) => {
|
|
377
|
-
router.listeners.push(listener as Listener)
|
|
378
|
-
return () => {
|
|
379
|
-
router.listeners = router.listeners.filter((x) => x !== listener)
|
|
380
|
-
}
|
|
381
|
-
},
|
|
382
|
-
getRoute: (id) => {
|
|
383
|
-
return router.routesById[id]
|
|
384
|
-
},
|
|
385
|
-
notify: (): void => {
|
|
386
|
-
router.state = {
|
|
387
|
-
...router.state,
|
|
388
|
-
isFetching:
|
|
389
|
-
router.state.status === 'loading' ||
|
|
390
|
-
router.state.matches.some((d) => d.isFetching),
|
|
391
|
-
isPreloading: Object.values(router.matchCache).some(
|
|
392
|
-
(d) =>
|
|
393
|
-
d.match.isFetching &&
|
|
394
|
-
!router.state.matches.find((dd) => dd.matchId === d.match.matchId),
|
|
395
|
-
),
|
|
445
|
+
#emit = (routerEvent: RouterEvent) => {
|
|
446
|
+
this.subscribers.forEach((listener) => {
|
|
447
|
+
if (listener.eventType === routerEvent.type) {
|
|
448
|
+
listener.fn(routerEvent)
|
|
396
449
|
}
|
|
450
|
+
})
|
|
451
|
+
}
|
|
397
452
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
453
|
+
reset = () => {
|
|
454
|
+
this.__store.setState((s) => Object.assign(s, getInitialRouterState()))
|
|
455
|
+
}
|
|
401
456
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
457
|
+
mount = () => {
|
|
458
|
+
// addEventListener does not exist in React Native, but window does
|
|
459
|
+
// In the future, we might need to invert control here for more adapters
|
|
460
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
461
|
+
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
462
|
+
window.addEventListener(visibilityChangeEvent, this.#onFocus, false)
|
|
463
|
+
window.addEventListener(focusEvent, this.#onFocus, false)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.safeLoad()
|
|
467
|
+
|
|
468
|
+
return () => {
|
|
469
|
+
if (typeof window !== 'undefined' && window.removeEventListener) {
|
|
470
|
+
window.removeEventListener(visibilityChangeEvent, this.#onFocus)
|
|
471
|
+
window.removeEventListener(focusEvent, this.#onFocus)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
#onFocus = () => {
|
|
477
|
+
if (this.options.reloadOnWindowFocus ?? true) {
|
|
478
|
+
this.invalidate({
|
|
479
|
+
__fromFocus: true,
|
|
407
480
|
})
|
|
481
|
+
}
|
|
482
|
+
}
|
|
408
483
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
484
|
+
update = (opts?: RouterOptions<any, any>): this => {
|
|
485
|
+
this.options = {
|
|
486
|
+
...this.options,
|
|
487
|
+
...opts,
|
|
488
|
+
context: {
|
|
489
|
+
...this.options.context,
|
|
490
|
+
...opts?.context,
|
|
491
|
+
},
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (
|
|
495
|
+
!this.history ||
|
|
496
|
+
(this.options.history && this.options.history !== this.history)
|
|
497
|
+
) {
|
|
498
|
+
if (this.#unsubHistory) {
|
|
499
|
+
this.#unsubHistory()
|
|
415
500
|
}
|
|
416
501
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
502
|
+
this.history =
|
|
503
|
+
this.options.history ??
|
|
504
|
+
(isServer ? createMemoryHistory() : createBrowserHistory()!)
|
|
505
|
+
|
|
506
|
+
const parsedLocation = this.#parseLocation()
|
|
507
|
+
|
|
508
|
+
this.__store.setState((s) => ({
|
|
509
|
+
...s,
|
|
510
|
+
resolvedLocation: parsedLocation as any,
|
|
511
|
+
location: parsedLocation as any,
|
|
512
|
+
}))
|
|
513
|
+
|
|
514
|
+
this.#unsubHistory = this.history.subscribe(() => {
|
|
515
|
+
this.safeLoad({
|
|
516
|
+
next: this.#parseLocation(this.state.location),
|
|
517
|
+
})
|
|
421
518
|
})
|
|
519
|
+
}
|
|
422
520
|
|
|
423
|
-
|
|
424
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
425
|
-
if (!isServer && window.addEventListener) {
|
|
426
|
-
// Listen to visibillitychange and focus
|
|
427
|
-
window.addEventListener('visibilitychange', router.onFocus, false)
|
|
428
|
-
window.addEventListener('focus', router.onFocus, false)
|
|
429
|
-
}
|
|
521
|
+
const { basepath, routeTree } = this.options
|
|
430
522
|
|
|
431
|
-
|
|
432
|
-
unsub()
|
|
433
|
-
// Be sure to unsubscribe if a new handler is set
|
|
434
|
-
window.removeEventListener('visibilitychange', router.onFocus)
|
|
435
|
-
window.removeEventListener('focus', router.onFocus)
|
|
436
|
-
}
|
|
437
|
-
},
|
|
523
|
+
this.basepath = `/${trimPath(basepath ?? '') ?? ''}`
|
|
438
524
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
525
|
+
if (routeTree && routeTree !== this.routeTree) {
|
|
526
|
+
this.#processRoutes(routeTree)
|
|
527
|
+
}
|
|
442
528
|
|
|
443
|
-
|
|
444
|
-
|
|
529
|
+
return this
|
|
530
|
+
}
|
|
445
531
|
|
|
446
|
-
|
|
532
|
+
cancelMatches = () => {
|
|
533
|
+
this.state.matches.forEach((match) => {
|
|
534
|
+
this.cancelMatch(match.id)
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
cancelMatch = (id: string) => {
|
|
539
|
+
this.getRouteMatch(id)?.abortController?.abort()
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
safeLoad = async (opts?: { next?: ParsedLocation }) => {
|
|
543
|
+
try {
|
|
544
|
+
return this.load(opts)
|
|
545
|
+
} catch (err) {
|
|
546
|
+
// Don't do anything
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
latestLoadPromise: Promise<void> = Promise.resolve()
|
|
551
|
+
|
|
552
|
+
load = async (opts?: {
|
|
553
|
+
next?: ParsedLocation
|
|
554
|
+
throwOnError?: boolean
|
|
555
|
+
__dehydratedMatches?: DehydratedRouteMatch[]
|
|
556
|
+
}) => {
|
|
557
|
+
const promise = new Promise<void>(async (resolve, reject) => {
|
|
558
|
+
const prevLocation = this.state.resolvedLocation
|
|
559
|
+
const pathDidChange = !!(
|
|
560
|
+
opts?.next && prevLocation!.href !== opts.next.href
|
|
561
|
+
)
|
|
447
562
|
|
|
448
|
-
|
|
563
|
+
let latestPromise: Promise<void> | undefined | null
|
|
449
564
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
565
|
+
const checkLatest = (): undefined | Promise<void> | null => {
|
|
566
|
+
return this.latestLoadPromise !== promise
|
|
567
|
+
? this.latestLoadPromise
|
|
568
|
+
: undefined
|
|
453
569
|
}
|
|
454
570
|
|
|
455
|
-
|
|
456
|
-
|
|
571
|
+
// Cancel any pending matches
|
|
572
|
+
|
|
573
|
+
let pendingMatches!: RouteMatch<any, any>[]
|
|
457
574
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
match.cancel()
|
|
575
|
+
this.#emit({
|
|
576
|
+
type: 'onBeforeLoad',
|
|
577
|
+
from: prevLocation,
|
|
578
|
+
to: opts?.next ?? this.state.location,
|
|
579
|
+
pathChanged: pathDidChange,
|
|
464
580
|
})
|
|
465
|
-
},
|
|
466
581
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
582
|
+
this.__store.batch(() => {
|
|
583
|
+
if (opts?.next) {
|
|
584
|
+
// Ingest the new location
|
|
585
|
+
this.__store.setState((s) => ({
|
|
586
|
+
...s,
|
|
587
|
+
location: opts.next! as any,
|
|
588
|
+
}))
|
|
589
|
+
}
|
|
470
590
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
591
|
+
// Match the routes
|
|
592
|
+
pendingMatches = this.matchRoutes(
|
|
593
|
+
this.state.location.pathname,
|
|
594
|
+
this.state.location.search,
|
|
595
|
+
{
|
|
596
|
+
throwOnError: opts?.throwOnError,
|
|
597
|
+
debug: true,
|
|
598
|
+
},
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
this.__store.setState((s) => ({
|
|
602
|
+
...s,
|
|
603
|
+
status: 'pending',
|
|
604
|
+
pendingMatchIds: pendingMatches.map((d) => d.id),
|
|
605
|
+
matchesById: this.#mergeMatches(s.matchesById, pendingMatches),
|
|
606
|
+
}))
|
|
607
|
+
})
|
|
475
608
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
609
|
+
try {
|
|
610
|
+
// Load the matches
|
|
611
|
+
try {
|
|
612
|
+
await this.loadMatches(pendingMatches.map((d) => d.id))
|
|
613
|
+
} catch (err) {
|
|
614
|
+
// swallow this error, since we'll display the
|
|
615
|
+
// errors on the route components
|
|
480
616
|
}
|
|
481
|
-
|
|
482
|
-
|
|
617
|
+
|
|
618
|
+
// Only apply the latest transition
|
|
619
|
+
if ((latestPromise = checkLatest())) {
|
|
620
|
+
return latestPromise
|
|
483
621
|
}
|
|
484
|
-
})
|
|
485
|
-
router.removeActionQueue = []
|
|
486
622
|
|
|
487
|
-
|
|
488
|
-
|
|
623
|
+
const exitingMatchIds = this.state.matchIds.filter(
|
|
624
|
+
(id) => !this.state.pendingMatchIds.includes(id),
|
|
625
|
+
)
|
|
489
626
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
})
|
|
627
|
+
const enteringMatchIds = this.state.pendingMatchIds.filter(
|
|
628
|
+
(id) => !this.state.matchIds.includes(id),
|
|
629
|
+
)
|
|
494
630
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
631
|
+
const stayingMatchIds = this.state.matchIds.filter((id) =>
|
|
632
|
+
this.state.pendingMatchIds.includes(id),
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
this.__store.setState((s) => ({
|
|
636
|
+
...s,
|
|
637
|
+
status: 'idle',
|
|
638
|
+
resolvedLocation: s.location,
|
|
639
|
+
matchIds: s.pendingMatchIds,
|
|
640
|
+
pendingMatchIds: [],
|
|
641
|
+
}))
|
|
642
|
+
;(
|
|
643
|
+
[
|
|
644
|
+
[exitingMatchIds, 'onLeave'],
|
|
645
|
+
[enteringMatchIds, 'onEnter'],
|
|
646
|
+
[stayingMatchIds, 'onTransition'],
|
|
647
|
+
] as const
|
|
648
|
+
).forEach(([matchIds, hook]) => {
|
|
649
|
+
matchIds.forEach((id) => {
|
|
650
|
+
const match = this.getRouteMatch(id)!
|
|
651
|
+
const route = this.getRoute(match.routeId)
|
|
652
|
+
route.options[hook]?.(match)
|
|
653
|
+
})
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
this.#emit({
|
|
657
|
+
type: 'onLoad',
|
|
658
|
+
from: prevLocation,
|
|
659
|
+
to: this.state.location,
|
|
660
|
+
pathChanged: pathDidChange,
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
resolve()
|
|
664
|
+
} catch (err) {
|
|
665
|
+
// Only apply the latest transition
|
|
666
|
+
if ((latestPromise = checkLatest())) {
|
|
667
|
+
return latestPromise
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
reject(err)
|
|
502
671
|
}
|
|
672
|
+
})
|
|
503
673
|
|
|
504
|
-
|
|
674
|
+
this.latestLoadPromise = promise
|
|
505
675
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
})
|
|
676
|
+
this.latestLoadPromise.then(() => {
|
|
677
|
+
this.cleanMatches()
|
|
678
|
+
})
|
|
510
679
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
680
|
+
return this.latestLoadPromise
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
#mergeMatches = (
|
|
684
|
+
prevMatchesById: Record<string, RouteMatch<TRouteTree>>,
|
|
685
|
+
nextMatches: AnyRouteMatch[],
|
|
686
|
+
): Record<string, RouteMatch<TRouteTree>> => {
|
|
687
|
+
let matchesById = { ...prevMatchesById }
|
|
688
|
+
|
|
689
|
+
nextMatches.forEach((match) => {
|
|
690
|
+
if (!matchesById[match.id]) {
|
|
691
|
+
matchesById[match.id] = match
|
|
514
692
|
}
|
|
515
693
|
|
|
516
|
-
|
|
694
|
+
matchesById[match.id] = {
|
|
695
|
+
...matchesById[match.id],
|
|
696
|
+
...match,
|
|
697
|
+
}
|
|
698
|
+
})
|
|
517
699
|
|
|
518
|
-
|
|
519
|
-
|
|
700
|
+
return matchesById
|
|
701
|
+
}
|
|
520
702
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
staying.push(d)
|
|
524
|
-
} else {
|
|
525
|
-
exiting.push(d)
|
|
526
|
-
}
|
|
527
|
-
})
|
|
703
|
+
getRoute = (id: string): Route => {
|
|
704
|
+
const route = (this.routesById as any)[id]
|
|
528
705
|
|
|
529
|
-
|
|
706
|
+
invariant(route, `Route with id "${id as string}" not found`)
|
|
530
707
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
708
|
+
return route as any
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
preloadRoute = async (
|
|
712
|
+
navigateOpts: BuildNextOptions & {
|
|
713
|
+
maxAge?: number
|
|
714
|
+
} = this.state.location,
|
|
715
|
+
) => {
|
|
716
|
+
let next = this.buildLocation(navigateOpts)
|
|
717
|
+
|
|
718
|
+
const matches = this.matchRoutes(next.pathname, next.search, {
|
|
719
|
+
throwOnError: true,
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
this.__store.setState((s) => {
|
|
723
|
+
return {
|
|
724
|
+
...s,
|
|
725
|
+
matchesById: this.#mergeMatches(s.matchesById, matches),
|
|
726
|
+
}
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
await this.loadMatches(
|
|
730
|
+
matches.map((d) => d.id),
|
|
731
|
+
{
|
|
732
|
+
preload: true,
|
|
733
|
+
maxAge: navigateOpts.maxAge,
|
|
734
|
+
},
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
return [last(matches)!, matches] as const
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
cleanMatches = () => {
|
|
741
|
+
const now = Date.now()
|
|
742
|
+
|
|
743
|
+
const outdatedMatchIds = Object.values(this.state.matchesById)
|
|
744
|
+
.filter((match) => {
|
|
745
|
+
const route = this.getRoute(match.routeId)
|
|
746
|
+
|
|
747
|
+
return (
|
|
748
|
+
!this.state.matchIds.includes(match.id) &&
|
|
749
|
+
!this.state.pendingMatchIds.includes(match.id) &&
|
|
750
|
+
(match.preloadMaxAge > -1
|
|
751
|
+
? match.updatedAt + match.preloadMaxAge < now
|
|
752
|
+
: true) &&
|
|
753
|
+
(route.options.gcMaxAge
|
|
754
|
+
? match.updatedAt + route.options.gcMaxAge < now
|
|
755
|
+
: true)
|
|
544
756
|
)
|
|
545
|
-
if (gc > 0) {
|
|
546
|
-
router.matchCache[d.matchId] = {
|
|
547
|
-
gc: gc == Infinity ? Number.MAX_SAFE_INTEGER : now + gc,
|
|
548
|
-
match: d,
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
757
|
})
|
|
758
|
+
.map((d) => d.id)
|
|
552
759
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
760
|
+
if (outdatedMatchIds.length) {
|
|
761
|
+
this.__store.setState((s) => {
|
|
762
|
+
const matchesById = { ...s.matchesById }
|
|
763
|
+
outdatedMatchIds.forEach((id) => {
|
|
764
|
+
delete matchesById[id]
|
|
557
765
|
})
|
|
766
|
+
return {
|
|
767
|
+
...s,
|
|
768
|
+
matchesById,
|
|
769
|
+
}
|
|
558
770
|
})
|
|
771
|
+
}
|
|
772
|
+
}
|
|
559
773
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
774
|
+
matchRoutes = (
|
|
775
|
+
pathname: string,
|
|
776
|
+
locationSearch: AnySearchSchema,
|
|
777
|
+
opts?: { throwOnError?: boolean; debug?: boolean },
|
|
778
|
+
): RouteMatch<TRouteTree>[] => {
|
|
779
|
+
let routeParams: AnyPathParams = {}
|
|
780
|
+
|
|
781
|
+
let foundRoute = this.flatRoutes.find((route) => {
|
|
782
|
+
const matchedParams = matchPathname(
|
|
783
|
+
this.basepath,
|
|
784
|
+
trimPathRight(pathname),
|
|
785
|
+
{
|
|
786
|
+
to: route.fullPath,
|
|
787
|
+
caseSensitive:
|
|
788
|
+
route.options.caseSensitive ?? this.options.caseSensitive,
|
|
789
|
+
},
|
|
790
|
+
)
|
|
563
791
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
})
|
|
569
|
-
delete router.matchCache[d.matchId]
|
|
570
|
-
})
|
|
792
|
+
if (matchedParams) {
|
|
793
|
+
routeParams = matchedParams
|
|
794
|
+
return true
|
|
795
|
+
}
|
|
571
796
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
797
|
+
return false
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
let routeCursor: AnyRoute =
|
|
801
|
+
foundRoute || (this.routesById as any)['__root__']
|
|
802
|
+
|
|
803
|
+
let matchedRoutes: AnyRoute[] = [routeCursor]
|
|
804
|
+
// let includingLayouts = true
|
|
805
|
+
while (routeCursor?.parentRoute) {
|
|
806
|
+
routeCursor = routeCursor.parentRoute
|
|
807
|
+
if (routeCursor) matchedRoutes.unshift(routeCursor)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Existing matches are matches that are already loaded along with
|
|
811
|
+
// pending matches that are still loading
|
|
812
|
+
|
|
813
|
+
const parseErrors = matchedRoutes.map((route) => {
|
|
814
|
+
let parsedParamsError
|
|
815
|
+
|
|
816
|
+
if (route.options.parseParams) {
|
|
817
|
+
try {
|
|
818
|
+
const parsedParams = route.options.parseParams(routeParams)
|
|
819
|
+
// Add the parsed params to the accumulated params bag
|
|
820
|
+
Object.assign(routeParams, parsedParams)
|
|
821
|
+
} catch (err: any) {
|
|
822
|
+
parsedParamsError = new PathParamError(err.message, {
|
|
823
|
+
cause: err,
|
|
824
|
+
})
|
|
825
|
+
|
|
826
|
+
if (opts?.throwOnError) {
|
|
827
|
+
throw parsedParamsError
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return parsedParamsError
|
|
831
|
+
}
|
|
577
832
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
833
|
+
|
|
834
|
+
return
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
const matches = matchedRoutes.map((route, index) => {
|
|
838
|
+
const interpolatedPath = interpolatePath(route.path, routeParams)
|
|
839
|
+
const loaderContext = route.options.loaderContext
|
|
840
|
+
? route.options.loaderContext({
|
|
841
|
+
search: locationSearch,
|
|
842
|
+
})
|
|
843
|
+
: undefined
|
|
844
|
+
|
|
845
|
+
const matchId = JSON.stringify(
|
|
846
|
+
[interpolatePath(route.id, routeParams, true), loaderContext].filter(
|
|
847
|
+
(d) => d !== undefined,
|
|
848
|
+
),
|
|
849
|
+
(key, value) => {
|
|
850
|
+
if (typeof value === 'function') {
|
|
851
|
+
console.info(route)
|
|
852
|
+
invariant(
|
|
853
|
+
false,
|
|
854
|
+
`Cannot return functions and other non-serializable values from routeOptions.loaderContext! Please use routeOptions.beforeLoad to do this. Route info is logged above 👆`,
|
|
855
|
+
)
|
|
856
|
+
}
|
|
857
|
+
if (typeof value === 'object' && value !== null) {
|
|
858
|
+
return Object.fromEntries(
|
|
859
|
+
Object.keys(value)
|
|
860
|
+
.sort()
|
|
861
|
+
.map((key) => [key, value[key]]),
|
|
862
|
+
)
|
|
863
|
+
}
|
|
864
|
+
return value
|
|
865
|
+
},
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
// Waste not, want not. If we already have a match for this route,
|
|
869
|
+
// reuse it. This is important for layout routes, which might stick
|
|
870
|
+
// around between navigation actions that only change leaf routes.
|
|
871
|
+
const existingMatch = this.getRouteMatch(matchId)
|
|
872
|
+
|
|
873
|
+
if (existingMatch) {
|
|
874
|
+
return { ...existingMatch }
|
|
581
875
|
}
|
|
582
876
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
877
|
+
// Create a fresh route match
|
|
878
|
+
const hasLoaders = !!(
|
|
879
|
+
route.options.loader ||
|
|
880
|
+
componentTypes.some((d) => (route.options[d] as any)?.preload)
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
const routeMatch: AnyRouteMatch = {
|
|
884
|
+
id: matchId,
|
|
885
|
+
loaderContext,
|
|
886
|
+
routeId: route.id,
|
|
887
|
+
params: routeParams,
|
|
888
|
+
pathname: joinPaths([this.basepath, interpolatedPath]),
|
|
889
|
+
updatedAt: Date.now(),
|
|
890
|
+
maxAge: -1,
|
|
891
|
+
preloadMaxAge: -1,
|
|
892
|
+
routeSearch: {},
|
|
893
|
+
search: {} as any,
|
|
894
|
+
status: hasLoaders ? 'pending' : 'success',
|
|
895
|
+
isFetching: false,
|
|
896
|
+
invalid: false,
|
|
897
|
+
error: undefined,
|
|
898
|
+
paramsError: parseErrors[index],
|
|
899
|
+
searchError: undefined,
|
|
900
|
+
loaderData: undefined,
|
|
901
|
+
loadPromise: Promise.resolve(),
|
|
902
|
+
context: undefined!,
|
|
903
|
+
abortController: new AbortController(),
|
|
904
|
+
fetchedAt: 0,
|
|
589
905
|
}
|
|
590
906
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
907
|
+
return routeMatch
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
// Take each match and resolve its search params and context
|
|
911
|
+
// This has to happen after the matches are created or found
|
|
912
|
+
// so that we can use the parent match's search params and context
|
|
913
|
+
matches.forEach((match, i): any => {
|
|
914
|
+
const parentMatch = matches[i - 1]
|
|
915
|
+
const route = this.getRoute(match.routeId)
|
|
916
|
+
|
|
917
|
+
const searchInfo = (() => {
|
|
918
|
+
// Validate the search params and stabilize them
|
|
919
|
+
const parentSearchInfo = {
|
|
920
|
+
search: parentMatch?.search ?? locationSearch,
|
|
921
|
+
routeSearch: parentMatch?.routeSearch ?? locationSearch,
|
|
922
|
+
}
|
|
594
923
|
|
|
595
|
-
|
|
596
|
-
|
|
924
|
+
try {
|
|
925
|
+
const validator =
|
|
926
|
+
typeof route.options.validateSearch === 'object'
|
|
927
|
+
? route.options.validateSearch.parse
|
|
928
|
+
: route.options.validateSearch
|
|
597
929
|
|
|
598
|
-
|
|
599
|
-
const entry = router.matchCache[matchId]!
|
|
930
|
+
let routeSearch = validator?.(parentSearchInfo.search) ?? {}
|
|
600
931
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
932
|
+
let search = {
|
|
933
|
+
...parentSearchInfo.search,
|
|
934
|
+
...routeSearch,
|
|
935
|
+
}
|
|
605
936
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
937
|
+
routeSearch = replaceEqualDeep(match.routeSearch, routeSearch)
|
|
938
|
+
search = replaceEqualDeep(match.search, search)
|
|
939
|
+
|
|
940
|
+
return {
|
|
941
|
+
routeSearch,
|
|
942
|
+
search,
|
|
943
|
+
searchDidChange: match.routeSearch !== routeSearch,
|
|
944
|
+
}
|
|
945
|
+
} catch (err: any) {
|
|
946
|
+
match.searchError = new SearchParamError(err.message, {
|
|
947
|
+
cause: err,
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
if (opts?.throwOnError) {
|
|
951
|
+
throw match.searchError
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return parentSearchInfo
|
|
609
955
|
}
|
|
956
|
+
})()
|
|
610
957
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
})
|
|
614
|
-
},
|
|
958
|
+
Object.assign(match, searchInfo)
|
|
959
|
+
})
|
|
615
960
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const matches = router.matchRoutes(next.pathname, {
|
|
619
|
-
strictParseParams: true,
|
|
620
|
-
})
|
|
621
|
-
await router.loadMatches(matches)
|
|
622
|
-
return matches
|
|
623
|
-
},
|
|
961
|
+
return matches as any
|
|
962
|
+
}
|
|
624
963
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
await router.loadMatches(matches, {
|
|
631
|
-
preload: true,
|
|
632
|
-
maxAge:
|
|
633
|
-
loaderOpts.maxAge ??
|
|
634
|
-
router.options.defaultPreloadMaxAge ??
|
|
635
|
-
router.options.defaultLoaderMaxAge ??
|
|
636
|
-
0,
|
|
637
|
-
gcMaxAge:
|
|
638
|
-
loaderOpts.gcMaxAge ??
|
|
639
|
-
router.options.defaultPreloadGcMaxAge ??
|
|
640
|
-
router.options.defaultLoaderGcMaxAge ??
|
|
641
|
-
0,
|
|
642
|
-
})
|
|
643
|
-
return matches
|
|
964
|
+
loadMatches = async (
|
|
965
|
+
matchIds: string[],
|
|
966
|
+
opts?: {
|
|
967
|
+
preload?: boolean
|
|
968
|
+
maxAge?: number
|
|
644
969
|
},
|
|
970
|
+
) => {
|
|
971
|
+
const getFreshMatches = () => matchIds.map((d) => this.getRouteMatch(d)!)
|
|
972
|
+
|
|
973
|
+
if (!opts?.preload) {
|
|
974
|
+
getFreshMatches().forEach((match) => {
|
|
975
|
+
// Update each match with its latest route data
|
|
976
|
+
this.setRouteMatch(match.id, (s) => ({
|
|
977
|
+
...s,
|
|
978
|
+
routeSearch: match.routeSearch,
|
|
979
|
+
search: match.search,
|
|
980
|
+
context: match.context,
|
|
981
|
+
error: match.error,
|
|
982
|
+
paramsError: match.paramsError,
|
|
983
|
+
searchError: match.searchError,
|
|
984
|
+
params: match.params,
|
|
985
|
+
preloadMaxAge: 0,
|
|
986
|
+
}))
|
|
987
|
+
})
|
|
988
|
+
}
|
|
645
989
|
|
|
646
|
-
|
|
647
|
-
router.cleanMatchCache()
|
|
990
|
+
let firstBadMatchIndex: number | undefined
|
|
648
991
|
|
|
649
|
-
|
|
992
|
+
// Check each match middleware to see if the route can be accessed
|
|
993
|
+
try {
|
|
994
|
+
for (const [index, match] of getFreshMatches().entries()) {
|
|
995
|
+
const parentMatch = getFreshMatches()[index - 1]
|
|
996
|
+
const route = this.getRoute(match.routeId)
|
|
650
997
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
998
|
+
const handleError = (err: any, code: string) => {
|
|
999
|
+
err.routerCode = code
|
|
1000
|
+
firstBadMatchIndex = firstBadMatchIndex ?? index
|
|
654
1001
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
]
|
|
1002
|
+
if (isRedirect(err)) {
|
|
1003
|
+
throw err
|
|
1004
|
+
}
|
|
659
1005
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1006
|
+
try {
|
|
1007
|
+
route.options.onError?.(err)
|
|
1008
|
+
} catch (errorHandlerErr) {
|
|
1009
|
+
err = errorHandlerErr
|
|
663
1010
|
|
|
664
|
-
|
|
1011
|
+
if (isRedirect(errorHandlerErr)) {
|
|
1012
|
+
throw errorHandlerErr
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
665
1015
|
|
|
666
|
-
|
|
1016
|
+
this.setRouteMatch(match.id, (s) => ({
|
|
1017
|
+
...s,
|
|
1018
|
+
error: err,
|
|
1019
|
+
status: 'error',
|
|
1020
|
+
updatedAt: Date.now(),
|
|
1021
|
+
}))
|
|
1022
|
+
}
|
|
667
1023
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1024
|
+
if (match.paramsError) {
|
|
1025
|
+
handleError(match.paramsError, 'PARSE_PARAMS')
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (match.searchError) {
|
|
1029
|
+
handleError(match.searchError, 'VALIDATE_SEARCH')
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
let didError = false
|
|
1033
|
+
|
|
1034
|
+
const parentContext =
|
|
1035
|
+
parentMatch?.context ?? this?.options.context ?? {}
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
const beforeLoadContext =
|
|
1039
|
+
(await route.options.beforeLoad?.({
|
|
1040
|
+
abortController: match.abortController,
|
|
1041
|
+
params: match.params,
|
|
1042
|
+
preload: !!opts?.preload,
|
|
1043
|
+
context: {
|
|
1044
|
+
...parentContext,
|
|
1045
|
+
...match.loaderContext,
|
|
1046
|
+
},
|
|
1047
|
+
})) ?? ({} as any)
|
|
1048
|
+
|
|
1049
|
+
const context = {
|
|
1050
|
+
...parentContext,
|
|
1051
|
+
...match.loaderContext,
|
|
1052
|
+
...beforeLoadContext,
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
this.setRouteMatch(match.id, (s) => ({
|
|
1056
|
+
...s,
|
|
1057
|
+
context: replaceEqualDeep(s.context, context),
|
|
1058
|
+
}))
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
handleError(err, 'BEFORE_LOAD')
|
|
1061
|
+
didError = true
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// If we errored, do not run the next matches' middleware
|
|
1065
|
+
if (didError) {
|
|
1066
|
+
break
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
if (isRedirect(err)) {
|
|
1071
|
+
if (!opts?.preload) this.navigate(err as any)
|
|
1072
|
+
return
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
throw err
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const validResolvedMatches = getFreshMatches().slice(0, firstBadMatchIndex)
|
|
1079
|
+
const matchPromises: Promise<any>[] = []
|
|
1080
|
+
|
|
1081
|
+
validResolvedMatches.forEach((match, index) => {
|
|
1082
|
+
matchPromises.push(
|
|
1083
|
+
(async () => {
|
|
1084
|
+
const parentMatchPromise = matchPromises[index - 1]
|
|
1085
|
+
const route = this.getRoute(match.routeId)
|
|
1086
|
+
|
|
1087
|
+
if (
|
|
1088
|
+
match.isFetching ||
|
|
1089
|
+
(match.status === 'success' &&
|
|
1090
|
+
!isMatchInvalid(match, {
|
|
1091
|
+
preload: opts?.preload,
|
|
1092
|
+
}))
|
|
1093
|
+
) {
|
|
1094
|
+
return this.getRouteMatch(match.id)?.loadPromise
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const fetchedAt = Date.now()
|
|
1098
|
+
const checkLatest = () => {
|
|
1099
|
+
const latest = this.getRouteMatch(match.id)
|
|
1100
|
+
return latest && latest.fetchedAt !== fetchedAt
|
|
1101
|
+
? latest.loadPromise
|
|
1102
|
+
: undefined
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const handleIfRedirect = (err: any) => {
|
|
1106
|
+
if (isRedirect(err)) {
|
|
1107
|
+
if (!opts?.preload) {
|
|
1108
|
+
this.navigate(err as any)
|
|
1109
|
+
}
|
|
1110
|
+
return true
|
|
675
1111
|
}
|
|
1112
|
+
return false
|
|
1113
|
+
}
|
|
676
1114
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
)
|
|
1115
|
+
const load = async () => {
|
|
1116
|
+
let latestPromise
|
|
680
1117
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1118
|
+
try {
|
|
1119
|
+
const componentsPromise = Promise.all(
|
|
1120
|
+
componentTypes.map(async (type) => {
|
|
1121
|
+
const component = route.options[type]
|
|
1122
|
+
|
|
1123
|
+
if ((component as any)?.preload) {
|
|
1124
|
+
await (component as any).preload()
|
|
1125
|
+
}
|
|
1126
|
+
}),
|
|
1127
|
+
)
|
|
687
1128
|
|
|
688
|
-
|
|
689
|
-
|
|
1129
|
+
const loaderPromise = route.options.loader?.({
|
|
1130
|
+
params: match.params,
|
|
1131
|
+
preload: !!opts?.preload,
|
|
1132
|
+
parentMatchPromise,
|
|
1133
|
+
abortController: match.abortController,
|
|
1134
|
+
context: match.context,
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
const [_, loader] = await Promise.all([
|
|
1138
|
+
componentsPromise,
|
|
1139
|
+
loaderPromise,
|
|
1140
|
+
])
|
|
1141
|
+
if ((latestPromise = checkLatest())) return await latestPromise
|
|
1142
|
+
|
|
1143
|
+
this.setRouteMatchData(match.id, () => loader, opts)
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if ((latestPromise = checkLatest())) return await latestPromise
|
|
1146
|
+
if (handleIfRedirect(error)) return
|
|
690
1147
|
|
|
691
1148
|
try {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (
|
|
696
|
-
throw err
|
|
697
|
-
}
|
|
1149
|
+
route.options.onError?.(error)
|
|
1150
|
+
} catch (onErrorError) {
|
|
1151
|
+
error = onErrorError
|
|
1152
|
+
if (handleIfRedirect(onErrorError)) return
|
|
698
1153
|
}
|
|
699
1154
|
|
|
700
|
-
|
|
701
|
-
...
|
|
702
|
-
|
|
703
|
-
|
|
1155
|
+
this.setRouteMatch(match.id, (s) => ({
|
|
1156
|
+
...s,
|
|
1157
|
+
error,
|
|
1158
|
+
status: 'error',
|
|
1159
|
+
isFetching: false,
|
|
1160
|
+
updatedAt: Date.now(),
|
|
1161
|
+
}))
|
|
704
1162
|
}
|
|
1163
|
+
}
|
|
705
1164
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1165
|
+
let loadPromise: Promise<void> | undefined
|
|
1166
|
+
|
|
1167
|
+
this.__store.batch(() => {
|
|
1168
|
+
this.setRouteMatch(match.id, (s) => ({
|
|
1169
|
+
...s,
|
|
1170
|
+
// status: s.status !== 'success' ? 'pending' : s.status,
|
|
1171
|
+
isFetching: true,
|
|
1172
|
+
fetchedAt,
|
|
1173
|
+
invalid: false,
|
|
1174
|
+
}))
|
|
709
1175
|
|
|
710
|
-
|
|
1176
|
+
loadPromise = load()
|
|
1177
|
+
|
|
1178
|
+
this.setRouteMatch(match.id, (s) => ({
|
|
1179
|
+
...s,
|
|
1180
|
+
loadPromise,
|
|
1181
|
+
}))
|
|
711
1182
|
})
|
|
712
1183
|
|
|
713
|
-
|
|
714
|
-
}
|
|
1184
|
+
await loadPromise
|
|
1185
|
+
})(),
|
|
1186
|
+
)
|
|
1187
|
+
})
|
|
715
1188
|
|
|
716
|
-
|
|
1189
|
+
await Promise.all(matchPromises)
|
|
1190
|
+
}
|
|
717
1191
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1192
|
+
resolvePath = (from: string, path: string) => {
|
|
1193
|
+
return resolvePath(this.basepath!, from, cleanPath(path))
|
|
1194
|
+
}
|
|
721
1195
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1196
|
+
navigate = async <
|
|
1197
|
+
TFrom extends RoutePaths<TRouteTree> = '/',
|
|
1198
|
+
TTo extends string = '',
|
|
1199
|
+
TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
|
|
1200
|
+
TMaskTo extends string = '',
|
|
1201
|
+
>({
|
|
1202
|
+
from,
|
|
1203
|
+
to = '' as any,
|
|
1204
|
+
...rest
|
|
1205
|
+
}: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>) => {
|
|
1206
|
+
// If this link simply reloads the current route,
|
|
1207
|
+
// make sure it has a new key so it will trigger a data refresh
|
|
1208
|
+
|
|
1209
|
+
// If this `to` is a valid external URL, return
|
|
1210
|
+
// null for LinkUtils
|
|
1211
|
+
const toString = String(to)
|
|
1212
|
+
const fromString = typeof from === 'undefined' ? from : String(from)
|
|
1213
|
+
let isExternal
|
|
1214
|
+
|
|
1215
|
+
try {
|
|
1216
|
+
new URL(`${toString}`)
|
|
1217
|
+
isExternal = true
|
|
1218
|
+
} catch (e) {}
|
|
1219
|
+
|
|
1220
|
+
invariant(
|
|
1221
|
+
!isExternal,
|
|
1222
|
+
'Attempting to navigate to external url with this.navigate!',
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
return this.#buildAndCommitLocation({
|
|
1226
|
+
...rest,
|
|
1227
|
+
from: fromString,
|
|
1228
|
+
to: toString,
|
|
1229
|
+
})
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
matchRoute = <
|
|
1233
|
+
TRouteTree extends AnyRoute = AnyRoute,
|
|
1234
|
+
TFrom extends RoutePaths<TRouteTree> = '/',
|
|
1235
|
+
TTo extends string = '',
|
|
1236
|
+
TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
|
|
1237
|
+
>(
|
|
1238
|
+
location: ToOptions<TRouteTree, TFrom, TTo>,
|
|
1239
|
+
opts?: MatchRouteOptions,
|
|
1240
|
+
): false | RouteById<TRouteTree, TResolved>['types']['allParams'] => {
|
|
1241
|
+
location = {
|
|
1242
|
+
...location,
|
|
1243
|
+
to: location.to
|
|
1244
|
+
? this.resolvePath(location.from ?? '', location.to)
|
|
1245
|
+
: undefined,
|
|
1246
|
+
} as any
|
|
1247
|
+
|
|
1248
|
+
const next = this.buildLocation(location)
|
|
1249
|
+
if (opts?.pending && this.state.status !== 'pending') {
|
|
1250
|
+
return false
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const baseLocation = opts?.pending
|
|
1254
|
+
? this.state.location
|
|
1255
|
+
: this.state.resolvedLocation
|
|
1256
|
+
|
|
1257
|
+
if (!baseLocation) {
|
|
1258
|
+
return false
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const match = matchPathname(this.basepath, baseLocation.pathname, {
|
|
1262
|
+
...opts,
|
|
1263
|
+
to: next.pathname,
|
|
1264
|
+
}) as any
|
|
1265
|
+
|
|
1266
|
+
if (!match) {
|
|
1267
|
+
return false
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (opts?.includeSearch ?? true) {
|
|
1271
|
+
return partialDeepEqual(baseLocation.search, next.search) ? match : false
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
return match
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
buildLink = <
|
|
1278
|
+
TFrom extends RoutePaths<TRouteTree> = '/',
|
|
1279
|
+
TTo extends string = '',
|
|
1280
|
+
>(
|
|
1281
|
+
dest: LinkOptions<TRouteTree, TFrom, TTo>,
|
|
1282
|
+
): LinkInfo => {
|
|
1283
|
+
// If this link simply reloads the current route,
|
|
1284
|
+
// make sure it has a new key so it will trigger a data refresh
|
|
725
1285
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
router.matchCache[matchId]?.match ||
|
|
729
|
-
createRouteMatch(router, foundRoute, {
|
|
730
|
-
matchId,
|
|
731
|
-
params,
|
|
732
|
-
pathname: joinPaths([pathname, interpolatedPath]),
|
|
733
|
-
})
|
|
1286
|
+
// If this `to` is a valid external URL, return
|
|
1287
|
+
// null for LinkUtils
|
|
734
1288
|
|
|
735
|
-
|
|
1289
|
+
const {
|
|
1290
|
+
to,
|
|
1291
|
+
preload: userPreload,
|
|
1292
|
+
preloadDelay: userPreloadDelay,
|
|
1293
|
+
activeOptions,
|
|
1294
|
+
disabled,
|
|
1295
|
+
target,
|
|
1296
|
+
replace,
|
|
1297
|
+
resetScroll,
|
|
1298
|
+
} = dest
|
|
1299
|
+
|
|
1300
|
+
try {
|
|
1301
|
+
new URL(`${to}`)
|
|
1302
|
+
return {
|
|
1303
|
+
type: 'external',
|
|
1304
|
+
href: to as any,
|
|
1305
|
+
}
|
|
1306
|
+
} catch (e) {}
|
|
1307
|
+
|
|
1308
|
+
const nextOpts = dest
|
|
1309
|
+
|
|
1310
|
+
const next = this.buildLocation(nextOpts)
|
|
1311
|
+
|
|
1312
|
+
const preload = userPreload ?? this.options.defaultPreload
|
|
1313
|
+
const preloadDelay =
|
|
1314
|
+
userPreloadDelay ?? this.options.defaultPreloadDelay ?? 0
|
|
1315
|
+
|
|
1316
|
+
// Compare path/hash for matches
|
|
1317
|
+
const currentPathSplit = this.state.location.pathname.split('/')
|
|
1318
|
+
const nextPathSplit = next.pathname.split('/')
|
|
1319
|
+
const pathIsFuzzyEqual = nextPathSplit.every(
|
|
1320
|
+
(d, i) => d === currentPathSplit[i],
|
|
1321
|
+
)
|
|
1322
|
+
// Combine the matches based on user options
|
|
1323
|
+
const pathTest = activeOptions?.exact
|
|
1324
|
+
? this.state.location.pathname === next.pathname
|
|
1325
|
+
: pathIsFuzzyEqual
|
|
1326
|
+
const hashTest = activeOptions?.includeHash
|
|
1327
|
+
? this.state.location.hash === next.hash
|
|
1328
|
+
: true
|
|
1329
|
+
const searchTest =
|
|
1330
|
+
activeOptions?.includeSearch ?? true
|
|
1331
|
+
? partialDeepEqual(this.state.location.search, next.search)
|
|
1332
|
+
: true
|
|
1333
|
+
|
|
1334
|
+
// The final "active" test
|
|
1335
|
+
const isActive = pathTest && hashTest && searchTest
|
|
1336
|
+
|
|
1337
|
+
// The click handler
|
|
1338
|
+
const handleClick = (e: MouseEvent) => {
|
|
1339
|
+
if (
|
|
1340
|
+
!disabled &&
|
|
1341
|
+
!isCtrlEvent(e) &&
|
|
1342
|
+
!e.defaultPrevented &&
|
|
1343
|
+
(!target || target === '_self') &&
|
|
1344
|
+
e.button === 0
|
|
1345
|
+
) {
|
|
1346
|
+
e.preventDefault()
|
|
1347
|
+
|
|
1348
|
+
// All is well? Navigate!
|
|
1349
|
+
this.#commitLocation({ ...next, replace, resetScroll })
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// The click handler
|
|
1354
|
+
const handleFocus = (e: MouseEvent) => {
|
|
1355
|
+
if (preload) {
|
|
1356
|
+
this.preloadRoute(nextOpts).catch((err) => {
|
|
1357
|
+
console.warn(err)
|
|
1358
|
+
console.warn(preloadWarning)
|
|
736
1359
|
})
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const handleTouchStart = (e: TouchEvent) => {
|
|
1364
|
+
this.preloadRoute(nextOpts).catch((err) => {
|
|
1365
|
+
console.warn(err)
|
|
1366
|
+
console.warn(preloadWarning)
|
|
1367
|
+
})
|
|
1368
|
+
}
|
|
737
1369
|
|
|
738
|
-
|
|
1370
|
+
const handleEnter = (e: MouseEvent) => {
|
|
1371
|
+
const target = (e.target || {}) as LinkCurrentTargetElement
|
|
739
1372
|
|
|
740
|
-
|
|
741
|
-
|
|
1373
|
+
if (preload) {
|
|
1374
|
+
if (target.preloadTimeout) {
|
|
1375
|
+
return
|
|
742
1376
|
}
|
|
1377
|
+
|
|
1378
|
+
target.preloadTimeout = setTimeout(() => {
|
|
1379
|
+
target.preloadTimeout = null
|
|
1380
|
+
this.preloadRoute(nextOpts).catch((err) => {
|
|
1381
|
+
console.warn(err)
|
|
1382
|
+
console.warn(preloadWarning)
|
|
1383
|
+
})
|
|
1384
|
+
}, preloadDelay)
|
|
743
1385
|
}
|
|
1386
|
+
}
|
|
744
1387
|
|
|
745
|
-
|
|
1388
|
+
const handleLeave = (e: MouseEvent) => {
|
|
1389
|
+
const target = (e.target || {}) as LinkCurrentTargetElement
|
|
746
1390
|
|
|
747
|
-
|
|
1391
|
+
if (target.preloadTimeout) {
|
|
1392
|
+
clearTimeout(target.preloadTimeout)
|
|
1393
|
+
target.preloadTimeout = null
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
return {
|
|
1398
|
+
type: 'internal',
|
|
1399
|
+
next,
|
|
1400
|
+
handleFocus,
|
|
1401
|
+
handleClick,
|
|
1402
|
+
handleEnter,
|
|
1403
|
+
handleLeave,
|
|
1404
|
+
handleTouchStart,
|
|
1405
|
+
isActive,
|
|
1406
|
+
disabled,
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
748
1409
|
|
|
749
|
-
|
|
750
|
-
|
|
1410
|
+
dehydrate = (): DehydratedRouter => {
|
|
1411
|
+
return {
|
|
1412
|
+
state: {
|
|
1413
|
+
matchIds: this.state.matchIds,
|
|
1414
|
+
dehydratedMatches: this.state.matches.map((d) =>
|
|
1415
|
+
pick(d, [
|
|
1416
|
+
'fetchedAt',
|
|
1417
|
+
'invalid',
|
|
1418
|
+
'preloadMaxAge',
|
|
1419
|
+
'maxAge',
|
|
1420
|
+
'id',
|
|
1421
|
+
'loaderData',
|
|
1422
|
+
'status',
|
|
1423
|
+
'updatedAt',
|
|
1424
|
+
]),
|
|
1425
|
+
),
|
|
1426
|
+
},
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
751
1429
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1430
|
+
hydrate = async (__do_not_use_server_ctx?: HydrationCtx) => {
|
|
1431
|
+
let _ctx = __do_not_use_server_ctx
|
|
1432
|
+
// Client hydrates from window
|
|
1433
|
+
if (typeof document !== 'undefined') {
|
|
1434
|
+
_ctx = window.__TSR_DEHYDRATED__
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
invariant(
|
|
1438
|
+
_ctx,
|
|
1439
|
+
'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
const ctx = _ctx
|
|
1443
|
+
this.dehydratedData = ctx.payload as any
|
|
1444
|
+
this.options.hydrate?.(ctx.payload as any)
|
|
1445
|
+
const dehydratedState = ctx.router.state
|
|
1446
|
+
|
|
1447
|
+
let matches = this.matchRoutes(
|
|
1448
|
+
this.state.location.pathname,
|
|
1449
|
+
this.state.location.search,
|
|
1450
|
+
).map((match) => {
|
|
1451
|
+
const dehydratedMatch = dehydratedState.dehydratedMatches.find(
|
|
1452
|
+
(d) => d.id === match.id,
|
|
1453
|
+
)
|
|
757
1454
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1455
|
+
invariant(
|
|
1456
|
+
dehydratedMatch,
|
|
1457
|
+
`Could not find a client-side match for dehydrated match with id: ${match.id}!`,
|
|
1458
|
+
)
|
|
761
1459
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1460
|
+
if (dehydratedMatch) {
|
|
1461
|
+
return {
|
|
1462
|
+
...match,
|
|
1463
|
+
...dehydratedMatch,
|
|
765
1464
|
}
|
|
766
|
-
}
|
|
1465
|
+
}
|
|
1466
|
+
return match
|
|
1467
|
+
})
|
|
767
1468
|
|
|
768
|
-
|
|
1469
|
+
this.__store.setState((s) => {
|
|
1470
|
+
return {
|
|
1471
|
+
...s,
|
|
1472
|
+
matchIds: dehydratedState.matchIds,
|
|
1473
|
+
matches,
|
|
1474
|
+
matchesById: this.#mergeMatches(s.matchesById, matches),
|
|
1475
|
+
}
|
|
1476
|
+
})
|
|
1477
|
+
}
|
|
769
1478
|
|
|
770
|
-
|
|
771
|
-
},
|
|
1479
|
+
injectedHtml: (string | (() => Promise<string> | string))[] = []
|
|
772
1480
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
.matchRoutes(next.pathname)
|
|
777
|
-
.map((d) => d.matchId)
|
|
778
|
-
;[
|
|
779
|
-
...router.state.matches,
|
|
780
|
-
...(router.state.pending?.matches ?? []),
|
|
781
|
-
].forEach((match) => {
|
|
782
|
-
if (unloadedMatchIds.includes(match.matchId)) {
|
|
783
|
-
match.invalidate()
|
|
784
|
-
}
|
|
785
|
-
})
|
|
786
|
-
},
|
|
1481
|
+
injectHtml = async (html: string | (() => Promise<string> | string)) => {
|
|
1482
|
+
this.injectedHtml.push(html)
|
|
1483
|
+
}
|
|
787
1484
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1485
|
+
dehydrateData = <T>(key: any, getData: T | (() => Promise<T> | T)) => {
|
|
1486
|
+
if (typeof document === 'undefined') {
|
|
1487
|
+
const strKey = typeof key === 'string' ? key : JSON.stringify(key)
|
|
1488
|
+
|
|
1489
|
+
this.injectHtml(async () => {
|
|
1490
|
+
const id = `__TSR_DEHYDRATED__${strKey}`
|
|
1491
|
+
const data =
|
|
1492
|
+
typeof getData === 'function' ? await (getData as any)() : getData
|
|
1493
|
+
return `<script id='${id}' suppressHydrationWarning>window["__TSR_DEHYDRATED__${escapeJSON(
|
|
1494
|
+
strKey,
|
|
1495
|
+
)}"] = ${JSON.stringify(data)}
|
|
1496
|
+
;(() => {
|
|
1497
|
+
var el = document.getElementById('${id}')
|
|
1498
|
+
el.parentElement.removeChild(el)
|
|
1499
|
+
})()
|
|
1500
|
+
</script>`
|
|
1501
|
+
})
|
|
794
1502
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
},
|
|
1503
|
+
return () => this.hydrateData<T>(key)
|
|
1504
|
+
}
|
|
798
1505
|
|
|
799
|
-
|
|
800
|
-
|
|
1506
|
+
return () => undefined
|
|
1507
|
+
}
|
|
801
1508
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
? router.resolvePath(location.from ?? '', location.to)
|
|
806
|
-
: undefined,
|
|
807
|
-
}
|
|
1509
|
+
hydrateData = <T = unknown>(key: any) => {
|
|
1510
|
+
if (typeof document !== 'undefined') {
|
|
1511
|
+
const strKey = typeof key === 'string' ? key : JSON.stringify(key)
|
|
808
1512
|
|
|
809
|
-
|
|
1513
|
+
return window[`__TSR_DEHYDRATED__${strKey}` as any] as T
|
|
1514
|
+
}
|
|
810
1515
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
return false
|
|
814
|
-
}
|
|
815
|
-
return !!matchPathname(router.state.pending.location.pathname, {
|
|
816
|
-
...opts,
|
|
817
|
-
to: next.pathname,
|
|
818
|
-
})
|
|
819
|
-
}
|
|
1516
|
+
return undefined
|
|
1517
|
+
}
|
|
820
1518
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1519
|
+
// resolveMatchPromise = (matchId: string, key: string, value: any) => {
|
|
1520
|
+
// this.state.matches
|
|
1521
|
+
// .find((d) => d.id === matchId)
|
|
1522
|
+
// ?.__promisesByKey[key]?.resolve(value)
|
|
1523
|
+
// }
|
|
826
1524
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1525
|
+
#processRoutes = (routeTree: TRouteTree) => {
|
|
1526
|
+
this.routeTree = routeTree as any
|
|
1527
|
+
this.routesById = {} as any
|
|
1528
|
+
this.routesByPath = {} as any
|
|
1529
|
+
this.flatRoutes = [] as any
|
|
830
1530
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
const fromString = String(from)
|
|
1531
|
+
const recurseRoutes = (routes: AnyRoute[]) => {
|
|
1532
|
+
routes.forEach((route, i) => {
|
|
1533
|
+
route.init({ originalIndex: i, router: this })
|
|
835
1534
|
|
|
836
|
-
|
|
1535
|
+
const existingRoute = (this.routesById as any)[route.id]
|
|
837
1536
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1537
|
+
invariant(
|
|
1538
|
+
!existingRoute,
|
|
1539
|
+
`Duplicate routes found with id: ${String(route.id)}`,
|
|
1540
|
+
)
|
|
1541
|
+
;(this.routesById as any)[route.id] = route
|
|
1542
|
+
|
|
1543
|
+
if (!route.isRoot && route.path) {
|
|
1544
|
+
const trimmedFullPath = trimPathRight(route.fullPath)
|
|
1545
|
+
if (
|
|
1546
|
+
!(this.routesByPath as any)[trimmedFullPath] ||
|
|
1547
|
+
route.fullPath.endsWith('/')
|
|
1548
|
+
) {
|
|
1549
|
+
;(this.routesByPath as any)[trimmedFullPath] = route
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
842
1552
|
|
|
843
|
-
|
|
844
|
-
!isExternal,
|
|
845
|
-
'Attempting to navigate to external url with router.navigate!',
|
|
846
|
-
)
|
|
1553
|
+
const children = route.children as Route[]
|
|
847
1554
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
search,
|
|
852
|
-
hash,
|
|
853
|
-
replace,
|
|
854
|
-
params,
|
|
1555
|
+
if (children?.length) {
|
|
1556
|
+
recurseRoutes(children)
|
|
1557
|
+
}
|
|
855
1558
|
})
|
|
856
|
-
}
|
|
1559
|
+
}
|
|
857
1560
|
|
|
858
|
-
|
|
859
|
-
from,
|
|
860
|
-
to = '.',
|
|
861
|
-
search,
|
|
862
|
-
params,
|
|
863
|
-
hash,
|
|
864
|
-
target,
|
|
865
|
-
replace,
|
|
866
|
-
activeOptions,
|
|
867
|
-
preload,
|
|
868
|
-
preloadMaxAge: userPreloadMaxAge,
|
|
869
|
-
preloadGcMaxAge: userPreloadGcMaxAge,
|
|
870
|
-
preloadDelay: userPreloadDelay,
|
|
871
|
-
disabled,
|
|
872
|
-
}) => {
|
|
873
|
-
// If this link simply reloads the current route,
|
|
874
|
-
// make sure it has a new key so it will trigger a data refresh
|
|
1561
|
+
recurseRoutes([routeTree])
|
|
875
1562
|
|
|
876
|
-
|
|
877
|
-
|
|
1563
|
+
this.flatRoutes = (Object.values(this.routesByPath) as AnyRoute[])
|
|
1564
|
+
.map((d, i) => {
|
|
1565
|
+
const trimmed = trimPath(d.fullPath)
|
|
1566
|
+
const parsed = parsePathname(trimmed)
|
|
878
1567
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
return {
|
|
882
|
-
type: 'external',
|
|
883
|
-
href: to,
|
|
1568
|
+
while (parsed.length > 1 && parsed[0]?.value === '/') {
|
|
1569
|
+
parsed.shift()
|
|
884
1570
|
}
|
|
885
|
-
} catch (e) {}
|
|
886
1571
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
params,
|
|
892
|
-
hash,
|
|
893
|
-
replace,
|
|
894
|
-
}
|
|
1572
|
+
const score = parsed.map((d) => {
|
|
1573
|
+
if (d.type === 'param') {
|
|
1574
|
+
return 0.5
|
|
1575
|
+
}
|
|
895
1576
|
|
|
896
|
-
|
|
1577
|
+
if (d.type === 'wildcard') {
|
|
1578
|
+
return 0.25
|
|
1579
|
+
}
|
|
897
1580
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0
|
|
1581
|
+
return 1
|
|
1582
|
+
})
|
|
901
1583
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const pathIsFuzzyEqual = nextPathSplit.every(
|
|
907
|
-
(d, i) => d === currentPathSplit[i],
|
|
908
|
-
)
|
|
909
|
-
const hashIsEqual = router.state.location.hash === next.hash
|
|
910
|
-
// Combine the matches based on user options
|
|
911
|
-
const pathTest = activeOptions?.exact ? pathIsEqual : pathIsFuzzyEqual
|
|
912
|
-
const hashTest = activeOptions?.includeHash ? hashIsEqual : true
|
|
913
|
-
|
|
914
|
-
// The final "active" test
|
|
915
|
-
const isActive = pathTest && hashTest
|
|
916
|
-
|
|
917
|
-
// The click handler
|
|
918
|
-
const handleClick = (e: MouseEvent) => {
|
|
919
|
-
if (
|
|
920
|
-
!disabled &&
|
|
921
|
-
!isCtrlEvent(e) &&
|
|
922
|
-
!e.defaultPrevented &&
|
|
923
|
-
(!target || target === '_self') &&
|
|
924
|
-
e.button === 0
|
|
925
|
-
) {
|
|
926
|
-
e.preventDefault()
|
|
927
|
-
if (pathIsEqual && !search && !hash) {
|
|
928
|
-
router.invalidateRoute(nextOpts)
|
|
929
|
-
}
|
|
1584
|
+
return { child: d, trimmed, parsed, index: i, score }
|
|
1585
|
+
})
|
|
1586
|
+
.sort((a, b) => {
|
|
1587
|
+
let isIndex = a.trimmed === '/' ? 1 : b.trimmed === '/' ? -1 : 0
|
|
930
1588
|
|
|
931
|
-
|
|
932
|
-
router.__.navigate(nextOpts)
|
|
933
|
-
}
|
|
934
|
-
}
|
|
1589
|
+
if (isIndex !== 0) return isIndex
|
|
935
1590
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
gcMaxAge: userPreloadGcMaxAge,
|
|
942
|
-
})
|
|
1591
|
+
const length = Math.min(a.score.length, b.score.length)
|
|
1592
|
+
|
|
1593
|
+
// Sort by length of score
|
|
1594
|
+
if (a.score.length !== b.score.length) {
|
|
1595
|
+
return b.score.length - a.score.length
|
|
943
1596
|
}
|
|
944
|
-
}
|
|
945
1597
|
|
|
946
|
-
|
|
947
|
-
|
|
1598
|
+
// Sort by min available score
|
|
1599
|
+
for (let i = 0; i < length; i++) {
|
|
1600
|
+
if (a.score[i] !== b.score[i]) {
|
|
1601
|
+
return b.score[i]! - a.score[i]!
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
948
1604
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1605
|
+
// Sort by min available parsed value
|
|
1606
|
+
for (let i = 0; i < length; i++) {
|
|
1607
|
+
if (a.parsed[i]!.value !== b.parsed[i]!.value) {
|
|
1608
|
+
return a.parsed[i]!.value! > b.parsed[i]!.value! ? 1 : -1
|
|
952
1609
|
}
|
|
1610
|
+
}
|
|
953
1611
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
maxAge: userPreloadMaxAge,
|
|
958
|
-
gcMaxAge: userPreloadGcMaxAge,
|
|
959
|
-
})
|
|
960
|
-
}, preloadDelay)
|
|
1612
|
+
// Sort by length of trimmed full path
|
|
1613
|
+
if (a.trimmed !== b.trimmed) {
|
|
1614
|
+
return a.trimmed > b.trimmed ? 1 : -1
|
|
961
1615
|
}
|
|
962
|
-
}
|
|
963
1616
|
|
|
964
|
-
|
|
965
|
-
|
|
1617
|
+
// Sort by original index
|
|
1618
|
+
return a.index - b.index
|
|
1619
|
+
})
|
|
1620
|
+
.map((d, i) => {
|
|
1621
|
+
d.child.rank = i
|
|
1622
|
+
return d.child
|
|
1623
|
+
}) as any
|
|
1624
|
+
}
|
|
966
1625
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1626
|
+
#parseLocation = (
|
|
1627
|
+
previousLocation?: ParsedLocation,
|
|
1628
|
+
): ParsedLocation<FullSearchSchema<TRouteTree>> => {
|
|
1629
|
+
const parse = ({
|
|
1630
|
+
pathname,
|
|
1631
|
+
search,
|
|
1632
|
+
hash,
|
|
1633
|
+
state,
|
|
1634
|
+
}: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
|
|
1635
|
+
const parsedSearch = this.options.parseSearch(search)
|
|
972
1636
|
|
|
973
1637
|
return {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
isActive,
|
|
981
|
-
disabled,
|
|
1638
|
+
pathname: pathname,
|
|
1639
|
+
searchStr: search,
|
|
1640
|
+
search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
|
|
1641
|
+
hash: hash.split('#').reverse()[0] ?? '',
|
|
1642
|
+
href: `${pathname}${search}${hash}`,
|
|
1643
|
+
state: replaceEqualDeep(previousLocation?.state, state) as HistoryState,
|
|
982
1644
|
}
|
|
983
|
-
}
|
|
984
|
-
buildNext: (opts: BuildNextOptions) => {
|
|
985
|
-
const next = router.__.buildLocation(opts)
|
|
1645
|
+
}
|
|
986
1646
|
|
|
987
|
-
|
|
1647
|
+
const location = parse(this.history.location)
|
|
988
1648
|
|
|
989
|
-
|
|
990
|
-
.map((match) => match.options.preSearchFilters ?? [])
|
|
991
|
-
.flat()
|
|
992
|
-
.filter(Boolean)
|
|
1649
|
+
let { __tempLocation, __tempKey } = location.state
|
|
993
1650
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1651
|
+
if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
|
|
1652
|
+
// Sync up the location keys
|
|
1653
|
+
const parsedTempLocation = parse(__tempLocation) as any
|
|
1654
|
+
parsedTempLocation.state.key = location.state.key
|
|
998
1655
|
|
|
999
|
-
|
|
1000
|
-
...opts,
|
|
1001
|
-
__preSearchFilters,
|
|
1002
|
-
__postSearchFilters,
|
|
1003
|
-
})
|
|
1004
|
-
},
|
|
1656
|
+
delete parsedTempLocation.state.__tempLocation
|
|
1005
1657
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
): Route<TAllRouteInfo, any>[] => {
|
|
1012
|
-
return routeConfigs.map((routeConfig) => {
|
|
1013
|
-
const routeOptions = routeConfig.options
|
|
1014
|
-
const route = createRoute(routeConfig, routeOptions, parent, router)
|
|
1015
|
-
|
|
1016
|
-
// {
|
|
1017
|
-
// pendingMs: routeOptions.pendingMs ?? router.defaultPendingMs,
|
|
1018
|
-
// pendingMinMs: routeOptions.pendingMinMs ?? router.defaultPendingMinMs,
|
|
1019
|
-
// }
|
|
1020
|
-
|
|
1021
|
-
const existingRoute = (router.routesById as any)[route.routeId]
|
|
1022
|
-
|
|
1023
|
-
if (existingRoute) {
|
|
1024
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
1025
|
-
console.warn(
|
|
1026
|
-
`Duplicate routes found with id: ${String(route.routeId)}`,
|
|
1027
|
-
router.routesById,
|
|
1028
|
-
route,
|
|
1029
|
-
)
|
|
1030
|
-
}
|
|
1031
|
-
throw new Error()
|
|
1032
|
-
}
|
|
1658
|
+
return {
|
|
1659
|
+
...parsedTempLocation,
|
|
1660
|
+
maskedLocation: location,
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1033
1663
|
|
|
1034
|
-
|
|
1664
|
+
return location
|
|
1665
|
+
}
|
|
1035
1666
|
|
|
1036
|
-
|
|
1667
|
+
buildLocation = (opts: BuildNextOptions = {}): ParsedLocation => {
|
|
1668
|
+
const build = (
|
|
1669
|
+
dest: BuildNextOptions & {
|
|
1670
|
+
unmaskOnReload?: boolean
|
|
1671
|
+
} = {},
|
|
1672
|
+
matches?: AnyRouteMatch[],
|
|
1673
|
+
): ParsedLocation => {
|
|
1674
|
+
const from = this.state.location
|
|
1675
|
+
|
|
1676
|
+
const fromPathname = dest.from ?? from.pathname
|
|
1677
|
+
|
|
1678
|
+
let pathname = resolvePath(
|
|
1679
|
+
this.basepath ?? '/',
|
|
1680
|
+
fromPathname,
|
|
1681
|
+
`${dest.to ?? ''}`,
|
|
1682
|
+
)
|
|
1037
1683
|
|
|
1038
|
-
|
|
1039
|
-
? recurseRoutes(children, route)
|
|
1040
|
-
: undefined
|
|
1684
|
+
const fromMatches = this.matchRoutes(fromPathname, from.search)
|
|
1041
1685
|
|
|
1042
|
-
|
|
1043
|
-
})
|
|
1044
|
-
}
|
|
1686
|
+
const prevParams = { ...last(fromMatches)?.params }
|
|
1045
1687
|
|
|
1046
|
-
|
|
1688
|
+
let nextParams =
|
|
1689
|
+
(dest.params ?? true) === true
|
|
1690
|
+
? prevParams
|
|
1691
|
+
: functionalUpdate(dest.params!, prevParams)
|
|
1047
1692
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1693
|
+
if (nextParams) {
|
|
1694
|
+
matches
|
|
1695
|
+
?.map((d) => this.getRoute(d.routeId).options.stringifyParams)
|
|
1696
|
+
.filter(Boolean)
|
|
1697
|
+
.forEach((fn) => {
|
|
1698
|
+
nextParams = { ...nextParams!, ...fn!(nextParams!) }
|
|
1699
|
+
})
|
|
1700
|
+
}
|
|
1050
1701
|
|
|
1051
|
-
|
|
1052
|
-
location: History['location'],
|
|
1053
|
-
previousLocation?: Location,
|
|
1054
|
-
): Location => {
|
|
1055
|
-
const parsedSearch = router.options.parseSearch(location.search)
|
|
1702
|
+
pathname = interpolatePath(pathname, nextParams ?? {})
|
|
1056
1703
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1704
|
+
const preSearchFilters =
|
|
1705
|
+
matches
|
|
1706
|
+
?.map(
|
|
1707
|
+
(match) =>
|
|
1708
|
+
this.getRoute(match.routeId).options.preSearchFilters ?? [],
|
|
1709
|
+
)
|
|
1710
|
+
.flat()
|
|
1711
|
+
.filter(Boolean) ?? []
|
|
1712
|
+
|
|
1713
|
+
const postSearchFilters =
|
|
1714
|
+
matches
|
|
1715
|
+
?.map(
|
|
1716
|
+
(match) =>
|
|
1717
|
+
this.getRoute(match.routeId).options.postSearchFilters ?? [],
|
|
1718
|
+
)
|
|
1719
|
+
.flat()
|
|
1720
|
+
.filter(Boolean) ?? []
|
|
1721
|
+
|
|
1722
|
+
// Pre filters first
|
|
1723
|
+
const preFilteredSearch = preSearchFilters?.length
|
|
1724
|
+
? preSearchFilters?.reduce(
|
|
1725
|
+
(prev, next) => next(prev) as any,
|
|
1726
|
+
from.search,
|
|
1727
|
+
)
|
|
1728
|
+
: from.search
|
|
1067
1729
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1730
|
+
// Then the link/navigate function
|
|
1731
|
+
const destSearch =
|
|
1732
|
+
dest.search === true
|
|
1733
|
+
? preFilteredSearch // Preserve resolvedFrom true
|
|
1734
|
+
: dest.search
|
|
1735
|
+
? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
|
|
1736
|
+
: preSearchFilters?.length
|
|
1737
|
+
? preFilteredSearch // Preserve resolvedFrom filters
|
|
1738
|
+
: {}
|
|
1072
1739
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
? router.location.pathname
|
|
1078
|
-
: dest.from ?? router.location.pathname
|
|
1079
|
-
|
|
1080
|
-
let pathname = resolvePath(
|
|
1081
|
-
router.basepath ?? '/',
|
|
1082
|
-
fromPathname,
|
|
1083
|
-
`${dest.to ?? '.'}`,
|
|
1084
|
-
)
|
|
1740
|
+
// Then post filters
|
|
1741
|
+
const postFilteredSearch = postSearchFilters?.length
|
|
1742
|
+
? postSearchFilters.reduce((prev, next) => next(prev), destSearch)
|
|
1743
|
+
: destSearch
|
|
1085
1744
|
|
|
1086
|
-
|
|
1087
|
-
strictParseParams: true,
|
|
1088
|
-
})
|
|
1745
|
+
const search = replaceEqualDeep(from.search, postFilteredSearch)
|
|
1089
1746
|
|
|
1090
|
-
|
|
1747
|
+
const searchStr = this.options.stringifySearch(search)
|
|
1091
1748
|
|
|
1092
|
-
|
|
1749
|
+
const hash =
|
|
1750
|
+
dest.hash === true
|
|
1751
|
+
? from.hash
|
|
1752
|
+
: dest.hash
|
|
1753
|
+
? functionalUpdate(dest.hash!, from.hash)
|
|
1754
|
+
: from.hash
|
|
1093
1755
|
|
|
1094
|
-
|
|
1095
|
-
(dest.params ?? true) === true
|
|
1096
|
-
? prevParams
|
|
1097
|
-
: functionalUpdate(dest.params!, prevParams)
|
|
1756
|
+
const hashStr = hash ? `#${hash}` : ''
|
|
1098
1757
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
})
|
|
1106
|
-
}
|
|
1758
|
+
let nextState =
|
|
1759
|
+
dest.state === true
|
|
1760
|
+
? from.state
|
|
1761
|
+
: dest.state
|
|
1762
|
+
? functionalUpdate(dest.state, from.state)
|
|
1763
|
+
: from.state
|
|
1107
1764
|
|
|
1108
|
-
|
|
1765
|
+
nextState = replaceEqualDeep(from.state, nextState)
|
|
1109
1766
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1767
|
+
return {
|
|
1768
|
+
pathname,
|
|
1769
|
+
search,
|
|
1770
|
+
searchStr,
|
|
1771
|
+
state: nextState as any,
|
|
1772
|
+
hash,
|
|
1773
|
+
href: this.history.createHref(`${pathname}${searchStr}${hashStr}`),
|
|
1774
|
+
unmaskOnReload: dest.unmaskOnReload,
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
const buildWithMatches = (
|
|
1779
|
+
dest: BuildNextOptions = {},
|
|
1780
|
+
maskedDest?: BuildNextOptions,
|
|
1781
|
+
) => {
|
|
1782
|
+
let next = build(dest)
|
|
1783
|
+
let maskedNext = maskedDest ? build(maskedDest) : undefined
|
|
1784
|
+
|
|
1785
|
+
if (!maskedNext) {
|
|
1786
|
+
let params = {}
|
|
1787
|
+
|
|
1788
|
+
let foundMask = this.options.routeMasks?.find((d) => {
|
|
1789
|
+
const match = matchPathname(this.basepath, next.pathname, {
|
|
1790
|
+
to: d.from,
|
|
1791
|
+
fuzzy: false,
|
|
1792
|
+
})
|
|
1135
1793
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1794
|
+
if (match) {
|
|
1795
|
+
params = match
|
|
1796
|
+
return true
|
|
1797
|
+
}
|
|
1140
1798
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
dest.hash === true
|
|
1144
|
-
? router.location.hash
|
|
1145
|
-
: functionalUpdate(dest.hash!, router.location.hash)
|
|
1146
|
-
hash = hash ? `#${hash}` : ''
|
|
1799
|
+
return false
|
|
1800
|
+
})
|
|
1147
1801
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
key: dest.key,
|
|
1802
|
+
if (foundMask) {
|
|
1803
|
+
foundMask = {
|
|
1804
|
+
...foundMask,
|
|
1805
|
+
from: interpolatePath(foundMask.from, params) as any,
|
|
1806
|
+
}
|
|
1807
|
+
maskedDest = foundMask
|
|
1808
|
+
maskedNext = build(maskedDest)
|
|
1156
1809
|
}
|
|
1157
|
-
}
|
|
1810
|
+
}
|
|
1158
1811
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1812
|
+
const nextMatches = this.matchRoutes(next.pathname, next.search)
|
|
1813
|
+
const maskedMatches = maskedNext
|
|
1814
|
+
? this.matchRoutes(maskedNext.pathname, maskedNext.search)
|
|
1815
|
+
: undefined
|
|
1816
|
+
const maskedFinal = maskedNext
|
|
1817
|
+
? build(maskedDest, maskedMatches)
|
|
1818
|
+
: undefined
|
|
1161
1819
|
|
|
1162
|
-
|
|
1820
|
+
const final = build(dest, nextMatches)
|
|
1163
1821
|
|
|
1164
|
-
|
|
1822
|
+
if (maskedFinal) {
|
|
1823
|
+
final.maskedLocation = maskedFinal
|
|
1824
|
+
}
|
|
1165
1825
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1826
|
+
return final
|
|
1827
|
+
}
|
|
1169
1828
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1829
|
+
if (opts.mask) {
|
|
1830
|
+
return buildWithMatches(opts, {
|
|
1831
|
+
...pick(opts, ['from']),
|
|
1832
|
+
...opts.mask,
|
|
1833
|
+
})
|
|
1834
|
+
}
|
|
1172
1835
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1836
|
+
return buildWithMatches(opts)
|
|
1837
|
+
}
|
|
1176
1838
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1839
|
+
#buildAndCommitLocation = ({
|
|
1840
|
+
replace,
|
|
1841
|
+
resetScroll,
|
|
1842
|
+
...rest
|
|
1843
|
+
}: BuildNextOptions & CommitLocationOptions = {}) => {
|
|
1844
|
+
const location = this.buildLocation(rest)
|
|
1845
|
+
return this.#commitLocation({
|
|
1846
|
+
...location,
|
|
1847
|
+
replace,
|
|
1848
|
+
resetScroll,
|
|
1849
|
+
})
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
#commitLocation = async (next: ParsedLocation & CommitLocationOptions) => {
|
|
1853
|
+
if (this.navigateTimeout) clearTimeout(this.navigateTimeout)
|
|
1854
|
+
|
|
1855
|
+
const isSameUrl = this.state.location.href === next.href
|
|
1856
|
+
|
|
1857
|
+
// If the next urls are the same and we're not replacing,
|
|
1858
|
+
// do nothing
|
|
1859
|
+
if (isSameUrl && !next.replace) {
|
|
1860
|
+
return
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
let { maskedLocation, ...nextHistory } = next
|
|
1864
|
+
|
|
1865
|
+
if (maskedLocation) {
|
|
1866
|
+
nextHistory = {
|
|
1867
|
+
...maskedLocation,
|
|
1868
|
+
state: {
|
|
1869
|
+
...maskedLocation.state,
|
|
1870
|
+
__tempKey: undefined,
|
|
1871
|
+
__tempLocation: {
|
|
1872
|
+
...nextHistory,
|
|
1873
|
+
search: nextHistory.searchStr,
|
|
1874
|
+
state: {
|
|
1875
|
+
...nextHistory.state,
|
|
1876
|
+
__tempKey: undefined!,
|
|
1877
|
+
__tempLocation: undefined!,
|
|
1878
|
+
key: undefined!,
|
|
1197
1879
|
},
|
|
1198
|
-
|
|
1199
|
-
}
|
|
1880
|
+
},
|
|
1881
|
+
},
|
|
1882
|
+
}
|
|
1200
1883
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1884
|
+
if (nextHistory.unmaskOnReload ?? this.options.unmaskOnReload ?? false) {
|
|
1885
|
+
nextHistory.state.__tempKey = this.tempLocationKey
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1203
1888
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
})
|
|
1889
|
+
this.history[next.replace ? 'replace' : 'push'](
|
|
1890
|
+
nextHistory.href,
|
|
1891
|
+
nextHistory.state,
|
|
1892
|
+
)
|
|
1209
1893
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1894
|
+
this.resetNextScroll = next.resetScroll ?? true
|
|
1895
|
+
|
|
1896
|
+
return this.latestLoadPromise
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
getRouteMatch = (id: string): undefined | RouteMatch<TRouteTree> => {
|
|
1900
|
+
return this.state.matchesById[id]
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
setRouteMatch = (
|
|
1904
|
+
id: string,
|
|
1905
|
+
updater: (prev: RouteMatch<TRouteTree>) => RouteMatch<TRouteTree>,
|
|
1906
|
+
) => {
|
|
1907
|
+
this.__store.setState((prev) => {
|
|
1908
|
+
if (!prev.matchesById[id]) {
|
|
1909
|
+
return prev
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
return {
|
|
1913
|
+
...prev,
|
|
1914
|
+
matchesById: {
|
|
1915
|
+
...prev.matchesById,
|
|
1916
|
+
[id]: updater(prev.matchesById[id] as any),
|
|
1917
|
+
},
|
|
1918
|
+
}
|
|
1919
|
+
})
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
setRouteMatchData = (
|
|
1923
|
+
id: string,
|
|
1924
|
+
updater: (prev: any) => any,
|
|
1925
|
+
opts?: {
|
|
1926
|
+
updatedAt?: number
|
|
1927
|
+
maxAge?: number
|
|
1212
1928
|
},
|
|
1929
|
+
) => {
|
|
1930
|
+
const match = this.getRouteMatch(id)
|
|
1931
|
+
|
|
1932
|
+
if (!match) return
|
|
1933
|
+
|
|
1934
|
+
const route = this.getRoute(match.routeId)
|
|
1935
|
+
const updatedAt = opts?.updatedAt ?? Date.now()
|
|
1936
|
+
|
|
1937
|
+
const preloadMaxAge =
|
|
1938
|
+
opts?.maxAge ??
|
|
1939
|
+
route.options.preloadMaxAge ??
|
|
1940
|
+
this.options.defaultPreloadMaxAge ??
|
|
1941
|
+
5000
|
|
1942
|
+
|
|
1943
|
+
const maxAge =
|
|
1944
|
+
opts?.maxAge ?? route.options.maxAge ?? this.options.defaultMaxAge ?? -1
|
|
1945
|
+
|
|
1946
|
+
this.setRouteMatch(id, (s) => ({
|
|
1947
|
+
...s,
|
|
1948
|
+
error: undefined,
|
|
1949
|
+
status: 'success',
|
|
1950
|
+
isFetching: false,
|
|
1951
|
+
updatedAt: updatedAt,
|
|
1952
|
+
loaderData: functionalUpdate(updater, s.loaderData),
|
|
1953
|
+
preloadMaxAge,
|
|
1954
|
+
maxAge,
|
|
1955
|
+
}))
|
|
1213
1956
|
}
|
|
1214
1957
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1958
|
+
invalidate = async (opts?: {
|
|
1959
|
+
matchId?: string
|
|
1960
|
+
reload?: boolean
|
|
1961
|
+
__fromFocus?: boolean
|
|
1962
|
+
}): Promise<void> => {
|
|
1963
|
+
if (opts?.matchId) {
|
|
1964
|
+
this.setRouteMatch(opts.matchId, (s) => ({
|
|
1965
|
+
...s,
|
|
1966
|
+
invalid: true,
|
|
1967
|
+
}))
|
|
1968
|
+
|
|
1969
|
+
const matchIndex = this.state.matches.findIndex(
|
|
1970
|
+
(d) => d.id === opts.matchId,
|
|
1971
|
+
)
|
|
1972
|
+
const childMatch = this.state.matches[matchIndex + 1]
|
|
1217
1973
|
|
|
1218
|
-
|
|
1974
|
+
if (childMatch) {
|
|
1975
|
+
return this.invalidate({
|
|
1976
|
+
matchId: childMatch.id,
|
|
1977
|
+
reload: false,
|
|
1978
|
+
__fromFocus: opts.__fromFocus,
|
|
1979
|
+
})
|
|
1980
|
+
}
|
|
1981
|
+
} else {
|
|
1982
|
+
this.__store.batch(() => {
|
|
1983
|
+
Object.values(this.state.matchesById).forEach((match) => {
|
|
1984
|
+
const route = this.getRoute(match.routeId)
|
|
1985
|
+
const shouldInvalidate = opts?.__fromFocus
|
|
1986
|
+
? route.options.reloadOnWindowFocus ?? true
|
|
1987
|
+
: true
|
|
1988
|
+
|
|
1989
|
+
if (shouldInvalidate) {
|
|
1990
|
+
this.setRouteMatch(match.id, (s) => ({
|
|
1991
|
+
...s,
|
|
1992
|
+
invalid: true,
|
|
1993
|
+
}))
|
|
1994
|
+
}
|
|
1995
|
+
})
|
|
1996
|
+
})
|
|
1997
|
+
}
|
|
1219
1998
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1999
|
+
if (opts?.reload ?? true) {
|
|
2000
|
+
return this.load()
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
1222
2004
|
|
|
1223
|
-
|
|
2005
|
+
// Detect if we're in the DOM
|
|
2006
|
+
const isServer = typeof window === 'undefined' || !window.document.createElement
|
|
2007
|
+
|
|
2008
|
+
function getInitialRouterState(): RouterState<any> {
|
|
2009
|
+
return {
|
|
2010
|
+
status: 'idle',
|
|
2011
|
+
isFetching: false,
|
|
2012
|
+
resolvedLocation: null!,
|
|
2013
|
+
location: null!,
|
|
2014
|
+
matchesById: {},
|
|
2015
|
+
matchIds: [],
|
|
2016
|
+
pendingMatchIds: [],
|
|
2017
|
+
matches: [],
|
|
2018
|
+
pendingMatches: [],
|
|
2019
|
+
renderedMatchIds: [],
|
|
2020
|
+
renderedMatches: [],
|
|
2021
|
+
lastUpdated: Date.now(),
|
|
2022
|
+
}
|
|
1224
2023
|
}
|
|
1225
2024
|
|
|
1226
2025
|
function isCtrlEvent(e: MouseEvent) {
|
|
1227
2026
|
return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
|
|
1228
2027
|
}
|
|
2028
|
+
|
|
2029
|
+
export type AnyRedirect = Redirect<any, any, any>
|
|
2030
|
+
|
|
2031
|
+
export type Redirect<
|
|
2032
|
+
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
|
|
2033
|
+
TFrom extends RoutePaths<TRouteTree> = '/',
|
|
2034
|
+
TTo extends string = '',
|
|
2035
|
+
TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
|
|
2036
|
+
TMaskTo extends string = '',
|
|
2037
|
+
> = NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
|
|
2038
|
+
code?: number
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
export function redirect<
|
|
2042
|
+
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
|
|
2043
|
+
TFrom extends RoutePaths<TRouteTree> = '/',
|
|
2044
|
+
TTo extends string = '',
|
|
2045
|
+
>(opts: Redirect<TRouteTree, TFrom, TTo>): Redirect<TRouteTree, TFrom, TTo> {
|
|
2046
|
+
;(opts as any).isRedirect = true
|
|
2047
|
+
return opts
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
export function isRedirect(obj: any): obj is AnyRedirect {
|
|
2051
|
+
return !!obj?.isRedirect
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
export class SearchParamError extends Error {}
|
|
2055
|
+
export class PathParamError extends Error {}
|
|
2056
|
+
|
|
2057
|
+
function escapeJSON(jsonString: string) {
|
|
2058
|
+
return jsonString
|
|
2059
|
+
.replace(/\\/g, '\\\\') // Escape backslashes
|
|
2060
|
+
.replace(/'/g, "\\'") // Escape single quotes
|
|
2061
|
+
.replace(/"/g, '\\"') // Escape double quotes
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// A function that takes an import() argument which is a function and returns a new function that will
|
|
2065
|
+
// proxy arguments from the caller to the imported function, retaining all type
|
|
2066
|
+
// information along the way
|
|
2067
|
+
export function lazyFn<
|
|
2068
|
+
T extends Record<string, (...args: any[]) => any>,
|
|
2069
|
+
TKey extends keyof T = 'default',
|
|
2070
|
+
>(fn: () => Promise<T>, key?: TKey) {
|
|
2071
|
+
return async (...args: Parameters<T[TKey]>): Promise<ReturnType<T[TKey]>> => {
|
|
2072
|
+
const imported = await fn()
|
|
2073
|
+
return imported[key || 'default'](...args)
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
export function isMatchInvalid(
|
|
2078
|
+
match: AnyRouteMatch,
|
|
2079
|
+
opts?: { preload?: boolean },
|
|
2080
|
+
) {
|
|
2081
|
+
const now = Date.now()
|
|
2082
|
+
|
|
2083
|
+
if (match.invalid) {
|
|
2084
|
+
return true
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (opts?.preload) {
|
|
2088
|
+
return match.preloadMaxAge < 0
|
|
2089
|
+
? false
|
|
2090
|
+
: match.updatedAt + match.preloadMaxAge < now
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
return match.maxAge < 0 ? false : match.updatedAt + match.maxAge < now
|
|
2094
|
+
}
|