Revert "Remove legacy PPR codepaths" (#95113)

This commit is contained in:
Sebastian "Sebbie" Silbermann
2026-06-24 13:56:10 +02:00
committed by GitHub
parent 727e545307
commit 6cc1049d4e
81 changed files with 1374 additions and 254 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ All flags need: `config-shared.ts` (type) → `config-schema.ts` (zod). If the f
## Where the Flag Is Consumed
**Client/bundled code only** (e.g. `__NEXT_CACHE_COMPONENTS` in client components): `define-env.ts` is sufficient. Webpack/Turbopack replaces `process.env.X` at the user's build time.
**Client/bundled code only** (e.g. `__NEXT_PPR` in client components): `define-env.ts` is sufficient. Webpack/Turbopack replaces `process.env.X` at the user's build time.
**Pre-compiled runtime bundles** (e.g. code in `app-render.tsx`): The flag must also be set as a real `process.env` var at runtime, because `app-render.tsx` runs from pre-compiled bundles where `define-env.ts` doesn't reach. Two approaches:
@@ -2109,6 +2109,7 @@ export async function handleBuildComplete({
onMatchHeaders: [],
rewrites,
restrictedRedirectPaths: [],
isAppPPREnabled: config.cacheComponents,
}).routesManifest.rsc,
},
outputs,
+4
View File
@@ -13,6 +13,7 @@ import { discoverRoutes } from '../route-discovery'
import { findPagesDir } from '../../lib/find-pages-dir'
import loadCustomRoutes from '../../lib/load-custom-routes'
import { generateRoutesManifest } from '../generate-routes-manifest'
import { checkIsAppPPREnabled } from '../../server/lib/experimental/ppr'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import http from 'node:http'
@@ -166,6 +167,8 @@ async function collectRoutesForAnalyze(
config.basePath ? `${config.basePath}${pathPrefix}` : pathPrefix
)
const isAppPPREnabled = checkIsAppPPREnabled(config.experimental.ppr)
// Generate routes manifest
const { routesManifest } = generateRoutesManifest({
appType,
@@ -176,6 +179,7 @@ async function collectRoutesForAnalyze(
onMatchHeaders,
rewrites,
restrictedRedirectPaths,
isAppPPREnabled,
})
return routesManifest.dynamicRoutes
+3
View File
@@ -7,6 +7,7 @@ import type { ProxyMatcher } from './analysis/get-page-static-info'
import type { Rewrite } from '../lib/load-custom-routes'
import path from 'node:path'
import { needsExperimentalReact } from '../lib/needs-experimental-react'
import { checkIsAppPPREnabled } from '../server/lib/experimental/ppr'
import {
getNextConfigEnv,
getNextPublicEnvironmentVariables,
@@ -120,6 +121,7 @@ export function getDefineEnv({
const nextPublicEnv = getNextPublicEnvironmentVariables()
const nextConfigEnv = getNextConfigEnv(config)
const isPPREnabled = checkIsAppPPREnabled(config.experimental.ppr)
const isCacheComponentsEnabled = !!config.cacheComponents
const isUseCacheEnabled = !!config.experimental.useCache
@@ -171,6 +173,7 @@ export function getDefineEnv({
'process.env.__NEXT_APP_NEW_SCROLL_HANDLER': Boolean(
config.experimental.appNewScrollHandler
),
'process.env.__NEXT_PPR': isPPREnabled,
'process.env.__NEXT_CACHE_COMPONENTS': isCacheComponentsEnabled,
'process.env.__NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS': Boolean(
config.experimental.cachedNavigations
@@ -34,6 +34,7 @@ export interface GenerateRoutesManifestOptions {
onMatchHeaders: CustomRoutes['headers']
rewrites: CustomRoutes['rewrites']
restrictedRedirectPaths: string[]
isAppPPREnabled: boolean
appType: 'pages' | 'app' | 'hybrid'
deploymentId?: string
}
@@ -60,6 +61,7 @@ export function generateRoutesManifest(
onMatchHeaders,
rewrites,
restrictedRedirectPaths,
isAppPPREnabled,
appType,
deploymentId,
} = options
@@ -92,8 +94,6 @@ export function generateRoutesManifest(
}
}
const enableCacheComponents = Boolean(config.cacheComponents)
const routesManifest: RoutesManifest = {
version: 3,
pages404: true,
@@ -130,9 +130,9 @@ export function generateRoutesManifest(
prefetchSegmentHeader: NEXT_ROUTER_SEGMENT_PREFETCH_HEADER,
prefetchSegmentSuffix: RSC_SEGMENT_SUFFIX,
prefetchSegmentDirSuffix: RSC_SEGMENTS_DIR_SUFFIX,
clientParamParsing: enableCacheComponents,
clientParamParsing: config.cacheComponents ?? false,
clientParamParsingOrigins: config.experimental.clientParamParsingOrigins,
dynamicRSCPrerender: enableCacheComponents,
dynamicRSCPrerender: isAppPPREnabled && config.cacheComponents === true,
},
rewriteHeaders: {
pathHeader: NEXT_REWRITTEN_PATH_HEADER,
@@ -140,7 +140,7 @@ export function generateRoutesManifest(
},
skipProxyUrlNormalize: config.skipProxyUrlNormalize,
deploymentId: deploymentId || undefined,
ppr: enableCacheComponents
ppr: isAppPPREnabled
? {
chain: {
headers: {
+22 -4
View File
@@ -191,6 +191,10 @@ import { traceMemoryUsage } from '../lib/memory/trace'
import { generateEncryptionKeyBase64 } from '../server/app-render/encryption-utils-server'
import type { DeepReadonly } from '../shared/lib/deep-readonly'
import uploadTrace from '../trace/upload-trace'
import {
checkIsAppPPREnabled,
checkIsRoutePPREnabled,
} from '../server/lib/experimental/ppr'
import { FallbackMode, fallbackModeToFallbackField } from '../lib/fallback'
import { RenderingMode } from './rendering-mode'
import { InvariantError } from '../shared/lib/invariant-error'
@@ -1520,6 +1524,7 @@ export default async function build(
const isAuthInterruptsEnabled = Boolean(
config.experimental.authInterrupts
)
const isAppPPREnabled = checkIsAppPPREnabled(config.experimental.ppr)
const routesManifestPath = path.join(distDir, ROUTES_MANIFEST)
@@ -1536,6 +1541,7 @@ export default async function build(
onMatchHeaders,
rewrites,
restrictedRedirectPaths,
isAppPPREnabled,
deploymentId: config.deploymentId,
})
)
@@ -2105,6 +2111,7 @@ export default async function build(
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
nextConfigOutput: config.output,
pprConfig: config.experimental.ppr,
cacheLifeProfiles: config.cacheLife,
buildId,
deploymentId: config.deploymentId,
@@ -2337,6 +2344,7 @@ export default async function build(
: config.experimental.isrFlushToDisk,
cacheMaxMemorySize: config.cacheMaxMemorySize,
nextConfigOutput: config.output,
pprConfig: config.experimental.ppr,
cacheLifeProfiles: config.cacheLife,
buildId,
deploymentId: config.deploymentId,
@@ -2741,6 +2749,10 @@ export default async function build(
featureName: 'experimental/nextScriptWorkers',
invocationCount: config.experimental.nextScriptWorkers ? 1 : 0,
},
{
featureName: 'experimental/ppr',
invocationCount: config.experimental.ppr ? 1 : 0,
},
{
featureName: 'turbopackFileSystemCache',
invocationCount: config.experimental?.turbopackFileSystemCacheForBuild
@@ -2930,6 +2942,10 @@ export default async function build(
const appConfig = appDefaultConfigs.get(originalAppPath)
const isDynamicError = appConfig?.dynamic === 'error'
const isRoutePPREnabled: boolean = appConfig
? checkIsRoutePPREnabled(config.experimental.ppr)
: false
routes.forEach((route) => {
// If the route has any dynamic root segments, we need to skip
// rendering the route. This is because we don't support
@@ -2954,6 +2970,7 @@ export default async function build(
_fallbackRouteParams: route.fallbackRouteParams,
_isDynamicError: isDynamicError,
_isAppDir: true,
_isRoutePPREnabled: isRoutePPREnabled,
_allowEmptyStaticShell: !route.throwOnEmptyStaticShell,
// A fallback shell can only be upgraded if at least one of
// its fallback params is a `generateStaticParams` candidate.
@@ -3124,7 +3141,8 @@ export default async function build(
// When this is an app page and PPR is enabled, the route supports
// partial pre-rendering.
const isRoutePPREnabled: true | undefined =
!isAppRouteHandler && Boolean(config.cacheComponents)
!isAppRouteHandler &&
checkIsRoutePPREnabled(config.experimental.ppr)
? true
: undefined
@@ -3306,7 +3324,7 @@ export default async function build(
prerenderManifest.routes[route.pathname] = {
initialStatus: status,
initialHeaders: meta.headers,
renderingMode: isAppCacheComponentsEnabled
renderingMode: isAppPPREnabled
? isRoutePPREnabled
? RenderingMode.PARTIALLY_STATIC
: RenderingMode.STATIC
@@ -3404,7 +3422,7 @@ export default async function build(
let dynamicRoute = routesManifest.dynamicRoutes.find(
(r) => r.page === route.pathname
)
if (!isAppRouteHandler && isAppCacheComponentsEnabled) {
if (!isAppRouteHandler && isAppPPREnabled) {
// If the dynamic route wasn't found, then we need to create
// it. This ensures that for each fallback shell there's an
// entry in the app routes manifest which enables routing for
@@ -3539,7 +3557,7 @@ export default async function build(
experimentalPPR: isRoutePPREnabled,
remainingPrerenderableParams:
route.remainingPrerenderableParams,
renderingMode: isAppCacheComponentsEnabled
renderingMode: isAppPPREnabled
? isRoutePPREnabled
? RenderingMode.PARTIALLY_STATIC
: RenderingMode.STATIC
+1 -1
View File
@@ -789,7 +789,7 @@ export async function buildAppStaticPaths({
fetchCacheKeyPrefix,
nextConfigOutput,
ComponentMod,
isRoutePPREnabled,
isRoutePPREnabled = false,
buildId,
deploymentId,
rootParamKeys,
+16 -3
View File
@@ -29,6 +29,7 @@ import {
NodeNextRequest,
NodeNextResponse,
} from '../../server/base-http/node' with { 'turbopack-transition': 'next-server-utility' }
import { checkIsAppPPREnabled } from '../../server/lib/experimental/ppr' with { 'turbopack-transition': 'next-server-utility' }
import { isRSCRequestHeader } from '../../server/lib/is-rsc-request' with { 'turbopack-transition': 'next-server-utility' }
import {
getFallbackRouteParams,
@@ -118,6 +119,7 @@ import * as entryBase from '../../server/app-render/entry-base' with { 'turbopac
import { RedirectStatusCode } from '../../client/components/redirect-status-code' with { 'turbopack-transition': 'next-server-utility' }
import { InvariantError } from '../../shared/lib/invariant-error' with { 'turbopack-transition': 'next-server-utility' }
import { scheduleOnNextTick } from '../../lib/scheduler' with { 'turbopack-transition': 'next-server-utility' }
import { isInterceptionRouteAppPath } from '../../shared/lib/router/utils/interception-routes' with { 'turbopack-transition': 'next-server-utility' }
import { getSegmentParam } from '../../shared/lib/router/utils/get-segment-param' with { 'turbopack-transition': 'next-server-utility' }
export * from '../../server/app-render/entry-base' with { 'turbopack-transition': 'next-server-utility' }
@@ -270,8 +272,16 @@ export async function handler(
// We use the resolvedPathname instead of the parsedUrl.pathname because it
// is not rewritten as resolvedPathname is. This will ensure that the correct
// prerender info is used instead of using the original pathname as the
// source.
const prerenderMatch = routeModule.match(resolvedPathname, prerenderManifest)
// source. If however PPR is enabled and cacheComponents is disabled, we
// treat the pathname as dynamic. Currently, there's a bug in the PPR
// implementation that incorrectly leaves %%drp placeholders in the output of
// parallel routes. This is addressed with cacheComponents.
const prerenderMatch =
nextConfig.experimental.ppr &&
!nextConfig.cacheComponents &&
isInterceptionRouteAppPath(resolvedPathname)
? null
: routeModule.match(resolvedPathname, prerenderManifest)
const prerenderInfo = prerenderMatch?.route ?? null
const isPrerendered = !!prerenderManifest.routes[resolvedPathname]
@@ -300,7 +310,9 @@ export async function handler(
* If the route being rendered is an app page, and the ppr feature has been
* enabled, then the given route _could_ support PPR.
*/
const couldSupportPPR = Boolean(nextConfig.cacheComponents)
const couldSupportPPR: boolean = checkIsAppPPREnabled(
nextConfig.experimental.ppr
)
// Stash postponed state for server actions when in minimal mode.
// We extract it here so the RDC is available for the re-render after the action completes.
@@ -899,6 +911,7 @@ export async function handler(
validationLevel:
nextConfig.experimental.instantInsights.validationLevel,
experimental: {
isRoutePPREnabled,
expireTime: nextConfig.expireTime,
staleTimes: nextConfig.experimental.staleTimes,
dynamicOnHover: Boolean(nextConfig.experimental.dynamicOnHover),
@@ -166,6 +166,7 @@ async function requestHandler(
cacheComponents: Boolean(nextConfig.cacheComponents),
validationLevel: nextConfig.experimental.instantInsights.validationLevel,
experimental: {
isRoutePPREnabled: false,
expireTime: nextConfig.expireTime,
staleTimes: nextConfig.experimental.staleTimes,
dynamicOnHover: Boolean(nextConfig.experimental.dynamicOnHover),
+9 -1
View File
@@ -2,6 +2,8 @@ import type {
NextConfigComplete,
NextConfigRuntime,
} from '../server/config-shared'
import type { ExperimentalPPRConfig } from '../server/lib/experimental/ppr'
import { checkIsRoutePPREnabled } from '../server/lib/experimental/ppr'
import type { AssetBinding } from './webpack/loaders/get-module-build-info'
import type { ServerRuntime } from '../types'
import type { BuildManifest } from '../server/get-page-files'
@@ -703,6 +705,7 @@ export async function isPageStatic({
cacheHandler,
cacheHandlers,
cacheLifeProfiles,
pprConfig,
buildId,
deploymentId,
clientAssetToken,
@@ -732,6 +735,7 @@ export async function isPageStatic({
[profile: string]: import('../server/use-cache/cache-life').CacheLife
}
nextConfigOutput: 'standalone' | 'export' | undefined
pprConfig: ExperimentalPPRConfig | undefined
buildId: string
deploymentId: string
clientAssetToken: string
@@ -859,8 +863,12 @@ export async function isPageStatic({
rootParamKeys = collectRootParamKeys(routeModule)
// A page supports partial prerendering if it is an app page and either
// the whole app has PPR enabled or this page has PPR enabled when we're
// in incremental mode.
isRoutePPREnabled =
routeModule.definition.kind === RouteKind.APP_PAGE && cacheComponents
routeModule.definition.kind === RouteKind.APP_PAGE &&
checkIsRoutePPREnabled(pprConfig)
// If force dynamic was set and we don't have PPR enabled, then set the
// revalidate to 0.
@@ -20,6 +20,7 @@ function hasFallbackRouteParams(): boolean {
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-client':
case 'prerender-ppr':
case 'validation-client':
const fallbackParams = workUnitStore.fallbackRouteParams
return fallbackParams ? fallbackParams.size > 0 : false
@@ -190,7 +190,10 @@ export async function fetchServerResponse(
// Typically, during a navigation, we decode the response using Flight's
// `createFromFetch` API, which accepts a `fetch` promise.
const shouldImmediatelyDecode = true
// TODO: Remove this check once the old PPR flag is removed
const isLegacyPPR =
process.env.__NEXT_PPR && !process.env.__NEXT_CACHE_COMPONENTS
const shouldImmediatelyDecode = !isLegacyPPR
const res = await createFetch<NavigationFlightResponse>(
url,
headers,
@@ -1,7 +1,11 @@
import { isHangingPromiseRejectionError } from '../../server/dynamic-rendering-utils'
import { isPostpone } from '../../server/lib/router-utils/is-postpone'
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
import { isNextRouterError } from './is-next-router-error'
import { isPrerenderInterruptedError } from '../../server/app-render/dynamic-rendering'
import {
isDynamicPostpone,
isPrerenderInterruptedError,
} from '../../server/app-render/dynamic-rendering'
import { isDynamicServerError } from './hooks-server-context'
export function unstable_rethrow(error: unknown): void {
@@ -9,6 +13,8 @@ export function unstable_rethrow(error: unknown): void {
isNextRouterError(error) ||
isBailoutToCSRError(error) ||
isDynamicServerError(error) ||
isDynamicPostpone(error) ||
isPostpone(error) ||
isHangingPromiseRejectionError(error) ||
isPrerenderInterruptedError(error)
) {
@@ -1,8 +1,10 @@
import { isDynamicServerError } from '../../client/components/hooks-server-context'
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
import { isNextRouterError } from '../../client/components/is-next-router-error'
import { isDynamicPostpone } from '../../server/app-render/dynamic-rendering'
export const isDynamicUsageError = (err: unknown) =>
isDynamicServerError(err) ||
isBailoutToCSRError(err) ||
isNextRouterError(err)
isNextRouterError(err) ||
isDynamicPostpone(err)
+2 -4
View File
@@ -880,11 +880,9 @@ async function exportAppImpl(
}
// Export mode provide static outputs that are not compatible with PPR mode.
if (!options.buildExport && nextConfig.cacheComponents) {
if (!options.buildExport && nextConfig.experimental.ppr) {
// TODO: add message
throw new Error(
'Invariant: Cache Components cannot be enabled in export mode'
)
throw new Error('Invariant: PPR cannot be enabled in export mode')
}
// copy prerendered routes to outDir
+4 -3
View File
@@ -105,7 +105,7 @@ export async function exportAppPage(
} = metadata
// Ensure we don't postpone without having PPR enabled.
if (postponed && !renderOpts.cacheComponents) {
if (postponed && !renderOpts.experimental.isRoutePPREnabled) {
throw new Error('Invariant: page postponed without PPR being enabled')
}
@@ -145,7 +145,8 @@ export async function exportAppPage(
const hasFallbackParams =
fallbackRouteParams != null && fallbackRouteParams.size > 0
const shouldWriteRsc =
!renderOpts.cacheComponents || (!postponed && !hasFallbackParams)
!renderOpts.experimental.isRoutePPREnabled ||
(!postponed && !hasFallbackParams)
hasStaticRsc = shouldWriteRsc
// With PPR enabled, we normally skip writing .rsc because it may contain
@@ -197,7 +198,7 @@ export async function exportAppPage(
// When PPR is enabled, we don't always send 200 for routes that have been
// pregenerated, so we should grab the status code from the mocked
// response.
let status: number | undefined = renderOpts.cacheComponents
let status: number | undefined = renderOpts.experimental.isRoutePPREnabled
? res.statusCode
: undefined
+15
View File
@@ -36,6 +36,7 @@ import { exportAppPage } from './routes/app-page'
import { exportPagesPage } from './routes/pages'
import { getParams } from './helpers/get-params'
import { createIncrementalCache } from './helpers/create-incremental-cache'
import { isPostpone } from '../server/lib/router-utils/is-postpone'
import { isDynamicUsageError } from './helpers/is-dynamic-usage-error'
import { isBailoutToCSRError } from '../shared/lib/lazy-dynamic/bailout-to-csr'
import {
@@ -108,6 +109,10 @@ async function exportPageImpl(
// Check if this should error when dynamic usage is detected.
_isDynamicError: isDynamicError = false,
// If this page supports partial prerendering, then we need to pass that to
// the renderOpts.
_isRoutePPREnabled: isRoutePPREnabled,
// Configure the rendering of the page to allow that an empty static shell
// is generated while rendering using PPR and Cache Components.
_allowEmptyStaticShell: allowEmptyStaticShell = false,
@@ -275,6 +280,10 @@ async function exportPageImpl(
allowEmptyStaticShell,
runInstantValidation,
isFallbackUpgradeable,
experimental: {
...commonRenderOpts.experimental,
isRoutePPREnabled,
},
renderResumeDataCache,
}
@@ -610,6 +619,12 @@ async function exportPage(
}
process.on('unhandledRejection', (err: unknown) => {
// if it's a postpone error, it'll be handled later
// when the postponed promise is actually awaited.
if (isPostpone(err)) {
return
}
// we don't want to log these errors
if (isDynamicUsageError(err)) {
return
@@ -16,6 +16,7 @@ import { isHTTPAccessFallbackError } from '../../client/components/http-access-f
import type { MetadataContext } from './types/resolvers'
import { createServerSearchParamsForMetadata } from '../../server/request/search-params'
import { createServerPathnameForMetadata } from '../../server/request/pathname'
import { isPostpone } from '../../server/lib/router-utils/is-postpone'
import {
workUnitAsyncStorage,
getStagedRenderingController,
@@ -90,6 +91,12 @@ export function createMetadataComponents({
isRuntimePrefetchable,
errorType
).catch((viewportErr) => {
// When Legacy PPR is enabled viewport can reject with a Postpone type
// This will go away once Legacy PPR is removed and dynamic metadata will
// stay pending until after the prerender is complete when it is dynamic
if (isPostpone(viewportErr)) {
throw viewportErr
}
if (!errorType && isHTTPAccessFallbackError(viewportErr)) {
return getNotFoundViewport(
tree,
@@ -137,6 +144,12 @@ export function createMetadataComponents({
isRuntimePrefetchable,
errorType
).catch((metadataErr) => {
// When Legacy PPR is enabled metadata can reject with a Postpone type
// This will go away once Legacy PPR is removed and dynamic metadata will
// stay pending until after the prerender is complete when it is dynamic
if (isPostpone(metadataErr)) {
throw metadataErr
}
if (!errorType && isHTTPAccessFallbackError(metadataErr)) {
return getNotFoundMetadata(
tree,
@@ -2139,7 +2139,8 @@ async function getRSCPayload(
//
// See similar comment in create-component-tree.tsx for more context.
const isPossiblyPartialHead =
workStore.isStaticGeneration && ctx.renderOpts.cacheComponents === true
workStore.isStaticGeneration &&
ctx.renderOpts.experimental.isRoutePPREnabled === true
return maybeAppendBuildIdToRSCPayload(ctx, {
// See the comment above the `Preloads` component (below) for why this is part of the payload
@@ -2302,7 +2303,8 @@ async function getErrorRSCPayload(
)
const isPossiblyPartialHead =
workStore.isStaticGeneration && ctx.renderOpts.cacheComponents === true
workStore.isStaticGeneration &&
ctx.renderOpts.experimental.isRoutePPREnabled === true
return maybeAppendBuildIdToRSCPayload(ctx, {
c: prepareInitialCanonicalUrl(url),
@@ -2489,6 +2491,7 @@ function installGlobalModuleLoadingHandlers(
case 'cache':
case 'private-cache':
return true
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'unstable-cache':
@@ -3064,7 +3067,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
// We read these values from the request object as, in certain cases,
// base-server will strip them to opt into different rendering behavior.
const parsedRequestHeaders = parseRequestHeaders(req.headers, {
isRoutePPREnabled: renderOpts.cacheComponents === true,
isRoutePPREnabled: renderOpts.experimental.isRoutePPREnabled === true,
previewModeId: renderOpts.previewProps?.previewModeId,
})
@@ -7213,7 +7216,7 @@ async function prerenderToStream(
const { reactServerErrorsByDigest } = workStore
// We don't report errors during prerendering through our instrumentation hooks
const reportErrors = !cacheComponents
const reportErrors = !experimental.isRoutePPREnabled
function onHTMLRenderRSCError(err: DigestedError, silenceLog: boolean) {
if (reportErrors) {
return onInstrumentationRequestError?.(
@@ -8156,6 +8159,248 @@ async function prerenderToStream(
collectedTags: finalServerPrerenderStore.tags,
renderResumeDataCache: createRenderResumeDataCache(resumeDataCache),
}
} else if (experimental.isRoutePPREnabled) {
// We're statically generating with PPR and need to do dynamic tracking
let dynamicTracking = createDynamicTrackingState(isDebugDynamicAccesses)
const resumeDataCache = createPrerenderResumeDataCache()
const pprReactServerPrerenderStore: PrerenderStore = (prerenderStore = {
type: 'prerender-ppr',
phase: 'render',
rootParams,
fallbackRouteParams,
implicitTags,
dynamicTracking,
revalidate: INFINITE_CACHE,
expire: INFINITE_CACHE,
stale: INFINITE_CACHE,
tags: [...implicitTags.tags],
resumeDataCache,
})
const RSCPayload = await workUnitAsyncStorage.run(
pprReactServerPrerenderStore,
getRSCPayload,
tree,
ctx,
{ is404: res.statusCode === 404 }
)
let reactServerResult: ReactServerPrerenderResult
reactServerResult = reactServerPrerenderResult =
await createReactServerPrerenderResultFromRender(
workUnitAsyncStorage.run(
pprReactServerPrerenderStore,
renderFlightStream,
ComponentMod,
RSCPayload,
clientModules,
{
filterStackFrame,
onError: serverComponentsErrorHandler,
}
)
)
const ssrPrerenderStore: PrerenderStore = {
type: 'prerender-ppr',
phase: 'render',
rootParams,
fallbackRouteParams,
implicitTags,
dynamicTracking,
revalidate: INFINITE_CACHE,
expire: INFINITE_CACHE,
stale: INFINITE_CACHE,
tags: [...implicitTags.tags],
resumeDataCache,
}
const pprOnHeaders = createOnHeadersCallback(appendHeader)
const { prelude: unprocessedPrelude, postponed } =
await workUnitAsyncStorage.run(
ssrPrerenderStore,
getClientPrerender,
// eslint-disable-next-line @next/internal/no-ambiguous-jsx
<App
reactServerStream={reactServerResult.asUnclosingStream()}
reactDebugStream={undefined}
debugEndTime={undefined}
preinitScripts={preinitScripts}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
/>,
{
onError: htmlRendererErrorHandler,
onHeaders: pprOnHeaders,
maxHeadersLength: reactMaxHeadersLength,
bootstrapScripts: [bootstrapScript],
}
)
const getServerInsertedHTML = makeGetServerInsertedHTML({
polyfills,
renderServerInsertedHTML,
serverCapturedErrors: allCapturedErrors,
basePath,
tracingMetadata: tracingMetadata,
})
// After awaiting here we've waited for the entire RSC render to complete. Crucially this means
// that when we detect whether we've used dynamic APIs below we know we'll have picked up even
// parts of the React Server render that might not be used in the SSR render.
const flightData = await streamToBuffer(reactServerResult.asStream())
if (shouldGenerateStaticFlightData(workStore)) {
metadata.flightData = flightData
await collectSegmentData(
flightData,
ssrPrerenderStore,
ComponentMod,
renderOpts,
ctx.pagePath,
metadata
)
}
const { prelude, preludeIsEmpty } =
await processPreludeOp(unprocessedPrelude)
/**
* When prerendering there are three outcomes to consider
*
* Dynamic HTML: The prerender has dynamic holes (caused by using Next.js Dynamic Rendering APIs)
* We will need to resume this result when requests are handled and we don't include
* any server inserted HTML or inlined flight data in the static HTML
*
* Dynamic Data: The prerender has no dynamic holes but dynamic APIs were used. We will not
* resume this render when requests are handled but we will generate new inlined
* flight data since it is dynamic and differences may end up reconciling on the client
*
* Static: The prerender has no dynamic holes and no dynamic APIs were used. We statically encode
* all server inserted HTML and flight data
*/
// First we check if we have any dynamic holes in our HTML prerender
if (accessedDynamicData(dynamicTracking.dynamicAccesses)) {
if (postponed != null) {
// Dynamic HTML case.
metadata.postponed = await getDynamicHTMLPostponedState(
postponed,
preludeIsEmpty
? DynamicHTMLPreludeState.Empty
: DynamicHTMLPreludeState.Full,
fallbackRouteParams,
resumeDataCache,
cacheComponents
)
} else {
// Dynamic Data case.
metadata.postponed = await getDynamicDataPostponedState(
resumeDataCache,
cacheComponents
)
}
// Regardless of whether this is the Dynamic HTML or Dynamic Data case we need to ensure we include
// server inserted html in the static response because the html that is part of the prerender may depend on it
// It is possible in the set of stream transforms for Dynamic HTML vs Dynamic Data may differ but currently both states
// require the same set so we unify the code path here
reactServerResult.consume()
const pprDynamicOpts = {
getServerInsertedHTML,
getServerInsertedMetadata,
deploymentId: ctx.sharedContext.deploymentId,
}
return {
digestErrorsMap: reactServerErrorsByDigest,
ssrErrors: allCapturedErrors,
stream: await continueDynamicPrerender(prelude, pprDynamicOpts),
dynamicAccess: dynamicTracking.dynamicAccesses,
// TODO: Should this include the SSR pass?
collectedRevalidate: pprReactServerPrerenderStore.revalidate,
collectedExpire: pprReactServerPrerenderStore.expire,
collectedStale: selectStaleTime(pprReactServerPrerenderStore.stale),
collectedTags: pprReactServerPrerenderStore.tags,
}
} else if (fallbackRouteParams && fallbackRouteParams.size > 0) {
// Rendering the fallback case.
metadata.postponed = await getDynamicDataPostponedState(
resumeDataCache,
cacheComponents
)
const pprFallbackDynamicOpts = {
getServerInsertedHTML,
getServerInsertedMetadata,
deploymentId: ctx.sharedContext.deploymentId,
}
return {
digestErrorsMap: reactServerErrorsByDigest,
ssrErrors: allCapturedErrors,
stream: await continueDynamicPrerender(
prelude,
pprFallbackDynamicOpts
),
dynamicAccess: dynamicTracking.dynamicAccesses,
// TODO: Should this include the SSR pass?
collectedRevalidate: pprReactServerPrerenderStore.revalidate,
collectedExpire: pprReactServerPrerenderStore.expire,
collectedStale: selectStaleTime(pprReactServerPrerenderStore.stale),
collectedTags: pprReactServerPrerenderStore.tags,
}
} else {
// Static case
// We still have not used any dynamic APIs. At this point we can produce an entirely static prerender response
if (workStore.forceDynamic) {
throw new StaticGenBailoutError(
'Invariant: a Page with `dynamic = "force-dynamic"` did not trigger the dynamic pathway. This is a bug in Next.js'
)
}
let htmlStream: AnyStream = prelude
if (postponed != null) {
// We postponed but nothing dynamic was used. We resume the render now and immediately abort it
// so we can set all the postponed boundaries to client render mode before we store the HTML response
const foreverStream = createPendingStream()
const resumePrelude = await resumeAndAbort(
// eslint-disable-next-line @next/internal/no-ambiguous-jsx
<App
reactServerStream={foreverStream}
reactDebugStream={undefined}
debugEndTime={undefined}
preinitScripts={() => {}}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
images={ctx.renderOpts.images}
/>,
JSON.parse(JSON.stringify(postponed)),
{
signal: createRenderInBrowserAbortSignal(),
onError: htmlRendererErrorHandler,
nonce,
}
)
// First we write everything from the prerender, then we write everything from the aborted resume render
htmlStream = chainStreams(prelude, resumePrelude)
}
return {
digestErrorsMap: reactServerErrorsByDigest,
ssrErrors: allCapturedErrors,
stream: await continueStaticPrerender(htmlStream, {
inlinedDataStream: createInlinedDataStream(
reactServerResult.consumeAsStream(),
nonce,
formState
),
getServerInsertedHTML,
getServerInsertedMetadata,
deploymentId: ctx.sharedContext.deploymentId,
}),
dynamicAccess: dynamicTracking.dynamicAccesses,
// TODO: Should this include the SSR pass?
collectedRevalidate: pprReactServerPrerenderStore.revalidate,
collectedExpire: pprReactServerPrerenderStore.expire,
collectedStale: selectStaleTime(pprReactServerPrerenderStore.stale),
collectedTags: pprReactServerPrerenderStore.tags,
}
}
} else {
const prerenderLegacyStore: PrerenderStore = (prerenderStore = {
type: 'prerender-legacy',
@@ -119,7 +119,7 @@ async function createComponentTreeInternal(
isRoot: boolean
): Promise<CacheNodeSeedData> {
const {
renderOpts: { nextConfigOutput, cacheComponents },
renderOpts: { nextConfigOutput, experimental, cacheComponents },
workStore,
componentMod: {
createElement,
@@ -135,6 +135,7 @@ async function createComponentTreeInternal(
createServerParamsForServerSegment,
createPrerenderParamsForClientSegment,
serverHooks: { DynamicServerError },
Postpone,
},
pagePath,
getDynamicParamFromSegment,
@@ -263,15 +264,6 @@ async function createComponentTreeInternal(
: []
let dynamic = layoutOrPageMod?.dynamic
if (typeof dynamic === 'string') {
if (cacheComponents) {
// TODO: Remove this since this should be caught during compilation.
// Left as a guard in case usage of Postpone was attempted
throw new Error(
`Route segment config "dynamic" is not compatible with \`nextConfig.cacheComponents\`. Please remove it.`
)
}
}
if (nextConfigOutput === 'export') {
if (!dynamic || dynamic === 'auto') {
@@ -293,7 +285,10 @@ async function createComponentTreeInternal(
} else if (dynamic === 'force-dynamic') {
workStore.forceDynamic = true
if (workStore.isStaticGeneration) {
// TODO: (PPR) remove this bailout once PPR is the default
if (workStore.isStaticGeneration && !experimental.isRoutePPREnabled) {
// If the postpone API isn't available, we can't postpone the render and
// therefore we can't use the dynamic API.
const err = new DynamicServerError(
`Page with \`dynamic = "force-dynamic"\` won't be rendered statically.`
)
@@ -325,6 +320,7 @@ async function createComponentTreeInternal(
case 'prerender':
case 'prerender-runtime':
case 'prerender-legacy':
case 'prerender-ppr':
if (workUnitStore.revalidate > defaultRevalidate) {
workUnitStore.revalidate = defaultRevalidate
}
@@ -351,7 +347,7 @@ async function createComponentTreeInternal(
defaultRevalidate === 0 &&
// If the postpone API isn't available, we can't postpone the render and
// therefore we can't use the dynamic API.
!cacheComponents
!experimental.isRoutePPREnabled
) {
const dynamicUsageDescription = `revalidate: 0 configured ${segment}`
workStore.dynamicUsageDescription = dynamicUsageDescription
@@ -375,6 +371,7 @@ async function createComponentTreeInternal(
case 'prerender':
case 'prerender-runtime':
case 'prerender-legacy':
case 'prerender-ppr':
if (workUnitStore.stale > pageStaleTime) {
workUnitStore.stale = pageStaleTime
}
@@ -417,7 +414,7 @@ async function createComponentTreeInternal(
// For dynamic requests, this must always be `false` because dynamic responses
// are never partial.
const isPossiblyPartialResponse =
isStaticGeneration && cacheComponents === true
isStaticGeneration && experimental.isRoutePPREnabled === true
const LayoutOrPage: ComponentType<any> | undefined = layoutOrPageMod
? interopDefault(layoutOrPageMod)
@@ -579,7 +576,7 @@ async function createComponentTreeInternal(
// possible during both prefetches and dynamic navigations. But during
// the beta period, we should be clear about this trade off in our
// communications.
!cacheComponents
!experimental.isRoutePPREnabled
) {
// Don't prefetch this child. This will trigger a lazy fetch by the
// client router.
@@ -774,15 +771,43 @@ async function createComponentTreeInternal(
}
const Component = MaybeComponent
// If force-dynamic is used and the current render supports postponing, we
// replace it with a node that will postpone the render. This ensures that the
// postpone is invoked during the react render phase and not during the next
// render phase.
// @TODO this does not actually do what it seems like it would or should do. The idea is that
// if we are rendering in a force-dynamic mode and we can postpone we should only make the segments
// that ask for force-dynamic to be dynamic, allowing other segments to still prerender. However
// because this comes after the children traversal and the static generation store is mutated every segment
// along the parent path of a force-dynamic segment will hit this condition effectively making the entire
// render force-dynamic. We should refactor this function so that we can correctly track which segments
// need to be dynamic
if (
workStore.isStaticGeneration &&
workStore.forceDynamic &&
cacheComponents
experimental.isRoutePPREnabled
) {
// TODO: Remove this since this should be caught during compilation.
// Left as a guard in case usage of Postpone was attempted
throw new Error(
`Route segment config "dynamic" is not compatible with \`nextConfig.cacheComponents\`. Please remove it.`
return createSeedData(
ctx,
createElement(
Fragment,
{
key: cacheNodeKey,
},
createElement(Postpone, {
reason: 'dynamic = "force-dynamic" was used',
route: workStore.route,
}),
layerAssets
),
parallelRouteCacheNodeSeedData,
loadingData,
true,
isRuntimePrefetchable,
// force-dynamic postpones without rendering the component, so no params
// are accessed. The vary params are empty.
emptyVaryParamsAccumulator
)
}
@@ -1306,6 +1331,7 @@ function createSeedData(
case 'prerender':
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -29,6 +29,7 @@ import type {
PrerenderStoreModernServer,
} from '../app-render/work-unit-async-storage.external'
// Once postpone is in stable we should switch to importing the postpone export directly
import React from 'react'
import { DynamicServerError } from '../../client/components/hooks-server-context'
@@ -78,6 +79,8 @@ import {
} from './instant-validation/boundary-tracking'
import type { InstantValidationSampleTracking } from './instant-validation/instant-samples'
const hasPostpone = typeof React.unstable_postpone === 'function'
export type DynamicAccess = {
/**
* If debugging, this will contain the stack trace of where the dynamic access
@@ -179,6 +182,7 @@ export function markCurrentScopeAsDynamic(
// A private cache scope is already dynamic by definition.
return
case 'prerender-legacy':
case 'prerender-ppr':
case 'request':
case 'generate-static-params':
break
@@ -200,6 +204,12 @@ export function markCurrentScopeAsDynamic(
if (workUnitStore) {
switch (workUnitStore.type) {
case 'prerender-ppr':
return postponeWithTracking(
store.route,
expression,
workUnitStore.dynamicTracking
)
case 'prerender-legacy':
workUnitStore.revalidate = 0
@@ -271,6 +281,7 @@ export function trackDynamicDataInDynamicRender(workUnitStore: WorkUnitStore) {
case 'prerender':
case 'prerender-runtime':
case 'prerender-legacy':
case 'prerender-ppr':
case 'prerender-client':
case 'validation-client':
case 'generate-static-params':
@@ -368,6 +379,78 @@ export function abortAndThrowOnSynchronousRequestDataAccess(
)
}
/**
* This component will call `React.postpone` that throws the postponed error.
*/
type PostponeProps = {
reason: string
route: string
}
export function Postpone({ reason, route }: PostponeProps): never {
const prerenderStore = workUnitAsyncStorage.getStore()
const dynamicTracking =
prerenderStore && prerenderStore.type === 'prerender-ppr'
? prerenderStore.dynamicTracking
: null
postponeWithTracking(route, reason, dynamicTracking)
}
export function postponeWithTracking(
route: string,
expression: string,
dynamicTracking: null | DynamicTrackingState
): never {
assertPostpone()
if (dynamicTracking) {
dynamicTracking.dynamicAccesses.push({
// When we aren't debugging, we don't need to create another error for the
// stack trace.
stack: dynamicTracking.isDebugDynamicAccesses
? new Error().stack
: undefined,
expression,
})
}
React.unstable_postpone(createPostponeReason(route, expression))
}
function createPostponeReason(route: string, expression: string) {
return (
`Route ${route} needs to bail out of prerendering at this point because it used ${expression}. ` +
`React throws this special object to indicate where. It should not be caught by ` +
`your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`
)
}
export function isDynamicPostpone(err: unknown) {
if (
typeof err === 'object' &&
err !== null &&
typeof (err as any).message === 'string'
) {
return isDynamicPostponeReason((err as any).message)
}
return false
}
function isDynamicPostponeReason(reason: string) {
return (
reason.includes(
'needs to bail out of prerendering at this point because it used'
) &&
reason.includes(
'Learn more: https://nextjs.org/docs/messages/ppr-caught-error'
)
)
}
if (isDynamicPostponeReason(createPostponeReason('%%%', '^^^')) === false) {
throw new Error(
'Invariant: isDynamicPostpone misidentified a postpone reason. This is a bug in Next.js'
)
}
const NEXT_PRERENDER_INTERRUPTED = 'NEXT_PRERENDER_INTERRUPTED'
function createPrerenderInterruptedError(message: string): Error {
@@ -448,6 +531,18 @@ export function formatDynamicAPIAccesses(
})
}
function assertPostpone() {
if (!hasPostpone) {
throw new Error(
`Invariant: React.unstable_postpone is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`
)
}
}
/**
* This is a bit of a hack to allow us to abort a render using a Postpone instance instead of an Error which changes React's
* abort semantics slightly.
*/
export function createRenderInBrowserAbortSignal(): AbortSignal {
const controller = new AbortController()
controller.abort(new BailoutToCSRError('Render in Browser'))
@@ -505,6 +600,7 @@ export function createHangingInputAbortSignal(
return controller.signal
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
@@ -559,6 +655,17 @@ export function useDynamicRouteParams(expression: string) {
throw new InvariantError(
`\`${expression}\` was called from a Server Component. Next.js should be preventing ${expression} from being included in server components statically, but did not in this case.`
)
case 'prerender-ppr': {
const fallbackParams = workUnitStore.fallbackRouteParams
if (fallbackParams && fallbackParams.size > 0) {
return postponeWithTracking(
workStore.route,
expression,
workUnitStore.dynamicTracking
)
}
break
}
case 'validation-client': {
// Don't check fallbackRouteParams here. We handle params that weren't
// provided in the samples using a proxy that throws when accessed.
@@ -614,7 +721,8 @@ export function useDynamicSearchParams(expression: string) {
)
break
}
case 'prerender-legacy': {
case 'prerender-legacy':
case 'prerender-ppr': {
if (workStore.forceStatic) {
return
}
@@ -293,6 +293,7 @@ export async function decryptActionBoundArgs(
break
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
@@ -62,6 +62,7 @@ export { createMetadataComponents } from '../../lib/metadata/metadata'
export { RootLayoutBoundary } from '../../lib/framework/boundary-components'
export { preloadStyle, preloadFont, preconnect } from './rsc/preloads'
export { Postpone } from './rsc/postpone'
export { taintObjectReference } from './rsc/taint'
export {
collectSegmentData,
@@ -24,6 +24,7 @@ function getValidationBoundaryTracking(): ValidationBoundaryTracking | null {
return store.boundaryState
case 'prerender':
case 'prerender-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'prerender-runtime':
case 'request':
@@ -32,6 +32,7 @@ export function instrumentParamsForClientValidation<TPArams extends Params>(
case 'prerender-runtime':
case 'prerender-client':
case 'prerender-legacy':
case 'prerender-ppr':
case 'prerender':
case 'cache':
case 'request':
@@ -71,6 +72,7 @@ export function expectCompleteParamsInClientValidation(
case 'prerender-runtime':
case 'prerender-client':
case 'prerender-legacy':
case 'prerender-ppr':
case 'prerender':
case 'cache':
case 'request':
@@ -107,6 +109,7 @@ export function instrumentSearchParamsForClientValidation(
case 'prerender-runtime':
case 'prerender-client':
case 'prerender-legacy':
case 'prerender-ppr':
case 'prerender':
case 'cache':
case 'request':
@@ -42,6 +42,7 @@ function getExpectedSampleTracking(): InstantValidationSampleTracking {
case 'private-cache':
case 'unstable-cache':
case 'prerender-legacy':
case 'prerender-ppr':
case 'prerender-client':
case 'prerender':
case 'prerender-runtime':
@@ -0,0 +1,8 @@
/*
Files in the rsc directory are meant to be packaged as part of the RSC graph using next-app-loader.
*/
// When postpone is available in canary React we can switch to importing it directly
export { Postpone } from '../dynamic-rendering'
@@ -149,6 +149,11 @@ export interface RenderOptsPartial {
isPrefetch?: boolean
htmlLimitedBots: string | undefined
experimental: {
/**
* When true, it indicates that the current page supports partial
* prerendering.
*/
isRoutePPREnabled?: boolean
expireTime: number | undefined
staleTimes: ExperimentalConfig['staleTimes'] | undefined
clientTraceMetadata: string[] | undefined
@@ -135,6 +135,7 @@ export function getFlightStream<T>(
return responseOnNextTick
case 'prerender':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
@@ -134,7 +134,7 @@ export async function walkTreeWithFlightRouterState({
if (
isInsideSharedLayout &&
!cacheComponents &&
!experimental.isRoutePPREnabled &&
// If PPR is disabled, and this is a request for the route tree, then we
// never render any components. Only send the router state.
(parsedRequestHeaders.isRouteTreePrefetchRequest ||
@@ -11,7 +11,10 @@ import type { OpaqueFallbackRouteParams } from '../request/fallback-params'
// Share the instance module in the next-shared layer
import { workUnitAsyncStorageInstance } from './work-unit-async-storage-instance' with { 'turbopack-transition': 'next-shared' }
import type { ServerComponentsHmrCache } from '../response-cache'
import type { ResumeDataCache } from '../resume-data-cache/resume-data-cache'
import type {
PrerenderResumeDataCache,
ResumeDataCache,
} from '../resume-data-cache/resume-data-cache'
import type { Params } from '../request/params'
import type { ImplicitTags } from '../lib/implicit-tags'
import type { WorkStore } from './work-async-storage.external'
@@ -280,6 +283,25 @@ interface StaticPrerenderStoreCommon {
readonly fallbackRouteParams: OpaqueFallbackRouteParams | null
}
export interface PrerenderStorePPR
extends CommonWorkUnitStore,
RevalidateStore {
readonly type: 'prerender-ppr'
readonly rootParams: Params
readonly dynamicTracking: null | DynamicTrackingState
/**
* The set of unknown route parameters. Accessing these will be tracked as
* a dynamic access.
*/
readonly fallbackRouteParams: OpaqueFallbackRouteParams | null
/**
* The resume data cache for this prerender. Always mutable in PPR mode.
*/
resumeDataCache: PrerenderResumeDataCache
}
export interface PrerenderStoreLegacy
extends CommonWorkUnitStore,
RevalidateStore {
@@ -287,7 +309,10 @@ export interface PrerenderStoreLegacy
readonly rootParams: Params
}
export type PrerenderStore = PrerenderStoreLegacy | PrerenderStoreModern
export type PrerenderStore =
| PrerenderStoreLegacy
| PrerenderStorePPR
| PrerenderStoreModern
// /** Like `PrerenderStoreModern`, but only including static prerenders (i.e. not runtime prerenders) */
export type StaticPrerenderStore = Exclude<
@@ -423,6 +448,7 @@ export function getResumeDataCache(
case 'prerender-runtime':
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
return workUnitStore.resumeDataCache
case 'cache':
case 'private-cache':
@@ -449,6 +475,7 @@ export function getHmrRefreshHash(
return workUnitStore.cookies.get(NEXT_HMR_REFRESH_HASH_COOKIE)?.value
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'unstable-cache':
case 'generate-static-params':
@@ -472,6 +499,7 @@ export function isHmrRefresh(workUnitStore: WorkUnitStore): boolean {
case 'prerender-client':
case 'validation-client':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'unstable-cache':
case 'generate-static-params':
@@ -497,6 +525,7 @@ export function getServerComponentsHmrCache(
case 'prerender-client':
case 'validation-client':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'unstable-cache':
case 'generate-static-params':
@@ -527,6 +556,7 @@ export function getDraftModeProviderForCacheScope(
case 'prerender':
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'generate-static-params':
break
@@ -548,6 +578,7 @@ export function getStagedRenderingController(
return workUnitStore.stagedRendering ?? null
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -575,6 +606,7 @@ export function getCacheSignal(
}
// fallthrough
}
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -595,6 +627,7 @@ export function getVaryParamsAccumulator(
case 'request': {
return workUnitStore.varyParamsAccumulator ?? null
}
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -34,7 +34,7 @@ export type WorkStoreContext = {
pendingWaitUntil?: Promise<any>
experimental: Pick<
RenderOpts['experimental'],
'authInterrupts' | 'useCacheTimeout'
'isRoutePPREnabled' | 'authInterrupts' | 'useCacheTimeout'
>
/**
+7 -5
View File
@@ -134,6 +134,7 @@ import { toRoute } from './lib/to-route'
import type { DeepReadonly } from '../shared/lib/deep-readonly'
import { isNodeNextRequest, isNodeNextResponse } from './base-http/helpers'
import { patchSetHeaderWithCookieSupport } from './lib/patch-set-header'
import { checkIsAppPPREnabled } from './lib/experimental/ppr'
import {
getBuiltinRequestContext,
type WaitUntil,
@@ -431,7 +432,7 @@ export default abstract class Server<
readonly data: NextDataPathnameNormalizer | undefined
}
private readonly isAppCacheComponentsEnabled: boolean
private readonly isAppPPREnabled: boolean
/**
* This is used to persist cache scopes across
@@ -517,8 +518,9 @@ export default abstract class Server<
this.enabledDirectories = this.getEnabledDirectories(dev)
this.isAppCacheComponentsEnabled =
this.enabledDirectories.app && Boolean(this.nextConfig.cacheComponents)
this.isAppPPREnabled =
this.enabledDirectories.app &&
checkIsAppPPREnabled(this.nextConfig.experimental.ppr)
this.normalizers = {
// We should normalize the pathname from the RSC prefix only in minimal
@@ -1095,7 +1097,7 @@ export default abstract class Server<
// It's important to execute the following block even it the request
// matches a pages data route from above.
if (
this.isAppCacheComponentsEnabled &&
this.isAppPPREnabled &&
this.minimalMode &&
req.headers[NEXT_RESUME_HEADER] === '1' &&
req.method === 'POST'
@@ -2217,7 +2219,7 @@ export default abstract class Server<
* enabled, then the given route _could_ support PPR.
*/
const couldSupportPPR: boolean =
this.isAppCacheComponentsEnabled &&
this.isAppPPREnabled &&
typeof routeModule !== 'undefined' &&
isAppPageRouteModule(routeModule)
@@ -41,6 +41,7 @@ const zExportMap: zod.ZodType<ExportPathMap> = z.record(
_fallbackRouteParams: z.array(z.any()).optional(),
_isAppDir: z.boolean().optional(),
_isDynamicError: z.boolean().optional(),
_isRoutePPREnabled: z.boolean().optional(),
_allowEmptyStaticShell: z.boolean().optional(),
_isFallbackUpgradeable: z.boolean().optional(),
})
+16
View File
@@ -11,6 +11,7 @@ import type { WEB_VITALS } from '../shared/lib/utils'
import type { NextParsedUrlQuery } from './request-meta'
import type { SizeLimit } from '../types'
import type { SupportedTestRunners } from '../cli/next-test'
import type { ExperimentalPPRConfig } from './lib/experimental/ppr'
import { INFINITE_CACHE } from '../lib/constants'
import type { FallbackRouteParam } from '../build/static-paths/types'
import type { MemoryEvictionMode } from '../build/swc/types'
@@ -972,6 +973,12 @@ export interface ExperimentalConfig {
*/
clientTraceMetadata?: string[]
/**
* @deprecated This configuration option has been merged into `cacheComponents`.
* The Partial Prerendering feature is still available via `cacheComponents`.
*/
ppr?: ExperimentalPPRConfig
/**
* Enables experimental taint APIs in React.
* Using this feature will enable the `react@experimental` for the `app` directory.
@@ -1356,6 +1363,11 @@ export type ExportPathMap = {
*/
_isDynamicError?: boolean
/**
* @internal
*/
_isRoutePPREnabled?: boolean
/**
* When true, the page is prerendered as a fallback shell, while allowing
* any dynamic accesses to result in an empty shell. This is the case when
@@ -2076,6 +2088,7 @@ export const defaultConfig = Object.freeze({
clientTraceMetadata: undefined,
parallelServerCompiles: false,
parallelServerBuildTraces: false,
ppr: false,
authInterrupts: false,
webpackBuildWorker: undefined,
webpackMemoryOptimizations: false,
@@ -2140,6 +2153,7 @@ export async function normalizeConfig(phase: string, config: any) {
// cacheComponents?: boolean;
// clientParamParsingOrigins?: string[];
// clientSegmentCache?: boolean;
// ppr?: boolean | 'incremental';
// serverActions?: Record<string, never>;
// };
// };
@@ -2182,6 +2196,7 @@ export interface NextConfigRuntime {
experimental: Pick<
NextConfigComplete['experimental'],
| 'ppr'
| 'taint'
| 'serverActions'
| 'staleTimes'
@@ -2249,6 +2264,7 @@ export function getNextConfigRuntime(
}
const experimental = {
ppr: ex.ppr,
taint: ex.taint,
serverActions: ex.serverActions,
staleTimes: ex.staleTimes,
+6 -1
View File
@@ -575,7 +575,7 @@ function assignDefaultsAndValidate(
}
}
if ((result.experimental as any).ppr) {
if (result.experimental.ppr) {
throw new HardDeprecatedConfigError(
`\`experimental.ppr\` has been merged into \`cacheComponents\`. The Partial Prerendering feature is still available, but is now enabled via \`cacheComponents\`. Please update your ${configFileName} accordingly.`
)
@@ -1570,6 +1570,11 @@ function assignDefaultsAndValidate(
result.experimental.mcpServer = true
}
if (result.cacheComponents) {
// TODO: remove once we've finished migrating internally to cacheComponents.
result.experimental.ppr = true
}
// "use cache" was originally implicitly enabled with the cacheComponents flag, so
// we transfer the value for cacheComponents to the explicit useCache flag to ensure
// backwards compatibility.
@@ -72,6 +72,7 @@ import { DefaultFileReader } from '../route-matcher-providers/dev/helpers/file-r
import { LRUCache } from '../lib/lru-cache'
import { getMiddlewareRouteMatcher } from '../../shared/lib/router/utils/middleware-route-matcher'
import { DetachedPromise } from '../../lib/detached-promise'
import { isPostpone } from '../lib/router-utils/is-postpone'
import { generateInterceptionRoutesRewrites } from '../../lib/generate-interception-routes-rewrites'
import { buildCustomRoute } from '../../lib/build-custom-route'
import { decorateServerError } from '../../shared/lib/error-source'
@@ -344,6 +345,11 @@ export default class DevServer extends Server {
}
process.on('unhandledRejection', (reason) => {
if (isPostpone(reason)) {
// React postpones that are unhandled might end up logged here but they're
// not really errors. They're just part of rendering.
return
}
this.logErrorWithOriginalStack(reason, 'unhandledRejection')
})
process.on('uncaughtException', (err) => {
@@ -800,6 +806,7 @@ export default class DevServer extends Server {
distDir: this.distDir,
pathname,
config: {
pprConfig: this.nextConfig.experimental.ppr,
configFileName,
cacheComponents: Boolean(this.nextConfig.cacheComponents),
},
@@ -17,6 +17,10 @@ import { loadComponents } from '../load-components'
import { setHttpClientAndAgentOptions } from '../setup-http-agent-env'
import type { IncrementalCache } from '../lib/incremental-cache'
import { isAppPageRouteModule } from '../route-modules/checks'
import {
checkIsRoutePPREnabled,
type ExperimentalPPRConfig,
} from '../lib/experimental/ppr'
import { InvariantError } from '../../shared/lib/invariant-error'
import { collectRootParamKeys } from '../../build/segment-config/app/collect-root-param-keys'
import { buildAppStaticPaths } from '../../build/static-paths/app'
@@ -25,6 +29,7 @@ import { createIncrementalCache } from '../../export/helpers/create-incremental-
import { parseNormalizedAppRoute } from '../../shared/lib/router/routes/app'
type RuntimeConfig = {
pprConfig: ExperimentalPPRConfig | undefined
configFileName: string
cacheComponents: boolean
}
@@ -127,7 +132,8 @@ export async function loadStaticPaths({
}
const isRoutePPREnabled =
isAppPageRouteModule(routeModule) && Boolean(config.cacheComponents)
isAppPageRouteModule(routeModule) &&
checkIsRoutePPREnabled(config.pprConfig)
const rootParamKeys = collectRootParamKeys(routeModule)
@@ -251,6 +251,7 @@ export function applyOwnerStack(error: Error): Error {
case 'unstable-cache':
case 'request':
case 'prerender':
case 'prerender-ppr':
case 'prerender-legacy':
case 'prerender-runtime':
case 'prerender-client':
@@ -0,0 +1,52 @@
/**
* If set to `incremental`, only those leaf pages that export
* `experimental_ppr = true` will have partial prerendering enabled. If any
* page exports this value as `false` or does not export it at all will not
* have partial prerendering enabled. If set to a boolean, the options for
* `experimental_ppr` will be ignored.
*/
export type ExperimentalPPRConfig = boolean | 'incremental'
/**
* Returns true if partial prerendering is enabled for the application. It does
* not tell you if a given route has PPR enabled, as that requires analysis of
* the route's configuration.
*
* @see {@link checkIsRoutePPREnabled} - for checking if a specific route has PPR enabled.
*/
export function checkIsAppPPREnabled(
config: ExperimentalPPRConfig | undefined
): boolean {
// If the config is undefined, partial prerendering is disabled.
if (typeof config === 'undefined') return false
// If the config is a boolean, use it directly.
if (typeof config === 'boolean') return config
// If the config is a string, it must be 'incremental' to enable partial
// prerendering.
if (config === 'incremental') return true
return false
}
/**
* Returns true if partial prerendering is supported for the current page with
* the provided app configuration. If the application doesn't have partial
* prerendering enabled, this function will always return false. If you want to
* check if the application has partial prerendering enabled
*
* @see {@link checkIsAppPPREnabled} for checking if the application has PPR enabled.
*/
export function checkIsRoutePPREnabled(
config: ExperimentalPPRConfig | undefined
): boolean {
// If the config is undefined, partial prerendering is disabled.
if (typeof config === 'undefined') return false
// If the config is a boolean, use it directly.
if (typeof config === 'boolean') return config
return false
}
@@ -369,6 +369,7 @@ export function createPatchedFetcher(
// TODO: Stop accumulating tags in client prerender. (fallthrough)
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -411,6 +412,7 @@ export function createPatchedFetcher(
case 'prerender-client':
case 'validation-client':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
@@ -582,6 +584,7 @@ export function createPatchedFetcher(
)
}
break
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -714,6 +717,7 @@ export function createPatchedFetcher(
)
}
break
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -760,6 +764,7 @@ export function createPatchedFetcher(
case 'prerender-client':
case 'validation-client':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'unstable-cache':
case 'generate-static-params':
@@ -912,6 +917,7 @@ export function createPatchedFetcher(
)
}
// fallthrough
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -995,6 +1001,7 @@ export function createPatchedFetcher(
)
}
break
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -1124,6 +1131,7 @@ export function createPatchedFetcher(
)
}
break
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -1175,6 +1183,7 @@ export function createPatchedFetcher(
case 'private-cache':
case 'unstable-cache':
case 'prerender-legacy':
case 'prerender-ppr':
case 'generate-static-params':
break
default:
@@ -24,6 +24,7 @@ import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix'
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
import setupCompression from 'next/dist/compiled/compression'
import { signalFromNodeResponse } from '../web/spec-extension/adapters/next-request'
import { isPostpone } from './router-utils/is-postpone'
import { parseUrl as parseUrlUtil } from '../../shared/lib/router/utils/parse-url'
import {
@@ -794,6 +795,11 @@ export async function initialize(opts: {
type: 'uncaughtException' | 'unhandledRejection',
err: Error | undefined
) => {
if (isPostpone(err)) {
// React postpones that are unhandled might end up logged here but they're
// not really errors. They're just part of rendering.
return
}
if (type === 'unhandledRejection') {
Log.error('unhandledRejection: ', err)
} else if (type === 'uncaughtException') {
@@ -0,0 +1,9 @@
const REACT_POSTPONE_TYPE: symbol = Symbol.for('react.postpone')
export function isPostpone(error: any): boolean {
return (
typeof error === 'object' &&
error !== null &&
error.$$typeof === REACT_POSTPONE_TYPE
)
}
@@ -257,6 +257,7 @@ function patchConsoleMethod(methodName: InterceptableConsoleMethod): void {
}
// intentional fallthrough
case 'prerender-legacy':
case 'prerender-ppr':
case 'cache':
case 'unstable-cache':
case 'private-cache':
@@ -90,6 +90,7 @@ export function io(expression: string, type: SyncIOApiType) {
break
}
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -1,3 +1,5 @@
import { isPostpone } from '../lib/router-utils/is-postpone'
let _global = globalThis as typeof globalThis & {
nextInitializedProcessErrorHandlers?: boolean
}
@@ -54,6 +56,11 @@ export function installProcessErrorHandlers(
// Install a new handler to prevent the process from crashing.
process.on('unhandledRejection', (reason: unknown) => {
if (isPostpone(reason)) {
// React postpones that are unhandled might end up logged here but they're
// not really errors. They're just part of rendering.
return
}
// Immediately log the error.
// TODO: Ideally, if we knew that this error was triggered by application
// code, we would suppress it entirely without logging. We can't reliably
@@ -74,6 +81,9 @@ export function installProcessErrorHandlers(
// is unrelated to the late-awaiting pattern. However, for similar reasons,
// we still shouldn't crash the process. Just log it.
process.on('uncaughtException', (reason: unknown) => {
if (isPostpone(reason)) {
return
}
console.error(reason)
})
}
@@ -615,6 +615,7 @@ function filteringUnhandledRejectionHandler(
}
break
}
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -76,6 +76,7 @@ async function getCachedImageResponseArrayBuffer(
case 'prerender-runtime':
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'generate-static-params':
return renderImageResponseArrayBuffer(args)
@@ -4,6 +4,7 @@ import {
workUnitAsyncStorage,
} from '../app-render/work-unit-async-storage.external'
import {
postponeWithTracking,
throwToInterruptStaticGeneration,
trackDynamicDataInDynamicRender,
} from '../app-render/dynamic-rendering'
@@ -95,6 +96,14 @@ export function connection(): Promise<void> {
`${exportName} must not be used within a Client Component. Next.js should be preventing ${exportName} from being included in Client Components statically, but did not in this case.`
)
}
case 'prerender-ppr':
// We use React's postpone API to interrupt rendering here to create a
// dynamic hole
return postponeWithTracking(
workStore.route,
'connection',
workUnitStore.dynamicTracking
)
case 'prerender-legacy':
// We throw an error here to interrupt prerendering to mark the route
// as dynamic
@@ -16,6 +16,7 @@ import {
isInEarlyRenderStage,
} from '../app-render/work-unit-async-storage.external'
import {
postponeWithTracking,
throwToInterruptStaticGeneration,
trackDynamicDataInDynamicRender,
} from '../app-render/dynamic-rendering'
@@ -82,6 +83,14 @@ export function cookies(): Promise<ReadonlyRequestCookies> {
throw new InvariantError(
`${exportName} must not be used within a Client Component. Next.js should be preventing ${exportName} from being included in Client Components statically, but did not in this case.`
)
case 'prerender-ppr':
// We need track dynamic access here eagerly to keep continuity with
// how cookies has worked in PPR without cacheComponents.
return postponeWithTracking(
workStore.route,
callingExpression,
workUnitStore.dynamicTracking
)
case 'prerender-legacy':
// We track dynamic access here so we don't need to wrap the cookies
// in individual property access tracking.
@@ -12,6 +12,7 @@ import {
import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external'
import {
abortAndThrowOnSynchronousRequestDataAccess,
postponeWithTracking,
trackDynamicDataInDynamicRender,
} from '../app-render/dynamic-rendering'
import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-logger'
@@ -68,6 +69,7 @@ export function draftMode(): Promise<DraftMode> {
// Otherwise, we fall through to providing an empty draft mode.
// eslint-disable-next-line no-fallthrough
case 'prerender':
case 'prerender-ppr':
case 'prerender-legacy':
// Return empty draft mode
return createOrGetCachedDraftMode(null, workStore)
@@ -241,6 +243,12 @@ function trackDynamicDraftMode(expression: string, constructorOpt: Function) {
throw new InvariantError(
`${exportName} must not be used within a Client Component. Next.js should be preventing ${exportName} from being included in Client Components statically, but did not in this case.`
)
case 'prerender-ppr':
return postponeWithTracking(
workStore.route,
expression,
workUnitStore.dynamicTracking
)
case 'prerender-legacy':
workUnitStore.revalidate = 0
@@ -14,6 +14,7 @@ import {
isInEarlyRenderStage,
} from '../app-render/work-unit-async-storage.external'
import {
postponeWithTracking,
throwToInterruptStaticGeneration,
trackDynamicDataInDynamicRender,
} from '../app-render/dynamic-rendering'
@@ -81,6 +82,7 @@ export function headers(): Promise<ReadonlyHeaders> {
case 'validation-client':
case 'private-cache':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
break
@@ -105,6 +107,16 @@ export function headers(): Promise<ReadonlyHeaders> {
throw new InvariantError(
`${exportName} must not be used within a client component. Next.js should be preventing ${exportName} from being included in client components statically, but did not in this case.`
)
case 'prerender-ppr':
// PPR Prerender (no cacheComponents)
// We are prerendering with PPR. We need track dynamic access here eagerly
// to keep continuity with how headers has worked in PPR without cacheComponents.
// TODO consider switching the semantic to throw on property access instead
return postponeWithTracking(
workStore.route,
callingExpression,
workUnitStore.dynamicTracking
)
case 'prerender-legacy':
// Legacy Prerender
// We are in a legacy static generation mode while prerendering
+5
View File
@@ -5,6 +5,7 @@ import {
makeDevtoolsIOAwarePromise,
} from '../dynamic-rendering-utils'
import { RenderStage } from '../app-render/staged-rendering'
import { throwPrerenderPPRRemovedError } from '../../shared/lib/ppr-removed-error'
import { isRequestApiAllowedInCurrentPhase } from './utils'
// A fulfilled thenable that React can unwrap synchronously via `use()` without
@@ -66,6 +67,10 @@ export function io(): Promise<void> {
workStore.route,
'`io()`'
)
case 'prerender-ppr':
// Dead code to be removed when we eliminate legacy ppr code
throwPrerenderPPRRemovedError()
break
case 'cache':
case 'private-cache':
case 'unstable-cache':
@@ -10,8 +10,15 @@ import {
} from '../app-render/vary-params'
import { ReflectAdapter } from '../web/spec-extension/adapters/reflect'
import {
throwToInterruptStaticGeneration,
postponeWithTracking,
} from '../app-render/dynamic-rendering'
import {
workUnitAsyncStorage,
type PrerenderStorePPR,
type PrerenderStoreLegacy,
type StaticPrerenderStoreModern,
type StaticPrerenderStore,
throwInvariantForMissingStore,
@@ -49,6 +56,7 @@ export function createParamsFromClient(
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-client':
case 'prerender-ppr':
case 'prerender-legacy':
// Client params don't need additional vary tracking because by the
// time they reach the client, the access would have already been
@@ -139,6 +147,7 @@ export function createServerParamsForRoute(
if (workUnitStore) {
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-ppr':
case 'prerender-legacy':
return createStaticPrerenderParams(
underlyingParams,
@@ -203,6 +212,7 @@ export function createServerParamsForServerSegment(
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-client':
case 'prerender-ppr':
case 'prerender-legacy':
return createStaticPrerenderParams(
underlyingParams,
@@ -298,6 +308,7 @@ export function createPrerenderParamsForClientSegment(
'createPrerenderParamsForClientSegment should not be called inside generateStaticParams.'
)
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
break
@@ -381,6 +392,22 @@ function createStaticPrerenderParams(
}
break
}
case 'prerender-ppr': {
const fallbackParams = prerenderStore.fallbackRouteParams
if (fallbackParams) {
for (const key in underlyingParams) {
if (fallbackParams.has(key)) {
return makeErroringParams(
underlyingParams,
fallbackParams,
workStore,
prerenderStore
)
}
}
}
break
}
case 'prerender-legacy':
break
default:
@@ -733,6 +760,65 @@ function makeHangingParams(
return promise
}
function makeErroringParams(
underlyingParams: Params,
fallbackParams: OpaqueFallbackRouteParams,
workStore: WorkStore,
prerenderStore: PrerenderStorePPR | PrerenderStoreLegacy
): Promise<Params> {
const cachedParams = CachedParams.get(underlyingParams)
if (cachedParams) {
return cachedParams
}
const augmentedUnderlying = { ...underlyingParams }
// We don't use makeResolvedReactPromise here because params
// supports copying with spread and we don't want to unnecessarily
// instrument the promise with spreadable properties of ReactPromise.
const promise = Promise.resolve(augmentedUnderlying)
CachedParams.set(underlyingParams, promise)
Object.keys(underlyingParams).forEach((prop) => {
if (wellKnownProperties.has(prop)) {
// These properties cannot be shadowed because they need to be the
// true underlying value for Promises to work correctly at runtime
} else {
if (fallbackParams.has(prop)) {
Object.defineProperty(augmentedUnderlying, prop, {
get() {
const expression = describeStringPropertyAccess('params', prop)
// In most dynamic APIs we also throw if `dynamic = "error"` however
// for params is only dynamic when we're generating a fallback shell
// and even when `dynamic = "error"` we still support generating dynamic
// fallback shells
// TODO remove this comment when cacheComponents is the default since there
// will be no `dynamic = "error"`
if (prerenderStore.type === 'prerender-ppr') {
// PPR Prerender (no cacheComponents)
postponeWithTracking(
workStore.route,
expression,
prerenderStore.dynamicTracking
)
} else {
// Legacy Prerender
throwToInterruptStaticGeneration(
expression,
workStore,
prerenderStore
)
}
},
enumerable: true,
})
}
}
})
return promise
}
function makeUntrackedParams(underlyingParams: Params): Promise<Params> {
const cachedParams = CachedParams.get(underlyingParams)
if (cachedParams) {
+53 -1
View File
@@ -3,11 +3,17 @@ import {
type WorkStore,
} from '../app-render/work-async-storage.external'
import {
postponeWithTracking,
type DynamicTrackingState,
} from '../app-render/dynamic-rendering'
import {
throwInvariantForMissingStore,
workUnitAsyncStorage,
type PrerenderStoreLegacy,
type PrerenderStoreModernServer,
type PrerenderStorePPR,
} from '../app-render/work-unit-async-storage.external'
import {
makeHangingPromise,
@@ -27,6 +33,7 @@ export function createServerPathnameForMetadata(
if (workUnitStore) {
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-ppr':
case 'prerender-legacy': {
return createPrerenderPathname(
underlyingPathname,
@@ -84,7 +91,10 @@ export function createServerPathnameForMetadata(
function createPrerenderPathname(
underlyingPathname: string,
workStore: WorkStore,
prerenderStore: PrerenderStoreLegacy | PrerenderStoreModernServer
prerenderStore:
| PrerenderStoreLegacy
| PrerenderStorePPR
| PrerenderStoreModernServer
): Promise<string> {
switch (prerenderStore.type) {
case 'prerender': {
@@ -98,6 +108,13 @@ function createPrerenderPathname(
}
break
}
case 'prerender-ppr': {
const fallbackParams = prerenderStore.fallbackRouteParams
if (fallbackParams && fallbackParams.size > 0) {
return makeErroringPathname(workStore, prerenderStore.dynamicTracking)
}
break
}
case 'prerender-legacy':
break
default:
@@ -108,6 +125,41 @@ function createPrerenderPathname(
return Promise.resolve(underlyingPathname)
}
function makeErroringPathname<T>(
workStore: WorkStore,
dynamicTracking: null | DynamicTrackingState
): Promise<T> {
let reject: null | ((reason: unknown) => void) = null
const promise = new Promise<T>((_, re) => {
reject = re
})
const originalThen = promise.then.bind(promise)
// We instrument .then so that we can generate a tracking event only if you actually
// await this promise, not just that it is created.
promise.then = (onfulfilled, onrejected) => {
if (reject) {
try {
postponeWithTracking(
workStore.route,
'metadata relative url resolving',
dynamicTracking
)
} catch (error) {
reject(error)
reject = null
}
}
return originalThen(onfulfilled, onrejected)
}
// We wrap in a noop proxy to trick the runtime into thinking it
// isn't a native promise (it's not really). This is so that awaiting
// the promise will call the `then` property triggering the lazy postpone
return new Proxy(promise, {})
}
function createRenderPathname(underlyingPathname: string): Promise<string> {
return Promise.resolve(underlyingPathname)
}
@@ -1,4 +1,8 @@
import { InvariantError } from '../../shared/lib/invariant-error'
import {
postponeWithTracking,
throwToInterruptStaticGeneration,
} from '../app-render/dynamic-rendering'
import {
workAsyncStorage,
type WorkStore,
@@ -7,9 +11,11 @@ import {
workUnitAsyncStorage,
type PrerenderStoreLegacy,
type PrerenderStoreModernServer,
type PrerenderStorePPR,
} from '../app-render/work-unit-async-storage.external'
import { makeHangingPromise } from '../dynamic-rendering-utils'
import type { ParamValue } from './params'
import { describeStringPropertyAccess } from '../../shared/lib/utils/reflect-utils'
import { actionAsyncStorage } from '../app-render/action-async-storage.external'
import { accumulateRootVaryParam } from '../app-render/vary-params'
@@ -67,6 +73,7 @@ export function getRootParam(paramName: string): Promise<ParamValue> {
return Promise.resolve(workUnitStore.rootParams[paramName])
}
case 'prerender':
case 'prerender-ppr':
case 'prerender-legacy': {
return createPrerenderRootParamPromise(
paramName,
@@ -132,12 +139,16 @@ export function getRootParam(paramName: string): Promise<ParamValue> {
function createPrerenderRootParamPromise(
paramName: string,
workStore: WorkStore,
prerenderStore: PrerenderStoreLegacy | PrerenderStoreModernServer,
prerenderStore:
| PrerenderStorePPR
| PrerenderStoreLegacy
| PrerenderStoreModernServer,
apiName: string
): Promise<ParamValue> {
switch (prerenderStore.type) {
case 'prerender':
case 'prerender-legacy':
case 'prerender-ppr':
default:
}
@@ -159,6 +170,22 @@ function createPrerenderRootParamPromise(
}
break
}
case 'prerender-ppr': {
// We aren't in a cacheComponents prerender, but the param is a fallback,
// so we need to make an erroring params object which will postpone/error if you access it
if (
prerenderStore.fallbackRouteParams &&
prerenderStore.fallbackRouteParams.has(paramName)
) {
return makeErroringRootParamPromise(
paramName,
workStore,
prerenderStore,
apiName
)
}
break
}
case 'prerender-legacy': {
// legacy prerenders can't have fallback params
break
@@ -172,3 +199,36 @@ function createPrerenderRootParamPromise(
accumulateRootVaryParam(paramName)
return Promise.resolve(underlyingParams[paramName])
}
/** Deliberately async -- we want to create a rejected promise, not error synchronously. */
async function makeErroringRootParamPromise(
paramName: string,
workStore: WorkStore,
prerenderStore: PrerenderStorePPR | PrerenderStoreLegacy,
apiName: string
): Promise<ParamValue> {
const expression = describeStringPropertyAccess(apiName, paramName)
// In most dynamic APIs, we also throw if `dynamic = "error"`.
// However, root params are only dynamic when we're generating a fallback shell,
// and even with `dynamic = "error"` we still support generating dynamic fallback shells.
// TODO: remove this comment when cacheComponents is the default since there will be no `dynamic = "error"`
switch (prerenderStore.type) {
case 'prerender-ppr': {
return postponeWithTracking(
workStore.route,
expression,
prerenderStore.dynamicTracking
)
}
case 'prerender-legacy': {
return throwToInterruptStaticGeneration(
expression,
workStore,
prerenderStore
)
}
default: {
prerenderStore satisfies never
}
}
}
@@ -11,12 +11,14 @@ import {
import { ReflectAdapter } from '../web/spec-extension/adapters/reflect'
import {
throwToInterruptStaticGeneration,
postponeWithTracking,
annotateDynamicAccess,
} from '../app-render/dynamic-rendering'
import {
workUnitAsyncStorage,
type PrerenderStoreLegacy,
type PrerenderStorePPR,
type PrerenderStoreModern,
type PrerenderStoreModernRuntime,
type StaticPrerenderStore,
@@ -57,6 +59,7 @@ export function createSearchParamsFromClient(
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-client':
case 'prerender-ppr':
case 'prerender-legacy':
return createStaticPrerenderSearchParams(workStore, workUnitStore)
case 'validation-client': {
@@ -123,6 +126,7 @@ export function createServerSearchParamsForServerPage(
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-client':
case 'prerender-ppr':
case 'prerender-legacy':
return createStaticPrerenderSearchParams(workStore, workUnitStore)
case 'validation-client':
@@ -201,6 +205,7 @@ export function createPrerenderSearchParamsForClientPage(): Promise<SearchParams
throw new InvariantError(
'createPrerenderSearchParamsForClientPage should not be called inside generateStaticParams.'
)
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
return Promise.resolve({})
@@ -226,6 +231,7 @@ function createStaticPrerenderSearchParams(
case 'prerender-client':
// We are in a cacheComponents (PPR or otherwise) prerender
return makeHangingSearchParams(workStore, prerenderStore)
case 'prerender-ppr':
case 'prerender-legacy':
// We are in a legacy static generation and need to interrupt the
// prerender when search params are accessed.
@@ -415,7 +421,7 @@ function makeHangingSearchParams(
function makeErroringSearchParams(
workStore: WorkStore,
prerenderStore: PrerenderStoreLegacy
prerenderStore: PrerenderStoreLegacy | PrerenderStorePPR
): Promise<SearchParams> {
const cachedSearchParams = CachedSearchParams.get(workStore)
if (cachedSearchParams) {
@@ -445,6 +451,13 @@ function makeErroringSearchParams(
workStore.route,
expression
)
} else if (prerenderStore.type === 'prerender-ppr') {
// PPR Prerender (no cacheComponents)
postponeWithTracking(
workStore.route,
expression,
prerenderStore.dynamicTracking
)
} else {
// Legacy Prerender
throwToInterruptStaticGeneration(
@@ -60,6 +60,7 @@ import { StaticGenBailoutError } from '../../../client/components/static-generat
import { isStaticGenEnabled } from './helpers/is-static-gen-enabled'
import {
abortAndThrowOnSynchronousRequestDataAccess,
postponeWithTracking,
createDynamicTrackingState,
getFirstDynamicReason,
} from '../../app-render/dynamic-rendering'
@@ -1351,6 +1352,12 @@ function trackDynamic(
throw new InvariantError(
'A runtime prerender store should not be used for a route handler.'
)
case 'prerender-ppr':
return postponeWithTracking(
store.route,
expression,
workUnitStore.dynamicTracking
)
case 'prerender-legacy':
workUnitStore.revalidate = 0
@@ -88,6 +88,7 @@ export function cacheLife(profile: CacheLifeProfiles | CacheLife): void {
case 'prerender-client':
case 'validation-client':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'unstable-cache':
@@ -15,6 +15,7 @@ export function cacheTag(...tags: string[]): void {
case 'prerender-client':
case 'validation-client':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'unstable-cache':
@@ -83,6 +83,7 @@ import {
} from './use-cache-errors'
import {
createHangingInputAbortSignal,
postponeWithTracking,
throwToInterruptStaticGeneration,
} from '../app-render/dynamic-rendering'
import {
@@ -685,6 +686,7 @@ function createUseCacheStore(
break
case 'prerender-runtime':
case 'prerender':
case 'prerender-ppr':
case 'prerender-legacy':
case 'unstable-cache':
case 'generate-static-params':
@@ -744,6 +746,7 @@ function captureOuterOwnerStack(
case 'unstable-cache':
case 'request':
case 'prerender':
case 'prerender-ppr':
case 'prerender-legacy':
case 'prerender-runtime':
case 'prerender-client':
@@ -913,6 +916,7 @@ function propagateCacheEntryMetadata(
case 'private-cache':
case 'prerender':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
propagateCacheLifeAndTagsToRevalidateStore(
cacheContext.outerWorkUnitStore,
@@ -977,7 +981,8 @@ function maybePropagateCacheEntryMetadata(
case 'private-cache':
case 'cache':
case 'unstable-cache':
case 'prerender-legacy': {
case 'prerender-legacy':
case 'prerender-ppr': {
propagateCacheEntryMetadata(cacheContext, metadata)
break
}
@@ -1216,6 +1221,7 @@ async function generateCacheEntryImpl(
}
})
break
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
@@ -1471,6 +1477,7 @@ async function generateCacheEntryImpl(
}
}
// fallthrough
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -1716,6 +1723,12 @@ export async function cache(
workStore.route,
expression
)
case 'prerender-ppr':
return postponeWithTracking(
workStore.route,
expression,
workUnitStore.dynamicTracking
)
case 'prerender-legacy':
return throwToInterruptStaticGeneration(
expression,
@@ -1803,6 +1816,7 @@ export async function cache(
}
case 'prerender':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'private-cache':
@@ -2058,6 +2072,7 @@ export async function cache(
break
}
// fallthrough
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
// TODO(restart-on-cache-miss): We need to handle params/searchParams on page components.
@@ -2196,6 +2211,7 @@ export async function cache(
workStore.route,
'dynamic "use cache"'
)
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
@@ -2350,6 +2366,7 @@ export async function cache(
}
break
}
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -2403,6 +2420,7 @@ export async function cache(
}
break
}
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -2510,6 +2528,7 @@ export async function cache(
)
}
break
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
@@ -2872,6 +2891,7 @@ export async function cache(
break
}
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -2907,6 +2927,7 @@ export async function cache(
}
case 'prerender':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -3282,6 +3303,7 @@ function shouldForceRevalidate(
case 'prerender':
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'unstable-cache':
case 'generate-static-params':
@@ -3325,6 +3347,7 @@ function shouldDiscardCacheEntry(
case 'prerender-runtime':
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
+1
View File
@@ -315,6 +315,7 @@ export async function adapter(
// TODO: remove validationLevel and other global config from renderOpts
validationLevel: 'warning',
experimental: {
isRoutePPREnabled: false,
authInterrupts:
!!params.request.nextConfig?.experimental?.authInterrupts,
// Proxy doesn't fill Cache Components entries, so this value
@@ -1,4 +1,7 @@
import { abortAndThrowOnSynchronousRequestDataAccess } from '../../app-render/dynamic-rendering'
import {
abortAndThrowOnSynchronousRequestDataAccess,
postponeWithTracking,
} from '../../app-render/dynamic-rendering'
import { isDynamicRoute } from '../../../shared/lib/router/utils'
import {
NEXT_CACHE_IMPLICIT_TAG_ID,
@@ -170,6 +173,12 @@ function revalidate(
throw new InvariantError(
`${expression} must not be used within a client component. Next.js should be preventing ${expression} from being included in client components statically, but did not in this case.`
)
case 'prerender-ppr':
return postponeWithTracking(
store.route,
expression,
workUnitStore.dynamicTracking
)
case 'prerender-legacy':
workUnitStore.revalidate = 0
@@ -166,6 +166,7 @@ export function unstable_cache<T extends Callback>(
case 'private-cache':
case 'prerender':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
// We update the store's revalidate property if the option.revalidate is a higher precedence
// options.revalidate === undefined doesn't affect timing.
@@ -417,6 +418,7 @@ function getFetchUrlPrefix(
case 'prerender-client':
case 'validation-client':
case 'prerender-runtime':
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
@@ -38,6 +38,7 @@ export function unstable_noStore() {
case 'prerender-runtime':
// unstable_noStore() is a noop in Dynamic I/O.
return
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
@@ -0,0 +1,13 @@
import { InvariantError } from './invariant-error'
/**
* Throws an InvariantError indicating that a prerender-ppr code path was
* reached. The prerender-ppr work unit type has been removed and all code
* handling it is dead. Use this in exhaustive switch cases while the
* prerender-ppr type is being cleaned up.
*/
export function throwPrerenderPPRRemovedError(): never {
throw new InvariantError(
'The prerender-ppr work unit type has been removed. This code path should be unreachable.'
)
}
@@ -319,13 +319,13 @@ describe('Cache Components Errors - Client Hook Abort Reasons', () => {
at useSearchParams (webpack:///<next-src>)
at UseSearchParams (webpack:///app/client-hook-abort-reasons/client.tsx:27:18)
at Page (webpack:///app/client-hook-abort-reasons/normal/use-search-params/[id]/page.tsx:8:7)
607 | return
608 | case 'prerender-client': {
> 609 | React.use(
714 | return
715 | case 'prerender-client': {
> 716 | React.use(
| ^
610 | makeClientHookHangingPromise(
611 | workUnitStore.renderSignal,
612 | new ClientHookDynamicError(workStore.route, expression) {
717 | makeClientHookHangingPromise(
718 | workUnitStore.renderSignal,
719 | new ClientHookDynamicError(workStore.route, expression) {
digest: 'CLIENT_HOOK_DYNAMIC'
}
To debug the issue, start the app in development mode by running \`next dev\`, then open "/client-hook-abort-reasons/normal/use-search-params/[id]" in your browser to investigate the error.
@@ -613,13 +613,13 @@ describe('Cache Components Errors - Client Hook Abort Reasons', () => {
at usePathname (webpack:///<next-src>)
at UsePathname (webpack:///app/client-hook-abort-reasons/client.tsx:22:14)
at Page (webpack:///app/client-hook-abort-reasons/normal/use-pathname/[id]/page.tsx:7:7)
547 | // hang here and never resolve. This will cause the currently
548 | // rendering component to effectively be a dynamic hole.
> 549 | React.use(
643 | // hang here and never resolve. This will cause the currently
644 | // rendering component to effectively be a dynamic hole.
> 645 | React.use(
| ^
550 | makeClientHookHangingPromise(
551 | workUnitStore.renderSignal,
552 | new ClientHookDynamicError(workStore.route, expression) {
646 | makeClientHookHangingPromise(
647 | workUnitStore.renderSignal,
648 | new ClientHookDynamicError(workStore.route, expression) {
digest: 'CLIENT_HOOK_DYNAMIC'
}
To debug the issue, start the app in development mode by running \`next dev\`, then open "/client-hook-abort-reasons/normal/use-pathname/[id]" in your browser to investigate the error.
@@ -907,13 +907,13 @@ describe('Cache Components Errors - Client Hook Abort Reasons', () => {
at useParams (webpack:///<next-src>)
at UseParams (webpack:///app/client-hook-abort-reasons/client.tsx:17:12)
at Page (webpack:///app/client-hook-abort-reasons/normal/use-params/[id]/page.tsx:8:7)
547 | // hang here and never resolve. This will cause the currently
548 | // rendering component to effectively be a dynamic hole.
> 549 | React.use(
643 | // hang here and never resolve. This will cause the currently
644 | // rendering component to effectively be a dynamic hole.
> 645 | React.use(
| ^
550 | makeClientHookHangingPromise(
551 | workUnitStore.renderSignal,
552 | new ClientHookDynamicError(workStore.route, expression) {
646 | makeClientHookHangingPromise(
647 | workUnitStore.renderSignal,
648 | new ClientHookDynamicError(workStore.route, expression) {
digest: 'CLIENT_HOOK_DYNAMIC'
}
To debug the issue, start the app in development mode by running \`next dev\`, then open "/client-hook-abort-reasons/normal/use-params/[id]" in your browser to investigate the error.
@@ -1201,13 +1201,13 @@ describe('Cache Components Errors - Client Hook Abort Reasons', () => {
at useSelectedLayoutSegments (webpack:///<next-src>)
at UseSelectedLayoutSegments (webpack:///app/client-hook-abort-reasons/client.tsx:37:28)
at Page (webpack:///app/client-hook-abort-reasons/normal/use-selected-layout-segments/[id]/page.tsx:7:7)
547 | // hang here and never resolve. This will cause the currently
548 | // rendering component to effectively be a dynamic hole.
> 549 | React.use(
643 | // hang here and never resolve. This will cause the currently
644 | // rendering component to effectively be a dynamic hole.
> 645 | React.use(
| ^
550 | makeClientHookHangingPromise(
551 | workUnitStore.renderSignal,
552 | new ClientHookDynamicError(workStore.route, expression) {
646 | makeClientHookHangingPromise(
647 | workUnitStore.renderSignal,
648 | new ClientHookDynamicError(workStore.route, expression) {
digest: 'CLIENT_HOOK_DYNAMIC'
}
To debug the issue, start the app in development mode by running \`next dev\`, then open "/client-hook-abort-reasons/normal/use-selected-layout-segments/[id]" in your browser to investigate the error.
@@ -1495,13 +1495,13 @@ describe('Cache Components Errors - Client Hook Abort Reasons', () => {
at useSelectedLayoutSegment (webpack:///<next-src>)
at UseSelectedLayoutSegment (webpack:///app/client-hook-abort-reasons/client.tsx:32:27)
at Page (webpack:///app/client-hook-abort-reasons/normal/use-selected-layout-segment/[id]/page.tsx:8:7)
547 | // hang here and never resolve. This will cause the currently
548 | // rendering component to effectively be a dynamic hole.
> 549 | React.use(
643 | // hang here and never resolve. This will cause the currently
644 | // rendering component to effectively be a dynamic hole.
> 645 | React.use(
| ^
550 | makeClientHookHangingPromise(
551 | workUnitStore.renderSignal,
552 | new ClientHookDynamicError(workStore.route, expression) {
646 | makeClientHookHangingPromise(
647 | workUnitStore.renderSignal,
648 | new ClientHookDynamicError(workStore.route, expression) {
digest: 'CLIENT_HOOK_DYNAMIC'
}
To debug the issue, start the app in development mode by running \`next dev\`, then open "/client-hook-abort-reasons/normal/use-selected-layout-segment/[id]" in your browser to investigate the error.
@@ -2227,13 +2227,13 @@ describe('Cache Components Errors - Client Hook Abort Reasons', () => {
at useParams (webpack:///<next-src>)
at UseParams (webpack:///app/client-hook-abort-reasons/client.tsx:17:12)
at Page (webpack:///app/client-hook-abort-reasons/sync-io/use-params/[id]/page.tsx:7:7)
547 | // hang here and never resolve. This will cause the currently
548 | // rendering component to effectively be a dynamic hole.
> 549 | React.use(
643 | // hang here and never resolve. This will cause the currently
644 | // rendering component to effectively be a dynamic hole.
> 645 | React.use(
| ^
550 | makeClientHookHangingPromise(
551 | workUnitStore.renderSignal,
552 | new ClientHookDynamicError(workStore.route, expression) {
646 | makeClientHookHangingPromise(
647 | workUnitStore.renderSignal,
648 | new ClientHookDynamicError(workStore.route, expression) {
digest: 'CLIENT_HOOK_DYNAMIC'
}
To debug the issue, start the app in development mode by running \`next dev\`, then open "/client-hook-abort-reasons/sync-io/use-params/[id]" in your browser to investigate the error.
@@ -2739,13 +2739,13 @@ describe('Cache Components Errors - Client Hook Abort Reasons', () => {
at useSelectedLayoutSegment (webpack:///<next-src>)
at UseSelectedLayoutSegment (webpack:///app/client-hook-abort-reasons/client.tsx:32:27)
at Page (webpack:///app/client-hook-abort-reasons/sync-io/use-selected-layout-segment/[id]/page.tsx:8:7)
547 | // hang here and never resolve. This will cause the currently
548 | // rendering component to effectively be a dynamic hole.
> 549 | React.use(
643 | // hang here and never resolve. This will cause the currently
644 | // rendering component to effectively be a dynamic hole.
> 645 | React.use(
| ^
550 | makeClientHookHangingPromise(
551 | workUnitStore.renderSignal,
552 | new ClientHookDynamicError(workStore.route, expression) {
646 | makeClientHookHangingPromise(
647 | workUnitStore.renderSignal,
648 | new ClientHookDynamicError(workStore.route, expression) {
digest: 'CLIENT_HOOK_DYNAMIC'
}
To debug the issue, start the app in development mode by running \`next dev\`, then open "/client-hook-abort-reasons/sync-io/use-selected-layout-segment/[id]" in your browser to investigate the error.
@@ -0,0 +1,9 @@
import React from 'react'
export default function Root({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
@@ -0,0 +1,20 @@
import { Suspense } from 'react'
import { cookies } from 'next/headers'
export default async function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Foobar />
</Suspense>
)
}
async function Foobar() {
try {
await cookies()
} catch (err) {
console.log('User land logged error: ' + err.message)
}
await cookies() // still postpones so doesn't fail build
return null
}
@@ -0,0 +1,13 @@
import React from 'react'
import { cookies } from 'next/headers'
export default async function Page() {
try {
await cookies()
} catch (err) {
throw new Error(
"Throwing a new error from 'no-suspense-boundary-re-throwing-error'"
)
}
return <div>Hello World</div>
}
@@ -0,0 +1,9 @@
import React from 'react'
import { cookies } from 'next/headers'
export default async function Page() {
try {
await cookies()
} catch (err) {}
return <div>Hello World</div>
}
+17
View File
@@ -0,0 +1,17 @@
import React, { Suspense } from 'react'
import { cookies } from 'next/headers'
export default async function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Foobar />
</Suspense>
)
}
async function Foobar() {
try {
await cookies()
} catch (err) {}
return null
}
@@ -0,0 +1,19 @@
import React, { Suspense } from 'react'
import { cookies } from 'next/headers'
export default async function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Foobar />
</Suspense>
)
}
async function Foobar() {
try {
await cookies()
} catch (err) {
throw new Error('The original error was caught and rethrown.')
}
return null
}
@@ -0,0 +1,13 @@
import React, { Suspense } from 'react'
export default async function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Foobar />
</Suspense>
)
}
async function Foobar() {
throw new Error('Kaboom')
}
@@ -0,0 +1,3 @@
export default async function Page() {
throw new Error('Regular Prerender Error')
}
@@ -0,0 +1,9 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
cacheComponents: true,
experimental: { prerenderEarlyExit: false },
}
module.exports = nextConfig
@@ -0,0 +1,48 @@
import { isNextStart, nextTestSetup } from 'e2e-utils'
// TODO(NAR-423): Migrate to Cache Components.
describe.skip('ppr build errors', () => {
;(isNextStart ? describe : describe.skip)('production only', () => {
const { next } = nextTestSetup({
files: __dirname,
skipStart: true,
})
let cliOutput: string
beforeAll(async () => {
const output = await next.build()
cliOutput = output.cliOutput
})
describe('within a suspense boundary', () => {
it('should fail the build for uncaught prerender errors', async () => {
expect(cliOutput).toContain(
'Error occurred prerendering page "/regular-error-suspense-boundary".'
)
expect(cliOutput).toContain(
'Error occurred prerendering page "/re-throwing-error".'
)
})
})
describe('outside of a suspense boundary', () => {
it('should fail the build for uncaught errors', async () => {
expect(cliOutput).toContain(
'Error occurred prerendering page "/regular-error".'
)
expect(cliOutput).toContain(
'Error occurred prerendering page "/no-suspense-boundary-re-throwing-error".'
)
})
})
describe('when a postpone call is caught and logged it should', () => {
it('should include a message telling why', async () => {
expect(cliOutput).toContain(
'User land logged error: Route /logging-error needs to bail out of prerendering at this point because it used cookies.'
)
})
})
})
})
@@ -2,10 +2,10 @@ import { nextTestSetup } from 'e2e-utils'
// TODO(NAR-423): Migrate to Cache Components.
describe.skip('static-shell-debugging', () => {
const cacheComponents = Boolean(process.env.__NEXT_CACHE_COMPONENTS)
const ppr = Boolean(process.env.__NEXT_CACHE_COMPONENTS)
const context = {
cacheComponents,
debugging: cacheComponents,
ppr,
debugging: ppr,
}
const { next, skipped, isNextDev } = nextTestSetup({
@@ -19,13 +19,13 @@ describe.skip('static-shell-debugging', () => {
: undefined,
},
nextConfig: {
cacheComponents: context.cacheComponents,
experimental: { ppr: context.ppr },
},
})
if (skipped) return
if (context.debugging && context.cacheComponents) {
if (context.debugging && context.ppr) {
it('should only render the static shell', async () => {
const res = await next.fetch('/?__nextppronly=1')
expect(res.status).toBe(200)
@@ -159,7 +159,7 @@ describe('Config Experimental Warning', () => {
nextConfig: {
experimental: {
ppr: 'incremental',
} as any,
},
},
})
@@ -2,7 +2,7 @@ import React from 'react';
import { DynamicServerError } from '../../client/components/hooks-server-context';
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout';
import { getPathname } from '../../lib/url';
const hasUnnamed = typeof React.unstable_unnamed === 'function';
const hasPostpone = typeof React.unstable_postpone === 'function';
export function createPrerenderState(isDebugSkeleton) {
return {
isDebugSkeleton,
@@ -16,7 +16,7 @@ export function markCurrentScopeAsDynamic(store, expression) {
} else if (store.dynamicShouldError) {
throw new StaticGenBailoutError(`Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`);
} else if (store.prerenderState) {
markPrerenderState(store.prerenderState, expression, pathname);
postponeWithTracking(store.prerenderState, expression, pathname);
} else {
store.revalidate = 0;
if (store.isStaticGeneration) {
@@ -34,7 +34,7 @@ export function trackDynamicDataAccessed(store, expression) {
} else if (store.dynamicShouldError) {
throw new StaticGenBailoutError(`Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`);
} else if (store.prerenderState) {
markPrerenderState(store.prerenderState, expression, pathname);
postponeWithTracking(store.prerenderState, expression, pathname);
} else {
store.revalidate = 0;
if (store.isStaticGeneration) {
@@ -45,21 +45,21 @@ export function trackDynamicDataAccessed(store, expression) {
}
}
}
export function ApiThatShallNotBeNamed({ reason, prerenderState, pathname }) {
markPrerenderState(prerenderState, reason, pathname);
export function Postpone({ reason, prerenderState, pathname }) {
postponeWithTracking(prerenderState, reason, pathname);
}
export function trackDynamicFetch(store, expression) {
if (!store.prerenderState || store.isUnstableCacheCallback) return;
markPrerenderState(store.prerenderState, expression, store.urlPathname);
postponeWithTracking(store.prerenderState, expression, store.urlPathname);
}
function markPrerenderState(prerenderState, expression, pathname) {
assertUnnamed();
function postponeWithTracking(prerenderState, expression, pathname) {
assertPostpone();
const reason = `Route ${pathname} needs to bail out of prerendering at this point because it used ${expression}. ` + `React throws this special object to indicate where. It should not be caught by ` + `your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`;
prerenderState.dynamicAccesses.push({
stack: prerenderState.isDebugSkeleton ? new Error().stack : undefined,
expression
});
React.unstable_unnamed(reason);
React.unstable_postpone(reason);
}
export function usedDynamicAPIs(prerenderState) {
return prerenderState.dynamicAccesses.length > 0;
@@ -81,16 +81,16 @@ export function formatDynamicAPIAccesses(prerenderState) {
return `Dynamic API Usage Debug - ${expression}:\n${stack}`;
});
}
function assertUnnamed() {
if (!hasUnnamed) {
throw new Error(`Invariant: React.unstable_unnamed is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`);
function assertPostpone() {
if (!hasPostpone) {
throw new Error(`Invariant: React.unstable_postpone is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`);
}
}
export function createUnnamedAbortSignal(reason) {
assertUnnamed();
export function createPostponedAbortSignal(reason) {
assertPostpone();
const controller = new AbortController();
try {
React.unstable_unnamed(reason);
React.unstable_postpone(reason);
} catch (x) {
controller.abort(x);
}
@@ -85,14 +85,14 @@ import { getPathname } from '../../lib/url';
## Item 9: Stmt 4, `VarDeclarator(0)`
```js
const hasUnnamed = typeof React.unstable_unnamed === 'function';
const hasPostpone = typeof React.unstable_postpone === 'function';
```
- Side effects
- Declares: `hasUnnamed`
- Declares: `hasPostpone`
- Reads: `React`
- Write: `React`, `hasUnnamed`
- Write: `React`, `hasPostpone`
## Item 10: Stmt 5, `Normal`
@@ -120,7 +120,7 @@ export function markCurrentScopeAsDynamic(store, expression) {
} else if (store.dynamicShouldError) {
throw new StaticGenBailoutError(`Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`);
} else if (store.prerenderState) {
markPrerenderState(store.prerenderState, expression, pathname);
postponeWithTracking(store.prerenderState, expression, pathname);
} else {
store.revalidate = 0;
if (store.isStaticGeneration) {
@@ -136,7 +136,7 @@ export function markCurrentScopeAsDynamic(store, expression) {
- Hoisted
- Declares: `markCurrentScopeAsDynamic`
- Reads (eventual): `getPathname`, `StaticGenBailoutError`, `markPrerenderState`, `DynamicServerError`
- Reads (eventual): `getPathname`, `StaticGenBailoutError`, `postponeWithTracking`, `DynamicServerError`
- Write: `markCurrentScopeAsDynamic`
## Item 12: Stmt 7, `Normal`
@@ -149,7 +149,7 @@ export function trackDynamicDataAccessed(store, expression) {
} else if (store.dynamicShouldError) {
throw new StaticGenBailoutError(`Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`);
} else if (store.prerenderState) {
markPrerenderState(store.prerenderState, expression, pathname);
postponeWithTracking(store.prerenderState, expression, pathname);
} else {
store.revalidate = 0;
if (store.isStaticGeneration) {
@@ -165,57 +165,57 @@ export function trackDynamicDataAccessed(store, expression) {
- Hoisted
- Declares: `trackDynamicDataAccessed`
- Reads (eventual): `getPathname`, `StaticGenBailoutError`, `markPrerenderState`, `DynamicServerError`
- Reads (eventual): `getPathname`, `StaticGenBailoutError`, `postponeWithTracking`, `DynamicServerError`
- Write: `trackDynamicDataAccessed`
## Item 13: Stmt 8, `Normal`
```js
export function ApiThatShallNotBeNamed({ reason, prerenderState, pathname }) {
markPrerenderState(prerenderState, reason, pathname);
export function Postpone({ reason, prerenderState, pathname }) {
postponeWithTracking(prerenderState, reason, pathname);
}
```
- Hoisted
- Declares: `ApiThatShallNotBeNamed`
- Reads (eventual): `markPrerenderState`
- Write: `ApiThatShallNotBeNamed`
- Declares: `Postpone`
- Reads (eventual): `postponeWithTracking`
- Write: `Postpone`
## Item 14: Stmt 9, `Normal`
```js
export function trackDynamicFetch(store, expression) {
if (!store.prerenderState || store.isUnstableCacheCallback) return;
markPrerenderState(store.prerenderState, expression, store.urlPathname);
postponeWithTracking(store.prerenderState, expression, store.urlPathname);
}
```
- Hoisted
- Declares: `trackDynamicFetch`
- Reads (eventual): `markPrerenderState`
- Reads (eventual): `postponeWithTracking`
- Write: `trackDynamicFetch`
## Item 15: Stmt 10, `Normal`
```js
function markPrerenderState(prerenderState, expression, pathname) {
assertUnnamed();
function postponeWithTracking(prerenderState, expression, pathname) {
assertPostpone();
const reason = `Route ${pathname} needs to bail out of prerendering at this point because it used ${expression}. ` + `React throws this special object to indicate where. It should not be caught by ` + `your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`;
prerenderState.dynamicAccesses.push({
stack: prerenderState.isDebugSkeleton ? new Error().stack : undefined,
expression
});
React.unstable_unnamed(reason);
React.unstable_postpone(reason);
}
```
- Hoisted
- Declares: `markPrerenderState`
- Reads (eventual): `assertUnnamed`, `React`
- Write: `markPrerenderState`
- Declares: `postponeWithTracking`
- Reads (eventual): `assertPostpone`, `React`
- Write: `postponeWithTracking`
- Write (eventual): `React`
## Item 16: Stmt 11, `Normal`
@@ -261,27 +261,27 @@ export function formatDynamicAPIAccesses(prerenderState) {
## Item 18: Stmt 13, `Normal`
```js
function assertUnnamed() {
if (!hasUnnamed) {
throw new Error(`Invariant: React.unstable_unnamed is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`);
function assertPostpone() {
if (!hasPostpone) {
throw new Error(`Invariant: React.unstable_postpone is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`);
}
}
```
- Hoisted
- Declares: `assertUnnamed`
- Reads (eventual): `hasUnnamed`
- Write: `assertUnnamed`
- Declares: `assertPostpone`
- Reads (eventual): `hasPostpone`
- Write: `assertPostpone`
## Item 19: Stmt 14, `Normal`
```js
export function createUnnamedAbortSignal(reason) {
assertUnnamed();
export function createPostponedAbortSignal(reason) {
assertPostpone();
const controller = new AbortController();
try {
React.unstable_unnamed(reason);
React.unstable_postpone(reason);
} catch (x) {
controller.abort(x);
}
@@ -291,9 +291,9 @@ export function createUnnamedAbortSignal(reason) {
```
- Hoisted
- Declares: `createUnnamedAbortSignal`
- Reads (eventual): `assertUnnamed`, `React`
- Write: `createUnnamedAbortSignal`
- Declares: `createPostponedAbortSignal`
- Reads (eventual): `assertPostpone`, `React`
- Write: `createPostponedAbortSignal`
- Write (eventual): `React`
# Phase 1
@@ -325,7 +325,7 @@ graph TD
Item22;
Item22["export trackDynamicDataAccessed"];
Item23;
Item23["export ApiThatShallNotBeNamed"];
Item23["export Postpone"];
Item24;
Item24["export trackDynamicFetch"];
Item25;
@@ -333,7 +333,7 @@ graph TD
Item26;
Item26["export formatDynamicAPIAccesses"];
Item27;
Item27["export createUnnamedAbortSignal"];
Item27["export createPostponedAbortSignal"];
Item2 --> Item1;
Item3 --> Item2;
Item4 --> Item3;
@@ -367,7 +367,7 @@ graph TD
Item22;
Item22["export trackDynamicDataAccessed"];
Item23;
Item23["export ApiThatShallNotBeNamed"];
Item23["export Postpone"];
Item24;
Item24["export trackDynamicFetch"];
Item25;
@@ -375,7 +375,7 @@ graph TD
Item26;
Item26["export formatDynamicAPIAccesses"];
Item27;
Item27["export createUnnamedAbortSignal"];
Item27["export createPostponedAbortSignal"];
Item2 --> Item1;
Item3 --> Item2;
Item4 --> Item3;
@@ -424,7 +424,7 @@ graph TD
Item22;
Item22["export trackDynamicDataAccessed"];
Item23;
Item23["export ApiThatShallNotBeNamed"];
Item23["export Postpone"];
Item24;
Item24["export trackDynamicFetch"];
Item25;
@@ -432,7 +432,7 @@ graph TD
Item26;
Item26["export formatDynamicAPIAccesses"];
Item27;
Item27["export createUnnamedAbortSignal"];
Item27["export createPostponedAbortSignal"];
Item2 --> Item1;
Item3 --> Item2;
Item4 --> Item3;
@@ -498,7 +498,7 @@ graph TD
Item22;
Item22["export trackDynamicDataAccessed"];
Item23;
Item23["export ApiThatShallNotBeNamed"];
Item23["export Postpone"];
Item24;
Item24["export trackDynamicFetch"];
Item25;
@@ -506,7 +506,7 @@ graph TD
Item26;
Item26["export formatDynamicAPIAccesses"];
Item27;
Item27["export createUnnamedAbortSignal"];
Item27["export createPostponedAbortSignal"];
Item2 --> Item1;
Item3 --> Item2;
Item4 --> Item3;
@@ -558,11 +558,11 @@ graph TD
N9["Items: [ItemId(5, Normal), ItemId(Export((&quot;createPrerenderState&quot;, #2), &quot;createPrerenderState&quot;))]"];
N10["Items: [ItemId(6, Normal), ItemId(Export((&quot;markCurrentScopeAsDynamic&quot;, #2), &quot;markCurrentScopeAsDynamic&quot;))]"];
N11["Items: [ItemId(7, Normal), ItemId(Export((&quot;trackDynamicDataAccessed&quot;, #2), &quot;trackDynamicDataAccessed&quot;))]"];
N12["Items: [ItemId(8, Normal), ItemId(Export((&quot;ApiThatShallNotBeNamed&quot;, #2), &quot;ApiThatShallNotBeNamed&quot;))]"];
N12["Items: [ItemId(8, Normal), ItemId(Export((&quot;Postpone&quot;, #2), &quot;Postpone&quot;))]"];
N13["Items: [ItemId(9, Normal), ItemId(Export((&quot;trackDynamicFetch&quot;, #2), &quot;trackDynamicFetch&quot;))]"];
N14["Items: [ItemId(11, Normal), ItemId(Export((&quot;usedDynamicAPIs&quot;, #2), &quot;usedDynamicAPIs&quot;))]"];
N15["Items: [ItemId(12, Normal), ItemId(Export((&quot;formatDynamicAPIAccesses&quot;, #2), &quot;formatDynamicAPIAccesses&quot;))]"];
N16["Items: [ItemId(14, Normal), ItemId(Export((&quot;createUnnamedAbortSignal&quot;, #2), &quot;createUnnamedAbortSignal&quot;))]"];
N16["Items: [ItemId(14, Normal), ItemId(Export((&quot;createPostponedAbortSignal&quot;, #2), &quot;createPostponedAbortSignal&quot;))]"];
N2 --> N0;
N4 --> N2;
N6 --> N4;
@@ -594,14 +594,14 @@ graph TD
{
ModuleEvaluation: 8,
Export(
"ApiThatShallNotBeNamed",
"Postpone",
): 12,
Export(
"createPostponedAbortSignal",
): 16,
Export(
"createPrerenderState",
): 9,
Export(
"createUnnamedAbortSignal",
): 16,
Export(
"formatDynamicAPIAccesses",
): 15,
@@ -698,28 +698,28 @@ import "__TURBOPACK_PART__" assert {
import "__TURBOPACK_PART__" assert {
__turbopack_part__: 3
};
const hasUnnamed = typeof React.unstable_unnamed === 'function';
function markPrerenderState(prerenderState, expression, pathname) {
assertUnnamed();
const hasPostpone = typeof React.unstable_postpone === 'function';
function postponeWithTracking(prerenderState, expression, pathname) {
assertPostpone();
const reason = `Route ${pathname} needs to bail out of prerendering at this point because it used ${expression}. ` + `React throws this special object to indicate where. It should not be caught by ` + `your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`;
prerenderState.dynamicAccesses.push({
stack: prerenderState.isDebugSkeleton ? new Error().stack : undefined,
expression
});
React.unstable_unnamed(reason);
React.unstable_postpone(reason);
}
function assertUnnamed() {
if (!hasUnnamed) {
throw new Error(`Invariant: React.unstable_unnamed is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`);
function assertPostpone() {
if (!hasPostpone) {
throw new Error(`Invariant: React.unstable_postpone is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`);
}
}
export { hasUnnamed as a } from "__TURBOPACK_VAR__" assert {
export { hasPostpone as a } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
export { markPrerenderState as b } from "__TURBOPACK_VAR__" assert {
export { postponeWithTracking as b } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
export { assertUnnamed as c } from "__TURBOPACK_VAR__" assert {
export { assertPostpone as c } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
export { };
@@ -753,7 +753,7 @@ import "__TURBOPACK_PART__" assert {
__turbopack_part__: 4
};
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout';
import { b as markPrerenderState } from "__TURBOPACK_PART__" assert {
import { b as postponeWithTracking } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -8
};
function markCurrentScopeAsDynamic(store, expression) {
@@ -763,7 +763,7 @@ function markCurrentScopeAsDynamic(store, expression) {
} else if (store.dynamicShouldError) {
throw new StaticGenBailoutError(`Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`);
} else if (store.prerenderState) {
markPrerenderState(store.prerenderState, expression, pathname);
postponeWithTracking(store.prerenderState, expression, pathname);
} else {
store.revalidate = 0;
if (store.isStaticGeneration) {
@@ -794,7 +794,7 @@ import "__TURBOPACK_PART__" assert {
__turbopack_part__: 4
};
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout';
import { b as markPrerenderState } from "__TURBOPACK_PART__" assert {
import { b as postponeWithTracking } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -8
};
function trackDynamicDataAccessed(store, expression) {
@@ -804,7 +804,7 @@ function trackDynamicDataAccessed(store, expression) {
} else if (store.dynamicShouldError) {
throw new StaticGenBailoutError(`Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`);
} else if (store.prerenderState) {
markPrerenderState(store.prerenderState, expression, pathname);
postponeWithTracking(store.prerenderState, expression, pathname);
} else {
store.revalidate = 0;
if (store.isStaticGeneration) {
@@ -823,26 +823,26 @@ export { trackDynamicDataAccessed as f } from "__TURBOPACK_VAR__" assert {
```
## Part 12
```js
import { b as markPrerenderState } from "__TURBOPACK_PART__" assert {
import { b as postponeWithTracking } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -8
};
function ApiThatShallNotBeNamed({ reason, prerenderState, pathname }) {
markPrerenderState(prerenderState, reason, pathname);
function Postpone({ reason, prerenderState, pathname }) {
postponeWithTracking(prerenderState, reason, pathname);
}
export { ApiThatShallNotBeNamed };
export { ApiThatShallNotBeNamed as g } from "__TURBOPACK_VAR__" assert {
export { Postpone };
export { Postpone as g } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
```
## Part 13
```js
import { b as markPrerenderState } from "__TURBOPACK_PART__" assert {
import { b as postponeWithTracking } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -8
};
function trackDynamicFetch(store, expression) {
if (!store.prerenderState || store.isUnstableCacheCallback) return;
markPrerenderState(store.prerenderState, expression, store.urlPathname);
postponeWithTracking(store.prerenderState, expression, store.urlPathname);
}
export { trackDynamicFetch };
export { trackDynamicFetch as h } from "__TURBOPACK_VAR__" assert {
@@ -892,21 +892,21 @@ import "__TURBOPACK_PART__" assert {
__turbopack_part__: 0
};
import React from 'react';
import { c as assertUnnamed } from "__TURBOPACK_PART__" assert {
import { c as assertPostpone } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -8
};
function createUnnamedAbortSignal(reason) {
assertUnnamed();
function createPostponedAbortSignal(reason) {
assertPostpone();
const controller = new AbortController();
try {
React.unstable_unnamed(reason);
React.unstable_postpone(reason);
} catch (x) {
controller.abort(x);
}
return controller.signal;
}
export { createUnnamedAbortSignal };
export { createUnnamedAbortSignal as k } from "__TURBOPACK_VAR__" assert {
export { createPostponedAbortSignal };
export { createPostponedAbortSignal as k } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
@@ -922,8 +922,8 @@ export { markCurrentScopeAsDynamic } from "__TURBOPACK_PART__" assert {
export { trackDynamicDataAccessed } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export trackDynamicDataAccessed"
};
export { ApiThatShallNotBeNamed } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export ApiThatShallNotBeNamed"
export { Postpone } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export Postpone"
};
export { trackDynamicFetch } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export trackDynamicFetch"
@@ -934,8 +934,8 @@ export { usedDynamicAPIs } from "__TURBOPACK_PART__" assert {
export { formatDynamicAPIAccesses } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export formatDynamicAPIAccesses"
};
export { createUnnamedAbortSignal } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export createUnnamedAbortSignal"
export { createPostponedAbortSignal } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export createPostponedAbortSignal"
};
```
@@ -957,28 +957,28 @@ import "__TURBOPACK_PART__" assert {
import "__TURBOPACK_PART__" assert {
__turbopack_part__: 3
};
const hasUnnamed = typeof React.unstable_unnamed === 'function';
function markPrerenderState(prerenderState, expression, pathname) {
assertUnnamed();
const hasPostpone = typeof React.unstable_postpone === 'function';
function postponeWithTracking(prerenderState, expression, pathname) {
assertPostpone();
const reason = `Route ${pathname} needs to bail out of prerendering at this point because it used ${expression}. ` + `React throws this special object to indicate where. It should not be caught by ` + `your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`;
prerenderState.dynamicAccesses.push({
stack: prerenderState.isDebugSkeleton ? new Error().stack : undefined,
expression
});
React.unstable_unnamed(reason);
React.unstable_postpone(reason);
}
function assertUnnamed() {
if (!hasUnnamed) {
throw new Error(`Invariant: React.unstable_unnamed is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`);
function assertPostpone() {
if (!hasPostpone) {
throw new Error(`Invariant: React.unstable_postpone is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`);
}
}
export { hasUnnamed as a } from "__TURBOPACK_VAR__" assert {
export { hasPostpone as a } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
export { markPrerenderState as b } from "__TURBOPACK_VAR__" assert {
export { postponeWithTracking as b } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
export { assertUnnamed as c } from "__TURBOPACK_VAR__" assert {
export { assertPostpone as c } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
export { };
@@ -990,14 +990,14 @@ export { };
{
ModuleEvaluation: 8,
Export(
"ApiThatShallNotBeNamed",
"Postpone",
): 12,
Export(
"createPostponedAbortSignal",
): 18,
Export(
"createPrerenderState",
): 9,
Export(
"createUnnamedAbortSignal",
): 18,
Export(
"formatDynamicAPIAccesses",
): 16,
@@ -1085,8 +1085,8 @@ import React from 'react';
import "__TURBOPACK_PART__" assert {
__turbopack_part__: 6
};
const hasUnnamed = typeof React.unstable_unnamed === 'function';
export { hasUnnamed as a } from "__TURBOPACK_VAR__" assert {
const hasPostpone = typeof React.unstable_postpone === 'function';
export { hasPostpone as a } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
export { };
@@ -1120,7 +1120,7 @@ import "__TURBOPACK_PART__" assert {
__turbopack_part__: 4
};
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout';
import { c as markPrerenderState } from "__TURBOPACK_PART__" assert {
import { c as postponeWithTracking } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -14
};
function markCurrentScopeAsDynamic(store, expression) {
@@ -1130,7 +1130,7 @@ function markCurrentScopeAsDynamic(store, expression) {
} else if (store.dynamicShouldError) {
throw new StaticGenBailoutError(`Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`);
} else if (store.prerenderState) {
markPrerenderState(store.prerenderState, expression, pathname);
postponeWithTracking(store.prerenderState, expression, pathname);
} else {
store.revalidate = 0;
if (store.isStaticGeneration) {
@@ -1161,7 +1161,7 @@ import "__TURBOPACK_PART__" assert {
__turbopack_part__: 4
};
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout';
import { c as markPrerenderState } from "__TURBOPACK_PART__" assert {
import { c as postponeWithTracking } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -14
};
function trackDynamicDataAccessed(store, expression) {
@@ -1171,7 +1171,7 @@ function trackDynamicDataAccessed(store, expression) {
} else if (store.dynamicShouldError) {
throw new StaticGenBailoutError(`Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`);
} else if (store.prerenderState) {
markPrerenderState(store.prerenderState, expression, pathname);
postponeWithTracking(store.prerenderState, expression, pathname);
} else {
store.revalidate = 0;
if (store.isStaticGeneration) {
@@ -1190,26 +1190,26 @@ export { trackDynamicDataAccessed as e } from "__TURBOPACK_VAR__" assert {
```
## Part 12
```js
import { c as markPrerenderState } from "__TURBOPACK_PART__" assert {
import { c as postponeWithTracking } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -14
};
function ApiThatShallNotBeNamed({ reason, prerenderState, pathname }) {
markPrerenderState(prerenderState, reason, pathname);
function Postpone({ reason, prerenderState, pathname }) {
postponeWithTracking(prerenderState, reason, pathname);
}
export { ApiThatShallNotBeNamed };
export { ApiThatShallNotBeNamed as f } from "__TURBOPACK_VAR__" assert {
export { Postpone };
export { Postpone as f } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
```
## Part 13
```js
import { c as markPrerenderState } from "__TURBOPACK_PART__" assert {
import { c as postponeWithTracking } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -14
};
function trackDynamicFetch(store, expression) {
if (!store.prerenderState || store.isUnstableCacheCallback) return;
markPrerenderState(store.prerenderState, expression, store.urlPathname);
postponeWithTracking(store.prerenderState, expression, store.urlPathname);
}
export { trackDynamicFetch };
export { trackDynamicFetch as g } from "__TURBOPACK_VAR__" assert {
@@ -1223,22 +1223,22 @@ import "__TURBOPACK_PART__" assert {
__turbopack_part__: 0
};
import React from 'react';
import { h as assertUnnamed } from "__TURBOPACK_PART__" assert {
import { h as assertPostpone } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -17
};
import "__TURBOPACK_PART__" assert {
__turbopack_part__: 8
};
function markPrerenderState(prerenderState, expression, pathname) {
assertUnnamed();
function postponeWithTracking(prerenderState, expression, pathname) {
assertPostpone();
const reason = `Route ${pathname} needs to bail out of prerendering at this point because it used ${expression}. ` + `React throws this special object to indicate where. It should not be caught by ` + `your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`;
prerenderState.dynamicAccesses.push({
stack: prerenderState.isDebugSkeleton ? new Error().stack : undefined,
expression
});
React.unstable_unnamed(reason);
React.unstable_postpone(reason);
}
export { markPrerenderState as c } from "__TURBOPACK_VAR__" assert {
export { postponeWithTracking as c } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
@@ -1281,15 +1281,15 @@ export { formatDynamicAPIAccesses as j } from "__TURBOPACK_VAR__" assert {
```
## Part 17
```js
import { a as hasUnnamed } from "__TURBOPACK_PART__" assert {
import { a as hasPostpone } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -8
};
function assertUnnamed() {
if (!hasUnnamed) {
throw new Error(`Invariant: React.unstable_unnamed is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`);
function assertPostpone() {
if (!hasPostpone) {
throw new Error(`Invariant: React.unstable_postpone is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js`);
}
}
export { assertUnnamed as h } from "__TURBOPACK_VAR__" assert {
export { assertPostpone as h } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
@@ -1300,24 +1300,24 @@ import "__TURBOPACK_PART__" assert {
__turbopack_part__: 0
};
import React from 'react';
import { h as assertUnnamed } from "__TURBOPACK_PART__" assert {
import { h as assertPostpone } from "__TURBOPACK_PART__" assert {
__turbopack_part__: -17
};
import "__TURBOPACK_PART__" assert {
__turbopack_part__: 8
};
function createUnnamedAbortSignal(reason) {
assertUnnamed();
function createPostponedAbortSignal(reason) {
assertPostpone();
const controller = new AbortController();
try {
React.unstable_unnamed(reason);
React.unstable_postpone(reason);
} catch (x) {
controller.abort(x);
}
return controller.signal;
}
export { createUnnamedAbortSignal };
export { createUnnamedAbortSignal as k } from "__TURBOPACK_VAR__" assert {
export { createPostponedAbortSignal };
export { createPostponedAbortSignal as k } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
@@ -1333,8 +1333,8 @@ export { markCurrentScopeAsDynamic } from "__TURBOPACK_PART__" assert {
export { trackDynamicDataAccessed } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export trackDynamicDataAccessed"
};
export { ApiThatShallNotBeNamed } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export ApiThatShallNotBeNamed"
export { Postpone } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export Postpone"
};
export { trackDynamicFetch } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export trackDynamicFetch"
@@ -1345,8 +1345,8 @@ export { usedDynamicAPIs } from "__TURBOPACK_PART__" assert {
export { formatDynamicAPIAccesses } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export formatDynamicAPIAccesses"
};
export { createUnnamedAbortSignal } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export createUnnamedAbortSignal"
export { createPostponedAbortSignal } from "__TURBOPACK_PART__" assert {
__turbopack_part__: "export createPostponedAbortSignal"
};
```
@@ -1359,8 +1359,8 @@ import React from 'react';
import "__TURBOPACK_PART__" assert {
__turbopack_part__: 6
};
const hasUnnamed = typeof React.unstable_unnamed === 'function';
export { hasUnnamed as a } from "__TURBOPACK_VAR__" assert {
const hasPostpone = typeof React.unstable_postpone === 'function';
export { hasPostpone as a } from "__TURBOPACK_VAR__" assert {
__turbopack_var__: true
};
export { };