UNPKG

next

Version:

The React Framework

946 lines 56.9 kB
/** * 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. */ // 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 { getStagedRenderingController, throwForMissingRequestStore, workUnitAsyncStorage } from './work-unit-async-storage.external'; import { workAsyncStorage } from '../app-render/work-async-storage.external'; import { ClientHookDynamicError, isClientHookDynamicError, makeClientHookHangingPromise, trackRuntimeDataAccessed } 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, createRuntimeBodyErrorInNavigation, createDynamicBodyErrorInNavigation, createDynamicOrRuntimeBodyError, createRuntimeMetadataError, createDynamicMetadataError, createRuntimeViewportError, createDynamicViewportError, createDynamicOrRuntimeViewportError, createDynamicOrRuntimeMetadataError, logBuildDebugHint, createLinkBodyErrorInNavigation, createLinkMetadataError, createLinkViewportError } 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 { allRequiredBoundariesRendered } from './instant-validation/boundary-tracking'; import { createUnrenderedSegmentError } from '../../shared/lib/instant-messages'; const hasPostpone = typeof React.unstable_postpone === 'function'; export function createDynamicTrackingState(isDebugDynamicAccesses) { return { isDebugDynamicAccesses, dynamicAccesses: [], syncDynamicErrorWithStack: null, syncDynamicErrorWithStackPostMicrotask: false }; } export function createDynamicValidationState() { return { hasSuspenseAboveBody: false, hasDynamicMetadata: false, dynamicMetadata: null, hasDynamicViewport: false, hasAllowedDynamic: false, dynamicErrors: [] }; } function getPendingClientSyncDynamicError(clientDynamic) { return clientDynamic.syncDynamicErrorWithStackPostMicrotask ? null : clientDynamic.syncDynamicErrorWithStack; } export function getFirstDynamicReason(trackingState) { var _trackingState_dynamicAccesses_; return (_trackingState_dynamicAccesses_ = trackingState.dynamicAccesses[0]) == null ? void 0 : _trackingState_dynamicAccesses_.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, workUnitStore, expression) { 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; } } // 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 Object.defineProperty(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`), "__NEXT_ERROR_CODE", { value: "E553", enumerable: false, configurable: true }); } 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 = Object.defineProperty(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`), "__NEXT_ERROR_CODE", { value: "E550", enumerable: false, configurable: true }); 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; } } } /** * 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, store, prerenderStore) { // We aren't prerendering but we are generating a static page. We need to bail out of static generation const err = Object.defineProperty(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`), "__NEXT_ERROR_CODE", { value: "E558", enumerable: false, configurable: true }); 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) { 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; } } function abortOnSynchronousDynamicDataAccess(route, expression, prerenderStore) { 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, expression, errorWithStack, prerenderStore) { const dynamicTracking = prerenderStore.dynamicTracking; if (dynamicTracking && dynamicTracking.syncDynamicErrorWithStack === null) { dynamicTracking.syncDynamicErrorWithStack = errorWithStack; // React completes the task that is currently rendering before scheduled // abort cleanup. Client tracking can attribute the sync IO only during // that current task; server tracking keeps the error regardless. queueMicrotask(()=>{ dynamicTracking.syncDynamicErrorWithStackPostMicrotask = true; }); } abortOnSynchronousDynamicDataAccess(route, expression, prerenderStore); } /** * 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, expression, errorWithStack, prerenderStore) { // The synchronously accessed request data would have been available during // a runtime prerender, which would have rendered past this point instead of // aborting — so a runtime prefetch would produce more content than this // render. Record that, same as when request data access creates a hanging // promise (see makeRuntimeHangingPromise). Unlike // `abortOnSynchronousPlatformIOAccess`, which aborts a runtime prerender // all the same and therefore must not record anything. trackRuntimeDataAccessed(prerenderStore); 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); // Preserve the exact server-side dynamic access for final validation after // interrupting this render. 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}.`); } export function Postpone({ reason, route }) { const prerenderStore = workUnitAsyncStorage.getStore(); const dynamicTracking = prerenderStore && prerenderStore.type === 'prerender-ppr' ? prerenderStore.dynamicTracking : null; postponeWithTracking(route, reason, dynamicTracking); } export function postponeWithTracking(route, expression, dynamicTracking) { 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, expression) { 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) { if (typeof err === 'object' && err !== null && typeof err.message === 'string') { return isDynamicPostponeReason(err.message); } return false; } function isDynamicPostponeReason(reason) { 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 Object.defineProperty(new Error('Invariant: isDynamicPostpone misidentified a postpone reason. This is a bug in Next.js'), "__NEXT_ERROR_CODE", { value: "E296", enumerable: false, configurable: true }); } const NEXT_PRERENDER_INTERRUPTED = 'NEXT_PRERENDER_INTERRUPTED'; function createPrerenderInterruptedError(message) { const error = Object.defineProperty(new Error(message), "__NEXT_ERROR_CODE", { value: "E394", enumerable: false, configurable: true }); error.digest = NEXT_PRERENDER_INTERRUPTED; return error; } export function isPrerenderInterruptedError(error) { return typeof error === 'object' && error !== null && error.digest === NEXT_PRERENDER_INTERRUPTED && 'name' in error && 'message' in error && error instanceof Error; } export function accessedDynamicData(dynamicAccesses) { return dynamicAccesses.length > 0; } export function consumeDynamicAccess(serverDynamic, clientDynamic) { // 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) { return dynamicAccesses.filter((access)=>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 Object.defineProperty(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`), "__NEXT_ERROR_CODE", { value: "E224", enumerable: false, configurable: true }); } } /** * 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() { const controller = new AbortController(); controller.abort(Object.defineProperty(new BailoutToCSRError('Render in Browser'), "__NEXT_ERROR_CODE", { value: "E721", enumerable: false, configurable: true })); return controller.signal; } export function createHangingInputAbortSignal(workUnitStore) { 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 final stage. // if an input didn't resolve at that point, then we can assume it never will. // // 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. const stagedRendering = getStagedRenderingController(workUnitStore); if (stagedRendering && stagedRendering.finalStage !== null) { stagedRendering.waitForStage(stagedRendering.finalStage).then(()=>scheduleOnNextTick(()=>controller.abort()), noop); } 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; } } function noop() {} export function annotateDynamicAccess(expression, prerenderStore) { const dynamicTracking = prerenderStore.dynamicTracking; if (dynamicTracking) { dynamicTracking.dynamicAccesses.push({ stack: dynamicTracking.isDebugDynamicAccesses ? new Error().stack : undefined, expression }); } } export function useDynamicRouteParams(expression) { const workStore = workAsyncStorage.getStore(); const workUnitStore = workUnitAsyncStorage.getStore(); if (workStore && workUnitStore) { switch(workUnitStore.type){ case 'prerender-client': { 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(makeClientHookHangingPromise(workUnitStore.renderSignal, new ClientHookDynamicError(workStore.route, expression))); } break; } case 'prerender': throw Object.defineProperty(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.`), "__NEXT_ERROR_CODE", { value: "E795", enumerable: false, configurable: true }); case 'prerender-ppr': { const fallbackParams = workUnitStore.fallbackRouteParams; if (fallbackParams && fallbackParams.size > 0) { return postponeWithTracking(workStore.route, expression, workUnitStore.dynamicTracking); } break; } case 'validation-client': { break; } case 'prerender-runtime': throw Object.defineProperty(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.`), "__NEXT_ERROR_CODE", { value: "E771", enumerable: false, configurable: true }); case 'cache': case 'private-cache': throw Object.defineProperty(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.`), "__NEXT_ERROR_CODE", { value: "E745", enumerable: false, configurable: true }); case 'generate-static-params': throw Object.defineProperty(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.`), "__NEXT_ERROR_CODE", { value: "E1130", enumerable: false, configurable: true }); case 'prerender-legacy': case 'request': case 'unstable-cache': break; default: workUnitStore; } } } export function useDynamicSearchParams(expression) { 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(makeClientHookHangingPromise(workUnitStore.renderSignal, new ClientHookDynamicError(workStore.route, expression))); break; } case 'prerender-legacy': case 'prerender-ppr': { if (workStore.forceStatic) { return; } throw Object.defineProperty(new BailoutToCSRError(expression), "__NEXT_ERROR_CODE", { value: "E394", enumerable: false, configurable: true }); } case 'prerender': case 'prerender-runtime': throw Object.defineProperty(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.`), "__NEXT_ERROR_CODE", { value: "E795", enumerable: false, configurable: true }); case 'cache': case 'unstable-cache': case 'private-cache': throw Object.defineProperty(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.`), "__NEXT_ERROR_CODE", { value: "E745", enumerable: false, configurable: true }); case 'generate-static-params': throw Object.defineProperty(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.`), "__NEXT_ERROR_CODE", { value: "E1130", enumerable: false, configurable: true }); case 'request': return; default: workUnitStore; } } 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, dynamicValidation) { 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; } /** * Inspects the component stack of an outlet boundary to discover whether the * user placed a Suspense boundary above the document body, and records the * opt-in on `dynamicValidation.hasSuspenseAboveBody` if so. * * The outlet itself isn't a meaningful source of dynamic — it only resolves * when metadata/viewport are dynamic, which we track via their own boundaries. * However, the outlet renders alongside the page content, so its stack passes * through the user's layout chain (typically reaching into `<body>` via the * root layout). That makes the outlet stack our best opportunity to spot a * Suspense boundary above the body, even when no real body content is dynamic. * Without this, a route whose only dynamic source is `generateViewport()` would * miss the Suspense-above-body opt-in, because the viewport's stack lives in * the head and never sees the user's root layout. * * We deliberately only set `hasSuspenseAboveBody`, not `hasAllowedDynamic`. The * latter tracks whether the body has dynamic content that's been wrapped in * Suspense (i.e., the page is partially dynamic). The outlet rendering tells us * about the structural opt-in for an empty shell, not about the body being * partially dynamic. The distinction matters because dynamic metadata is only * acceptable when the page is partially dynamic (via real body holes), and we * don't want this outlet-based detection to mask that case. */ function trackOutletSuspenseAboveBody(componentStack, dynamicValidation) { if (hasSuspenseBeforeRootLayoutWithoutBodyOrImplicitBodyRegex.test(componentStack)) { dynamicValidation.hasSuspenseAboveBody = true; } } export function trackAllowedDynamicAccess(dynamicReason, workStore, componentStack, dynamicValidation, clientDynamic) { const syncDynamicError = getPendingClientSyncDynamicError(clientDynamic); if (hasOutletRegex.test(componentStack)) { trackOutletSuspenseAboveBody(componentStack, dynamicValidation); 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 (syncDynamicError) { dynamicValidation.dynamicErrors.push(syncDynamicError); return; } if (isClientHookDynamicError(dynamicReason)) { dynamicValidation.dynamicErrors.push(addErrorContext(dynamicReason, componentStack, null)); return; } const error = addErrorContext(createDynamicOrRuntimeBodyError(workStore.route), componentStack, null); dynamicValidation.dynamicErrors.push(error); return; } export var DynamicHoleKind = /*#__PURE__*/ function(DynamicHoleKind) { /** We know that this hole is caused by link data. */ DynamicHoleKind[DynamicHoleKind["Link"] = 1] = "Link"; /** We know that this hole is caused by runtime data. */ DynamicHoleKind[DynamicHoleKind["Runtime"] = 2] = "Runtime"; /** We know that this hole is caused by dynamic data. */ DynamicHoleKind[DynamicHoleKind["Dynamic"] = 3] = "Dynamic"; return DynamicHoleKind; }({}); export function createInstantValidationState(slotStacks) { return { hasDynamicMetadata: false, hasAllowedClientDynamicAboveBoundary: false, dynamicMetadata: null, hasDynamicViewport: false, hasAllowedDynamic: false, dynamicErrors: [], validationPreventingErrors: [], thrownErrorsOutsideBoundary: [], slotStacks }; } export function trackDynamicHoleInNavigation(dynamicReason, workStore, componentStack, dynamicValidation, clientDynamic, kind, boundaryState) { const syncDynamicError = getPendingClientSyncDynamicError(clientDynamic); 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 === 1 ? createLinkMetadataError(workStore.route) : kind === 2 ? createRuntimeMetadataError(workStore.route) : createDynamicMetadataError(workStore.route), componentStack, effectiveCreateInstantStack); dynamicValidation.dynamicMetadata = error; return; } if (hasViewportRegex.test(componentStack)) { const error = addErrorContext(kind === 1 ? createLinkViewportError(workStore.route) : kind === 2 ? 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 \`instant\` because a Client Component in a parent segment prevented the page from rendering.`; const error = addErrorContext(Object.defineProperty(new Error(message), "__NEXT_ERROR_CODE", { value: "E1331", enumerable: false, configurable: true }), 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 (syncDynamicError) { if (effectiveCreateInstantStack !== null && syncDynamicError.cause === undefined) { syncDynamicError.cause = effectiveCreateInstantStack(); } dynamicValidation.dynamicErrors.push(syncDynamicError); return; } if (isClientHookDynamicError(dynamicReason)) { dynamicValidation.dynamicErrors.push(addErrorContext(dynamicReason, componentStack, effectiveCreateInstantStack)); return; } const error = addErrorContext(kind === 1 ? createLinkBodyErrorInNavigation(workStore.route) : kind === 2 ? createRuntimeBodyErrorInNavigation(workStore.route) : createDynamicBodyErrorInNavigation(workStore.route), componentStack, effectiveCreateInstantStack); dynamicValidation.dynamicErrors.push(error); return; } export function trackThrownErrorInNavigation(workStore, dynamicValidation, thrownValue, componentStack) { 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(Object.defineProperty(new Error('An error occurred while attempting to validate instant UI. This error may be preventing the validation from completing.', { cause: thrownValue }), "__NEXT_ERROR_CODE", { value: "E1118", enumerable: false, configurable: true }), 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 \`instant\` because an error prevented the target segment from rendering.`; const error = addErrorContext(Object.defineProperty(new Error(message, { cause: thrownValue }), "__NEXT_ERROR_CODE", { value: "E1338", enumerable: false, configurable: true }), componentStack, null // TODO(instant-validation-build): conflicting use of cause ); dynamicValidation.validationPreventingErrors.push(error); } } export function trackDynamicHoleInRuntimeShell(dynamicReason, workStore, componentStack, dynamicValidation, clientDynamic) { const syncDynamicError = getPendingClientSyncDynamicError(clientDynamic); if (hasOutletRegex.test(componentStack)) { trackOutletSuspenseAboveBody(componentStack, dynamicValidation); return; } else if (hasMetadataRegex.test(componentStack)) { const error = addErrorContext(createDynamicMetadataError(workStore.route), componentStack, null); dynamicValidation.dynamicMetadata = error; return; } else if (hasViewportRegex.test(componentStack)) { 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 (syncDynamicError) { dynamicValidation.dynamicErrors.push(syncDynamicError); return; } if (isClientHookDynamicError(dynamicReason)) { dynamicValidation.dynamicErrors.push(addErrorContext(dynamicReason, componentStack, null)); return; } const error = addErrorContext(createDynamicBodyError(workStore.route), componentStack, null); dynamicValidation.dynamicErrors.push(error); return; } export function trackDynamicHoleInStaticShell(dynamicReason, workStore, componentStack, dynamicValidation, clientDynamic) { const syncDynamicError = getPendingClientSyncDynamicError(clientDynamic); if (hasOutletRegex.test(componentStack)) { trackOutletSuspenseAboveBody(componentStack, dynamicValidation); 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 (syncDynamicError) { dynamicValidation.dynamicErrors.push(syncDynamicError); return; } if (isClientHookDynamicError(dynamicReason)) { dynamicValidation.dynamicErrors.push(addErrorContext(dynamicReason, componentStack, null)); return; } 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, componentStack, createInstantStack) { 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 var PreludeState = /*#__PURE__*/ function(PreludeState) { PreludeState[PreludeState["Full"] = 0] = "Full"; PreludeState[PreludeState["Empty"] = 1] = "Empty"; PreludeState[PreludeState["Errored"] = 2] = "Errored"; return PreludeState; }({}); export function logDisallowedDynamicError(workStore, error) { console.error(error); logBuildDebugHint(workStore.route); } export function throwIfSyncIOUsed(workStore, serverDynamic) { if (serverDynamic.syncDynamicErrorWithStack) { logDisallowedDynamicError(workStore, serverDynamic.syncDynamicErrorWithStack); throw new StaticGenBailoutError(); } } export function throwIfDisallowedDynamic(workStore, prelude, dynamicValidation, serverDynamic, allowEmptyStaticShell) { throwIfSyncIOUsed(workStore, serverDynamic); // The dynamic metadata error is a mistake-detection signal. It fires when the // rest of the shell is otherwise fully static apart from metadata, suggesting // the dynamic data access in `generateMetadata` was probably unintentional. // That condition is independent of whether the user or build phase accepted // an empty shell, so we surface it before any opt-in bypass. if (prelude === 0 && dynamicValidation.hasAllowedDynamic === false && dynamicValidation.hasDynamicMetadata) { console.error(createDynamicOrRuntimeMetadataError(workStore.route).message); throw new StaticGenBailoutError(); } // Either flag expresses "this shell is allowed to be empty/blocking": // - `allowEmptyStaticShell` covers `instant = false` (user opt-in) // and the build-phase fallback-shell case. // - `hasSuspenseAboveBody` is the structural opt-in inside the user's root // layout. // Treat them as synonyms for the purpose of bypassing shell-failure errors. if (allowEmptyStaticShell || dynamicValidation.hasSuspenseAboveBody) { return; } if (prelude !== 0) { // 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(createDynamicOrRuntimeViewportError(workStore.route).message); throw new StaticGenBailoutError(); } if (prelude === 1) { // 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(); } } } export function getStaticShellDisallowedDynamicReasons(workStore, prelude, dynamicValidation, allowEmptyStaticShell) { // The dynamic metadata error is a mistake-detection signal. It fires when the // rest of the shell is otherwise fully static apart from metadata, suggesting // the dynamic data access in `generateMetadata` was probably unintentional. // That condition is independent of whether the user or build phase accepted // an empty shell, so we surface it before any opt-in bypass. if (prelude