Skip to content

Commit 35ab38b

Browse files
committed
offline navigations: replay dynamic routes from known routes (22/25)
1 parent 61e7b42 commit 35ab38b

13 files changed

Lines changed: 569 additions & 17 deletions

File tree

packages/next/src/build/define-env.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,9 @@ export function getDefineEnv({
384384
'process.env.__NEXT_GESTURE_TRANSITION':
385385
config.experimental.gestureTransition ?? false,
386386
'process.env.__NEXT_OPTIMISTIC_ROUTING':
387-
config.experimental.optimisticRouting ?? false,
387+
config.experimental.optimisticRouting ||
388+
config.experimental.offlineNavigations ||
389+
false,
388390
'process.env.__NEXT_VARY_PARAMS': config.experimental.varyParams ?? false,
389391
'process.env.__NEXT_EXPOSE_TESTING_API':
390392
dev || config.experimental.exposeTestingApiInProductionBuild === true,

packages/next/src/build/templates/app-page.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,8 @@ export async function handler(
890890
staleTimes: nextConfig.experimental.staleTimes,
891891
dynamicOnHover: Boolean(nextConfig.experimental.dynamicOnHover),
892892
optimisticRouting: Boolean(
893-
nextConfig.experimental.optimisticRouting
893+
nextConfig.experimental.optimisticRouting ||
894+
nextConfig.experimental.offlineNavigations
894895
),
895896
inlineCss: Boolean(nextConfig.experimental.inlineCss),
896897
prefetchInlining: nextConfig.experimental.prefetchInlining ?? false,

packages/next/src/build/templates/edge-ssr-app.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ async function requestHandler(
165165
expireTime: nextConfig.expireTime,
166166
staleTimes: nextConfig.experimental.staleTimes,
167167
dynamicOnHover: Boolean(nextConfig.experimental.dynamicOnHover),
168-
optimisticRouting: Boolean(nextConfig.experimental.optimisticRouting),
168+
optimisticRouting: Boolean(
169+
nextConfig.experimental.optimisticRouting ||
170+
nextConfig.experimental.offlineNavigations
171+
),
169172
inlineCss: Boolean(nextConfig.experimental.inlineCss),
170173
prefetchInlining: nextConfig.experimental.prefetchInlining ?? false,
171174
authInterrupts: Boolean(nextConfig.experimental.authInterrupts),

packages/next/src/client/components/segment-cache/cache.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,11 @@ import { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment'
125125
import { FetchStrategy } from './types'
126126
import { createPromiseWithResolvers } from '../../../shared/lib/promise-with-resolvers'
127127
import { readFromBFCache, UnknownDynamicStaleTime } from './bfcache'
128-
import { discoverKnownRoute, matchKnownRoute } from './optimistic-routes'
128+
import {
129+
discoverKnownRoute,
130+
matchKnownRoute,
131+
restoreKnownRouteFromCacheEntry,
132+
} from './optimistic-routes'
129133
import { convertServerPatchToFullTree, type NavigationSeed } from './navigation'
130134
import { getNavigationBuildId } from '../../navigation-build-id'
131135
import { NEXT_NAV_DEPLOYMENT_ID_HEADER } from '../../../lib/constants'
@@ -1442,7 +1446,14 @@ export async function hydrateOfflineNavigationRouterCacheFromRecords(
14421446
}
14431447

14441448
for (const record of routeRecords) {
1445-
if (writeOfflineNavigationRouteRecordIntoCache(now, record)) {
1449+
const entry = writeOfflineNavigationRouteRecordIntoCache(now, record)
1450+
if (entry !== null) {
1451+
restoreKnownRouteFromCacheEntry(
1452+
now,
1453+
record.route.pathname,
1454+
record.route.nextUrl,
1455+
entry
1456+
)
14461457
result.routes.hydrated++
14471458
} else {
14481459
result.routes.skipped++
@@ -1463,7 +1474,7 @@ export async function hydrateOfflineNavigationRouterCacheFromRecords(
14631474
export function writeOfflineNavigationRouteRecordIntoCache(
14641475
now: number,
14651476
record: OfflineNavigationRouteRecord
1466-
): boolean {
1477+
): FulfilledRouteCacheEntry | null {
14671478
const route = record.route
14681479
if (
14691480
record.staleAt <= now ||
@@ -1473,16 +1484,19 @@ export function writeOfflineNavigationRouteRecordIntoCache(
14731484
record.metadata === null ||
14741485
typeof route.canonicalUrl !== 'string' ||
14751486
typeof route.renderedSearch !== 'string' ||
1487+
typeof route.pathname !== 'string' ||
1488+
typeof route.search !== 'string' ||
1489+
(route.nextUrl !== null && typeof route.nextUrl !== 'string') ||
14761490
typeof route.couldBeIntercepted !== 'boolean' ||
14771491
typeof route.supportsPerSegmentPrefetching !== 'boolean' ||
14781492
typeof route.hasDynamicRewrite !== 'boolean'
14791493
) {
1480-
return false
1494+
return null
14811495
}
14821496

14831497
const varyPath = deserializeOfflineNavigationVaryPath(record.routeVaryPath)
14841498
if (varyPath === null) {
1485-
return false
1499+
return null
14861500
}
14871501

14881502
const fulfilledEntry: FulfilledRouteCacheEntry = {
@@ -1507,7 +1521,7 @@ export function writeOfflineNavigationRouteRecordIntoCache(
15071521
fulfilledEntry,
15081522
isRevalidation
15091523
)
1510-
return true
1524+
return fulfilledEntry
15111525
}
15121526

15131527
export async function writeOfflineNavigationSegmentRecordIntoCache(

packages/next/src/client/components/segment-cache/optimistic-routes.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import {
5454
createMetadataRouteTree,
5555
} from './cache'
5656
import { isValueExpired } from './cache-map'
57+
import { hasBasePath } from '../../has-base-path'
58+
import { removeBasePath } from '../../remove-base-path'
5759
import { doesStaticSegmentAppearInURL } from '../../route-params'
5860
import type {
5961
NormalizedNextUrl,
@@ -186,6 +188,10 @@ function createEmptyPart(): KnownRoutePart {
186188
// The root of the known route tree.
187189
let knownRouteTreeRoot: KnownRoutePart = createEmptyPart()
188190

191+
function removeBasePathIfPresent(pathname: string): string {
192+
return hasBasePath(pathname) ? removeBasePath(pathname) : pathname
193+
}
194+
189195
/**
190196
* Learns a route pattern from a server response and inserts it into the cache.
191197
*
@@ -218,7 +224,8 @@ export function discoverKnownRoute(
218224
): FulfilledRouteCacheEntry {
219225
const tree = routeTree
220226

221-
const pathnameParts = pathname.split('/').filter((p) => p !== '')
227+
const routePathname = removeBasePathIfPresent(pathname)
228+
const pathnameParts = routePathname.split('/').filter((p) => p !== '')
222229
const firstPart = pathnameParts.length > 0 ? pathnameParts[0] : null
223230
const remainingParts = pathnameParts.length > 0 ? pathnameParts.slice(1) : []
224231
let fulfilledEntry: FulfilledRouteCacheEntry
@@ -325,6 +332,36 @@ function persistOfflineNavigationRouteRecord(
325332
})
326333
}
327334

335+
export function restoreKnownRouteFromCacheEntry(
336+
now: number,
337+
pathname: string,
338+
nextUrl: string | null,
339+
entry: FulfilledRouteCacheEntry
340+
): void {
341+
const routePathname = removeBasePathIfPresent(pathname)
342+
const pathnameParts = routePathname.split('/').filter((p) => p !== '')
343+
const firstPart = pathnameParts.length > 0 ? pathnameParts[0] : null
344+
const remainingParts = pathnameParts.length > 0 ? pathnameParts.slice(1) : []
345+
const metadataVaryPath = entry.metadata.varyPath as PageVaryPath
346+
347+
discoverKnownRoutePart(
348+
knownRouteTreeRoot,
349+
entry.tree,
350+
firstPart,
351+
remainingParts,
352+
entry,
353+
now,
354+
pathname,
355+
nextUrl,
356+
entry.tree,
357+
metadataVaryPath,
358+
entry.couldBeIntercepted,
359+
entry.canonicalUrl,
360+
entry.supportsPerSegmentPrefetching,
361+
entry.hasDynamicRewrite
362+
)
363+
}
364+
328365
/**
329366
* Gets or creates the dynamic child node for a KnownRoutePart.
330367
* A node can have at most one dynamic child (you can't have both [slug] and
@@ -574,7 +611,8 @@ export function matchKnownRoute(
574611
pathname: string,
575612
search: NormalizedSearch
576613
): FulfilledRouteCacheEntry | null {
577-
const pathnameParts = pathname.split('/').filter((p) => p !== '')
614+
const routePathname = removeBasePathIfPresent(pathname)
615+
const pathnameParts = routePathname.split('/').filter((p) => p !== '')
578616
const resolvedParams: ResolvedParams = new Map()
579617
const match = matchKnownRoutePart(
580618
now,

packages/next/src/client/route-params.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
NEXT_REWRITTEN_QUERY_HEADER,
1111
NEXT_RSC_UNION_QUERY,
1212
} from './components/app-router-headers'
13+
import { hasBasePath } from './has-base-path'
14+
import { removeBasePath } from './remove-base-path'
1315
import type {
1416
NormalizedPathname,
1517
NormalizedSearch,
@@ -44,9 +46,11 @@ export function getRenderedPathname(
4446
// page will be different from the pathname in the request URL. In this case,
4547
// the response will include a header that gives the rewritten pathname.
4648
const rewrittenPath = response.headers.get(NEXT_REWRITTEN_PATH_HEADER)
47-
return (rewrittenPath ??
48-
urlToUrlWithoutFlightMarker(new URL(response.url))
49-
.pathname) as NormalizedPathname
49+
const pathname =
50+
rewrittenPath ?? urlToUrlWithoutFlightMarker(new URL(response.url)).pathname
51+
return (
52+
hasBasePath(pathname) ? removeBasePath(pathname) : pathname
53+
) as NormalizedPathname
5054
}
5155

5256
// Pathname parts come from `URL.pathname.split('/')`, so they are already

packages/next/src/export/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,10 @@ async function exportAppImpl(
510510
clientParamParsingOrigins:
511511
nextConfig.experimental.clientParamParsingOrigins,
512512
dynamicOnHover: nextConfig.experimental.dynamicOnHover ?? false,
513-
optimisticRouting: nextConfig.experimental.optimisticRouting ?? false,
513+
optimisticRouting:
514+
nextConfig.experimental.optimisticRouting ||
515+
nextConfig.experimental.offlineNavigations ||
516+
false,
514517
inlineCss: nextConfig.experimental.inlineCss ?? false,
515518
prefetchInlining: nextConfig.experimental.prefetchInlining ?? false,
516519
authInterrupts: !!nextConfig.experimental.authInterrupts,

packages/next/src/server/base-server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,9 @@ export default abstract class Server<
578578
this.nextConfig.experimental.clientParamParsingOrigins,
579579
dynamicOnHover: this.nextConfig.experimental.dynamicOnHover ?? false,
580580
optimisticRouting:
581-
this.nextConfig.experimental.optimisticRouting ?? false,
581+
this.nextConfig.experimental.optimisticRouting ||
582+
this.nextConfig.experimental.offlineNavigations ||
583+
false,
582584
inlineCss: this.nextConfig.experimental.inlineCss ?? false,
583585
prefetchInlining:
584586
this.nextConfig.experimental.prefetchInlining ?? false,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use client'
2+
3+
import { useParams } from 'next/navigation'
4+
5+
export function DynamicPrefetchValue() {
6+
const params = useParams<{ value: string }>()
7+
8+
return <p id="dynamic-prefetch-page">dynamic prefetch path: {params.value}</p>
9+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Suspense } from 'react'
2+
import { OfflineStatus } from '../../offline-status'
3+
import { DynamicPrefetchValue } from './dynamic-prefetch-value'
4+
5+
export function generateStaticParams() {
6+
return [{ value: '__TEST__' }]
7+
}
8+
9+
export default function DynamicPrefetchPage() {
10+
return (
11+
<>
12+
<Suspense fallback={<p>loading dynamic prefetch value</p>}>
13+
<DynamicPrefetchValue />
14+
</Suspense>
15+
<OfflineStatus />
16+
</>
17+
)
18+
}

0 commit comments

Comments
 (0)