Skip to content

Commit ddea272

Browse files
committed
offline navigations: support dynamic route patterns (12/13)
1 parent 9682004 commit ddea272

13 files changed

Lines changed: 572 additions & 18 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
@@ -115,7 +115,11 @@ import { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment'
115115
import { FetchStrategy } from './types'
116116
import { createPromiseWithResolvers } from '../../../shared/lib/promise-with-resolvers'
117117
import { readFromBFCache, UnknownDynamicStaleTime } from './bfcache'
118-
import { discoverKnownRoute, matchKnownRoute } from './optimistic-routes'
118+
import {
119+
discoverKnownRoute,
120+
matchKnownRoute,
121+
restoreKnownRouteFromCacheEntry,
122+
} from './optimistic-routes'
119123
import { convertServerPatchToFullTree, type NavigationSeed } from './navigation'
120124
import { getNavigationBuildId } from '../../navigation-build-id'
121125
import { NEXT_NAV_DEPLOYMENT_ID_HEADER } from '../../../lib/constants'
@@ -1479,7 +1483,14 @@ export async function hydrateOfflineNavigationRouterCacheFromRecords(
14791483
}
14801484

14811485
for (const record of routeRecords) {
1482-
if (writeOfflineNavigationRouteRecordIntoCache(now, record)) {
1486+
const entry = writeOfflineNavigationRouteRecordIntoCache(now, record)
1487+
if (entry !== null) {
1488+
restoreKnownRouteFromCacheEntry(
1489+
now,
1490+
record.route.pathname,
1491+
record.route.nextUrl,
1492+
entry
1493+
)
14831494
result.routes.hydrated++
14841495
} else {
14851496
result.routes.skipped++
@@ -1500,7 +1511,7 @@ export async function hydrateOfflineNavigationRouterCacheFromRecords(
15001511
export function writeOfflineNavigationRouteRecordIntoCache(
15011512
now: number,
15021513
record: OfflineNavigationRouteRecord
1503-
): boolean {
1514+
): FulfilledRouteCacheEntry | null {
15041515
const route = record.route
15051516
if (
15061517
record.staleAt <= now ||
@@ -1510,16 +1521,19 @@ export function writeOfflineNavigationRouteRecordIntoCache(
15101521
record.metadata === null ||
15111522
typeof route.canonicalUrl !== 'string' ||
15121523
typeof route.renderedSearch !== 'string' ||
1524+
typeof route.pathname !== 'string' ||
1525+
typeof route.search !== 'string' ||
1526+
(route.nextUrl !== null && typeof route.nextUrl !== 'string') ||
15131527
typeof route.couldBeIntercepted !== 'boolean' ||
15141528
typeof route.supportsPerSegmentPrefetching !== 'boolean' ||
15151529
typeof route.hasDynamicRewrite !== 'boolean'
15161530
) {
1517-
return false
1531+
return null
15181532
}
15191533

15201534
const varyPath = deserializeOfflineNavigationVaryPath(record.routeVaryPath)
15211535
if (varyPath === null) {
1522-
return false
1536+
return null
15231537
}
15241538

15251539
const fulfilledEntry: FulfilledRouteCacheEntry = {
@@ -1544,7 +1558,7 @@ export function writeOfflineNavigationRouteRecordIntoCache(
15441558
fulfilledEntry,
15451559
isRevalidation
15461560
)
1547-
return true
1561+
return fulfilledEntry
15481562
}
15491563

15501564
export async function writeOfflineNavigationSegmentRecordIntoCache(

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

Lines changed: 43 additions & 3 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,
@@ -201,6 +203,10 @@ function createEmptyPart(): KnownRoutePart {
201203
// The root of the known route tree.
202204
let knownRouteTreeRoot: KnownRoutePart = createEmptyPart()
203205

206+
function removeBasePathIfPresent(pathname: string): string {
207+
return hasBasePath(pathname) ? removeBasePath(pathname) : pathname
208+
}
209+
204210
/**
205211
* Learns a route pattern from a server response and inserts it into the cache.
206212
*
@@ -233,7 +239,8 @@ export function discoverKnownRoute(
233239
): FulfilledRouteCacheEntry {
234240
const tree = routeTree
235241

236-
const pathnameParts = pathname.split('/').filter((p) => p !== '')
242+
const routePathname = removeBasePathIfPresent(pathname)
243+
const pathnameParts = routePathname.split('/').filter((p) => p !== '')
237244
const firstPart = pathnameParts.length > 0 ? pathnameParts[0] : null
238245
const remainingParts = pathnameParts.length > 0 ? pathnameParts.slice(1) : []
239246
let fulfilledEntry: FulfilledRouteCacheEntry
@@ -292,7 +299,9 @@ export function discoverKnownRoute(
292299
)
293300
}
294301

295-
persistOfflineNavigationRouteRecord(now, pathname, nextUrl, fulfilledEntry)
302+
if (process.env.__NEXT_OFFLINE_NAVIGATIONS) {
303+
persistOfflineNavigationRouteRecord(now, pathname, nextUrl, fulfilledEntry)
304+
}
296305
return fulfilledEntry
297306
}
298307

@@ -348,6 +357,36 @@ function persistOfflineNavigationRouteRecord(
348357
})
349358
}
350359

360+
export function restoreKnownRouteFromCacheEntry(
361+
now: number,
362+
pathname: string,
363+
nextUrl: string | null,
364+
entry: FulfilledRouteCacheEntry
365+
): void {
366+
const routePathname = removeBasePathIfPresent(pathname)
367+
const pathnameParts = routePathname.split('/').filter((p) => p !== '')
368+
const firstPart = pathnameParts.length > 0 ? pathnameParts[0] : null
369+
const remainingParts = pathnameParts.length > 0 ? pathnameParts.slice(1) : []
370+
const metadataVaryPath = entry.metadata.varyPath as PageVaryPath
371+
372+
discoverKnownRoutePart(
373+
knownRouteTreeRoot,
374+
entry.tree,
375+
firstPart,
376+
remainingParts,
377+
entry,
378+
now,
379+
pathname,
380+
nextUrl,
381+
entry.tree,
382+
metadataVaryPath,
383+
entry.couldBeIntercepted,
384+
entry.canonicalUrl,
385+
entry.supportsPerSegmentPrefetching,
386+
entry.hasDynamicRewrite
387+
)
388+
}
389+
351390
/**
352391
* Gets or creates the dynamic child node for a KnownRoutePart.
353392
* A node can have at most one dynamic child (you can't have both [slug] and
@@ -597,7 +636,8 @@ export function matchKnownRoute(
597636
pathname: string,
598637
search: NormalizedSearch
599638
): FulfilledRouteCacheEntry | null {
600-
const pathnameParts = pathname.split('/').filter((p) => p !== '')
639+
const routePathname = removeBasePathIfPresent(pathname)
640+
const pathnameParts = routePathname.split('/').filter((p) => p !== '')
601641
const resolvedParams: ResolvedParams = new Map()
602642
const match = matchKnownRoutePart(
603643
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)