@tanstack/router-core 1.131.14 → 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/router.cjs +570 -491
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +18 -5
- package/dist/esm/router.d.ts +18 -5
- package/dist/esm/router.js +570 -491
- package/dist/esm/router.js.map +1 -1
- package/package.json +1 -1
- package/src/router.ts +822 -683
package/src/router.ts
CHANGED
|
@@ -763,6 +763,18 @@ export type CreateRouterFn = <
|
|
|
763
763
|
TDehydrated
|
|
764
764
|
>
|
|
765
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
|
+
|
|
766
778
|
export class RouterCore<
|
|
767
779
|
in out TRouteTree extends AnyRoute,
|
|
768
780
|
in out TTrailingSlashOption extends TrailingSlashOption,
|
|
@@ -2083,719 +2095,838 @@ export class RouterCore<
|
|
|
2083
2095
|
)
|
|
2084
2096
|
}
|
|
2085
2097
|
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
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
|
+
}
|
|
2127
|
+
|
|
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
|
+
}))
|
|
2107
2143
|
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
rendered = true
|
|
2111
|
-
await onReady?.()
|
|
2144
|
+
if (isNotFound(err) && !err.routeId) {
|
|
2145
|
+
err.routeId = match.routeId
|
|
2112
2146
|
}
|
|
2147
|
+
|
|
2148
|
+
match._nonReactive.loadPromise?.resolve()
|
|
2113
2149
|
}
|
|
2114
2150
|
|
|
2115
|
-
|
|
2116
|
-
|
|
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
|
|
2117
2160
|
}
|
|
2161
|
+
}
|
|
2118
2162
|
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
if
|
|
2122
|
-
|
|
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
|
|
2123
2168
|
}
|
|
2124
2169
|
|
|
2125
|
-
|
|
2126
|
-
match
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2170
|
+
if (this.isServer) {
|
|
2171
|
+
if (match.ssr === false) {
|
|
2172
|
+
return true
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return false
|
|
2176
|
+
}
|
|
2130
2177
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
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')
|
|
2135
2356
|
) {
|
|
2136
|
-
|
|
2357
|
+
this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
|
|
2137
2358
|
}
|
|
2359
|
+
return shouldExecuteBeforeLoad
|
|
2360
|
+
}
|
|
2138
2361
|
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
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
|
+
}
|
|
2145
2367
|
|
|
2146
|
-
|
|
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
|
|
2147
2378
|
|
|
2148
|
-
|
|
2379
|
+
return {
|
|
2149
2380
|
...prev,
|
|
2150
|
-
status,
|
|
2151
2381
|
isFetching: false,
|
|
2152
|
-
|
|
2153
|
-
|
|
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)!
|
|
2154
2438
|
|
|
2155
|
-
|
|
2156
|
-
|
|
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
|
+
}
|
|
2460
|
+
|
|
2461
|
+
const updateContext = (beforeLoadContext: any) => {
|
|
2462
|
+
if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
|
|
2463
|
+
this.handleSerialError(
|
|
2464
|
+
innerLoadContext,
|
|
2465
|
+
index,
|
|
2466
|
+
beforeLoadContext,
|
|
2467
|
+
'BEFORE_LOAD',
|
|
2468
|
+
)
|
|
2157
2469
|
}
|
|
2158
2470
|
|
|
2159
|
-
|
|
2471
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2472
|
+
...prev,
|
|
2473
|
+
__beforeLoadContext: beforeLoadContext,
|
|
2474
|
+
context: {
|
|
2475
|
+
...parentMatchContext,
|
|
2476
|
+
...prev.__routeContext,
|
|
2477
|
+
...beforeLoadContext,
|
|
2478
|
+
},
|
|
2479
|
+
abortController,
|
|
2480
|
+
}))
|
|
2160
2481
|
}
|
|
2161
2482
|
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
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)
|
|
2168
2491
|
} else {
|
|
2169
|
-
|
|
2170
|
-
throw err
|
|
2492
|
+
updateContext(beforeLoadContext)
|
|
2171
2493
|
}
|
|
2494
|
+
} catch (err) {
|
|
2495
|
+
this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
|
|
2172
2496
|
}
|
|
2173
2497
|
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2498
|
+
resolve()
|
|
2499
|
+
return
|
|
2500
|
+
}
|
|
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]!
|
|
2180
2508
|
|
|
2509
|
+
const serverSsr = () => {
|
|
2510
|
+
// on the server, determine whether SSR the current match or not
|
|
2181
2511
|
if (this.isServer) {
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2512
|
+
const maybePromise = this.isBeforeLoadSsr(
|
|
2513
|
+
innerLoadContext,
|
|
2514
|
+
matchId,
|
|
2515
|
+
index,
|
|
2516
|
+
route,
|
|
2517
|
+
)
|
|
2518
|
+
if (isPromise(maybePromise)) return maybePromise.then(queueExecution)
|
|
2185
2519
|
}
|
|
2186
|
-
return
|
|
2520
|
+
return queueExecution()
|
|
2521
|
+
}
|
|
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
|
|
2187
2541
|
}
|
|
2188
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> => {
|
|
2189
2636
|
try {
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
err: any,
|
|
2196
|
-
routerCode: string,
|
|
2197
|
-
) => {
|
|
2198
|
-
const { id: matchId, routeId } = matches[index]!
|
|
2199
|
-
const route = this.looseRoutesById[routeId]!
|
|
2200
|
-
|
|
2201
|
-
// Much like suspense, we use a promise here to know if
|
|
2202
|
-
// we've been outdated by a new loadMatches call and
|
|
2203
|
-
// should abort the current async operation
|
|
2204
|
-
if (err instanceof Promise) {
|
|
2205
|
-
throw err
|
|
2206
|
-
}
|
|
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
|
|
2207
2642
|
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
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
|
+
}
|
|
2211
2648
|
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
+
)
|
|
2218
2665
|
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
...prev,
|
|
2226
|
-
error: err,
|
|
2227
|
-
status: 'error',
|
|
2228
|
-
isFetching: false,
|
|
2229
|
-
updatedAt: Date.now(),
|
|
2230
|
-
abortController: new AbortController(),
|
|
2231
|
-
}
|
|
2232
|
-
})
|
|
2233
|
-
}
|
|
2666
|
+
if (willLoadSomething) {
|
|
2667
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2668
|
+
...prev,
|
|
2669
|
+
isFetching: 'loader',
|
|
2670
|
+
}))
|
|
2671
|
+
}
|
|
2234
2672
|
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
? this.getMatch(parentMatchId)!
|
|
2240
|
-
: undefined
|
|
2241
|
-
|
|
2242
|
-
const route = this.looseRoutesById[routeId]!
|
|
2243
|
-
|
|
2244
|
-
const pendingMs =
|
|
2245
|
-
route.options.pendingMs ?? this.options.defaultPendingMs
|
|
2246
|
-
|
|
2247
|
-
// on the server, determine whether SSR the current match or not
|
|
2248
|
-
if (this.isServer) {
|
|
2249
|
-
let ssr: boolean | 'data-only'
|
|
2250
|
-
// in SPA mode, only SSR the root route
|
|
2251
|
-
if (this.isShell()) {
|
|
2252
|
-
ssr = matchId === rootRouteId
|
|
2253
|
-
} else {
|
|
2254
|
-
const defaultSsr = this.options.defaultSsr ?? true
|
|
2255
|
-
if (parentMatch?.ssr === false) {
|
|
2256
|
-
ssr = false
|
|
2257
|
-
} else {
|
|
2258
|
-
let tempSsr: boolean | 'data-only'
|
|
2259
|
-
if (route.options.ssr === undefined) {
|
|
2260
|
-
tempSsr = defaultSsr
|
|
2261
|
-
} else if (typeof route.options.ssr === 'function') {
|
|
2262
|
-
const { search, params } = this.getMatch(matchId)!
|
|
2263
|
-
|
|
2264
|
-
function makeMaybe(value: any, error: any) {
|
|
2265
|
-
if (error) {
|
|
2266
|
-
return { status: 'error' as const, error }
|
|
2267
|
-
}
|
|
2268
|
-
return { status: 'success' as const, value }
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
const ssrFnContext: SsrContextOptions<any, any, any> = {
|
|
2272
|
-
search: makeMaybe(search, existingMatch.searchError),
|
|
2273
|
-
params: makeMaybe(params, existingMatch.paramsError),
|
|
2274
|
-
location,
|
|
2275
|
-
matches: matches.map((match) => ({
|
|
2276
|
-
index: match.index,
|
|
2277
|
-
pathname: match.pathname,
|
|
2278
|
-
fullPath: match.fullPath,
|
|
2279
|
-
staticData: match.staticData,
|
|
2280
|
-
id: match.id,
|
|
2281
|
-
routeId: match.routeId,
|
|
2282
|
-
search: makeMaybe(match.search, match.searchError),
|
|
2283
|
-
params: makeMaybe(match.params, match.paramsError),
|
|
2284
|
-
ssr: match.ssr,
|
|
2285
|
-
})),
|
|
2286
|
-
}
|
|
2287
|
-
tempSsr =
|
|
2288
|
-
(await route.options.ssr(ssrFnContext)) ?? defaultSsr
|
|
2289
|
-
} else {
|
|
2290
|
-
tempSsr = route.options.ssr
|
|
2291
|
-
}
|
|
2673
|
+
if (route.options.loader) {
|
|
2674
|
+
const loaderData = loaderResultIsPromise
|
|
2675
|
+
? await loaderResult
|
|
2676
|
+
: loaderResult
|
|
2292
2677
|
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2678
|
+
this.handleRedirectAndNotFound(
|
|
2679
|
+
innerLoadContext,
|
|
2680
|
+
this.getMatch(matchId),
|
|
2681
|
+
loaderData,
|
|
2682
|
+
)
|
|
2683
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2684
|
+
...prev,
|
|
2685
|
+
loaderData,
|
|
2686
|
+
}))
|
|
2687
|
+
}
|
|
2302
2688
|
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
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
|
|
2306
2711
|
|
|
2307
|
-
|
|
2308
|
-
onReady &&
|
|
2309
|
-
!this.isServer &&
|
|
2310
|
-
!resolvePreload(matchId) &&
|
|
2311
|
-
(route.options.loader ||
|
|
2312
|
-
route.options.beforeLoad ||
|
|
2313
|
-
routeNeedsPreload(route)) &&
|
|
2314
|
-
typeof pendingMs === 'number' &&
|
|
2315
|
-
pendingMs !== Infinity &&
|
|
2316
|
-
(route.options.pendingComponent ??
|
|
2317
|
-
(this.options as any)?.defaultPendingComponent)
|
|
2318
|
-
)
|
|
2712
|
+
await this.potentialPendingMinPromise(matchId)
|
|
2319
2713
|
|
|
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
|
-
|
|
2455
|
-
isFetching: false,
|
|
2456
|
-
}
|
|
2457
|
-
})
|
|
2458
|
-
}
|
|
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)
|
|
2459
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
|
+
}
|
|
2460
2876
|
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
if (!match) {
|
|
2475
|
-
return
|
|
2476
|
-
}
|
|
2477
|
-
if (
|
|
2478
|
-
!route.options.head &&
|
|
2479
|
-
!route.options.scripts &&
|
|
2480
|
-
!route.options.headers
|
|
2481
|
-
) {
|
|
2482
|
-
return
|
|
2483
|
-
}
|
|
2484
|
-
const assetContext = {
|
|
2485
|
-
matches,
|
|
2486
|
-
match,
|
|
2487
|
-
params: match.params,
|
|
2488
|
-
loaderData: match.loaderData,
|
|
2489
|
-
}
|
|
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
|
+
}
|
|
2490
2890
|
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
meta,
|
|
2503
|
-
links,
|
|
2504
|
-
headScripts,
|
|
2505
|
-
headers,
|
|
2506
|
-
scripts,
|
|
2507
|
-
styles,
|
|
2508
|
-
}
|
|
2509
|
-
})
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
const potentialPendingMinPromise = () => {
|
|
2513
|
-
const latestMatch = this.getMatch(matchId)!
|
|
2514
|
-
return latestMatch._nonReactive.minPendingPromise
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
const prevMatch = this.getMatch(matchId)!
|
|
2518
|
-
if (shouldSkipLoader(matchId)) {
|
|
2519
|
-
if (this.isServer) {
|
|
2520
|
-
const headResult = executeHead()
|
|
2521
|
-
if (headResult) {
|
|
2522
|
-
const head = await headResult
|
|
2523
|
-
updateMatch(matchId, (prev) => ({
|
|
2524
|
-
...prev,
|
|
2525
|
-
...head,
|
|
2526
|
-
}))
|
|
2527
|
-
}
|
|
2528
|
-
return this.getMatch(matchId)!
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
// there is a loaderPromise, so we are in the middle of a load
|
|
2532
|
-
else if (prevMatch._nonReactive.loaderPromise) {
|
|
2533
|
-
// do not block if we already have stale data we can show
|
|
2534
|
-
// but only if the ongoing load is not a preload since error handling is different for preloads
|
|
2535
|
-
// and we don't want to swallow errors
|
|
2536
|
-
if (
|
|
2537
|
-
prevMatch.status === 'success' &&
|
|
2538
|
-
!sync &&
|
|
2539
|
-
!prevMatch.preload
|
|
2540
|
-
) {
|
|
2541
|
-
return this.getMatch(matchId)!
|
|
2542
|
-
}
|
|
2543
|
-
await prevMatch._nonReactive.loaderPromise
|
|
2544
|
-
const match = this.getMatch(matchId)!
|
|
2545
|
-
if (match.error) {
|
|
2546
|
-
handleRedirectAndNotFound(match, match.error)
|
|
2547
|
-
}
|
|
2548
|
-
} else {
|
|
2549
|
-
const parentMatchPromise = matchPromises[index - 1] as any
|
|
2550
|
-
|
|
2551
|
-
const getLoaderContext = (): LoaderFnContext => {
|
|
2552
|
-
const {
|
|
2553
|
-
params,
|
|
2554
|
-
loaderDeps,
|
|
2555
|
-
abortController,
|
|
2556
|
-
context,
|
|
2557
|
-
cause,
|
|
2558
|
-
} = this.getMatch(matchId)!
|
|
2559
|
-
|
|
2560
|
-
const preload = resolvePreload(matchId)
|
|
2561
|
-
|
|
2562
|
-
return {
|
|
2563
|
-
params,
|
|
2564
|
-
deps: loaderDeps,
|
|
2565
|
-
preload: !!preload,
|
|
2566
|
-
parentMatchPromise,
|
|
2567
|
-
abortController: abortController,
|
|
2568
|
-
context,
|
|
2569
|
-
location,
|
|
2570
|
-
navigate: (opts) =>
|
|
2571
|
-
this.navigate({ ...opts, _fromLocation: location }),
|
|
2572
|
-
cause: preload ? 'preload' : cause,
|
|
2573
|
-
route,
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
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 = []
|
|
2576
2902
|
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
const staleAge = preload
|
|
2583
|
-
? (route.options.preloadStaleTime ??
|
|
2584
|
-
this.options.defaultPreloadStaleTime ??
|
|
2585
|
-
30_000) // 30 seconds for preloads by default
|
|
2586
|
-
: (route.options.staleTime ??
|
|
2587
|
-
this.options.defaultStaleTime ??
|
|
2588
|
-
0)
|
|
2589
|
-
|
|
2590
|
-
const shouldReloadOption = route.options.shouldReload
|
|
2591
|
-
|
|
2592
|
-
// Default to reloading the route all the time
|
|
2593
|
-
// Allow shouldReload to get the last say,
|
|
2594
|
-
// if provided.
|
|
2595
|
-
const shouldReload =
|
|
2596
|
-
typeof shouldReloadOption === 'function'
|
|
2597
|
-
? shouldReloadOption(getLoaderContext())
|
|
2598
|
-
: shouldReloadOption
|
|
2599
|
-
|
|
2600
|
-
updateMatch(matchId, (prev) => {
|
|
2601
|
-
prev._nonReactive.loaderPromise =
|
|
2602
|
-
createControlledPromise<void>()
|
|
2603
|
-
return {
|
|
2604
|
-
...prev,
|
|
2605
|
-
preload:
|
|
2606
|
-
!!preload &&
|
|
2607
|
-
!this.state.matches.some((d) => d.id === matchId),
|
|
2608
|
-
}
|
|
2609
|
-
})
|
|
2610
|
-
|
|
2611
|
-
const runLoader = async () => {
|
|
2612
|
-
try {
|
|
2613
|
-
// If the Matches component rendered
|
|
2614
|
-
// the pending component and needs to show it for
|
|
2615
|
-
// a minimum duration, we''ll wait for it to resolve
|
|
2616
|
-
// before committing to the match and resolving
|
|
2617
|
-
// the loadPromise
|
|
2618
|
-
|
|
2619
|
-
// Actually run the loader and handle the result
|
|
2620
|
-
try {
|
|
2621
|
-
if (
|
|
2622
|
-
!this.isServer ||
|
|
2623
|
-
this.getMatch(matchId)!.ssr === true
|
|
2624
|
-
) {
|
|
2625
|
-
this.loadRouteChunk(route)
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
// Kick off the loader!
|
|
2629
|
-
const loaderResult =
|
|
2630
|
-
route.options.loader?.(getLoaderContext())
|
|
2631
|
-
const loaderResultIsPromise =
|
|
2632
|
-
route.options.loader && isPromise(loaderResult)
|
|
2633
|
-
|
|
2634
|
-
const willLoadSomething = !!(
|
|
2635
|
-
loaderResultIsPromise ||
|
|
2636
|
-
route._lazyPromise ||
|
|
2637
|
-
route._componentsPromise ||
|
|
2638
|
-
route.options.head ||
|
|
2639
|
-
route.options.scripts ||
|
|
2640
|
-
route.options.headers ||
|
|
2641
|
-
this.getMatch(matchId)!._nonReactive
|
|
2642
|
-
.minPendingPromise
|
|
2643
|
-
)
|
|
2644
|
-
|
|
2645
|
-
if (willLoadSomething) {
|
|
2646
|
-
updateMatch(matchId, (prev) => ({
|
|
2647
|
-
...prev,
|
|
2648
|
-
isFetching: 'loader',
|
|
2649
|
-
}))
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
if (route.options.loader) {
|
|
2653
|
-
const loaderData = loaderResultIsPromise
|
|
2654
|
-
? await loaderResult
|
|
2655
|
-
: loaderResult
|
|
2656
|
-
|
|
2657
|
-
handleRedirectAndNotFound(
|
|
2658
|
-
this.getMatch(matchId),
|
|
2659
|
-
loaderData,
|
|
2660
|
-
)
|
|
2661
|
-
updateMatch(matchId, (prev) => ({
|
|
2662
|
-
...prev,
|
|
2663
|
-
loaderData,
|
|
2664
|
-
}))
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
// Lazy option can modify the route options,
|
|
2668
|
-
// so we need to wait for it to resolve before
|
|
2669
|
-
// we can use the options
|
|
2670
|
-
if (route._lazyPromise) await route._lazyPromise
|
|
2671
|
-
const headResult = executeHead()
|
|
2672
|
-
const head = headResult ? await headResult : undefined
|
|
2673
|
-
const pendingPromise = potentialPendingMinPromise()
|
|
2674
|
-
if (pendingPromise) await pendingPromise
|
|
2675
|
-
|
|
2676
|
-
// Last but not least, wait for the the components
|
|
2677
|
-
// to be preloaded before we resolve the match
|
|
2678
|
-
if (route._componentsPromise)
|
|
2679
|
-
await route._componentsPromise
|
|
2680
|
-
updateMatch(matchId, (prev) => ({
|
|
2681
|
-
...prev,
|
|
2682
|
-
error: undefined,
|
|
2683
|
-
status: 'success',
|
|
2684
|
-
isFetching: false,
|
|
2685
|
-
updatedAt: Date.now(),
|
|
2686
|
-
...head,
|
|
2687
|
-
}))
|
|
2688
|
-
} catch (e) {
|
|
2689
|
-
let error = e
|
|
2690
|
-
|
|
2691
|
-
await potentialPendingMinPromise()
|
|
2692
|
-
|
|
2693
|
-
handleRedirectAndNotFound(this.getMatch(matchId), e)
|
|
2694
|
-
|
|
2695
|
-
try {
|
|
2696
|
-
route.options.onError?.(e)
|
|
2697
|
-
} catch (onErrorError) {
|
|
2698
|
-
error = onErrorError
|
|
2699
|
-
handleRedirectAndNotFound(
|
|
2700
|
-
this.getMatch(matchId),
|
|
2701
|
-
onErrorError,
|
|
2702
|
-
)
|
|
2703
|
-
}
|
|
2704
|
-
const headResult = executeHead()
|
|
2705
|
-
const head = headResult ? await headResult : undefined
|
|
2706
|
-
updateMatch(matchId, (prev) => ({
|
|
2707
|
-
...prev,
|
|
2708
|
-
error,
|
|
2709
|
-
status: 'error',
|
|
2710
|
-
isFetching: false,
|
|
2711
|
-
...head,
|
|
2712
|
-
}))
|
|
2713
|
-
}
|
|
2714
|
-
} catch (err) {
|
|
2715
|
-
const match = this.getMatch(matchId)
|
|
2716
|
-
// in case of a redirecting match during preload, the match does not exist
|
|
2717
|
-
if (match) {
|
|
2718
|
-
const headResult = executeHead()
|
|
2719
|
-
if (headResult) {
|
|
2720
|
-
const head = await headResult
|
|
2721
|
-
updateMatch(matchId, (prev) => ({
|
|
2722
|
-
...prev,
|
|
2723
|
-
...head,
|
|
2724
|
-
}))
|
|
2725
|
-
}
|
|
2726
|
-
match._nonReactive.loaderPromise = undefined
|
|
2727
|
-
}
|
|
2728
|
-
handleRedirectAndNotFound(match, err)
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
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
|
+
}
|
|
2731
2908
|
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
if (isRedirect(err)) {
|
|
2750
|
-
await this.navigate(err.options)
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
})()
|
|
2754
|
-
} else if (
|
|
2755
|
-
status !== 'success' ||
|
|
2756
|
-
(loaderShouldRunAsync && sync)
|
|
2757
|
-
) {
|
|
2758
|
-
await runLoader()
|
|
2759
|
-
} else {
|
|
2760
|
-
// if the loader did not run, still update head.
|
|
2761
|
-
// reason: parent's beforeLoad may have changed the route context
|
|
2762
|
-
// and only now do we know the route context (and that the loader would not run)
|
|
2763
|
-
const headResult = executeHead()
|
|
2764
|
-
if (headResult) {
|
|
2765
|
-
const head = await headResult
|
|
2766
|
-
updateMatch(matchId, (prev) => ({
|
|
2767
|
-
...prev,
|
|
2768
|
-
...head,
|
|
2769
|
-
}))
|
|
2770
|
-
}
|
|
2771
|
-
}
|
|
2772
|
-
}
|
|
2773
|
-
if (!loaderIsRunningAsync) {
|
|
2774
|
-
const match = this.getMatch(matchId)!
|
|
2775
|
-
match._nonReactive.loaderPromise?.resolve()
|
|
2776
|
-
match._nonReactive.loadPromise?.resolve()
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
|
-
updateMatch(matchId, (prev) => {
|
|
2780
|
-
clearTimeout(prev._nonReactive.pendingTimeout)
|
|
2781
|
-
prev._nonReactive.pendingTimeout = undefined
|
|
2782
|
-
if (!loaderIsRunningAsync)
|
|
2783
|
-
prev._nonReactive.loaderPromise = undefined
|
|
2784
|
-
prev._nonReactive.dehydrated = undefined
|
|
2785
|
-
return {
|
|
2786
|
-
...prev,
|
|
2787
|
-
isFetching: loaderIsRunningAsync
|
|
2788
|
-
? prev.isFetching
|
|
2789
|
-
: false,
|
|
2790
|
-
invalid: false,
|
|
2791
|
-
}
|
|
2792
|
-
})
|
|
2793
|
-
return this.getMatch(matchId)!
|
|
2794
|
-
})(),
|
|
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),
|
|
2795
2926
|
)
|
|
2796
|
-
}
|
|
2927
|
+
}
|
|
2797
2928
|
|
|
2798
|
-
await Promise.all(matchPromises)
|
|
2929
|
+
await Promise.all(innerLoadContext.matchPromises)
|
|
2799
2930
|
|
|
2800
2931
|
resolveAll()
|
|
2801
2932
|
} catch (err) {
|
|
@@ -2803,18 +2934,20 @@ export class RouterCore<
|
|
|
2803
2934
|
}
|
|
2804
2935
|
})()
|
|
2805
2936
|
})
|
|
2806
|
-
|
|
2937
|
+
const readyPromise = this.triggerOnReady(innerLoadContext)
|
|
2938
|
+
if (isPromise(readyPromise)) await readyPromise
|
|
2807
2939
|
} catch (err) {
|
|
2808
|
-
if (
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
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)) {
|
|
2813
2946
|
throw err
|
|
2814
2947
|
}
|
|
2815
2948
|
}
|
|
2816
2949
|
|
|
2817
|
-
return matches
|
|
2950
|
+
return innerLoadContext.matches
|
|
2818
2951
|
}
|
|
2819
2952
|
|
|
2820
2953
|
invalidate: InvalidateFn<
|
|
@@ -3088,13 +3221,9 @@ export class RouterCore<
|
|
|
3088
3221
|
|
|
3089
3222
|
serverSsr?: ServerSsr
|
|
3090
3223
|
|
|
3091
|
-
_handleNotFound = (
|
|
3092
|
-
|
|
3224
|
+
private _handleNotFound = (
|
|
3225
|
+
innerLoadContext: InnerLoadContext,
|
|
3093
3226
|
err: NotFoundError,
|
|
3094
|
-
updateMatch: (
|
|
3095
|
-
id: string,
|
|
3096
|
-
updater: (match: AnyRouteMatch) => AnyRouteMatch,
|
|
3097
|
-
) => void = this.updateMatch,
|
|
3098
3227
|
) => {
|
|
3099
3228
|
// Find the route that should handle the not found error
|
|
3100
3229
|
// First check if a specific route is requested to show the error
|
|
@@ -3102,7 +3231,7 @@ export class RouterCore<
|
|
|
3102
3231
|
const matchesByRouteId: Record<string, AnyRouteMatch> = {}
|
|
3103
3232
|
|
|
3104
3233
|
// Setup routesByRouteId object for quick access
|
|
3105
|
-
for (const match of matches) {
|
|
3234
|
+
for (const match of innerLoadContext.matches) {
|
|
3106
3235
|
matchesByRouteId[match.routeId] = match
|
|
3107
3236
|
}
|
|
3108
3237
|
|
|
@@ -3131,7 +3260,7 @@ export class RouterCore<
|
|
|
3131
3260
|
)
|
|
3132
3261
|
|
|
3133
3262
|
// Assign the error to the match - using non-null assertion since we've checked with invariant
|
|
3134
|
-
updateMatch(matchForRoute.id, (prev) => ({
|
|
3263
|
+
innerLoadContext.updateMatch(matchForRoute.id, (prev) => ({
|
|
3135
3264
|
...prev,
|
|
3136
3265
|
status: 'notFound',
|
|
3137
3266
|
error: err,
|
|
@@ -3140,7 +3269,7 @@ export class RouterCore<
|
|
|
3140
3269
|
|
|
3141
3270
|
if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
|
|
3142
3271
|
err.routeId = routeCursor.parentRoute.id
|
|
3143
|
-
this._handleNotFound(
|
|
3272
|
+
this._handleNotFound(innerLoadContext, err)
|
|
3144
3273
|
}
|
|
3145
3274
|
}
|
|
3146
3275
|
|
|
@@ -3155,6 +3284,16 @@ export class SearchParamError extends Error {}
|
|
|
3155
3284
|
|
|
3156
3285
|
export class PathParamError extends Error {}
|
|
3157
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
|
+
|
|
3158
3297
|
const normalize = (str: string) =>
|
|
3159
3298
|
str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str
|
|
3160
3299
|
function comparePaths(a: string, b: string) {
|