@tanstack/router-core 1.124.2 → 1.125.1
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 +29 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/route.cjs +0 -5
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +20 -6
- package/dist/cjs/router.cjs +208 -79
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +6 -1
- package/dist/cjs/ssr/ssr-client.cjs +38 -2
- package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-client.d.cts +1 -0
- package/dist/cjs/ssr/ssr-server.cjs +2 -1
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/utils.cjs +5 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +1 -0
- package/dist/esm/Matches.d.ts +29 -0
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/route.d.ts +20 -6
- package/dist/esm/route.js +0 -5
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +6 -1
- package/dist/esm/router.js +208 -79
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/ssr/ssr-client.d.ts +1 -0
- package/dist/esm/ssr/ssr-client.js +38 -2
- package/dist/esm/ssr/ssr-client.js.map +1 -1
- package/dist/esm/ssr/ssr-server.js +2 -1
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/utils.d.ts +1 -0
- package/dist/esm/utils.js +5 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/Matches.ts +38 -0
- package/src/index.ts +1 -0
- package/src/route.ts +32 -10
- package/src/router.ts +259 -96
- package/src/ssr/ssr-client.ts +49 -3
- package/src/ssr/ssr-server.ts +1 -0
- package/src/utils.ts +12 -0
package/src/router.ts
CHANGED
|
@@ -57,6 +57,7 @@ import type {
|
|
|
57
57
|
RouteContextOptions,
|
|
58
58
|
RouteMask,
|
|
59
59
|
SearchMiddleware,
|
|
60
|
+
SsrContextOptions,
|
|
60
61
|
} from './route'
|
|
61
62
|
import type {
|
|
62
63
|
FullSearchSchema,
|
|
@@ -342,7 +343,12 @@ export interface RouterOptions<
|
|
|
342
343
|
*/
|
|
343
344
|
isServer?: boolean
|
|
344
345
|
|
|
345
|
-
|
|
346
|
+
/**
|
|
347
|
+
* The default `ssr` a route should use if no `ssr` is provided.
|
|
348
|
+
*
|
|
349
|
+
* @default true
|
|
350
|
+
*/
|
|
351
|
+
defaultSsr?: boolean | 'data-only'
|
|
346
352
|
|
|
347
353
|
search?: {
|
|
348
354
|
/**
|
|
@@ -946,7 +952,6 @@ export class RouterCore<
|
|
|
946
952
|
initRoute: (route, i) => {
|
|
947
953
|
route.init({
|
|
948
954
|
originalIndex: i,
|
|
949
|
-
defaultSsr: this.options.defaultSsr,
|
|
950
955
|
})
|
|
951
956
|
},
|
|
952
957
|
})
|
|
@@ -960,7 +965,6 @@ export class RouterCore<
|
|
|
960
965
|
if (notFoundRoute) {
|
|
961
966
|
notFoundRoute.init({
|
|
962
967
|
originalIndex: 99999999999,
|
|
963
|
-
defaultSsr: this.options.defaultSsr,
|
|
964
968
|
})
|
|
965
969
|
this.routesById[notFoundRoute.id] = notFoundRoute
|
|
966
970
|
}
|
|
@@ -1387,7 +1391,13 @@ export class RouterCore<
|
|
|
1387
1391
|
if (!match) return
|
|
1388
1392
|
|
|
1389
1393
|
match.abortController.abort()
|
|
1390
|
-
|
|
1394
|
+
this.updateMatch(id, (prev) => {
|
|
1395
|
+
clearTimeout(prev.pendingTimeout)
|
|
1396
|
+
return {
|
|
1397
|
+
...prev,
|
|
1398
|
+
pendingTimeout: undefined,
|
|
1399
|
+
}
|
|
1400
|
+
})
|
|
1391
1401
|
}
|
|
1392
1402
|
|
|
1393
1403
|
cancelMatches = () => {
|
|
@@ -1787,7 +1797,11 @@ export class RouterCore<
|
|
|
1787
1797
|
}
|
|
1788
1798
|
}
|
|
1789
1799
|
// Match the routes
|
|
1790
|
-
|
|
1800
|
+
let pendingMatches = this.matchRoutes(this.latestLocation)
|
|
1801
|
+
// in SPA mode we only want to load the root route
|
|
1802
|
+
if (this.isShell) {
|
|
1803
|
+
pendingMatches = pendingMatches.slice(0, 1)
|
|
1804
|
+
}
|
|
1791
1805
|
|
|
1792
1806
|
// Ingest the new matches
|
|
1793
1807
|
this.__store.setState((s) => ({
|
|
@@ -1806,7 +1820,6 @@ export class RouterCore<
|
|
|
1806
1820
|
load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
|
|
1807
1821
|
let redirect: AnyRedirect | undefined
|
|
1808
1822
|
let notFound: NotFoundError | undefined
|
|
1809
|
-
|
|
1810
1823
|
let loadPromise: Promise<void>
|
|
1811
1824
|
|
|
1812
1825
|
// eslint-disable-next-line prefer-const
|
|
@@ -2061,6 +2074,40 @@ export class RouterCore<
|
|
|
2061
2074
|
const triggerOnReady = async () => {
|
|
2062
2075
|
if (!rendered) {
|
|
2063
2076
|
rendered = true
|
|
2077
|
+
|
|
2078
|
+
// create a minPendingPromise for matches that have forcePending set to true
|
|
2079
|
+
// usually the minPendingPromise is created in the Match component if a pending match is rendered
|
|
2080
|
+
// however, this might be too late if the match synchronously resolves
|
|
2081
|
+
if (!allPreload && !this.isServer) {
|
|
2082
|
+
matches.forEach((match) => {
|
|
2083
|
+
const {
|
|
2084
|
+
id: matchId,
|
|
2085
|
+
routeId,
|
|
2086
|
+
_forcePending,
|
|
2087
|
+
minPendingPromise,
|
|
2088
|
+
} = match
|
|
2089
|
+
const route = this.looseRoutesById[routeId]!
|
|
2090
|
+
const pendingMinMs =
|
|
2091
|
+
route.options.pendingMinMs ?? this.options.defaultPendingMinMs
|
|
2092
|
+
if (_forcePending && pendingMinMs && !minPendingPromise) {
|
|
2093
|
+
const minPendingPromise = createControlledPromise<void>()
|
|
2094
|
+
updateMatch(matchId, (prev) => ({
|
|
2095
|
+
...prev,
|
|
2096
|
+
minPendingPromise,
|
|
2097
|
+
}))
|
|
2098
|
+
|
|
2099
|
+
setTimeout(() => {
|
|
2100
|
+
minPendingPromise.resolve()
|
|
2101
|
+
// We've handled the minPendingPromise, so we can delete it
|
|
2102
|
+
updateMatch(matchId, (prev) => ({
|
|
2103
|
+
...prev,
|
|
2104
|
+
minPendingPromise: undefined,
|
|
2105
|
+
}))
|
|
2106
|
+
}, pendingMinMs)
|
|
2107
|
+
}
|
|
2108
|
+
})
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2064
2111
|
await onReady?.()
|
|
2065
2112
|
}
|
|
2066
2113
|
}
|
|
@@ -2069,6 +2116,12 @@ export class RouterCore<
|
|
|
2069
2116
|
return !!(allPreload && !this.state.matches.find((d) => d.id === matchId))
|
|
2070
2117
|
}
|
|
2071
2118
|
|
|
2119
|
+
// make sure the pending component is immediately rendered when hydrating a match that is not SSRed
|
|
2120
|
+
// the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
|
|
2121
|
+
if (!this.isServer && this.state.matches.find((d) => d._forcePending)) {
|
|
2122
|
+
triggerOnReady()
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2072
2125
|
const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
|
|
2073
2126
|
if (isRedirect(err) || isNotFound(err)) {
|
|
2074
2127
|
if (isRedirect(err)) {
|
|
@@ -2120,6 +2173,21 @@ export class RouterCore<
|
|
|
2120
2173
|
}
|
|
2121
2174
|
}
|
|
2122
2175
|
|
|
2176
|
+
const shouldSkipLoader = (matchId: string) => {
|
|
2177
|
+
const match = this.getMatch(matchId)!
|
|
2178
|
+
// upon hydration, we skip the loader if the match has been dehydrated on the server
|
|
2179
|
+
if (!this.isServer && match._dehydrated) {
|
|
2180
|
+
return true
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
if (this.isServer) {
|
|
2184
|
+
if (match.ssr === false) {
|
|
2185
|
+
return true
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return false
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2123
2191
|
try {
|
|
2124
2192
|
await new Promise<void>((resolveAll, rejectAll) => {
|
|
2125
2193
|
;(async () => {
|
|
@@ -2169,12 +2237,73 @@ export class RouterCore<
|
|
|
2169
2237
|
for (const [index, { id: matchId, routeId }] of matches.entries()) {
|
|
2170
2238
|
const existingMatch = this.getMatch(matchId)!
|
|
2171
2239
|
const parentMatchId = matches[index - 1]?.id
|
|
2240
|
+
const parentMatch = parentMatchId
|
|
2241
|
+
? this.getMatch(parentMatchId)!
|
|
2242
|
+
: undefined
|
|
2172
2243
|
|
|
2173
2244
|
const route = this.looseRoutesById[routeId]!
|
|
2174
2245
|
|
|
2175
2246
|
const pendingMs =
|
|
2176
2247
|
route.options.pendingMs ?? this.options.defaultPendingMs
|
|
2177
2248
|
|
|
2249
|
+
// on the server, determine whether SSR the current match or not
|
|
2250
|
+
if (this.isServer) {
|
|
2251
|
+
const defaultSsr = this.options.defaultSsr ?? true
|
|
2252
|
+
let ssr: boolean | 'data-only'
|
|
2253
|
+
if (parentMatch?.ssr === false) {
|
|
2254
|
+
ssr = false
|
|
2255
|
+
} else {
|
|
2256
|
+
let tempSsr: boolean | 'data-only'
|
|
2257
|
+
if (route.options.ssr === undefined) {
|
|
2258
|
+
tempSsr = defaultSsr
|
|
2259
|
+
} else if (typeof route.options.ssr === 'function') {
|
|
2260
|
+
const { search, params } = this.getMatch(matchId)!
|
|
2261
|
+
|
|
2262
|
+
function makeMaybe(value: any, error: any) {
|
|
2263
|
+
if (error) {
|
|
2264
|
+
return { status: 'error' as const, error }
|
|
2265
|
+
}
|
|
2266
|
+
return { status: 'success' as const, value }
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
const ssrFnContext: SsrContextOptions<any, any, any> = {
|
|
2270
|
+
search: makeMaybe(search, existingMatch.searchError),
|
|
2271
|
+
params: makeMaybe(params, existingMatch.paramsError),
|
|
2272
|
+
location,
|
|
2273
|
+
matches: matches.map((match) => ({
|
|
2274
|
+
index: match.index,
|
|
2275
|
+
pathname: match.pathname,
|
|
2276
|
+
fullPath: match.fullPath,
|
|
2277
|
+
staticData: match.staticData,
|
|
2278
|
+
id: match.id,
|
|
2279
|
+
routeId: match.routeId,
|
|
2280
|
+
search: makeMaybe(match.search, match.searchError),
|
|
2281
|
+
params: makeMaybe(match.params, match.paramsError),
|
|
2282
|
+
ssr: match.ssr,
|
|
2283
|
+
})),
|
|
2284
|
+
}
|
|
2285
|
+
tempSsr =
|
|
2286
|
+
(await route.options.ssr(ssrFnContext)) ?? defaultSsr
|
|
2287
|
+
} else {
|
|
2288
|
+
tempSsr = route.options.ssr
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
if (tempSsr === true && parentMatch?.ssr === 'data-only') {
|
|
2292
|
+
ssr = 'data-only'
|
|
2293
|
+
} else {
|
|
2294
|
+
ssr = tempSsr
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
updateMatch(matchId, (prev) => ({
|
|
2298
|
+
...prev,
|
|
2299
|
+
ssr,
|
|
2300
|
+
}))
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
if (shouldSkipLoader(matchId)) {
|
|
2304
|
+
continue
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2178
2307
|
const shouldPending = !!(
|
|
2179
2308
|
onReady &&
|
|
2180
2309
|
!this.isServer &&
|
|
@@ -2189,21 +2318,31 @@ export class RouterCore<
|
|
|
2189
2318
|
)
|
|
2190
2319
|
|
|
2191
2320
|
let executeBeforeLoad = true
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
if (shouldPending) {
|
|
2199
|
-
setTimeout(() => {
|
|
2321
|
+
const setupPendingTimeout = () => {
|
|
2322
|
+
if (
|
|
2323
|
+
shouldPending &&
|
|
2324
|
+
this.getMatch(matchId)!.pendingTimeout === undefined
|
|
2325
|
+
) {
|
|
2326
|
+
const pendingTimeout = setTimeout(() => {
|
|
2200
2327
|
try {
|
|
2201
2328
|
// Update the match and prematurely resolve the loadMatches promise so that
|
|
2202
2329
|
// the pending component can start rendering
|
|
2203
2330
|
triggerOnReady()
|
|
2204
2331
|
} catch {}
|
|
2205
2332
|
}, pendingMs)
|
|
2333
|
+
updateMatch(matchId, (prev) => ({
|
|
2334
|
+
...prev,
|
|
2335
|
+
pendingTimeout,
|
|
2336
|
+
}))
|
|
2206
2337
|
}
|
|
2338
|
+
}
|
|
2339
|
+
if (
|
|
2340
|
+
// If we are in the middle of a load, either of these will be present
|
|
2341
|
+
// (not to be confused with `loadPromise`, which is always defined)
|
|
2342
|
+
existingMatch.beforeLoadPromise ||
|
|
2343
|
+
existingMatch.loaderPromise
|
|
2344
|
+
) {
|
|
2345
|
+
setupPendingTimeout()
|
|
2207
2346
|
|
|
2208
2347
|
// Wait for the beforeLoad to resolve before we continue
|
|
2209
2348
|
await existingMatch.beforeLoadPromise
|
|
@@ -2231,21 +2370,6 @@ export class RouterCore<
|
|
|
2231
2370
|
beforeLoadPromise: createControlledPromise<void>(),
|
|
2232
2371
|
}
|
|
2233
2372
|
})
|
|
2234
|
-
const abortController = new AbortController()
|
|
2235
|
-
|
|
2236
|
-
let pendingTimeout: ReturnType<typeof setTimeout>
|
|
2237
|
-
|
|
2238
|
-
if (shouldPending) {
|
|
2239
|
-
// If we might show a pending component, we need to wait for the
|
|
2240
|
-
// pending promise to resolve before we start showing that state
|
|
2241
|
-
pendingTimeout = setTimeout(() => {
|
|
2242
|
-
try {
|
|
2243
|
-
// Update the match and prematurely resolve the loadMatches promise so that
|
|
2244
|
-
// the pending component can start rendering
|
|
2245
|
-
triggerOnReady()
|
|
2246
|
-
} catch {}
|
|
2247
|
-
}, pendingMs)
|
|
2248
|
-
}
|
|
2249
2373
|
|
|
2250
2374
|
const { paramsError, searchError } = this.getMatch(matchId)!
|
|
2251
2375
|
|
|
@@ -2257,19 +2381,20 @@ export class RouterCore<
|
|
|
2257
2381
|
handleSerialError(index, searchError, 'VALIDATE_SEARCH')
|
|
2258
2382
|
}
|
|
2259
2383
|
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2384
|
+
setupPendingTimeout()
|
|
2385
|
+
|
|
2386
|
+
const abortController = new AbortController()
|
|
2387
|
+
|
|
2388
|
+
const parentMatchContext =
|
|
2389
|
+
parentMatch?.context ?? this.options.context ?? {}
|
|
2264
2390
|
|
|
2265
2391
|
updateMatch(matchId, (prev) => ({
|
|
2266
2392
|
...prev,
|
|
2267
2393
|
isFetching: 'beforeLoad',
|
|
2268
2394
|
fetchCount: prev.fetchCount + 1,
|
|
2269
2395
|
abortController,
|
|
2270
|
-
pendingTimeout,
|
|
2271
2396
|
context: {
|
|
2272
|
-
...
|
|
2397
|
+
...parentMatchContext,
|
|
2273
2398
|
...prev.__routeContext,
|
|
2274
2399
|
},
|
|
2275
2400
|
}))
|
|
@@ -2315,7 +2440,7 @@ export class RouterCore<
|
|
|
2315
2440
|
...prev,
|
|
2316
2441
|
__beforeLoadContext: beforeLoadContext,
|
|
2317
2442
|
context: {
|
|
2318
|
-
...
|
|
2443
|
+
...parentMatchContext,
|
|
2319
2444
|
...prev.__routeContext,
|
|
2320
2445
|
...beforeLoadContext,
|
|
2321
2446
|
},
|
|
@@ -2346,10 +2471,65 @@ export class RouterCore<
|
|
|
2346
2471
|
(async () => {
|
|
2347
2472
|
let loaderShouldRunAsync = false
|
|
2348
2473
|
let loaderIsRunningAsync = false
|
|
2474
|
+
const route = this.looseRoutesById[routeId]!
|
|
2475
|
+
|
|
2476
|
+
const executeHead = async () => {
|
|
2477
|
+
const match = this.getMatch(matchId)
|
|
2478
|
+
// in case of a redirecting match during preload, the match does not exist
|
|
2479
|
+
if (!match) {
|
|
2480
|
+
return
|
|
2481
|
+
}
|
|
2482
|
+
const assetContext = {
|
|
2483
|
+
matches,
|
|
2484
|
+
match,
|
|
2485
|
+
params: match.params,
|
|
2486
|
+
loaderData: match.loaderData,
|
|
2487
|
+
}
|
|
2488
|
+
const headFnContent =
|
|
2489
|
+
await route.options.head?.(assetContext)
|
|
2490
|
+
const meta = headFnContent?.meta
|
|
2491
|
+
const links = headFnContent?.links
|
|
2492
|
+
const headScripts = headFnContent?.scripts
|
|
2493
|
+
const styles = headFnContent?.styles
|
|
2494
|
+
|
|
2495
|
+
const scripts = await route.options.scripts?.(assetContext)
|
|
2496
|
+
const headers = await route.options.headers?.(assetContext)
|
|
2497
|
+
return {
|
|
2498
|
+
meta,
|
|
2499
|
+
links,
|
|
2500
|
+
headScripts,
|
|
2501
|
+
headers,
|
|
2502
|
+
scripts,
|
|
2503
|
+
styles,
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
const potentialPendingMinPromise = async () => {
|
|
2508
|
+
const latestMatch = this.getMatch(matchId)!
|
|
2509
|
+
if (latestMatch.minPendingPromise) {
|
|
2510
|
+
await latestMatch.minPendingPromise
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2349
2513
|
|
|
2350
2514
|
const prevMatch = this.getMatch(matchId)!
|
|
2515
|
+
if (shouldSkipLoader(matchId)) {
|
|
2516
|
+
if (this.isServer) {
|
|
2517
|
+
const head = await executeHead()
|
|
2518
|
+
updateMatch(matchId, (prev) => ({
|
|
2519
|
+
...prev,
|
|
2520
|
+
...head,
|
|
2521
|
+
}))
|
|
2522
|
+
this.serverSsr?.onMatchSettled({
|
|
2523
|
+
router: this,
|
|
2524
|
+
match: this.getMatch(matchId)!,
|
|
2525
|
+
})
|
|
2526
|
+
return this.getMatch(matchId)!
|
|
2527
|
+
} else {
|
|
2528
|
+
await potentialPendingMinPromise()
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2351
2531
|
// there is a loaderPromise, so we are in the middle of a load
|
|
2352
|
-
if (prevMatch.loaderPromise) {
|
|
2532
|
+
else if (prevMatch.loaderPromise) {
|
|
2353
2533
|
// do not block if we already have stale data we can show
|
|
2354
2534
|
// but only if the ongoing load is not a preload since error handling is different for preloads
|
|
2355
2535
|
// and we don't want to swallow errors
|
|
@@ -2367,7 +2547,6 @@ export class RouterCore<
|
|
|
2367
2547
|
}
|
|
2368
2548
|
} else {
|
|
2369
2549
|
const parentMatchPromise = matchPromises[index - 1] as any
|
|
2370
|
-
const route = this.looseRoutesById[routeId]!
|
|
2371
2550
|
|
|
2372
2551
|
const getLoaderContext = (): LoaderFnContext => {
|
|
2373
2552
|
const {
|
|
@@ -2426,39 +2605,6 @@ export class RouterCore<
|
|
|
2426
2605
|
!this.state.matches.find((d) => d.id === matchId),
|
|
2427
2606
|
}))
|
|
2428
2607
|
|
|
2429
|
-
const executeHead = async () => {
|
|
2430
|
-
const match = this.getMatch(matchId)
|
|
2431
|
-
// in case of a redirecting match during preload, the match does not exist
|
|
2432
|
-
if (!match) {
|
|
2433
|
-
return
|
|
2434
|
-
}
|
|
2435
|
-
const assetContext = {
|
|
2436
|
-
matches,
|
|
2437
|
-
match,
|
|
2438
|
-
params: match.params,
|
|
2439
|
-
loaderData: match.loaderData,
|
|
2440
|
-
}
|
|
2441
|
-
const headFnContent =
|
|
2442
|
-
await route.options.head?.(assetContext)
|
|
2443
|
-
const meta = headFnContent?.meta
|
|
2444
|
-
const links = headFnContent?.links
|
|
2445
|
-
const headScripts = headFnContent?.scripts
|
|
2446
|
-
const styles = headFnContent?.styles
|
|
2447
|
-
|
|
2448
|
-
const scripts =
|
|
2449
|
-
await route.options.scripts?.(assetContext)
|
|
2450
|
-
const headers =
|
|
2451
|
-
await route.options.headers?.(assetContext)
|
|
2452
|
-
return {
|
|
2453
|
-
meta,
|
|
2454
|
-
links,
|
|
2455
|
-
headScripts,
|
|
2456
|
-
headers,
|
|
2457
|
-
scripts,
|
|
2458
|
-
styles,
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
2608
|
const runLoader = async () => {
|
|
2463
2609
|
try {
|
|
2464
2610
|
// If the Matches component rendered
|
|
@@ -2466,17 +2612,16 @@ export class RouterCore<
|
|
|
2466
2612
|
// a minimum duration, we''ll wait for it to resolve
|
|
2467
2613
|
// before committing to the match and resolving
|
|
2468
2614
|
// the loadPromise
|
|
2469
|
-
const potentialPendingMinPromise = async () => {
|
|
2470
|
-
const latestMatch = this.getMatch(matchId)!
|
|
2471
|
-
|
|
2472
|
-
if (latestMatch.minPendingPromise) {
|
|
2473
|
-
await latestMatch.minPendingPromise
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
2615
|
|
|
2477
2616
|
// Actually run the loader and handle the result
|
|
2478
2617
|
try {
|
|
2479
|
-
|
|
2618
|
+
if (
|
|
2619
|
+
!this.isServer ||
|
|
2620
|
+
(this.isServer &&
|
|
2621
|
+
this.getMatch(matchId)!.ssr === true)
|
|
2622
|
+
) {
|
|
2623
|
+
this.loadRouteChunk(route)
|
|
2624
|
+
}
|
|
2480
2625
|
|
|
2481
2626
|
updateMatch(matchId, (prev) => ({
|
|
2482
2627
|
...prev,
|
|
@@ -2491,29 +2636,27 @@ export class RouterCore<
|
|
|
2491
2636
|
this.getMatch(matchId)!,
|
|
2492
2637
|
loaderData,
|
|
2493
2638
|
)
|
|
2639
|
+
updateMatch(matchId, (prev) => ({
|
|
2640
|
+
...prev,
|
|
2641
|
+
loaderData,
|
|
2642
|
+
}))
|
|
2494
2643
|
|
|
2495
2644
|
// Lazy option can modify the route options,
|
|
2496
2645
|
// so we need to wait for it to resolve before
|
|
2497
2646
|
// we can use the options
|
|
2498
2647
|
await route._lazyPromise
|
|
2499
|
-
|
|
2648
|
+
const head = await executeHead()
|
|
2500
2649
|
await potentialPendingMinPromise()
|
|
2501
2650
|
|
|
2502
2651
|
// Last but not least, wait for the the components
|
|
2503
2652
|
// to be preloaded before we resolve the match
|
|
2504
2653
|
await route._componentsPromise
|
|
2505
|
-
|
|
2506
2654
|
updateMatch(matchId, (prev) => ({
|
|
2507
2655
|
...prev,
|
|
2508
2656
|
error: undefined,
|
|
2509
2657
|
status: 'success',
|
|
2510
2658
|
isFetching: false,
|
|
2511
2659
|
updatedAt: Date.now(),
|
|
2512
|
-
loaderData,
|
|
2513
|
-
}))
|
|
2514
|
-
const head = await executeHead()
|
|
2515
|
-
updateMatch(matchId, (prev) => ({
|
|
2516
|
-
...prev,
|
|
2517
2660
|
...head,
|
|
2518
2661
|
}))
|
|
2519
2662
|
} catch (e) {
|
|
@@ -2559,13 +2702,18 @@ export class RouterCore<
|
|
|
2559
2702
|
}
|
|
2560
2703
|
|
|
2561
2704
|
// If the route is successful and still fresh, just resolve
|
|
2562
|
-
const { status, invalid } =
|
|
2705
|
+
const { status, invalid, _forcePending } =
|
|
2706
|
+
this.getMatch(matchId)!
|
|
2563
2707
|
loaderShouldRunAsync =
|
|
2564
2708
|
status === 'success' &&
|
|
2565
2709
|
(invalid || (shouldReload ?? age > staleAge))
|
|
2566
2710
|
if (preload && route.options.preload === false) {
|
|
2567
2711
|
// Do nothing
|
|
2568
|
-
} else if (
|
|
2712
|
+
} else if (
|
|
2713
|
+
loaderShouldRunAsync &&
|
|
2714
|
+
!sync &&
|
|
2715
|
+
!_forcePending
|
|
2716
|
+
) {
|
|
2569
2717
|
loaderIsRunningAsync = true
|
|
2570
2718
|
;(async () => {
|
|
2571
2719
|
try {
|
|
@@ -2590,6 +2738,9 @@ export class RouterCore<
|
|
|
2590
2738
|
) {
|
|
2591
2739
|
await runLoader()
|
|
2592
2740
|
} else {
|
|
2741
|
+
if (_forcePending) {
|
|
2742
|
+
await potentialPendingMinPromise()
|
|
2743
|
+
}
|
|
2593
2744
|
// if the loader did not run, still update head.
|
|
2594
2745
|
// reason: parent's beforeLoad may have changed the route context
|
|
2595
2746
|
// and only now do we know the route context (and that the loader would not run)
|
|
@@ -2598,6 +2749,10 @@ export class RouterCore<
|
|
|
2598
2749
|
...prev,
|
|
2599
2750
|
...head,
|
|
2600
2751
|
}))
|
|
2752
|
+
this.serverSsr?.onMatchSettled({
|
|
2753
|
+
router: this,
|
|
2754
|
+
match: this.getMatch(matchId)!,
|
|
2755
|
+
})
|
|
2601
2756
|
}
|
|
2602
2757
|
}
|
|
2603
2758
|
if (!loaderIsRunningAsync) {
|
|
@@ -2607,14 +2762,22 @@ export class RouterCore<
|
|
|
2607
2762
|
loadPromise?.resolve()
|
|
2608
2763
|
}
|
|
2609
2764
|
|
|
2610
|
-
updateMatch(matchId, (prev) =>
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2765
|
+
updateMatch(matchId, (prev) => {
|
|
2766
|
+
clearTimeout(prev.pendingTimeout)
|
|
2767
|
+
return {
|
|
2768
|
+
...prev,
|
|
2769
|
+
isFetching: loaderIsRunningAsync
|
|
2770
|
+
? prev.isFetching
|
|
2771
|
+
: false,
|
|
2772
|
+
loaderPromise: loaderIsRunningAsync
|
|
2773
|
+
? prev.loaderPromise
|
|
2774
|
+
: undefined,
|
|
2775
|
+
invalid: false,
|
|
2776
|
+
pendingTimeout: undefined,
|
|
2777
|
+
_dehydrated: undefined,
|
|
2778
|
+
_forcePending: undefined,
|
|
2779
|
+
}
|
|
2780
|
+
})
|
|
2618
2781
|
return this.getMatch(matchId)!
|
|
2619
2782
|
})(),
|
|
2620
2783
|
)
|
package/src/ssr/ssr-client.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface SsrMatch {
|
|
|
42
42
|
extracted?: Array<ClientExtractedEntry>
|
|
43
43
|
updatedAt: MakeRouteMatch['updatedAt']
|
|
44
44
|
status: MakeRouteMatch['status']
|
|
45
|
+
ssr?: boolean | 'data-only'
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export type ClientExtractedEntry =
|
|
@@ -123,17 +124,36 @@ export async function hydrate(router: AnyRouter): Promise<any> {
|
|
|
123
124
|
|
|
124
125
|
// Right after hydration and before the first render, we need to rehydrate each match
|
|
125
126
|
// First step is to reyhdrate loaderData and __beforeLoadContext
|
|
127
|
+
let firstNonSsrMatchIndex: number | undefined = undefined
|
|
126
128
|
matches.forEach((match) => {
|
|
127
129
|
const dehydratedMatch = window.__TSR_SSR__!.matches.find(
|
|
128
130
|
(d) => d.id === match.id,
|
|
129
131
|
)
|
|
130
132
|
|
|
131
133
|
if (!dehydratedMatch) {
|
|
134
|
+
Object.assign(match, { dehydrated: false, ssr: false })
|
|
132
135
|
return
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
Object.assign(match, dehydratedMatch)
|
|
136
139
|
|
|
140
|
+
if (match.ssr === false) {
|
|
141
|
+
match._dehydrated = false
|
|
142
|
+
} else {
|
|
143
|
+
match._dehydrated = true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (match.ssr === 'data-only' || match.ssr === false) {
|
|
147
|
+
if (firstNonSsrMatchIndex === undefined) {
|
|
148
|
+
firstNonSsrMatchIndex = match.index
|
|
149
|
+
match._forcePending = true
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (match.ssr === false) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
137
157
|
// Handle beforeLoadContext
|
|
138
158
|
if (dehydratedMatch.__beforeLoadContext) {
|
|
139
159
|
match.__beforeLoadContext = router.ssr!.serializer.parse(
|
|
@@ -157,8 +177,6 @@ export async function hydrate(router: AnyRouter): Promise<any> {
|
|
|
157
177
|
;(match as unknown as SsrMatch).extracted?.forEach((ex) => {
|
|
158
178
|
deepMutableSetByPath(match, ['loaderData', ...ex.path], ex.value)
|
|
159
179
|
})
|
|
160
|
-
|
|
161
|
-
return match
|
|
162
180
|
})
|
|
163
181
|
|
|
164
182
|
router.__store.setState((s) => {
|
|
@@ -222,8 +240,36 @@ export async function hydrate(router: AnyRouter): Promise<any> {
|
|
|
222
240
|
}),
|
|
223
241
|
)
|
|
224
242
|
|
|
243
|
+
// schedule router.load() to run after the next tick so we can store the promise in the match before loading starts
|
|
244
|
+
const loadPromise = Promise.resolve()
|
|
245
|
+
.then(() => router.load())
|
|
246
|
+
.catch((err) => {
|
|
247
|
+
console.error('Error during router hydration:', err)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// in SPA mode we need to keep the outermost match pending until router.load() is finished
|
|
251
|
+
// this will prevent that other pending components are rendered but hydration is not blocked
|
|
225
252
|
if (matches[matches.length - 1]!.id !== lastMatchId) {
|
|
226
|
-
|
|
253
|
+
const matchId = matches[0]!.id
|
|
254
|
+
router.updateMatch(matchId, (prev) => {
|
|
255
|
+
return {
|
|
256
|
+
...prev,
|
|
257
|
+
_displayPending: true,
|
|
258
|
+
displayPendingPromise: loadPromise,
|
|
259
|
+
// make sure that the pending component is displayed for at least pendingMinMs
|
|
260
|
+
_forcePending: true,
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
// hide the pending component once the load is finished
|
|
264
|
+
loadPromise.then(() => {
|
|
265
|
+
router.updateMatch(matchId, (prev) => {
|
|
266
|
+
return {
|
|
267
|
+
...prev,
|
|
268
|
+
_displayPending: undefined,
|
|
269
|
+
displayPendingPromise: undefined,
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
})
|
|
227
273
|
}
|
|
228
274
|
|
|
229
275
|
return routeChunkPromise
|
package/src/ssr/ssr-server.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -461,3 +461,15 @@ export function shallow<T>(objA: T, objB: T) {
|
|
|
461
461
|
}
|
|
462
462
|
return true
|
|
463
463
|
}
|
|
464
|
+
|
|
465
|
+
export function isModuleNotFoundError(error: any): boolean {
|
|
466
|
+
// chrome: "Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split"
|
|
467
|
+
// firefox: "error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split"
|
|
468
|
+
// safari: "Importing a module script failed."
|
|
469
|
+
if (typeof error?.message !== 'string') return false
|
|
470
|
+
return (
|
|
471
|
+
error.message.startsWith('Failed to fetch dynamically imported module') ||
|
|
472
|
+
error.message.startsWith('error loading dynamically imported module') ||
|
|
473
|
+
error.message.startsWith('Importing a module script failed')
|
|
474
|
+
)
|
|
475
|
+
}
|