fix: only generate prefetch rsc for ppr enabled routes (#66395)
This adds details for every ISR cache request if the page being requested supports PPR. If it does, it'll attempt to load the `.prefetch.rsc` payload instead of the `.rsc` payload. This corrects a bug that was present in deployed environments. This additionally refactors the `isAppPPREnabled` out of most of the application, as it's only used to determine if we should add to the `prefetchDataRoute` fields in the `prerender-manifest.json`. To support loading the prefetch file or not, we pass the `isRoutePPREnabled` through with the cache get/set operations instead. x-slack-ref: https://vercel.slack.com/archives/C075MSFK9ML/p1717094328986429
This commit is contained in:
parent
c5b39e31fd
commit
be7d0c970e
24 changed files with 196 additions and 47 deletions
|
@ -1820,7 +1820,6 @@ export default async function build(
|
||||||
minimalMode: ciEnvironment.hasNextSupport,
|
minimalMode: ciEnvironment.hasNextSupport,
|
||||||
allowedRevalidateHeaderKeys:
|
allowedRevalidateHeaderKeys:
|
||||||
config.experimental.allowedRevalidateHeaderKeys,
|
config.experimental.allowedRevalidateHeaderKeys,
|
||||||
isAppPPREnabled,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
incrementalCacheIpcPort = cacheInitialization.ipcPort
|
incrementalCacheIpcPort = cacheInitialization.ipcPort
|
||||||
|
@ -2794,8 +2793,10 @@ export default async function build(
|
||||||
}
|
}
|
||||||
|
|
||||||
let prefetchDataRoute: string | null | undefined
|
let prefetchDataRoute: string | null | undefined
|
||||||
// We write the `.prefetch.rsc` when the app has PPR enabled, so
|
// While we may only write the `.rsc` when the route does not
|
||||||
// always add the prefetch data route to the manifest.
|
// have PPR enabled, we still want to generate the route when
|
||||||
|
// deployed so it doesn't 404. If the app has PPR enabled, we
|
||||||
|
// should add this key.
|
||||||
if (!isRouteHandler && isAppPPREnabled) {
|
if (!isRouteHandler && isAppPPREnabled) {
|
||||||
prefetchDataRoute = path.posix.join(
|
prefetchDataRoute = path.posix.join(
|
||||||
`${normalizedRoute}${RSC_PREFETCH_SUFFIX}`
|
`${normalizedRoute}${RSC_PREFETCH_SUFFIX}`
|
||||||
|
@ -2868,8 +2869,10 @@ export default async function build(
|
||||||
|
|
||||||
let prefetchDataRoute: string | undefined
|
let prefetchDataRoute: string | undefined
|
||||||
|
|
||||||
// We write the `.prefetch.rsc` when the app has PPR enabled, so
|
// While we may only write the `.rsc` when the route does not
|
||||||
// always add the prefetch data route to the manifest.
|
// have PPR enabled, we still want to generate the route when
|
||||||
|
// deployed so it doesn't 404. If the app has PPR enabled, we
|
||||||
|
// should add this key.
|
||||||
if (!isRouteHandler && isAppPPREnabled) {
|
if (!isRouteHandler && isAppPPREnabled) {
|
||||||
prefetchDataRoute = path.posix.join(
|
prefetchDataRoute = path.posix.join(
|
||||||
`${normalizedRoute}${RSC_PREFETCH_SUFFIX}`
|
`${normalizedRoute}${RSC_PREFETCH_SUFFIX}`
|
||||||
|
|
|
@ -1379,7 +1379,6 @@ export async function buildAppStaticPaths({
|
||||||
CurCacheHandler: CacheHandler,
|
CurCacheHandler: CacheHandler,
|
||||||
requestHeaders,
|
requestHeaders,
|
||||||
minimalMode: ciEnvironment.hasNextSupport,
|
minimalMode: ciEnvironment.hasNextSupport,
|
||||||
isAppPPREnabled: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return StaticGenerationAsyncStorageWrapper.wrap(
|
return StaticGenerationAsyncStorageWrapper.wrap(
|
||||||
|
|
|
@ -14,7 +14,6 @@ export async function createIncrementalCache({
|
||||||
distDir,
|
distDir,
|
||||||
dir,
|
dir,
|
||||||
enabledDirectories,
|
enabledDirectories,
|
||||||
isAppPPREnabled,
|
|
||||||
flushToDisk,
|
flushToDisk,
|
||||||
}: {
|
}: {
|
||||||
cacheHandler?: string
|
cacheHandler?: string
|
||||||
|
@ -23,7 +22,6 @@ export async function createIncrementalCache({
|
||||||
distDir: string
|
distDir: string
|
||||||
dir: string
|
dir: string
|
||||||
enabledDirectories: NextEnabledDirectories
|
enabledDirectories: NextEnabledDirectories
|
||||||
isAppPPREnabled: boolean
|
|
||||||
flushToDisk?: boolean
|
flushToDisk?: boolean
|
||||||
}) {
|
}) {
|
||||||
// Custom cache handler overrides.
|
// Custom cache handler overrides.
|
||||||
|
@ -60,7 +58,6 @@ export async function createIncrementalCache({
|
||||||
serverDistDir: path.join(distDir, 'server'),
|
serverDistDir: path.join(distDir, 'server'),
|
||||||
CurCacheHandler: CacheHandler,
|
CurCacheHandler: CacheHandler,
|
||||||
minimalMode: hasNextSupport,
|
minimalMode: hasNextSupport,
|
||||||
isAppPPREnabled,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
;(globalThis as any).__incrementalCache = incrementalCache
|
;(globalThis as any).__incrementalCache = incrementalCache
|
||||||
|
|
|
@ -55,7 +55,6 @@ import { validateRevalidate } from '../server/lib/patch-fetch'
|
||||||
import { TurborepoAccessTraceResult } from '../build/turborepo-access-trace'
|
import { TurborepoAccessTraceResult } from '../build/turborepo-access-trace'
|
||||||
import { createProgress } from '../build/progress'
|
import { createProgress } from '../build/progress'
|
||||||
import type { DeepReadonly } from '../shared/lib/deep-readonly'
|
import type { DeepReadonly } from '../shared/lib/deep-readonly'
|
||||||
import { checkIsAppPPREnabled } from '../server/lib/experimental/ppr'
|
|
||||||
|
|
||||||
export class ExportError extends Error {
|
export class ExportError extends Error {
|
||||||
code = 'NEXT_EXPORT_ERROR'
|
code = 'NEXT_EXPORT_ERROR'
|
||||||
|
@ -358,7 +357,6 @@ export async function exportAppImpl(
|
||||||
strictNextHead: nextConfig.experimental.strictNextHead ?? true,
|
strictNextHead: nextConfig.experimental.strictNextHead ?? true,
|
||||||
deploymentId: nextConfig.deploymentId,
|
deploymentId: nextConfig.deploymentId,
|
||||||
experimental: {
|
experimental: {
|
||||||
isAppPPREnabled: checkIsAppPPREnabled(nextConfig.experimental.ppr),
|
|
||||||
clientTraceMetadata: nextConfig.experimental.clientTraceMetadata,
|
clientTraceMetadata: nextConfig.experimental.clientTraceMetadata,
|
||||||
swrDelta: nextConfig.swrDelta,
|
swrDelta: nextConfig.swrDelta,
|
||||||
after: nextConfig.experimental.after ?? false,
|
after: nextConfig.experimental.after ?? false,
|
||||||
|
|
|
@ -95,7 +95,7 @@ export async function exportAppPage(
|
||||||
// instead of the standard rsc. This is because the standard rsc will
|
// instead of the standard rsc. This is because the standard rsc will
|
||||||
// contain the dynamic data. We do this if any routes have PPR enabled so
|
// contain the dynamic data. We do this if any routes have PPR enabled so
|
||||||
// that the cache read/write is the same.
|
// that the cache read/write is the same.
|
||||||
else if (renderOpts.experimental.isAppPPREnabled) {
|
else if (renderOpts.experimental.isRoutePPREnabled) {
|
||||||
// If PPR is enabled, we should emit the flight data as the prefetch
|
// If PPR is enabled, we should emit the flight data as the prefetch
|
||||||
// payload.
|
// payload.
|
||||||
await fileWriter(
|
await fileWriter(
|
||||||
|
|
|
@ -231,7 +231,6 @@ async function exportPageImpl(
|
||||||
distDir,
|
distDir,
|
||||||
dir,
|
dir,
|
||||||
enabledDirectories,
|
enabledDirectories,
|
||||||
isAppPPREnabled: input.renderOpts.experimental.isAppPPREnabled,
|
|
||||||
// skip writing to disk in minimal mode for now, pending some
|
// skip writing to disk in minimal mode for now, pending some
|
||||||
// changes to better support it
|
// changes to better support it
|
||||||
flushToDisk: !hasNextSupport,
|
flushToDisk: !hasNextSupport,
|
||||||
|
|
|
@ -160,11 +160,6 @@ export interface RenderOptsPartial {
|
||||||
params?: ParsedUrlQuery
|
params?: ParsedUrlQuery
|
||||||
isPrefetch?: boolean
|
isPrefetch?: boolean
|
||||||
experimental: {
|
experimental: {
|
||||||
/**
|
|
||||||
* When true, some routes support partial prerendering (PPR).
|
|
||||||
*/
|
|
||||||
isAppPPREnabled: boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When true, it indicates that the current page supports partial
|
* When true, it indicates that the current page supports partial
|
||||||
* prerendering.
|
* prerendering.
|
||||||
|
|
|
@ -426,6 +426,8 @@ export default abstract class Server<
|
||||||
readonly data: NextDataPathnameNormalizer | undefined
|
readonly data: NextDataPathnameNormalizer | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly isAppPPREnabled: boolean
|
||||||
|
|
||||||
public constructor(options: ServerOptions) {
|
public constructor(options: ServerOptions) {
|
||||||
const {
|
const {
|
||||||
dir = '.',
|
dir = '.',
|
||||||
|
@ -491,7 +493,7 @@ export default abstract class Server<
|
||||||
|
|
||||||
this.enabledDirectories = this.getEnabledDirectories(dev)
|
this.enabledDirectories = this.getEnabledDirectories(dev)
|
||||||
|
|
||||||
const isAppPPREnabled =
|
this.isAppPPREnabled =
|
||||||
this.enabledDirectories.app &&
|
this.enabledDirectories.app &&
|
||||||
checkIsAppPPREnabled(this.nextConfig.experimental.ppr)
|
checkIsAppPPREnabled(this.nextConfig.experimental.ppr)
|
||||||
|
|
||||||
|
@ -500,7 +502,7 @@ export default abstract class Server<
|
||||||
// mode as otherwise that route is not exposed external to the server as
|
// mode as otherwise that route is not exposed external to the server as
|
||||||
// we instead only rely on the headers.
|
// we instead only rely on the headers.
|
||||||
postponed:
|
postponed:
|
||||||
isAppPPREnabled && this.minimalMode
|
this.isAppPPREnabled && this.minimalMode
|
||||||
? new PostponedPathnameNormalizer()
|
? new PostponedPathnameNormalizer()
|
||||||
: undefined,
|
: undefined,
|
||||||
rsc:
|
rsc:
|
||||||
|
@ -508,7 +510,7 @@ export default abstract class Server<
|
||||||
? new RSCPathnameNormalizer()
|
? new RSCPathnameNormalizer()
|
||||||
: undefined,
|
: undefined,
|
||||||
prefetchRSC:
|
prefetchRSC:
|
||||||
isAppPPREnabled && this.minimalMode
|
this.isAppPPREnabled && this.minimalMode
|
||||||
? new PrefetchRSCPathnameNormalizer()
|
? new PrefetchRSCPathnameNormalizer()
|
||||||
: undefined,
|
: undefined,
|
||||||
data: this.enabledDirectories.pages
|
data: this.enabledDirectories.pages
|
||||||
|
@ -568,7 +570,6 @@ export default abstract class Server<
|
||||||
// @ts-expect-error internal field not publicly exposed
|
// @ts-expect-error internal field not publicly exposed
|
||||||
isExperimentalCompile: this.nextConfig.experimental.isExperimentalCompile,
|
isExperimentalCompile: this.nextConfig.experimental.isExperimentalCompile,
|
||||||
experimental: {
|
experimental: {
|
||||||
isAppPPREnabled,
|
|
||||||
swrDelta: this.nextConfig.swrDelta,
|
swrDelta: this.nextConfig.swrDelta,
|
||||||
clientTraceMetadata: this.nextConfig.experimental.clientTraceMetadata,
|
clientTraceMetadata: this.nextConfig.experimental.clientTraceMetadata,
|
||||||
after: this.nextConfig.experimental.after ?? false,
|
after: this.nextConfig.experimental.after ?? false,
|
||||||
|
@ -2009,9 +2010,9 @@ export default abstract class Server<
|
||||||
* enabled, then the given route _could_ support PPR.
|
* enabled, then the given route _could_ support PPR.
|
||||||
*/
|
*/
|
||||||
const couldSupportPPR: boolean =
|
const couldSupportPPR: boolean =
|
||||||
|
this.isAppPPREnabled &&
|
||||||
typeof routeModule !== 'undefined' &&
|
typeof routeModule !== 'undefined' &&
|
||||||
isAppPageRouteModule(routeModule) &&
|
isAppPageRouteModule(routeModule)
|
||||||
this.renderOpts.experimental.isAppPPREnabled
|
|
||||||
|
|
||||||
// If this is a request that's rendering an app page that support's PPR,
|
// If this is a request that's rendering an app page that support's PPR,
|
||||||
// then if we're in development mode (or using the experimental test
|
// then if we're in development mode (or using the experimental test
|
||||||
|
@ -2772,6 +2773,7 @@ export default abstract class Server<
|
||||||
incrementalCache,
|
incrementalCache,
|
||||||
isOnDemandRevalidate,
|
isOnDemandRevalidate,
|
||||||
isPrefetch: req.headers.purpose === 'prefetch',
|
isPrefetch: req.headers.purpose === 'prefetch',
|
||||||
|
isRoutePPREnabled,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,6 @@ type FileSystemCacheContext = Omit<
|
||||||
> & {
|
> & {
|
||||||
fs: CacheFs
|
fs: CacheFs
|
||||||
serverDistDir: string
|
serverDistDir: string
|
||||||
|
|
||||||
/**
|
|
||||||
* isAppPPREnabled is true when PPR has been enabled either globally or just for
|
|
||||||
* some pages via the `incremental` option.
|
|
||||||
*/
|
|
||||||
isAppPPREnabled: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TagsManifest = {
|
type TagsManifest = {
|
||||||
|
@ -42,7 +36,6 @@ export default class FileSystemCache implements CacheHandler {
|
||||||
private pagesDir: boolean
|
private pagesDir: boolean
|
||||||
private tagsManifestPath?: string
|
private tagsManifestPath?: string
|
||||||
private revalidatedTags: string[]
|
private revalidatedTags: string[]
|
||||||
private readonly isAppPPREnabled: boolean
|
|
||||||
private debug: boolean
|
private debug: boolean
|
||||||
|
|
||||||
constructor(ctx: FileSystemCacheContext) {
|
constructor(ctx: FileSystemCacheContext) {
|
||||||
|
@ -52,7 +45,6 @@ export default class FileSystemCache implements CacheHandler {
|
||||||
this.appDir = !!ctx._appDir
|
this.appDir = !!ctx._appDir
|
||||||
this.pagesDir = !!ctx._pagesDir
|
this.pagesDir = !!ctx._pagesDir
|
||||||
this.revalidatedTags = ctx.revalidatedTags
|
this.revalidatedTags = ctx.revalidatedTags
|
||||||
this.isAppPPREnabled = ctx.isAppPPREnabled
|
|
||||||
this.debug = !!process.env.NEXT_PRIVATE_DEBUG_CACHE
|
this.debug = !!process.env.NEXT_PRIVATE_DEBUG_CACHE
|
||||||
|
|
||||||
if (ctx.maxMemoryCacheSize) {
|
if (ctx.maxMemoryCacheSize) {
|
||||||
|
@ -177,7 +169,7 @@ export default class FileSystemCache implements CacheHandler {
|
||||||
|
|
||||||
public async get(...args: Parameters<CacheHandler['get']>) {
|
public async get(...args: Parameters<CacheHandler['get']>) {
|
||||||
const [key, ctx = {}] = args
|
const [key, ctx = {}] = args
|
||||||
const { tags, softTags, kindHint } = ctx
|
const { tags, softTags, kindHint, isRoutePPREnabled } = ctx
|
||||||
let data = memoryCache?.get(key)
|
let data = memoryCache?.get(key)
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
|
@ -246,7 +238,10 @@ export default class FileSystemCache implements CacheHandler {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('tags vs storedTags mismatch', tags, storedTags)
|
console.log('tags vs storedTags mismatch', tags, storedTags)
|
||||||
}
|
}
|
||||||
await this.set(key, data.value, { tags })
|
await this.set(key, data.value, {
|
||||||
|
tags,
|
||||||
|
isRoutePPREnabled,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -254,7 +249,7 @@ export default class FileSystemCache implements CacheHandler {
|
||||||
? await this.fs.readFile(
|
? await this.fs.readFile(
|
||||||
this.getFilePath(
|
this.getFilePath(
|
||||||
`${key}${
|
`${key}${
|
||||||
this.isAppPPREnabled ? RSC_PREFETCH_SUFFIX : RSC_SUFFIX
|
isRoutePPREnabled ? RSC_PREFETCH_SUFFIX : RSC_SUFFIX
|
||||||
}`,
|
}`,
|
||||||
'app'
|
'app'
|
||||||
),
|
),
|
||||||
|
@ -398,7 +393,7 @@ export default class FileSystemCache implements CacheHandler {
|
||||||
this.getFilePath(
|
this.getFilePath(
|
||||||
`${key}${
|
`${key}${
|
||||||
isAppPath
|
isAppPath
|
||||||
? this.isAppPPREnabled
|
? ctx.isRoutePPREnabled
|
||||||
? RSC_PREFETCH_SUFFIX
|
? RSC_PREFETCH_SUFFIX
|
||||||
: RSC_SUFFIX
|
: RSC_SUFFIX
|
||||||
: NEXT_DATA_SUFFIX
|
: NEXT_DATA_SUFFIX
|
||||||
|
|
|
@ -31,7 +31,6 @@ export interface CacheHandlerContext {
|
||||||
fetchCacheKeyPrefix?: string
|
fetchCacheKeyPrefix?: string
|
||||||
prerenderManifest?: PrerenderManifest
|
prerenderManifest?: PrerenderManifest
|
||||||
revalidatedTags: string[]
|
revalidatedTags: string[]
|
||||||
isAppPPREnabled?: boolean
|
|
||||||
_appDir: boolean
|
_appDir: boolean
|
||||||
_pagesDir: boolean
|
_pagesDir: boolean
|
||||||
_requestHeaders: IncrementalCache['requestHeaders']
|
_requestHeaders: IncrementalCache['requestHeaders']
|
||||||
|
@ -104,7 +103,6 @@ export class IncrementalCache implements IncrementalCacheType {
|
||||||
fetchCacheKeyPrefix,
|
fetchCacheKeyPrefix,
|
||||||
CurCacheHandler,
|
CurCacheHandler,
|
||||||
allowedRevalidateHeaderKeys,
|
allowedRevalidateHeaderKeys,
|
||||||
isAppPPREnabled,
|
|
||||||
}: {
|
}: {
|
||||||
fs?: CacheFs
|
fs?: CacheFs
|
||||||
dev: boolean
|
dev: boolean
|
||||||
|
@ -121,7 +119,6 @@ export class IncrementalCache implements IncrementalCacheType {
|
||||||
getPrerenderManifest: () => DeepReadonly<PrerenderManifest>
|
getPrerenderManifest: () => DeepReadonly<PrerenderManifest>
|
||||||
fetchCacheKeyPrefix?: string
|
fetchCacheKeyPrefix?: string
|
||||||
CurCacheHandler?: typeof CacheHandler
|
CurCacheHandler?: typeof CacheHandler
|
||||||
isAppPPREnabled: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const debug = !!process.env.NEXT_PRIVATE_DEBUG_CACHE
|
const debug = !!process.env.NEXT_PRIVATE_DEBUG_CACHE
|
||||||
this.hasCustomCacheHandler = Boolean(CurCacheHandler)
|
this.hasCustomCacheHandler = Boolean(CurCacheHandler)
|
||||||
|
@ -193,7 +190,6 @@ export class IncrementalCache implements IncrementalCacheType {
|
||||||
_appDir: !!appDir,
|
_appDir: !!appDir,
|
||||||
_requestHeaders: requestHeaders,
|
_requestHeaders: requestHeaders,
|
||||||
fetchCacheKeyPrefix,
|
fetchCacheKeyPrefix,
|
||||||
isAppPPREnabled,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -434,6 +430,7 @@ export class IncrementalCache implements IncrementalCacheType {
|
||||||
fetchIdx?: number
|
fetchIdx?: number
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
softTags?: string[]
|
softTags?: string[]
|
||||||
|
isRoutePPREnabled?: boolean
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<IncrementalCacheEntry | null> {
|
): Promise<IncrementalCacheEntry | null> {
|
||||||
if (
|
if (
|
||||||
|
@ -556,6 +553,7 @@ export class IncrementalCache implements IncrementalCacheType {
|
||||||
fetchUrl?: string
|
fetchUrl?: string
|
||||||
fetchIdx?: number
|
fetchIdx?: number
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
|
isRoutePPREnabled?: boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -391,7 +391,6 @@ export default class NextNodeServer extends BaseServer<
|
||||||
!this.minimalMode && this.nextConfig.experimental.isrFlushToDisk,
|
!this.minimalMode && this.nextConfig.experimental.isrFlushToDisk,
|
||||||
getPrerenderManifest: () => this.getPrerenderManifest(),
|
getPrerenderManifest: () => this.getPrerenderManifest(),
|
||||||
CurCacheHandler: CacheHandler,
|
CurCacheHandler: CacheHandler,
|
||||||
isAppPPREnabled: this.renderOpts.experimental.isAppPPREnabled,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ export default class ResponseCache implements ResponseCacheBase {
|
||||||
isOnDemandRevalidate?: boolean
|
isOnDemandRevalidate?: boolean
|
||||||
isPrefetch?: boolean
|
isPrefetch?: boolean
|
||||||
incrementalCache: IncrementalCache
|
incrementalCache: IncrementalCache
|
||||||
|
isRoutePPREnabled?: boolean
|
||||||
}
|
}
|
||||||
): Promise<ResponseCacheEntry | null> {
|
): Promise<ResponseCacheEntry | null> {
|
||||||
// If there is no key for the cache, we can't possibly look this up in the
|
// If there is no key for the cache, we can't possibly look this up in the
|
||||||
|
@ -89,7 +90,10 @@ export default class ResponseCache implements ResponseCacheBase {
|
||||||
let cachedResponse: IncrementalCacheItem = null
|
let cachedResponse: IncrementalCacheItem = null
|
||||||
try {
|
try {
|
||||||
cachedResponse = !this.minimalMode
|
cachedResponse = !this.minimalMode
|
||||||
? await incrementalCache.get(key, { kindHint })
|
? await incrementalCache.get(key, {
|
||||||
|
kindHint,
|
||||||
|
isRoutePPREnabled: context.isRoutePPREnabled,
|
||||||
|
})
|
||||||
: null
|
: null
|
||||||
|
|
||||||
if (cachedResponse && !isOnDemandRevalidate) {
|
if (cachedResponse && !isOnDemandRevalidate) {
|
||||||
|
@ -153,6 +157,7 @@ export default class ResponseCache implements ResponseCacheBase {
|
||||||
} else {
|
} else {
|
||||||
await incrementalCache.set(key, resolveValue.value, {
|
await incrementalCache.set(key, resolveValue.value, {
|
||||||
revalidate: resolveValue.revalidate,
|
revalidate: resolveValue.revalidate,
|
||||||
|
isRoutePPREnabled: context.isRoutePPREnabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,6 +172,7 @@ export default class ResponseCache implements ResponseCacheBase {
|
||||||
Math.max(cachedResponse.revalidate || 3, 3),
|
Math.max(cachedResponse.revalidate || 3, 3),
|
||||||
30
|
30
|
||||||
),
|
),
|
||||||
|
isRoutePPREnabled: context.isRoutePPREnabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ export interface ResponseCacheBase {
|
||||||
* provided it will test the filesystem to check.
|
* provided it will test the filesystem to check.
|
||||||
*/
|
*/
|
||||||
routeKind?: RouteKind
|
routeKind?: RouteKind
|
||||||
|
|
||||||
|
isRoutePPREnabled?: boolean
|
||||||
}
|
}
|
||||||
): Promise<ResponseCacheEntry | null>
|
): Promise<ResponseCacheEntry | null>
|
||||||
}
|
}
|
||||||
|
@ -139,11 +141,16 @@ export interface IncrementalCache {
|
||||||
* determine the kind from the filesystem.
|
* determine the kind from the filesystem.
|
||||||
*/
|
*/
|
||||||
kindHint?: IncrementalCacheKindHint
|
kindHint?: IncrementalCacheKindHint
|
||||||
|
|
||||||
|
isRoutePPREnabled?: boolean
|
||||||
}
|
}
|
||||||
) => Promise<IncrementalCacheItem>
|
) => Promise<IncrementalCacheItem>
|
||||||
set: (
|
set: (
|
||||||
key: string,
|
key: string,
|
||||||
data: IncrementalCacheValue | null,
|
data: IncrementalCacheValue | null,
|
||||||
ctx: { revalidate: Revalidate }
|
ctx: {
|
||||||
|
revalidate: Revalidate
|
||||||
|
isRoutePPREnabled?: boolean
|
||||||
|
}
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,8 +93,6 @@ export default class NextWebServer extends BaseServer<
|
||||||
CurCacheHandler:
|
CurCacheHandler:
|
||||||
this.serverOptions.webServerConfig.incrementalCacheHandler,
|
this.serverOptions.webServerConfig.incrementalCacheHandler,
|
||||||
getPrerenderManifest: () => this.getPrerenderManifest(),
|
getPrerenderManifest: () => this.getPrerenderManifest(),
|
||||||
// PPR is not supported in the Edge runtime.
|
|
||||||
isAppPPREnabled: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
protected getResponseCache() {
|
protected getResponseCache() {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { TestPage } from '../../../components/page'
|
||||||
|
|
||||||
|
export const experimental_ppr = true
|
||||||
|
|
||||||
|
export default function Page({ params: { locale } }) {
|
||||||
|
return <TestPage pathname={`/${locale}/about`} />
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { locales } from '../../components/page'
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
return locales.map((locale) => ({ locale }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Layout({ children, params: { locale } }) {
|
||||||
|
return (
|
||||||
|
<html lang={locale}>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { TestPage } from '../../components/page'
|
||||||
|
|
||||||
|
export default function Page({ params: { locale } }) {
|
||||||
|
return <TestPage pathname={`/${locale}`} />
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { TestPage } from '../../../components/page'
|
||||||
|
|
||||||
|
export default function Page({ params: { locale } }) {
|
||||||
|
return <TestPage pathname={`/${locale}/static`} noDynamic />
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default ({ children }) => {
|
||||||
|
return children
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
import { locales } from '../components/page'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
// Redirect to the default locale
|
||||||
|
return redirect(`/${locales[0]}`)
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { unstable_noStore } from 'next/cache'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
|
export const locales = ['en', 'fr']
|
||||||
|
|
||||||
|
export const links = [
|
||||||
|
{ href: '/', text: 'Home' },
|
||||||
|
...locales
|
||||||
|
.map((locale) => {
|
||||||
|
return [
|
||||||
|
{ href: `/${locale}`, text: locale },
|
||||||
|
{ href: `/${locale}/about`, text: `${locale} - About` },
|
||||||
|
{ href: `/${locale}/static`, text: `${locale} - Static` },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.flat(),
|
||||||
|
]
|
||||||
|
|
||||||
|
function Dynamic({ noDynamic = false }) {
|
||||||
|
if (!noDynamic) unstable_noStore()
|
||||||
|
return <div id="dynamic">Dynamic</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TestPage({ pathname, noDynamic = false }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{links.map(({ href, text }) => (
|
||||||
|
<li key={href}>
|
||||||
|
<Link href={href}>{text}</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<code data-value={pathname}>{pathname}</code>
|
||||||
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<Dynamic noDynamic={noDynamic} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { NextInstance, nextTestSetup } from 'e2e-utils'
|
||||||
|
import { links, locales } from './components/page'
|
||||||
|
import glob from 'glob'
|
||||||
|
import { promisify } from 'node:util'
|
||||||
|
|
||||||
|
const globp = promisify(glob)
|
||||||
|
|
||||||
|
async function getDotNextFiles(next: NextInstance): Promise<Array<string>> {
|
||||||
|
const files = await globp('**/.next/**/*', {
|
||||||
|
cwd: next.testDir,
|
||||||
|
absolute: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ppr-navigations incremental', () => {
|
||||||
|
const { next, isNextDev, isTurbopack } = nextTestSetup({
|
||||||
|
files: __dirname,
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can navigate between all the links and back without writing to disk', async () => {
|
||||||
|
const before = !isNextDev && !isTurbopack ? await getDotNextFiles(next) : []
|
||||||
|
|
||||||
|
const browser = await next.browser('/')
|
||||||
|
|
||||||
|
// Add a variable to the window so we can tell if it MPA navigated. If this
|
||||||
|
// value is still true at the end of the test, then we know that the page
|
||||||
|
// didn't MPA navigate.
|
||||||
|
const random = Math.random().toString(36).substring(7)
|
||||||
|
await browser.eval(`window.random = ${JSON.stringify(random)}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const { href } of links) {
|
||||||
|
// Find the link element for the href and click it.
|
||||||
|
await browser.elementByCss(`a[href="${href}"]`).click()
|
||||||
|
|
||||||
|
// Wait for the network to be idle. This seems odd, but this is to help
|
||||||
|
// catch the condition where a rouge 404 triggers a MPA navigation.
|
||||||
|
await browser.waitForIdleNetwork()
|
||||||
|
|
||||||
|
// Check if the page navigated.
|
||||||
|
expect(await browser.eval(`window.random`)).toBe(random)
|
||||||
|
|
||||||
|
// Wait for that page to load.
|
||||||
|
if (href === '/') {
|
||||||
|
// The root page redirects to the first locale.
|
||||||
|
await browser.waitForElementByCss(`[data-value="/${locales[0]}"]`)
|
||||||
|
} else {
|
||||||
|
await browser.waitForElementByCss(`[data-value="${href}"]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.elementByCss('#dynamic')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await browser.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNextDev && !isTurbopack) {
|
||||||
|
const after = await getDotNextFiles(next)
|
||||||
|
|
||||||
|
// Ensure that no new files were written to disk. If this test fails, it's
|
||||||
|
// likely that there was a change to the incremental cache or file system
|
||||||
|
// cache that resulted in information like the ppr state not being properly
|
||||||
|
// propagated.
|
||||||
|
expect(after).toEqual(before)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
experimental: {
|
||||||
|
ppr: 'incremental',
|
||||||
|
},
|
||||||
|
}
|
|
@ -15,7 +15,6 @@ describe('FileSystemCache', () => {
|
||||||
fs: nodeFs,
|
fs: nodeFs,
|
||||||
serverDistDir: cacheDir,
|
serverDistDir: cacheDir,
|
||||||
revalidatedTags: [],
|
revalidatedTags: [],
|
||||||
isAppPPREnabled: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const binary = await fs.readFile(
|
const binary = await fs.readFile(
|
||||||
|
@ -55,7 +54,6 @@ describe('FileSystemCache (isrMemory 0)', () => {
|
||||||
fs: nodeFs,
|
fs: nodeFs,
|
||||||
serverDistDir: cacheDir,
|
serverDistDir: cacheDir,
|
||||||
revalidatedTags: [],
|
revalidatedTags: [],
|
||||||
isAppPPREnabled: false,
|
|
||||||
maxMemoryCacheSize: 0, // disable memory cache
|
maxMemoryCacheSize: 0, // disable memory cache
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue