@tanstack/router-core 1.132.0-alpha.0 → 1.132.0-alpha.2
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/Matches.cjs.map +1 -1
- package/dist/cjs/Matches.d.cts +7 -9
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +0 -4
- package/dist/cjs/router.cjs +629 -528
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +19 -8
- package/dist/cjs/scroll-restoration.d.cts +0 -9
- package/dist/cjs/ssr/ssr-client.cjs +38 -39
- package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
- package/dist/cjs/typePrimitives.d.cts +6 -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/Matches.d.ts +7 -9
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/route.d.ts +0 -4
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +19 -8
- package/dist/esm/router.js +630 -529
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/scroll-restoration.d.ts +0 -9
- package/dist/esm/ssr/ssr-client.js +38 -39
- package/dist/esm/ssr/ssr-client.js.map +1 -1
- package/dist/esm/typePrimitives.d.ts +6 -6
- 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 +2 -2
- package/src/Matches.ts +16 -8
- package/src/route.ts +10 -2
- package/src/router.ts +909 -717
- package/src/ssr/ssr-client.ts +42 -41
- package/src/typePrimitives.ts +6 -6
- 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,
|
|
@@ -1139,8 +1152,8 @@ export class RouterCore<
|
|
|
1139
1152
|
const parentMatchId = parentMatch?.id
|
|
1140
1153
|
|
|
1141
1154
|
const parentContext = !parentMatchId
|
|
1142
|
-
? ((this.options.context as any) ??
|
|
1143
|
-
: (parentMatch.context ?? this.options.context ??
|
|
1155
|
+
? ((this.options.context as any) ?? undefined)
|
|
1156
|
+
: (parentMatch.context ?? this.options.context ?? undefined)
|
|
1144
1157
|
|
|
1145
1158
|
return parentContext
|
|
1146
1159
|
}
|
|
@@ -1162,12 +1175,12 @@ export class RouterCore<
|
|
|
1162
1175
|
] = (() => {
|
|
1163
1176
|
// Validate the search params and stabilize them
|
|
1164
1177
|
const parentSearch = parentMatch?.search ?? next.search
|
|
1165
|
-
const parentStrictSearch = parentMatch?._strictSearch ??
|
|
1178
|
+
const parentStrictSearch = parentMatch?._strictSearch ?? undefined
|
|
1166
1179
|
|
|
1167
1180
|
try {
|
|
1168
1181
|
const strictSearch =
|
|
1169
1182
|
validateSearch(route.options.validateSearch, { ...parentSearch }) ??
|
|
1170
|
-
|
|
1183
|
+
undefined
|
|
1171
1184
|
|
|
1172
1185
|
return [
|
|
1173
1186
|
{
|
|
@@ -1277,7 +1290,10 @@ export class RouterCore<
|
|
|
1277
1290
|
isFetching: false,
|
|
1278
1291
|
error: undefined,
|
|
1279
1292
|
paramsError: parseErrors[index],
|
|
1280
|
-
__routeContext:
|
|
1293
|
+
__routeContext: undefined,
|
|
1294
|
+
_nonReactive: {
|
|
1295
|
+
loadPromise: createControlledPromise(),
|
|
1296
|
+
},
|
|
1281
1297
|
__beforeLoadContext: undefined,
|
|
1282
1298
|
context: {},
|
|
1283
1299
|
abortController: new AbortController(),
|
|
@@ -1293,7 +1309,6 @@ export class RouterCore<
|
|
|
1293
1309
|
headScripts: undefined,
|
|
1294
1310
|
meta: undefined,
|
|
1295
1311
|
staticData: route.options.staticData || {},
|
|
1296
|
-
loadPromise: createControlledPromise(),
|
|
1297
1312
|
fullPath: route.fullPath,
|
|
1298
1313
|
}
|
|
1299
1314
|
}
|
|
@@ -1328,22 +1343,25 @@ export class RouterCore<
|
|
|
1328
1343
|
const parentContext = getParentContext(parentMatch)
|
|
1329
1344
|
|
|
1330
1345
|
// Update the match's context
|
|
1331
|
-
const contextFnContext: RouteContextOptions<any, any, any, any> = {
|
|
1332
|
-
deps: match.loaderDeps,
|
|
1333
|
-
params: match.params,
|
|
1334
|
-
context: parentContext,
|
|
1335
|
-
location: next,
|
|
1336
|
-
navigate: (opts: any) =>
|
|
1337
|
-
this.navigate({ ...opts, _fromLocation: next }),
|
|
1338
|
-
buildLocation: this.buildLocation,
|
|
1339
|
-
cause: match.cause,
|
|
1340
|
-
abortController: match.abortController,
|
|
1341
|
-
preload: !!match.preload,
|
|
1342
|
-
matches,
|
|
1343
|
-
}
|
|
1344
1346
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
+
if (route.options.context) {
|
|
1348
|
+
const contextFnContext: RouteContextOptions<any, any, any, any> = {
|
|
1349
|
+
deps: match.loaderDeps,
|
|
1350
|
+
params: match.params,
|
|
1351
|
+
context: parentContext ?? {},
|
|
1352
|
+
location: next,
|
|
1353
|
+
navigate: (opts: any) =>
|
|
1354
|
+
this.navigate({ ...opts, _fromLocation: next }),
|
|
1355
|
+
buildLocation: this.buildLocation,
|
|
1356
|
+
cause: match.cause,
|
|
1357
|
+
abortController: match.abortController,
|
|
1358
|
+
preload: !!match.preload,
|
|
1359
|
+
matches,
|
|
1360
|
+
}
|
|
1361
|
+
// Get the route context
|
|
1362
|
+
match.__routeContext =
|
|
1363
|
+
route.options.context(contextFnContext) ?? undefined
|
|
1364
|
+
}
|
|
1347
1365
|
|
|
1348
1366
|
match.context = {
|
|
1349
1367
|
...parentContext,
|
|
@@ -1381,13 +1399,8 @@ export class RouterCore<
|
|
|
1381
1399
|
if (!match) return
|
|
1382
1400
|
|
|
1383
1401
|
match.abortController.abort()
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
return {
|
|
1387
|
-
...prev,
|
|
1388
|
-
pendingTimeout: undefined,
|
|
1389
|
-
}
|
|
1390
|
-
})
|
|
1402
|
+
clearTimeout(match._nonReactive.pendingTimeout)
|
|
1403
|
+
match._nonReactive.pendingTimeout = undefined
|
|
1391
1404
|
}
|
|
1392
1405
|
|
|
1393
1406
|
cancelMatches = () => {
|
|
@@ -1413,7 +1426,7 @@ export class RouterCore<
|
|
|
1413
1426
|
|
|
1414
1427
|
// First let's find the starting pathname
|
|
1415
1428
|
// By default, start with the current location
|
|
1416
|
-
let fromPath = lastMatch.fullPath
|
|
1429
|
+
let fromPath = this.resolvePathWithBase(lastMatch.fullPath, '.')
|
|
1417
1430
|
const toPath = dest.to
|
|
1418
1431
|
? this.resolvePathWithBase(fromPath, `${dest.to}`)
|
|
1419
1432
|
: this.resolvePathWithBase(fromPath, '.')
|
|
@@ -1454,6 +1467,8 @@ export class RouterCore<
|
|
|
1454
1467
|
}
|
|
1455
1468
|
}
|
|
1456
1469
|
|
|
1470
|
+
fromPath = this.resolvePathWithBase(fromPath, '.')
|
|
1471
|
+
|
|
1457
1472
|
// From search should always use the current location
|
|
1458
1473
|
const fromSearch = lastMatch.search
|
|
1459
1474
|
// Same with params. It can't hurt to provide as many as possible
|
|
@@ -1482,13 +1497,9 @@ export class RouterCore<
|
|
|
1482
1497
|
parseCache: this.parsePathnameCache,
|
|
1483
1498
|
}).interpolatedPath
|
|
1484
1499
|
|
|
1485
|
-
const destRoutes = this.matchRoutes(
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
{
|
|
1489
|
-
_buildLocation: true,
|
|
1490
|
-
},
|
|
1491
|
-
).map((d) => this.looseRoutesById[d.routeId]!)
|
|
1500
|
+
const destRoutes = this.matchRoutes(interpolatedNextTo, undefined, {
|
|
1501
|
+
_buildLocation: true,
|
|
1502
|
+
}).map((d) => this.looseRoutesById[d.routeId]!)
|
|
1492
1503
|
|
|
1493
1504
|
// If there are any params, we need to stringify them
|
|
1494
1505
|
if (Object.keys(nextParams).length > 0) {
|
|
@@ -2077,702 +2088,871 @@ export class RouterCore<
|
|
|
2077
2088
|
)
|
|
2078
2089
|
}
|
|
2079
2090
|
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2091
|
+
private triggerOnReady = (
|
|
2092
|
+
innerLoadContext: InnerLoadContext,
|
|
2093
|
+
): void | Promise<void> => {
|
|
2094
|
+
if (!innerLoadContext.rendered) {
|
|
2095
|
+
innerLoadContext.rendered = true
|
|
2096
|
+
return innerLoadContext.onReady?.()
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
private resolvePreload = (
|
|
2101
|
+
innerLoadContext: InnerLoadContext,
|
|
2102
|
+
matchId: string,
|
|
2103
|
+
): boolean => {
|
|
2104
|
+
return !!(
|
|
2105
|
+
innerLoadContext.preload &&
|
|
2106
|
+
!this.state.matches.some((d) => d.id === matchId)
|
|
2107
|
+
)
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
private handleRedirectAndNotFound = (
|
|
2111
|
+
innerLoadContext: InnerLoadContext,
|
|
2112
|
+
match: AnyRouteMatch | undefined,
|
|
2113
|
+
err: unknown,
|
|
2114
|
+
): void => {
|
|
2115
|
+
if (!isRedirect(err) && !isNotFound(err)) return
|
|
2101
2116
|
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2117
|
+
if (isRedirect(err) && err.redirectHandled && !err.options.reloadDocument) {
|
|
2118
|
+
throw err
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// in case of a redirecting match during preload, the match does not exist
|
|
2122
|
+
if (match) {
|
|
2123
|
+
match._nonReactive.beforeLoadPromise?.resolve()
|
|
2124
|
+
match._nonReactive.loaderPromise?.resolve()
|
|
2125
|
+
match._nonReactive.beforeLoadPromise = undefined
|
|
2126
|
+
match._nonReactive.loaderPromise = undefined
|
|
2127
|
+
|
|
2128
|
+
const status = isRedirect(err) ? 'redirected' : 'notFound'
|
|
2129
|
+
|
|
2130
|
+
innerLoadContext.updateMatch(match.id, (prev) => ({
|
|
2131
|
+
...prev,
|
|
2132
|
+
status,
|
|
2133
|
+
isFetching: false,
|
|
2134
|
+
error: err,
|
|
2135
|
+
}))
|
|
2136
|
+
|
|
2137
|
+
if (isNotFound(err) && !err.routeId) {
|
|
2138
|
+
err.routeId = match.routeId
|
|
2106
2139
|
}
|
|
2140
|
+
|
|
2141
|
+
match._nonReactive.loadPromise?.resolve()
|
|
2107
2142
|
}
|
|
2108
2143
|
|
|
2109
|
-
|
|
2110
|
-
|
|
2144
|
+
if (isRedirect(err)) {
|
|
2145
|
+
innerLoadContext.rendered = true
|
|
2146
|
+
err.options._fromLocation = innerLoadContext.location
|
|
2147
|
+
err.redirectHandled = true
|
|
2148
|
+
err = this.resolveRedirect(err)
|
|
2149
|
+
throw err
|
|
2150
|
+
} else {
|
|
2151
|
+
this._handleNotFound(innerLoadContext, err)
|
|
2152
|
+
throw err
|
|
2111
2153
|
}
|
|
2154
|
+
}
|
|
2112
2155
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
if
|
|
2116
|
-
|
|
2156
|
+
private shouldSkipLoader = (matchId: string): boolean => {
|
|
2157
|
+
const match = this.getMatch(matchId)!
|
|
2158
|
+
// upon hydration, we skip the loader if the match has been dehydrated on the server
|
|
2159
|
+
if (!this.isServer && match._nonReactive.dehydrated) {
|
|
2160
|
+
return true
|
|
2117
2161
|
}
|
|
2118
2162
|
|
|
2119
|
-
|
|
2120
|
-
if (
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2163
|
+
if (this.isServer) {
|
|
2164
|
+
if (match.ssr === false) {
|
|
2165
|
+
return true
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
return false
|
|
2169
|
+
}
|
|
2128
2170
|
|
|
2129
|
-
|
|
2130
|
-
|
|
2171
|
+
private handleSerialError = (
|
|
2172
|
+
innerLoadContext: InnerLoadContext,
|
|
2173
|
+
index: number,
|
|
2174
|
+
err: any,
|
|
2175
|
+
routerCode: string,
|
|
2176
|
+
): void => {
|
|
2177
|
+
const { id: matchId, routeId } = innerLoadContext.matches[index]!
|
|
2178
|
+
const route = this.looseRoutesById[routeId]!
|
|
2179
|
+
|
|
2180
|
+
// Much like suspense, we use a promise here to know if
|
|
2181
|
+
// we've been outdated by a new loadMatches call and
|
|
2182
|
+
// should abort the current async operation
|
|
2183
|
+
if (err instanceof Promise) {
|
|
2184
|
+
throw err
|
|
2185
|
+
}
|
|
2131
2186
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
isFetching: false,
|
|
2140
|
-
error: err,
|
|
2141
|
-
beforeLoadPromise: undefined,
|
|
2142
|
-
loaderPromise: undefined,
|
|
2143
|
-
}))
|
|
2187
|
+
err.routerCode = routerCode
|
|
2188
|
+
innerLoadContext.firstBadMatchIndex ??= index
|
|
2189
|
+
this.handleRedirectAndNotFound(
|
|
2190
|
+
innerLoadContext,
|
|
2191
|
+
this.getMatch(matchId),
|
|
2192
|
+
err,
|
|
2193
|
+
)
|
|
2144
2194
|
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2195
|
+
try {
|
|
2196
|
+
route.options.onError?.(err)
|
|
2197
|
+
} catch (errorHandlerErr) {
|
|
2198
|
+
err = errorHandlerErr
|
|
2199
|
+
this.handleRedirectAndNotFound(
|
|
2200
|
+
innerLoadContext,
|
|
2201
|
+
this.getMatch(matchId),
|
|
2202
|
+
err,
|
|
2203
|
+
)
|
|
2204
|
+
}
|
|
2148
2205
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2206
|
+
innerLoadContext.updateMatch(matchId, (prev) => {
|
|
2207
|
+
prev._nonReactive.beforeLoadPromise?.resolve()
|
|
2208
|
+
prev._nonReactive.beforeLoadPromise = undefined
|
|
2209
|
+
prev._nonReactive.loadPromise?.resolve()
|
|
2210
|
+
|
|
2211
|
+
return {
|
|
2212
|
+
...prev,
|
|
2213
|
+
error: err,
|
|
2214
|
+
status: 'error',
|
|
2215
|
+
isFetching: false,
|
|
2216
|
+
updatedAt: Date.now(),
|
|
2217
|
+
abortController: new AbortController(),
|
|
2218
|
+
}
|
|
2219
|
+
})
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
private isBeforeLoadSsr = (
|
|
2223
|
+
innerLoadContext: InnerLoadContext,
|
|
2224
|
+
matchId: string,
|
|
2225
|
+
index: number,
|
|
2226
|
+
route: AnyRoute,
|
|
2227
|
+
): void | Promise<void> => {
|
|
2228
|
+
const existingMatch = this.getMatch(matchId)!
|
|
2229
|
+
const parentMatchId = innerLoadContext.matches[index - 1]?.id
|
|
2230
|
+
const parentMatch = parentMatchId
|
|
2231
|
+
? this.getMatch(parentMatchId)!
|
|
2232
|
+
: undefined
|
|
2233
|
+
|
|
2234
|
+
// in SPA mode, only SSR the root route
|
|
2235
|
+
if (this.isShell()) {
|
|
2236
|
+
existingMatch.ssr = matchId === rootRouteId
|
|
2237
|
+
return
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
if (parentMatch?.ssr === false) {
|
|
2241
|
+
existingMatch.ssr = false
|
|
2242
|
+
return
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
const parentOverride = (tempSsr: boolean | 'data-only') => {
|
|
2246
|
+
if (tempSsr === true && parentMatch?.ssr === 'data-only') {
|
|
2247
|
+
return 'data-only'
|
|
2163
2248
|
}
|
|
2249
|
+
return tempSsr
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
const defaultSsr = this.options.defaultSsr ?? true
|
|
2253
|
+
|
|
2254
|
+
if (route.options.ssr === undefined) {
|
|
2255
|
+
existingMatch.ssr = parentOverride(defaultSsr)
|
|
2256
|
+
return
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
if (typeof route.options.ssr !== 'function') {
|
|
2260
|
+
existingMatch.ssr = parentOverride(route.options.ssr)
|
|
2261
|
+
return
|
|
2262
|
+
}
|
|
2263
|
+
const { search, params } = this.getMatch(matchId)!
|
|
2264
|
+
|
|
2265
|
+
const ssrFnContext: SsrContextOptions<any, any, any> = {
|
|
2266
|
+
search: makeMaybe(search, existingMatch.searchError),
|
|
2267
|
+
params: makeMaybe(params, existingMatch.paramsError),
|
|
2268
|
+
location: innerLoadContext.location,
|
|
2269
|
+
matches: innerLoadContext.matches.map((match) => ({
|
|
2270
|
+
index: match.index,
|
|
2271
|
+
pathname: match.pathname,
|
|
2272
|
+
fullPath: match.fullPath,
|
|
2273
|
+
staticData: match.staticData,
|
|
2274
|
+
id: match.id,
|
|
2275
|
+
routeId: match.routeId,
|
|
2276
|
+
search: makeMaybe(match.search, match.searchError),
|
|
2277
|
+
params: makeMaybe(match.params, match.paramsError),
|
|
2278
|
+
ssr: match.ssr,
|
|
2279
|
+
})),
|
|
2164
2280
|
}
|
|
2165
2281
|
|
|
2166
|
-
const
|
|
2282
|
+
const tempSsr = route.options.ssr(ssrFnContext)
|
|
2283
|
+
if (isPromise(tempSsr)) {
|
|
2284
|
+
return tempSsr.then((ssr) => {
|
|
2285
|
+
existingMatch.ssr = parentOverride(ssr ?? defaultSsr)
|
|
2286
|
+
})
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
existingMatch.ssr = parentOverride(tempSsr ?? defaultSsr)
|
|
2290
|
+
return
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
private setupPendingTimeout = (
|
|
2294
|
+
innerLoadContext: InnerLoadContext,
|
|
2295
|
+
matchId: string,
|
|
2296
|
+
route: AnyRoute,
|
|
2297
|
+
): void => {
|
|
2298
|
+
const pendingMs = route.options.pendingMs ?? this.options.defaultPendingMs
|
|
2299
|
+
const shouldPending = !!(
|
|
2300
|
+
innerLoadContext.onReady &&
|
|
2301
|
+
!this.isServer &&
|
|
2302
|
+
!this.resolvePreload(innerLoadContext, matchId) &&
|
|
2303
|
+
(route.options.loader ||
|
|
2304
|
+
route.options.beforeLoad ||
|
|
2305
|
+
routeNeedsPreload(route)) &&
|
|
2306
|
+
typeof pendingMs === 'number' &&
|
|
2307
|
+
pendingMs !== Infinity &&
|
|
2308
|
+
(route.options.pendingComponent ??
|
|
2309
|
+
(this.options as any)?.defaultPendingComponent)
|
|
2310
|
+
)
|
|
2311
|
+
const match = this.getMatch(matchId)!
|
|
2312
|
+
if (shouldPending && match._nonReactive.pendingTimeout === undefined) {
|
|
2313
|
+
const pendingTimeout = setTimeout(() => {
|
|
2314
|
+
// Update the match and prematurely resolve the loadMatches promise so that
|
|
2315
|
+
// the pending component can start rendering
|
|
2316
|
+
this.triggerOnReady(innerLoadContext)
|
|
2317
|
+
}, pendingMs)
|
|
2318
|
+
match._nonReactive.pendingTimeout = pendingTimeout
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
private shouldExecuteBeforeLoad = (
|
|
2323
|
+
innerLoadContext: InnerLoadContext,
|
|
2324
|
+
matchId: string,
|
|
2325
|
+
route: AnyRoute,
|
|
2326
|
+
): boolean | Promise<boolean> => {
|
|
2327
|
+
const existingMatch = this.getMatch(matchId)!
|
|
2328
|
+
|
|
2329
|
+
// If we are in the middle of a load, either of these will be present
|
|
2330
|
+
// (not to be confused with `loadPromise`, which is always defined)
|
|
2331
|
+
if (
|
|
2332
|
+
!existingMatch._nonReactive.beforeLoadPromise &&
|
|
2333
|
+
!existingMatch._nonReactive.loaderPromise
|
|
2334
|
+
)
|
|
2335
|
+
return true
|
|
2336
|
+
|
|
2337
|
+
this.setupPendingTimeout(innerLoadContext, matchId, route)
|
|
2338
|
+
|
|
2339
|
+
const then = () => {
|
|
2340
|
+
let shouldExecuteBeforeLoad = true
|
|
2167
2341
|
const match = this.getMatch(matchId)!
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2342
|
+
if (match.status === 'error') {
|
|
2343
|
+
shouldExecuteBeforeLoad = true
|
|
2344
|
+
} else if (
|
|
2345
|
+
match.preload &&
|
|
2346
|
+
(match.status === 'redirected' || match.status === 'notFound')
|
|
2347
|
+
) {
|
|
2348
|
+
this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
|
|
2171
2349
|
}
|
|
2350
|
+
return shouldExecuteBeforeLoad
|
|
2351
|
+
}
|
|
2172
2352
|
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2353
|
+
// Wait for the beforeLoad to resolve before we continue
|
|
2354
|
+
return existingMatch._nonReactive.beforeLoadPromise
|
|
2355
|
+
? existingMatch._nonReactive.beforeLoadPromise.then(then)
|
|
2356
|
+
: then()
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
private executeBeforeLoad = (
|
|
2360
|
+
innerLoadContext: InnerLoadContext,
|
|
2361
|
+
matchId: string,
|
|
2362
|
+
index: number,
|
|
2363
|
+
route: AnyRoute,
|
|
2364
|
+
): void | Promise<void> => {
|
|
2365
|
+
const match = this.getMatch(matchId)!
|
|
2366
|
+
|
|
2367
|
+
match._nonReactive.beforeLoadPromise = createControlledPromise<void>()
|
|
2368
|
+
// explicitly capture the previous loadPromise
|
|
2369
|
+
const prevLoadPromise = match._nonReactive.loadPromise
|
|
2370
|
+
match._nonReactive.loadPromise = createControlledPromise<void>(() => {
|
|
2371
|
+
prevLoadPromise?.resolve()
|
|
2372
|
+
})
|
|
2373
|
+
|
|
2374
|
+
const { paramsError, searchError } = match
|
|
2375
|
+
|
|
2376
|
+
if (paramsError) {
|
|
2377
|
+
this.handleSerialError(
|
|
2378
|
+
innerLoadContext,
|
|
2379
|
+
index,
|
|
2380
|
+
paramsError,
|
|
2381
|
+
'PARSE_PARAMS',
|
|
2382
|
+
)
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
if (searchError) {
|
|
2386
|
+
this.handleSerialError(
|
|
2387
|
+
innerLoadContext,
|
|
2388
|
+
index,
|
|
2389
|
+
searchError,
|
|
2390
|
+
'VALIDATE_SEARCH',
|
|
2391
|
+
)
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
this.setupPendingTimeout(innerLoadContext, matchId, route)
|
|
2395
|
+
|
|
2396
|
+
const abortController = new AbortController()
|
|
2397
|
+
|
|
2398
|
+
const parentMatchId = innerLoadContext.matches[index - 1]?.id
|
|
2399
|
+
const parentMatch = parentMatchId
|
|
2400
|
+
? this.getMatch(parentMatchId)!
|
|
2401
|
+
: undefined
|
|
2402
|
+
const parentMatchContext =
|
|
2403
|
+
parentMatch?.context ?? this.options.context ?? undefined
|
|
2404
|
+
|
|
2405
|
+
const context = { ...parentMatchContext, ...match.__routeContext }
|
|
2406
|
+
|
|
2407
|
+
let isPending = false
|
|
2408
|
+
const pending = () => {
|
|
2409
|
+
if (isPending) return
|
|
2410
|
+
isPending = true
|
|
2411
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2412
|
+
...prev,
|
|
2413
|
+
isFetching: 'beforeLoad',
|
|
2414
|
+
fetchCount: prev.fetchCount + 1,
|
|
2415
|
+
abortController,
|
|
2416
|
+
context,
|
|
2417
|
+
}))
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
const resolve = () => {
|
|
2421
|
+
match._nonReactive.beforeLoadPromise?.resolve()
|
|
2422
|
+
match._nonReactive.beforeLoadPromise = undefined
|
|
2423
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2424
|
+
...prev,
|
|
2425
|
+
isFetching: false,
|
|
2426
|
+
}))
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// if there is no `beforeLoad` option, skip everything, batch update the store, return early
|
|
2430
|
+
if (!route.options.beforeLoad) {
|
|
2431
|
+
batch(() => {
|
|
2432
|
+
pending()
|
|
2433
|
+
resolve()
|
|
2434
|
+
})
|
|
2435
|
+
return
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
const { search, params, cause } = match
|
|
2439
|
+
const preload = this.resolvePreload(innerLoadContext, matchId)
|
|
2440
|
+
const beforeLoadFnContext: BeforeLoadContextOptions<
|
|
2441
|
+
any,
|
|
2442
|
+
any,
|
|
2443
|
+
any,
|
|
2444
|
+
any,
|
|
2445
|
+
any
|
|
2446
|
+
> = {
|
|
2447
|
+
search,
|
|
2448
|
+
abortController,
|
|
2449
|
+
params,
|
|
2450
|
+
preload,
|
|
2451
|
+
context,
|
|
2452
|
+
location: innerLoadContext.location,
|
|
2453
|
+
navigate: (opts: any) =>
|
|
2454
|
+
this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
|
|
2455
|
+
buildLocation: this.buildLocation,
|
|
2456
|
+
cause: preload ? 'preload' : cause,
|
|
2457
|
+
matches: innerLoadContext.matches,
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
const updateContext = (beforeLoadContext: any) => {
|
|
2461
|
+
if (beforeLoadContext === undefined) {
|
|
2462
|
+
batch(() => {
|
|
2463
|
+
pending()
|
|
2464
|
+
resolve()
|
|
2465
|
+
})
|
|
2466
|
+
return
|
|
2177
2467
|
}
|
|
2178
|
-
|
|
2468
|
+
if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
|
|
2469
|
+
pending()
|
|
2470
|
+
this.handleSerialError(
|
|
2471
|
+
innerLoadContext,
|
|
2472
|
+
index,
|
|
2473
|
+
beforeLoadContext,
|
|
2474
|
+
'BEFORE_LOAD',
|
|
2475
|
+
)
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
batch(() => {
|
|
2479
|
+
pending()
|
|
2480
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2481
|
+
...prev,
|
|
2482
|
+
__beforeLoadContext: beforeLoadContext,
|
|
2483
|
+
context: {
|
|
2484
|
+
...prev.context,
|
|
2485
|
+
...beforeLoadContext,
|
|
2486
|
+
},
|
|
2487
|
+
}))
|
|
2488
|
+
resolve()
|
|
2489
|
+
})
|
|
2179
2490
|
}
|
|
2180
2491
|
|
|
2492
|
+
let beforeLoadContext
|
|
2181
2493
|
try {
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
// should abort the current async operation
|
|
2196
|
-
if (err instanceof Promise) {
|
|
2197
|
-
throw err
|
|
2198
|
-
}
|
|
2494
|
+
beforeLoadContext = route.options.beforeLoad(beforeLoadFnContext)
|
|
2495
|
+
if (isPromise(beforeLoadContext)) {
|
|
2496
|
+
pending()
|
|
2497
|
+
return beforeLoadContext
|
|
2498
|
+
.catch((err) => {
|
|
2499
|
+
this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
|
|
2500
|
+
})
|
|
2501
|
+
.then(updateContext)
|
|
2502
|
+
}
|
|
2503
|
+
} catch (err) {
|
|
2504
|
+
pending()
|
|
2505
|
+
this.handleSerialError(innerLoadContext, index, err, 'BEFORE_LOAD')
|
|
2506
|
+
}
|
|
2199
2507
|
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2508
|
+
updateContext(beforeLoadContext)
|
|
2509
|
+
return
|
|
2510
|
+
}
|
|
2203
2511
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2512
|
+
private handleBeforeLoad = (
|
|
2513
|
+
innerLoadContext: InnerLoadContext,
|
|
2514
|
+
index: number,
|
|
2515
|
+
): void | Promise<void> => {
|
|
2516
|
+
const { id: matchId, routeId } = innerLoadContext.matches[index]!
|
|
2517
|
+
const route = this.looseRoutesById[routeId]!
|
|
2210
2518
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
})
|
|
2225
|
-
}
|
|
2519
|
+
const serverSsr = () => {
|
|
2520
|
+
// on the server, determine whether SSR the current match or not
|
|
2521
|
+
if (this.isServer) {
|
|
2522
|
+
const maybePromise = this.isBeforeLoadSsr(
|
|
2523
|
+
innerLoadContext,
|
|
2524
|
+
matchId,
|
|
2525
|
+
index,
|
|
2526
|
+
route,
|
|
2527
|
+
)
|
|
2528
|
+
if (isPromise(maybePromise)) return maybePromise.then(queueExecution)
|
|
2529
|
+
}
|
|
2530
|
+
return queueExecution()
|
|
2531
|
+
}
|
|
2226
2532
|
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
// on the server, determine whether SSR the current match or not
|
|
2240
|
-
if (this.isServer) {
|
|
2241
|
-
let ssr: boolean | 'data-only'
|
|
2242
|
-
// in SPA mode, only SSR the root route
|
|
2243
|
-
if (this.isShell()) {
|
|
2244
|
-
ssr = matchId === rootRouteId
|
|
2245
|
-
} else {
|
|
2246
|
-
const defaultSsr = this.options.defaultSsr ?? true
|
|
2247
|
-
if (parentMatch?.ssr === false) {
|
|
2248
|
-
ssr = false
|
|
2249
|
-
} else {
|
|
2250
|
-
let tempSsr: boolean | 'data-only'
|
|
2251
|
-
if (route.options.ssr === undefined) {
|
|
2252
|
-
tempSsr = defaultSsr
|
|
2253
|
-
} else if (typeof route.options.ssr === 'function') {
|
|
2254
|
-
const { search, params } = this.getMatch(matchId)!
|
|
2255
|
-
|
|
2256
|
-
function makeMaybe(value: any, error: any) {
|
|
2257
|
-
if (error) {
|
|
2258
|
-
return { status: 'error' as const, error }
|
|
2259
|
-
}
|
|
2260
|
-
return { status: 'success' as const, value }
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
const ssrFnContext: SsrContextOptions<any, any, any> = {
|
|
2264
|
-
search: makeMaybe(search, existingMatch.searchError),
|
|
2265
|
-
params: makeMaybe(params, existingMatch.paramsError),
|
|
2266
|
-
location,
|
|
2267
|
-
matches: matches.map((match) => ({
|
|
2268
|
-
index: match.index,
|
|
2269
|
-
pathname: match.pathname,
|
|
2270
|
-
fullPath: match.fullPath,
|
|
2271
|
-
staticData: match.staticData,
|
|
2272
|
-
id: match.id,
|
|
2273
|
-
routeId: match.routeId,
|
|
2274
|
-
search: makeMaybe(match.search, match.searchError),
|
|
2275
|
-
params: makeMaybe(match.params, match.paramsError),
|
|
2276
|
-
ssr: match.ssr,
|
|
2277
|
-
})),
|
|
2278
|
-
}
|
|
2279
|
-
tempSsr =
|
|
2280
|
-
(await route.options.ssr(ssrFnContext)) ?? defaultSsr
|
|
2281
|
-
} else {
|
|
2282
|
-
tempSsr = route.options.ssr
|
|
2283
|
-
}
|
|
2533
|
+
const queueExecution = () => {
|
|
2534
|
+
if (this.shouldSkipLoader(matchId)) return
|
|
2535
|
+
const shouldExecuteBeforeLoadResult = this.shouldExecuteBeforeLoad(
|
|
2536
|
+
innerLoadContext,
|
|
2537
|
+
matchId,
|
|
2538
|
+
route,
|
|
2539
|
+
)
|
|
2540
|
+
return isPromise(shouldExecuteBeforeLoadResult)
|
|
2541
|
+
? shouldExecuteBeforeLoadResult.then(execute)
|
|
2542
|
+
: execute(shouldExecuteBeforeLoadResult)
|
|
2543
|
+
}
|
|
2284
2544
|
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
updateMatch(matchId, (prev) => ({
|
|
2293
|
-
...prev,
|
|
2294
|
-
ssr,
|
|
2295
|
-
}))
|
|
2296
|
-
}
|
|
2545
|
+
const execute = (shouldExecuteBeforeLoad: boolean) => {
|
|
2546
|
+
if (shouldExecuteBeforeLoad) {
|
|
2547
|
+
// If we are not in the middle of a load OR the previous load failed, start it
|
|
2548
|
+
return this.executeBeforeLoad(innerLoadContext, matchId, index, route)
|
|
2549
|
+
}
|
|
2550
|
+
return
|
|
2551
|
+
}
|
|
2297
2552
|
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
}
|
|
2553
|
+
return serverSsr()
|
|
2554
|
+
}
|
|
2301
2555
|
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2556
|
+
private executeHead = (
|
|
2557
|
+
innerLoadContext: InnerLoadContext,
|
|
2558
|
+
matchId: string,
|
|
2559
|
+
route: AnyRoute,
|
|
2560
|
+
): void | Promise<
|
|
2561
|
+
Pick<
|
|
2562
|
+
AnyRouteMatch,
|
|
2563
|
+
'meta' | 'links' | 'headScripts' | 'headers' | 'scripts' | 'styles'
|
|
2564
|
+
>
|
|
2565
|
+
> => {
|
|
2566
|
+
const match = this.getMatch(matchId)
|
|
2567
|
+
// in case of a redirecting match during preload, the match does not exist
|
|
2568
|
+
if (!match) {
|
|
2569
|
+
return
|
|
2570
|
+
}
|
|
2571
|
+
if (
|
|
2572
|
+
!route.options.head &&
|
|
2573
|
+
!route.options.scripts &&
|
|
2574
|
+
!route.options.headers
|
|
2575
|
+
) {
|
|
2576
|
+
return
|
|
2577
|
+
}
|
|
2578
|
+
const assetContext = {
|
|
2579
|
+
matches: innerLoadContext.matches,
|
|
2580
|
+
match,
|
|
2581
|
+
params: match.params,
|
|
2582
|
+
loaderData: match.loaderData,
|
|
2583
|
+
}
|
|
2314
2584
|
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
// the pending component can start rendering
|
|
2325
|
-
triggerOnReady()
|
|
2326
|
-
} catch {}
|
|
2327
|
-
}, pendingMs)
|
|
2328
|
-
updateMatch(matchId, (prev) => ({
|
|
2329
|
-
...prev,
|
|
2330
|
-
pendingTimeout,
|
|
2331
|
-
}))
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
if (
|
|
2335
|
-
// If we are in the middle of a load, either of these will be present
|
|
2336
|
-
// (not to be confused with `loadPromise`, which is always defined)
|
|
2337
|
-
existingMatch.beforeLoadPromise ||
|
|
2338
|
-
existingMatch.loaderPromise
|
|
2339
|
-
) {
|
|
2340
|
-
setupPendingTimeout()
|
|
2341
|
-
|
|
2342
|
-
// Wait for the beforeLoad to resolve before we continue
|
|
2343
|
-
await existingMatch.beforeLoadPromise
|
|
2344
|
-
const match = this.getMatch(matchId)!
|
|
2345
|
-
if (match.status === 'error') {
|
|
2346
|
-
executeBeforeLoad = true
|
|
2347
|
-
} else if (
|
|
2348
|
-
match.preload &&
|
|
2349
|
-
(match.status === 'redirected' || match.status === 'notFound')
|
|
2350
|
-
) {
|
|
2351
|
-
handleRedirectAndNotFound(match, match.error)
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
if (executeBeforeLoad) {
|
|
2355
|
-
// If we are not in the middle of a load OR the previous load failed, start it
|
|
2356
|
-
try {
|
|
2357
|
-
updateMatch(matchId, (prev) => {
|
|
2358
|
-
// explicitly capture the previous loadPromise
|
|
2359
|
-
const prevLoadPromise = prev.loadPromise
|
|
2360
|
-
return {
|
|
2361
|
-
...prev,
|
|
2362
|
-
loadPromise: createControlledPromise<void>(() => {
|
|
2363
|
-
prevLoadPromise?.resolve()
|
|
2364
|
-
}),
|
|
2365
|
-
beforeLoadPromise: createControlledPromise<void>(),
|
|
2366
|
-
}
|
|
2367
|
-
})
|
|
2585
|
+
return Promise.all([
|
|
2586
|
+
route.options.head?.(assetContext),
|
|
2587
|
+
route.options.scripts?.(assetContext),
|
|
2588
|
+
route.options.headers?.(assetContext),
|
|
2589
|
+
]).then(([headFnContent, scripts, headers]) => {
|
|
2590
|
+
const meta = headFnContent?.meta
|
|
2591
|
+
const links = headFnContent?.links
|
|
2592
|
+
const headScripts = headFnContent?.scripts
|
|
2593
|
+
const styles = headFnContent?.styles
|
|
2368
2594
|
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
setupPendingTimeout()
|
|
2380
|
-
|
|
2381
|
-
const abortController = new AbortController()
|
|
2382
|
-
|
|
2383
|
-
const parentMatchContext =
|
|
2384
|
-
parentMatch?.context ?? this.options.context ?? {}
|
|
2385
|
-
|
|
2386
|
-
updateMatch(matchId, (prev) => ({
|
|
2387
|
-
...prev,
|
|
2388
|
-
isFetching: 'beforeLoad',
|
|
2389
|
-
fetchCount: prev.fetchCount + 1,
|
|
2390
|
-
abortController,
|
|
2391
|
-
context: {
|
|
2392
|
-
...parentMatchContext,
|
|
2393
|
-
...prev.__routeContext,
|
|
2394
|
-
},
|
|
2395
|
-
}))
|
|
2396
|
-
|
|
2397
|
-
const { search, params, context, cause } =
|
|
2398
|
-
this.getMatch(matchId)!
|
|
2399
|
-
|
|
2400
|
-
const preload = resolvePreload(matchId)
|
|
2401
|
-
|
|
2402
|
-
const beforeLoadFnContext: BeforeLoadContextOptions<
|
|
2403
|
-
any,
|
|
2404
|
-
any,
|
|
2405
|
-
any,
|
|
2406
|
-
any,
|
|
2407
|
-
any
|
|
2408
|
-
> = {
|
|
2409
|
-
search,
|
|
2410
|
-
abortController,
|
|
2411
|
-
params,
|
|
2412
|
-
preload,
|
|
2413
|
-
context,
|
|
2414
|
-
location,
|
|
2415
|
-
navigate: (opts: any) =>
|
|
2416
|
-
this.navigate({ ...opts, _fromLocation: location }),
|
|
2417
|
-
buildLocation: this.buildLocation,
|
|
2418
|
-
cause: preload ? 'preload' : cause,
|
|
2419
|
-
matches,
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
const beforeLoadContext =
|
|
2423
|
-
await route.options.beforeLoad?.(beforeLoadFnContext)
|
|
2424
|
-
|
|
2425
|
-
if (
|
|
2426
|
-
isRedirect(beforeLoadContext) ||
|
|
2427
|
-
isNotFound(beforeLoadContext)
|
|
2428
|
-
) {
|
|
2429
|
-
handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
|
-
updateMatch(matchId, (prev) => {
|
|
2433
|
-
return {
|
|
2434
|
-
...prev,
|
|
2435
|
-
__beforeLoadContext: beforeLoadContext,
|
|
2436
|
-
context: {
|
|
2437
|
-
...parentMatchContext,
|
|
2438
|
-
...prev.__routeContext,
|
|
2439
|
-
...beforeLoadContext,
|
|
2440
|
-
},
|
|
2441
|
-
abortController,
|
|
2442
|
-
}
|
|
2443
|
-
})
|
|
2444
|
-
} catch (err) {
|
|
2445
|
-
handleSerialError(index, err, 'BEFORE_LOAD')
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
updateMatch(matchId, (prev) => {
|
|
2449
|
-
prev.beforeLoadPromise?.resolve()
|
|
2450
|
-
|
|
2451
|
-
return {
|
|
2452
|
-
...prev,
|
|
2453
|
-
beforeLoadPromise: undefined,
|
|
2454
|
-
isFetching: false,
|
|
2455
|
-
}
|
|
2456
|
-
})
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2595
|
+
return {
|
|
2596
|
+
meta,
|
|
2597
|
+
links,
|
|
2598
|
+
headScripts,
|
|
2599
|
+
headers,
|
|
2600
|
+
scripts,
|
|
2601
|
+
styles,
|
|
2602
|
+
}
|
|
2603
|
+
})
|
|
2604
|
+
}
|
|
2459
2605
|
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
let loaderShouldRunAsync = false
|
|
2467
|
-
let loaderIsRunningAsync = false
|
|
2468
|
-
const route = this.looseRoutesById[routeId]!
|
|
2469
|
-
|
|
2470
|
-
const executeHead = async () => {
|
|
2471
|
-
const match = this.getMatch(matchId)
|
|
2472
|
-
// in case of a redirecting match during preload, the match does not exist
|
|
2473
|
-
if (!match) {
|
|
2474
|
-
return
|
|
2475
|
-
}
|
|
2476
|
-
const assetContext = {
|
|
2477
|
-
matches,
|
|
2478
|
-
match,
|
|
2479
|
-
params: match.params,
|
|
2480
|
-
loaderData: match.loaderData,
|
|
2481
|
-
}
|
|
2482
|
-
const headFnContent =
|
|
2483
|
-
await route.options.head?.(assetContext)
|
|
2484
|
-
const meta = headFnContent?.meta
|
|
2485
|
-
const links = headFnContent?.links
|
|
2486
|
-
const headScripts = headFnContent?.scripts
|
|
2487
|
-
const styles = headFnContent?.styles
|
|
2488
|
-
|
|
2489
|
-
const scripts = await route.options.scripts?.(assetContext)
|
|
2490
|
-
const headers = await route.options.headers?.(assetContext)
|
|
2491
|
-
return {
|
|
2492
|
-
meta,
|
|
2493
|
-
links,
|
|
2494
|
-
headScripts,
|
|
2495
|
-
headers,
|
|
2496
|
-
scripts,
|
|
2497
|
-
styles,
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2606
|
+
private potentialPendingMinPromise = (
|
|
2607
|
+
matchId: string,
|
|
2608
|
+
): void | ControlledPromise<void> => {
|
|
2609
|
+
const latestMatch = this.getMatch(matchId)!
|
|
2610
|
+
return latestMatch._nonReactive.minPendingPromise
|
|
2611
|
+
}
|
|
2500
2612
|
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
!prevMatch.preload
|
|
2528
|
-
) {
|
|
2529
|
-
return this.getMatch(matchId)!
|
|
2530
|
-
}
|
|
2531
|
-
await prevMatch.loaderPromise
|
|
2532
|
-
const match = this.getMatch(matchId)!
|
|
2533
|
-
if (match.error) {
|
|
2534
|
-
handleRedirectAndNotFound(match, match.error)
|
|
2535
|
-
}
|
|
2536
|
-
} else {
|
|
2537
|
-
const parentMatchPromise = matchPromises[index - 1] as any
|
|
2538
|
-
|
|
2539
|
-
const getLoaderContext = (): LoaderFnContext => {
|
|
2540
|
-
const {
|
|
2541
|
-
params,
|
|
2542
|
-
loaderDeps,
|
|
2543
|
-
abortController,
|
|
2544
|
-
context,
|
|
2545
|
-
cause,
|
|
2546
|
-
} = this.getMatch(matchId)!
|
|
2547
|
-
|
|
2548
|
-
const preload = resolvePreload(matchId)
|
|
2549
|
-
|
|
2550
|
-
return {
|
|
2551
|
-
params,
|
|
2552
|
-
deps: loaderDeps,
|
|
2553
|
-
preload: !!preload,
|
|
2554
|
-
parentMatchPromise,
|
|
2555
|
-
abortController: abortController,
|
|
2556
|
-
context,
|
|
2557
|
-
location,
|
|
2558
|
-
navigate: (opts) =>
|
|
2559
|
-
this.navigate({ ...opts, _fromLocation: location }),
|
|
2560
|
-
cause: preload ? 'preload' : cause,
|
|
2561
|
-
route,
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2613
|
+
private getLoaderContext = (
|
|
2614
|
+
innerLoadContext: InnerLoadContext,
|
|
2615
|
+
matchId: string,
|
|
2616
|
+
index: number,
|
|
2617
|
+
route: AnyRoute,
|
|
2618
|
+
): LoaderFnContext => {
|
|
2619
|
+
const parentMatchPromise = innerLoadContext.matchPromises[index - 1] as any
|
|
2620
|
+
const { params, loaderDeps, abortController, context, cause } =
|
|
2621
|
+
this.getMatch(matchId)!
|
|
2622
|
+
|
|
2623
|
+
const preload = this.resolvePreload(innerLoadContext, matchId)
|
|
2624
|
+
|
|
2625
|
+
return {
|
|
2626
|
+
params,
|
|
2627
|
+
deps: loaderDeps,
|
|
2628
|
+
preload: !!preload,
|
|
2629
|
+
parentMatchPromise,
|
|
2630
|
+
abortController: abortController,
|
|
2631
|
+
context,
|
|
2632
|
+
location: innerLoadContext.location,
|
|
2633
|
+
navigate: (opts) =>
|
|
2634
|
+
this.navigate({ ...opts, _fromLocation: innerLoadContext.location }),
|
|
2635
|
+
cause: preload ? 'preload' : cause,
|
|
2636
|
+
route,
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2564
2639
|
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
const shouldReloadOption = route.options.shouldReload
|
|
2579
|
-
|
|
2580
|
-
// Default to reloading the route all the time
|
|
2581
|
-
// Allow shouldReload to get the last say,
|
|
2582
|
-
// if provided.
|
|
2583
|
-
const shouldReload =
|
|
2584
|
-
typeof shouldReloadOption === 'function'
|
|
2585
|
-
? shouldReloadOption(getLoaderContext())
|
|
2586
|
-
: shouldReloadOption
|
|
2587
|
-
|
|
2588
|
-
updateMatch(matchId, (prev) => ({
|
|
2589
|
-
...prev,
|
|
2590
|
-
loaderPromise: createControlledPromise<void>(),
|
|
2591
|
-
preload:
|
|
2592
|
-
!!preload &&
|
|
2593
|
-
!this.state.matches.some((d) => d.id === matchId),
|
|
2594
|
-
}))
|
|
2595
|
-
|
|
2596
|
-
const runLoader = async () => {
|
|
2597
|
-
try {
|
|
2598
|
-
// If the Matches component rendered
|
|
2599
|
-
// the pending component and needs to show it for
|
|
2600
|
-
// a minimum duration, we''ll wait for it to resolve
|
|
2601
|
-
// before committing to the match and resolving
|
|
2602
|
-
// the loadPromise
|
|
2603
|
-
|
|
2604
|
-
// Actually run the loader and handle the result
|
|
2605
|
-
try {
|
|
2606
|
-
if (
|
|
2607
|
-
!this.isServer ||
|
|
2608
|
-
(this.isServer &&
|
|
2609
|
-
this.getMatch(matchId)!.ssr === true)
|
|
2610
|
-
) {
|
|
2611
|
-
this.loadRouteChunk(route)
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
updateMatch(matchId, (prev) => ({
|
|
2615
|
-
...prev,
|
|
2616
|
-
isFetching: 'loader',
|
|
2617
|
-
}))
|
|
2618
|
-
|
|
2619
|
-
// Kick off the loader!
|
|
2620
|
-
const loaderData =
|
|
2621
|
-
await route.options.loader?.(getLoaderContext())
|
|
2622
|
-
|
|
2623
|
-
handleRedirectAndNotFound(
|
|
2624
|
-
this.getMatch(matchId)!,
|
|
2625
|
-
loaderData,
|
|
2626
|
-
)
|
|
2627
|
-
updateMatch(matchId, (prev) => ({
|
|
2628
|
-
...prev,
|
|
2629
|
-
loaderData,
|
|
2630
|
-
}))
|
|
2631
|
-
|
|
2632
|
-
// Lazy option can modify the route options,
|
|
2633
|
-
// so we need to wait for it to resolve before
|
|
2634
|
-
// we can use the options
|
|
2635
|
-
await route._lazyPromise
|
|
2636
|
-
const head = await executeHead()
|
|
2637
|
-
await potentialPendingMinPromise()
|
|
2638
|
-
|
|
2639
|
-
// Last but not least, wait for the the components
|
|
2640
|
-
// to be preloaded before we resolve the match
|
|
2641
|
-
await route._componentsPromise
|
|
2642
|
-
updateMatch(matchId, (prev) => ({
|
|
2643
|
-
...prev,
|
|
2644
|
-
error: undefined,
|
|
2645
|
-
status: 'success',
|
|
2646
|
-
isFetching: false,
|
|
2647
|
-
updatedAt: Date.now(),
|
|
2648
|
-
...head,
|
|
2649
|
-
}))
|
|
2650
|
-
} catch (e) {
|
|
2651
|
-
let error = e
|
|
2652
|
-
|
|
2653
|
-
await potentialPendingMinPromise()
|
|
2654
|
-
|
|
2655
|
-
handleRedirectAndNotFound(this.getMatch(matchId)!, e)
|
|
2656
|
-
|
|
2657
|
-
try {
|
|
2658
|
-
route.options.onError?.(e)
|
|
2659
|
-
} catch (onErrorError) {
|
|
2660
|
-
error = onErrorError
|
|
2661
|
-
handleRedirectAndNotFound(
|
|
2662
|
-
this.getMatch(matchId)!,
|
|
2663
|
-
onErrorError,
|
|
2664
|
-
)
|
|
2665
|
-
}
|
|
2666
|
-
const head = await executeHead()
|
|
2667
|
-
updateMatch(matchId, (prev) => ({
|
|
2668
|
-
...prev,
|
|
2669
|
-
error,
|
|
2670
|
-
status: 'error',
|
|
2671
|
-
isFetching: false,
|
|
2672
|
-
...head,
|
|
2673
|
-
}))
|
|
2674
|
-
}
|
|
2675
|
-
} catch (err) {
|
|
2676
|
-
const head = await executeHead()
|
|
2677
|
-
|
|
2678
|
-
updateMatch(matchId, (prev) => ({
|
|
2679
|
-
...prev,
|
|
2680
|
-
loaderPromise: undefined,
|
|
2681
|
-
...head,
|
|
2682
|
-
}))
|
|
2683
|
-
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2640
|
+
private runLoader = async (
|
|
2641
|
+
innerLoadContext: InnerLoadContext,
|
|
2642
|
+
matchId: string,
|
|
2643
|
+
index: number,
|
|
2644
|
+
route: AnyRoute,
|
|
2645
|
+
): Promise<void> => {
|
|
2646
|
+
try {
|
|
2647
|
+
// If the Matches component rendered
|
|
2648
|
+
// the pending component and needs to show it for
|
|
2649
|
+
// a minimum duration, we''ll wait for it to resolve
|
|
2650
|
+
// before committing to the match and resolving
|
|
2651
|
+
// the loadPromise
|
|
2686
2652
|
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
if (preload && route.options.preload === false) {
|
|
2693
|
-
// Do nothing
|
|
2694
|
-
} else if (loaderShouldRunAsync && !sync) {
|
|
2695
|
-
loaderIsRunningAsync = true
|
|
2696
|
-
;(async () => {
|
|
2697
|
-
try {
|
|
2698
|
-
await runLoader()
|
|
2699
|
-
const { loaderPromise, loadPromise } =
|
|
2700
|
-
this.getMatch(matchId)!
|
|
2701
|
-
loaderPromise?.resolve()
|
|
2702
|
-
loadPromise?.resolve()
|
|
2703
|
-
updateMatch(matchId, (prev) => ({
|
|
2704
|
-
...prev,
|
|
2705
|
-
loaderPromise: undefined,
|
|
2706
|
-
}))
|
|
2707
|
-
} catch (err) {
|
|
2708
|
-
if (isRedirect(err)) {
|
|
2709
|
-
await this.navigate(err.options)
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
})()
|
|
2713
|
-
} else if (
|
|
2714
|
-
status !== 'success' ||
|
|
2715
|
-
(loaderShouldRunAsync && sync)
|
|
2716
|
-
) {
|
|
2717
|
-
await runLoader()
|
|
2718
|
-
} else {
|
|
2719
|
-
// if the loader did not run, still update head.
|
|
2720
|
-
// reason: parent's beforeLoad may have changed the route context
|
|
2721
|
-
// and only now do we know the route context (and that the loader would not run)
|
|
2722
|
-
const head = await executeHead()
|
|
2723
|
-
updateMatch(matchId, (prev) => ({
|
|
2724
|
-
...prev,
|
|
2725
|
-
...head,
|
|
2726
|
-
}))
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
if (!loaderIsRunningAsync) {
|
|
2730
|
-
const { loaderPromise, loadPromise } =
|
|
2731
|
-
this.getMatch(matchId)!
|
|
2732
|
-
loaderPromise?.resolve()
|
|
2733
|
-
loadPromise?.resolve()
|
|
2734
|
-
}
|
|
2735
|
-
|
|
2736
|
-
updateMatch(matchId, (prev) => {
|
|
2737
|
-
clearTimeout(prev.pendingTimeout)
|
|
2738
|
-
return {
|
|
2739
|
-
...prev,
|
|
2740
|
-
isFetching: loaderIsRunningAsync
|
|
2741
|
-
? prev.isFetching
|
|
2742
|
-
: false,
|
|
2743
|
-
loaderPromise: loaderIsRunningAsync
|
|
2744
|
-
? prev.loaderPromise
|
|
2745
|
-
: undefined,
|
|
2746
|
-
invalid: false,
|
|
2747
|
-
pendingTimeout: undefined,
|
|
2748
|
-
_dehydrated: undefined,
|
|
2749
|
-
}
|
|
2750
|
-
})
|
|
2751
|
-
return this.getMatch(matchId)!
|
|
2752
|
-
})(),
|
|
2753
|
-
)
|
|
2754
|
-
})
|
|
2653
|
+
// Actually run the loader and handle the result
|
|
2654
|
+
try {
|
|
2655
|
+
if (!this.isServer || this.getMatch(matchId)!.ssr === true) {
|
|
2656
|
+
this.loadRouteChunk(route)
|
|
2657
|
+
}
|
|
2755
2658
|
|
|
2756
|
-
|
|
2659
|
+
// Kick off the loader!
|
|
2660
|
+
const loaderResult = route.options.loader?.(
|
|
2661
|
+
this.getLoaderContext(innerLoadContext, matchId, index, route),
|
|
2662
|
+
)
|
|
2663
|
+
const loaderResultIsPromise =
|
|
2664
|
+
route.options.loader && isPromise(loaderResult)
|
|
2665
|
+
|
|
2666
|
+
const willLoadSomething = !!(
|
|
2667
|
+
loaderResultIsPromise ||
|
|
2668
|
+
route._lazyPromise ||
|
|
2669
|
+
route._componentsPromise ||
|
|
2670
|
+
route.options.head ||
|
|
2671
|
+
route.options.scripts ||
|
|
2672
|
+
route.options.headers ||
|
|
2673
|
+
this.getMatch(matchId)!._nonReactive.minPendingPromise
|
|
2674
|
+
)
|
|
2675
|
+
|
|
2676
|
+
if (willLoadSomething) {
|
|
2677
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2678
|
+
...prev,
|
|
2679
|
+
isFetching: 'loader',
|
|
2680
|
+
}))
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
if (route.options.loader) {
|
|
2684
|
+
const loaderData = loaderResultIsPromise
|
|
2685
|
+
? await loaderResult
|
|
2686
|
+
: loaderResult
|
|
2687
|
+
|
|
2688
|
+
this.handleRedirectAndNotFound(
|
|
2689
|
+
innerLoadContext,
|
|
2690
|
+
this.getMatch(matchId),
|
|
2691
|
+
loaderData,
|
|
2692
|
+
)
|
|
2693
|
+
if (loaderData !== undefined) {
|
|
2694
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2695
|
+
...prev,
|
|
2696
|
+
loaderData,
|
|
2697
|
+
}))
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2757
2700
|
|
|
2758
|
-
|
|
2701
|
+
// Lazy option can modify the route options,
|
|
2702
|
+
// so we need to wait for it to resolve before
|
|
2703
|
+
// we can use the options
|
|
2704
|
+
if (route._lazyPromise) await route._lazyPromise
|
|
2705
|
+
const headResult = this.executeHead(innerLoadContext, matchId, route)
|
|
2706
|
+
const head = headResult ? await headResult : undefined
|
|
2707
|
+
const pendingPromise = this.potentialPendingMinPromise(matchId)
|
|
2708
|
+
if (pendingPromise) await pendingPromise
|
|
2709
|
+
|
|
2710
|
+
// Last but not least, wait for the the components
|
|
2711
|
+
// to be preloaded before we resolve the match
|
|
2712
|
+
if (route._componentsPromise) await route._componentsPromise
|
|
2713
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2714
|
+
...prev,
|
|
2715
|
+
error: undefined,
|
|
2716
|
+
status: 'success',
|
|
2717
|
+
isFetching: false,
|
|
2718
|
+
updatedAt: Date.now(),
|
|
2719
|
+
...head,
|
|
2720
|
+
}))
|
|
2721
|
+
} catch (e) {
|
|
2722
|
+
let error = e
|
|
2723
|
+
|
|
2724
|
+
const pendingPromise = this.potentialPendingMinPromise(matchId)
|
|
2725
|
+
if (pendingPromise) await pendingPromise
|
|
2726
|
+
|
|
2727
|
+
this.handleRedirectAndNotFound(
|
|
2728
|
+
innerLoadContext,
|
|
2729
|
+
this.getMatch(matchId),
|
|
2730
|
+
e,
|
|
2731
|
+
)
|
|
2732
|
+
|
|
2733
|
+
try {
|
|
2734
|
+
route.options.onError?.(e)
|
|
2735
|
+
} catch (onErrorError) {
|
|
2736
|
+
error = onErrorError
|
|
2737
|
+
this.handleRedirectAndNotFound(
|
|
2738
|
+
innerLoadContext,
|
|
2739
|
+
this.getMatch(matchId),
|
|
2740
|
+
onErrorError,
|
|
2741
|
+
)
|
|
2742
|
+
}
|
|
2743
|
+
const headResult = this.executeHead(innerLoadContext, matchId, route)
|
|
2744
|
+
const head = headResult ? await headResult : undefined
|
|
2745
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2746
|
+
...prev,
|
|
2747
|
+
error,
|
|
2748
|
+
status: 'error',
|
|
2749
|
+
isFetching: false,
|
|
2750
|
+
...head,
|
|
2751
|
+
}))
|
|
2752
|
+
}
|
|
2753
|
+
} catch (err) {
|
|
2754
|
+
const match = this.getMatch(matchId)
|
|
2755
|
+
// in case of a redirecting match during preload, the match does not exist
|
|
2756
|
+
if (match) {
|
|
2757
|
+
const headResult = this.executeHead(innerLoadContext, matchId, route)
|
|
2758
|
+
if (headResult) {
|
|
2759
|
+
const head = await headResult
|
|
2760
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2761
|
+
...prev,
|
|
2762
|
+
...head,
|
|
2763
|
+
}))
|
|
2764
|
+
}
|
|
2765
|
+
match._nonReactive.loaderPromise = undefined
|
|
2766
|
+
}
|
|
2767
|
+
this.handleRedirectAndNotFound(innerLoadContext, match, err)
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
private loadRouteMatch = async (
|
|
2772
|
+
innerLoadContext: InnerLoadContext,
|
|
2773
|
+
index: number,
|
|
2774
|
+
): Promise<AnyRouteMatch> => {
|
|
2775
|
+
const { id: matchId, routeId } = innerLoadContext.matches[index]!
|
|
2776
|
+
let loaderShouldRunAsync = false
|
|
2777
|
+
let loaderIsRunningAsync = false
|
|
2778
|
+
const route = this.looseRoutesById[routeId]!
|
|
2779
|
+
|
|
2780
|
+
const prevMatch = this.getMatch(matchId)!
|
|
2781
|
+
if (this.shouldSkipLoader(matchId)) {
|
|
2782
|
+
if (this.isServer) {
|
|
2783
|
+
const headResult = this.executeHead(innerLoadContext, matchId, route)
|
|
2784
|
+
if (headResult) {
|
|
2785
|
+
const head = await headResult
|
|
2786
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2787
|
+
...prev,
|
|
2788
|
+
...head,
|
|
2789
|
+
}))
|
|
2790
|
+
}
|
|
2791
|
+
return this.getMatch(matchId)!
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
// there is a loaderPromise, so we are in the middle of a load
|
|
2795
|
+
else if (prevMatch._nonReactive.loaderPromise) {
|
|
2796
|
+
// do not block if we already have stale data we can show
|
|
2797
|
+
// but only if the ongoing load is not a preload since error handling is different for preloads
|
|
2798
|
+
// and we don't want to swallow errors
|
|
2799
|
+
if (
|
|
2800
|
+
prevMatch.status === 'success' &&
|
|
2801
|
+
!innerLoadContext.sync &&
|
|
2802
|
+
!prevMatch.preload
|
|
2803
|
+
) {
|
|
2804
|
+
return this.getMatch(matchId)!
|
|
2805
|
+
}
|
|
2806
|
+
await prevMatch._nonReactive.loaderPromise
|
|
2807
|
+
const match = this.getMatch(matchId)!
|
|
2808
|
+
if (match.error) {
|
|
2809
|
+
this.handleRedirectAndNotFound(innerLoadContext, match, match.error)
|
|
2810
|
+
}
|
|
2811
|
+
} else {
|
|
2812
|
+
// This is where all of the stale-while-revalidate magic happens
|
|
2813
|
+
const age = Date.now() - this.getMatch(matchId)!.updatedAt
|
|
2814
|
+
|
|
2815
|
+
const preload = this.resolvePreload(innerLoadContext, matchId)
|
|
2816
|
+
|
|
2817
|
+
const staleAge = preload
|
|
2818
|
+
? (route.options.preloadStaleTime ??
|
|
2819
|
+
this.options.defaultPreloadStaleTime ??
|
|
2820
|
+
30_000) // 30 seconds for preloads by default
|
|
2821
|
+
: (route.options.staleTime ?? this.options.defaultStaleTime ?? 0)
|
|
2822
|
+
|
|
2823
|
+
const shouldReloadOption = route.options.shouldReload
|
|
2824
|
+
|
|
2825
|
+
// Default to reloading the route all the time
|
|
2826
|
+
// Allow shouldReload to get the last say,
|
|
2827
|
+
// if provided.
|
|
2828
|
+
const shouldReload =
|
|
2829
|
+
typeof shouldReloadOption === 'function'
|
|
2830
|
+
? shouldReloadOption(
|
|
2831
|
+
this.getLoaderContext(innerLoadContext, matchId, index, route),
|
|
2832
|
+
)
|
|
2833
|
+
: shouldReloadOption
|
|
2834
|
+
|
|
2835
|
+
const nextPreload =
|
|
2836
|
+
!!preload && !this.state.matches.some((d) => d.id === matchId)
|
|
2837
|
+
const match = this.getMatch(matchId)!
|
|
2838
|
+
match._nonReactive.loaderPromise = createControlledPromise<void>()
|
|
2839
|
+
if (nextPreload !== match.preload) {
|
|
2840
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2841
|
+
...prev,
|
|
2842
|
+
preload: nextPreload,
|
|
2843
|
+
}))
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// If the route is successful and still fresh, just resolve
|
|
2847
|
+
const { status, invalid } = this.getMatch(matchId)!
|
|
2848
|
+
loaderShouldRunAsync =
|
|
2849
|
+
status === 'success' && (invalid || (shouldReload ?? age > staleAge))
|
|
2850
|
+
if (preload && route.options.preload === false) {
|
|
2851
|
+
// Do nothing
|
|
2852
|
+
} else if (loaderShouldRunAsync && !innerLoadContext.sync) {
|
|
2853
|
+
loaderIsRunningAsync = true
|
|
2854
|
+
;(async () => {
|
|
2855
|
+
try {
|
|
2856
|
+
await this.runLoader(innerLoadContext, matchId, index, route)
|
|
2857
|
+
const match = this.getMatch(matchId)!
|
|
2858
|
+
match._nonReactive.loaderPromise?.resolve()
|
|
2859
|
+
match._nonReactive.loadPromise?.resolve()
|
|
2860
|
+
match._nonReactive.loaderPromise = undefined
|
|
2759
2861
|
} catch (err) {
|
|
2760
|
-
|
|
2862
|
+
if (isRedirect(err)) {
|
|
2863
|
+
await this.navigate(err.options)
|
|
2864
|
+
}
|
|
2761
2865
|
}
|
|
2762
2866
|
})()
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2867
|
+
} else if (
|
|
2868
|
+
status !== 'success' ||
|
|
2869
|
+
(loaderShouldRunAsync && innerLoadContext.sync)
|
|
2870
|
+
) {
|
|
2871
|
+
await this.runLoader(innerLoadContext, matchId, index, route)
|
|
2872
|
+
} else {
|
|
2873
|
+
// if the loader did not run, still update head.
|
|
2874
|
+
// reason: parent's beforeLoad may have changed the route context
|
|
2875
|
+
// and only now do we know the route context (and that the loader would not run)
|
|
2876
|
+
const headResult = this.executeHead(innerLoadContext, matchId, route)
|
|
2877
|
+
if (headResult) {
|
|
2878
|
+
const head = await headResult
|
|
2879
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2880
|
+
...prev,
|
|
2881
|
+
...head,
|
|
2882
|
+
}))
|
|
2769
2883
|
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const match = this.getMatch(matchId)!
|
|
2887
|
+
if (!loaderIsRunningAsync) {
|
|
2888
|
+
match._nonReactive.loaderPromise?.resolve()
|
|
2889
|
+
match._nonReactive.loadPromise?.resolve()
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
clearTimeout(match._nonReactive.pendingTimeout)
|
|
2893
|
+
match._nonReactive.pendingTimeout = undefined
|
|
2894
|
+
if (!loaderIsRunningAsync) match._nonReactive.loaderPromise = undefined
|
|
2895
|
+
match._nonReactive.dehydrated = undefined
|
|
2896
|
+
const nextIsFetching = loaderIsRunningAsync ? match.isFetching : false
|
|
2897
|
+
if (nextIsFetching !== match.isFetching || match.invalid !== false) {
|
|
2898
|
+
innerLoadContext.updateMatch(matchId, (prev) => ({
|
|
2899
|
+
...prev,
|
|
2900
|
+
isFetching: nextIsFetching,
|
|
2901
|
+
invalid: false,
|
|
2902
|
+
}))
|
|
2903
|
+
}
|
|
2904
|
+
return this.getMatch(matchId)!
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
loadMatches = async (baseContext: {
|
|
2908
|
+
location: ParsedLocation
|
|
2909
|
+
matches: Array<AnyRouteMatch>
|
|
2910
|
+
preload?: boolean
|
|
2911
|
+
onReady?: () => Promise<void>
|
|
2912
|
+
updateMatch?: UpdateMatchFn
|
|
2913
|
+
sync?: boolean
|
|
2914
|
+
}): Promise<Array<MakeRouteMatch>> => {
|
|
2915
|
+
const innerLoadContext = baseContext as InnerLoadContext
|
|
2916
|
+
innerLoadContext.updateMatch ??= this.updateMatch
|
|
2917
|
+
innerLoadContext.matchPromises = []
|
|
2918
|
+
|
|
2919
|
+
// make sure the pending component is immediately rendered when hydrating a match that is not SSRed
|
|
2920
|
+
// the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
|
|
2921
|
+
if (!this.isServer && this.state.matches.some((d) => d._forcePending)) {
|
|
2922
|
+
this.triggerOnReady(innerLoadContext)
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
try {
|
|
2926
|
+
// Execute all beforeLoads one by one
|
|
2927
|
+
for (let i = 0; i < innerLoadContext.matches.length; i++) {
|
|
2928
|
+
const beforeLoad = this.handleBeforeLoad(innerLoadContext, i)
|
|
2929
|
+
if (isPromise(beforeLoad)) await beforeLoad
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
// Execute all loaders in parallel
|
|
2933
|
+
const max =
|
|
2934
|
+
innerLoadContext.firstBadMatchIndex ?? innerLoadContext.matches.length
|
|
2935
|
+
for (let i = 0; i < max; i++) {
|
|
2936
|
+
innerLoadContext.matchPromises.push(
|
|
2937
|
+
this.loadRouteMatch(innerLoadContext, i),
|
|
2938
|
+
)
|
|
2939
|
+
}
|
|
2940
|
+
await Promise.all(innerLoadContext.matchPromises)
|
|
2770
2941
|
|
|
2942
|
+
const readyPromise = this.triggerOnReady(innerLoadContext)
|
|
2943
|
+
if (isPromise(readyPromise)) await readyPromise
|
|
2944
|
+
} catch (err) {
|
|
2945
|
+
if (isNotFound(err) && !innerLoadContext.preload) {
|
|
2946
|
+
const readyPromise = this.triggerOnReady(innerLoadContext)
|
|
2947
|
+
if (isPromise(readyPromise)) await readyPromise
|
|
2948
|
+
throw err
|
|
2949
|
+
}
|
|
2950
|
+
if (isRedirect(err)) {
|
|
2771
2951
|
throw err
|
|
2772
2952
|
}
|
|
2773
2953
|
}
|
|
2774
2954
|
|
|
2775
|
-
return matches
|
|
2955
|
+
return innerLoadContext.matches
|
|
2776
2956
|
}
|
|
2777
2957
|
|
|
2778
2958
|
invalidate: InvalidateFn<
|
|
@@ -2791,7 +2971,7 @@ export class RouterCore<
|
|
|
2791
2971
|
invalid: true,
|
|
2792
2972
|
...(opts?.forcePending || d.status === 'error'
|
|
2793
2973
|
? ({ status: 'pending', error: undefined } as const)
|
|
2794
|
-
:
|
|
2974
|
+
: undefined),
|
|
2795
2975
|
}
|
|
2796
2976
|
}
|
|
2797
2977
|
return d
|
|
@@ -2869,33 +3049,44 @@ export class RouterCore<
|
|
|
2869
3049
|
}
|
|
2870
3050
|
|
|
2871
3051
|
loadRouteChunk = (route: AnyRoute) => {
|
|
2872
|
-
if (route._lazyPromise === undefined) {
|
|
3052
|
+
if (!route._lazyLoaded && route._lazyPromise === undefined) {
|
|
2873
3053
|
if (route.lazyFn) {
|
|
2874
3054
|
route._lazyPromise = route.lazyFn().then((lazyRoute) => {
|
|
2875
3055
|
// explicitly don't copy over the lazy route's id
|
|
2876
3056
|
const { id: _id, ...options } = lazyRoute.options
|
|
2877
3057
|
Object.assign(route.options, options)
|
|
3058
|
+
route._lazyLoaded = true
|
|
3059
|
+
route._lazyPromise = undefined // gc promise, we won't need it anymore
|
|
2878
3060
|
})
|
|
2879
3061
|
} else {
|
|
2880
|
-
route.
|
|
3062
|
+
route._lazyLoaded = true
|
|
2881
3063
|
}
|
|
2882
3064
|
}
|
|
2883
3065
|
|
|
2884
3066
|
// If for some reason lazy resolves more lazy components...
|
|
2885
|
-
// We'll wait for that before
|
|
3067
|
+
// We'll wait for that before we attempt to preload the
|
|
2886
3068
|
// components themselves.
|
|
2887
|
-
if (route._componentsPromise === undefined) {
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
3069
|
+
if (!route._componentsLoaded && route._componentsPromise === undefined) {
|
|
3070
|
+
const loadComponents = () => {
|
|
3071
|
+
const preloads = []
|
|
3072
|
+
for (const type of componentTypes) {
|
|
3073
|
+
const preload = (route.options[type] as any)?.preload
|
|
3074
|
+
if (preload) preloads.push(preload())
|
|
3075
|
+
}
|
|
3076
|
+
if (preloads.length)
|
|
3077
|
+
return Promise.all(preloads).then(() => {
|
|
3078
|
+
route._componentsLoaded = true
|
|
3079
|
+
route._componentsPromise = undefined // gc promise, we won't need it anymore
|
|
3080
|
+
})
|
|
3081
|
+
route._componentsLoaded = true
|
|
3082
|
+
route._componentsPromise = undefined // gc promise, we won't need it anymore
|
|
3083
|
+
return
|
|
3084
|
+
}
|
|
3085
|
+
route._componentsPromise = route._lazyPromise
|
|
3086
|
+
? route._lazyPromise.then(loadComponents)
|
|
3087
|
+
: loadComponents()
|
|
2898
3088
|
}
|
|
3089
|
+
|
|
2899
3090
|
return route._componentsPromise
|
|
2900
3091
|
}
|
|
2901
3092
|
|
|
@@ -3035,17 +3226,9 @@ export class RouterCore<
|
|
|
3035
3226
|
|
|
3036
3227
|
serverSsr?: ServerSsr
|
|
3037
3228
|
|
|
3038
|
-
_handleNotFound = (
|
|
3039
|
-
|
|
3229
|
+
private _handleNotFound = (
|
|
3230
|
+
innerLoadContext: InnerLoadContext,
|
|
3040
3231
|
err: NotFoundError,
|
|
3041
|
-
{
|
|
3042
|
-
updateMatch = this.updateMatch,
|
|
3043
|
-
}: {
|
|
3044
|
-
updateMatch?: (
|
|
3045
|
-
id: string,
|
|
3046
|
-
updater: (match: AnyRouteMatch) => AnyRouteMatch,
|
|
3047
|
-
) => void
|
|
3048
|
-
} = {},
|
|
3049
3232
|
) => {
|
|
3050
3233
|
// Find the route that should handle the not found error
|
|
3051
3234
|
// First check if a specific route is requested to show the error
|
|
@@ -3053,7 +3236,7 @@ export class RouterCore<
|
|
|
3053
3236
|
const matchesByRouteId: Record<string, AnyRouteMatch> = {}
|
|
3054
3237
|
|
|
3055
3238
|
// Setup routesByRouteId object for quick access
|
|
3056
|
-
for (const match of matches) {
|
|
3239
|
+
for (const match of innerLoadContext.matches) {
|
|
3057
3240
|
matchesByRouteId[match.routeId] = match
|
|
3058
3241
|
}
|
|
3059
3242
|
|
|
@@ -3082,7 +3265,7 @@ export class RouterCore<
|
|
|
3082
3265
|
)
|
|
3083
3266
|
|
|
3084
3267
|
// Assign the error to the match - using non-null assertion since we've checked with invariant
|
|
3085
|
-
updateMatch(matchForRoute.id, (prev) => ({
|
|
3268
|
+
innerLoadContext.updateMatch(matchForRoute.id, (prev) => ({
|
|
3086
3269
|
...prev,
|
|
3087
3270
|
status: 'notFound',
|
|
3088
3271
|
error: err,
|
|
@@ -3091,9 +3274,7 @@ export class RouterCore<
|
|
|
3091
3274
|
|
|
3092
3275
|
if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
|
|
3093
3276
|
err.routeId = routeCursor.parentRoute.id
|
|
3094
|
-
this._handleNotFound(
|
|
3095
|
-
updateMatch,
|
|
3096
|
-
})
|
|
3277
|
+
this._handleNotFound(innerLoadContext, err)
|
|
3097
3278
|
}
|
|
3098
3279
|
}
|
|
3099
3280
|
|
|
@@ -3108,6 +3289,16 @@ export class SearchParamError extends Error {}
|
|
|
3108
3289
|
|
|
3109
3290
|
export class PathParamError extends Error {}
|
|
3110
3291
|
|
|
3292
|
+
function makeMaybe<TValue, TError>(
|
|
3293
|
+
value: TValue,
|
|
3294
|
+
error: TError,
|
|
3295
|
+
): { status: 'success'; value: TValue } | { status: 'error'; error: TError } {
|
|
3296
|
+
if (error) {
|
|
3297
|
+
return { status: 'error' as const, error }
|
|
3298
|
+
}
|
|
3299
|
+
return { status: 'success' as const, value }
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3111
3302
|
const normalize = (str: string) =>
|
|
3112
3303
|
str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str
|
|
3113
3304
|
function comparePaths(a: string, b: string) {
|
|
@@ -3562,7 +3753,8 @@ function applySearchMiddleware({
|
|
|
3562
3753
|
try {
|
|
3563
3754
|
const validatedSearch = {
|
|
3564
3755
|
...result,
|
|
3565
|
-
...(validateSearch(route.options.validateSearch, result) ??
|
|
3756
|
+
...(validateSearch(route.options.validateSearch, result) ??
|
|
3757
|
+
undefined),
|
|
3566
3758
|
}
|
|
3567
3759
|
return validatedSearch
|
|
3568
3760
|
} catch {
|