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