/**
* The functions provided by this module are used to communicate certain properties
* about the currently running code so that Next.js can make decisions on how to handle
* the current execution in different rendering modes such as pre-rendering, resuming, and SSR.
*
* Today Next.js treats all code as potentially static. Certain APIs may only make sense when dynamically rendering.
* Traditionally this meant deopting the entire render to dynamic however with PPR we can now deopt parts
* of a React tree as dynamic while still keeping other parts static. There are really two different kinds of
* Dynamic indications.
*
* The first is simply an intention to be dynamic. unstable_noStore is an example of this where
* the currently executing code simply declares that the current scope is dynamic but if you use it
* inside unstable_cache it can still be cached. This type of indication can be removed if we ever
* make the default dynamic to begin with because the only way you would ever be static is inside
* a cache scope which this indication does not affect.
*
* The second is an indication that a dynamic data source was read. This is a stronger form of dynamic
* because it means that it is inappropriate to cache this at all. using a dynamic data source inside
* unstable_cache should error. If you want to use some dynamic data inside unstable_cache you should
* read that data outside the cache and pass it in as an argument to the cached function.
*/
import type { WorkStore } from '../app-render/work-async-storage.external'
import type {
WorkUnitStore,
PrerenderStoreLegacy,
PrerenderStoreModern,
ValidationStoreClient,
} 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'
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
import {
throwForMissingRequestStore,
workUnitAsyncStorage,
} from './work-unit-async-storage.external'
import { workAsyncStorage } from '../app-render/work-async-storage.external'
import { makeHangingPromise, getRuntimeStage } from '../dynamic-rendering-utils'
import {
METADATA_BOUNDARY_NAME,
VIEWPORT_BOUNDARY_NAME,
OUTLET_BOUNDARY_NAME,
ROOT_LAYOUT_BOUNDARY_NAME,
} from '../../lib/framework/boundary-constants'
import { scheduleOnNextTick } from '../../lib/scheduler'
import { BailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
import {
createRuntimeBodyError,
createDynamicBodyError,
createDynamicOrRuntimeBodyError,
createRuntimeMetadataError,
createDynamicMetadataError,
createRuntimeViewportError,
createDynamicViewportError,
disallowedDynamicViewportMessage,
disallowedDynamicMetadataMessage,
logBuildDebugHint,
} from './blocking-route-messages'
import { InvariantError } from '../../shared/lib/invariant-error'
import {
INSTANT_VALIDATION_BOUNDARY_NAME,
INSTANT_SLOT_MARKER_PREFIX,
INSTANT_SLOT_MARKER_SUFFIX,
} from './instant-validation/boundary-constants'
import {
type ValidationBoundaryTracking,
allRequiredBoundariesRendered,
} 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
* occurred. This is used to provide more information to the user about why
* their page is being rendered dynamically.
*/
stack?: string
/**
* The expression that was accessed dynamically.
*/
expression: string
}
// Stores dynamic reasons used during an RSC render.
export type DynamicTrackingState = {
/**
* When true, stack information will also be tracked during dynamic access.
*/
readonly isDebugDynamicAccesses: boolean | undefined
/**
* The dynamic accesses that occurred during the render.
*/
readonly dynamicAccesses: Array<DynamicAccess>
syncDynamicErrorWithStack: null | Error
}
// Stores dynamic reasons used during an SSR render.
export type DynamicValidationState = {
hasSuspenseAboveBody: boolean
hasDynamicMetadata: boolean
dynamicMetadata: null | Error
hasDynamicViewport: boolean
hasAllowedDynamic: boolean
dynamicErrors: Array<Error>
}
export function createDynamicTrackingState(
isDebugDynamicAccesses: boolean | undefined
): DynamicTrackingState {
return {
isDebugDynamicAccesses,
dynamicAccesses: [],
syncDynamicErrorWithStack: null,
}
}
export function createDynamicValidationState(): DynamicValidationState {
return {
hasSuspenseAboveBody: false,
hasDynamicMetadata: false,
dynamicMetadata: null,
hasDynamicViewport: false,
hasAllowedDynamic: false,
dynamicErrors: [],
}
}
export function getFirstDynamicReason(
trackingState: DynamicTrackingState
): undefined | string {
return trackingState.dynamicAccesses[0]?.expression
}
/**
* This function communicates that the current scope should be treated as dynamic.
*
* In most cases this function is a no-op but if called during
* a PPR prerender it will postpone the current sub-tree and calling
* it during a normal prerender will cause the entire prerender to abort
*/
export function markCurrentScopeAsDynamic(
store: WorkStore,
workUnitStore: undefined | Exclude<WorkUnitStore, PrerenderStoreModern>,
expression: string
): void {
if (workUnitStore) {
switch (workUnitStore.type) {
case 'cache':
case 'unstable-cache':
// Inside cache scopes, marking a scope as dynamic has no effect,
// because the outer cache scope creates a cache boundary. This is
// subtly different from reading a dynamic data source, which is
// forbidden inside a cache scope.
return
case 'private-cache':
// A private cache scope is already dynamic by definition.
return
case 'prerender-legacy':
case 'prerender-ppr':
case 'request':
case 'generate-static-params':
break
default:
workUnitStore satisfies never
}
}
// If we're forcing dynamic rendering or we're forcing static rendering, we
// don't need to do anything here because the entire page is already dynamic
// or it's static and it should not throw or postpone here.
if (store.forceDynamic || store.forceStatic) return
if (store.dynamicShouldError) {
throw new StaticGenBailoutError(
`Route ${store.route} 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`
)
}
if (workUnitStore) {
switch (workUnitStore.type) {
case 'prerender-ppr':
return postponeWithTracking(
store.route,
expression,
workUnitStore.dynamicTracking
)
case 'prerender-legacy':
workUnitStore.revalidate = 0
// We aren't prerendering, but we are generating a static page. We need
// to bail out of static generation.
const err = new DynamicServerError(
`Route ${store.route} couldn't be rendered statically because it used ${expression}. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
)
store.dynamicUsageDescription = expression
store.dynamicUsageStack = err.stack
throw err
case 'request':
if (process.env.NODE_ENV !== 'production') {
workUnitStore.usedDynamic = true
}
break
case 'generate-static-params':
break
default:
workUnitStore satisfies never
}
}
}
/**
* This function is meant to be used when prerendering without cacheComponents or PPR.
* When called during a build it will cause Next.js to consider the route as dynamic.
*
* @internal
*/
export function throwToInterruptStaticGeneration(
expression: string,
store: WorkStore,
prerenderStore: PrerenderStoreLegacy
): never {
// We aren't prerendering but we are generating a static page. We need to bail out of static generation
const err = new DynamicServerError(
`Route ${store.route} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
)
prerenderStore.revalidate = 0
store.dynamicUsageDescription = expression
store.dynamicUsageStack = err.stack
throw err
}
/**
* This function should be used to track whether something dynamic happened even when
* we are in a dynamic render. This is useful for Dev where all renders are dynamic but
* we still track whether dynamic APIs were accessed for helpful messaging
*
* @internal
*/
export function trackDynamicDataInDynamicRender(workUnitStore: WorkUnitStore) {
switch (workUnitStore.type) {
case 'cache':
case 'unstable-cache':
// Inside cache scopes, marking a scope as dynamic has no effect,
// because the outer cache scope creates a cache boundary. This is
// subtly different from reading a dynamic data source, which is
// forbidden inside a cache scope.
return
case 'private-cache':
// A private cache scope is already dynamic by definition.
return
case 'prerender':
case 'prerender-runtime':
case 'prerender-legacy':
case 'prerender-ppr':
case 'prerender-client':
case 'validation-client':
case 'generate-static-params':
break
case 'request':
if (process.env.NODE_ENV !== 'production') {
workUnitStore.usedDynamic = true
}
break
default:
workUnitStore satisfies never
}
}
function abortOnSynchronousDynamicDataAccess(
route: string,
expression: string,
prerenderStore: PrerenderStoreModern
): void {
const reason = `Route ${route} needs to bail out of prerendering at this point because it used ${expression}.`
const error = createPrerenderInterruptedError(reason)
prerenderStore.controller.abort(error)
const dynamicTracking = prerenderStore.dynamicTracking
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,
})
}
}
export function abortOnSynchronousPlatformIOAccess(
route: string,
expression: string,
errorWithStack: Error,
prerenderStore: PrerenderStoreModern
): void {
const dynamicTracking = prerenderStore.dynamicTracking
abortOnSynchronousDynamicDataAccess(route, expression, prerenderStore)
// It is important that we set this tracking value after aborting. Aborts are executed
// synchronously except for the case where you abort during render itself. By setting this
// value late we can use it to determine if any of the aborted tasks are the task that
// called the sync IO expression in the first place.
if (dynamicTracking) {
if (dynamicTracking.syncDynamicErrorWithStack === null) {
dynamicTracking.syncDynamicErrorWithStack = errorWithStack
}
}
}
/**
* use this function when prerendering with cacheComponents. If we are doing a
* prospective prerender we don't actually abort because we want to discover
* all caches for the shell. If this is the actual prerender we do abort.
*
* This function accepts a prerenderStore but the caller should ensure we're
* actually running in cacheComponents mode.
*
* @internal
*/
export function abortAndThrowOnSynchronousRequestDataAccess(
route: string,
expression: string,
errorWithStack: Error,
prerenderStore: PrerenderStoreModern
): never {
const prerenderSignal = prerenderStore.controller.signal
if (prerenderSignal.aborted === false) {
// TODO it would be better to move this aborted check into the callsite so we can avoid making
// the error object when it isn't relevant to the aborting of the prerender however
// since we need the throw semantics regardless of whether we abort it is easier to land
// this way. See how this was handled with `abortOnSynchronousPlatformIOAccess` for a closer
// to ideal implementation
abortOnSynchronousDynamicDataAccess(route, expression, prerenderStore)
// It is important that we set this tracking value after aborting. Aborts are executed
// synchronously except for the case where you abort during render itself. By setting this
// value late we can use it to determine if any of the aborted tasks are the task that
// called the sync IO expression in the first place.
const dynamicTracking = prerenderStore.dynamicTracking
if (dynamicTracking) {
if (dynamicTracking.syncDynamicErrorWithStack === null) {
dynamicTracking.syncDynamicErrorWithStack = errorWithStack
}
}
}
throw createPrerenderInterruptedError(
`Route ${route} needs to bail out of prerendering at this point because it used ${expression}.`
)
}
/**
* 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 {
const error = new Error(message)
;(error as any).digest = NEXT_PRERENDER_INTERRUPTED
return error
}
type DigestError = Error & {
digest: string
}
export function isPrerenderInterruptedError(
error: unknown
): error is DigestError {
return (
typeof error === 'object' &&
error !== null &&
(error as any).digest === NEXT_PRERENDER_INTERRUPTED &&
'name' in error &&
'message' in error &&
error instanceof Error
)
}
export function accessedDynamicData(
dynamicAccesses: Array<DynamicAccess>
): boolean {
return dynamicAccesses.length > 0
}
export function consumeDynamicAccess(
serverDynamic: DynamicTrackingState,
clientDynamic: DynamicTrackingState
): DynamicTrackingState['dynamicAccesses'] {
// We mutate because we only call this once we are no longer writing
// to the dynamicTrackingState and it's more efficient than creating a new
// array.
serverDynamic.dynamicAccesses.push(...clientDynamic.dynamicAccesses)
return serverDynamic.dynamicAccesses
}
export function formatDynamicAPIAccesses(
dynamicAccesses: Array<DynamicAccess>
): string[] {
return dynamicAccesses
.filter(
(access): access is Required<DynamicAccess> =>
typeof access.stack === 'string' && access.stack.length > 0
)
.map(({ expression, stack }) => {
stack = stack
.split('\n')
// Remove the "Error: " prefix from the first line of the stack trace as
// well as the first 4 lines of the stack trace which is the distance
// from the user code and the `new Error().stack` call.
.slice(4)
.filter((line) => {
// Exclude Next.js internals from the stack trace.
if (line.includes('node_modules/next/')) {
return false
}
// Exclude anonymous functions from the stack trace.
if (line.includes(' (<anonymous>)')) {
return false
}
// Exclude Node.js internals from the stack trace.
if (line.includes(' (node:')) {
return false
}
return true
})
.join('\n')
return `Dynamic API Usage Debug - ${expression}:\n${stack}`
})
}
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'))
return controller.signal
}
/**
* In a prerender, we may end up with hanging Promises as inputs due them
* stalling on connection() or because they're loading dynamic data. In that
* case we need to abort the encoding of arguments since they'll never complete.
*/
export function createHangingInputAbortSignal(
workUnitStore: WorkUnitStore
): AbortSignal | undefined {
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-runtime':
const controller = new AbortController()
if (workUnitStore.cacheSignal) {
// If we have a cacheSignal it means we're in a prospective render. If
// the input we're waiting on is coming from another cache, we do want
// to wait for it so that we can resolve this cache entry too.
workUnitStore.cacheSignal.inputReady().then(() => {
controller.abort()
})
} else {
// Otherwise we're in the final render and we should already have all
// our caches filled.
// If the prerender uses stages, we have wait until the runtime stage,
// at which point all runtime inputs will be resolved.
// (otherwise, a runtime prerender might consider `cookies()` hanging
// even though they'd resolve in the next task.)
//
// We might still be waiting on some microtasks so we
// wait one tick before giving up. When we give up, we still want to
// render the content of this cache as deeply as we can so that we can
// suspend as deeply as possible in the tree or not at all if we don't
// end up waiting for the input.
if (
// eslint-disable-next-line no-restricted-syntax -- We are discriminating between two different refined types and don't need an addition exhaustive switch here
workUnitStore.type === 'prerender-runtime' &&
workUnitStore.stagedRendering
) {
const { stagedRendering } = workUnitStore
stagedRendering
.waitForStage(getRuntimeStage(stagedRendering))
.then(() => scheduleOnNextTick(() => controller.abort()))
} else {
scheduleOnNextTick(() => controller.abort())
}
}
return controller.signal
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'request':
case 'cache':
case 'private-cache':
case 'unstable-cache':
case 'generate-static-params':
return undefined
default:
workUnitStore satisfies never
}
}
export function annotateDynamicAccess(
expression: string,
prerenderStore: PrerenderStoreModern | ValidationStoreClient
) {
const dynamicTracking = prerenderStore.dynamicTracking
if (dynamicTracking) {
dynamicTracking.dynamicAccesses.push({
stack: dynamicTracking.isDebugDynamicAccesses
? new Error().stack
: undefined,
expression,
})
}
}
export function useDynamicRouteParams(expression: string) {
const workStore = workAsyncStorage.getStore()
const workUnitStore = workUnitAsyncStorage.getStore()
if (workStore && workUnitStore) {
switch (workUnitStore.type) {
case 'prerender-client':
case 'prerender': {
const fallbackParams = workUnitStore.fallbackRouteParams
if (fallbackParams && fallbackParams.size > 0) {
// We are in a prerender with cacheComponents semantics. We are going to
// hang here and never resolve. This will cause the currently
// rendering component to effectively be a dynamic hole.
React.use(
makeHangingPromise(
workUnitStore.renderSignal,
workStore.route,
expression
)
)
}
break
}
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.
break
}
case 'prerender-runtime':
throw new InvariantError(
`\`${expression}\` was called during a runtime prerender. Next.js should be preventing ${expression} from being included in server components statically, but did not in this case.`
)
case 'cache':
case 'private-cache':
throw new InvariantError(
`\`${expression}\` was called inside a cache scope. Next.js should be preventing ${expression} from being included in server components statically, but did not in this case.`
)
case 'generate-static-params':
throw new InvariantError(
`\`${expression}\` was called in \`generateStaticParams\`. Next.js should be preventing ${expression} from being included in server component files statically, but did not in this case.`
)
case 'prerender-legacy':
case 'request':
case 'unstable-cache':
break
default:
workUnitStore satisfies never
}
}
}
export function useDynamicSearchParams(expression: string) {
const workStore = workAsyncStorage.getStore()
const workUnitStore = workUnitAsyncStorage.getStore()
if (!workStore) {
// We assume pages router context and just return
return
}
if (!workUnitStore) {
throwForMissingRequestStore(expression)
}
switch (workUnitStore.type) {
case 'validation-client':
// During instant validation we try to behave as close to client as possible,
// so this shouldn't hang during SSR.
return
case 'prerender-client': {
React.use(
makeHangingPromise(
workUnitStore.renderSignal,
workStore.route,
expression
)
)
break
}
case 'prerender-legacy':
case 'prerender-ppr': {
if (workStore.forceStatic) {
return
}
throw new BailoutToCSRError(expression)
}
case 'prerender':
case 'prerender-runtime':
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 'cache':
case 'unstable-cache':
case 'private-cache':
throw new InvariantError(
`\`${expression}\` was called inside a cache scope. Next.js should be preventing ${expression} from being included in server components statically, but did not in this case.`
)
case 'generate-static-params':
throw new InvariantError(
`\`${expression}\` was called in \`generateStaticParams\`. Next.js should be preventing ${expression} from being included in server component files statically, but did not in this case.`
)
case 'request':
return
default:
workUnitStore satisfies never
}
}
const hasSuspenseRegex = /\n\s+at Suspense \(<anonymous>\)/
// Common implicit body tags that React will treat as body when placed directly in html
const bodyAndImplicitTags =
'body|div|main|section|article|aside|header|footer|nav|form|p|span|h1|h2|h3|h4|h5|h6'
// Detects when RootLayoutBoundary (our framework marker component) appears
// after Suspense in the component stack, indicating the root layout is wrapped
// within a Suspense boundary. Ensures no body/html/implicit-body components are in between.
//
// Example matches:
// at Suspense (<anonymous>)
// at __next_root_layout_boundary__ (<anonymous>)
//
// Or with other components in between (but not body/html/implicit-body):
// at Suspense (<anonymous>)
// at SomeComponent (<anonymous>)
// at __next_root_layout_boundary__ (<anonymous>)
const hasSuspenseBeforeRootLayoutWithoutBodyOrImplicitBodyRegex = new RegExp(
`\\n\\s+at Suspense \\(<anonymous>\\)(?:(?!\\n\\s+at (?:${bodyAndImplicitTags}) \\(<anonymous>\\))[\\s\\S])*?\\n\\s+at ${ROOT_LAYOUT_BOUNDARY_NAME} \\([^\\n]*\\)`
)
const hasMetadataRegex = new RegExp(
`\\n\\s+at ${METADATA_BOUNDARY_NAME}[\\n\\s]`
)
const hasViewportRegex = new RegExp(
`\\n\\s+at ${VIEWPORT_BOUNDARY_NAME}[\\n\\s]`
)
const hasOutletRegex = new RegExp(`\\n\\s+at ${OUTLET_BOUNDARY_NAME}[\\n\\s]`)
const hasInstantValidationBoundaryRegex = new RegExp(
`\\n\\s+at ${INSTANT_VALIDATION_BOUNDARY_NAME}[\\n\\s]`
)
const slotMarkerRegex = new RegExp(
`\\n\\s+at ${INSTANT_SLOT_MARKER_PREFIX}(\\d+)${INSTANT_SLOT_MARKER_SUFFIX}[\\n\\s]`
)
/** Look up the config factory for the slot this error belongs to.
* Checks the component stack for a slot marker (__next_instant_slot_N__)
* and returns the config at that index. Falls back to index 0 (root
* config) when no slot marker is found or the slot has no config. */
function resolveInstantStack(
componentStack: string,
dynamicValidation: InstantValidationState
): (() => Error) | null {
const { slotStacks } = dynamicValidation
if (slotStacks.length > 1) {
const match = slotMarkerRegex.exec(componentStack)
if (match) {
// Slot markers are 0-indexed in the component name but
// slotStacks is 1-indexed (index 0 is the root config).
const slotIndex = parseInt(match[1], 10) + 1
const slotStack = slotStacks[slotIndex]
if (slotStack != null) {
return slotStack
}
}
}
// Fall back to root config (index 0)
return slotStacks[0] ?? null
}
export function trackAllowedDynamicAccess(
workStore: WorkStore,
componentStack: string,
dynamicValidation: DynamicValidationState,
clientDynamic: DynamicTrackingState
) {
if (hasOutletRegex.test(componentStack)) {
// We don't need to track that this is dynamic. It is only so when something else is also dynamic.
return
} else if (hasMetadataRegex.test(componentStack)) {
dynamicValidation.hasDynamicMetadata = true
return
} else if (hasViewportRegex.test(componentStack)) {
dynamicValidation.hasDynamicViewport = true
return
} else if (
hasSuspenseBeforeRootLayoutWithoutBodyOrImplicitBodyRegex.test(
componentStack
)
) {
// For Suspense within body, the prelude wouldn't be empty so it wouldn't violate the empty static shells rule.
// But if you have Suspense above body, the prelude is empty but we allow that because having Suspense
// is an explicit signal from the user that they acknowledge the empty shell and want dynamic rendering.
dynamicValidation.hasAllowedDynamic = true
dynamicValidation.hasSuspenseAboveBody = true
return
} else if (hasSuspenseRegex.test(componentStack)) {
// this error had a Suspense boundary above it so we don't need to report it as a source
// of disallowed
dynamicValidation.hasAllowedDynamic = true
return
} else if (clientDynamic.syncDynamicErrorWithStack) {
dynamicValidation.dynamicErrors.push(
clientDynamic.syncDynamicErrorWithStack
)
return
} else {
const error = addErrorContext(
createDynamicOrRuntimeBodyError(workStore.route),
componentStack,
null
)
dynamicValidation.dynamicErrors.push(error)
return
}
}
export enum DynamicHoleKind {
/** We know that this hole is caused by runtime data. */
Runtime = 1,
/** We know that this hole is caused by dynamic data. */
Dynamic = 2,
}
/** Stores dynamic reasons used during an SSR render in instant validation. */
export type InstantValidationState = {
hasDynamicMetadata: boolean
hasAllowedClientDynamicAboveBoundary: boolean
dynamicMetadata: null | Error
hasDynamicViewport: boolean
hasAllowedDynamic: boolean
dynamicErrors: Array<Error>
validationPreventingErrors: Array<Error>
thrownErrorsOutsideBoundary: Array<unknown>
/** Per-slot config factories. Index 0 is the root config (fallback).
* Indices 1+ correspond to slot marker components in the tree. */
slotStacks: Array<(() => Error) | null>
}
export function createInstantValidationState(
slotStacks: Array<(() => Error) | null>
): InstantValidationState {
return {
hasDynamicMetadata: false,
hasAllowedClientDynamicAboveBoundary: false,
dynamicMetadata: null,
hasDynamicViewport: false,
hasAllowedDynamic: false,
dynamicErrors: [],
validationPreventingErrors: [],
thrownErrorsOutsideBoundary: [],
slotStacks,
}
}
export function trackDynamicHoleInNavigation(
workStore: WorkStore,
componentStack: string,
dynamicValidation: InstantValidationState,
clientDynamic: DynamicTrackingState,
kind: DynamicHoleKind,
boundaryState: ValidationBoundaryTracking
) {
if (hasOutletRegex.test(componentStack)) {
// We don't need to track that this is dynamic. It is only so when something else is also dynamic.
return
}
// Resolve the config stack for this specific error. If the error
// is inside a slot marker, use that slot's config. Otherwise fall
// back to the default.
const effectiveCreateInstantStack = resolveInstantStack(
componentStack,
dynamicValidation
)
if (hasMetadataRegex.test(componentStack)) {
const error = addErrorContext(
kind === DynamicHoleKind.Runtime
? createRuntimeMetadataError(workStore.route)
: createDynamicMetadataError(workStore.route),
componentStack,
effectiveCreateInstantStack
)
dynamicValidation.dynamicMetadata = error
return
}
if (hasViewportRegex.test(componentStack)) {
const error = addErrorContext(
kind === DynamicHoleKind.Runtime
? createRuntimeViewportError(workStore.route)
: createDynamicViewportError(workStore.route),
componentStack,
effectiveCreateInstantStack
)
dynamicValidation.dynamicErrors.push(error)
return
}
const boundaryLocation =
hasInstantValidationBoundaryRegex.exec(componentStack)
if (!boundaryLocation) {
// We don't see the validation boundary in the component stack,
// so this hole must be coming from a shared parent.
// Shared parents are fully resolved and don't have RSC holes,
// but they can still suspend in a client component during SSR.
// If we managed to render all the validation boundaries, that means
// that the client holes aren't blocking validation and we can disregard them.
// Note that we don't even care whether they have suspense or not.
if (allRequiredBoundariesRendered(boundaryState)) {
dynamicValidation.hasAllowedClientDynamicAboveBoundary = true
dynamicValidation.hasAllowedDynamic = true // Holes outside the boundary contribute to allowing dynamic metadata
return
} else {
// TODO(instant-validation) TODO(NAR-787)
// If shared parents blocked us from validating, we should only log
// the errors from the innermost (segments), i.e. omit layouts whose
// slots managed to render (because clearly they didn't block validation)
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because a Client Component in a parent segment prevented the page from rendering.`
const error = addErrorContext(
new Error(message),
componentStack,
effectiveCreateInstantStack
)
dynamicValidation.validationPreventingErrors.push(error)
return
}
} else {
// The hole originates inside the validation boundary.
//
// Check if we have a Suspense above the hole, but below the validation boundary.
// If we do, then this dynamic usage wouldn't block a navigation to this subtree.
// Conversely, if the nearest suspense is above the validation boundary, then this subtree would block.
//
// Note that in the component stack, children come before parents.
//
// Valid:
// ...
// at Suspense
// ...
// at __next_prefetch_validation_boundary__
//
// Invalid:
// ...
// at __next_prefetch_validation_boundary__
// ...
// at Suspense
//
const suspenseLocation = hasSuspenseRegex.exec(componentStack)
if (suspenseLocation) {
if (suspenseLocation.index < boundaryLocation.index) {
dynamicValidation.hasAllowedDynamic = true
return
} else {
// invalid - fallthrough
}
}
}
if (clientDynamic.syncDynamicErrorWithStack) {
const syncError = clientDynamic.syncDynamicErrorWithStack
if (effectiveCreateInstantStack !== null && syncError.cause === undefined) {
syncError.cause = effectiveCreateInstantStack()
}
dynamicValidation.dynamicErrors.push(syncError)
return
}
const error = addErrorContext(
kind === DynamicHoleKind.Runtime
? createRuntimeBodyError(workStore.route)
: createDynamicBodyError(workStore.route),
componentStack,
effectiveCreateInstantStack
)
dynamicValidation.dynamicErrors.push(error)
return
}
export function trackThrownErrorInNavigation(
workStore: WorkStore,
dynamicValidation: InstantValidationState,
thrownValue: unknown,
componentStack: string
) {
const boundaryLocation =
hasInstantValidationBoundaryRegex.exec(componentStack)
if (!boundaryLocation) {
// There's no validation boundary on the component stack.
// This error may have blocked a boundary from rendering.
// Wrap the error to provide component context.
// This helps for errors from node_modules which would otherwise
// have no useful stack information due to ignore-listing,
// e.g. next/dynamic with `ssr: false`.
const error = addErrorContext(
new Error(
'An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.',
{ cause: thrownValue }
),
componentStack,
null
)
dynamicValidation.thrownErrorsOutsideBoundary.push(error)
} else {
// There's validation boundary on the component stack,
// so we know this error didn't block a validation boundary from rendering.
// However, this error might be hiding be hiding dynamic content that would
// cause validation to fail.
const suspenseLocation = hasSuspenseRegex.exec(componentStack)
if (suspenseLocation) {
if (suspenseLocation.index < boundaryLocation.index) {
// There's a Suspense below the validation boundary but above this error's location.
// This subtree can't fail instant validation because any potential
// dynamic holes would be guarded by the Suspense anyway,
// so we can allow this.
return
} else {
// invalid - fallthrough
}
}
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because an error prevented the target segment from rendering.`
const error = addErrorContext(
new Error(message, { cause: thrownValue }),
componentStack,
null // TODO(instant-validation-build): conflicting use of cause
)
dynamicValidation.validationPreventingErrors.push(error)
}
}
export function trackDynamicHoleInRuntimeShell(
workStore: WorkStore,
componentStack: string,
dynamicValidation: DynamicValidationState,
clientDynamic: DynamicTrackingState
) {
if (hasOutletRegex.test(componentStack)) {
// We don't need to track that this is dynamic. It is only so when something else is also dynamic.
return
} else if (hasMetadataRegex.test(componentStack)) {
const error = addErrorContext(
createDynamicMetadataError(workStore.route),
componentStack,
null
)
dynamicValidation.dynamicMetadata = error
return
} else if (hasViewportRegex.test(componentStack)) {
// TODO(instant-validation): If the page only has holes caused by runtime data,
// we won't find out if there's a suspense-above-body and error for dynamic viewport
// even if there is in fact a suspense-above-body
const error = addErrorContext(
createDynamicViewportError(workStore.route),
componentStack,
null
)
dynamicValidation.dynamicErrors.push(error)
return
} else if (
hasSuspenseBeforeRootLayoutWithoutBodyOrImplicitBodyRegex.test(
componentStack
)
) {
// For Suspense within body, the prelude wouldn't be empty so it wouldn't violate the empty static shells rule.
// But if you have Suspense above body, the prelude is empty but we allow that because having Suspense
// is an explicit signal from the user that they acknowledge the empty shell and want dynamic rendering.
dynamicValidation.hasAllowedDynamic = true
dynamicValidation.hasSuspenseAboveBody = true
return
} else if (hasSuspenseRegex.test(componentStack)) {
// this error had a Suspense boundary above it so we don't need to report it as a source
// of disallowed
dynamicValidation.hasAllowedDynamic = true
return
} else if (clientDynamic.syncDynamicErrorWithStack) {
dynamicValidation.dynamicErrors.push(
clientDynamic.syncDynamicErrorWithStack
)
return
}
const error = addErrorContext(
createDynamicBodyError(workStore.route),
componentStack,
null
)
dynamicValidation.dynamicErrors.push(error)
return
}
export function trackDynamicHoleInStaticShell(
workStore: WorkStore,
componentStack: string,
dynamicValidation: DynamicValidationState,
clientDynamic: DynamicTrackingState
) {
if (hasOutletRegex.test(componentStack)) {
// We don't need to track that this is dynamic. It is only so when something else is also dynamic.
return
} else if (hasMetadataRegex.test(componentStack)) {
const error = addErrorContext(
createRuntimeMetadataError(workStore.route),
componentStack,
null
)
dynamicValidation.dynamicMetadata = error
return
} else if (hasViewportRegex.test(componentStack)) {
const error = addErrorContext(
createRuntimeViewportError(workStore.route),
componentStack,
null
)
dynamicValidation.dynamicErrors.push(error)
return
} else if (
hasSuspenseBeforeRootLayoutWithoutBodyOrImplicitBodyRegex.test(
componentStack
)
) {
// For Suspense within body, the prelude wouldn't be empty so it wouldn't violate the empty static shells rule.
// But if you have Suspense above body, the prelude is empty but we allow that because having Suspense
// is an explicit signal from the user that they acknowledge the empty shell and want dynamic rendering.
dynamicValidation.hasAllowedDynamic = true
dynamicValidation.hasSuspenseAboveBody = true
return
} else if (hasSuspenseRegex.test(componentStack)) {
// this error had a Suspense boundary above it so we don't need to report it as a source
// of disallowed
dynamicValidation.hasAllowedDynamic = true
return
} else if (clientDynamic.syncDynamicErrorWithStack) {
dynamicValidation.dynamicErrors.push(
clientDynamic.syncDynamicErrorWithStack
)
return
} else {
const error = addErrorContext(
createRuntimeBodyError(workStore.route),
componentStack,
null
)
dynamicValidation.dynamicErrors.push(error)
return
}
}
/**
* In dev mode, we prefer using the owner stack, otherwise the provided
* component stack is used.
*
* Accepts an already-created Error so the SWC error-code plugin can see the
* `new Error(...)` call at each call site and auto-assign error codes.
*/
function addErrorContext(
error: Error,
componentStack: string,
createInstantStack: (() => Error) | null
) {
const ownerStack =
process.env.NODE_ENV !== 'production' && React.captureOwnerStack
? React.captureOwnerStack()
: null
if (createInstantStack !== null) {
error.cause = createInstantStack()
}
// TODO go back to owner stack here if available. This is temporarily using componentStack to get the right
//
error.stack =
error.name + ': ' + error.message + (ownerStack || componentStack)
return error
}
export enum PreludeState {
Full = 0,
Empty = 1,
Errored = 2,
}
export function logDisallowedDynamicError(
workStore: WorkStore,
error: Error
): void {
console.error(error)
logBuildDebugHint(workStore.route)
}
export function throwIfDisallowedDynamic(
workStore: WorkStore,
prelude: PreludeState,
dynamicValidation: DynamicValidationState,
serverDynamic: DynamicTrackingState
): void {
if (serverDynamic.syncDynamicErrorWithStack) {
logDisallowedDynamicError(
workStore,
serverDynamic.syncDynamicErrorWithStack
)
throw new StaticGenBailoutError()
}
if (prelude !== PreludeState.Full) {
if (dynamicValidation.hasSuspenseAboveBody) {
// This route has opted into allowing fully dynamic rendering
// by including a Suspense boundary above the body. In this case
// a lack of a shell is not considered disallowed so we simply return
return
}
// We didn't have any sync bailouts but there may be user code which
// blocked the root. We would have captured these during the prerender
// and can log them here and then terminate the build/validating render
const dynamicErrors = dynamicValidation.dynamicErrors
if (dynamicErrors.length > 0) {
for (let i = 0; i < dynamicErrors.length; i++) {
logDisallowedDynamicError(workStore, dynamicErrors[i])
}
throw new StaticGenBailoutError()
}
// If we got this far then the only other thing that could be blocking
// the root is dynamic Viewport. If this is dynamic then
// you need to opt into that by adding a Suspense boundary above the body
// to indicate your are ok with fully dynamic rendering.
if (dynamicValidation.hasDynamicViewport) {
console.error(disallowedDynamicViewportMessage(workStore.route))
throw new StaticGenBailoutError()
}
if (prelude === PreludeState.Empty) {
// If we ever get this far then we messed up the tracking of invalid dynamic.
// We still adhere to the constraint that you must produce a shell but invite the
// user to report this as a bug in Next.js.
console.error(
`Route "${workStore.route}" did not produce a static shell and Next.js was unable to determine a reason. This is a bug in Next.js.`
)
throw new StaticGenBailoutError()
}
} else {
if (
dynamicValidation.hasAllowedDynamic === false &&
dynamicValidation.hasDynamicMetadata
) {
console.error(disallowedDynamicMetadataMessage(workStore.route))
throw new StaticGenBailoutError()
}
}
}
export function getStaticShellDisallowedDynamicReasons(
workStore: WorkStore,
prelude: PreludeState,
dynamicValidation: DynamicValidationState,
configAllowsBlocking: boolean
): Array<Error> {
if (configAllowsBlocking || dynamicValidation.hasSuspenseAboveBody) {
// This route has opted into allowing fully dynamic rendering
// by including a Suspense boundary above the body. In this case
// a lack of a shell is not considered disallowed so we simply return
return []
}
if (prelude !== PreludeState.Full) {
// We didn't have any sync bailouts but there may be user code which
// blocked the root. We would have captured these during the prerender
// and can log them here and then terminate the build/validating render
const dynamicErrors = dynamicValidation.dynamicErrors
if (dynamicErrors.length > 0) {
return dynamicErrors
}
if (prelude === PreludeState.Empty) {
// If we ever get this far then we messed up the tracking of invalid dynamic.
// We still adhere to the constraint that you must produce a shell but invite the
// user to report this as a bug in Next.js.
return [
new InvariantError(
`Route "${workStore.route}" did not produce a static shell and Next.js was unable to determine a reason.`
),
]
}
} else {
// We have a prelude but we might still have dynamic metadata without any other dynamic access
if (
dynamicValidation.hasAllowedDynamic === false &&
dynamicValidation.dynamicErrors.length === 0 &&
dynamicValidation.dynamicMetadata
) {
return [dynamicValidation.dynamicMetadata]
}
}
// We had a non-empty prelude and there are no dynamic holes
return []
}
export function getNavigationDisallowedDynamicReasons(
workStore: WorkStore,
prelude: PreludeState,
dynamicValidation: InstantValidationState,
validationSampleTracking: InstantValidationSampleTracking | null,
boundaryState: ValidationBoundaryTracking
): Array<Error> {
// If we have errors related to missing samples, those should take precedence over everything else.
if (validationSampleTracking) {
const { missingSampleErrors } = validationSampleTracking
if (missingSampleErrors.length > 0) {
return missingSampleErrors
}
}
const { validationPreventingErrors } = dynamicValidation
if (validationPreventingErrors.length > 0) {
return validationPreventingErrors
}
if (!allRequiredBoundariesRendered(boundaryState)) {
const { thrownErrorsOutsideBoundary } = dynamicValidation
const rootInstantStack = dynamicValidation.slotStacks[0]
if (thrownErrorsOutsideBoundary.length === 0) {
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering for an unknown reason.`
const error = rootInstantStack !== null ? rootInstantStack() : new Error()
error.name = 'Error'
error.message = message
return [error]
} else if (thrownErrorsOutsideBoundary.length === 1) {
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to the following error.`
const error = rootInstantStack !== null ? rootInstantStack() : new Error()
error.name = 'Error'
error.message = message
return [error, thrownErrorsOutsideBoundary[0] as Error]
} else {
const message = `Route "${workStore.route}": Could not validate \`unstable_instant\` because the target segment was prevented from rendering, likely due to one of the following errors.`
const error = rootInstantStack !== null ? rootInstantStack() : new Error()
error.name = 'Error'
error.message = message
return [error, ...(thrownErrorsOutsideBoundary as Error[])]
}
}
// NOTE: We don't care about Suspense above body here,
// we're only concerned with the validation boundary
if (prelude !== PreludeState.Full) {
const dynamicErrors = dynamicValidation.dynamicErrors
if (dynamicErrors.length > 0) {
return dynamicErrors
}
if (prelude === PreludeState.Empty) {
// If a client component suspended prevented us from rendering a shell
// but didn't block validation, we don't require a prelude.
if (dynamicValidation.hasAllowedClientDynamicAboveBoundary) {
return []
}
// If we ever get this far then we messed up the tracking of invalid dynamic.
return [
new InvariantError(
`Route "${workStore.route}" failed to render during instant validation and Next.js was unable to determine a reason.`
),
]
}
} else {
const dynamicErrors = dynamicValidation.dynamicErrors
if (dynamicErrors.length > 0) {
return dynamicErrors
}
if (
dynamicValidation.hasAllowedDynamic === false &&
dynamicValidation.dynamicMetadata
) {
return [dynamicValidation.dynamicMetadata]
}
}
// We had a non-empty prelude and there are no dynamic holes
return []
}