@tanstack/router-core 1.131.13 → 1.131.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +0 -4
- package/dist/cjs/router.cjs +592 -497
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +19 -6
- package/dist/cjs/utils.cjs +6 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +1 -0
- package/dist/esm/route.d.ts +0 -4
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +19 -6
- package/dist/esm/router.js +593 -498
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/utils.d.ts +1 -0
- package/dist/esm/utils.js +6 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/route.ts +10 -2
- package/src/router.ts +848 -677
- package/src/utils.ts +10 -0
package/src/router.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createControlledPromise,
|
|
10
10
|
deepEqual,
|
|
11
11
|
functionalUpdate,
|
|
12
|
+
isPromise,
|
|
12
13
|
last,
|
|
13
14
|
pick,
|
|
14
15
|
replaceEqualDeep,
|
|
@@ -762,6 +763,18 @@ export type CreateRouterFn = <
|
|
|
762
763
|
TDehydrated
|
|
763
764
|
>
|
|
764
765
|
|
|
766
|
+
type InnerLoadContext = {
|
|
767
|
+
location: ParsedLocation
|
|
768
|
+
firstBadMatchIndex?: number
|
|
769
|
+
rendered?: boolean
|
|
770
|
+
updateMatch: UpdateMatchFn
|
|
771
|
+
matches: Array<AnyRouteMatch>
|
|
772
|
+
preload?: boolean
|
|
773
|
+
onReady?: () => Promise<void>
|
|
774
|
+
sync?: boolean
|
|
775
|
+
matchPromises: Array<Promise<AnyRouteMatch>>
|
|
776
|
+
}
|
|
777
|
+
|
|
765
778
|
export class RouterCore<
|
|
766
779
|
in out TRouteTree extends AnyRoute,
|
|
767
780
|
in out TTrailingSlashOption extends TrailingSlashOption,
|
|
@@ -2082,699 +2095,838 @@ export class RouterCore<
|
|
|
2082
2095
|
)
|
|
2083
2096
|
}
|
|
2084
2097
|
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2098
|
+
private triggerOnReady = (
|
|
2099
|
+
innerLoadContext: InnerLoadContext,
|
|
2100
|
+
): void | Promise<void> => {
|
|
2101
|
+
if (!innerLoadContext.rendered) {
|
|
2102
|
+
innerLoadContext.rendered = true
|
|
2103
|
+
return innerLoadContext.onReady?.()
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
private resolvePreload = (
|
|
2108
|
+
innerLoadContext: InnerLoadContext,
|
|
2109
|
+
matchId: string,
|
|
2110
|
+
): boolean => {
|
|
2111
|
+
return !!(
|
|
2112
|
+
innerLoadContext.preload &&
|
|
2113
|
+
!this.state.matches.some((d) => d.id === matchId)
|
|
2114
|
+
)
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
private handleRedirectAndNotFound = (
|
|
2118
|
+
innerLoadContext: InnerLoadContext,
|
|
2119
|
+
match: AnyRouteMatch | undefined,
|
|
2120
|
+
err: unknown,
|
|
2121
|
+
): void => {
|
|
2122
|
+
if (!isRedirect(err) && !isNotFound(err)) return
|
|
2123
|
+
|
|
2124
|
+
if (isRedirect(err) && err.redirectHandled && !err.options.reloadDocument) {
|
|
2125
|
+
throw err
|
|
2126
|
+
}
|
|
2106
2127
|
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2128
|
+
// in case of a redirecting match during preload, the match does not exist
|
|
2129
|
+
if (match) {
|
|
2130
|
+
match._nonReactive.beforeLoadPromise?.resolve()
|
|
2131
|
+
match._nonReactive.loaderPromise?.resolve()
|
|
2132
|
+
match._nonReactive.beforeLoadPromise = undefined
|
|
2133
|
+
match._nonReactive.loaderPromise = undefined
|
|
2134
|
+
|
|
2135
|
+
const status = isRedirect(err) ? 'redirected' : 'notFound'
|
|
2136
|
+
|
|
2137
|
+
innerLoadContext.updateMatch(match.id, (prev) => ({
|
|
2138
|
+
...prev,
|
|
2139
|
+
status,
|
|
2140
|
+
isFetching: false,
|
|
2141
|
+
error: err,
|
|
2142
|
+
}))
|
|
2143
|
+
|
|
2144
|
+
if (isNotFound(err) && !err.routeId) {
|
|
2145
|
+
err.routeId = match.routeId
|
|
2111
2146
|
}
|
|
2147
|
+
|
|
2148
|
+
match._nonReactive.loadPromise?.resolve()
|
|
2112
2149
|
}
|
|
2113
2150
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2151
|
+
if (isRedirect(err)) {
|
|
2152
|
+
innerLoadContext.rendered = true
|
|
2153
|
+
err.options._fromLocation = innerLoadContext.location
|
|
2154
|
+
err.redirectHandled = true
|
|
2155
|
+
err = this.resolveRedirect(err)
|
|
2156
|
+
throw err
|
|
2157
|
+
} else {
|
|
2158
|
+
this._handleNotFound(innerLoadContext, err)
|
|
2159
|
+
throw err
|
|
2116
2160
|
}
|
|
2161
|
+
}
|
|
2117
2162
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
if
|
|
2121
|
-
|
|
2163
|
+
private shouldSkipLoader = (matchId: string): boolean => {
|
|
2164
|
+
const match = this.getMatch(matchId)!
|
|
2165
|
+
// upon hydration, we skip the loader if the match has been dehydrated on the server
|
|
2166
|
+
if (!this.isServer && match._nonReactive.dehydrated) {
|
|
2167
|
+
return true
|
|
2122
2168
|
}
|
|
2123
2169
|
|
|
2124
|
-
|
|
2125
|
-
match
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2170
|
+
if (this.isServer) {
|
|
2171
|
+
if (match.ssr === false) {
|
|
2172
|
+
return true
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return false
|
|
2176
|
+
}
|
|
2129
2177
|
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2178
|
+
private handleSerialError = (
|
|
2179
|
+
innerLoadContext: InnerLoadContext,
|
|
2180
|
+
index: number,
|
|
2181
|
+
err: any,
|
|
2182
|
+
routerCode: string,
|
|
2183
|
+
): void => {
|
|
2184
|
+
const { id: matchId, routeId } = innerLoadContext.matches[index]!
|
|
2185
|
+
const route = this.looseRoutesById[routeId]!
|
|
2186
|
+
|
|
2187
|
+
// Much like suspense, we use a promise here to know if
|
|
2188
|
+
// we've been outdated by a new loadMatches call and
|
|
2189
|
+
// should abort the current async operation
|
|
2190
|
+
if (err instanceof Promise) {
|
|
2191
|
+
throw err
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
err.routerCode = routerCode
|
|
2195
|
+
innerLoadContext.firstBadMatchIndex ??= index
|
|
2196
|
+
this.handleRedirectAndNotFound(
|
|
2197
|
+
innerLoadContext,
|
|
2198
|
+
this.getMatch(matchId),
|
|
2199
|
+
err,
|
|
2200
|
+
)
|
|
2201
|
+
|
|
2202
|
+
try {
|
|
2203
|
+
route.options.onError?.(err)
|
|
2204
|
+
} catch (errorHandlerErr) {
|
|
2205
|
+
err = errorHandlerErr
|
|
2206
|
+
this.handleRedirectAndNotFound(
|
|
2207
|
+
innerLoadContext,
|
|
2208
|
+
this.getMatch(matchId),
|
|
2209
|
+
err,
|
|
2210
|
+
)
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
innerLoadContext.updateMatch(matchId, (prev) => {
|
|
2214
|
+
prev._nonReactive.beforeLoadPromise?.resolve()
|
|
2215
|
+
prev._nonReactive.beforeLoadPromise = undefined
|
|
2216
|
+
prev._nonReactive.loadPromise?.resolve()
|
|
2217
|
+
|
|
2218
|
+
return {
|
|
2219
|
+
...prev,
|
|
2220
|
+
error: err,
|
|
2221
|
+
status: 'error',
|
|
2222
|
+
isFetching: false,
|
|
2223
|
+
updatedAt: Date.now(),
|
|
2224
|
+
abortController: new AbortController(),
|
|
2225
|
+
}
|
|
2226
|
+
})
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
private isBeforeLoadSsr = (
|
|
2230
|
+
innerLoadContext: InnerLoadContext,
|
|
2231
|
+
matchId: string,
|
|
2232
|
+
index: number,
|
|
2233
|
+
route: AnyRoute,
|
|
2234
|
+
): void | Promise<void> => {
|
|
2235
|
+
const existingMatch = this.getMatch(matchId)!
|
|
2236
|
+
const parentMatchId = innerLoadContext.matches[index - 1]?.id
|
|
2237
|
+
const parentMatch = parentMatchId
|
|
2238
|
+
? this.getMatch(parentMatchId)!
|
|
2239
|
+
: undefined
|
|
2240
|
+
|
|
2241
|
+
// in SPA mode, only SSR the root route
|
|
2242
|
+
if (this.isShell()) {
|
|
2243
|
+
existingMatch.ssr = matchId === rootRouteId
|
|
2244
|
+
return
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
if (parentMatch?.ssr === false) {
|
|
2248
|
+
existingMatch.ssr = false
|
|
2249
|
+
return
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
const parentOverride = (tempSsr: boolean | 'data-only') => {
|
|
2253
|
+
if (tempSsr === true && parentMatch?.ssr === 'data-only') {
|
|
2254
|
+
return 'data-only'
|
|
2255
|
+
}
|
|
2256
|
+
return tempSsr
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
const defaultSsr = this.options.defaultSsr ?? true
|
|
2260
|
+
|
|
2261
|
+
if (route.options.ssr === undefined) {
|
|
2262
|
+
existingMatch.ssr = parentOverride(defaultSsr)
|
|
2263
|
+
return
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (typeof route.options.ssr !== 'function') {
|
|
2267
|
+
existingMatch.ssr = parentOverride(route.options.ssr)
|
|
2268
|
+
return
|
|
2269
|
+
}
|
|
2270
|
+
const { search, params } = this.getMatch(matchId)!
|
|
2271
|
+
|
|
2272
|
+
const ssrFnContext: SsrContextOptions<any, any, any> = {
|
|
2273
|
+
search: makeMaybe(search, existingMatch.searchError),
|
|
2274
|
+
params: makeMaybe(params, existingMatch.paramsError),
|
|
2275
|
+
location: innerLoadContext.location,
|
|
2276
|
+
matches: innerLoadContext.matches.map((match) => ({
|
|
2277
|
+
index: match.index,
|
|
2278
|
+
pathname: match.pathname,
|
|
2279
|
+
fullPath: match.fullPath,
|
|
2280
|
+
staticData: match.staticData,
|
|
2281
|
+
id: match.id,
|
|
2282
|
+
routeId: match.routeId,
|
|
2283
|
+
search: makeMaybe(match.search, match.searchError),
|
|
2284
|
+
params: makeMaybe(match.params, match.paramsError),
|
|
2285
|
+
ssr: match.ssr,
|
|
2286
|
+
})),
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
const tempSsr = route.options.ssr(ssrFnContext)
|
|
2290
|
+
if (isPromise(tempSsr)) {
|
|
2291
|
+
return tempSsr.then((ssr) => {
|
|
2292
|
+
existingMatch.ssr = parentOverride(ssr ?? defaultSsr)
|
|
2293
|
+
})
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
existingMatch.ssr = parentOverride(tempSsr ?? defaultSsr)
|
|
2297
|
+
return
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
private setupPendingTimeout = (
|
|
2301
|
+
innerLoadContext: InnerLoadContext,
|
|
2302
|
+
matchId: string,
|
|
2303
|
+
route: AnyRoute,
|
|
2304
|
+
): void => {
|
|
2305
|
+
const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs
|
|
2306
|
+
const shouldPending = !!(
|
|
2307
|
+
innerLoadContext.onReady &&
|
|
2308
|
+
!this.isServer &&
|
|
2309
|
+
!this.resolvePreload(innerLoadContext, matchId) &&
|
|
2310
|
+
(route.options.loader ||
|
|
2311
|
+
route.options.beforeLoad ||
|
|
2312
|
+
routeNeedsPreload(route)) &&
|
|
2313
|
+
typeof pendingMs === 'number' &&
|
|
2314
|
+
pendingMs !== Infinity &&
|
|
2315
|
+
(route.options.pendingComponent ??
|
|
2316
|
+
(this.options as any)?.defaultPendingComponent)
|
|
2317
|
+
)
|
|
2318
|
+
const match = this.getMatch(matchId)!
|
|
2319
|
+
if (shouldPending && match._nonReactive.pendingTimeout === undefined) {
|
|
2320
|
+
const pendingTimeout = setTimeout(() => {
|
|
2321
|
+
try {
|
|
2322
|
+
// Update the match and prematurely resolve the loadMatches promise so that
|
|
2323
|
+
// the pending component can start rendering
|
|
2324
|
+
this.triggerOnReady(innerLoadContext)
|
|
2325
|
+
} catch {}
|
|
2326
|
+
}, pendingMs)
|
|
2327
|
+
match._nonReactive.pendingTimeout = pendingTimeout
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
private shouldExecuteBeforeLoad = (
|
|
2332
|
+
innerLoadContext: InnerLoadContext,
|
|
2333
|
+
matchId: string,
|
|
2334
|
+
route: AnyRoute,
|
|
2335
|
+
): boolean | Promise<boolean> => {
|
|
2336
|
+
const existingMatch = this.getMatch(matchId)!
|
|
2337
|
+
|
|
2338
|
+
// If we are in the middle of a load, either of these will be present
|
|
2339
|
+
// (not to be confused with `loadPromise`, which is always defined)
|
|
2340
|
+
if (
|
|
2341
|
+
!existingMatch._nonReactive.beforeLoadPromise &&
|
|
2342
|
+
!existingMatch._nonReactive.loaderPromise
|
|
2343
|
+
)
|
|
2344
|
+
return true
|
|
2345
|
+
|
|
2346
|
+
this.setupPendingTimeout(innerLoadContext, matchId, route)
|
|
2347
|
+
|
|
2348
|
+
const then = () => {
|
|
2349
|
+
let shouldExecuteBeforeLoad = true
|
|
2350
|
+
const match = this.getMatch(matchId)!
|
|
2351
|
+
if (match.status === 'error') {
|
|
2352
|
+
shouldExecuteBeforeLoad = true
|
|
2353
|
+
} else if (
|
|
2354
|
+
match.preload &&
|
|
2355
|
+
(match.status === 'redirected' || match.status === 'notFound')
|
|
2134
2356
|
) {
|
|
2135
|
-
|
|
2357
|
+
this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
|
|
2136
2358
|
}
|
|
2359
|
+
return shouldExecuteBeforeLoad
|
|
2360
|
+
}
|
|
2137
2361
|
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
match._nonReactive.loaderPromise = undefined
|
|
2362
|
+
// Wait for the beforeLoad to resolve before we continue
|
|
2363
|
+
return existingMatch._nonReactive.beforeLoadPromise
|
|
2364
|
+
? existingMatch._nonReactive.beforeLoadPromise.then(then)
|
|
2365
|
+
: then()
|
|
2366
|
+
}
|
|
2144
2367
|
|
|
2145
|
-
|
|
2368
|
+
private executeBeforeLoad = (
|
|
2369
|
+
innerLoadContext: InnerLoadContext,
|
|
2370
|
+
matchId: string,
|
|
2371
|
+
index: number,
|
|
2372
|
+
route: AnyRoute,
|
|
2373
|
+
): void | Promise<void> => {
|
|
2374
|
+
const resolve = () => {
|
|
2375
|
+
innerLoadContext.updateMatch(matchId, (prev) => {
|
|
2376
|
+
prev._nonReactive.beforeLoadPromise?.resolve()
|
|
2377
|
+
prev._nonReactive.beforeLoadPromise = undefined
|
|
2146
2378
|
|
|
2147
|
-
|
|
2379
|
+
return {
|
|
2148
2380
|
...prev,
|
|
2149
|
-
status,
|
|
2150
2381
|
isFetching: false,
|
|
2151
|
-
|
|
2152
|
-
|
|
2382
|
+
}
|
|
2383
|
+
})
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
try {
|
|
2387
|
+
const match = this.getMatch(matchId)!
|
|
2388
|
+
match._nonReactive.beforeLoadPromise = createControlledPromise<void>()
|
|
2389
|
+
// explicitly capture the previous loadPromise
|
|
2390
|
+
const prevLoadPromise = match._nonReactive.loadPromise
|
|
2391
|
+
match._nonReactive.loadPromise = createControlledPromise<void>(() => {
|
|
2392
|
+
prevLoadPromise?.resolve()
|
|
2393
|
+
})
|
|
2394
|
+
|
|
2395
|
+
const { paramsError, searchError } = this.getMatch(matchId)!
|
|
2396
|
+
|
|
2397
|
+
if (paramsError) {
|
|
2398
|
+
this.handleSerialError(
|
|
2399
|
+
innerLoadContext,
|
|
2400
|
+
index,
|
|
2401
|
+
paramsError,
|
|
2402
|
+
'PARSE_PARAMS',
|
|
2403
|
+
)
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
if (searchError) {
|
|
2407
|
+
this.handleSerialError(
|
|
2408
|
+
innerLoadContext,
|
|
2409
|
+
index,
|
|
2410
|
+
searchError,
|
|
2411
|
+
'VALIDATE_SEARCH',
|
|
2412
|
+
)
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
this.setupPendingTimeout(innerLoadContext, matchId, route)
|
|
2416
|
+
|
|
2417
|
+
const abortController = new AbortController()
|
|
2418
|
+
|
|
2419
|
+
const parentMatchId = innerLoadContext.matches[index - 1]?.id
|
|
2420
|
+
const parentMatch = parentMatchId
|
|
2421
|
+
? this.getMatch(parentMatchId)!
|
|
2422
|
+
: undefined
|
|
2423
|
+
const parentMatchContext =
|
|
2424
|
+
parentMatch?.context ?? this.options.context ?? undefined
|
|
2425
|
+
|
|
2426
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2427
|
+
...prev,
|
|
2428
|
+
isFetching: 'beforeLoad',
|
|
2429
|
+
fetchCount: prev.fetchCount + 1,
|
|
2430
|
+
abortController,
|
|
2431
|
+
context: {
|
|
2432
|
+
...parentMatchContext,
|
|
2433
|
+
...prev.__routeContext,
|
|
2434
|
+
},
|
|
2435
|
+
}))
|
|
2436
|
+
|
|
2437
|
+
const { search, params, context, cause } = this.getMatch(matchId)!
|
|
2438
|
+
|
|
2439
|
+
const preload = this.resolvePreload(innerLoadContext, matchId)
|
|
2440
|
+
|
|
2441
|
+
const beforeLoadFnContext: BeforeLoadContextOptions<
|
|
2442
|
+
any,
|
|
2443
|
+
any,
|
|
2444
|
+
any,
|
|
2445
|
+
any,
|
|
2446
|
+
any
|
|
2447
|
+
> = {
|
|
2448
|
+
search,
|
|
2449
|
+
abortController,
|
|
2450
|
+
params,
|
|
2451
|
+
preload,
|
|
2452
|
+
context,
|
|
2453
|
+
location: innerLoadContext.location,
|
|
2454
|
+
navigate: (opts: any) =>
|
|
2455
|
+
this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
|
|
2456
|
+
buildLocation: this.buildLocation,
|
|
2457
|
+
cause: preload ? 'preload' : cause,
|
|
2458
|
+
matches: innerLoadContext.matches,
|
|
2459
|
+
}
|
|
2153
2460
|
|
|
2154
|
-
|
|
2155
|
-
|
|
2461
|
+
const updateContext = (beforeLoadContext: any) => {
|
|
2462
|
+
if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
|
|
2463
|
+
this.handleSerialError(
|
|
2464
|
+
innerLoadContext,
|
|
2465
|
+
index,
|
|
2466
|
+
beforeLoadContext,
|
|
2467
|
+
'BEFORE_LOAD',
|
|
2468
|
+
)
|
|
2156
2469
|
}
|
|
2157
2470
|
|
|
2158
|
-
|
|
2471
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2472
|
+
...prev,
|
|
2473
|
+
__beforeLoadContext: beforeLoadContext,
|
|
2474
|
+
context: {
|
|
2475
|
+
...parentMatchContext,
|
|
2476
|
+
...prev.__routeContext,
|
|
2477
|
+
...beforeLoadContext,
|
|
2478
|
+
},
|
|
2479
|
+
abortController,
|
|
2480
|
+
}))
|
|
2159
2481
|
}
|
|
2160
2482
|
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2483
|
+
const beforeLoadContext = route.options.beforeLoad?.(beforeLoadFnContext)
|
|
2484
|
+
if (isPromise(beforeLoadContext)) {
|
|
2485
|
+
return beforeLoadContext
|
|
2486
|
+
.then(updateContext)
|
|
2487
|
+
.catch((err) => {
|
|
2488
|
+
this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
|
|
2489
|
+
})
|
|
2490
|
+
.then(resolve)
|
|
2167
2491
|
} else {
|
|
2168
|
-
|
|
2169
|
-
throw err
|
|
2492
|
+
updateContext(beforeLoadContext)
|
|
2170
2493
|
}
|
|
2494
|
+
} catch (err) {
|
|
2495
|
+
this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
|
|
2171
2496
|
}
|
|
2172
2497
|
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
if (!this.isServer && match._nonReactive.dehydrated) {
|
|
2177
|
-
return true
|
|
2178
|
-
}
|
|
2498
|
+
resolve()
|
|
2499
|
+
return
|
|
2500
|
+
}
|
|
2179
2501
|
|
|
2502
|
+
private handleBeforeLoad = (
|
|
2503
|
+
innerLoadContext: InnerLoadContext,
|
|
2504
|
+
index: number,
|
|
2505
|
+
): void | Promise<void> => {
|
|
2506
|
+
const { id: matchId, routeId } = innerLoadContext.matches[index]!
|
|
2507
|
+
const route = this.looseRoutesById[routeId]!
|
|
2508
|
+
|
|
2509
|
+
const serverSsr = () => {
|
|
2510
|
+
// on the server, determine whether SSR the current match or not
|
|
2180
2511
|
if (this.isServer) {
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2512
|
+
const maybePromise = this.isBeforeLoadSsr(
|
|
2513
|
+
innerLoadContext,
|
|
2514
|
+
matchId,
|
|
2515
|
+
index,
|
|
2516
|
+
route,
|
|
2517
|
+
)
|
|
2518
|
+
if (isPromise(maybePromise)) return maybePromise.then(queueExecution)
|
|
2184
2519
|
}
|
|
2185
|
-
return
|
|
2520
|
+
return queueExecution()
|
|
2186
2521
|
}
|
|
2187
2522
|
|
|
2523
|
+
const queueExecution = () => {
|
|
2524
|
+
if (this.shouldSkipLoader(matchId)) return
|
|
2525
|
+
const shouldExecuteBeforeLoadResult = this.shouldExecuteBeforeLoad(
|
|
2526
|
+
innerLoadContext,
|
|
2527
|
+
matchId,
|
|
2528
|
+
route,
|
|
2529
|
+
)
|
|
2530
|
+
return isPromise(shouldExecuteBeforeLoadResult)
|
|
2531
|
+
? shouldExecuteBeforeLoadResult.then(execute)
|
|
2532
|
+
: execute(shouldExecuteBeforeLoadResult)
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
const execute = (shouldExecuteBeforeLoad: boolean) => {
|
|
2536
|
+
if (shouldExecuteBeforeLoad) {
|
|
2537
|
+
// If we are not in the middle of a load OR the previous load failed, start it
|
|
2538
|
+
return this.executeBeforeLoad(innerLoadContext, matchId, index, route)
|
|
2539
|
+
}
|
|
2540
|
+
return
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
return serverSsr()
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
private executeHead = (
|
|
2547
|
+
innerLoadContext: InnerLoadContext,
|
|
2548
|
+
matchId: string,
|
|
2549
|
+
route: AnyRoute,
|
|
2550
|
+
): void | Promise<
|
|
2551
|
+
Pick<
|
|
2552
|
+
AnyRouteMatch,
|
|
2553
|
+
'meta' | 'links' | 'headScripts' | 'headers' | 'scripts' | 'styles'
|
|
2554
|
+
>
|
|
2555
|
+
> => {
|
|
2556
|
+
const match = this.getMatch(matchId)
|
|
2557
|
+
// in case of a redirecting match during preload, the match does not exist
|
|
2558
|
+
if (!match) {
|
|
2559
|
+
return
|
|
2560
|
+
}
|
|
2561
|
+
if (
|
|
2562
|
+
!route.options.head &&
|
|
2563
|
+
!route.options.scripts &&
|
|
2564
|
+
!route.options.headers
|
|
2565
|
+
) {
|
|
2566
|
+
return
|
|
2567
|
+
}
|
|
2568
|
+
const assetContext = {
|
|
2569
|
+
matches: innerLoadContext.matches,
|
|
2570
|
+
match,
|
|
2571
|
+
params: match.params,
|
|
2572
|
+
loaderData: match.loaderData,
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
return Promise.all([
|
|
2576
|
+
route.options.head?.(assetContext),
|
|
2577
|
+
route.options.scripts?.(assetContext),
|
|
2578
|
+
route.options.headers?.(assetContext),
|
|
2579
|
+
]).then(([headFnContent, scripts, headers]) => {
|
|
2580
|
+
const meta = headFnContent?.meta
|
|
2581
|
+
const links = headFnContent?.links
|
|
2582
|
+
const headScripts = headFnContent?.scripts
|
|
2583
|
+
const styles = headFnContent?.styles
|
|
2584
|
+
|
|
2585
|
+
return {
|
|
2586
|
+
meta,
|
|
2587
|
+
links,
|
|
2588
|
+
headScripts,
|
|
2589
|
+
headers,
|
|
2590
|
+
scripts,
|
|
2591
|
+
styles,
|
|
2592
|
+
}
|
|
2593
|
+
})
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
private potentialPendingMinPromise = (
|
|
2597
|
+
matchId: string,
|
|
2598
|
+
): void | ControlledPromise<void> => {
|
|
2599
|
+
const latestMatch = this.getMatch(matchId)!
|
|
2600
|
+
return latestMatch._nonReactive.minPendingPromise
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
private getLoaderContext = (
|
|
2604
|
+
innerLoadContext: InnerLoadContext,
|
|
2605
|
+
matchId: string,
|
|
2606
|
+
index: number,
|
|
2607
|
+
route: AnyRoute,
|
|
2608
|
+
): LoaderFnContext => {
|
|
2609
|
+
const parentMatchPromise = innerLoadContext.matchPromises[index - 1] as any
|
|
2610
|
+
const { params, loaderDeps, abortController, context, cause } =
|
|
2611
|
+
this.getMatch(matchId)!
|
|
2612
|
+
|
|
2613
|
+
const preload = this.resolvePreload(innerLoadContext, matchId)
|
|
2614
|
+
|
|
2615
|
+
return {
|
|
2616
|
+
params,
|
|
2617
|
+
deps: loaderDeps,
|
|
2618
|
+
preload: !!preload,
|
|
2619
|
+
parentMatchPromise,
|
|
2620
|
+
abortController: abortController,
|
|
2621
|
+
context,
|
|
2622
|
+
location: innerLoadContext.location,
|
|
2623
|
+
navigate: (opts) =>
|
|
2624
|
+
this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
|
|
2625
|
+
cause: preload ? 'preload' : cause,
|
|
2626
|
+
route,
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
private runLoader = async (
|
|
2631
|
+
innerLoadContext: InnerLoadContext,
|
|
2632
|
+
matchId: string,
|
|
2633
|
+
index: number,
|
|
2634
|
+
route: AnyRoute,
|
|
2635
|
+
): Promise<void> => {
|
|
2188
2636
|
try {
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
err: any,
|
|
2195
|
-
routerCode: string,
|
|
2196
|
-
) => {
|
|
2197
|
-
const { id: matchId, routeId } = matches[index]!
|
|
2198
|
-
const route = this.looseRoutesById[routeId]!
|
|
2199
|
-
|
|
2200
|
-
// Much like suspense, we use a promise here to know if
|
|
2201
|
-
// we've been outdated by a new loadMatches call and
|
|
2202
|
-
// should abort the current async operation
|
|
2203
|
-
if (err instanceof Promise) {
|
|
2204
|
-
throw err
|
|
2205
|
-
}
|
|
2637
|
+
// If the Matches component rendered
|
|
2638
|
+
// the pending component and needs to show it for
|
|
2639
|
+
// a minimum duration, we''ll wait for it to resolve
|
|
2640
|
+
// before committing to the match and resolving
|
|
2641
|
+
// the loadPromise
|
|
2206
2642
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2643
|
+
// Actually run the loader and handle the result
|
|
2644
|
+
try {
|
|
2645
|
+
if (!this.isServer || this.getMatch(matchId)!.ssr === true) {
|
|
2646
|
+
this.loadRouteChunk(route)
|
|
2647
|
+
}
|
|
2210
2648
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2649
|
+
// Kick off the loader!
|
|
2650
|
+
const loaderResult = route.options.loader?.(
|
|
2651
|
+
this.getLoaderContext(innerLoadContext, matchId, index, route),
|
|
2652
|
+
)
|
|
2653
|
+
const loaderResultIsPromise =
|
|
2654
|
+
route.options.loader && isPromise(loaderResult)
|
|
2655
|
+
|
|
2656
|
+
const willLoadSomething = !!(
|
|
2657
|
+
loaderResultIsPromise ||
|
|
2658
|
+
route._lazyPromise ||
|
|
2659
|
+
route._componentsPromise ||
|
|
2660
|
+
route.options.head ||
|
|
2661
|
+
route.options.scripts ||
|
|
2662
|
+
route.options.headers ||
|
|
2663
|
+
this.getMatch(matchId)!._nonReactive.minPendingPromise
|
|
2664
|
+
)
|
|
2217
2665
|
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
...prev,
|
|
2225
|
-
error: err,
|
|
2226
|
-
status: 'error',
|
|
2227
|
-
isFetching: false,
|
|
2228
|
-
updatedAt: Date.now(),
|
|
2229
|
-
abortController: new AbortController(),
|
|
2230
|
-
}
|
|
2231
|
-
})
|
|
2232
|
-
}
|
|
2666
|
+
if (willLoadSomething) {
|
|
2667
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2668
|
+
...prev,
|
|
2669
|
+
isFetching: 'loader',
|
|
2670
|
+
}))
|
|
2671
|
+
}
|
|
2233
2672
|
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
? this.getMatch(parentMatchId)!
|
|
2239
|
-
: undefined
|
|
2240
|
-
|
|
2241
|
-
const route = this.looseRoutesById[routeId]!
|
|
2242
|
-
|
|
2243
|
-
const pendingMs =
|
|
2244
|
-
route.options.pendingMs ?? this.options.defaultPendingMs
|
|
2245
|
-
|
|
2246
|
-
// on the server, determine whether SSR the current match or not
|
|
2247
|
-
if (this.isServer) {
|
|
2248
|
-
let ssr: boolean | 'data-only'
|
|
2249
|
-
// in SPA mode, only SSR the root route
|
|
2250
|
-
if (this.isShell()) {
|
|
2251
|
-
ssr = matchId === rootRouteId
|
|
2252
|
-
} else {
|
|
2253
|
-
const defaultSsr = this.options.defaultSsr ?? true
|
|
2254
|
-
if (parentMatch?.ssr === false) {
|
|
2255
|
-
ssr = false
|
|
2256
|
-
} else {
|
|
2257
|
-
let tempSsr: boolean | 'data-only'
|
|
2258
|
-
if (route.options.ssr === undefined) {
|
|
2259
|
-
tempSsr = defaultSsr
|
|
2260
|
-
} else if (typeof route.options.ssr === 'function') {
|
|
2261
|
-
const { search, params } = this.getMatch(matchId)!
|
|
2262
|
-
|
|
2263
|
-
function makeMaybe(value: any, error: any) {
|
|
2264
|
-
if (error) {
|
|
2265
|
-
return { status: 'error' as const, error }
|
|
2266
|
-
}
|
|
2267
|
-
return { status: 'success' as const, value }
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
const ssrFnContext: SsrContextOptions<any, any, any> = {
|
|
2271
|
-
search: makeMaybe(search, existingMatch.searchError),
|
|
2272
|
-
params: makeMaybe(params, existingMatch.paramsError),
|
|
2273
|
-
location,
|
|
2274
|
-
matches: matches.map((match) => ({
|
|
2275
|
-
index: match.index,
|
|
2276
|
-
pathname: match.pathname,
|
|
2277
|
-
fullPath: match.fullPath,
|
|
2278
|
-
staticData: match.staticData,
|
|
2279
|
-
id: match.id,
|
|
2280
|
-
routeId: match.routeId,
|
|
2281
|
-
search: makeMaybe(match.search, match.searchError),
|
|
2282
|
-
params: makeMaybe(match.params, match.paramsError),
|
|
2283
|
-
ssr: match.ssr,
|
|
2284
|
-
})),
|
|
2285
|
-
}
|
|
2286
|
-
tempSsr =
|
|
2287
|
-
(await route.options.ssr(ssrFnContext)) ?? defaultSsr
|
|
2288
|
-
} else {
|
|
2289
|
-
tempSsr = route.options.ssr
|
|
2290
|
-
}
|
|
2673
|
+
if (route.options.loader) {
|
|
2674
|
+
const loaderData = loaderResultIsPromise
|
|
2675
|
+
? await loaderResult
|
|
2676
|
+
: loaderResult
|
|
2291
2677
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2678
|
+
this.handleRedirectAndNotFound(
|
|
2679
|
+
innerLoadContext,
|
|
2680
|
+
this.getMatch(matchId),
|
|
2681
|
+
loaderData,
|
|
2682
|
+
)
|
|
2683
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2684
|
+
...prev,
|
|
2685
|
+
loaderData,
|
|
2686
|
+
}))
|
|
2687
|
+
}
|
|
2301
2688
|
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2689
|
+
// Lazy option can modify the route options,
|
|
2690
|
+
// so we need to wait for it to resolve before
|
|
2691
|
+
// we can use the options
|
|
2692
|
+
if (route._lazyPromise) await route._lazyPromise
|
|
2693
|
+
const headResult = this.executeHead(innerLoadContext, matchId, route)
|
|
2694
|
+
const head = headResult ? await headResult : undefined
|
|
2695
|
+
const pendingPromise = this.potentialPendingMinPromise(matchId)
|
|
2696
|
+
if (pendingPromise) await pendingPromise
|
|
2697
|
+
|
|
2698
|
+
// Last but not least, wait for the the components
|
|
2699
|
+
// to be preloaded before we resolve the match
|
|
2700
|
+
if (route._componentsPromise) await route._componentsPromise
|
|
2701
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2702
|
+
...prev,
|
|
2703
|
+
error: undefined,
|
|
2704
|
+
status: 'success',
|
|
2705
|
+
isFetching: false,
|
|
2706
|
+
updatedAt: Date.now(),
|
|
2707
|
+
...head,
|
|
2708
|
+
}))
|
|
2709
|
+
} catch (e) {
|
|
2710
|
+
let error = e
|
|
2305
2711
|
|
|
2306
|
-
|
|
2307
|
-
onReady &&
|
|
2308
|
-
!this.isServer &&
|
|
2309
|
-
!resolvePreload(matchId) &&
|
|
2310
|
-
(route.options.loader ||
|
|
2311
|
-
route.options.beforeLoad ||
|
|
2312
|
-
routeNeedsPreload(route)) &&
|
|
2313
|
-
typeof pendingMs === 'number' &&
|
|
2314
|
-
pendingMs !== Infinity &&
|
|
2315
|
-
(route.options.pendingComponent ??
|
|
2316
|
-
(this.options as any)?.defaultPendingComponent)
|
|
2317
|
-
)
|
|
2712
|
+
await this.potentialPendingMinPromise(matchId)
|
|
2318
2713
|
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
isFetching: false,
|
|
2455
|
-
}
|
|
2456
|
-
})
|
|
2457
|
-
}
|
|
2714
|
+
this.handleRedirectAndNotFound(
|
|
2715
|
+
innerLoadContext,
|
|
2716
|
+
this.getMatch(matchId),
|
|
2717
|
+
e,
|
|
2718
|
+
)
|
|
2719
|
+
|
|
2720
|
+
try {
|
|
2721
|
+
route.options.onError?.(e)
|
|
2722
|
+
} catch (onErrorError) {
|
|
2723
|
+
error = onErrorError
|
|
2724
|
+
this.handleRedirectAndNotFound(
|
|
2725
|
+
innerLoadContext,
|
|
2726
|
+
this.getMatch(matchId),
|
|
2727
|
+
onErrorError,
|
|
2728
|
+
)
|
|
2729
|
+
}
|
|
2730
|
+
const headResult = this.executeHead(innerLoadContext, matchId, route)
|
|
2731
|
+
const head = headResult ? await headResult : undefined
|
|
2732
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2733
|
+
...prev,
|
|
2734
|
+
error,
|
|
2735
|
+
status: 'error',
|
|
2736
|
+
isFetching: false,
|
|
2737
|
+
...head,
|
|
2738
|
+
}))
|
|
2739
|
+
}
|
|
2740
|
+
} catch (err) {
|
|
2741
|
+
const match = this.getMatch(matchId)
|
|
2742
|
+
// in case of a redirecting match during preload, the match does not exist
|
|
2743
|
+
if (match) {
|
|
2744
|
+
const headResult = this.executeHead(innerLoadContext, matchId, route)
|
|
2745
|
+
if (headResult) {
|
|
2746
|
+
const head = await headResult
|
|
2747
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2748
|
+
...prev,
|
|
2749
|
+
...head,
|
|
2750
|
+
}))
|
|
2751
|
+
}
|
|
2752
|
+
match._nonReactive.loaderPromise = undefined
|
|
2753
|
+
}
|
|
2754
|
+
this.handleRedirectAndNotFound(innerLoadContext, match, err)
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
private loadRouteMatch = async (
|
|
2759
|
+
innerLoadContext: InnerLoadContext,
|
|
2760
|
+
index: number,
|
|
2761
|
+
): Promise<AnyRouteMatch> => {
|
|
2762
|
+
const { id: matchId, routeId } = innerLoadContext.matches[index]!
|
|
2763
|
+
let loaderShouldRunAsync = false
|
|
2764
|
+
let loaderIsRunningAsync = false
|
|
2765
|
+
const route = this.looseRoutesById[routeId]!
|
|
2766
|
+
|
|
2767
|
+
const prevMatch = this.getMatch(matchId)!
|
|
2768
|
+
if (this.shouldSkipLoader(matchId)) {
|
|
2769
|
+
if (this.isServer) {
|
|
2770
|
+
const headResult = this.executeHead(innerLoadContext, matchId, route)
|
|
2771
|
+
if (headResult) {
|
|
2772
|
+
const head = await headResult
|
|
2773
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2774
|
+
...prev,
|
|
2775
|
+
...head,
|
|
2776
|
+
}))
|
|
2777
|
+
}
|
|
2778
|
+
return this.getMatch(matchId)!
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
// there is a loaderPromise, so we are in the middle of a load
|
|
2782
|
+
else if (prevMatch._nonReactive.loaderPromise) {
|
|
2783
|
+
// do not block if we already have stale data we can show
|
|
2784
|
+
// but only if the ongoing load is not a preload since error handling is different for preloads
|
|
2785
|
+
// and we don't want to swallow errors
|
|
2786
|
+
if (
|
|
2787
|
+
prevMatch.status === 'success' &&
|
|
2788
|
+
!innerLoadContext.sync &&
|
|
2789
|
+
!prevMatch.preload
|
|
2790
|
+
) {
|
|
2791
|
+
return this.getMatch(matchId)!
|
|
2792
|
+
}
|
|
2793
|
+
await prevMatch._nonReactive.loaderPromise
|
|
2794
|
+
const match = this.getMatch(matchId)!
|
|
2795
|
+
if (match.error) {
|
|
2796
|
+
this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
|
|
2797
|
+
}
|
|
2798
|
+
} else {
|
|
2799
|
+
// This is where all of the stale-while-revalidate magic happens
|
|
2800
|
+
const age = Date.now() - this.getMatch(matchId)!.updatedAt
|
|
2801
|
+
|
|
2802
|
+
const preload = this.resolvePreload(innerLoadContext, matchId)
|
|
2803
|
+
|
|
2804
|
+
const staleAge = preload
|
|
2805
|
+
? (route.options.preloadStaleTime ??
|
|
2806
|
+
this.options.defaultPreloadStaleTime ??
|
|
2807
|
+
30_000) // 30 seconds for preloads by default
|
|
2808
|
+
: (route.options.staleTime ?? this.options.defaultStaleTime ?? 0)
|
|
2809
|
+
|
|
2810
|
+
const shouldReloadOption = route.options.shouldReload
|
|
2811
|
+
|
|
2812
|
+
// Default to reloading the route all the time
|
|
2813
|
+
// Allow shouldReload to get the last say,
|
|
2814
|
+
// if provided.
|
|
2815
|
+
const shouldReload =
|
|
2816
|
+
typeof shouldReloadOption === 'function'
|
|
2817
|
+
? shouldReloadOption(
|
|
2818
|
+
this.getLoaderContext(innerLoadContext, matchId, index, route),
|
|
2819
|
+
)
|
|
2820
|
+
: shouldReloadOption
|
|
2821
|
+
|
|
2822
|
+
innerLoadContext.updateMatch(matchId, (prev) => {
|
|
2823
|
+
prev._nonReactive.loaderPromise = createControlledPromise<void>()
|
|
2824
|
+
return {
|
|
2825
|
+
...prev,
|
|
2826
|
+
preload:
|
|
2827
|
+
!!preload && !this.state.matches.some((d) => d.id === matchId),
|
|
2828
|
+
}
|
|
2829
|
+
})
|
|
2830
|
+
|
|
2831
|
+
// If the route is successful and still fresh, just resolve
|
|
2832
|
+
const { status, invalid } = this.getMatch(matchId)!
|
|
2833
|
+
loaderShouldRunAsync =
|
|
2834
|
+
status === 'success' && (invalid || (shouldReload ?? age > staleAge))
|
|
2835
|
+
if (preload && route.options.preload === false) {
|
|
2836
|
+
// Do nothing
|
|
2837
|
+
} else if (loaderShouldRunAsync && !innerLoadContext.sync) {
|
|
2838
|
+
loaderIsRunningAsync = true
|
|
2839
|
+
;(async () => {
|
|
2840
|
+
try {
|
|
2841
|
+
await this.runLoader(innerLoadContext, matchId, index, route)
|
|
2842
|
+
const match = this.getMatch(matchId)!
|
|
2843
|
+
match._nonReactive.loaderPromise?.resolve()
|
|
2844
|
+
match._nonReactive.loadPromise?.resolve()
|
|
2845
|
+
match._nonReactive.loaderPromise = undefined
|
|
2846
|
+
} catch (err) {
|
|
2847
|
+
if (isRedirect(err)) {
|
|
2848
|
+
await this.navigate(err.options)
|
|
2458
2849
|
}
|
|
2850
|
+
}
|
|
2851
|
+
})()
|
|
2852
|
+
} else if (
|
|
2853
|
+
status !== 'success' ||
|
|
2854
|
+
(loaderShouldRunAsync && innerLoadContext.sync)
|
|
2855
|
+
) {
|
|
2856
|
+
await this.runLoader(innerLoadContext, matchId, index, route)
|
|
2857
|
+
} else {
|
|
2858
|
+
// if the loader did not run, still update head.
|
|
2859
|
+
// reason: parent's beforeLoad may have changed the route context
|
|
2860
|
+
// and only now do we know the route context (and that the loader would not run)
|
|
2861
|
+
const headResult = this.executeHead(innerLoadContext, matchId, route)
|
|
2862
|
+
if (headResult) {
|
|
2863
|
+
const head = await headResult
|
|
2864
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2865
|
+
...prev,
|
|
2866
|
+
...head,
|
|
2867
|
+
}))
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
if (!loaderIsRunningAsync) {
|
|
2872
|
+
const match = this.getMatch(matchId)!
|
|
2873
|
+
match._nonReactive.loaderPromise?.resolve()
|
|
2874
|
+
match._nonReactive.loadPromise?.resolve()
|
|
2875
|
+
}
|
|
2459
2876
|
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
if (!match) {
|
|
2474
|
-
return
|
|
2475
|
-
}
|
|
2476
|
-
if (
|
|
2477
|
-
!route.options.head &&
|
|
2478
|
-
!route.options.scripts &&
|
|
2479
|
-
!route.options.headers
|
|
2480
|
-
) {
|
|
2481
|
-
return
|
|
2482
|
-
}
|
|
2483
|
-
const assetContext = {
|
|
2484
|
-
matches,
|
|
2485
|
-
match,
|
|
2486
|
-
params: match.params,
|
|
2487
|
-
loaderData: match.loaderData,
|
|
2488
|
-
}
|
|
2877
|
+
innerLoadContext.updateMatch(matchId, (prev) => {
|
|
2878
|
+
clearTimeout(prev._nonReactive.pendingTimeout)
|
|
2879
|
+
prev._nonReactive.pendingTimeout = undefined
|
|
2880
|
+
if (!loaderIsRunningAsync) prev._nonReactive.loaderPromise = undefined
|
|
2881
|
+
prev._nonReactive.dehydrated = undefined
|
|
2882
|
+
return {
|
|
2883
|
+
...prev,
|
|
2884
|
+
isFetching: loaderIsRunningAsync ? prev.isFetching : false,
|
|
2885
|
+
invalid: false,
|
|
2886
|
+
}
|
|
2887
|
+
})
|
|
2888
|
+
return this.getMatch(matchId)!
|
|
2889
|
+
}
|
|
2489
2890
|
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
meta,
|
|
2502
|
-
links,
|
|
2503
|
-
headScripts,
|
|
2504
|
-
headers,
|
|
2505
|
-
scripts,
|
|
2506
|
-
styles,
|
|
2507
|
-
}
|
|
2508
|
-
})
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
const potentialPendingMinPromise = async () => {
|
|
2512
|
-
const latestMatch = this.getMatch(matchId)!
|
|
2513
|
-
if (latestMatch._nonReactive.minPendingPromise) {
|
|
2514
|
-
await latestMatch._nonReactive.minPendingPromise
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
const prevMatch = this.getMatch(matchId)!
|
|
2519
|
-
if (shouldSkipLoader(matchId)) {
|
|
2520
|
-
if (this.isServer) {
|
|
2521
|
-
const headResult = executeHead()
|
|
2522
|
-
if (headResult) {
|
|
2523
|
-
const head = await headResult
|
|
2524
|
-
updateMatch(matchId, (prev) => ({
|
|
2525
|
-
...prev,
|
|
2526
|
-
...head,
|
|
2527
|
-
}))
|
|
2528
|
-
}
|
|
2529
|
-
return this.getMatch(matchId)!
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
// there is a loaderPromise, so we are in the middle of a load
|
|
2533
|
-
else if (prevMatch._nonReactive.loaderPromise) {
|
|
2534
|
-
// do not block if we already have stale data we can show
|
|
2535
|
-
// but only if the ongoing load is not a preload since error handling is different for preloads
|
|
2536
|
-
// and we don't want to swallow errors
|
|
2537
|
-
if (
|
|
2538
|
-
prevMatch.status === 'success' &&
|
|
2539
|
-
!sync &&
|
|
2540
|
-
!prevMatch.preload
|
|
2541
|
-
) {
|
|
2542
|
-
return this.getMatch(matchId)!
|
|
2543
|
-
}
|
|
2544
|
-
await prevMatch._nonReactive.loaderPromise
|
|
2545
|
-
const match = this.getMatch(matchId)!
|
|
2546
|
-
if (match.error) {
|
|
2547
|
-
handleRedirectAndNotFound(match, match.error)
|
|
2548
|
-
}
|
|
2549
|
-
} else {
|
|
2550
|
-
const parentMatchPromise = matchPromises[index - 1] as any
|
|
2551
|
-
|
|
2552
|
-
const getLoaderContext = (): LoaderFnContext => {
|
|
2553
|
-
const {
|
|
2554
|
-
params,
|
|
2555
|
-
loaderDeps,
|
|
2556
|
-
abortController,
|
|
2557
|
-
context,
|
|
2558
|
-
cause,
|
|
2559
|
-
} = this.getMatch(matchId)!
|
|
2560
|
-
|
|
2561
|
-
const preload = resolvePreload(matchId)
|
|
2562
|
-
|
|
2563
|
-
return {
|
|
2564
|
-
params,
|
|
2565
|
-
deps: loaderDeps,
|
|
2566
|
-
preload: !!preload,
|
|
2567
|
-
parentMatchPromise,
|
|
2568
|
-
abortController: abortController,
|
|
2569
|
-
context,
|
|
2570
|
-
location,
|
|
2571
|
-
navigate: (opts) =>
|
|
2572
|
-
this.navigate({ ...opts, _fromLocation: location }),
|
|
2573
|
-
cause: preload ? 'preload' : cause,
|
|
2574
|
-
route,
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2891
|
+
loadMatches = async (baseContext: {
|
|
2892
|
+
location: ParsedLocation
|
|
2893
|
+
matches: Array<AnyRouteMatch>
|
|
2894
|
+
preload?: boolean
|
|
2895
|
+
onReady?: () => Promise<void>
|
|
2896
|
+
updateMatch?: UpdateMatchFn
|
|
2897
|
+
sync?: boolean
|
|
2898
|
+
}): Promise<Array<MakeRouteMatch>> => {
|
|
2899
|
+
const innerLoadContext = baseContext as InnerLoadContext
|
|
2900
|
+
innerLoadContext.updateMatch ??= this.updateMatch
|
|
2901
|
+
innerLoadContext.matchPromises = []
|
|
2577
2902
|
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
const staleAge = preload
|
|
2584
|
-
? (route.options.preloadStaleTime ??
|
|
2585
|
-
this.options.defaultPreloadStaleTime ??
|
|
2586
|
-
30_000) // 30 seconds for preloads by default
|
|
2587
|
-
: (route.options.staleTime ??
|
|
2588
|
-
this.options.defaultStaleTime ??
|
|
2589
|
-
0)
|
|
2590
|
-
|
|
2591
|
-
const shouldReloadOption = route.options.shouldReload
|
|
2592
|
-
|
|
2593
|
-
// Default to reloading the route all the time
|
|
2594
|
-
// Allow shouldReload to get the last say,
|
|
2595
|
-
// if provided.
|
|
2596
|
-
const shouldReload =
|
|
2597
|
-
typeof shouldReloadOption === 'function'
|
|
2598
|
-
? shouldReloadOption(getLoaderContext())
|
|
2599
|
-
: shouldReloadOption
|
|
2600
|
-
|
|
2601
|
-
updateMatch(matchId, (prev) => {
|
|
2602
|
-
prev._nonReactive.loaderPromise =
|
|
2603
|
-
createControlledPromise<void>()
|
|
2604
|
-
return {
|
|
2605
|
-
...prev,
|
|
2606
|
-
preload:
|
|
2607
|
-
!!preload &&
|
|
2608
|
-
!this.state.matches.some((d) => d.id === matchId),
|
|
2609
|
-
}
|
|
2610
|
-
})
|
|
2611
|
-
|
|
2612
|
-
const runLoader = async () => {
|
|
2613
|
-
try {
|
|
2614
|
-
// If the Matches component rendered
|
|
2615
|
-
// the pending component and needs to show it for
|
|
2616
|
-
// a minimum duration, we''ll wait for it to resolve
|
|
2617
|
-
// before committing to the match and resolving
|
|
2618
|
-
// the loadPromise
|
|
2619
|
-
|
|
2620
|
-
// Actually run the loader and handle the result
|
|
2621
|
-
try {
|
|
2622
|
-
if (
|
|
2623
|
-
!this.isServer ||
|
|
2624
|
-
(this.isServer &&
|
|
2625
|
-
this.getMatch(matchId)!.ssr === true)
|
|
2626
|
-
) {
|
|
2627
|
-
this.loadRouteChunk(route)
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
updateMatch(matchId, (prev) => ({
|
|
2631
|
-
...prev,
|
|
2632
|
-
isFetching: 'loader',
|
|
2633
|
-
}))
|
|
2634
|
-
|
|
2635
|
-
// Kick off the loader!
|
|
2636
|
-
const loaderData =
|
|
2637
|
-
await route.options.loader?.(getLoaderContext())
|
|
2638
|
-
|
|
2639
|
-
handleRedirectAndNotFound(
|
|
2640
|
-
this.getMatch(matchId),
|
|
2641
|
-
loaderData,
|
|
2642
|
-
)
|
|
2643
|
-
updateMatch(matchId, (prev) => ({
|
|
2644
|
-
...prev,
|
|
2645
|
-
loaderData,
|
|
2646
|
-
}))
|
|
2647
|
-
|
|
2648
|
-
// Lazy option can modify the route options,
|
|
2649
|
-
// so we need to wait for it to resolve before
|
|
2650
|
-
// we can use the options
|
|
2651
|
-
await route._lazyPromise
|
|
2652
|
-
const headResult = executeHead()
|
|
2653
|
-
const head = headResult ? await headResult : undefined
|
|
2654
|
-
await potentialPendingMinPromise()
|
|
2655
|
-
|
|
2656
|
-
// Last but not least, wait for the the components
|
|
2657
|
-
// to be preloaded before we resolve the match
|
|
2658
|
-
await route._componentsPromise
|
|
2659
|
-
updateMatch(matchId, (prev) => ({
|
|
2660
|
-
...prev,
|
|
2661
|
-
error: undefined,
|
|
2662
|
-
status: 'success',
|
|
2663
|
-
isFetching: false,
|
|
2664
|
-
updatedAt: Date.now(),
|
|
2665
|
-
...head,
|
|
2666
|
-
}))
|
|
2667
|
-
} catch (e) {
|
|
2668
|
-
let error = e
|
|
2669
|
-
|
|
2670
|
-
await potentialPendingMinPromise()
|
|
2671
|
-
|
|
2672
|
-
handleRedirectAndNotFound(this.getMatch(matchId), e)
|
|
2673
|
-
|
|
2674
|
-
try {
|
|
2675
|
-
route.options.onError?.(e)
|
|
2676
|
-
} catch (onErrorError) {
|
|
2677
|
-
error = onErrorError
|
|
2678
|
-
handleRedirectAndNotFound(
|
|
2679
|
-
this.getMatch(matchId),
|
|
2680
|
-
onErrorError,
|
|
2681
|
-
)
|
|
2682
|
-
}
|
|
2683
|
-
const headResult = executeHead()
|
|
2684
|
-
const head = headResult ? await headResult : undefined
|
|
2685
|
-
updateMatch(matchId, (prev) => ({
|
|
2686
|
-
...prev,
|
|
2687
|
-
error,
|
|
2688
|
-
status: 'error',
|
|
2689
|
-
isFetching: false,
|
|
2690
|
-
...head,
|
|
2691
|
-
}))
|
|
2692
|
-
}
|
|
2693
|
-
} catch (err) {
|
|
2694
|
-
const match = this.getMatch(matchId)
|
|
2695
|
-
// in case of a redirecting match during preload, the match does not exist
|
|
2696
|
-
if (match) {
|
|
2697
|
-
const headResult = executeHead()
|
|
2698
|
-
if (headResult) {
|
|
2699
|
-
const head = await headResult
|
|
2700
|
-
updateMatch(matchId, (prev) => ({
|
|
2701
|
-
...prev,
|
|
2702
|
-
...head,
|
|
2703
|
-
}))
|
|
2704
|
-
}
|
|
2705
|
-
match._nonReactive.loaderPromise = undefined
|
|
2706
|
-
}
|
|
2707
|
-
handleRedirectAndNotFound(match, err)
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2903
|
+
// make sure the pending component is immediately rendered when hydrating a match that is not SSRed
|
|
2904
|
+
// the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
|
|
2905
|
+
if (!this.isServer && this.state.matches.some((d) => d._forcePending)) {
|
|
2906
|
+
this.triggerOnReady(innerLoadContext)
|
|
2907
|
+
}
|
|
2710
2908
|
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
if (isRedirect(err)) {
|
|
2729
|
-
await this.navigate(err.options)
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
})()
|
|
2733
|
-
} else if (
|
|
2734
|
-
status !== 'success' ||
|
|
2735
|
-
(loaderShouldRunAsync && sync)
|
|
2736
|
-
) {
|
|
2737
|
-
await runLoader()
|
|
2738
|
-
} else {
|
|
2739
|
-
// if the loader did not run, still update head.
|
|
2740
|
-
// reason: parent's beforeLoad may have changed the route context
|
|
2741
|
-
// and only now do we know the route context (and that the loader would not run)
|
|
2742
|
-
const headResult = executeHead()
|
|
2743
|
-
if (headResult) {
|
|
2744
|
-
const head = await headResult
|
|
2745
|
-
updateMatch(matchId, (prev) => ({
|
|
2746
|
-
...prev,
|
|
2747
|
-
...head,
|
|
2748
|
-
}))
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
if (!loaderIsRunningAsync) {
|
|
2753
|
-
const match = this.getMatch(matchId)!
|
|
2754
|
-
match._nonReactive.loaderPromise?.resolve()
|
|
2755
|
-
match._nonReactive.loadPromise?.resolve()
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
updateMatch(matchId, (prev) => {
|
|
2759
|
-
clearTimeout(prev._nonReactive.pendingTimeout)
|
|
2760
|
-
prev._nonReactive.pendingTimeout = undefined
|
|
2761
|
-
if (!loaderIsRunningAsync)
|
|
2762
|
-
prev._nonReactive.loaderPromise = undefined
|
|
2763
|
-
prev._nonReactive.dehydrated = undefined
|
|
2764
|
-
return {
|
|
2765
|
-
...prev,
|
|
2766
|
-
isFetching: loaderIsRunningAsync
|
|
2767
|
-
? prev.isFetching
|
|
2768
|
-
: false,
|
|
2769
|
-
invalid: false,
|
|
2770
|
-
}
|
|
2771
|
-
})
|
|
2772
|
-
return this.getMatch(matchId)!
|
|
2773
|
-
})(),
|
|
2909
|
+
try {
|
|
2910
|
+
await new Promise<void>((resolveAll, rejectAll) => {
|
|
2911
|
+
;(async () => {
|
|
2912
|
+
try {
|
|
2913
|
+
// Execute all beforeLoads one by one
|
|
2914
|
+
for (let i = 0; i < innerLoadContext.matches.length; i++) {
|
|
2915
|
+
const beforeLoad = this.handleBeforeLoad(innerLoadContext, i)
|
|
2916
|
+
if (isPromise(beforeLoad)) await beforeLoad
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
// Execute all loaders in parallel
|
|
2920
|
+
const max =
|
|
2921
|
+
innerLoadContext.firstBadMatchIndex ??
|
|
2922
|
+
innerLoadContext.matches.length
|
|
2923
|
+
for (let i = 0; i < max; i++) {
|
|
2924
|
+
innerLoadContext.matchPromises.push(
|
|
2925
|
+
this.loadRouteMatch(innerLoadContext, i),
|
|
2774
2926
|
)
|
|
2775
|
-
}
|
|
2927
|
+
}
|
|
2776
2928
|
|
|
2777
|
-
await Promise.all(matchPromises)
|
|
2929
|
+
await Promise.all(innerLoadContext.matchPromises)
|
|
2778
2930
|
|
|
2779
2931
|
resolveAll()
|
|
2780
2932
|
} catch (err) {
|
|
@@ -2782,18 +2934,20 @@ export class RouterCore<
|
|
|
2782
2934
|
}
|
|
2783
2935
|
})()
|
|
2784
2936
|
})
|
|
2785
|
-
|
|
2937
|
+
const readyPromise = this.triggerOnReady(innerLoadContext)
|
|
2938
|
+
if (isPromise(readyPromise)) await readyPromise
|
|
2786
2939
|
} catch (err) {
|
|
2787
|
-
if (
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2940
|
+
if (isNotFound(err) && !innerLoadContext.preload) {
|
|
2941
|
+
const readyPromise = this.triggerOnReady(innerLoadContext)
|
|
2942
|
+
if (isPromise(readyPromise)) await readyPromise
|
|
2943
|
+
throw err
|
|
2944
|
+
}
|
|
2945
|
+
if (isRedirect(err)) {
|
|
2792
2946
|
throw err
|
|
2793
2947
|
}
|
|
2794
2948
|
}
|
|
2795
2949
|
|
|
2796
|
-
return matches
|
|
2950
|
+
return innerLoadContext.matches
|
|
2797
2951
|
}
|
|
2798
2952
|
|
|
2799
2953
|
invalidate: InvalidateFn<
|
|
@@ -2890,33 +3044,44 @@ export class RouterCore<
|
|
|
2890
3044
|
}
|
|
2891
3045
|
|
|
2892
3046
|
loadRouteChunk = (route: AnyRoute) => {
|
|
2893
|
-
if (route._lazyPromise === undefined) {
|
|
3047
|
+
if (!route._lazyLoaded && route._lazyPromise === undefined) {
|
|
2894
3048
|
if (route.lazyFn) {
|
|
2895
3049
|
route._lazyPromise = route.lazyFn().then((lazyRoute) => {
|
|
2896
3050
|
// explicitly don't copy over the lazy route's id
|
|
2897
3051
|
const { id: _id, ...options } = lazyRoute.options
|
|
2898
3052
|
Object.assign(route.options, options)
|
|
3053
|
+
route._lazyLoaded = true
|
|
3054
|
+
route._lazyPromise = undefined // gc promise, we won't need it anymore
|
|
2899
3055
|
})
|
|
2900
3056
|
} else {
|
|
2901
|
-
route.
|
|
3057
|
+
route._lazyLoaded = true
|
|
2902
3058
|
}
|
|
2903
3059
|
}
|
|
2904
3060
|
|
|
2905
3061
|
// If for some reason lazy resolves more lazy components...
|
|
2906
|
-
// We'll wait for that before
|
|
3062
|
+
// We'll wait for that before we attempt to preload the
|
|
2907
3063
|
// components themselves.
|
|
2908
|
-
if (route._componentsPromise === undefined) {
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
3064
|
+
if (!route._componentsLoaded && route._componentsPromise === undefined) {
|
|
3065
|
+
const loadComponents = () => {
|
|
3066
|
+
const preloads = []
|
|
3067
|
+
for (const type of componentTypes) {
|
|
3068
|
+
const preload = (route.options[type] as any)?.preload
|
|
3069
|
+
if (preload) preloads.push(preload())
|
|
3070
|
+
}
|
|
3071
|
+
if (preloads.length)
|
|
3072
|
+
return Promise.all(preloads).then(() => {
|
|
3073
|
+
route._componentsLoaded = true
|
|
3074
|
+
route._componentsPromise = undefined // gc promise, we won't need it anymore
|
|
3075
|
+
})
|
|
3076
|
+
route._componentsLoaded = true
|
|
3077
|
+
route._componentsPromise = undefined // gc promise, we won't need it anymore
|
|
3078
|
+
return
|
|
3079
|
+
}
|
|
3080
|
+
route._componentsPromise = route._lazyPromise
|
|
3081
|
+
? route._lazyPromise.then(loadComponents)
|
|
3082
|
+
: loadComponents()
|
|
2919
3083
|
}
|
|
3084
|
+
|
|
2920
3085
|
return route._componentsPromise
|
|
2921
3086
|
}
|
|
2922
3087
|
|
|
@@ -3056,13 +3221,9 @@ export class RouterCore<
|
|
|
3056
3221
|
|
|
3057
3222
|
serverSsr?: ServerSsr
|
|
3058
3223
|
|
|
3059
|
-
_handleNotFound = (
|
|
3060
|
-
|
|
3224
|
+
private _handleNotFound = (
|
|
3225
|
+
innerLoadContext: InnerLoadContext,
|
|
3061
3226
|
err: NotFoundError,
|
|
3062
|
-
updateMatch: (
|
|
3063
|
-
id: string,
|
|
3064
|
-
updater: (match: AnyRouteMatch) => AnyRouteMatch,
|
|
3065
|
-
) => void = this.updateMatch,
|
|
3066
3227
|
) => {
|
|
3067
3228
|
// Find the route that should handle the not found error
|
|
3068
3229
|
// First check if a specific route is requested to show the error
|
|
@@ -3070,7 +3231,7 @@ export class RouterCore<
|
|
|
3070
3231
|
const matchesByRouteId: Record<string, AnyRouteMatch> = {}
|
|
3071
3232
|
|
|
3072
3233
|
// Setup routesByRouteId object for quick access
|
|
3073
|
-
for (const match of matches) {
|
|
3234
|
+
for (const match of innerLoadContext.matches) {
|
|
3074
3235
|
matchesByRouteId[match.routeId] = match
|
|
3075
3236
|
}
|
|
3076
3237
|
|
|
@@ -3099,7 +3260,7 @@ export class RouterCore<
|
|
|
3099
3260
|
)
|
|
3100
3261
|
|
|
3101
3262
|
// Assign the error to the match - using non-null assertion since we've checked with invariant
|
|
3102
|
-
updateMatch(matchForRoute.id, (prev) => ({
|
|
3263
|
+
innerLoadContext.updateMatch(matchForRoute.id, (prev) => ({
|
|
3103
3264
|
...prev,
|
|
3104
3265
|
status: 'notFound',
|
|
3105
3266
|
error: err,
|
|
@@ -3108,7 +3269,7 @@ export class RouterCore<
|
|
|
3108
3269
|
|
|
3109
3270
|
if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
|
|
3110
3271
|
err.routeId = routeCursor.parentRoute.id
|
|
3111
|
-
this._handleNotFound(
|
|
3272
|
+
this._handleNotFound(innerLoadContext, err)
|
|
3112
3273
|
}
|
|
3113
3274
|
}
|
|
3114
3275
|
|
|
@@ -3123,6 +3284,16 @@ export class SearchParamError extends Error {}
|
|
|
3123
3284
|
|
|
3124
3285
|
export class PathParamError extends Error {}
|
|
3125
3286
|
|
|
3287
|
+
function makeMaybe<TValue, TError>(
|
|
3288
|
+
value: TValue,
|
|
3289
|
+
error: TError,
|
|
3290
|
+
): { status: 'success'; value: TValue } | { status: 'error'; error: TError } {
|
|
3291
|
+
if (error) {
|
|
3292
|
+
return { status: 'error' as const, error }
|
|
3293
|
+
}
|
|
3294
|
+
return { status: 'success' as const, value }
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3126
3297
|
const normalize = (str: string) =>
|
|
3127
3298
|
str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str
|
|
3128
3299
|
function comparePaths(a: string, b: string) {
|