@tanstack/router-core 1.124.0 → 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 +178 -56
- 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 +178 -56
- 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 +225 -66
- 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
|
}
|
|
@@ -1415,11 +1419,15 @@ export class RouterCore<
|
|
|
1415
1419
|
// By default, start with the current location
|
|
1416
1420
|
let fromPath = lastMatch.fullPath
|
|
1417
1421
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1422
|
+
const routeIsChanging =
|
|
1423
|
+
!!dest.to &&
|
|
1424
|
+
dest.to !== fromPath &&
|
|
1425
|
+
this.resolvePathWithBase(fromPath, `${dest.to}`) !== fromPath
|
|
1426
|
+
|
|
1427
|
+
// If the route is changing we need to find the relative fromPath
|
|
1420
1428
|
if (dest.unsafeRelative === 'path') {
|
|
1421
1429
|
fromPath = currentLocation.pathname
|
|
1422
|
-
} else if (
|
|
1430
|
+
} else if (routeIsChanging && dest.from) {
|
|
1423
1431
|
fromPath = dest.from
|
|
1424
1432
|
const existingFrom = [...allFromMatches].reverse().find((d) => {
|
|
1425
1433
|
return (
|
|
@@ -1708,6 +1716,7 @@ export class RouterCore<
|
|
|
1708
1716
|
}: BuildNextOptions & CommitLocationOptions = {}) => {
|
|
1709
1717
|
if (href) {
|
|
1710
1718
|
const currentIndex = this.history.location.state.__TSR_index
|
|
1719
|
+
|
|
1711
1720
|
const parsed = parseHref(href, {
|
|
1712
1721
|
__TSR_index: replace ? currentIndex : currentIndex + 1,
|
|
1713
1722
|
})
|
|
@@ -1721,6 +1730,7 @@ export class RouterCore<
|
|
|
1721
1730
|
...(rest as any),
|
|
1722
1731
|
_includeValidateSearch: true,
|
|
1723
1732
|
})
|
|
1733
|
+
|
|
1724
1734
|
return this.commitLocation({
|
|
1725
1735
|
...location,
|
|
1726
1736
|
viewTransition,
|
|
@@ -1781,7 +1791,11 @@ export class RouterCore<
|
|
|
1781
1791
|
}
|
|
1782
1792
|
}
|
|
1783
1793
|
// Match the routes
|
|
1784
|
-
|
|
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
|
+
}
|
|
1785
1799
|
|
|
1786
1800
|
// Ingest the new matches
|
|
1787
1801
|
this.__store.setState((s) => ({
|
|
@@ -1800,7 +1814,6 @@ export class RouterCore<
|
|
|
1800
1814
|
load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
|
|
1801
1815
|
let redirect: AnyRedirect | undefined
|
|
1802
1816
|
let notFound: NotFoundError | undefined
|
|
1803
|
-
|
|
1804
1817
|
let loadPromise: Promise<void>
|
|
1805
1818
|
|
|
1806
1819
|
// eslint-disable-next-line prefer-const
|
|
@@ -2055,6 +2068,40 @@ export class RouterCore<
|
|
|
2055
2068
|
const triggerOnReady = async () => {
|
|
2056
2069
|
if (!rendered) {
|
|
2057
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
|
+
|
|
2058
2105
|
await onReady?.()
|
|
2059
2106
|
}
|
|
2060
2107
|
}
|
|
@@ -2063,6 +2110,12 @@ export class RouterCore<
|
|
|
2063
2110
|
return !!(allPreload && !this.state.matches.find((d) => d.id === matchId))
|
|
2064
2111
|
}
|
|
2065
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
|
+
|
|
2066
2119
|
const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
|
|
2067
2120
|
if (isRedirect(err) || isNotFound(err)) {
|
|
2068
2121
|
if (isRedirect(err)) {
|
|
@@ -2114,6 +2167,21 @@ export class RouterCore<
|
|
|
2114
2167
|
}
|
|
2115
2168
|
}
|
|
2116
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
|
+
|
|
2117
2185
|
try {
|
|
2118
2186
|
await new Promise<void>((resolveAll, rejectAll) => {
|
|
2119
2187
|
;(async () => {
|
|
@@ -2163,12 +2231,73 @@ export class RouterCore<
|
|
|
2163
2231
|
for (const [index, { id: matchId, routeId }] of matches.entries()) {
|
|
2164
2232
|
const existingMatch = this.getMatch(matchId)!
|
|
2165
2233
|
const parentMatchId = matches[index - 1]?.id
|
|
2234
|
+
const parentMatch = parentMatchId
|
|
2235
|
+
? this.getMatch(parentMatchId)!
|
|
2236
|
+
: undefined
|
|
2166
2237
|
|
|
2167
2238
|
const route = this.looseRoutesById[routeId]!
|
|
2168
2239
|
|
|
2169
2240
|
const pendingMs =
|
|
2170
2241
|
route.options.pendingMs ?? this.options.defaultPendingMs
|
|
2171
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
|
+
|
|
2172
2301
|
const shouldPending = !!(
|
|
2173
2302
|
onReady &&
|
|
2174
2303
|
!this.isServer &&
|
|
@@ -2251,10 +2380,8 @@ export class RouterCore<
|
|
|
2251
2380
|
handleSerialError(index, searchError, 'VALIDATE_SEARCH')
|
|
2252
2381
|
}
|
|
2253
2382
|
|
|
2254
|
-
const
|
|
2255
|
-
|
|
2256
|
-
? this.getMatch(parentMatchId)!.context
|
|
2257
|
-
: (this.options.context ?? {})
|
|
2383
|
+
const parentMatchContext =
|
|
2384
|
+
parentMatch?.context ?? this.options.context ?? {}
|
|
2258
2385
|
|
|
2259
2386
|
updateMatch(matchId, (prev) => ({
|
|
2260
2387
|
...prev,
|
|
@@ -2263,7 +2390,7 @@ export class RouterCore<
|
|
|
2263
2390
|
abortController,
|
|
2264
2391
|
pendingTimeout,
|
|
2265
2392
|
context: {
|
|
2266
|
-
...
|
|
2393
|
+
...parentMatchContext,
|
|
2267
2394
|
...prev.__routeContext,
|
|
2268
2395
|
},
|
|
2269
2396
|
}))
|
|
@@ -2309,7 +2436,7 @@ export class RouterCore<
|
|
|
2309
2436
|
...prev,
|
|
2310
2437
|
__beforeLoadContext: beforeLoadContext,
|
|
2311
2438
|
context: {
|
|
2312
|
-
...
|
|
2439
|
+
...parentMatchContext,
|
|
2313
2440
|
...prev.__routeContext,
|
|
2314
2441
|
...beforeLoadContext,
|
|
2315
2442
|
},
|
|
@@ -2340,10 +2467,65 @@ export class RouterCore<
|
|
|
2340
2467
|
(async () => {
|
|
2341
2468
|
let loaderShouldRunAsync = false
|
|
2342
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
|
+
}
|
|
2343
2509
|
|
|
2344
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
|
+
}
|
|
2345
2527
|
// there is a loaderPromise, so we are in the middle of a load
|
|
2346
|
-
if (prevMatch.loaderPromise) {
|
|
2528
|
+
else if (prevMatch.loaderPromise) {
|
|
2347
2529
|
// do not block if we already have stale data we can show
|
|
2348
2530
|
// but only if the ongoing load is not a preload since error handling is different for preloads
|
|
2349
2531
|
// and we don't want to swallow errors
|
|
@@ -2361,7 +2543,6 @@ export class RouterCore<
|
|
|
2361
2543
|
}
|
|
2362
2544
|
} else {
|
|
2363
2545
|
const parentMatchPromise = matchPromises[index - 1] as any
|
|
2364
|
-
const route = this.looseRoutesById[routeId]!
|
|
2365
2546
|
|
|
2366
2547
|
const getLoaderContext = (): LoaderFnContext => {
|
|
2367
2548
|
const {
|
|
@@ -2420,39 +2601,6 @@ export class RouterCore<
|
|
|
2420
2601
|
!this.state.matches.find((d) => d.id === matchId),
|
|
2421
2602
|
}))
|
|
2422
2603
|
|
|
2423
|
-
const executeHead = async () => {
|
|
2424
|
-
const match = this.getMatch(matchId)
|
|
2425
|
-
// in case of a redirecting match during preload, the match does not exist
|
|
2426
|
-
if (!match) {
|
|
2427
|
-
return
|
|
2428
|
-
}
|
|
2429
|
-
const assetContext = {
|
|
2430
|
-
matches,
|
|
2431
|
-
match,
|
|
2432
|
-
params: match.params,
|
|
2433
|
-
loaderData: match.loaderData,
|
|
2434
|
-
}
|
|
2435
|
-
const headFnContent =
|
|
2436
|
-
await route.options.head?.(assetContext)
|
|
2437
|
-
const meta = headFnContent?.meta
|
|
2438
|
-
const links = headFnContent?.links
|
|
2439
|
-
const headScripts = headFnContent?.scripts
|
|
2440
|
-
const styles = headFnContent?.styles
|
|
2441
|
-
|
|
2442
|
-
const scripts =
|
|
2443
|
-
await route.options.scripts?.(assetContext)
|
|
2444
|
-
const headers =
|
|
2445
|
-
await route.options.headers?.(assetContext)
|
|
2446
|
-
return {
|
|
2447
|
-
meta,
|
|
2448
|
-
links,
|
|
2449
|
-
headScripts,
|
|
2450
|
-
headers,
|
|
2451
|
-
scripts,
|
|
2452
|
-
styles,
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
2604
|
const runLoader = async () => {
|
|
2457
2605
|
try {
|
|
2458
2606
|
// If the Matches component rendered
|
|
@@ -2460,17 +2608,16 @@ export class RouterCore<
|
|
|
2460
2608
|
// a minimum duration, we''ll wait for it to resolve
|
|
2461
2609
|
// before committing to the match and resolving
|
|
2462
2610
|
// the loadPromise
|
|
2463
|
-
const potentialPendingMinPromise = async () => {
|
|
2464
|
-
const latestMatch = this.getMatch(matchId)!
|
|
2465
|
-
|
|
2466
|
-
if (latestMatch.minPendingPromise) {
|
|
2467
|
-
await latestMatch.minPendingPromise
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
2611
|
|
|
2471
2612
|
// Actually run the loader and handle the result
|
|
2472
2613
|
try {
|
|
2473
|
-
|
|
2614
|
+
if (
|
|
2615
|
+
!this.isServer ||
|
|
2616
|
+
(this.isServer &&
|
|
2617
|
+
this.getMatch(matchId)!.ssr === true)
|
|
2618
|
+
) {
|
|
2619
|
+
this.loadRouteChunk(route)
|
|
2620
|
+
}
|
|
2474
2621
|
|
|
2475
2622
|
updateMatch(matchId, (prev) => ({
|
|
2476
2623
|
...prev,
|
|
@@ -2485,29 +2632,27 @@ export class RouterCore<
|
|
|
2485
2632
|
this.getMatch(matchId)!,
|
|
2486
2633
|
loaderData,
|
|
2487
2634
|
)
|
|
2635
|
+
updateMatch(matchId, (prev) => ({
|
|
2636
|
+
...prev,
|
|
2637
|
+
loaderData,
|
|
2638
|
+
}))
|
|
2488
2639
|
|
|
2489
2640
|
// Lazy option can modify the route options,
|
|
2490
2641
|
// so we need to wait for it to resolve before
|
|
2491
2642
|
// we can use the options
|
|
2492
2643
|
await route._lazyPromise
|
|
2493
|
-
|
|
2644
|
+
const head = await executeHead()
|
|
2494
2645
|
await potentialPendingMinPromise()
|
|
2495
2646
|
|
|
2496
2647
|
// Last but not least, wait for the the components
|
|
2497
2648
|
// to be preloaded before we resolve the match
|
|
2498
2649
|
await route._componentsPromise
|
|
2499
|
-
|
|
2500
2650
|
updateMatch(matchId, (prev) => ({
|
|
2501
2651
|
...prev,
|
|
2502
2652
|
error: undefined,
|
|
2503
2653
|
status: 'success',
|
|
2504
2654
|
isFetching: false,
|
|
2505
2655
|
updatedAt: Date.now(),
|
|
2506
|
-
loaderData,
|
|
2507
|
-
}))
|
|
2508
|
-
const head = await executeHead()
|
|
2509
|
-
updateMatch(matchId, (prev) => ({
|
|
2510
|
-
...prev,
|
|
2511
2656
|
...head,
|
|
2512
2657
|
}))
|
|
2513
2658
|
} catch (e) {
|
|
@@ -2553,13 +2698,18 @@ export class RouterCore<
|
|
|
2553
2698
|
}
|
|
2554
2699
|
|
|
2555
2700
|
// If the route is successful and still fresh, just resolve
|
|
2556
|
-
const { status, invalid } =
|
|
2701
|
+
const { status, invalid, _forcePending } =
|
|
2702
|
+
this.getMatch(matchId)!
|
|
2557
2703
|
loaderShouldRunAsync =
|
|
2558
2704
|
status === 'success' &&
|
|
2559
2705
|
(invalid || (shouldReload ?? age > staleAge))
|
|
2560
2706
|
if (preload && route.options.preload === false) {
|
|
2561
2707
|
// Do nothing
|
|
2562
|
-
} else if (
|
|
2708
|
+
} else if (
|
|
2709
|
+
loaderShouldRunAsync &&
|
|
2710
|
+
!sync &&
|
|
2711
|
+
!_forcePending
|
|
2712
|
+
) {
|
|
2563
2713
|
loaderIsRunningAsync = true
|
|
2564
2714
|
;(async () => {
|
|
2565
2715
|
try {
|
|
@@ -2584,6 +2734,9 @@ export class RouterCore<
|
|
|
2584
2734
|
) {
|
|
2585
2735
|
await runLoader()
|
|
2586
2736
|
} else {
|
|
2737
|
+
if (_forcePending) {
|
|
2738
|
+
await potentialPendingMinPromise()
|
|
2739
|
+
}
|
|
2587
2740
|
// if the loader did not run, still update head.
|
|
2588
2741
|
// reason: parent's beforeLoad may have changed the route context
|
|
2589
2742
|
// and only now do we know the route context (and that the loader would not run)
|
|
@@ -2592,6 +2745,10 @@ export class RouterCore<
|
|
|
2592
2745
|
...prev,
|
|
2593
2746
|
...head,
|
|
2594
2747
|
}))
|
|
2748
|
+
this.serverSsr?.onMatchSettled({
|
|
2749
|
+
router: this,
|
|
2750
|
+
match: this.getMatch(matchId)!,
|
|
2751
|
+
})
|
|
2595
2752
|
}
|
|
2596
2753
|
}
|
|
2597
2754
|
if (!loaderIsRunningAsync) {
|
|
@@ -2608,6 +2765,8 @@ export class RouterCore<
|
|
|
2608
2765
|
? prev.loaderPromise
|
|
2609
2766
|
: undefined,
|
|
2610
2767
|
invalid: false,
|
|
2768
|
+
_dehydrated: undefined,
|
|
2769
|
+
_forcePending: undefined,
|
|
2611
2770
|
}))
|
|
2612
2771
|
return this.getMatch(matchId)!
|
|
2613
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
|
+
}
|