@tanstack/router-core 1.124.2 → 1.125.0
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 +176 -55
- 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 +176 -55
- 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 +216 -63
- 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
|
}
|
|
@@ -1787,7 +1791,11 @@ export class RouterCore<
|
|
|
1787
1791
|
}
|
|
1788
1792
|
}
|
|
1789
1793
|
// Match the routes
|
|
1790
|
-
|
|
1794
|
+
let pendingMatches = this.matchRoutes(this.latestLocation)
|
|
1795
|
+
// in SPA mode we only want to load the root route
|
|
1796
|
+
if (this.isShell) {
|
|
1797
|
+
pendingMatches = pendingMatches.slice(0, 1)
|
|
1798
|
+
}
|
|
1791
1799
|
|
|
1792
1800
|
// Ingest the new matches
|
|
1793
1801
|
this.__store.setState((s) => ({
|
|
@@ -1806,7 +1814,6 @@ export class RouterCore<
|
|
|
1806
1814
|
load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
|
|
1807
1815
|
let redirect: AnyRedirect | undefined
|
|
1808
1816
|
let notFound: NotFoundError | undefined
|
|
1809
|
-
|
|
1810
1817
|
let loadPromise: Promise<void>
|
|
1811
1818
|
|
|
1812
1819
|
// eslint-disable-next-line prefer-const
|
|
@@ -2061,6 +2068,40 @@ export class RouterCore<
|
|
|
2061
2068
|
const triggerOnReady = async () => {
|
|
2062
2069
|
if (!rendered) {
|
|
2063
2070
|
rendered = true
|
|
2071
|
+
|
|
2072
|
+
// create a minPendingPromise for matches that have forcePending set to true
|
|
2073
|
+
// usually the minPendingPromise is created in the Match component if a pending match is rendered
|
|
2074
|
+
// however, this might be too late if the match synchronously resolves
|
|
2075
|
+
if (!allPreload && !this.isServer) {
|
|
2076
|
+
matches.forEach((match) => {
|
|
2077
|
+
const {
|
|
2078
|
+
id: matchId,
|
|
2079
|
+
routeId,
|
|
2080
|
+
_forcePending,
|
|
2081
|
+
minPendingPromise,
|
|
2082
|
+
} = match
|
|
2083
|
+
const route = this.looseRoutesById[routeId]!
|
|
2084
|
+
const pendingMinMs =
|
|
2085
|
+
route.options.pendingMinMs ?? this.options.defaultPendingMinMs
|
|
2086
|
+
if (_forcePending && pendingMinMs && !minPendingPromise) {
|
|
2087
|
+
const minPendingPromise = createControlledPromise<void>()
|
|
2088
|
+
updateMatch(matchId, (prev) => ({
|
|
2089
|
+
...prev,
|
|
2090
|
+
minPendingPromise,
|
|
2091
|
+
}))
|
|
2092
|
+
|
|
2093
|
+
setTimeout(() => {
|
|
2094
|
+
minPendingPromise.resolve()
|
|
2095
|
+
// We've handled the minPendingPromise, so we can delete it
|
|
2096
|
+
updateMatch(matchId, (prev) => ({
|
|
2097
|
+
...prev,
|
|
2098
|
+
minPendingPromise: undefined,
|
|
2099
|
+
}))
|
|
2100
|
+
}, pendingMinMs)
|
|
2101
|
+
}
|
|
2102
|
+
})
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2064
2105
|
await onReady?.()
|
|
2065
2106
|
}
|
|
2066
2107
|
}
|
|
@@ -2069,6 +2110,12 @@ export class RouterCore<
|
|
|
2069
2110
|
return !!(allPreload && !this.state.matches.find((d) => d.id === matchId))
|
|
2070
2111
|
}
|
|
2071
2112
|
|
|
2113
|
+
// make sure the pending component is immediately rendered when hydrating a match that is not SSRed
|
|
2114
|
+
// the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
|
|
2115
|
+
if (!this.isServer && this.state.matches.find((d) => d._forcePending)) {
|
|
2116
|
+
triggerOnReady()
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2072
2119
|
const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
|
|
2073
2120
|
if (isRedirect(err) || isNotFound(err)) {
|
|
2074
2121
|
if (isRedirect(err)) {
|
|
@@ -2120,6 +2167,21 @@ export class RouterCore<
|
|
|
2120
2167
|
}
|
|
2121
2168
|
}
|
|
2122
2169
|
|
|
2170
|
+
const shouldSkipLoader = (matchId: string) => {
|
|
2171
|
+
const match = this.getMatch(matchId)!
|
|
2172
|
+
// upon hydration, we skip the loader if the match has been dehydrated on the server
|
|
2173
|
+
if (!this.isServer && match._dehydrated) {
|
|
2174
|
+
return true
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
if (this.isServer) {
|
|
2178
|
+
if (match.ssr === false) {
|
|
2179
|
+
return true
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return false
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2123
2185
|
try {
|
|
2124
2186
|
await new Promise<void>((resolveAll, rejectAll) => {
|
|
2125
2187
|
;(async () => {
|
|
@@ -2169,12 +2231,73 @@ export class RouterCore<
|
|
|
2169
2231
|
for (const [index, { id: matchId, routeId }] of matches.entries()) {
|
|
2170
2232
|
const existingMatch = this.getMatch(matchId)!
|
|
2171
2233
|
const parentMatchId = matches[index - 1]?.id
|
|
2234
|
+
const parentMatch = parentMatchId
|
|
2235
|
+
? this.getMatch(parentMatchId)!
|
|
2236
|
+
: undefined
|
|
2172
2237
|
|
|
2173
2238
|
const route = this.looseRoutesById[routeId]!
|
|
2174
2239
|
|
|
2175
2240
|
const pendingMs =
|
|
2176
2241
|
route.options.pendingMs ?? this.options.defaultPendingMs
|
|
2177
2242
|
|
|
2243
|
+
// on the server, determine whether SSR the current match or not
|
|
2244
|
+
if (this.isServer) {
|
|
2245
|
+
const defaultSsr = this.options.defaultSsr ?? true
|
|
2246
|
+
let ssr: boolean | 'data-only'
|
|
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
|
+
}
|
|
2284
|
+
|
|
2285
|
+
if (tempSsr === true && parentMatch?.ssr === 'data-only') {
|
|
2286
|
+
ssr = 'data-only'
|
|
2287
|
+
} else {
|
|
2288
|
+
ssr = tempSsr
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
updateMatch(matchId, (prev) => ({
|
|
2292
|
+
...prev,
|
|
2293
|
+
ssr,
|
|
2294
|
+
}))
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
if (shouldSkipLoader(matchId)) {
|
|
2298
|
+
continue
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2178
2301
|
const shouldPending = !!(
|
|
2179
2302
|
onReady &&
|
|
2180
2303
|
!this.isServer &&
|
|
@@ -2257,10 +2380,8 @@ export class RouterCore<
|
|
|
2257
2380
|
handleSerialError(index, searchError, 'VALIDATE_SEARCH')
|
|
2258
2381
|
}
|
|
2259
2382
|
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
? this.getMatch(parentMatchId)!.context
|
|
2263
|
-
: (this.options.context ?? {})
|
|
2383
|
+
const parentMatchContext =
|
|
2384
|
+
parentMatch?.context ?? this.options.context ?? {}
|
|
2264
2385
|
|
|
2265
2386
|
updateMatch(matchId, (prev) => ({
|
|
2266
2387
|
...prev,
|
|
@@ -2269,7 +2390,7 @@ export class RouterCore<
|
|
|
2269
2390
|
abortController,
|
|
2270
2391
|
pendingTimeout,
|
|
2271
2392
|
context: {
|
|
2272
|
-
...
|
|
2393
|
+
...parentMatchContext,
|
|
2273
2394
|
...prev.__routeContext,
|
|
2274
2395
|
},
|
|
2275
2396
|
}))
|
|
@@ -2315,7 +2436,7 @@ export class RouterCore<
|
|
|
2315
2436
|
...prev,
|
|
2316
2437
|
__beforeLoadContext: beforeLoadContext,
|
|
2317
2438
|
context: {
|
|
2318
|
-
...
|
|
2439
|
+
...parentMatchContext,
|
|
2319
2440
|
...prev.__routeContext,
|
|
2320
2441
|
...beforeLoadContext,
|
|
2321
2442
|
},
|
|
@@ -2346,10 +2467,65 @@ export class RouterCore<
|
|
|
2346
2467
|
(async () => {
|
|
2347
2468
|
let loaderShouldRunAsync = false
|
|
2348
2469
|
let loaderIsRunningAsync = false
|
|
2470
|
+
const route = this.looseRoutesById[routeId]!
|
|
2471
|
+
|
|
2472
|
+
const executeHead = async () => {
|
|
2473
|
+
const match = this.getMatch(matchId)
|
|
2474
|
+
// in case of a redirecting match during preload, the match does not exist
|
|
2475
|
+
if (!match) {
|
|
2476
|
+
return
|
|
2477
|
+
}
|
|
2478
|
+
const assetContext = {
|
|
2479
|
+
matches,
|
|
2480
|
+
match,
|
|
2481
|
+
params: match.params,
|
|
2482
|
+
loaderData: match.loaderData,
|
|
2483
|
+
}
|
|
2484
|
+
const headFnContent =
|
|
2485
|
+
await route.options.head?.(assetContext)
|
|
2486
|
+
const meta = headFnContent?.meta
|
|
2487
|
+
const links = headFnContent?.links
|
|
2488
|
+
const headScripts = headFnContent?.scripts
|
|
2489
|
+
const styles = headFnContent?.styles
|
|
2490
|
+
|
|
2491
|
+
const scripts = await route.options.scripts?.(assetContext)
|
|
2492
|
+
const headers = await route.options.headers?.(assetContext)
|
|
2493
|
+
return {
|
|
2494
|
+
meta,
|
|
2495
|
+
links,
|
|
2496
|
+
headScripts,
|
|
2497
|
+
headers,
|
|
2498
|
+
scripts,
|
|
2499
|
+
styles,
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
const potentialPendingMinPromise = async () => {
|
|
2504
|
+
const latestMatch = this.getMatch(matchId)!
|
|
2505
|
+
if (latestMatch.minPendingPromise) {
|
|
2506
|
+
await latestMatch.minPendingPromise
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2349
2509
|
|
|
2350
2510
|
const prevMatch = this.getMatch(matchId)!
|
|
2511
|
+
if (shouldSkipLoader(matchId)) {
|
|
2512
|
+
if (this.isServer) {
|
|
2513
|
+
const head = await executeHead()
|
|
2514
|
+
updateMatch(matchId, (prev) => ({
|
|
2515
|
+
...prev,
|
|
2516
|
+
...head,
|
|
2517
|
+
}))
|
|
2518
|
+
this.serverSsr?.onMatchSettled({
|
|
2519
|
+
router: this,
|
|
2520
|
+
match: this.getMatch(matchId)!,
|
|
2521
|
+
})
|
|
2522
|
+
return this.getMatch(matchId)!
|
|
2523
|
+
} else {
|
|
2524
|
+
await potentialPendingMinPromise()
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2351
2527
|
// there is a loaderPromise, so we are in the middle of a load
|
|
2352
|
-
if (prevMatch.loaderPromise) {
|
|
2528
|
+
else if (prevMatch.loaderPromise) {
|
|
2353
2529
|
// do not block if we already have stale data we can show
|
|
2354
2530
|
// but only if the ongoing load is not a preload since error handling is different for preloads
|
|
2355
2531
|
// and we don't want to swallow errors
|
|
@@ -2367,7 +2543,6 @@ export class RouterCore<
|
|
|
2367
2543
|
}
|
|
2368
2544
|
} else {
|
|
2369
2545
|
const parentMatchPromise = matchPromises[index - 1] as any
|
|
2370
|
-
const route = this.looseRoutesById[routeId]!
|
|
2371
2546
|
|
|
2372
2547
|
const getLoaderContext = (): LoaderFnContext => {
|
|
2373
2548
|
const {
|
|
@@ -2426,39 +2601,6 @@ export class RouterCore<
|
|
|
2426
2601
|
!this.state.matches.find((d) => d.id === matchId),
|
|
2427
2602
|
}))
|
|
2428
2603
|
|
|
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
2604
|
const runLoader = async () => {
|
|
2463
2605
|
try {
|
|
2464
2606
|
// If the Matches component rendered
|
|
@@ -2466,17 +2608,16 @@ export class RouterCore<
|
|
|
2466
2608
|
// a minimum duration, we''ll wait for it to resolve
|
|
2467
2609
|
// before committing to the match and resolving
|
|
2468
2610
|
// the loadPromise
|
|
2469
|
-
const potentialPendingMinPromise = async () => {
|
|
2470
|
-
const latestMatch = this.getMatch(matchId)!
|
|
2471
|
-
|
|
2472
|
-
if (latestMatch.minPendingPromise) {
|
|
2473
|
-
await latestMatch.minPendingPromise
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
2611
|
|
|
2477
2612
|
// Actually run the loader and handle the result
|
|
2478
2613
|
try {
|
|
2479
|
-
|
|
2614
|
+
if (
|
|
2615
|
+
!this.isServer ||
|
|
2616
|
+
(this.isServer &&
|
|
2617
|
+
this.getMatch(matchId)!.ssr === true)
|
|
2618
|
+
) {
|
|
2619
|
+
this.loadRouteChunk(route)
|
|
2620
|
+
}
|
|
2480
2621
|
|
|
2481
2622
|
updateMatch(matchId, (prev) => ({
|
|
2482
2623
|
...prev,
|
|
@@ -2491,29 +2632,27 @@ export class RouterCore<
|
|
|
2491
2632
|
this.getMatch(matchId)!,
|
|
2492
2633
|
loaderData,
|
|
2493
2634
|
)
|
|
2635
|
+
updateMatch(matchId, (prev) => ({
|
|
2636
|
+
...prev,
|
|
2637
|
+
loaderData,
|
|
2638
|
+
}))
|
|
2494
2639
|
|
|
2495
2640
|
// Lazy option can modify the route options,
|
|
2496
2641
|
// so we need to wait for it to resolve before
|
|
2497
2642
|
// we can use the options
|
|
2498
2643
|
await route._lazyPromise
|
|
2499
|
-
|
|
2644
|
+
const head = await executeHead()
|
|
2500
2645
|
await potentialPendingMinPromise()
|
|
2501
2646
|
|
|
2502
2647
|
// Last but not least, wait for the the components
|
|
2503
2648
|
// to be preloaded before we resolve the match
|
|
2504
2649
|
await route._componentsPromise
|
|
2505
|
-
|
|
2506
2650
|
updateMatch(matchId, (prev) => ({
|
|
2507
2651
|
...prev,
|
|
2508
2652
|
error: undefined,
|
|
2509
2653
|
status: 'success',
|
|
2510
2654
|
isFetching: false,
|
|
2511
2655
|
updatedAt: Date.now(),
|
|
2512
|
-
loaderData,
|
|
2513
|
-
}))
|
|
2514
|
-
const head = await executeHead()
|
|
2515
|
-
updateMatch(matchId, (prev) => ({
|
|
2516
|
-
...prev,
|
|
2517
2656
|
...head,
|
|
2518
2657
|
}))
|
|
2519
2658
|
} catch (e) {
|
|
@@ -2559,13 +2698,18 @@ export class RouterCore<
|
|
|
2559
2698
|
}
|
|
2560
2699
|
|
|
2561
2700
|
// If the route is successful and still fresh, just resolve
|
|
2562
|
-
const { status, invalid } =
|
|
2701
|
+
const { status, invalid, _forcePending } =
|
|
2702
|
+
this.getMatch(matchId)!
|
|
2563
2703
|
loaderShouldRunAsync =
|
|
2564
2704
|
status === 'success' &&
|
|
2565
2705
|
(invalid || (shouldReload ?? age > staleAge))
|
|
2566
2706
|
if (preload && route.options.preload === false) {
|
|
2567
2707
|
// Do nothing
|
|
2568
|
-
} else if (
|
|
2708
|
+
} else if (
|
|
2709
|
+
loaderShouldRunAsync &&
|
|
2710
|
+
!sync &&
|
|
2711
|
+
!_forcePending
|
|
2712
|
+
) {
|
|
2569
2713
|
loaderIsRunningAsync = true
|
|
2570
2714
|
;(async () => {
|
|
2571
2715
|
try {
|
|
@@ -2590,6 +2734,9 @@ export class RouterCore<
|
|
|
2590
2734
|
) {
|
|
2591
2735
|
await runLoader()
|
|
2592
2736
|
} else {
|
|
2737
|
+
if (_forcePending) {
|
|
2738
|
+
await potentialPendingMinPromise()
|
|
2739
|
+
}
|
|
2593
2740
|
// if the loader did not run, still update head.
|
|
2594
2741
|
// reason: parent's beforeLoad may have changed the route context
|
|
2595
2742
|
// and only now do we know the route context (and that the loader would not run)
|
|
@@ -2598,6 +2745,10 @@ export class RouterCore<
|
|
|
2598
2745
|
...prev,
|
|
2599
2746
|
...head,
|
|
2600
2747
|
}))
|
|
2748
|
+
this.serverSsr?.onMatchSettled({
|
|
2749
|
+
router: this,
|
|
2750
|
+
match: this.getMatch(matchId)!,
|
|
2751
|
+
})
|
|
2601
2752
|
}
|
|
2602
2753
|
}
|
|
2603
2754
|
if (!loaderIsRunningAsync) {
|
|
@@ -2614,6 +2765,8 @@ export class RouterCore<
|
|
|
2614
2765
|
? prev.loaderPromise
|
|
2615
2766
|
: undefined,
|
|
2616
2767
|
invalid: false,
|
|
2768
|
+
_dehydrated: undefined,
|
|
2769
|
+
_forcePending: undefined,
|
|
2617
2770
|
}))
|
|
2618
2771
|
return this.getMatch(matchId)!
|
|
2619
2772
|
})(),
|
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
|
+
}
|