4414 lines
236 KiB
JavaScript
4414 lines
236 KiB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
|
|
import { workAsyncStorage } from '../app-render/work-async-storage.external';
|
|
import * as ReactClient from 'react';
|
|
import RenderResult from '../render-result';
|
|
import { chainStreams, continueFizzStream, continueDynamicPrerender, continueStaticPrerender, continueDynamicHTMLResume, continueStaticFallbackPrerender, streamToBuffer, streamToString, createInlinedDataStream, createPendingStream, createOnHeadersCallback, resumeAndAbort, renderToFlightStream, renderToFizzStream, resumeToFizzStream, getServerPrerender, getClientPrerender, processPrelude as processPreludeOp, createDocumentClosingStream } from './stream-ops';
|
|
import { stripInternalQueries } from '../internal-utils';
|
|
import { NEXT_HMR_REFRESH_HEADER, NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, NEXT_ROUTER_STALE_TIME_HEADER, NEXT_URL, RSC_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_REQUEST_ID_HEADER, NEXT_HTML_REQUEST_ID_HEADER } from '../../client/components/app-router-headers';
|
|
import { createMetadataContext } from '../../lib/metadata/metadata-context';
|
|
import { createRequestStoreForRender } from '../async-storage/request-store';
|
|
import { createWorkStore } from '../async-storage/work-store';
|
|
import { getAccessFallbackErrorTypeByStatus, getAccessFallbackHTTPStatus, isHTTPAccessFallbackError } from '../../client/components/http-access-fallback/http-access-fallback';
|
|
import { getURLFromRedirectError, getRedirectStatusCodeFromError } from '../../client/components/redirect';
|
|
import { isRedirectError } from '../../client/components/redirect-error';
|
|
import { getImplicitTags } from '../lib/implicit-tags';
|
|
import { AppRenderSpan, NextNodeServerSpan } from '../lib/trace/constants';
|
|
import { getTracer, SpanStatusCode } from '../lib/trace/tracer';
|
|
import { FlightRenderResult } from './flight-render-result';
|
|
import { createReactServerErrorHandler, createHTMLErrorHandler, isUserLandError, getDigestForWellKnownError } from './create-error-handler';
|
|
import { dynamicParamTypes } from './get-short-dynamic-param-type';
|
|
import { getSegmentParam } from '../../shared/lib/router/utils/get-segment-param';
|
|
import { getScriptNonceFromHeader } from './get-script-nonce-from-header';
|
|
import { parseAndValidateFlightRouterState } from './parse-and-validate-flight-router-state';
|
|
import { createFlightRouterStateFromLoaderTree } from './create-flight-router-state-from-loader-tree';
|
|
import { handleAction } from './action-handler';
|
|
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr';
|
|
import { warn, error } from '../../build/output/log';
|
|
import { appendMutableCookies } from '../web/spec-extension/adapters/request-cookies';
|
|
import { createServerInsertedHTML } from './server-inserted-html';
|
|
import { getRequiredScripts } from './required-scripts';
|
|
import { addPathPrefix } from '../../shared/lib/router/utils/add-path-prefix';
|
|
import { makeGetServerInsertedHTML } from './make-get-server-inserted-html';
|
|
import { walkTreeWithFlightRouterState, createFullTreeFlightDataForNavigation } from './walk-tree-with-flight-router-state';
|
|
import { createComponentTree, getRootParams } from './create-component-tree';
|
|
import { getAssetQueryString } from './get-asset-query-string';
|
|
import { getClientReferenceManifest, getServerModuleMap } from './manifests-singleton';
|
|
import { DynamicState, DynamicHTMLPreludeState, parsePostponedState } from './postponed-state';
|
|
import { getDynamicDataPostponedState, getDynamicHTMLPostponedState, getPostponedFromState } from './postponed-state';
|
|
import { isDynamicServerError } from '../../client/components/hooks-server-context';
|
|
import { getFlightStream } from './use-flight-response';
|
|
import { StaticGenBailoutError, isStaticGenBailoutError } from '../../client/components/static-generation-bailout';
|
|
import { getStackWithoutErrorMessage } from '../../lib/format-server-error';
|
|
import { extractNextErrorCode } from '../../lib/error-telemetry-utils';
|
|
import { accessedDynamicData, createRenderInBrowserAbortSignal, formatDynamicAPIAccesses, isPrerenderInterruptedError, createDynamicTrackingState, createDynamicValidationState, trackAllowedDynamicAccess, throwIfDisallowedDynamic, PreludeState, consumeDynamicAccess, logDisallowedDynamicError, trackDynamicHoleInRuntimeShell, trackDynamicHoleInStaticShell, getStaticShellDisallowedDynamicReasons, getNavigationDisallowedDynamicReasons, trackDynamicHoleInNavigation, DynamicHoleKind, trackThrownErrorInNavigation, createInstantValidationState } from './dynamic-rendering';
|
|
import { getClientComponentLoaderMetrics, wrapClientComponentLoader } from '../client-component-renderer-logger';
|
|
import { isNodeNextRequest } from '../base-http/helpers';
|
|
import { parseRelativeUrl } from '../../shared/lib/router/utils/parse-relative-url';
|
|
import AppRouter from '../../client/components/app-router';
|
|
import { getIsPossibleServerAction } from '../lib/server-action-request-meta';
|
|
import { createInitialRouterState } from '../../client/components/router-reducer/create-initial-router-state';
|
|
import { createMutableActionQueue } from '../../client/components/app-router-instance';
|
|
import { getRevalidateReason } from '../instrumentation/utils';
|
|
import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment';
|
|
import { getFallbackRouteParams } from '../request/fallback-params';
|
|
import { createReactServerPrerenderResult, ReactServerResult, createReactServerPrerenderResultFromRender } from './app-render-prerender-utils';
|
|
import { Phase, printDebugThrownValueForProspectiveRender } from './prospective-render-utils';
|
|
import { runInSequentialTasks } from './app-render-render-utils';
|
|
import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler';
|
|
import { getHmrRefreshHash, workUnitAsyncStorage } from './work-unit-async-storage.external';
|
|
import { consoleAsyncStorage } from './console-async-storage.external';
|
|
import { CacheSignal } from './cache-signal';
|
|
import { createResponseVaryParamsAccumulator, finishAccumulatingVaryParams, getMetadataVaryParamsThenable } from './vary-params';
|
|
import { getTracedMetadata } from '../lib/trace/utils';
|
|
import { InvariantError } from '../../shared/lib/invariant-error';
|
|
import { StaleTimeIterable, createSelectStaleTime, trackStaleTime, finishStaleTimeTracking } from './stale-time';
|
|
import { HTML_CONTENT_TYPE_HEADER, INFINITE_CACHE } from '../../lib/constants';
|
|
import { createComponentStylesAndScripts } from './create-component-styles-and-scripts';
|
|
import { parseLoaderTree } from '../../shared/lib/router/utils/parse-loader-tree';
|
|
import { createPrerenderResumeDataCache, createRenderResumeDataCache } from '../resume-data-cache/resume-data-cache';
|
|
import isError from '../../lib/is-error';
|
|
import { createServerInsertedMetadata } from './metadata-insertion/create-server-inserted-metadata';
|
|
import { getPreviouslyRevalidatedTags } from '../server-utils';
|
|
import { executeRevalidates } from '../revalidation-utils';
|
|
import { trackPendingChunkLoad, trackPendingImport, trackPendingModules } from './module-loading/track-module-loading.external';
|
|
import { isReactLargeShellError } from './react-large-shell-error';
|
|
import { normalizeConventionFilePath } from './segment-explorer-path';
|
|
import { getRequestMeta } from '../request-meta';
|
|
import { getDynamicParam, interpolateParallelRouteParams } from '../../shared/lib/router/utils/get-dynamic-param';
|
|
import { ImageConfigContext } from '../../shared/lib/image-config-context.shared-runtime';
|
|
import { imageConfigDefault } from '../../shared/lib/image-config';
|
|
import { RenderStage, StagedRenderingController } from './staged-rendering';
|
|
import { anySegmentHasRuntimePrefetchEnabled, isPageAllowedToBlock, anySegmentNeedsInstantValidationInDev, anySegmentNeedsInstantValidationInBuild, resolveInstantConfigSamplesForPage } from './instant-validation/instant-config';
|
|
import { warnOnce } from '../../shared/lib/utils/warn-once';
|
|
import { createDebugChannel } from './debug-channel-server';
|
|
import { createNodeStreamWithLateRelease } from './instant-validation/stream-utils';
|
|
import { createValidationBoundaryTracking } from './instant-validation/boundary-tracking';
|
|
import { ResponseCookies } from '../web/spec-extension/cookies';
|
|
import { isInstantValidationError } from './instant-validation/instant-validation-error';
|
|
function maybeAppendBuildIdToRSCPayload(ctx, payload) {
|
|
if (!ctx.sharedContext.deploymentId) {
|
|
// When using the build id, we need to initialize the id on initial page load, so a build id
|
|
// header wouldn't be enough.
|
|
payload.b = ctx.sharedContext.buildId;
|
|
}
|
|
return payload;
|
|
}
|
|
const flightDataPathHeadKey = 'h';
|
|
const getFlightViewportKey = (requestId)=>requestId + 'v';
|
|
const getFlightMetadataKey = (requestId)=>requestId + 'm';
|
|
const filterStackFrame = process.env.NODE_ENV !== 'production' ? require('../lib/source-maps').filterStackFrameDEV : undefined;
|
|
function parseRequestHeaders(headers, options) {
|
|
// runtime prefetch requests are *not* treated as prefetch requests
|
|
// (TODO: this is confusing, we should refactor this to express this better)
|
|
const isPrefetchRequest = headers[NEXT_ROUTER_PREFETCH_HEADER] === '1';
|
|
const isRuntimePrefetchRequest = headers[NEXT_ROUTER_PREFETCH_HEADER] === '2';
|
|
const isHmrRefresh = headers[NEXT_HMR_REFRESH_HEADER] !== undefined;
|
|
const isRSCRequest = headers[RSC_HEADER] !== undefined;
|
|
const shouldProvideFlightRouterState = isRSCRequest && (!isPrefetchRequest || !options.isRoutePPREnabled);
|
|
const flightRouterState = shouldProvideFlightRouterState ? parseAndValidateFlightRouterState(headers[NEXT_ROUTER_STATE_TREE_HEADER]) : undefined;
|
|
// Checks if this is a prefetch of the Route Tree by the Segment Cache
|
|
const isRouteTreePrefetchRequest = headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER] === '/_tree';
|
|
const csp = headers['content-security-policy'] || headers['content-security-policy-report-only'];
|
|
const nonce = typeof csp === 'string' ? getScriptNonceFromHeader(csp) : undefined;
|
|
const previouslyRevalidatedTags = getPreviouslyRevalidatedTags(headers, options.previewModeId);
|
|
let requestId;
|
|
let htmlRequestId;
|
|
if (process.env.__NEXT_DEV_SERVER) {
|
|
// The request IDs are only used for the dev server to send debug
|
|
// information to the matching client (identified by the HTML request ID
|
|
// that was sent to the client with the HTML document) for the current
|
|
// request (identified by the request ID, as defined by the client).
|
|
requestId = typeof headers[NEXT_REQUEST_ID_HEADER] === 'string' ? headers[NEXT_REQUEST_ID_HEADER] : undefined;
|
|
htmlRequestId = typeof headers[NEXT_HTML_REQUEST_ID_HEADER] === 'string' ? headers[NEXT_HTML_REQUEST_ID_HEADER] : undefined;
|
|
}
|
|
return {
|
|
flightRouterState,
|
|
isPrefetchRequest,
|
|
isRuntimePrefetchRequest,
|
|
isRouteTreePrefetchRequest,
|
|
isHmrRefresh,
|
|
isRSCRequest,
|
|
nonce,
|
|
previouslyRevalidatedTags,
|
|
requestId,
|
|
htmlRequestId
|
|
};
|
|
}
|
|
/**
|
|
* Walks the loader tree to find the minimum `unstable_dynamicStaleTime` exported by
|
|
* any page module. Returns null if no page exports the config.
|
|
*
|
|
* This only reads static exports from page modules — it does not render any
|
|
* server components, so it's cheap to call.
|
|
*
|
|
* TODO: Move this to the prefetch hints file so we don't have to walk the
|
|
* tree on every render.
|
|
*/ async function getDynamicStaleTime(tree) {
|
|
const { page, parallelRoutes } = parseLoaderTree(tree);
|
|
let result = null;
|
|
// Only pages (not layouts) can export unstable_dynamicStaleTime.
|
|
if (typeof page !== 'undefined') {
|
|
const pageMod = await page[0]();
|
|
if (pageMod && typeof pageMod.unstable_dynamicStaleTime === 'number') {
|
|
const value = pageMod.unstable_dynamicStaleTime;
|
|
result = result !== null ? Math.min(result, value) : value;
|
|
}
|
|
}
|
|
const childPromises = [];
|
|
for(const parallelRouteKey in parallelRoutes){
|
|
childPromises.push(getDynamicStaleTime(parallelRoutes[parallelRouteKey]));
|
|
}
|
|
const childResults = await Promise.all(childPromises);
|
|
for (const childResult of childResults){
|
|
if (childResult !== null) {
|
|
result = result !== null ? Math.min(result, childResult) : childResult;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function createNotFoundLoaderTree(loaderTree) {
|
|
const components = loaderTree[2];
|
|
const hasGlobalNotFound = !!components['global-not-found'];
|
|
const notFoundTreeComponents = hasGlobalNotFound ? {
|
|
layout: components['global-not-found'],
|
|
page: [
|
|
()=>null,
|
|
'next/dist/client/components/builtin/empty-stub'
|
|
]
|
|
} : {
|
|
page: components['not-found']
|
|
};
|
|
return [
|
|
'',
|
|
{
|
|
children: [
|
|
PAGE_SEGMENT_KEY,
|
|
{},
|
|
notFoundTreeComponents,
|
|
null
|
|
]
|
|
},
|
|
// Always include global-error so that getGlobalErrorStyles can access it.
|
|
// When global-not-found is present, use full components.
|
|
// Otherwise, only include global-error module.
|
|
hasGlobalNotFound ? components : {
|
|
'global-error': components['global-error']
|
|
},
|
|
null
|
|
];
|
|
}
|
|
/**
|
|
* Returns a function that parses the dynamic segment and return the associated value.
|
|
*/ function makeGetDynamicParamFromSegment(interpolatedParams, fallbackRouteParams, optimisticRouting) {
|
|
return function getDynamicParamFromSegment(loaderTree) {
|
|
const [segment, , , staticSiblings] = loaderTree;
|
|
const segmentParam = getSegmentParam(segment);
|
|
if (!segmentParam) {
|
|
return null;
|
|
}
|
|
const segmentKey = segmentParam.paramName;
|
|
const dynamicParamType = dynamicParamTypes[segmentParam.paramType];
|
|
// Static siblings are only included when optimistic routing is enabled
|
|
const siblings = optimisticRouting ? staticSiblings : null;
|
|
return getDynamicParam(interpolatedParams, segmentKey, dynamicParamType, fallbackRouteParams, siblings);
|
|
};
|
|
}
|
|
function NonIndex({ createElement, pagePath, statusCode, isPossibleServerAction }) {
|
|
const is404Page = pagePath === '/404';
|
|
const isInvalidStatusCode = typeof statusCode === 'number' && statusCode > 400;
|
|
// Only render noindex for page request, skip for server actions
|
|
// TODO: is this correct if `isPossibleServerAction` is a false positive?
|
|
if (!isPossibleServerAction && (is404Page || isInvalidStatusCode)) {
|
|
return createElement('meta', {
|
|
name: 'robots',
|
|
content: 'noindex'
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* This is used by server actions & client-side navigations to generate RSC data from a client-side request.
|
|
* This function is only called on "dynamic" requests (ie, there wasn't already a static response).
|
|
* It uses request headers (namely `next-router-state-tree`) to determine where to start rendering.
|
|
*/ async function generateDynamicRSCPayload(ctx, options) {
|
|
// Flight data that is going to be passed to the browser.
|
|
// Currently a single item array but in the future multiple patches might be combined in a single request.
|
|
// We initialize `flightData` to an empty string because the client router knows how to tolerate
|
|
// it (treating it as an MPA navigation). The only time this function wouldn't generate flight data
|
|
// is for server actions, if the server action handler instructs this function to skip it. When the server
|
|
// action reducer sees a falsy value, it'll simply resolve the action with no data.
|
|
let flightData = '';
|
|
const { componentMod: { routeModule: { userland: { loaderTree } }, createElement, createMetadataComponents, Fragment }, query, requestId, flightRouterState, workStore, url } = ctx;
|
|
const serveStreamingMetadata = !!ctx.renderOpts.serveStreamingMetadata;
|
|
if (!(options == null ? void 0 : options.skipPageRendering)) {
|
|
var _ctx_renderOpts_prefetchHints;
|
|
const preloadCallbacks = [];
|
|
// If we're performing instant validation, we need to render the whole tree,
|
|
// without skipping shared layouts.
|
|
const needsFullTree = process.env.__NEXT_DEV_SERVER && ctx.renderOpts.cacheComponents && !(options == null ? void 0 : options.actionResult) && // Only for navigations
|
|
await anySegmentNeedsInstantValidationInDev(loaderTree);
|
|
const metadataIsRuntimePrefetchable = await anySegmentHasRuntimePrefetchEnabled(loaderTree);
|
|
const { Viewport, Metadata, MetadataOutlet } = createMetadataComponents({
|
|
tree: loaderTree,
|
|
parsedQuery: query,
|
|
pathname: url.pathname,
|
|
metadataContext: createMetadataContext(ctx.renderOpts),
|
|
interpolatedParams: ctx.interpolatedParams,
|
|
serveStreamingMetadata,
|
|
isRuntimePrefetchable: metadataIsRuntimePrefetchable
|
|
});
|
|
const rscHead = createElement(Fragment, {
|
|
key: flightDataPathHeadKey
|
|
}, createElement(NonIndex, {
|
|
createElement,
|
|
pagePath: ctx.pagePath,
|
|
statusCode: ctx.res.statusCode,
|
|
isPossibleServerAction: ctx.isPossibleServerAction
|
|
}), createElement(Viewport, {
|
|
key: getFlightViewportKey(requestId)
|
|
}), createElement(Metadata, {
|
|
key: getFlightMetadataKey(requestId)
|
|
}));
|
|
flightData = (needsFullTree ? await createFullTreeFlightDataForNavigation({
|
|
ctx,
|
|
loaderTree,
|
|
rscHead,
|
|
injectedCSS: new Set(),
|
|
injectedJS: new Set(),
|
|
injectedFontPreloadTags: new Set(),
|
|
preloadCallbacks,
|
|
MetadataOutlet
|
|
}) : await walkTreeWithFlightRouterState({
|
|
ctx,
|
|
loaderTreeToFilter: loaderTree,
|
|
parentParams: {},
|
|
flightRouterState,
|
|
rscHead,
|
|
injectedCSS: new Set(),
|
|
injectedJS: new Set(),
|
|
injectedFontPreloadTags: new Set(),
|
|
rootLayoutIncluded: false,
|
|
preloadCallbacks,
|
|
MetadataOutlet,
|
|
hintTree: ((_ctx_renderOpts_prefetchHints = ctx.renderOpts.prefetchHints) == null ? void 0 : _ctx_renderOpts_prefetchHints[ctx.pagePath]) ?? null
|
|
})).map((path)=>path.slice(1)) // remove the '' (root) segment
|
|
;
|
|
}
|
|
// In dev, the Vary header may not reliably reflect whether a route can
|
|
// be intercepted, because interception routes are compiled on demand.
|
|
// Default to true so the client doesn't cache a stale Fallback entry.
|
|
const varyHeader = ctx.res.getHeader('vary');
|
|
const couldBeIntercepted = !!process.env.__NEXT_DEV_SERVER || typeof varyHeader === 'string' && varyHeader.includes(NEXT_URL);
|
|
// If we have an action result, then this is a server action response.
|
|
// We can rely on this because `ActionResult` will always be a promise, even if
|
|
// the result is falsey.
|
|
if (options == null ? void 0 : options.actionResult) {
|
|
return maybeAppendBuildIdToRSCPayload(ctx, {
|
|
a: options.actionResult,
|
|
f: flightData,
|
|
q: getRenderedSearch(query),
|
|
i: !!couldBeIntercepted
|
|
});
|
|
}
|
|
// Otherwise, it's a regular RSC response.
|
|
const baseResponse = maybeAppendBuildIdToRSCPayload(ctx, {
|
|
f: flightData,
|
|
q: getRenderedSearch(query),
|
|
i: !!couldBeIntercepted,
|
|
S: workStore.isStaticGeneration,
|
|
h: getMetadataVaryParamsThenable()
|
|
});
|
|
if ((options == null ? void 0 : options.staleTimeIterable) !== undefined) {
|
|
baseResponse.s = options.staleTimeIterable;
|
|
}
|
|
if ((options == null ? void 0 : options.staticStageByteLengthPromise) !== undefined) {
|
|
baseResponse.l = options.staticStageByteLengthPromise;
|
|
}
|
|
if ((options == null ? void 0 : options.runtimePrefetchStream) !== undefined) {
|
|
baseResponse.p = options.runtimePrefetchStream;
|
|
}
|
|
// Include the per-page dynamic stale time from unstable_dynamicStaleTime, but only
|
|
// for dynamic renders (not prerenders/static generation). The client treats
|
|
// its presence as authoritative.
|
|
// TODO: Move this to the prefetch hints file so we don't have to walk the
|
|
// tree on every render.
|
|
if (!workStore.isStaticGeneration) {
|
|
const dynamicStaleTime = await getDynamicStaleTime(ctx.componentMod.routeModule.userland.loaderTree);
|
|
if (dynamicStaleTime !== null) {
|
|
baseResponse.d = dynamicStaleTime;
|
|
}
|
|
}
|
|
return baseResponse;
|
|
}
|
|
function createErrorContext(ctx, renderSource) {
|
|
return {
|
|
routerKind: 'App Router',
|
|
routePath: ctx.pagePath,
|
|
// TODO: is this correct if `isPossibleServerAction` is a false positive?
|
|
routeType: ctx.isPossibleServerAction ? 'action' : 'render',
|
|
renderSource,
|
|
revalidateReason: getRevalidateReason(ctx.workStore)
|
|
};
|
|
}
|
|
/**
|
|
* Produces a RenderResult containing the Flight data for the given request. See
|
|
* `generateDynamicRSCPayload` for information on the contents of the render result.
|
|
*/ async function generateDynamicFlightRenderResult(req, ctx, requestStore, options) {
|
|
const { htmlRequestId, renderOpts, requestId, workStore } = ctx;
|
|
const { onInstrumentationRequestError, setReactDebugChannel, isBuildTimePrerendering = false } = renderOpts;
|
|
function onFlightDataRenderError(err, silenceLog) {
|
|
return onInstrumentationRequestError == null ? void 0 : onInstrumentationRequestError(err, req, createErrorContext(ctx, 'react-server-components-payload'), silenceLog);
|
|
}
|
|
const onError = createReactServerErrorHandler(process.env.NODE_ENV === 'development', isBuildTimePrerendering, workStore.reactServerErrorsByDigest, onFlightDataRenderError);
|
|
const debugChannel = setReactDebugChannel && createDebugChannel();
|
|
if (debugChannel) {
|
|
setReactDebugChannel(debugChannel.clientSide, htmlRequestId, requestId);
|
|
}
|
|
const { clientModules } = getClientReferenceManifest();
|
|
// For app dir, use the bundled version of Flight server renderer (renderToReadableStream)
|
|
// which contains the subset React.
|
|
const rscPayload = await workUnitAsyncStorage.run(requestStore, generateDynamicRSCPayload, ctx, options);
|
|
const flightStream = workUnitAsyncStorage.run(requestStore, renderToFlightStream, ctx.componentMod, rscPayload, clientModules, {
|
|
onError,
|
|
temporaryReferences: options == null ? void 0 : options.temporaryReferences,
|
|
filterStackFrame,
|
|
debugChannel: debugChannel == null ? void 0 : debugChannel.serverSide
|
|
});
|
|
return new FlightRenderResult(flightStream, {
|
|
fetchMetrics: workStore.fetchMetrics
|
|
}, options == null ? void 0 : options.waitUntil);
|
|
}
|
|
/**
|
|
* Production-only staged dynamic flight render for cache components. Uses
|
|
* staged rendering to separate static (RDC-backed) from runtime/dynamic
|
|
* content.
|
|
*/ async function generateStagedDynamicFlightRenderResult(req, ctx, requestStore) {
|
|
const { componentMod, workStore, renderOpts } = ctx;
|
|
const { renderToReadableStream, routeModule } = componentMod;
|
|
const { loaderTree } = routeModule.userland;
|
|
const { onInstrumentationRequestError, experimental } = renderOpts;
|
|
function onFlightDataRenderError(err, silenceLog) {
|
|
return onInstrumentationRequestError == null ? void 0 : onInstrumentationRequestError(err, req, createErrorContext(ctx, 'react-server-components-payload'), silenceLog);
|
|
}
|
|
const onError = createReactServerErrorHandler(false, false, workStore.reactServerErrorsByDigest, onFlightDataRenderError);
|
|
const selectStaleTime = createSelectStaleTime(experimental);
|
|
const staleTimeIterable = new StaleTimeIterable();
|
|
// TODO(cached-navs): this assumes that we checked during build that there's no sync IO.
|
|
// but it can happen e.g. after a revalidation or conditionally for a param that wasn't prerendered.
|
|
// we should change this to track sync IO, log an error and advance to dynamic.
|
|
const shouldTrackSyncIO = false;
|
|
const stageController = new StagedRenderingController(null, null, shouldTrackSyncIO);
|
|
// Initialize stale time tracking on the request store.
|
|
requestStore.stale = INFINITE_CACHE;
|
|
requestStore.stagedRendering = stageController;
|
|
requestStore.asyncApiPromises = createAsyncApiPromises(stageController, requestStore.cookies, requestStore.mutableCookies, requestStore.headers);
|
|
trackStaleTime(requestStore, staleTimeIterable, selectStaleTime);
|
|
// Deferred promise for the static stage byte length. Flight serializes the
|
|
// resolved value into the stream so the client knows where the static
|
|
// prefix ends.
|
|
let resolveStaticStageByteLength;
|
|
const staticStageByteLengthPromise = new Promise((resolve)=>{
|
|
resolveStaticStageByteLength = resolve;
|
|
});
|
|
// Check if this route has opted into runtime prefetching via
|
|
// unstable_instant. If so, we piggyback on the dynamic render to fill caches
|
|
// and then spawn a final runtime prerender whose result stream is embedded in
|
|
// the RSC payload. This is gated on the explicit opt-in because it adds extra
|
|
// server processing, increases the response payload size, and the runtime
|
|
// prefetch output should have been validated first.
|
|
const hasRuntimePrefetch = await anySegmentHasRuntimePrefetchEnabled(loaderTree);
|
|
let runtimePrefetchStream;
|
|
if (hasRuntimePrefetch) {
|
|
// Create a mutable cache that gets filled during the dynamic render.
|
|
const prerenderResumeDataCache = createPrerenderResumeDataCache();
|
|
requestStore.prerenderResumeDataCache = prerenderResumeDataCache;
|
|
const cacheSignal = new CacheSignal();
|
|
trackPendingModules(cacheSignal);
|
|
requestStore.cacheSignal = cacheSignal;
|
|
// Create a deferred stream for the runtime prefetch result. Its readable
|
|
// side goes into the RSC payload (Flight serializes it lazily). The
|
|
// writable side receives the runtime prerender result once the dynamic
|
|
// render has filled all caches.
|
|
const runtimePrefetchTransform = new TransformStream();
|
|
runtimePrefetchStream = runtimePrefetchTransform.readable;
|
|
// Wait for the dynamic render to fill caches, then run the final runtime
|
|
// prerender (fire-and-forget — does not block the response).
|
|
void cacheSignal.cacheReady().then(()=>spawnRuntimePrefetchWithFilledCaches(runtimePrefetchTransform.writable, ctx, prerenderResumeDataCache, requestStore, onError));
|
|
}
|
|
const rscPayload = await workUnitAsyncStorage.run(requestStore, generateDynamicRSCPayload, ctx, {
|
|
staleTimeIterable,
|
|
staticStageByteLengthPromise,
|
|
runtimePrefetchStream
|
|
});
|
|
const { clientModules } = getClientReferenceManifest();
|
|
const flightReadableStream = await runInSequentialTasks(()=>{
|
|
stageController.advanceStage(RenderStage.Static);
|
|
const stream = workUnitAsyncStorage.run(requestStore, renderToReadableStream, rscPayload, clientModules, {
|
|
onError,
|
|
filterStackFrame
|
|
});
|
|
const [dynamicStream, staticStream] = stream.tee();
|
|
countStaticStageBytes(staticStream, stageController).then(resolveStaticStageByteLength);
|
|
return dynamicStream;
|
|
}, ()=>{
|
|
// This is a separate task that doesn't advance a stage. It forces
|
|
// draining the microtask queue so that the stale time iterable is closed
|
|
// before we advance to the dynamic stage.
|
|
void finishStaleTimeTracking(staleTimeIterable);
|
|
}, ()=>{
|
|
stageController.advanceStage(RenderStage.Dynamic);
|
|
});
|
|
return new FlightRenderResult(flightReadableStream, {
|
|
fetchMetrics: workStore.fetchMetrics
|
|
});
|
|
}
|
|
/**
|
|
* Runs a final runtime prerender using the provided (already filled) cache and
|
|
* pipes its output into the provided writable stream. The caller is responsible
|
|
* for waiting until caches are warm before calling this function.
|
|
*/ async function spawnRuntimePrefetchWithFilledCaches(writable, ctx, prerenderResumeDataCache, requestStore, onError) {
|
|
try {
|
|
const { componentMod, getDynamicParamFromSegment } = ctx;
|
|
const { loaderTree } = componentMod.routeModule.userland;
|
|
const rootParams = getRootParams(loaderTree, getDynamicParamFromSegment);
|
|
const staleTimeIterable = new StaleTimeIterable();
|
|
const { result } = await finalRuntimeServerPrerender(ctx, generateDynamicRSCPayload.bind(null, ctx, {
|
|
staleTimeIterable
|
|
}), prerenderResumeDataCache, null, rootParams, requestStore.headers, requestStore.cookies, requestStore.draftMode, onError, staleTimeIterable);
|
|
await result.prelude.pipeTo(writable);
|
|
} catch {
|
|
// Runtime prerender failed. Close the stream gracefully — the navigation
|
|
// still works, we just won't get cached runtime data.
|
|
try {
|
|
await writable.close();
|
|
} catch {
|
|
// Writable may already be closed/errored.
|
|
}
|
|
}
|
|
}
|
|
async function stagedRenderToReadableStreamWithoutCachesInDev(ctx, requestStore, getPayload, options) {
|
|
// We're rendering while bypassing caches,
|
|
// so we have no hope of showing a useful runtime stage.
|
|
// But we still want things like `params` to show up in devtools correctly,
|
|
// which relies on mechanisms we've set up for staged rendering,
|
|
// so we do a 2-task version (Static -> Dynamic) instead.
|
|
// We aren't filling caches so we don't need to abort this render, it'll
|
|
// stream in a single pass
|
|
const stageController = new StagedRenderingController(null, null, false // do not track sync IO (we don't have reliable stages)
|
|
);
|
|
const environmentName = ()=>{
|
|
const currentStage = stageController.currentStage;
|
|
switch(currentStage){
|
|
case RenderStage.Before:
|
|
case RenderStage.EarlyStatic:
|
|
case RenderStage.Static:
|
|
return 'Prerender';
|
|
case RenderStage.EarlyRuntime:
|
|
case RenderStage.Runtime:
|
|
case RenderStage.Dynamic:
|
|
case RenderStage.Abandoned:
|
|
return 'Server';
|
|
default:
|
|
currentStage;
|
|
throw Object.defineProperty(new InvariantError(`Invalid render stage: ${currentStage}`), "__NEXT_ERROR_CODE", {
|
|
value: "E881",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
};
|
|
requestStore.stagedRendering = stageController;
|
|
requestStore.asyncApiPromises = createAsyncApiPromises(stageController, requestStore.cookies, requestStore.mutableCookies, requestStore.headers);
|
|
const { clientModules } = getClientReferenceManifest();
|
|
const rscPayload = await getPayload(requestStore);
|
|
return await runInSequentialTasks(()=>{
|
|
stageController.advanceStage(RenderStage.Static);
|
|
return workUnitAsyncStorage.run(requestStore, renderToFlightStream, ctx.componentMod, rscPayload, clientModules, {
|
|
...options,
|
|
environmentName
|
|
});
|
|
}, ()=>{
|
|
stageController.advanceStage(RenderStage.Dynamic);
|
|
});
|
|
}
|
|
/**
|
|
* Fork of `generateDynamicFlightRenderResult` that renders using `renderWithRestartOnCacheMissInDev`
|
|
* to ensure correct separation of environments Prerender/Server (for use in Cache Components)
|
|
*/ async function generateDynamicFlightRenderResultWithStagesInDev(req, ctx, initialRequestStore, createRequestStore, fallbackParams) {
|
|
const { htmlRequestId, renderOpts, requestId, workStore, componentMod: { createElement, routeModule: { userland: { loaderTree } } }, url } = ctx;
|
|
const { onInstrumentationRequestError, setReactDebugChannel, setCacheStatus, isBuildTimePrerendering = false } = renderOpts;
|
|
function onFlightDataRenderError(err, silenceLog) {
|
|
return onInstrumentationRequestError == null ? void 0 : onInstrumentationRequestError(err, req, createErrorContext(ctx, 'react-server-components-payload'), silenceLog);
|
|
}
|
|
const onError = createReactServerErrorHandler(process.env.NODE_ENV === 'development', isBuildTimePrerendering, workStore.reactServerErrorsByDigest, onFlightDataRenderError);
|
|
// We validate RSC requests for HMR refreshes and client navigations when
|
|
// instant configs exist, since we render all the layouts necessary to perform
|
|
// the validation in those cases.
|
|
const shouldValidate = !isBypassingCachesInDev(initialRequestStore) && (initialRequestStore.isHmrRefresh === true || await anySegmentNeedsInstantValidationInDev(loaderTree));
|
|
const getPayload = async (requestStore)=>{
|
|
const payload = await workUnitAsyncStorage.run(requestStore, generateDynamicRSCPayload, ctx, undefined);
|
|
if (isBypassingCachesInDev(requestStore)) {
|
|
// Mark the RSC payload to indicate that caches were bypassed in dev.
|
|
// This lets the client know not to cache anything based on this render.
|
|
payload._bypassCachesInDev = createElement(WarnForBypassCachesInDev, {
|
|
route: workStore.route
|
|
});
|
|
} else if (shouldValidate) {
|
|
// If this payload will be used for validation, it needs to contain the
|
|
// canonical URL. Without it we'd get an error.
|
|
payload.c = prepareInitialCanonicalUrl(url);
|
|
}
|
|
return payload;
|
|
};
|
|
let debugChannel;
|
|
let stream;
|
|
if (// We only do this flow if we can safely recreate the store from scratch
|
|
// (which is not the case for renders after an action)
|
|
createRequestStore && // We only do this flow if we're not bypassing caches in dev using
|
|
// "disable cache" in devtools or a hard refresh (cache-control: "no-store")
|
|
!isBypassingCachesInDev(initialRequestStore)) {
|
|
// Before we kick off the render, we set the cache status back to it's initial state
|
|
// in case a previous render bypassed the cache.
|
|
if (setCacheStatus) {
|
|
setCacheStatus('ready', htmlRequestId);
|
|
}
|
|
const { stream: serverStream, accumulatedChunksPromise, syncInterruptReason, startTime, staticStageEndTime, runtimeStageEndTime, debugChannel: returnedDebugChannel, requestStore: finalRequestStore } = await renderWithRestartOnCacheMissInDev(ctx, initialRequestStore, createRequestStore, getPayload, onError);
|
|
if (shouldValidate) {
|
|
let validationDebugChannelClient = undefined;
|
|
if (returnedDebugChannel) {
|
|
const [t1, t2] = returnedDebugChannel.clientSide.readable.tee();
|
|
returnedDebugChannel.clientSide.readable = t1;
|
|
validationDebugChannelClient = nodeStreamFromReadableStream(t2);
|
|
}
|
|
consoleAsyncStorage.run({
|
|
dim: true
|
|
}, spawnStaticShellValidationInDev, accumulatedChunksPromise, syncInterruptReason, startTime, staticStageEndTime, runtimeStageEndTime, ctx, finalRequestStore, fallbackParams, validationDebugChannelClient);
|
|
} else {
|
|
logValidationSkipped(ctx);
|
|
}
|
|
debugChannel = returnedDebugChannel;
|
|
stream = serverStream;
|
|
} else {
|
|
// We're either bypassing caches or we can't restart the render.
|
|
// Do a dynamic render, but with (basic) environment labels.
|
|
// Set cache status to bypass when specifically bypassing caches in dev
|
|
if (setCacheStatus) {
|
|
setCacheStatus('bypass', htmlRequestId);
|
|
}
|
|
debugChannel = setReactDebugChannel && createDebugChannel();
|
|
stream = await stagedRenderToReadableStreamWithoutCachesInDev(ctx, initialRequestStore, getPayload, {
|
|
onError: onError,
|
|
filterStackFrame,
|
|
debugChannel: debugChannel == null ? void 0 : debugChannel.serverSide
|
|
});
|
|
}
|
|
if (debugChannel && setReactDebugChannel) {
|
|
setReactDebugChannel(debugChannel.clientSide, htmlRequestId, requestId);
|
|
}
|
|
return new FlightRenderResult(stream, {
|
|
fetchMetrics: workStore.fetchMetrics
|
|
});
|
|
}
|
|
async function generateRuntimePrefetchResult(req, ctx, requestStore) {
|
|
const { workStore, renderOpts } = ctx;
|
|
const { isBuildTimePrerendering = false, onInstrumentationRequestError } = renderOpts;
|
|
function onFlightDataRenderError(err, silenceLog) {
|
|
return onInstrumentationRequestError == null ? void 0 : onInstrumentationRequestError(err, req, // TODO(runtime-ppr): should we use a different value?
|
|
createErrorContext(ctx, 'react-server-components-payload'), silenceLog);
|
|
}
|
|
const onError = createReactServerErrorHandler(false, isBuildTimePrerendering, workStore.reactServerErrorsByDigest, onFlightDataRenderError);
|
|
const metadata = {};
|
|
const staleTimeIterable = new StaleTimeIterable();
|
|
const { componentMod: { routeModule: { userland: { loaderTree } } }, getDynamicParamFromSegment } = ctx;
|
|
const rootParams = getRootParams(loaderTree, getDynamicParamFromSegment);
|
|
// We need to share caches between the prospective prerender and the final prerender,
|
|
// but we're not going to persist this anywhere.
|
|
const prerenderResumeDataCache = createPrerenderResumeDataCache();
|
|
// We're not resuming an existing render.
|
|
const renderResumeDataCache = null;
|
|
await prospectiveRuntimeServerPrerender(ctx, generateDynamicRSCPayload.bind(null, ctx), prerenderResumeDataCache, renderResumeDataCache, rootParams, requestStore.headers, requestStore.cookies, requestStore.draftMode);
|
|
const response = await finalRuntimeServerPrerender(ctx, generateDynamicRSCPayload.bind(null, ctx, {
|
|
staleTimeIterable
|
|
}), prerenderResumeDataCache, renderResumeDataCache, rootParams, requestStore.headers, requestStore.cookies, requestStore.draftMode, onError, staleTimeIterable);
|
|
applyMetadataFromPrerenderResult(response, metadata, workStore);
|
|
metadata.fetchMetrics = ctx.workStore.fetchMetrics;
|
|
return new FlightRenderResult(response.result.prelude, metadata);
|
|
}
|
|
async function prospectiveRuntimeServerPrerender(ctx, getPayload, prerenderResumeDataCache, renderResumeDataCache, rootParams, headers, cookies, draftMode) {
|
|
const { implicitTags, renderOpts, workStore } = ctx;
|
|
const { ComponentMod } = renderOpts;
|
|
// Prerender controller represents the lifetime of the prerender.
|
|
// It will be aborted when a Task is complete or a synchronously aborting
|
|
// API is called. Notably during cache-filling renders this does not actually
|
|
// terminate the render itself which will continue until all caches are filled
|
|
const initialServerPrerenderController = new AbortController();
|
|
// This controller represents the lifetime of the React render call. Notably
|
|
// during the cache-filling render it is different from the prerender controller
|
|
// because we don't want to end the react render until all caches are filled.
|
|
const initialServerRenderController = new AbortController();
|
|
// The cacheSignal helps us track whether caches are still filling or we are ready
|
|
// to cut the render off.
|
|
const cacheSignal = new CacheSignal();
|
|
const initialServerPrerenderStore = {
|
|
type: 'prerender-runtime',
|
|
phase: 'render',
|
|
rootParams,
|
|
implicitTags,
|
|
renderSignal: initialServerRenderController.signal,
|
|
controller: initialServerPrerenderController,
|
|
// During the initial prerender we need to track all cache reads to ensure
|
|
// we render long enough to fill every cache it is possible to visit during
|
|
// the final prerender.
|
|
cacheSignal,
|
|
// We only need to track dynamic accesses during the final prerender.
|
|
dynamicTracking: null,
|
|
// Runtime prefetches are never cached server-side, only client-side,
|
|
// so we set `expire` and `revalidate` to their minimum values just in case.
|
|
revalidate: 1,
|
|
expire: 0,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
renderResumeDataCache,
|
|
prerenderResumeDataCache,
|
|
hmrRefreshHash: undefined,
|
|
// We don't track vary params during initial prerender, only the final one
|
|
varyParamsAccumulator: null,
|
|
// No stage sequencing needed for prospective renders.
|
|
stagedRendering: null,
|
|
// These are not present in regular prerenders, but allowed in a runtime prerender.
|
|
headers,
|
|
cookies,
|
|
draftMode
|
|
};
|
|
const { clientModules } = getClientReferenceManifest();
|
|
// We're not going to use the result of this render because the only time it could be used
|
|
// is if it completes in a microtask and that's likely very rare for any non-trivial app
|
|
const initialServerPayload = await workUnitAsyncStorage.run(initialServerPrerenderStore, getPayload);
|
|
const prerenderOptions = {
|
|
filterStackFrame,
|
|
onError: (err)=>{
|
|
const digest = getDigestForWellKnownError(err);
|
|
if (digest) {
|
|
return digest;
|
|
}
|
|
if (initialServerPrerenderController.signal.aborted) {
|
|
// The render aborted before this error was handled which indicates
|
|
// the error is caused by unfinished components within the render
|
|
return;
|
|
} else if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
|
printDebugThrownValueForProspectiveRender(err, workStore.route, Phase.ProspectiveRender);
|
|
}
|
|
},
|
|
// We don't want to stop rendering until the cacheSignal is complete so we pass
|
|
// a different signal to this render call than is used by dynamic APIs to signify
|
|
// transitioning out of the prerender environment
|
|
signal: initialServerRenderController.signal
|
|
};
|
|
const pendingInitialServerResult = workUnitAsyncStorage.run(initialServerPrerenderStore, getServerPrerender(ComponentMod), initialServerPayload, clientModules, prerenderOptions);
|
|
// Wait for all caches to be finished filling and for async imports to resolve
|
|
trackPendingModules(cacheSignal);
|
|
await cacheSignal.cacheReady();
|
|
initialServerRenderController.abort();
|
|
initialServerPrerenderController.abort();
|
|
// We don't need to continue the prerender process if we already
|
|
// detected invalid dynamic usage in the initial prerender phase.
|
|
if (workStore.invalidDynamicUsageError) {
|
|
throw workStore.invalidDynamicUsageError;
|
|
}
|
|
try {
|
|
return await createReactServerPrerenderResult(pendingInitialServerResult);
|
|
} catch (err) {
|
|
if (initialServerRenderController.signal.aborted || initialServerPrerenderController.signal.aborted) {
|
|
// These are expected errors that might error the prerender. we ignore them.
|
|
} else if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
|
// We don't normally log these errors because we are going to retry anyway but
|
|
// it can be useful for debugging Next.js itself to get visibility here when needed
|
|
printDebugThrownValueForProspectiveRender(err, workStore.route, Phase.ProspectiveRender);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Prepends a single ASCII byte to the stream indicating whether the response
|
|
* is partial (contains dynamic holes): '~' (0x7e) for partial, '#' (0x23)
|
|
* for complete.
|
|
*/ function prependIsPartialByte(stream, isPartial) {
|
|
const byte = new Uint8Array([
|
|
isPartial ? 0x7e : 0x23
|
|
]);
|
|
return stream.pipeThrough(new TransformStream({
|
|
start (controller) {
|
|
controller.enqueue(byte);
|
|
}
|
|
}));
|
|
}
|
|
async function finalRuntimeServerPrerender(ctx, getPayload, prerenderResumeDataCache, renderResumeDataCache, rootParams, headers, cookies, draftMode, onError, staleTimeIterable) {
|
|
const { implicitTags, renderOpts } = ctx;
|
|
const { ComponentMod, experimental, isDebugDynamicAccesses } = renderOpts;
|
|
const selectStaleTime = createSelectStaleTime(experimental);
|
|
let serverIsDynamic = false;
|
|
const finalServerController = new AbortController();
|
|
const serverDynamicTracking = createDynamicTrackingState(isDebugDynamicAccesses);
|
|
const finalStageController = new StagedRenderingController(finalServerController.signal, null, true // track sync IO
|
|
);
|
|
const varyParamsAccumulator = createResponseVaryParamsAccumulator();
|
|
const finalServerPrerenderStore = {
|
|
type: 'prerender-runtime',
|
|
phase: 'render',
|
|
rootParams,
|
|
implicitTags,
|
|
renderSignal: finalServerController.signal,
|
|
controller: finalServerController,
|
|
// All caches we could read must already be filled so no tracking is necessary
|
|
cacheSignal: null,
|
|
dynamicTracking: serverDynamicTracking,
|
|
// Runtime prefetches are never cached server-side, only client-side,
|
|
// so we set `expire` and `revalidate` to their minimum values just in case.
|
|
revalidate: 1,
|
|
expire: 0,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
prerenderResumeDataCache,
|
|
renderResumeDataCache,
|
|
hmrRefreshHash: undefined,
|
|
varyParamsAccumulator,
|
|
// Used to separate the stages in the 5-task pipeline.
|
|
stagedRendering: finalStageController,
|
|
// These are not present in regular prerenders, but allowed in a runtime prerender.
|
|
headers,
|
|
cookies,
|
|
draftMode
|
|
};
|
|
trackStaleTime(finalServerPrerenderStore, staleTimeIterable, selectStaleTime);
|
|
const { clientModules } = getClientReferenceManifest();
|
|
const finalRSCPayload = await workUnitAsyncStorage.run(finalServerPrerenderStore, getPayload);
|
|
let prerenderIsPending = true;
|
|
const result = await runInSequentialTasks(async ()=>{
|
|
// EarlyStatic stage: render begins.
|
|
// Runtime-prefetchable segments render immediately.
|
|
// Non-prefetchable segments are gated until the Static stage.
|
|
finalStageController.advanceStage(RenderStage.EarlyStatic);
|
|
const prerenderResult = await workUnitAsyncStorage.run(finalServerPrerenderStore, getServerPrerender(ComponentMod), finalRSCPayload, clientModules, {
|
|
filterStackFrame,
|
|
onError,
|
|
signal: finalServerController.signal
|
|
});
|
|
prerenderIsPending = false;
|
|
return prerenderResult;
|
|
}, ()=>{
|
|
// Advance to Static stage: resolve promise holding back
|
|
// non-prefetchable segments so they can begin rendering.
|
|
finalStageController.advanceStage(RenderStage.Static);
|
|
}, ()=>{
|
|
// Advance to EarlyRuntime stage: resolve cookies/headers for
|
|
// runtime-prefetchable segments. Sync IO is checked here.
|
|
finalStageController.advanceStage(RenderStage.EarlyRuntime);
|
|
}, ()=>{
|
|
// Advance to Runtime stage: resolve cookies/headers for
|
|
// non-prefetchable segments. Sync IO is allowed here.
|
|
finalStageController.advanceStage(RenderStage.Runtime);
|
|
}, ()=>{
|
|
Promise.all([
|
|
finishStaleTimeTracking(staleTimeIterable),
|
|
finishAccumulatingVaryParams(varyParamsAccumulator)
|
|
]).then(()=>{
|
|
// Abort. This runs as a microtask after Flight has flushed the
|
|
// staleTime and varyParams closing chunks, but before the next
|
|
// macrotask resolves the overall result.
|
|
if (finalServerController.signal.aborted) {
|
|
// If the server controller is already aborted we must have called
|
|
// something that required aborting the prerender synchronously such
|
|
// as with new Date()
|
|
serverIsDynamic = true;
|
|
return;
|
|
}
|
|
if (prerenderIsPending) {
|
|
// If prerenderIsPending then we have blocked for longer than a Task
|
|
// and we assume there is something unfinished.
|
|
serverIsDynamic = true;
|
|
}
|
|
finalServerController.abort();
|
|
});
|
|
});
|
|
result.prelude = prependIsPartialByte(result.prelude, serverIsDynamic);
|
|
return {
|
|
result,
|
|
// TODO(runtime-ppr): do we need to produce a digest map here?
|
|
// digestErrorsMap: ...,
|
|
dynamicAccess: serverDynamicTracking,
|
|
isPartial: serverIsDynamic,
|
|
collectedRevalidate: finalServerPrerenderStore.revalidate,
|
|
collectedExpire: finalServerPrerenderStore.expire,
|
|
collectedStale: staleTimeIterable.currentValue,
|
|
collectedTags: finalServerPrerenderStore.tags
|
|
};
|
|
}
|
|
/**
|
|
* Crawlers will inadvertently think the canonicalUrl in the RSC payload should be crawled
|
|
* when our intention is to just seed the router state with the current URL.
|
|
* This function splits up the pathname so that we can later join it on
|
|
* when we're ready to consume the path.
|
|
*/ function prepareInitialCanonicalUrl(url) {
|
|
return (url.pathname + url.search).split('/');
|
|
}
|
|
function getRenderedSearch(query) {
|
|
// Inlined implementation of querystring.encode, which is not available in
|
|
// the Edge runtime.
|
|
const pairs = [];
|
|
for(const key in query){
|
|
const value = query[key];
|
|
if (value == null) continue;
|
|
if (Array.isArray(value)) {
|
|
for (const v of value){
|
|
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`);
|
|
}
|
|
} else {
|
|
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
}
|
|
}
|
|
// The result should match the format of a web URL's `search` property, since
|
|
// this is the format that's stored in the App Router state.
|
|
// TODO: We're a bit inconsistent about this. The x-nextjs-rewritten-query
|
|
// header omits the leading question mark. Should refactor to always do
|
|
// that instead.
|
|
if (pairs.length === 0) {
|
|
// If the search string is empty, return an empty string.
|
|
return '';
|
|
}
|
|
// Prepend '?' to the search params string.
|
|
return '?' + pairs.join('&');
|
|
}
|
|
// This is the data necessary to render <AppRouter /> when no SSR errors are encountered
|
|
async function getRSCPayload(tree, ctx, options) {
|
|
var _ctx_renderOpts_prefetchHints;
|
|
const { is404, staleTimeIterable, staticStageByteLengthPromise, runtimePrefetchStream } = options;
|
|
const injectedCSS = new Set();
|
|
const injectedJS = new Set();
|
|
const injectedFontPreloadTags = new Set();
|
|
let missingSlots;
|
|
// We only track missing parallel slots in development
|
|
if (process.env.__NEXT_DEV_SERVER) {
|
|
missingSlots = new Set();
|
|
}
|
|
const { getDynamicParamFromSegment, query, appUsingSizeAdjustment, componentMod: { createMetadataComponents, createElement, Fragment }, url, workStore } = ctx;
|
|
const hints = ((_ctx_renderOpts_prefetchHints = ctx.renderOpts.prefetchHints) == null ? void 0 : _ctx_renderOpts_prefetchHints[ctx.pagePath]) ?? null;
|
|
const initialTree = await createFlightRouterStateFromLoaderTree(tree, hints, getDynamicParamFromSegment, query);
|
|
const serveStreamingMetadata = !!ctx.renderOpts.serveStreamingMetadata;
|
|
const hasGlobalNotFound = !!tree[2]['global-not-found'];
|
|
const metadataIsRuntimePrefetchable = await anySegmentHasRuntimePrefetchEnabled(tree);
|
|
const { Viewport, Metadata, MetadataOutlet } = createMetadataComponents({
|
|
tree,
|
|
// When it's using global-not-found, metadata errorType is undefined, which will retrieve the
|
|
// metadata from the page.
|
|
// When it's using not-found, metadata errorType is 'not-found', which will retrieve the
|
|
// metadata from the not-found.js boundary.
|
|
// TODO: remove this condition and keep it undefined when global-not-found is stabilized.
|
|
errorType: is404 && !hasGlobalNotFound ? 'not-found' : undefined,
|
|
parsedQuery: query,
|
|
pathname: url.pathname,
|
|
metadataContext: createMetadataContext(ctx.renderOpts),
|
|
interpolatedParams: ctx.interpolatedParams,
|
|
serveStreamingMetadata,
|
|
isRuntimePrefetchable: metadataIsRuntimePrefetchable
|
|
});
|
|
const preloadCallbacks = [];
|
|
const seedData = await createComponentTree({
|
|
ctx,
|
|
loaderTree: tree,
|
|
parentParams: {},
|
|
parentOptionalCatchAllParamName: null,
|
|
parentRuntimePrefetchable: false,
|
|
injectedCSS,
|
|
injectedJS,
|
|
injectedFontPreloadTags,
|
|
rootLayoutIncluded: false,
|
|
missingSlots,
|
|
preloadCallbacks,
|
|
authInterrupts: ctx.renderOpts.experimental.authInterrupts,
|
|
MetadataOutlet
|
|
});
|
|
// When the `vary` response header is present with `Next-URL`, that means there's a chance
|
|
// it could respond differently if there's an interception route. We provide this information
|
|
// to `AppRouter` so that it can properly seed the prefetch cache with a prefix, if needed.
|
|
// In dev, the Vary header may not reliably reflect whether a route can
|
|
// be intercepted, because interception routes are compiled on demand.
|
|
// Default to true so the client doesn't cache a stale Fallback entry.
|
|
const varyHeader = ctx.res.getHeader('vary');
|
|
const couldBeIntercepted = !!process.env.__NEXT_DEV_SERVER || typeof varyHeader === 'string' && varyHeader.includes(NEXT_URL);
|
|
const initialHead = createElement(Fragment, {
|
|
key: flightDataPathHeadKey
|
|
}, createElement(NonIndex, {
|
|
createElement,
|
|
pagePath: ctx.pagePath,
|
|
statusCode: ctx.res.statusCode,
|
|
isPossibleServerAction: ctx.isPossibleServerAction
|
|
}), createElement(Viewport, null), createElement(Metadata, null), appUsingSizeAdjustment ? createElement('meta', {
|
|
name: 'next-size-adjust',
|
|
content: ''
|
|
}) : null);
|
|
const { GlobalError, styles: globalErrorStyles } = await getGlobalErrorStyles(tree, ctx);
|
|
// Assume the head we're rendering contains only partial data if PPR is
|
|
// enabled and this is a statically generated response. This is used by the
|
|
// client Segment Cache after a prefetch to determine if it can skip the
|
|
// second request to fill in the dynamic data.
|
|
//
|
|
// See similar comment in create-component-tree.tsx for more context.
|
|
const isPossiblyPartialHead = 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
|
|
P: createElement(Preloads, {
|
|
preloadCallbacks: preloadCallbacks
|
|
}),
|
|
c: prepareInitialCanonicalUrl(url),
|
|
q: getRenderedSearch(query),
|
|
i: !!couldBeIntercepted,
|
|
f: [
|
|
[
|
|
initialTree,
|
|
seedData,
|
|
initialHead,
|
|
isPossiblyPartialHead
|
|
]
|
|
],
|
|
m: missingSlots,
|
|
G: [
|
|
GlobalError,
|
|
globalErrorStyles
|
|
],
|
|
// Tells the client whether this route supports per-segment prefetching.
|
|
// With Cache Components, all routes support it. Without it, only fully
|
|
// static pages do, because their per-segment prefetch responses are
|
|
// generated during static generation (build or ISR).
|
|
S: workStore.isStaticGeneration || ctx.renderOpts.cacheComponents,
|
|
h: getMetadataVaryParamsThenable(),
|
|
s: staleTimeIterable,
|
|
l: staticStageByteLengthPromise,
|
|
p: runtimePrefetchStream,
|
|
// Include the per-page dynamic stale time from unstable_dynamicStaleTime, but
|
|
// only for dynamic renders. The client treats its presence as
|
|
// authoritative.
|
|
// TODO: Move this to the prefetch hints file so we don't have to walk
|
|
// the tree on every render.
|
|
d: !workStore.isStaticGeneration ? await getDynamicStaleTime(tree) ?? undefined : undefined
|
|
});
|
|
}
|
|
/**
|
|
* Preload calls (such as `ReactDOM.preloadStyle` and `ReactDOM.preloadFont`) need to be called during rendering
|
|
* in order to create the appropriate preload tags in the DOM, otherwise they're a no-op. Since we invoke
|
|
* renderToReadableStream with a function that returns component props rather than a component itself, we use
|
|
* this component to "render " the preload calls.
|
|
*/ function Preloads({ preloadCallbacks }) {
|
|
preloadCallbacks.forEach((preloadFn)=>preloadFn());
|
|
return null;
|
|
}
|
|
// This is the data necessary to render <AppRouter /> when an error state is triggered
|
|
async function getErrorRSCPayload(tree, ctx, ssrError, errorType) {
|
|
var _ctx_renderOpts_prefetchHints;
|
|
const { getDynamicParamFromSegment, query, componentMod: { createMetadataComponents, createElement, Fragment }, url, workStore } = ctx;
|
|
const serveStreamingMetadata = !!ctx.renderOpts.serveStreamingMetadata;
|
|
const metadataIsRuntimePrefetchable = await anySegmentHasRuntimePrefetchEnabled(tree);
|
|
const { Viewport, Metadata } = createMetadataComponents({
|
|
tree,
|
|
parsedQuery: query,
|
|
pathname: url.pathname,
|
|
metadataContext: createMetadataContext(ctx.renderOpts),
|
|
errorType,
|
|
interpolatedParams: ctx.interpolatedParams,
|
|
serveStreamingMetadata: serveStreamingMetadata,
|
|
isRuntimePrefetchable: metadataIsRuntimePrefetchable
|
|
});
|
|
const initialHead = createElement(Fragment, {
|
|
key: flightDataPathHeadKey
|
|
}, createElement(NonIndex, {
|
|
createElement,
|
|
pagePath: ctx.pagePath,
|
|
statusCode: ctx.res.statusCode,
|
|
isPossibleServerAction: ctx.isPossibleServerAction
|
|
}), createElement(Viewport, null), process.env.__NEXT_DEV_SERVER && createElement('meta', {
|
|
name: 'next-error',
|
|
content: 'not-found'
|
|
}), createElement(Metadata, null));
|
|
const errorHints = ((_ctx_renderOpts_prefetchHints = ctx.renderOpts.prefetchHints) == null ? void 0 : _ctx_renderOpts_prefetchHints[ctx.pagePath]) ?? null;
|
|
const initialTree = await createFlightRouterStateFromLoaderTree(tree, errorHints, getDynamicParamFromSegment, query);
|
|
let err = undefined;
|
|
if (ssrError) {
|
|
err = isError(ssrError) ? ssrError : Object.defineProperty(new Error(ssrError + ''), "__NEXT_ERROR_CODE", {
|
|
value: "E394",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
// For metadata notFound error there's no global not found boundary on top
|
|
// so we create a not found page with AppRouter
|
|
const seedData = [
|
|
createElement('html', {
|
|
id: '__next_error__'
|
|
}, createElement('head', null), createElement('body', null, process.env.__NEXT_DEV_SERVER && err ? createElement('template', {
|
|
'data-next-error-message': err.message,
|
|
'data-next-error-digest': 'digest' in err ? err.digest : '',
|
|
'data-next-error-stack': err.stack
|
|
}) : null)),
|
|
{},
|
|
null,
|
|
false,
|
|
null
|
|
];
|
|
const { GlobalError, styles: globalErrorStyles } = await getGlobalErrorStyles(tree, ctx);
|
|
const isPossiblyPartialHead = workStore.isStaticGeneration && ctx.renderOpts.experimental.isRoutePPREnabled === true;
|
|
return maybeAppendBuildIdToRSCPayload(ctx, {
|
|
c: prepareInitialCanonicalUrl(url),
|
|
q: getRenderedSearch(query),
|
|
m: undefined,
|
|
i: false,
|
|
f: [
|
|
[
|
|
initialTree,
|
|
seedData,
|
|
initialHead,
|
|
isPossiblyPartialHead
|
|
]
|
|
],
|
|
G: [
|
|
GlobalError,
|
|
globalErrorStyles
|
|
],
|
|
// Tells the client whether this route supports per-segment prefetching.
|
|
// With Cache Components, all routes support it. Without it, only fully
|
|
// static pages do, because their per-segment prefetch responses are
|
|
// generated during static generation (build or ISR).
|
|
S: workStore.isStaticGeneration || ctx.renderOpts.cacheComponents,
|
|
h: getMetadataVaryParamsThenable()
|
|
});
|
|
}
|
|
// This component must run in an SSR context. It will render the RSC root component
|
|
function App({ reactServerStream, reactDebugStream, debugEndTime, preinitScripts, ServerInsertedHTMLProvider, nonce, images }) {
|
|
preinitScripts();
|
|
const response = ReactClient.use(getFlightStream(reactServerStream, reactDebugStream, debugEndTime, nonce));
|
|
const initialState = createInitialRouterState({
|
|
// This is not used during hydration, so we don't have to pass a
|
|
// real timestamp.
|
|
navigatedAt: -1,
|
|
initialRSCPayload: response,
|
|
// location is not initialized in the SSR render
|
|
// it's set to window.location during hydration
|
|
location: null
|
|
});
|
|
const actionQueue = createMutableActionQueue(initialState, null);
|
|
const { HeadManagerContext } = require('../../shared/lib/head-manager-context.shared-runtime');
|
|
return /*#__PURE__*/ _jsx(HeadManagerContext.Provider, {
|
|
value: {
|
|
appDir: true,
|
|
nonce
|
|
},
|
|
children: /*#__PURE__*/ _jsx(ImageConfigContext.Provider, {
|
|
value: images ?? imageConfigDefault,
|
|
children: /*#__PURE__*/ _jsx(ServerInsertedHTMLProvider, {
|
|
children: /*#__PURE__*/ _jsx(AppRouter, {
|
|
actionQueue: actionQueue,
|
|
globalErrorState: response.G
|
|
})
|
|
})
|
|
})
|
|
});
|
|
/* eslint-enable @next/internal/no-ambiguous-jsx -- React Client */ }
|
|
// @TODO our error stream should be probably just use the same root component. But it was previously
|
|
// different I don't want to figure out if that is meaningful at this time so just keeping the behavior
|
|
// consistent for now.
|
|
function ErrorApp({ reactServerStream, preinitScripts, ServerInsertedHTMLProvider, nonce, images }) {
|
|
/* eslint-disable @next/internal/no-ambiguous-jsx -- React Client */ preinitScripts();
|
|
const response = ReactClient.use(getFlightStream(reactServerStream, undefined, undefined, nonce));
|
|
const initialState = createInitialRouterState({
|
|
// This is not used during hydration, so we don't have to pass a
|
|
// real timestamp.
|
|
navigatedAt: -1,
|
|
initialRSCPayload: response,
|
|
// location is not initialized in the SSR render
|
|
// it's set to window.location during hydration
|
|
location: null
|
|
});
|
|
const actionQueue = createMutableActionQueue(initialState, null);
|
|
return /*#__PURE__*/ _jsx(ImageConfigContext.Provider, {
|
|
value: images ?? imageConfigDefault,
|
|
children: /*#__PURE__*/ _jsx(ServerInsertedHTMLProvider, {
|
|
children: /*#__PURE__*/ _jsx(AppRouter, {
|
|
actionQueue: actionQueue,
|
|
globalErrorState: response.G
|
|
})
|
|
})
|
|
});
|
|
/* eslint-enable @next/internal/no-ambiguous-jsx -- React Client */ }
|
|
async function renderToHTMLOrFlightImpl(req, res, url, pagePath, query, renderOpts, workStore, parsedRequestHeaders, postponedState, serverComponentsHmrCache, sharedContext, interpolatedParams, fallbackRouteParams) {
|
|
const isNotFoundPath = pagePath === '/404';
|
|
if (isNotFoundPath) {
|
|
res.statusCode = 404;
|
|
}
|
|
// A unique request timestamp used by development to ensure that it's
|
|
// consistent and won't change during this request. This is important to
|
|
// avoid that resources can be deduped by React Float if the same resource is
|
|
// rendered or preloaded multiple times: `<link href="a.css?v={Date.now()}"/>`.
|
|
const requestTimestamp = Date.now();
|
|
const { ComponentMod, nextFontManifest, serverActions, assetPrefix = '', enableTainting, cacheComponents, setIsrStatus } = renderOpts;
|
|
const { cachedNavigations } = renderOpts.experimental;
|
|
// We need to expose the bundled `require` API globally for
|
|
// react-server-dom-webpack. This is a hack until we find a better way.
|
|
if (ComponentMod.__next_app__) {
|
|
const instrumented = wrapClientComponentLoader(ComponentMod);
|
|
// When we are prerendering if there is a cacheSignal for tracking
|
|
// cache reads we track calls to `loadChunk` and `require`. This allows us
|
|
// to treat chunk/module loading with similar semantics as cache reads to avoid
|
|
// module loading from causing a prerender to abort too early.
|
|
const shouldTrackModuleLoading = ()=>{
|
|
if (!cacheComponents) {
|
|
return false;
|
|
}
|
|
if (process.env.__NEXT_DEV_SERVER) {
|
|
return true;
|
|
}
|
|
const workUnitStore = workUnitAsyncStorage.getStore();
|
|
if (!workUnitStore) {
|
|
return false;
|
|
}
|
|
switch(workUnitStore.type){
|
|
case 'prerender':
|
|
case 'prerender-client':
|
|
case 'validation-client':
|
|
case 'prerender-runtime':
|
|
case 'cache':
|
|
case 'private-cache':
|
|
return true;
|
|
case 'prerender-ppr':
|
|
case 'prerender-legacy':
|
|
case 'request':
|
|
case 'unstable-cache':
|
|
case 'generate-static-params':
|
|
return false;
|
|
default:
|
|
workUnitStore;
|
|
}
|
|
};
|
|
const __next_require__ = (...args)=>{
|
|
const exportsOrPromise = instrumented.require(...args);
|
|
if (shouldTrackModuleLoading()) {
|
|
// requiring an async module returns a promise.
|
|
trackPendingImport(exportsOrPromise);
|
|
}
|
|
return exportsOrPromise;
|
|
};
|
|
// @ts-expect-error
|
|
globalThis.__next_require__ = __next_require__;
|
|
const __next_chunk_load__ = (...args)=>{
|
|
const loadingChunk = instrumented.loadChunk(...args);
|
|
if (shouldTrackModuleLoading()) {
|
|
trackPendingChunkLoad(loadingChunk);
|
|
}
|
|
return loadingChunk;
|
|
};
|
|
// @ts-expect-error
|
|
globalThis.__next_chunk_load__ = __next_chunk_load__;
|
|
}
|
|
if (process.env.__NEXT_DEV_SERVER && setIsrStatus && !cacheComponents) {
|
|
// Reset the ISR status at start of request.
|
|
const { pathname } = new URL(req.url || '/', 'http://n');
|
|
setIsrStatus(pathname, // Only pages using the Node runtime can use ISR, Edge is always dynamic.
|
|
process.env.NEXT_RUNTIME === 'edge' ? false : undefined);
|
|
}
|
|
if (// The type check here ensures that `req` is correctly typed, and the
|
|
// environment variable check provides dead code elimination.
|
|
process.env.NEXT_RUNTIME !== 'edge' && isNodeNextRequest(req)) {
|
|
res.onClose(()=>{
|
|
// We stop tracking fetch metrics when the response closes, since we
|
|
// report them at that time.
|
|
workStore.shouldTrackFetchMetrics = false;
|
|
});
|
|
req.originalRequest.on('end', ()=>{
|
|
if ('performance' in globalThis) {
|
|
const metrics = getClientComponentLoaderMetrics({
|
|
reset: true
|
|
});
|
|
if (metrics) {
|
|
getTracer().startSpan(NextNodeServerSpan.clientComponentLoading, {
|
|
startTime: metrics.clientComponentLoadStart,
|
|
attributes: {
|
|
'next.clientComponentLoadCount': metrics.clientComponentLoadCount,
|
|
'next.span_type': NextNodeServerSpan.clientComponentLoading
|
|
}
|
|
}).end(metrics.clientComponentLoadStart + metrics.clientComponentLoadTimes);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
const metadata = {
|
|
statusCode: isNotFoundPath ? 404 : undefined
|
|
};
|
|
const appUsingSizeAdjustment = !!(nextFontManifest == null ? void 0 : nextFontManifest.appUsingSizeAdjust);
|
|
ComponentMod.patchFetch();
|
|
// Pull out the hooks/references from the component.
|
|
const { routeModule: { userland: { loaderTree } }, taintObjectReference } = ComponentMod;
|
|
if (enableTainting) {
|
|
taintObjectReference('Do not pass process.env to Client Components since it will leak sensitive data', process.env);
|
|
}
|
|
workStore.fetchMetrics = [];
|
|
metadata.fetchMetrics = workStore.fetchMetrics;
|
|
// don't modify original query object
|
|
query = {
|
|
...query
|
|
};
|
|
stripInternalQueries(query);
|
|
const { isStaticGeneration } = workStore;
|
|
let requestId;
|
|
let htmlRequestId;
|
|
const { flightRouterState, isPrefetchRequest, isRuntimePrefetchRequest, isRSCRequest, isHmrRefresh, nonce } = parsedRequestHeaders;
|
|
if (parsedRequestHeaders.requestId) {
|
|
// If the client has provided a request ID (in development mode), we use it.
|
|
requestId = parsedRequestHeaders.requestId;
|
|
} else {
|
|
// Otherwise we generate a new request ID.
|
|
if (isStaticGeneration) {
|
|
requestId = Buffer.from(await crypto.subtle.digest('SHA-1', Buffer.from(req.url))).toString('hex');
|
|
} else if (process.env.NEXT_RUNTIME === 'edge') {
|
|
requestId = crypto.randomUUID();
|
|
} else {
|
|
requestId = require('next/dist/compiled/nanoid').nanoid();
|
|
}
|
|
}
|
|
// If the client has provided an HTML request ID, we use it to associate the
|
|
// request with the HTML document from which it originated, which is used to
|
|
// send debug information to the associated WebSocket client. Otherwise, this
|
|
// is the request for the HTML document, so we use the request ID also as the
|
|
// HTML request ID.
|
|
htmlRequestId = parsedRequestHeaders.htmlRequestId || requestId;
|
|
const getDynamicParamFromSegment = makeGetDynamicParamFromSegment(interpolatedParams, fallbackRouteParams, renderOpts.experimental.optimisticRouting);
|
|
const isPossibleActionRequest = getIsPossibleServerAction(req);
|
|
// For implicit tags, we use the resolved pathname which has dynamic params
|
|
// interpolated, is decoded, and has trailing slash removed.
|
|
const resolvedPathname = getRequestMeta(req, 'resolvedPathname');
|
|
if (!resolvedPathname) {
|
|
throw Object.defineProperty(new InvariantError('resolvedPathname must be set in request metadata'), "__NEXT_ERROR_CODE", {
|
|
value: "E981",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
const implicitTags = await getImplicitTags(workStore.page, resolvedPathname, fallbackRouteParams);
|
|
const ctx = {
|
|
componentMod: ComponentMod,
|
|
url,
|
|
renderOpts,
|
|
workStore,
|
|
parsedRequestHeaders,
|
|
getDynamicParamFromSegment,
|
|
interpolatedParams,
|
|
query,
|
|
isPrefetch: isPrefetchRequest,
|
|
isPossibleServerAction: isPossibleActionRequest,
|
|
requestTimestamp,
|
|
appUsingSizeAdjustment,
|
|
flightRouterState,
|
|
requestId,
|
|
htmlRequestId,
|
|
pagePath,
|
|
assetPrefix,
|
|
isNotFoundPath,
|
|
nonce,
|
|
res,
|
|
sharedContext,
|
|
implicitTags
|
|
};
|
|
getTracer().setRootSpanAttribute('next.route', pagePath);
|
|
if (isStaticGeneration) {
|
|
// We're either building or revalidating. In either case we need to
|
|
// prerender our page rather than render it.
|
|
const prerenderToStreamWithTracing = getTracer().wrap(AppRenderSpan.getBodyResult, {
|
|
spanName: `prerender route (app) ${pagePath}`,
|
|
attributes: {
|
|
'next.route': pagePath
|
|
}
|
|
}, prerenderToStream);
|
|
const response = await prerenderToStreamWithTracing(req, res, ctx, metadata, loaderTree, fallbackRouteParams);
|
|
// If we're debugging partial prerendering, print all the dynamic API accesses
|
|
// that occurred during the render.
|
|
// @TODO move into renderToStream function
|
|
if (response.dynamicAccess && accessedDynamicData(response.dynamicAccess) && renderOpts.isDebugDynamicAccesses) {
|
|
warn('The following dynamic usage was detected:');
|
|
for (const access of formatDynamicAPIAccesses(response.dynamicAccess)){
|
|
warn(access);
|
|
}
|
|
}
|
|
// If we encountered any unexpected errors during build we fail the
|
|
// prerendering phase and the build.
|
|
if (workStore.invalidDynamicUsageError) {
|
|
logDisallowedDynamicError(workStore, workStore.invalidDynamicUsageError);
|
|
throw new StaticGenBailoutError();
|
|
}
|
|
if (response.digestErrorsMap.size) {
|
|
const buildFailingError = response.digestErrorsMap.values().next().value;
|
|
if (buildFailingError) throw buildFailingError;
|
|
}
|
|
// Pick first userland SSR error, which is also not a RSC error.
|
|
if (response.ssrErrors.length) {
|
|
const buildFailingError = response.ssrErrors.find((err)=>isUserLandError(err));
|
|
if (buildFailingError) throw buildFailingError;
|
|
}
|
|
const options = {
|
|
metadata,
|
|
contentType: HTML_CONTENT_TYPE_HEADER
|
|
};
|
|
// If we have pending revalidates, wait until they are all resolved.
|
|
const maybeRevalidatesPromise = executeRevalidates(workStore);
|
|
if (maybeRevalidatesPromise !== false) {
|
|
const revalidatesPromise = maybeRevalidatesPromise.finally(()=>{
|
|
if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
|
|
console.log('pending revalidates promise finished for:', url.href);
|
|
}
|
|
});
|
|
if (renderOpts.waitUntil) {
|
|
renderOpts.waitUntil(revalidatesPromise);
|
|
} else {
|
|
options.waitUntil = revalidatesPromise;
|
|
}
|
|
}
|
|
applyMetadataFromPrerenderResult(response, metadata, workStore);
|
|
if (response.renderResumeDataCache) {
|
|
metadata.renderResumeDataCache = response.renderResumeDataCache;
|
|
}
|
|
const streamString = await streamToString(response.stream);
|
|
const result = new RenderResult(streamString, options);
|
|
// Run build-time instant validation if the page has instant configs
|
|
// TODO(instant-validation-build): This is not a great place to wire this in.
|
|
if (workStore.cacheComponentsEnabled && workStore.isBuildTimePrerendering && renderOpts.runInstantValidation && await anySegmentNeedsInstantValidationInBuild(loaderTree)) {
|
|
// Throws StaticGenBailoutError if validation failed.
|
|
await validateInstantConfigsInBuild(ctx, response.renderResumeDataCache ?? null);
|
|
}
|
|
return result;
|
|
} else {
|
|
// We're rendering dynamically
|
|
const renderResumeDataCache = renderOpts.renderResumeDataCache ?? (postponedState == null ? void 0 : postponedState.renderResumeDataCache) ?? null;
|
|
const rootParams = getRootParams(loaderTree, ctx.getDynamicParamFromSegment);
|
|
const fallbackParams = getRequestMeta(req, 'fallbackParams') || null;
|
|
const createRequestStore = createRequestStoreForRender.bind(null, req, res, url, rootParams, implicitTags, renderOpts.onUpdateCookies, renderOpts.previewProps, isHmrRefresh, serverComponentsHmrCache, renderResumeDataCache, fallbackParams);
|
|
const requestStore = createRequestStore();
|
|
if (process.env.__NEXT_DEV_SERVER && setIsrStatus && !cacheComponents && // Only pages using the Node runtime can use ISR, so we only need to
|
|
// update the status for those.
|
|
// The type check here ensures that `req` is correctly typed, and the
|
|
// environment variable check provides dead code elimination.
|
|
process.env.NEXT_RUNTIME !== 'edge' && isNodeNextRequest(req)) {
|
|
req.originalRequest.on('end', ()=>{
|
|
const { pathname } = new URL(req.url || '/', 'http://n');
|
|
const isStatic = !requestStore.usedDynamic && !workStore.forceDynamic;
|
|
setIsrStatus(pathname, isStatic);
|
|
});
|
|
}
|
|
if (isRSCRequest) {
|
|
if (isRuntimePrefetchRequest) {
|
|
return generateRuntimePrefetchResult(req, ctx, requestStore);
|
|
} else {
|
|
if (process.env.__NEXT_DEV_SERVER && process.env.NEXT_RUNTIME !== 'edge' && cacheComponents) {
|
|
return generateDynamicFlightRenderResultWithStagesInDev(req, ctx, requestStore, createRequestStore, fallbackParams);
|
|
} else if (cacheComponents && cachedNavigations) {
|
|
return generateStagedDynamicFlightRenderResult(req, ctx, requestStore);
|
|
} else {
|
|
return generateDynamicFlightRenderResult(req, ctx, requestStore);
|
|
}
|
|
}
|
|
}
|
|
let didExecuteServerAction = false;
|
|
let formState = null;
|
|
if (isPossibleActionRequest) {
|
|
// For action requests, we handle them differently with a special render result.
|
|
const actionRequestResult = await handleAction({
|
|
req,
|
|
res,
|
|
ComponentMod,
|
|
generateFlight: generateDynamicFlightRenderResult,
|
|
workStore,
|
|
requestStore,
|
|
serverActions,
|
|
ctx,
|
|
metadata
|
|
});
|
|
if (actionRequestResult) {
|
|
if (actionRequestResult.type === 'not-found') {
|
|
const notFoundLoaderTree = createNotFoundLoaderTree(loaderTree);
|
|
res.statusCode = 404;
|
|
metadata.statusCode = 404;
|
|
const stream = await renderToStream(requestStore, req, res, ctx, notFoundLoaderTree, formState, postponedState, metadata, undefined, fallbackParams);
|
|
return new RenderResult(stream, {
|
|
metadata,
|
|
contentType: HTML_CONTENT_TYPE_HEADER
|
|
});
|
|
} else if (actionRequestResult.type === 'done') {
|
|
if (actionRequestResult.result) {
|
|
actionRequestResult.result.assignMetadata(metadata);
|
|
return actionRequestResult.result;
|
|
} else if (actionRequestResult.formState) {
|
|
formState = actionRequestResult.formState;
|
|
}
|
|
}
|
|
}
|
|
didExecuteServerAction = true;
|
|
}
|
|
const options = {
|
|
metadata,
|
|
contentType: HTML_CONTENT_TYPE_HEADER
|
|
};
|
|
const stream = await renderToStream(// NOTE: in Cache Components (dev), if the render is restarted, it will use a different requestStore
|
|
// than the one that we're passing in here.
|
|
requestStore, req, res, ctx, loaderTree, formState, postponedState, metadata, // If we're rendering HTML after an action, we don't want restartable-render behavior
|
|
// because the result should be dynamic, like it is in prod.
|
|
// Also, the request store might have been mutated by the action (e.g. enabling draftMode)
|
|
// and we currently we don't copy changes over when creating a new store,
|
|
// so the restarted render wouldn't be correct.
|
|
didExecuteServerAction ? undefined : createRequestStore, fallbackParams);
|
|
// Invalid dynamic usages should only error the request in development.
|
|
// In production, it's better to produce a result.
|
|
// (the dynamic error will still be thrown inside the component tree, but it's catchable by error boundaries)
|
|
if (workStore.invalidDynamicUsageError && process.env.__NEXT_DEV_SERVER) {
|
|
throw workStore.invalidDynamicUsageError;
|
|
}
|
|
// If we have pending revalidates, wait until they are all resolved.
|
|
const maybeRevalidatesPromise = executeRevalidates(workStore);
|
|
if (maybeRevalidatesPromise !== false) {
|
|
const revalidatesPromise = maybeRevalidatesPromise.finally(()=>{
|
|
if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
|
|
console.log('pending revalidates promise finished for:', url.href);
|
|
}
|
|
});
|
|
if (renderOpts.waitUntil) {
|
|
renderOpts.waitUntil(revalidatesPromise);
|
|
} else {
|
|
options.waitUntil = revalidatesPromise;
|
|
}
|
|
}
|
|
// Create the new render result for the response.
|
|
return new RenderResult(stream, options);
|
|
}
|
|
}
|
|
export const renderToHTMLOrFlight = (req, res, pagePath, query, fallbackRouteParams, renderOpts, serverComponentsHmrCache, sharedContext)=>{
|
|
var _renderOpts_previewProps;
|
|
if (!req.url) {
|
|
throw Object.defineProperty(new Error('Invalid URL'), "__NEXT_ERROR_CODE", {
|
|
value: "E182",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
const url = parseRelativeUrl(req.url, undefined, false);
|
|
// 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.experimental.isRoutePPREnabled === true,
|
|
previewModeId: (_renderOpts_previewProps = renderOpts.previewProps) == null ? void 0 : _renderOpts_previewProps.previewModeId
|
|
});
|
|
const { isPrefetchRequest, previouslyRevalidatedTags, nonce } = parsedRequestHeaders;
|
|
let interpolatedParams;
|
|
let postponedState = null;
|
|
// If provided, the postpone state should be parsed so it can be provided to
|
|
// React.
|
|
if (typeof renderOpts.postponed === 'string') {
|
|
if (fallbackRouteParams) {
|
|
throw Object.defineProperty(new InvariantError('postponed state should not be provided when fallback params are provided'), "__NEXT_ERROR_CODE", {
|
|
value: "E592",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
interpolatedParams = interpolateParallelRouteParams(renderOpts.ComponentMod.routeModule.userland.loaderTree, renderOpts.params ?? {}, pagePath, fallbackRouteParams);
|
|
postponedState = parsePostponedState(renderOpts.postponed, interpolatedParams, renderOpts.experimental.maxPostponedStateSizeBytes);
|
|
} else {
|
|
interpolatedParams = interpolateParallelRouteParams(renderOpts.ComponentMod.routeModule.userland.loaderTree, renderOpts.params ?? {}, pagePath, fallbackRouteParams);
|
|
}
|
|
if ((postponedState == null ? void 0 : postponedState.renderResumeDataCache) && renderOpts.renderResumeDataCache) {
|
|
throw Object.defineProperty(new InvariantError('postponed state and dev warmup immutable resume data cache should not be provided together'), "__NEXT_ERROR_CODE", {
|
|
value: "E589",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
const workStore = createWorkStore({
|
|
page: renderOpts.routeModule.definition.page,
|
|
renderOpts,
|
|
// @TODO move to workUnitStore of type Request
|
|
isPrefetchRequest,
|
|
buildId: sharedContext.buildId,
|
|
previouslyRevalidatedTags,
|
|
nonce
|
|
});
|
|
return workAsyncStorage.run(workStore, // The function to run
|
|
renderToHTMLOrFlightImpl, // all of it's args
|
|
req, res, url, pagePath, query, renderOpts, workStore, parsedRequestHeaders, postponedState, serverComponentsHmrCache, sharedContext, interpolatedParams, fallbackRouteParams);
|
|
};
|
|
function applyMetadataFromPrerenderResult(response, metadata, workStore) {
|
|
if (response.collectedTags) {
|
|
metadata.fetchTags = response.collectedTags.join(',');
|
|
}
|
|
// Let the client router know how long to keep the cached entry around.
|
|
const staleHeader = String(response.collectedStale);
|
|
metadata.headers ??= {};
|
|
metadata.headers[NEXT_ROUTER_STALE_TIME_HEADER] = staleHeader;
|
|
// If force static is specifically set to false, we should not revalidate
|
|
// the page.
|
|
if (workStore.forceStatic === false || response.collectedRevalidate === 0) {
|
|
metadata.cacheControl = {
|
|
revalidate: 0,
|
|
expire: undefined
|
|
};
|
|
} else {
|
|
// Copy the cache control value onto the render result metadata.
|
|
metadata.cacheControl = {
|
|
revalidate: response.collectedRevalidate >= INFINITE_CACHE ? false : response.collectedRevalidate,
|
|
expire: response.collectedExpire >= INFINITE_CACHE ? undefined : response.collectedExpire
|
|
};
|
|
}
|
|
// provide bailout info for debugging
|
|
if (metadata.cacheControl.revalidate === 0) {
|
|
metadata.staticBailoutInfo = {
|
|
description: workStore.dynamicUsageDescription,
|
|
stack: workStore.dynamicUsageStack
|
|
};
|
|
}
|
|
}
|
|
async function renderToStream(requestStore, req, res, ctx, tree, formState, postponedState, metadata, createRequestStore, fallbackParams) {
|
|
/* eslint-disable @next/internal/no-ambiguous-jsx -- React Client */ const { assetPrefix, htmlRequestId, nonce, pagePath, renderOpts, requestId, workStore } = ctx;
|
|
const { basePath, buildManifest, ComponentMod: { createElement }, crossOrigin, experimental, isBuildTimePrerendering = false, onInstrumentationRequestError, page, reactMaxHeadersLength, setReactDebugChannel, shouldWaitOnAllReady, subresourceIntegrityManifest, supportsDynamicResponse, cacheComponents } = renderOpts;
|
|
const { cachedNavigations } = renderOpts.experimental;
|
|
const { ServerInsertedHTMLProvider, renderServerInsertedHTML } = createServerInsertedHTML();
|
|
const getServerInsertedMetadata = createServerInsertedMetadata(nonce);
|
|
const tracingMetadata = getTracedMetadata(getTracer().getTracePropagationData(), experimental.clientTraceMetadata);
|
|
const polyfills = buildManifest.polyfillFiles.filter((polyfill)=>polyfill.endsWith('.js') && !polyfill.endsWith('.module.js')).map((polyfill)=>({
|
|
src: `${assetPrefix}/_next/${polyfill}${getAssetQueryString(ctx, false)}`,
|
|
integrity: subresourceIntegrityManifest == null ? void 0 : subresourceIntegrityManifest[polyfill],
|
|
crossOrigin,
|
|
noModule: true,
|
|
nonce
|
|
}));
|
|
const [preinitScripts, bootstrapScript] = getRequiredScripts(buildManifest, // Why is assetPrefix optional on renderOpts?
|
|
// @TODO make it default empty string on renderOpts and get rid of it from ctx
|
|
assetPrefix, crossOrigin, subresourceIntegrityManifest, getAssetQueryString(ctx, true), nonce, page);
|
|
// In development mode, set the request ID as a global variable, before the
|
|
// bootstrap script is executed, which depends on it during hydration.
|
|
// For MPA navigations (page reload, direct URL entry), the request ID
|
|
// header is not present, so we generate a random one.
|
|
const bootstrapScriptContent = process.env.__NEXT_DEV_SERVER ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}` : undefined;
|
|
// Create the "render route (app)" span manually so we can keep it open during streaming.
|
|
// This is necessary because errors inside Suspense boundaries are reported asynchronously
|
|
// during stream consumption, after a typical wrapped function would have ended the span.
|
|
// Note: We pass the full span name as the first argument since startSpan uses it directly.
|
|
const renderSpan = getTracer().startSpan(`render route (app) ${pagePath}`, {
|
|
attributes: {
|
|
'next.span_name': `render route (app) ${pagePath}`,
|
|
'next.span_type': AppRenderSpan.getBodyResult,
|
|
'next.route': pagePath
|
|
}
|
|
});
|
|
// Helper to end the span with error status (used when throwing from catch blocks)
|
|
const endSpanWithError = (err)=>{
|
|
if (!renderSpan.isRecording()) return;
|
|
if (err instanceof Error) {
|
|
renderSpan.recordException(err);
|
|
renderSpan.setAttribute('error.type', err.name);
|
|
}
|
|
renderSpan.setStatus({
|
|
code: SpanStatusCode.ERROR,
|
|
message: err instanceof Error ? err.message : undefined
|
|
});
|
|
renderSpan.end();
|
|
};
|
|
// Run the rest of the function within the span's context so child spans
|
|
// (like "build component tree", "generateMetadata") are properly parented.
|
|
return getTracer().withSpan(renderSpan, async ()=>{
|
|
const { reactServerErrorsByDigest } = workStore;
|
|
function onHTMLRenderRSCError(err, silenceLog) {
|
|
return onInstrumentationRequestError == null ? void 0 : onInstrumentationRequestError(err, req, createErrorContext(ctx, 'react-server-components'), silenceLog);
|
|
}
|
|
const serverComponentsErrorHandler = createReactServerErrorHandler(process.env.NODE_ENV === 'development', isBuildTimePrerendering, reactServerErrorsByDigest, onHTMLRenderRSCError, renderSpan);
|
|
function onHTMLRenderSSRError(err) {
|
|
// We don't need to silence logs here. onHTMLRenderSSRError won't be called
|
|
// at all if the error was logged before in the RSC error handler.
|
|
const silenceLog = false;
|
|
return onInstrumentationRequestError == null ? void 0 : onInstrumentationRequestError(err, req, createErrorContext(ctx, 'server-rendering'), silenceLog);
|
|
}
|
|
const allCapturedErrors = [];
|
|
const htmlRendererErrorHandler = createHTMLErrorHandler(process.env.NODE_ENV === 'development', isBuildTimePrerendering, reactServerErrorsByDigest, allCapturedErrors, onHTMLRenderSSRError, renderSpan);
|
|
let reactServerResult = null;
|
|
let reactDebugStream;
|
|
const setHeader = res.setHeader.bind(res);
|
|
const appendHeader = res.appendHeader.bind(res);
|
|
const { clientModules } = getClientReferenceManifest();
|
|
try {
|
|
if (process.env.__NEXT_DEV_SERVER && // Edge routes never prerender so we don't have a Prerender environment for anything in edge runtime
|
|
process.env.NEXT_RUNTIME !== 'edge' && // We only have a Prerender environment for projects opted into cacheComponents
|
|
cacheComponents) {
|
|
let debugChannel;
|
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
const getPayload = async (requestStore)=>{
|
|
const payload = await workUnitAsyncStorage.run(requestStore, getRSCPayload, tree, ctx, {
|
|
is404: res.statusCode === 404
|
|
});
|
|
if (isBypassingCachesInDev(requestStore)) {
|
|
// Mark the RSC payload to indicate that caches were bypassed in dev.
|
|
// This lets the client know not to cache anything based on this render.
|
|
if (renderOpts.setCacheStatus) {
|
|
// we know this is available when cacheComponents is enabled, but typeguard to be safe
|
|
renderOpts.setCacheStatus('bypass', htmlRequestId);
|
|
}
|
|
payload._bypassCachesInDev = createElement(WarnForBypassCachesInDev, {
|
|
route: workStore.route
|
|
});
|
|
}
|
|
return payload;
|
|
};
|
|
if (// We only do this flow if we can safely recreate the store from scratch
|
|
// (which is not the case for renders after an action)
|
|
createRequestStore && // We only do this flow if we're not bypassing caches in dev using
|
|
// "disable cache" in devtools or a hard refresh (cache-control: "no-store")
|
|
!isBypassingCachesInDev(requestStore)) {
|
|
const { stream: serverStream, accumulatedChunksPromise, syncInterruptReason, startTime, staticStageEndTime, runtimeStageEndTime, debugChannel: returnedDebugChannel, requestStore: finalRequestStore } = await renderWithRestartOnCacheMissInDev(ctx, requestStore, createRequestStore, getPayload, serverComponentsErrorHandler);
|
|
let validationDebugChannelClient = undefined;
|
|
if (returnedDebugChannel) {
|
|
const [t1, t2] = returnedDebugChannel.clientSide.readable.tee();
|
|
returnedDebugChannel.clientSide.readable = t1;
|
|
validationDebugChannelClient = nodeStreamFromReadableStream(t2);
|
|
}
|
|
consoleAsyncStorage.run({
|
|
dim: true
|
|
}, spawnStaticShellValidationInDev, accumulatedChunksPromise, syncInterruptReason, startTime, staticStageEndTime, runtimeStageEndTime, ctx, finalRequestStore, fallbackParams, validationDebugChannelClient);
|
|
reactServerResult = new ReactServerResult(serverStream);
|
|
requestStore = finalRequestStore;
|
|
debugChannel = returnedDebugChannel;
|
|
} else {
|
|
logValidationSkipped(ctx);
|
|
// We're either bypassing caches or we can't restart the render.
|
|
// Do a dynamic render, but with (basic) environment labels.
|
|
debugChannel = setReactDebugChannel && createDebugChannel();
|
|
const serverStream = await stagedRenderToReadableStreamWithoutCachesInDev(ctx, requestStore, getPayload, {
|
|
onError: serverComponentsErrorHandler,
|
|
filterStackFrame,
|
|
debugChannel: debugChannel == null ? void 0 : debugChannel.serverSide
|
|
});
|
|
reactServerResult = new ReactServerResult(serverStream);
|
|
}
|
|
if (debugChannel && setReactDebugChannel) {
|
|
const [readableSsr, readableBrowser] = debugChannel.clientSide.readable.tee();
|
|
reactDebugStream = readableSsr;
|
|
setReactDebugChannel({
|
|
readable: readableBrowser
|
|
}, htmlRequestId, requestId);
|
|
}
|
|
} else if (cacheComponents && cachedNavigations) {
|
|
// Production Cache Components + Cached Navigations: use staged
|
|
// rendering so the RSC payload includes the static stage byte length
|
|
// (`l` field), enabling the client to cache the static subset during
|
|
// hydration.
|
|
const { renderToReadableStream } = ctx.componentMod;
|
|
const selectStaleTime = createSelectStaleTime(experimental);
|
|
const staleTimeIterable = new StaleTimeIterable();
|
|
// TODO(cached-navs): this assumes that we checked during build that there's no sync IO.
|
|
// but it can happen e.g. after a revalidation or conditionally for a param that wasn't prerendered.
|
|
// we should change this to track sync IO, log an error and advance to dynamic.
|
|
const shouldTrackSyncIO = false;
|
|
const stageController = new StagedRenderingController(null, null, shouldTrackSyncIO);
|
|
requestStore.stale = INFINITE_CACHE;
|
|
requestStore.stagedRendering = stageController;
|
|
trackStaleTime(requestStore, staleTimeIterable, selectStaleTime);
|
|
let resolveStaticStageByteLength;
|
|
const staticStageByteLengthPromise = new Promise((resolve)=>{
|
|
resolveStaticStageByteLength = resolve;
|
|
});
|
|
// If the route has runtime prefetching enabled, spawn a runtime
|
|
// prerender after the resume render fills caches. The result is
|
|
// embedded in the initial RSC payload so the client can cache
|
|
// runtime-prefetchable content during hydration.
|
|
const hasRuntimePrefetch = await anySegmentHasRuntimePrefetchEnabled(tree);
|
|
let runtimePrefetchStream;
|
|
if (hasRuntimePrefetch) {
|
|
const prerenderResumeDataCache = createPrerenderResumeDataCache();
|
|
requestStore.prerenderResumeDataCache = prerenderResumeDataCache;
|
|
const cacheSignal = new CacheSignal();
|
|
trackPendingModules(cacheSignal);
|
|
requestStore.cacheSignal = cacheSignal;
|
|
const runtimePrefetchTransform = new TransformStream();
|
|
runtimePrefetchStream = runtimePrefetchTransform.readable;
|
|
void cacheSignal.cacheReady().then(()=>spawnRuntimePrefetchWithFilledCaches(runtimePrefetchTransform.writable, ctx, prerenderResumeDataCache, requestStore, serverComponentsErrorHandler));
|
|
}
|
|
const RSCPayload = await workUnitAsyncStorage.run(requestStore, getRSCPayload, tree, ctx, {
|
|
is404: res.statusCode === 404,
|
|
staleTimeIterable,
|
|
staticStageByteLengthPromise,
|
|
runtimePrefetchStream
|
|
});
|
|
const flightStream = await runInSequentialTasks(()=>{
|
|
stageController.advanceStage(RenderStage.Static);
|
|
const stream = workUnitAsyncStorage.run(requestStore, renderToReadableStream, RSCPayload, clientModules, {
|
|
onError: serverComponentsErrorHandler,
|
|
filterStackFrame
|
|
});
|
|
const [dynamicStream, staticStream] = stream.tee();
|
|
countStaticStageBytes(staticStream, stageController).then(resolveStaticStageByteLength);
|
|
return dynamicStream;
|
|
}, ()=>{
|
|
// This is a separate task that doesn't advance a stage. It forces
|
|
// draining the microtask queue so that the stale time iterable is
|
|
// closed before we advance to the dynamic stage.
|
|
void finishStaleTimeTracking(staleTimeIterable);
|
|
}, ()=>{
|
|
stageController.advanceStage(RenderStage.Dynamic);
|
|
});
|
|
reactServerResult = new ReactServerResult(flightStream);
|
|
} else {
|
|
// This is a dynamic render. We don't do dynamic tracking because we're not prerendering
|
|
const RSCPayload = await workUnitAsyncStorage.run(requestStore, getRSCPayload, tree, ctx, {
|
|
is404: res.statusCode === 404
|
|
});
|
|
const debugChannel = setReactDebugChannel && createDebugChannel();
|
|
if (debugChannel) {
|
|
const [readableSsr, readableBrowser] = debugChannel.clientSide.readable.tee();
|
|
reactDebugStream = readableSsr;
|
|
setReactDebugChannel({
|
|
readable: readableBrowser
|
|
}, htmlRequestId, requestId);
|
|
}
|
|
reactServerResult = new ReactServerResult(workUnitAsyncStorage.run(requestStore, renderToFlightStream, ctx.componentMod, RSCPayload, clientModules, {
|
|
filterStackFrame,
|
|
onError: serverComponentsErrorHandler,
|
|
debugChannel: debugChannel == null ? void 0 : debugChannel.serverSide
|
|
}));
|
|
}
|
|
// React doesn't start rendering synchronously but we want the RSC render to have a chance to start
|
|
// before we begin SSR rendering because we want to capture any available preload headers so we tick
|
|
// one task before continuing
|
|
await waitAtLeastOneReactRenderTask();
|
|
// If provided, the postpone state should be parsed as JSON so it can be
|
|
// provided to React.
|
|
if (typeof renderOpts.postponed === 'string') {
|
|
if ((postponedState == null ? void 0 : postponedState.type) === DynamicState.DATA) {
|
|
// We have a complete HTML Document in the prerender but we need to
|
|
// still include the new server component render because it was not included
|
|
// in the static prelude.
|
|
const inlinedDataStream = createInlinedDataStream(reactServerResult.tee(), nonce, formState);
|
|
// End the span since there's no async rendering in this path
|
|
if (renderSpan.isRecording()) renderSpan.end();
|
|
return chainStreams(inlinedDataStream, createDocumentClosingStream());
|
|
} else if (postponedState) {
|
|
// We assume we have dynamic HTML requiring a resume render to complete
|
|
const { postponed, preludeState } = getPostponedFromState(postponedState);
|
|
const resumeAppElement = /*#__PURE__*/ _jsx(App, {
|
|
reactServerStream: reactServerResult.tee(),
|
|
reactDebugStream: reactDebugStream,
|
|
debugEndTime: undefined,
|
|
preinitScripts: preinitScripts,
|
|
ServerInsertedHTMLProvider: ServerInsertedHTMLProvider,
|
|
nonce: nonce,
|
|
images: ctx.renderOpts.images
|
|
});
|
|
const getServerInsertedHTML = makeGetServerInsertedHTML({
|
|
polyfills,
|
|
renderServerInsertedHTML,
|
|
serverCapturedErrors: allCapturedErrors,
|
|
basePath,
|
|
tracingMetadata: tracingMetadata
|
|
});
|
|
const { stream: htmlStream, allReady } = await workUnitAsyncStorage.run(requestStore, resumeToFizzStream, resumeAppElement, postponed, {
|
|
onError: htmlRendererErrorHandler,
|
|
nonce
|
|
});
|
|
// End the render span only after React completed rendering (including anything inside Suspense boundaries)
|
|
allReady.finally(()=>{
|
|
if (renderSpan.isRecording()) renderSpan.end();
|
|
});
|
|
return await continueDynamicHTMLResume(htmlStream, {
|
|
delayDataUntilFirstHtmlChunk: preludeState === DynamicHTMLPreludeState.Empty,
|
|
inlinedDataStream: createInlinedDataStream(reactServerResult.consume(), nonce, formState),
|
|
getServerInsertedHTML,
|
|
getServerInsertedMetadata,
|
|
deploymentId: ctx.sharedContext.deploymentId
|
|
});
|
|
}
|
|
}
|
|
// This is a regular dynamic render
|
|
const getServerInsertedHTML = makeGetServerInsertedHTML({
|
|
polyfills,
|
|
renderServerInsertedHTML,
|
|
serverCapturedErrors: allCapturedErrors,
|
|
basePath,
|
|
tracingMetadata: tracingMetadata
|
|
});
|
|
const generateStaticHTML = supportsDynamicResponse !== true || !!shouldWaitOnAllReady;
|
|
const appElement = /*#__PURE__*/ _jsx(App, {
|
|
reactServerStream: reactServerResult.tee(),
|
|
reactDebugStream: reactDebugStream,
|
|
debugEndTime: undefined,
|
|
preinitScripts: preinitScripts,
|
|
ServerInsertedHTMLProvider: ServerInsertedHTMLProvider,
|
|
nonce: nonce,
|
|
images: ctx.renderOpts.images
|
|
});
|
|
const fizzOptions = {
|
|
onError: htmlRendererErrorHandler,
|
|
nonce,
|
|
onHeaders: (headers)=>{
|
|
headers.forEach((value, key)=>{
|
|
appendHeader(key, value);
|
|
});
|
|
},
|
|
maxHeadersLength: reactMaxHeadersLength,
|
|
bootstrapScriptContent,
|
|
bootstrapScripts: [
|
|
bootstrapScript
|
|
],
|
|
formState
|
|
};
|
|
const { stream: htmlStream, allReady } = await workUnitAsyncStorage.run(requestStore, renderToFizzStream, appElement, fizzOptions);
|
|
// End the render span only after React completed rendering (including anything inside Suspense boundaries)
|
|
allReady.finally(()=>{
|
|
if (renderSpan.isRecording()) renderSpan.end();
|
|
});
|
|
return await continueFizzStream(htmlStream, {
|
|
inlinedDataStream: createInlinedDataStream(reactServerResult.consume(), nonce, formState),
|
|
isStaticGeneration: generateStaticHTML,
|
|
allReady,
|
|
deploymentId: ctx.sharedContext.deploymentId,
|
|
getServerInsertedHTML,
|
|
getServerInsertedMetadata,
|
|
validateRootLayout: !!process.env.__NEXT_DEV_SERVER
|
|
});
|
|
} catch (err) {
|
|
if (isStaticGenBailoutError(err) || typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string' && err.message.includes('https://nextjs.org/docs/advanced-features/static-html-export')) {
|
|
// Ensure that "next dev" prints the red error overlay
|
|
endSpanWithError(err);
|
|
throw err;
|
|
}
|
|
// If a bailout made it to this point, it means it wasn't wrapped inside
|
|
// a suspense boundary.
|
|
const shouldBailoutToCSR = isBailoutToCSRError(err);
|
|
if (shouldBailoutToCSR) {
|
|
const stack = getStackWithoutErrorMessage(err);
|
|
error(`${err.reason} should be wrapped in a suspense boundary at page "${pagePath}". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout\n${stack}`);
|
|
endSpanWithError(err);
|
|
throw err;
|
|
}
|
|
let errorType;
|
|
if (isHTTPAccessFallbackError(err)) {
|
|
res.statusCode = getAccessFallbackHTTPStatus(err);
|
|
metadata.statusCode = res.statusCode;
|
|
errorType = getAccessFallbackErrorTypeByStatus(res.statusCode);
|
|
} else if (isRedirectError(err)) {
|
|
errorType = 'redirect';
|
|
res.statusCode = getRedirectStatusCodeFromError(err);
|
|
metadata.statusCode = res.statusCode;
|
|
const redirectUrl = addPathPrefix(getURLFromRedirectError(err), basePath);
|
|
// If there were mutable cookies set, we need to set them on the
|
|
// response.
|
|
const headers = new Headers();
|
|
if (appendMutableCookies(headers, requestStore.mutableCookies)) {
|
|
setHeader('set-cookie', Array.from(headers.values()));
|
|
}
|
|
setHeader('location', redirectUrl);
|
|
} else if (!shouldBailoutToCSR) {
|
|
res.statusCode = 500;
|
|
metadata.statusCode = res.statusCode;
|
|
}
|
|
const [errorPreinitScripts, errorBootstrapScript] = getRequiredScripts(buildManifest, assetPrefix, crossOrigin, subresourceIntegrityManifest, getAssetQueryString(ctx, false), nonce, '/_not-found/page');
|
|
let errorRSCPayload;
|
|
let errorServerStream;
|
|
try {
|
|
errorRSCPayload = await workUnitAsyncStorage.run(requestStore, getErrorRSCPayload, tree, ctx, reactServerErrorsByDigest.has(err.digest) ? null : err, errorType);
|
|
errorServerStream = workUnitAsyncStorage.run(requestStore, renderToFlightStream, ctx.componentMod, errorRSCPayload, clientModules, {
|
|
filterStackFrame,
|
|
onError: serverComponentsErrorHandler
|
|
});
|
|
if (reactServerResult === null) {
|
|
// We errored when we did not have an RSC stream to read from. This is not just a render
|
|
// error, we need to throw early
|
|
endSpanWithError(err);
|
|
throw err;
|
|
}
|
|
} catch (setupErr) {
|
|
endSpanWithError(setupErr);
|
|
throw setupErr;
|
|
}
|
|
try {
|
|
const generateStaticHTML = supportsDynamicResponse !== true || !!shouldWaitOnAllReady;
|
|
const { stream: errorHtmlStream, allReady: errorAllReady } = await workUnitAsyncStorage.run(requestStore, renderToFizzStream, /*#__PURE__*/ _jsx(ErrorApp, {
|
|
reactServerStream: errorServerStream,
|
|
ServerInsertedHTMLProvider: ServerInsertedHTMLProvider,
|
|
preinitScripts: errorPreinitScripts,
|
|
nonce: nonce,
|
|
images: ctx.renderOpts.images
|
|
}), {
|
|
nonce,
|
|
bootstrapScriptContent,
|
|
bootstrapScripts: [
|
|
errorBootstrapScript
|
|
],
|
|
formState
|
|
});
|
|
// End the render span only after React completed rendering (including anything inside Suspense boundaries)
|
|
errorAllReady.finally(()=>{
|
|
if (renderSpan.isRecording()) renderSpan.end();
|
|
});
|
|
return await continueFizzStream(errorHtmlStream, {
|
|
inlinedDataStream: createInlinedDataStream(// This is intentionally using the readable datastream from the
|
|
// main render rather than the flight data from the error page
|
|
// render
|
|
reactServerResult.consume(), nonce, formState),
|
|
isStaticGeneration: generateStaticHTML,
|
|
deploymentId: ctx.sharedContext.deploymentId,
|
|
getServerInsertedHTML: makeGetServerInsertedHTML({
|
|
polyfills,
|
|
renderServerInsertedHTML,
|
|
serverCapturedErrors: [],
|
|
basePath,
|
|
tracingMetadata: tracingMetadata
|
|
}),
|
|
getServerInsertedMetadata,
|
|
validateRootLayout: !!process.env.__NEXT_DEV_SERVER
|
|
});
|
|
} catch (finalErr) {
|
|
if (process.env.__NEXT_DEV_SERVER && isHTTPAccessFallbackError(finalErr)) {
|
|
const { bailOnRootNotFound } = require('../../client/components/dev-root-http-access-fallback-boundary');
|
|
bailOnRootNotFound();
|
|
}
|
|
endSpanWithError(finalErr);
|
|
throw finalErr;
|
|
}
|
|
}
|
|
});
|
|
/* eslint-enable @next/internal/no-ambiguous-jsx */ }
|
|
async function renderWithRestartOnCacheMissInDev(ctx, initialRequestStore, createRequestStore, getPayload, onError) {
|
|
const { htmlRequestId, renderOpts } = ctx;
|
|
const { ComponentMod, setCacheStatus, setReactDebugChannel } = renderOpts;
|
|
let startTime = -Infinity;
|
|
// If the render is restarted, we'll recreate a fresh request store
|
|
let requestStore = initialRequestStore;
|
|
const environmentName = ()=>{
|
|
const currentStage = requestStore.stagedRendering.currentStage;
|
|
switch(currentStage){
|
|
case RenderStage.Before:
|
|
case RenderStage.EarlyStatic:
|
|
case RenderStage.Static:
|
|
return 'Prerender';
|
|
case RenderStage.EarlyRuntime:
|
|
return 'Prefetch';
|
|
case RenderStage.Runtime:
|
|
return 'Prefetchable';
|
|
case RenderStage.Dynamic:
|
|
case RenderStage.Abandoned:
|
|
return 'Server';
|
|
default:
|
|
currentStage;
|
|
throw Object.defineProperty(new InvariantError(`Invalid render stage: ${currentStage}`), "__NEXT_ERROR_CODE", {
|
|
value: "E881",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
};
|
|
//===============================================
|
|
// Initial render
|
|
//===============================================
|
|
// Try to render the page and see if there's any cache misses.
|
|
// If there are, wait for caches to finish and restart the render.
|
|
// This render might end up being used as a prospective render (if there's cache misses),
|
|
// so we need to set it up for filling caches.
|
|
const cacheSignal = new CacheSignal();
|
|
// If we encounter async modules that delay rendering, we'll also need to restart.
|
|
// TODO(restart-on-cache-miss): technically, we only need to wait for pending *server* modules here,
|
|
// but `trackPendingModules` doesn't distinguish between client and server.
|
|
trackPendingModules(cacheSignal);
|
|
const prerenderResumeDataCache = createPrerenderResumeDataCache();
|
|
const initialReactController = new AbortController();
|
|
const initialDataController = new AbortController() // Controls hanging promises we create
|
|
;
|
|
const initialAbandonController = new AbortController() // Controls whether this render is abandoned
|
|
;
|
|
const initialStageController = new StagedRenderingController(initialDataController.signal, initialAbandonController, true // track sync IO
|
|
);
|
|
requestStore.prerenderResumeDataCache = prerenderResumeDataCache;
|
|
// `getRenderResumeDataCache` will fall back to using `prerenderResumeDataCache` as `renderResumeDataCache`,
|
|
// so not having a resume data cache won't break any expectations in case we don't need to restart.
|
|
requestStore.renderResumeDataCache = null;
|
|
requestStore.stagedRendering = initialStageController;
|
|
requestStore.asyncApiPromises = createAsyncApiPromises(initialStageController, requestStore.cookies, requestStore.mutableCookies, requestStore.headers);
|
|
requestStore.cacheSignal = cacheSignal;
|
|
let debugChannel = setReactDebugChannel && createDebugChannel();
|
|
const { clientModules } = getClientReferenceManifest();
|
|
// Note: The stage controller starts out in the `Before` stage,
|
|
// where sync IO does not cause aborts, so it's okay if it happens before render.
|
|
const initialRscPayload = await getPayload(requestStore);
|
|
const initialStreamResult = await runInSequentialTasks(()=>{
|
|
initialStageController.advanceStage(RenderStage.EarlyStatic);
|
|
startTime = performance.now() + performance.timeOrigin;
|
|
const streamPair = workUnitAsyncStorage.run(requestStore, renderToFlightStream, ComponentMod, initialRscPayload, clientModules, {
|
|
onError,
|
|
environmentName,
|
|
startTime,
|
|
filterStackFrame,
|
|
debugChannel: debugChannel == null ? void 0 : debugChannel.serverSide,
|
|
signal: initialReactController.signal
|
|
}).tee();
|
|
// If we abort the render, we want to reject the stage-dependent promises as well.
|
|
// Note that we want to install this listener after the render is started
|
|
// so that it runs after react is finished running its abort code.
|
|
initialReactController.signal.addEventListener('abort', ()=>{
|
|
const { reason } = initialReactController.signal;
|
|
initialDataController.abort(reason);
|
|
}, {
|
|
once: true
|
|
});
|
|
const stream = streamPair[0];
|
|
const accumulatedChunksPromise = accumulateStreamChunks(streamPair[1], initialStageController, initialDataController.signal);
|
|
initialDataController.signal.addEventListener('abort', ()=>{
|
|
accumulatedChunksPromise.catch(()=>{});
|
|
stream.cancel();
|
|
}, {
|
|
once: true
|
|
});
|
|
return {
|
|
stream,
|
|
accumulatedChunksPromise
|
|
};
|
|
}, ()=>{
|
|
if (initialAbandonController.signal.aborted === true) {
|
|
return;
|
|
} else if (cacheSignal.hasPendingReads()) {
|
|
initialAbandonController.abort();
|
|
} else {
|
|
initialStageController.advanceStage(RenderStage.Static);
|
|
}
|
|
}, ()=>{
|
|
if (initialAbandonController.signal.aborted === true) {
|
|
return;
|
|
} else if (cacheSignal.hasPendingReads()) {
|
|
initialAbandonController.abort();
|
|
} else {
|
|
initialStageController.advanceStage(RenderStage.EarlyRuntime);
|
|
}
|
|
}, ()=>{
|
|
if (initialAbandonController.signal.aborted === true) {
|
|
return;
|
|
} else if (cacheSignal.hasPendingReads()) {
|
|
initialAbandonController.abort();
|
|
} else {
|
|
initialStageController.advanceStage(RenderStage.Runtime);
|
|
}
|
|
}, ()=>{
|
|
if (initialAbandonController.signal.aborted === true) {
|
|
return;
|
|
} else if (cacheSignal.hasPendingReads()) {
|
|
initialAbandonController.abort();
|
|
} else {
|
|
initialStageController.advanceStage(RenderStage.Dynamic);
|
|
}
|
|
});
|
|
if (initialStageController.currentStage !== RenderStage.Abandoned) {
|
|
// No cache misses. We can use the result as-is.
|
|
return {
|
|
stream: initialStreamResult.stream,
|
|
accumulatedChunksPromise: initialStreamResult.accumulatedChunksPromise,
|
|
syncInterruptReason: initialStageController.getSyncInterruptReason(),
|
|
startTime,
|
|
staticStageEndTime: initialStageController.getStaticStageEndTime(),
|
|
runtimeStageEndTime: initialStageController.getRuntimeStageEndTime(),
|
|
debugChannel,
|
|
requestStore
|
|
};
|
|
}
|
|
if (process.env.__NEXT_DEV_SERVER && setCacheStatus) {
|
|
setCacheStatus('filling', htmlRequestId);
|
|
}
|
|
// Cache miss. We will use the initial render to fill caches, and discard its result.
|
|
// Then, we can render again with warm caches.
|
|
// TODO(restart-on-cache-miss):
|
|
// This might end up waiting for more caches than strictly necessary,
|
|
// because we can't abort the render yet, and we'll let runtime/dynamic APIs resolve.
|
|
// Ideally we'd only wait for caches that are needed in the static stage.
|
|
// This will be optimized in the future by not allowing runtime/dynamic APIs to resolve.
|
|
await cacheSignal.cacheReady();
|
|
initialReactController.abort();
|
|
//===============================================
|
|
// Final render (restarted)
|
|
//===============================================
|
|
// The initial render acted as a prospective render to warm the caches.
|
|
requestStore = createRequestStore();
|
|
// We are going to render this pass all the way through because we've already
|
|
// filled any caches so we won't be aborting this time.
|
|
const abortSignal = null;
|
|
const finalStageController = new StagedRenderingController(abortSignal, null, true // track sync IO
|
|
);
|
|
// We've filled the caches, so now we can render as usual,
|
|
// without any cache-filling mechanics.
|
|
requestStore.prerenderResumeDataCache = null;
|
|
requestStore.renderResumeDataCache = createRenderResumeDataCache(prerenderResumeDataCache);
|
|
requestStore.stagedRendering = finalStageController;
|
|
requestStore.cacheSignal = null;
|
|
requestStore.asyncApiPromises = createAsyncApiPromises(finalStageController, requestStore.cookies, requestStore.mutableCookies, requestStore.headers);
|
|
// The initial render already wrote to its debug channel.
|
|
// We're not using it, so we need to create a new one.
|
|
debugChannel = setReactDebugChannel && createDebugChannel();
|
|
// Note: The stage controller starts out in the `Before` stage,
|
|
// where sync IO does not cause aborts, so it's okay if it happens before render.
|
|
const finalRscPayload = await getPayload(requestStore);
|
|
const finalStreamResult = await runInSequentialTasks(()=>{
|
|
finalStageController.advanceStage(RenderStage.EarlyStatic);
|
|
startTime = performance.now() + performance.timeOrigin;
|
|
const streamPair = workUnitAsyncStorage.run(requestStore, renderToFlightStream, ComponentMod, finalRscPayload, clientModules, {
|
|
onError,
|
|
environmentName,
|
|
startTime,
|
|
filterStackFrame,
|
|
debugChannel: debugChannel == null ? void 0 : debugChannel.serverSide
|
|
}).tee();
|
|
return {
|
|
stream: streamPair[0],
|
|
accumulatedChunksPromise: accumulateStreamChunks(streamPair[1], finalStageController, null)
|
|
};
|
|
}, ()=>{
|
|
// Static stage
|
|
finalStageController.advanceStage(RenderStage.Static);
|
|
}, ()=>{
|
|
// EarlyRuntime stage
|
|
finalStageController.advanceStage(RenderStage.EarlyRuntime);
|
|
}, ()=>{
|
|
// Runtime stage
|
|
finalStageController.advanceStage(RenderStage.Runtime);
|
|
}, ()=>{
|
|
// Dynamic stage
|
|
finalStageController.advanceStage(RenderStage.Dynamic);
|
|
});
|
|
if (process.env.__NEXT_DEV_SERVER && setCacheStatus) {
|
|
setCacheStatus('filled', htmlRequestId);
|
|
}
|
|
return {
|
|
stream: finalStreamResult.stream,
|
|
accumulatedChunksPromise: finalStreamResult.accumulatedChunksPromise,
|
|
syncInterruptReason: finalStageController.getSyncInterruptReason(),
|
|
startTime,
|
|
staticStageEndTime: finalStageController.getStaticStageEndTime(),
|
|
runtimeStageEndTime: finalStageController.getRuntimeStageEndTime(),
|
|
debugChannel,
|
|
requestStore
|
|
};
|
|
}
|
|
async function accumulateStreamChunks(stream, stageController, signal) {
|
|
const staticChunks = [];
|
|
const runtimeChunks = [];
|
|
const dynamicChunks = [];
|
|
const reader = stream.getReader();
|
|
let cancelled = false;
|
|
function cancel() {
|
|
if (!cancelled) {
|
|
cancelled = true;
|
|
reader.cancel();
|
|
}
|
|
}
|
|
if (signal) {
|
|
signal.addEventListener('abort', cancel, {
|
|
once: true
|
|
});
|
|
}
|
|
try {
|
|
while(!cancelled){
|
|
const { done, value } = await reader.read();
|
|
if (done) {
|
|
cancel();
|
|
break;
|
|
}
|
|
switch(stageController.currentStage){
|
|
case RenderStage.Before:
|
|
throw Object.defineProperty(new InvariantError('Unexpected stream chunk while in Before stage'), "__NEXT_ERROR_CODE", {
|
|
value: "E942",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
case RenderStage.EarlyStatic:
|
|
case RenderStage.Static:
|
|
staticChunks.push(value);
|
|
// fall through
|
|
case RenderStage.EarlyRuntime:
|
|
case RenderStage.Runtime:
|
|
runtimeChunks.push(value);
|
|
// fall through
|
|
case RenderStage.Dynamic:
|
|
dynamicChunks.push(value);
|
|
break;
|
|
case RenderStage.Abandoned:
|
|
break;
|
|
default:
|
|
stageController.currentStage;
|
|
break;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// When we cancel the reader we may reject the read.
|
|
// Only swallow errors caused by our intentional cancel();
|
|
// re-throw unexpected errors to avoid silently returning partial data.
|
|
if (!cancelled) {
|
|
throw err;
|
|
}
|
|
}
|
|
return {
|
|
staticChunks,
|
|
runtimeChunks,
|
|
dynamicChunks
|
|
};
|
|
}
|
|
async function countStaticStageBytes(stream, stageController) {
|
|
let byteLength = 0;
|
|
const reader = stream.getReader();
|
|
stageController.onStage(RenderStage.EarlyRuntime, ()=>{
|
|
reader.cancel();
|
|
});
|
|
while(true){
|
|
const { done, value } = await reader.read();
|
|
if (done) {
|
|
break;
|
|
}
|
|
if (stageController.currentStage <= RenderStage.Static) {
|
|
byteLength += value.byteLength;
|
|
} else {
|
|
reader.cancel();
|
|
break;
|
|
}
|
|
}
|
|
return byteLength;
|
|
}
|
|
function createAsyncApiPromises(stagedRendering, cookies, mutableCookies, headers) {
|
|
return {
|
|
// Runtime APIs (for prefetch segments)
|
|
cookies: stagedRendering.delayUntilStage(RenderStage.Runtime, 'cookies', cookies),
|
|
earlyCookies: stagedRendering.delayUntilStage(RenderStage.EarlyRuntime, 'cookies', cookies),
|
|
mutableCookies: stagedRendering.delayUntilStage(RenderStage.Runtime, 'cookies', mutableCookies),
|
|
earlyMutableCookies: stagedRendering.delayUntilStage(RenderStage.EarlyRuntime, 'cookies', mutableCookies),
|
|
headers: stagedRendering.delayUntilStage(RenderStage.Runtime, 'headers', headers),
|
|
earlyHeaders: stagedRendering.delayUntilStage(RenderStage.EarlyRuntime, 'headers', headers),
|
|
// These are not used directly, but we chain other `params`/`searchParams` promises off of them.
|
|
sharedParamsParent: stagedRendering.delayUntilStage(RenderStage.Runtime, undefined, '<internal params>'),
|
|
earlySharedParamsParent: stagedRendering.delayUntilStage(RenderStage.EarlyRuntime, undefined, '<internal params>'),
|
|
sharedSearchParamsParent: stagedRendering.delayUntilStage(RenderStage.Runtime, undefined, '<internal searchParams>'),
|
|
earlySharedSearchParamsParent: stagedRendering.delayUntilStage(RenderStage.EarlyRuntime, undefined, '<internal searchParams>'),
|
|
connection: stagedRendering.delayUntilStage(RenderStage.Dynamic, 'connection', undefined)
|
|
};
|
|
}
|
|
/**
|
|
* Logs the given messages, and sends the error instances to the browser as an
|
|
* RSC stream, where they can be deserialized and logged (or otherwise presented
|
|
* in the devtools), while leveraging React's capabilities to not only
|
|
* source-map the stack frames (via findSourceMapURL), but also create virtual
|
|
* server modules that allow users to inspect the server source code in the
|
|
* browser.
|
|
*/ async function logMessagesAndSendErrorsToBrowser(messages, ctx) {
|
|
const { htmlRequestId, renderOpts } = ctx;
|
|
const { sendErrorsToBrowser } = renderOpts;
|
|
const errors = [];
|
|
for (const message of messages){
|
|
// Log the error to the CLI. Prevent the logs from being dimmed, which we
|
|
// apply for other logs during the spawned validation.
|
|
consoleAsyncStorage.exit(()=>{
|
|
console.error(message);
|
|
});
|
|
// Error instances are also sent to the browser. We're currently using a
|
|
// non-Error message only in debug build mode as a message that is only
|
|
// meant for the CLI. FIXME: This is a bit spooky action at a distance. We
|
|
// should maybe have a more explicit way of determining which messages
|
|
// should be sent to the browser. Regardless, only real errors with a proper
|
|
// stack make sense to be "replayed" in the browser.
|
|
if (message instanceof Error) {
|
|
errors.push(message);
|
|
}
|
|
}
|
|
if (errors.length > 0) {
|
|
if (!sendErrorsToBrowser) {
|
|
throw Object.defineProperty(new InvariantError('Expected `sendErrorsToBrowser` to be defined in renderOpts.'), "__NEXT_ERROR_CODE", {
|
|
value: "E947",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
// Build a Map of error → error code for errors that have one.
|
|
// React doesn't revive __NEXT_ERROR_CODE during RSC deserialization, so we
|
|
// send it as a side-channel Map. RSC preserves object identity, so the
|
|
// deserialized Map keys will reference the same Error objects.
|
|
const errorCodes = new Map();
|
|
for (const err of errors){
|
|
const code = extractNextErrorCode(err);
|
|
if (code !== undefined) {
|
|
errorCodes.set(err, code);
|
|
}
|
|
}
|
|
const { clientModules } = getClientReferenceManifest();
|
|
const errorsFlightStream = renderToFlightStream(ctx.componentMod, {
|
|
errors,
|
|
errorCodes
|
|
}, clientModules, {
|
|
filterStackFrame
|
|
});
|
|
sendErrorsToBrowser(errorsFlightStream, htmlRequestId);
|
|
}
|
|
}
|
|
function logValidationSkipped(ctx) {
|
|
if (process.env.__NEXT_TEST_MODE && process.env.NEXT_TEST_LOG_VALIDATION) {
|
|
const requestId = ctx.requestId;
|
|
const url = ctx.url.href;
|
|
console.log('<VALIDATION_MESSAGE>' + JSON.stringify({
|
|
type: 'validation_start',
|
|
requestId,
|
|
url
|
|
}) + '</VALIDATION_MESSAGE>');
|
|
console.log('<VALIDATION_MESSAGE>' + JSON.stringify({
|
|
type: 'validation_end',
|
|
requestId,
|
|
url
|
|
}) + '</VALIDATION_MESSAGE>');
|
|
}
|
|
}
|
|
async function spawnStaticShellValidationInDev(...args) {
|
|
if (process.env.__NEXT_TEST_MODE && process.env.NEXT_TEST_LOG_VALIDATION) {
|
|
const ctx = args[5];
|
|
const requestId = ctx.requestId;
|
|
const url = ctx.url.href;
|
|
console.log('<VALIDATION_MESSAGE>' + JSON.stringify({
|
|
type: 'validation_start',
|
|
requestId,
|
|
url
|
|
}) + '</VALIDATION_MESSAGE>');
|
|
try {
|
|
return await spawnStaticShellValidationInDevImpl(...args);
|
|
} finally{
|
|
console.log('<VALIDATION_MESSAGE>' + JSON.stringify({
|
|
type: 'validation_end',
|
|
requestId,
|
|
url
|
|
}) + '</VALIDATION_MESSAGE>');
|
|
}
|
|
} else {
|
|
return await spawnStaticShellValidationInDevImpl(...args);
|
|
}
|
|
}
|
|
/**
|
|
* This function is a fork of prerenderToStream cacheComponents branch.
|
|
* While it doesn't return a stream we want it to have identical
|
|
* prerender semantics to prerenderToStream and should update it
|
|
* in conjunction with any changes to that function.
|
|
*/ async function spawnStaticShellValidationInDevImpl(accumulatedChunksPromise, syncInterruptReason, startTime, staticStageEndTime, runtimeStageEndTime, ctx, requestStore, fallbackRouteParams, debugChannelClient) {
|
|
const debug = process.env.NEXT_PRIVATE_DEBUG_VALIDATION === '1' ? console.log : undefined;
|
|
const { componentMod: ComponentMod, getDynamicParamFromSegment, renderOpts, workStore } = ctx;
|
|
const loaderTree = ComponentMod.routeModule.userland.loaderTree;
|
|
const allowEmptyStaticShell = (renderOpts.allowEmptyStaticShell ?? false) || await isPageAllowedToBlock(loaderTree);
|
|
const rootParams = getRootParams(loaderTree, getDynamicParamFromSegment);
|
|
const hmrRefreshHash = getHmrRefreshHash(requestStore);
|
|
// We don't need to continue the prerender process if we already
|
|
// detected invalid dynamic usage in the initial prerender phase.
|
|
const { invalidDynamicUsageError } = workStore;
|
|
if (invalidDynamicUsageError) {
|
|
return logMessagesAndSendErrorsToBrowser([
|
|
invalidDynamicUsageError
|
|
], ctx);
|
|
}
|
|
if (syncInterruptReason) {
|
|
return logMessagesAndSendErrorsToBrowser([
|
|
syncInterruptReason
|
|
], ctx);
|
|
}
|
|
let debugChunks = null;
|
|
if (debugChannelClient) {
|
|
debugChunks = [];
|
|
debugChannelClient.on('data', (c)=>{
|
|
debugChunks.push(c);
|
|
});
|
|
}
|
|
const accumulatedChunks = await accumulatedChunksPromise;
|
|
const { staticChunks, runtimeChunks, dynamicChunks } = accumulatedChunks;
|
|
const needsInstantValidation = await anySegmentNeedsInstantValidationInDev(loaderTree);
|
|
// `samples` from instant config are only used during build
|
|
const validationSamples = null;
|
|
const validationSampleTracking = null;
|
|
// First we warmup SSR with the runtime chunks. This ensures that when we do
|
|
// the full prerender pass with dynamic tracking module loading won't
|
|
// interrupt the prerender and can properly observe the entire content
|
|
await warmupClientModulesForStagedValidation(// if we're going to be validating prefetches, we'll be rendering some segments in the dynamic stage.
|
|
// otherwise, for static shell validation, we only need to warm up to the runtime stage.
|
|
// we also need to use a different store type, because instant validation allows more APIs to resolve.
|
|
needsInstantValidation ? 'validation-client' : 'prerender-client', needsInstantValidation ? dynamicChunks : runtimeChunks, dynamicChunks, rootParams, fallbackRouteParams, allowEmptyStaticShell, ctx, validationSamples, validationSampleTracking);
|
|
debug == null ? void 0 : debug(`Starting static shell validation...`);
|
|
const runtimeResult = await validateStagedShell(runtimeChunks, dynamicChunks, debugChunks, runtimeStageEndTime, rootParams, fallbackRouteParams, allowEmptyStaticShell, ctx, hmrRefreshHash, trackDynamicHoleInRuntimeShell);
|
|
if (runtimeResult.length > 0) {
|
|
debug == null ? void 0 : debug(`❌ Failed - ${runtimeResult.length} errors from runtime stage`);
|
|
// We have something to report from the runtime validation
|
|
// We can skip the rest
|
|
return logMessagesAndSendErrorsToBrowser(runtimeResult, ctx);
|
|
}
|
|
const staticResult = await validateStagedShell(staticChunks, dynamicChunks, debugChunks, staticStageEndTime, rootParams, fallbackRouteParams, allowEmptyStaticShell, ctx, hmrRefreshHash, trackDynamicHoleInStaticShell);
|
|
if (staticResult.length > 0) {
|
|
debug == null ? void 0 : debug(`❌ Failed - ${staticResult.length} errors from static stage`);
|
|
// We have something to report from the static validation
|
|
// We can skip the rest
|
|
return logMessagesAndSendErrorsToBrowser(staticResult, ctx);
|
|
}
|
|
debug == null ? void 0 : debug(`✅ Passed`);
|
|
if (needsInstantValidation) {
|
|
const instantConfigsResult = await validateInstantConfigs(accumulatedChunks, debugChunks, startTime, rootParams, fallbackRouteParams, ctx, hmrRefreshHash, validationSamples);
|
|
if (instantConfigsResult.length > 0) {
|
|
return logMessagesAndSendErrorsToBrowser(instantConfigsResult, ctx);
|
|
}
|
|
}
|
|
}
|
|
async function warmupClientModulesForStagedValidation(storeType, partialServerChunks, allServerChunks, rootParams, fallbackRouteParams, allowEmptyStaticShell, ctx, validationSamples, validationSampleTracking) {
|
|
const { implicitTags, nonce, workStore } = ctx;
|
|
// Warmup SSR
|
|
const initialClientPrerenderController = new AbortController();
|
|
const initialClientReactController = new AbortController();
|
|
const initialClientRenderController = new AbortController();
|
|
const preinitScripts = ()=>{};
|
|
const { ServerInsertedHTMLProvider } = createServerInsertedHTML();
|
|
let initialClientPrerenderStore;
|
|
if (storeType === 'prerender-client') {
|
|
const store = {
|
|
type: 'prerender-client',
|
|
phase: 'render',
|
|
rootParams,
|
|
fallbackRouteParams,
|
|
implicitTags,
|
|
renderSignal: initialClientRenderController.signal,
|
|
controller: initialClientPrerenderController,
|
|
// For HTML Generation the only cache tracked activity
|
|
// is module loading, which has it's own cache signal
|
|
cacheSignal: null,
|
|
dynamicTracking: null,
|
|
allowEmptyStaticShell,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
// TODO should this be removed from client stores?
|
|
prerenderResumeDataCache: null,
|
|
renderResumeDataCache: null,
|
|
hmrRefreshHash: undefined,
|
|
// Client prerenders don't track server param access
|
|
varyParamsAccumulator: null
|
|
};
|
|
initialClientPrerenderStore = store;
|
|
} else {
|
|
const store = {
|
|
type: 'validation-client',
|
|
phase: 'render',
|
|
rootParams,
|
|
implicitTags,
|
|
renderSignal: initialClientRenderController.signal,
|
|
controller: initialClientPrerenderController,
|
|
// For HTML Generation the only cache tracked activity
|
|
// is module loading, which has it's own cache signal
|
|
cacheSignal: null,
|
|
dynamicTracking: null,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
// TODO should this be removed from client stores?
|
|
prerenderResumeDataCache: null,
|
|
renderResumeDataCache: null,
|
|
hmrRefreshHash: undefined,
|
|
// Client prerenders don't track server param access
|
|
varyParamsAccumulator: null,
|
|
// We're not rendering any validation boundaries yet.
|
|
boundaryState: null,
|
|
validationSamples,
|
|
validationSampleTracking,
|
|
fallbackRouteParams
|
|
};
|
|
initialClientPrerenderStore = store;
|
|
}
|
|
// TODO: maybe conditionally switch between runtime chunks and all chunks?
|
|
// but warming too much should always be fine, just not always necessary
|
|
const serverStream = createNodeStreamWithLateRelease(partialServerChunks, allServerChunks, initialClientReactController.signal);
|
|
const pendingInitialClientResult = workUnitAsyncStorage.run(initialClientPrerenderStore, getClientPrerender, // eslint-disable-next-line @next/internal/no-ambiguous-jsx -- React Client
|
|
/*#__PURE__*/ _jsx(App, {
|
|
reactServerStream: serverStream,
|
|
reactDebugStream: undefined,
|
|
debugEndTime: undefined,
|
|
preinitScripts: preinitScripts,
|
|
ServerInsertedHTMLProvider: ServerInsertedHTMLProvider,
|
|
nonce: nonce,
|
|
images: ctx.renderOpts.images
|
|
}), {
|
|
signal: initialClientReactController.signal,
|
|
onError: (err)=>{
|
|
const digest = getDigestForWellKnownError(err);
|
|
if (digest) {
|
|
return digest;
|
|
}
|
|
if (isReactLargeShellError(err)) {
|
|
// TODO: Aggregate
|
|
console.error(err);
|
|
return undefined;
|
|
}
|
|
if (initialClientReactController.signal.aborted) {
|
|
// These are expected errors that might error the prerender. we ignore them.
|
|
} else if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
|
// We don't normally log these errors because we are going to retry anyway but
|
|
// it can be useful for debugging Next.js itself to get visibility here when needed
|
|
printDebugThrownValueForProspectiveRender(err, workStore.route, Phase.ProspectiveRender);
|
|
}
|
|
}
|
|
});
|
|
// The listener to abort our own render controller must be added after React
|
|
// has added its listener, to ensure that pending I/O is not
|
|
// aborted/rejected too early.
|
|
initialClientReactController.signal.addEventListener('abort', ()=>{
|
|
initialClientRenderController.abort();
|
|
}, {
|
|
once: true
|
|
});
|
|
pendingInitialClientResult.catch((err)=>{
|
|
if (initialClientReactController.signal.aborted || isPrerenderInterruptedError(err)) {
|
|
// These are expected errors that might error the prerender. we ignore them.
|
|
} else if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
|
// We don't normally log these errors because we are going to retry anyway but
|
|
// it can be useful for debugging Next.js itself to get visibility here when needed
|
|
printDebugThrownValueForProspectiveRender(err, workStore.route, Phase.ProspectiveRender);
|
|
}
|
|
});
|
|
// This is mostly needed for dynamic `import()`s in client components.
|
|
// Promises passed to client were already awaited above (assuming that they came from cached functions)
|
|
const cacheSignal = new CacheSignal();
|
|
trackPendingModules(cacheSignal);
|
|
await cacheSignal.cacheReady();
|
|
initialClientReactController.abort();
|
|
}
|
|
async function validateStagedShell(stageChunks, allServerChunks, debugChunks, debugEndTime, rootParams, fallbackRouteParams, allowEmptyStaticShell, ctx, hmrRefreshHash, trackDynamicHole) {
|
|
const { implicitTags, nonce, workStore } = ctx;
|
|
const clientDynamicTracking = createDynamicTrackingState(false //isDebugDynamicAccesses
|
|
);
|
|
const clientReactController = new AbortController();
|
|
const clientRenderController = new AbortController();
|
|
const preinitScripts = ()=>{};
|
|
const { ServerInsertedHTMLProvider } = createServerInsertedHTML();
|
|
const finalClientPrerenderStore = {
|
|
type: 'prerender-client',
|
|
phase: 'render',
|
|
rootParams,
|
|
fallbackRouteParams,
|
|
implicitTags,
|
|
renderSignal: clientRenderController.signal,
|
|
controller: clientReactController,
|
|
// No APIs require a cacheSignal through the workUnitStore during the HTML prerender
|
|
cacheSignal: null,
|
|
dynamicTracking: clientDynamicTracking,
|
|
allowEmptyStaticShell,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
// TODO should this be removed from client stores?
|
|
prerenderResumeDataCache: null,
|
|
renderResumeDataCache: null,
|
|
hmrRefreshHash,
|
|
// Client prerenders don't track server param access
|
|
varyParamsAccumulator: null
|
|
};
|
|
const dynamicValidation = createDynamicValidationState();
|
|
const serverStream = createNodeStreamWithLateRelease(stageChunks, allServerChunks, clientReactController.signal);
|
|
const debugChannelClient = debugChunks ? createNodeStreamWithLateRelease(debugChunks, debugChunks, clientReactController.signal) : undefined;
|
|
try {
|
|
let { prelude: unprocessedPrelude } = await runInSequentialTasks(()=>{
|
|
const pendingFinalClientResult = workUnitAsyncStorage.run(finalClientPrerenderStore, getClientPrerender, // eslint-disable-next-line @next/internal/no-ambiguous-jsx -- React Client
|
|
/*#__PURE__*/ _jsx(App, {
|
|
reactServerStream: serverStream,
|
|
reactDebugStream: debugChannelClient,
|
|
debugEndTime: debugEndTime,
|
|
preinitScripts: preinitScripts,
|
|
ServerInsertedHTMLProvider: ServerInsertedHTMLProvider,
|
|
nonce: nonce,
|
|
images: ctx.renderOpts.images
|
|
}), {
|
|
signal: clientReactController.signal,
|
|
onError: (err, errorInfo)=>{
|
|
if (isPrerenderInterruptedError(err) || clientReactController.signal.aborted) {
|
|
const componentStack = errorInfo.componentStack;
|
|
if (typeof componentStack === 'string') {
|
|
trackDynamicHole(workStore, componentStack, dynamicValidation, clientDynamicTracking);
|
|
}
|
|
return;
|
|
}
|
|
if (isReactLargeShellError(err)) {
|
|
// TODO: Aggregate
|
|
console.error(err);
|
|
return undefined;
|
|
}
|
|
return getDigestForWellKnownError(err);
|
|
}
|
|
});
|
|
// The listener to abort our own render controller must be added after
|
|
// React has added its listener, to ensure that pending I/O is not
|
|
// aborted/rejected too early.
|
|
clientReactController.signal.addEventListener('abort', ()=>{
|
|
clientRenderController.abort();
|
|
}, {
|
|
once: true
|
|
});
|
|
return pendingFinalClientResult;
|
|
}, ()=>{
|
|
clientReactController.abort();
|
|
});
|
|
const { preludeIsEmpty } = await processPreludeOp(unprocessedPrelude);
|
|
return getStaticShellDisallowedDynamicReasons(workStore, preludeIsEmpty ? PreludeState.Empty : PreludeState.Full, dynamicValidation, // TODO(instant-validation): if allowEmptyStaticShell is true (likely due to blocking configs),
|
|
// we should probably just skip this altogether
|
|
allowEmptyStaticShell);
|
|
} catch (thrownValue) {
|
|
// Even if the root errors we still want to report any cache components errors
|
|
// that were discovered before the root errored.
|
|
let errors = getStaticShellDisallowedDynamicReasons(workStore, PreludeState.Errored, dynamicValidation, // TODO(instant-validation): if allowEmptyStaticShell is true (likely due to blocking configs),
|
|
// we should probably just skip this altogether
|
|
allowEmptyStaticShell);
|
|
if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
|
errors.unshift('During dynamic validation the root of the page errored. The next logged error is the thrown value. It may be a duplicate of errors reported during the normal development mode render.', thrownValue);
|
|
}
|
|
return errors;
|
|
}
|
|
}
|
|
/**
|
|
* Validates instant configs by iterating URL depths from deepest to
|
|
* shallowest. At each depth, builds a combined payload where segments
|
|
* above the boundary use Dynamic stage (already mounted) and segments
|
|
* below use Static/Runtime stage (being prefetched). If the new subtree
|
|
* contains any `unstable_instant` configs, the payload is rendered to
|
|
* detect dynamic holes without Suspense.
|
|
*/ async function validateInstantConfigs(accumulatedChunks, debugChunks, startTime, rootParams, fallbackRouteParams, ctx, hmrRefreshHash, validationSamples) {
|
|
const debug = process.env.NEXT_PRIVATE_DEBUG_VALIDATION === '1' ? console.log : undefined;
|
|
const { createCombinedPayloadAtDepth, createCombinedPayloadStream, collectStagedSegmentData, discoverValidationDepths } = ctx.componentMod.InstantValidation();
|
|
const { createValidationSampleTracking } = require('./instant-validation/instant-samples');
|
|
debug == null ? void 0 : debug('\nStarting depth-based instant validation...');
|
|
const loaderTree = ctx.componentMod.routeModule.userland.loaderTree;
|
|
// Only affects a debug environment name label, not functional behavior.
|
|
const hasRuntimePrefetch = true;
|
|
const clientReferenceManifest = getClientReferenceManifest();
|
|
const { cache, payload: initialRscPayload, stageEndTimes } = await collectStagedSegmentData({
|
|
[RenderStage.Static]: accumulatedChunks.staticChunks,
|
|
[RenderStage.Runtime]: accumulatedChunks.runtimeChunks,
|
|
[RenderStage.Dynamic]: accumulatedChunks.dynamicChunks
|
|
}, debugChunks, startTime, hasRuntimePrefetch, clientReferenceManifest);
|
|
const { implicitTags, nonce, workStore } = ctx;
|
|
const isDebugChannelEnabled = !!ctx.renderOpts.setReactDebugChannel;
|
|
/**
|
|
* Build and validate a combined payload at the given URL depth.
|
|
*
|
|
* Returns null if no instant config exists at this depth.
|
|
* Returns an empty array if validation passed.
|
|
* Returns a non-empty array of errors if validation failed.
|
|
*
|
|
* When the initial validation uses static segments and finds errors,
|
|
* automatically retries with runtime stages to discriminate between
|
|
* runtime and dynamic errors, returning the more specific result.
|
|
*/ async function validateAtDepth(depth, groupDepthForValidation) {
|
|
return validateAtDepthImpl(depth, groupDepthForValidation, null);
|
|
}
|
|
async function validateAtDepthImpl(depth, groupDepthForValidation, previousBoundaryState) {
|
|
const extraChunksController = new AbortController();
|
|
const boundaryState = createValidationBoundaryTracking();
|
|
let useRuntimeStageForPartialSegments = false;
|
|
if (previousBoundaryState) {
|
|
// We're doing a followup render to better discriminate error types
|
|
useRuntimeStageForPartialSegments = true;
|
|
for (const id of previousBoundaryState.expectedIds){
|
|
boundaryState.expectedIds.add(id);
|
|
}
|
|
}
|
|
const payloadResult = await createCombinedPayloadAtDepth(initialRscPayload, cache, loaderTree, ctx.getDynamicParamFromSegment, ctx.query, depth, groupDepthForValidation, extraChunksController.signal, boundaryState, clientReferenceManifest, stageEndTimes, useRuntimeStageForPartialSegments);
|
|
if (payloadResult === null) {
|
|
return null;
|
|
}
|
|
const reactController = new AbortController();
|
|
const renderController = new AbortController();
|
|
const preinitScripts = ()=>{};
|
|
const { ServerInsertedHTMLProvider } = createServerInsertedHTML();
|
|
const { stream: serverStream, debugStream } = await createCombinedPayloadStream(payloadResult.payload, extraChunksController, reactController.signal, clientReferenceManifest, startTime, isDebugChannelEnabled);
|
|
const instantValidationState = createInstantValidationState(payloadResult.createInstantStack);
|
|
const validationSampleTracking = validationSamples !== null ? createValidationSampleTracking() : null;
|
|
const clientDynamicTracking = createDynamicTrackingState(false);
|
|
const prerenderStore = {
|
|
type: 'validation-client',
|
|
phase: 'render',
|
|
rootParams,
|
|
implicitTags,
|
|
renderSignal: renderController.signal,
|
|
controller: reactController,
|
|
cacheSignal: null,
|
|
dynamicTracking: clientDynamicTracking,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
prerenderResumeDataCache: null,
|
|
renderResumeDataCache: null,
|
|
hmrRefreshHash,
|
|
varyParamsAccumulator: null,
|
|
boundaryState,
|
|
fallbackRouteParams,
|
|
validationSamples,
|
|
validationSampleTracking
|
|
};
|
|
let errors;
|
|
try {
|
|
const { prelude: unprocessedPrelude } = await runInSequentialTasks(()=>{
|
|
const pendingResult = workUnitAsyncStorage.run(prerenderStore, getClientPrerender, // eslint-disable-next-line @next/internal/no-ambiguous-jsx -- React Client
|
|
/*#__PURE__*/ _jsx(App, {
|
|
reactServerStream: serverStream,
|
|
reactDebugStream: debugStream ?? undefined,
|
|
debugEndTime: undefined,
|
|
preinitScripts: preinitScripts,
|
|
ServerInsertedHTMLProvider: ServerInsertedHTMLProvider,
|
|
nonce: nonce,
|
|
images: ctx.renderOpts.images
|
|
}), {
|
|
signal: reactController.signal,
|
|
onError: (err, errorInfo)=>{
|
|
if (isPrerenderInterruptedError(err) || reactController.signal.aborted) {
|
|
const componentStack = errorInfo.componentStack;
|
|
if (typeof componentStack === 'string') {
|
|
trackDynamicHoleInNavigation(workStore, componentStack, instantValidationState, clientDynamicTracking, payloadResult.hasAmbiguousErrors ? DynamicHoleKind.Runtime : DynamicHoleKind.Dynamic, boundaryState);
|
|
}
|
|
return;
|
|
} else if (!reactController.signal.aborted) {
|
|
const componentStack = errorInfo.componentStack;
|
|
if (typeof componentStack === 'string') {
|
|
let errorForDisplay = err;
|
|
if (process.env.NODE_ENV === 'production') {
|
|
// In production (i.e. build validation), Flight omits everything except the digest
|
|
// when serializing errors, which makes them very unfriendly for debugging.
|
|
// Map the deserialized errors back to their original error object to make it more useful.
|
|
if (err && typeof err === 'object' && 'digest' in err && typeof err.digest === 'string') {
|
|
const serverError = workStore.reactServerErrorsByDigest.get(err.digest);
|
|
if (serverError !== undefined) {
|
|
errorForDisplay = serverError;
|
|
}
|
|
}
|
|
}
|
|
trackThrownErrorInNavigation(workStore, instantValidationState, errorForDisplay, componentStack);
|
|
}
|
|
}
|
|
if (isReactLargeShellError(err)) {
|
|
console.error(err);
|
|
return undefined;
|
|
}
|
|
return getDigestForWellKnownError(err);
|
|
}
|
|
});
|
|
reactController.signal.addEventListener('abort', ()=>{
|
|
renderController.abort();
|
|
}, {
|
|
once: true
|
|
});
|
|
return pendingResult;
|
|
}, ()=>{
|
|
reactController.abort();
|
|
});
|
|
const { preludeIsEmpty } = await processPreludeOp(unprocessedPrelude);
|
|
errors = getNavigationDisallowedDynamicReasons(workStore, preludeIsEmpty ? PreludeState.Empty : PreludeState.Full, instantValidationState, validationSampleTracking, boundaryState);
|
|
} catch (thrownValue) {
|
|
errors = getNavigationDisallowedDynamicReasons(workStore, PreludeState.Errored, instantValidationState, validationSampleTracking, boundaryState);
|
|
}
|
|
// This prerender did not produce any errors
|
|
if (errors.length === 0) {
|
|
return [];
|
|
}
|
|
if (previousBoundaryState === null && payloadResult.hasAmbiguousErrors) {
|
|
// This is the first validation attempt. we prepared a payload where dynamic holes might be runtime data dependencies
|
|
// or dynamic data dependencies. We do a followup validation using a payload with only Runtime segments to discriminate
|
|
const dynamicOnlyErrors = await validateAtDepthImpl(depth, groupDepthForValidation, boundaryState);
|
|
if (dynamicOnlyErrors !== null && dynamicOnlyErrors.length > 0) {
|
|
// The dynamic errors only validation found errors to report so we favor those
|
|
return dynamicOnlyErrors;
|
|
}
|
|
}
|
|
// If we didn't return some other errors at this point the only thing to return is this validation's errors
|
|
return errors;
|
|
}
|
|
// Discover validation depth bounds from the LoaderTree. The array
|
|
// length is the max URL depth; each entry is the max group depth
|
|
// (route group segments) between that URL depth and the next.
|
|
const groupDepthsByUrlDepth = discoverValidationDepths(loaderTree);
|
|
const maxDepth = groupDepthsByUrlDepth.length;
|
|
for(let depth = maxDepth - 1; depth >= 0; depth--){
|
|
const maxGroupDepth = groupDepthsByUrlDepth[depth];
|
|
for(let currentGroupDepth = maxGroupDepth; currentGroupDepth >= 0; currentGroupDepth--){
|
|
debug == null ? void 0 : debug(`Trying depth ${depth}` + (currentGroupDepth > 0 ? ` + groupDepth ${currentGroupDepth}...` : '...'));
|
|
const errors = await validateAtDepth(depth, currentGroupDepth);
|
|
if (errors === null) {
|
|
debug == null ? void 0 : debug(` No config at depth ${depth}+${currentGroupDepth}, skipping.`);
|
|
continue;
|
|
}
|
|
if (errors.length > 0) {
|
|
debug == null ? void 0 : debug(` Depth ${depth}+${currentGroupDepth}: ❌ Failed (${errors.length} errors)`);
|
|
return errors;
|
|
}
|
|
debug == null ? void 0 : debug(` Depth ${depth}+${currentGroupDepth}: ✅ Passed`);
|
|
}
|
|
}
|
|
debug == null ? void 0 : debug(`✅ All depths passed`);
|
|
return [];
|
|
}
|
|
/**
|
|
* Two-pass render for build-time instant validation.
|
|
* The flow is similar to `renderWithRestartOnCacheMissInDev`: pass 1 warms caches,
|
|
* pass 2 renders with warm caches. If pass 1 has no cache misses,
|
|
* its result is returned directly.
|
|
*
|
|
* Differences from `renderWithRestartOnCacheMissInDev`:
|
|
* - both renders are abortable: if we know that we can't use a stream, we can just
|
|
* throw it away, we don't have to render a complete result.
|
|
* - We don't need to tee the stream, we only care about accumulating chunks.
|
|
*/ async function renderWithRestartOnCacheMissInValidation(ctx, initialRequestStore, createRequestStore, getPayload, createOnError, prefilledDataCache) {
|
|
const { componentMod: ComponentMod } = ctx;
|
|
const { clientModules } = getClientReferenceManifest();
|
|
let startTime = -Infinity;
|
|
let requestStore = initialRequestStore;
|
|
//===============================================
|
|
// Initial render (prospective — may warm caches)
|
|
//===============================================
|
|
const cacheSignal = new CacheSignal();
|
|
trackPendingModules(cacheSignal);
|
|
// The prerender we rean before the validation probably already filled some caches,
|
|
// so we want to save work and re-use them.
|
|
const prerenderResumeDataCache = prefilledDataCache ? createPrerenderResumeDataCache(prefilledDataCache) : createPrerenderResumeDataCache();
|
|
const initialReactController = new AbortController();
|
|
const initialDataController = new AbortController();
|
|
const initialAbandonController = new AbortController();
|
|
const initialStageController = new StagedRenderingController(initialDataController.signal, initialAbandonController, true // track sync IO
|
|
);
|
|
requestStore.prerenderResumeDataCache = prerenderResumeDataCache;
|
|
requestStore.renderResumeDataCache = null;
|
|
requestStore.stagedRendering = initialStageController;
|
|
requestStore.cacheSignal = cacheSignal;
|
|
requestStore.asyncApiPromises = createAsyncApiPromises(initialStageController, requestStore.cookies, requestStore.mutableCookies, requestStore.headers);
|
|
// We don't set `requestStore.controller and requestStore.renderSignal here.
|
|
// Right now, we only abort for sync IO, and in the first render, that's just a restart
|
|
// (after waiting for caches)
|
|
requestStore.controller = undefined;
|
|
requestStore.renderSignal = undefined;
|
|
const initialRscPayload = await getPayload(requestStore);
|
|
const advanceStageIfNoCacheMiss = (stage)=>{
|
|
if (initialAbandonController.signal.aborted === true) {
|
|
return;
|
|
} else if (cacheSignal.hasPendingReads()) {
|
|
initialAbandonController.abort();
|
|
} else {
|
|
initialStageController.advanceStage(stage);
|
|
}
|
|
};
|
|
const initialResult = await runInSequentialTasks(()=>{
|
|
initialStageController.advanceStage(RenderStage.EarlyStatic);
|
|
startTime = performance.now() + performance.timeOrigin;
|
|
const stream = workUnitAsyncStorage.run(requestStore, renderToFlightStream, ComponentMod, initialRscPayload, clientModules, {
|
|
onError: createOnError(initialReactController.signal, false),
|
|
startTime,
|
|
filterStackFrame,
|
|
signal: initialReactController.signal
|
|
});
|
|
initialReactController.signal.addEventListener('abort', ()=>{
|
|
const { reason } = initialReactController.signal;
|
|
initialDataController.abort(reason);
|
|
}, {
|
|
once: true
|
|
});
|
|
const accumulatedChunksPromise = accumulateStreamChunks(stream, initialStageController, initialDataController.signal);
|
|
accumulatedChunksPromise.catch(()=>{});
|
|
return {
|
|
accumulatedChunksPromise
|
|
};
|
|
}, ()=>{
|
|
advanceStageIfNoCacheMiss(RenderStage.Static);
|
|
}, ()=>{
|
|
advanceStageIfNoCacheMiss(RenderStage.EarlyRuntime);
|
|
}, ()=>{
|
|
advanceStageIfNoCacheMiss(RenderStage.Runtime);
|
|
}, ()=>{
|
|
advanceStageIfNoCacheMiss(RenderStage.Dynamic);
|
|
});
|
|
if (initialStageController.currentStage !== RenderStage.Abandoned) {
|
|
// No cache misses. Use the result as-is.
|
|
return {
|
|
accumulatedChunksPromise: initialResult.accumulatedChunksPromise,
|
|
startTime,
|
|
stageController: initialStageController,
|
|
requestStore
|
|
};
|
|
}
|
|
// Cache miss. Wait for caches to fill, then re-render with warm caches.
|
|
await cacheSignal.cacheReady();
|
|
initialReactController.abort();
|
|
//===============================================
|
|
// Final render (restarted, with warm caches)
|
|
//===============================================
|
|
requestStore = createRequestStore();
|
|
// Unlike dev, where we're re-using the render that'll be visible in the browser,
|
|
// we *can* abort the validation render.
|
|
const finalReactController = new AbortController();
|
|
const finalDataController = new AbortController();
|
|
const finalStageController = new StagedRenderingController(finalDataController.signal, null, true // track sync IO
|
|
);
|
|
requestStore.prerenderResumeDataCache = null;
|
|
requestStore.renderResumeDataCache = createRenderResumeDataCache(prerenderResumeDataCache);
|
|
requestStore.stagedRendering = finalStageController;
|
|
requestStore.cacheSignal = null;
|
|
requestStore.asyncApiPromises = createAsyncApiPromises(finalStageController, requestStore.cookies, requestStore.mutableCookies, requestStore.headers);
|
|
// Right now, we only abort for sync IO.
|
|
// If sync IO occurs in a place where it's not allowed, then we have to fail validation,
|
|
// and we can abort the render immediately, without waiting for anything else..
|
|
requestStore.controller = finalReactController;
|
|
requestStore.renderSignal = finalDataController.signal;
|
|
const finalRscPayload = await getPayload(requestStore);
|
|
const finalResult = await runInSequentialTasks(()=>{
|
|
finalStageController.advanceStage(RenderStage.EarlyStatic);
|
|
startTime = performance.now() + performance.timeOrigin;
|
|
const stream = workUnitAsyncStorage.run(requestStore, renderToFlightStream, ComponentMod, finalRscPayload, clientModules, {
|
|
onError: createOnError(finalReactController.signal, true),
|
|
startTime,
|
|
filterStackFrame,
|
|
signal: finalReactController.signal
|
|
});
|
|
finalReactController.signal.addEventListener('abort', ()=>{
|
|
finalDataController.abort(finalReactController.signal.reason);
|
|
}, {
|
|
once: true
|
|
});
|
|
const accumulatedChunksPromise = accumulateStreamChunks(stream, finalStageController, null);
|
|
accumulatedChunksPromise.catch(()=>{});
|
|
return {
|
|
accumulatedChunksPromise
|
|
};
|
|
}, ()=>{
|
|
finalStageController.advanceStage(RenderStage.Static);
|
|
}, ()=>{
|
|
finalStageController.advanceStage(RenderStage.EarlyRuntime);
|
|
}, ()=>{
|
|
finalStageController.advanceStage(RenderStage.Runtime);
|
|
}, ()=>{
|
|
finalStageController.advanceStage(RenderStage.Dynamic);
|
|
});
|
|
return {
|
|
accumulatedChunksPromise: finalResult.accumulatedChunksPromise,
|
|
startTime,
|
|
stageController: finalStageController,
|
|
requestStore
|
|
};
|
|
}
|
|
async function validateInstantConfigsInBuild(ctx, prefilledDataCache) {
|
|
const run = async ()=>{
|
|
let success;
|
|
try {
|
|
// The validation renders are separate renders, and use a separate WorkStore.
|
|
// However, we defensively exit the existing workStore to avoid relying on something from there
|
|
// before we shadow it.
|
|
success = await workAsyncStorage.exit(async ()=>validateInstantConfigsInBuildImpl(ctx, prefilledDataCache));
|
|
} catch (err) {
|
|
console.error(Object.defineProperty(new InvariantError('An unexpected error occcured during instant validation', {
|
|
cause: err
|
|
}), "__NEXT_ERROR_CODE", {
|
|
value: "E1097",
|
|
enumerable: false,
|
|
configurable: true
|
|
}));
|
|
success = false;
|
|
}
|
|
if (!success) {
|
|
console.error('Stopping prerender due to instant validation errors.');
|
|
throw new StaticGenBailoutError();
|
|
}
|
|
};
|
|
if (process.env.__NEXT_TEST_MODE && process.env.NEXT_TEST_LOG_VALIDATION) {
|
|
// In tests, we use these markers to extract the relevant portion of the CLI logs.
|
|
// We want consistent ordering of these messages and other console.error calls,
|
|
// so we use console.error here as well. Using console.log leads to non-deterministic
|
|
// log order, likely stdout/stderr can interleave in non-deterministic ways.
|
|
const requestId = Date.now();
|
|
const route = ctx.workStore.route;
|
|
console.error('<VALIDATION_MESSAGE>' + JSON.stringify({
|
|
type: 'validation_start',
|
|
requestId,
|
|
url: route
|
|
}) + '</VALIDATION_MESSAGE>');
|
|
try {
|
|
return await run();
|
|
} finally{
|
|
console.error('<VALIDATION_MESSAGE>' + JSON.stringify({
|
|
type: 'validation_end',
|
|
requestId,
|
|
url: route
|
|
}) + '</VALIDATION_MESSAGE>');
|
|
}
|
|
} else {
|
|
return await run();
|
|
}
|
|
}
|
|
/**
|
|
* Runs instant validation at build time using the `samples` from `unstable_instant`.
|
|
*
|
|
* For each sample, this creates a staged RSC render with a synthetic `RequestStore`
|
|
* populated from sample data, then feeds the accumulated chunks to
|
|
* `validateInstantConfigs` which handles the actual validation.
|
|
*/ async function validateInstantConfigsInBuildImpl(ctx, prefilledDataCache) {
|
|
const debug = process.env.NEXT_PRIVATE_DEBUG_VALIDATION === '1' ? console.log : undefined;
|
|
const { workStore: outerWorkStore } = ctx;
|
|
const route = outerWorkStore.route;
|
|
const loaderTree = ctx.componentMod.routeModule.userland.loaderTree;
|
|
let samples = await resolveInstantConfigSamplesForPage(loaderTree);
|
|
if (!samples || samples.length === 0) {
|
|
// No samples defined; use a single empty sample to still run validation
|
|
samples = [
|
|
{}
|
|
];
|
|
}
|
|
debug == null ? void 0 : debug('Resolved samples:', samples);
|
|
const allPossibleFallbackRouteParams = getFallbackRouteParams(route, ctx.componentMod.routeModule);
|
|
for(let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++){
|
|
const sample = samples[sampleIndex];
|
|
debug == null ? void 0 : debug(`Validating sample (${sampleIndex + 1}/${samples.length}):`, sample);
|
|
let errors;
|
|
try {
|
|
errors = await consoleAsyncStorage.run({
|
|
dim: true
|
|
}, ()=>validateInstantConfigInBuildWithSample(ctx, sample, allPossibleFallbackRouteParams, prefilledDataCache));
|
|
} catch (err) {
|
|
if (isInstantValidationError(err)) {
|
|
errors = [
|
|
err
|
|
];
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
if (errors.length > 0) {
|
|
debug == null ? void 0 : debug(`❌ Sample failed validation (${errors.length} errors)`);
|
|
const sampleDesc = samples.length > 1 ? ` (sample ${sampleIndex + 1} of ${samples.length})` : '';
|
|
for (const err of errors){
|
|
console.error(err);
|
|
}
|
|
console.error(`Build-time instant validation failed for route "${route}"${sampleDesc}.`);
|
|
return false;
|
|
} else {
|
|
debug == null ? void 0 : debug('✅ Sample validated successfully');
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
async function validateInstantConfigInBuildWithSample(outerCtx, sample, allPossibleFallbackRouteParams, prefilledDataCache) {
|
|
// The flow for build mirrors what we do when validating in dev.
|
|
// We have to perform a full dynamic render to get the RSC chunks for each stage.
|
|
// In order to do that, we have to set up a mock AppRenderContext, workStore, and requestStore
|
|
// based on the `sample` we're using.
|
|
const { workStore: outerWorkStore } = outerCtx;
|
|
const loaderTree = outerCtx.componentMod.routeModule.userland.loaderTree;
|
|
const route = outerWorkStore.route;
|
|
const { createCookiesFromSample, createHeadersFromSample, createDraftModeForValidation, createRelativeURLFromSamples, createValidationSampleTracking } = require('./instant-validation/instant-samples');
|
|
// TODO(instant-validation-build): it feels like this should happen higher up
|
|
// and go through existing URL parsing/generation logic?
|
|
const sampleUrl = createRelativeURLFromSamples(route, sample.params, sample.searchParams);
|
|
const sampleParams = sample.params ?? {};
|
|
let fallbackRouteParams = null;
|
|
if (allPossibleFallbackRouteParams) {
|
|
const fallbackRouteParamsMut = new Map();
|
|
for (const [paramKey, value] of allPossibleFallbackRouteParams){
|
|
if (!(paramKey in sampleParams)) {
|
|
fallbackRouteParamsMut.set(paramKey, value);
|
|
}
|
|
}
|
|
fallbackRouteParams = fallbackRouteParamsMut;
|
|
}
|
|
const getDynamicParamFromSegment = makeGetDynamicParamFromSegment(sampleParams, fallbackRouteParams, false);
|
|
const sampleRootParams = getRootParams(loaderTree, getDynamicParamFromSegment);
|
|
let sampleUrlWithoutQuery;
|
|
let sampleQuery;
|
|
({ query: sampleQuery, ...sampleUrlWithoutQuery } = sampleUrl);
|
|
const { AfterContext } = require('../after/after-context');
|
|
// NOTE: Matching the field order in `createWorkStore` to avoid deopting.
|
|
const workStore = {
|
|
isStaticGeneration: false,
|
|
page: outerWorkStore.page,
|
|
route: outerWorkStore.route,
|
|
incrementalCache: outerWorkStore.incrementalCache,
|
|
cacheLifeProfiles: outerWorkStore.cacheLifeProfiles,
|
|
isBuildTimePrerendering: false,
|
|
fetchCache: outerWorkStore.fetchCache,
|
|
isOnDemandRevalidate: false,
|
|
isDraftMode: false,
|
|
isPrefetchRequest: false,
|
|
buildId: outerWorkStore.buildId,
|
|
reactLoadableManifest: outerWorkStore.reactLoadableManifest,
|
|
assetPrefix: outerWorkStore.assetPrefix,
|
|
nonce: outerWorkStore.nonce,
|
|
// Never run `after()` for this validation render. by definition, `after` can't affect the rendered output.
|
|
afterContext: new AfterContext({
|
|
waitUntil (promise) {
|
|
promise.catch(()=>{});
|
|
},
|
|
onClose () {},
|
|
onTaskError () {}
|
|
}),
|
|
cacheComponentsEnabled: outerWorkStore.cacheComponentsEnabled,
|
|
previouslyRevalidatedTags: [],
|
|
refreshTagsByCacheKind: new Map(),
|
|
runInCleanSnapshot: outerWorkStore.runInCleanSnapshot,
|
|
shouldTrackFetchMetrics: false,
|
|
reactServerErrorsByDigest: new Map()
|
|
};
|
|
return workAsyncStorage.run(workStore, async ()=>{
|
|
// NOTE: match field order in renderToHTMLOrFlightImpl to avoid deopts
|
|
const validationCtx = {
|
|
componentMod: outerCtx.componentMod,
|
|
url: sampleUrlWithoutQuery,
|
|
renderOpts: outerCtx.renderOpts,
|
|
workStore,
|
|
parsedRequestHeaders: outerCtx.parsedRequestHeaders,
|
|
getDynamicParamFromSegment,
|
|
interpolatedParams: sampleParams,
|
|
query: sampleQuery,
|
|
isPrefetch: false,
|
|
isPossibleServerAction: false,
|
|
requestTimestamp: outerCtx.requestTimestamp,
|
|
appUsingSizeAdjustment: outerCtx.appUsingSizeAdjustment,
|
|
flightRouterState: undefined,
|
|
requestId: outerCtx.requestId,
|
|
htmlRequestId: outerCtx.htmlRequestId,
|
|
pagePath: outerCtx.pagePath,
|
|
assetPrefix: outerCtx.assetPrefix,
|
|
isNotFoundPath: outerCtx.isNotFoundPath,
|
|
nonce: outerCtx.nonce,
|
|
res: outerCtx.res,
|
|
sharedContext: outerCtx.sharedContext,
|
|
implicitTags: outerCtx.implicitTags
|
|
};
|
|
const validationSamples = {
|
|
params: sample.params,
|
|
searchParams: sample.searchParams
|
|
};
|
|
const createRequestStore = ()=>{
|
|
// Create exhaustive request data from sample
|
|
const sampleCookies = createCookiesFromSample(sample.cookies, route);
|
|
// We don't have to bother initializing these, pages can't access them anyway,
|
|
// we just need them because RequestStore requires them.
|
|
const unusedMutableCookies = new ResponseCookies(new Headers());
|
|
// Create headers.
|
|
const sampleHeaders = createHeadersFromSample(sample.headers, sample.cookies, route);
|
|
const draftMode = createDraftModeForValidation();
|
|
return {
|
|
type: 'request',
|
|
phase: 'render',
|
|
implicitTags: outerCtx.implicitTags,
|
|
url: {
|
|
pathname: sampleUrl.pathname,
|
|
search: sampleUrl.search
|
|
},
|
|
headers: sampleHeaders,
|
|
cookies: sampleCookies,
|
|
mutableCookies: unusedMutableCookies,
|
|
userspaceMutableCookies: unusedMutableCookies,
|
|
draftMode,
|
|
rootParams: sampleRootParams,
|
|
validationSamples,
|
|
validationSampleTracking: createValidationSampleTracking(),
|
|
// These will be set when rendering
|
|
renderResumeDataCache: null,
|
|
prerenderResumeDataCache: null,
|
|
stagedRendering: null,
|
|
asyncApiPromises: undefined
|
|
};
|
|
};
|
|
// Track server errors. If one of them surfaces during the client render
|
|
// in the deserialized form (with no message/stack) we'll use this to map it
|
|
// back to the original.
|
|
const onServerError = createReactServerErrorHandler(true, true, workStore.reactServerErrorsByDigest, ()=>{} // Don't report anything here. If needed, it will be reported in the client render.
|
|
);
|
|
const { accumulatedChunksPromise, startTime, stageController, requestStore: finalServerStore } = await renderWithRestartOnCacheMissInValidation(validationCtx, createRequestStore(), createRequestStore, (requestStore)=>workUnitAsyncStorage.run(requestStore, getRSCPayload, loaderTree, validationCtx, {
|
|
is404: false
|
|
}), (signal)=>function onError(err) {
|
|
const digest = getDigestForWellKnownError(err);
|
|
if (digest) {
|
|
return digest;
|
|
}
|
|
if (signal.aborted) {
|
|
return;
|
|
}
|
|
return onServerError(err);
|
|
}, prefilledDataCache);
|
|
const accumulatedChunks = await accumulatedChunksPromise;
|
|
const debugChunks = null // TODO(instant-validation-build): support debugChannel
|
|
;
|
|
// Missing sample errors take priority over everything else,
|
|
// because they prevent us from rendering everything we need to validate.
|
|
const serverValidationSampleTracking = finalServerStore.validationSampleTracking;
|
|
if (serverValidationSampleTracking.missingSampleErrors.length > 0) {
|
|
return serverValidationSampleTracking.missingSampleErrors;
|
|
}
|
|
// We also error for sync IO. This runs after the prerender,
|
|
// so if we get sync IO errors here, they're likely from the runtime stage --
|
|
// the prerender probably discovered sync IO in the static stage
|
|
if (stageController.currentStage === RenderStage.Abandoned && stageController.syncInterruptReason) {
|
|
return [
|
|
stageController.syncInterruptReason
|
|
];
|
|
}
|
|
const allowEmptyStaticShell = (validationCtx.renderOpts.allowEmptyStaticShell ?? false) || await isPageAllowedToBlock(loaderTree);
|
|
// Now we the chunks of a fully rendered page, just like in dev.
|
|
// We can use them to validate all the navigations required by `instant` configs.
|
|
// Note that we're not performing static shell validation here -- that happens
|
|
// implicitly as part of the static prerender.
|
|
// The static prerender has warmed some client modules already,
|
|
// but we'll be reaching Runtime/Dynamic stages and thus rendering more content,
|
|
// so we need to warm again.
|
|
// TODO(instant-validation-build): This might warm too much, possibly hitting errors on code that didn't expect
|
|
// to run at build time. For example, we generally don't need to render leaf segments (e.g. __PAGE__) in
|
|
// the Dynamic stage, they're Runtime at best.
|
|
const warmupValidationSamplesTracking = createValidationSampleTracking();
|
|
await warmupClientModulesForStagedValidation('validation-client', accumulatedChunks.dynamicChunks, accumulatedChunks.dynamicChunks, sampleRootParams, fallbackRouteParams, allowEmptyStaticShell, validationCtx, validationSamples, warmupValidationSamplesTracking);
|
|
if (warmupValidationSamplesTracking.missingSampleErrors.length > 0) {
|
|
return warmupValidationSamplesTracking.missingSampleErrors;
|
|
}
|
|
return await validateInstantConfigs(accumulatedChunks, debugChunks, startTime, sampleRootParams, fallbackRouteParams, validationCtx, undefined, validationSamples);
|
|
});
|
|
}
|
|
/**
|
|
* Determines whether we should generate static flight data.
|
|
*/ function shouldGenerateStaticFlightData(workStore) {
|
|
const { isStaticGeneration } = workStore;
|
|
if (!isStaticGeneration) return false;
|
|
return true;
|
|
}
|
|
async function prerenderToStream(req, res, ctx, metadata, tree, fallbackRouteParams) {
|
|
// When prerendering formState is always null. We still include it
|
|
// because some shared APIs expect a formState value and this is slightly
|
|
// more explicit than making it an optional function argument
|
|
const formState = null;
|
|
const { assetPrefix, getDynamicParamFromSegment, implicitTags, nonce, pagePath, renderOpts, workStore } = ctx;
|
|
const { basePath, buildManifest, ComponentMod, crossOrigin, experimental, isDebugDynamicAccesses, isBuildTimePrerendering = false, onInstrumentationRequestError, page, reactMaxHeadersLength, subresourceIntegrityManifest, cacheComponents } = renderOpts;
|
|
const { cachedNavigations } = renderOpts.experimental;
|
|
const allowEmptyStaticShell = (renderOpts.allowEmptyStaticShell ?? false) || await isPageAllowedToBlock(tree);
|
|
const rootParams = getRootParams(tree, getDynamicParamFromSegment);
|
|
const { ServerInsertedHTMLProvider, renderServerInsertedHTML } = createServerInsertedHTML();
|
|
const getServerInsertedMetadata = createServerInsertedMetadata(nonce);
|
|
const tracingMetadata = getTracedMetadata(getTracer().getTracePropagationData(), experimental.clientTraceMetadata);
|
|
const polyfills = buildManifest.polyfillFiles.filter((polyfill)=>polyfill.endsWith('.js') && !polyfill.endsWith('.module.js')).map((polyfill)=>({
|
|
src: `${assetPrefix}/_next/${polyfill}${getAssetQueryString(ctx, false)}`,
|
|
integrity: subresourceIntegrityManifest == null ? void 0 : subresourceIntegrityManifest[polyfill],
|
|
crossOrigin,
|
|
noModule: true,
|
|
nonce
|
|
}));
|
|
const [preinitScripts, bootstrapScript] = getRequiredScripts(buildManifest, // Why is assetPrefix optional on renderOpts?
|
|
// @TODO make it default empty string on renderOpts and get rid of it from ctx
|
|
assetPrefix, crossOrigin, subresourceIntegrityManifest, getAssetQueryString(ctx, true), nonce, page);
|
|
const { reactServerErrorsByDigest } = workStore;
|
|
// We don't report errors during prerendering through our instrumentation hooks
|
|
const reportErrors = !experimental.isRoutePPREnabled;
|
|
function onHTMLRenderRSCError(err, silenceLog) {
|
|
if (reportErrors) {
|
|
return onInstrumentationRequestError == null ? void 0 : onInstrumentationRequestError(err, req, createErrorContext(ctx, 'react-server-components'), silenceLog);
|
|
}
|
|
}
|
|
const serverComponentsErrorHandler = createReactServerErrorHandler(process.env.NODE_ENV === 'development', isBuildTimePrerendering, reactServerErrorsByDigest, onHTMLRenderRSCError);
|
|
function onHTMLRenderSSRError(err) {
|
|
if (reportErrors) {
|
|
// We don't need to silence logs here. onHTMLRenderSSRError won't be
|
|
// called at all if the error was logged before in the RSC error handler.
|
|
const silenceLog = false;
|
|
return onInstrumentationRequestError == null ? void 0 : onInstrumentationRequestError(err, req, createErrorContext(ctx, 'server-rendering'), silenceLog);
|
|
}
|
|
}
|
|
const allCapturedErrors = [];
|
|
const htmlRendererErrorHandler = createHTMLErrorHandler(process.env.NODE_ENV === 'development', isBuildTimePrerendering, reactServerErrorsByDigest, allCapturedErrors, onHTMLRenderSSRError);
|
|
let reactServerPrerenderResult = null;
|
|
const setMetadataHeader = (name)=>{
|
|
metadata.headers ??= {};
|
|
metadata.headers[name] = res.getHeader(name);
|
|
};
|
|
const setHeader = (name, value)=>{
|
|
res.setHeader(name, value);
|
|
setMetadataHeader(name);
|
|
return res;
|
|
};
|
|
const appendHeader = (name, value)=>{
|
|
if (Array.isArray(value)) {
|
|
value.forEach((item)=>{
|
|
res.appendHeader(name, item);
|
|
});
|
|
} else {
|
|
res.appendHeader(name, value);
|
|
}
|
|
setMetadataHeader(name);
|
|
};
|
|
const selectStaleTime = createSelectStaleTime(experimental);
|
|
const { clientModules } = getClientReferenceManifest();
|
|
let prerenderStore = null;
|
|
try {
|
|
if (cacheComponents) {
|
|
/**
|
|
* cacheComponents with PPR
|
|
*
|
|
* The general approach is to render the RSC stream first allowing any cache reads to resolve.
|
|
* Once we have settled all cache reads we restart the render and abort after a single Task.
|
|
*
|
|
* Unlike with the non PPR case we can't synchronously abort the render when a dynamic API is used
|
|
* during the initial render because we need to ensure all caches can be filled as part of the initial Task
|
|
* and a synchronous abort might prevent us from filling all caches.
|
|
*
|
|
* Once the render is complete we allow the SSR render to finish and use a combination of the postponed state
|
|
* and the reactServerIsDynamic value to determine how to treat the resulting render
|
|
*/ // The prerender controller represents the lifetime of the prerender. It
|
|
// will be aborted when a task is complete or a synchronously aborting API
|
|
// is called. Notably, during prospective prerenders, this does not
|
|
// actually terminate the prerender itself, which will continue until all
|
|
// caches are filled.
|
|
const initialServerPrerenderController = new AbortController();
|
|
// This controller is used to abort the React prerender.
|
|
const initialServerReactController = new AbortController();
|
|
// This controller represents the lifetime of the React prerender. Its
|
|
// signal can be used for any I/O operation to abort the I/O and/or to
|
|
// reject, when prerendering aborts. This includes our own hanging
|
|
// promises for accessing request data, and for fetch calls. It might be
|
|
// replaced in the future by React.cacheSignal(). It's aborted after the
|
|
// React controller, so that no pending I/O can register abort listeners
|
|
// that are called before React's abort listener is called. This ensures
|
|
// that pending I/O is not rejected too early when aborting the prerender.
|
|
// Notably, during the prospective prerender, it is different from the
|
|
// prerender controller because we don't want to end the React prerender
|
|
// until all caches are filled.
|
|
const initialServerRenderController = new AbortController();
|
|
// The cacheSignal helps us track whether caches are still filling or we are ready
|
|
// to cut the render off.
|
|
const cacheSignal = new CacheSignal();
|
|
// Always start with a fresh prerender RDC so warmup can fill misses,
|
|
// even when we have a prefilled render RDC to seed from.
|
|
const prerenderResumeDataCache = createPrerenderResumeDataCache();
|
|
let renderResumeDataCache = renderOpts.renderResumeDataCache ?? null;
|
|
const initialServerPayloadPrerenderStore = {
|
|
type: 'prerender',
|
|
phase: 'render',
|
|
rootParams,
|
|
fallbackRouteParams,
|
|
implicitTags,
|
|
// While this render signal isn't going to be used to abort a React render while getting the RSC payload
|
|
// various request data APIs bind to this controller to reject after completion.
|
|
renderSignal: initialServerRenderController.signal,
|
|
// When we generate the RSC payload we might abort this controller due to sync IO
|
|
// but we don't actually care about sync IO in this phase so we use a throw away controller
|
|
// that isn't connected to anything
|
|
controller: new AbortController(),
|
|
// During the initial prerender we need to track all cache reads to ensure
|
|
// we render long enough to fill every cache it is possible to visit during
|
|
// the final prerender.
|
|
cacheSignal,
|
|
dynamicTracking: null,
|
|
allowEmptyStaticShell,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
prerenderResumeDataCache,
|
|
renderResumeDataCache,
|
|
hmrRefreshHash: undefined,
|
|
// We don't track vary params during initial prerender, only the final one
|
|
varyParamsAccumulator: null
|
|
};
|
|
// We're not going to use the result of this render because the only time it could be used
|
|
// is if it completes in a microtask and that's likely very rare for any non-trivial app
|
|
const initialServerPayload = await workUnitAsyncStorage.run(initialServerPayloadPrerenderStore, getRSCPayload, tree, ctx, {
|
|
is404: res.statusCode === 404
|
|
});
|
|
const initialServerPrerenderStore = prerenderStore = {
|
|
type: 'prerender',
|
|
phase: 'render',
|
|
rootParams,
|
|
fallbackRouteParams,
|
|
implicitTags,
|
|
renderSignal: initialServerRenderController.signal,
|
|
controller: initialServerPrerenderController,
|
|
// During the initial prerender we need to track all cache reads to ensure
|
|
// we render long enough to fill every cache it is possible to visit during
|
|
// the final prerender.
|
|
cacheSignal,
|
|
dynamicTracking: null,
|
|
allowEmptyStaticShell,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
prerenderResumeDataCache,
|
|
renderResumeDataCache,
|
|
hmrRefreshHash: undefined,
|
|
// We don't track vary params during initial prerender, only the final one
|
|
varyParamsAccumulator: null
|
|
};
|
|
const initialPrerenderOptions = {
|
|
filterStackFrame,
|
|
onError: (err)=>{
|
|
const digest = getDigestForWellKnownError(err);
|
|
if (digest) {
|
|
return digest;
|
|
}
|
|
if (isReactLargeShellError(err)) {
|
|
// TODO: Aggregate
|
|
console.error(err);
|
|
return undefined;
|
|
}
|
|
if (initialServerPrerenderController.signal.aborted) {
|
|
// The render aborted before this error was handled which indicates
|
|
// the error is caused by unfinished components within the render
|
|
return;
|
|
} else if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
|
printDebugThrownValueForProspectiveRender(err, workStore.route, Phase.ProspectiveRender);
|
|
}
|
|
},
|
|
// We don't want to stop rendering until the cacheSignal is complete so we pass
|
|
// a different signal to this render call than is used by dynamic APIs to signify
|
|
// transitioning out of the prerender environment
|
|
signal: initialServerReactController.signal
|
|
};
|
|
const pendingInitialServerResult = workUnitAsyncStorage.run(initialServerPrerenderStore, getServerPrerender(ComponentMod), initialServerPayload, clientModules, initialPrerenderOptions);
|
|
// The listener to abort our own render controller must be added after
|
|
// React has added its listener, to ensure that pending I/O is not
|
|
// aborted/rejected too early.
|
|
initialServerReactController.signal.addEventListener('abort', ()=>{
|
|
initialServerRenderController.abort();
|
|
initialServerPrerenderController.abort();
|
|
}, {
|
|
once: true
|
|
});
|
|
// Wait for all caches to be finished filling and for async imports to resolve
|
|
trackPendingModules(cacheSignal);
|
|
await cacheSignal.cacheReady();
|
|
initialServerReactController.abort();
|
|
// We don't need to continue the prerender process if we already
|
|
// detected invalid dynamic usage in the initial prerender phase.
|
|
if (workStore.invalidDynamicUsageError) {
|
|
logDisallowedDynamicError(workStore, workStore.invalidDynamicUsageError);
|
|
throw new StaticGenBailoutError();
|
|
}
|
|
let initialServerResult;
|
|
try {
|
|
initialServerResult = await createReactServerPrerenderResult(pendingInitialServerResult);
|
|
} catch (err) {
|
|
if (initialServerReactController.signal.aborted || initialServerPrerenderController.signal.aborted) {
|
|
// These are expected errors that might error the prerender. we ignore them.
|
|
} else if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
|
// We don't normally log these errors because we are going to retry anyway but
|
|
// it can be useful for debugging Next.js itself to get visibility here when needed
|
|
printDebugThrownValueForProspectiveRender(err, workStore.route, Phase.ProspectiveRender);
|
|
}
|
|
}
|
|
if (initialServerResult) {
|
|
const initialClientPrerenderController = new AbortController();
|
|
const initialClientReactController = new AbortController();
|
|
const initialClientRenderController = new AbortController();
|
|
const initialClientPrerenderStore = {
|
|
type: 'prerender-client',
|
|
phase: 'render',
|
|
rootParams,
|
|
fallbackRouteParams,
|
|
implicitTags,
|
|
renderSignal: initialClientRenderController.signal,
|
|
controller: initialClientPrerenderController,
|
|
// For HTML Generation the only cache tracked activity
|
|
// is module loading, which has it's own cache signal
|
|
cacheSignal: null,
|
|
dynamicTracking: null,
|
|
allowEmptyStaticShell,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
prerenderResumeDataCache,
|
|
renderResumeDataCache,
|
|
hmrRefreshHash: undefined,
|
|
// Client prerenders don't track server param access
|
|
varyParamsAccumulator: null
|
|
};
|
|
const pendingInitialClientResult = workUnitAsyncStorage.run(initialClientPrerenderStore, getClientPrerender, // eslint-disable-next-line @next/internal/no-ambiguous-jsx
|
|
/*#__PURE__*/ _jsx(App, {
|
|
reactServerStream: initialServerResult.asUnclosingStream(),
|
|
reactDebugStream: undefined,
|
|
debugEndTime: undefined,
|
|
preinitScripts: preinitScripts,
|
|
ServerInsertedHTMLProvider: ServerInsertedHTMLProvider,
|
|
nonce: nonce,
|
|
images: ctx.renderOpts.images
|
|
}), {
|
|
signal: initialClientReactController.signal,
|
|
onError: (err)=>{
|
|
const digest = getDigestForWellKnownError(err);
|
|
if (digest) {
|
|
return digest;
|
|
}
|
|
if (isReactLargeShellError(err)) {
|
|
// TODO: Aggregate
|
|
console.error(err);
|
|
return undefined;
|
|
}
|
|
if (initialClientReactController.signal.aborted) {
|
|
// These are expected errors that might error the prerender. we ignore them.
|
|
} else if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
|
// We don't normally log these errors because we are going to retry anyway but
|
|
// it can be useful for debugging Next.js itself to get visibility here when needed
|
|
printDebugThrownValueForProspectiveRender(err, workStore.route, Phase.ProspectiveRender);
|
|
}
|
|
},
|
|
bootstrapScripts: [
|
|
bootstrapScript
|
|
]
|
|
});
|
|
// The listener to abort our own render controller must be added after
|
|
// React has added its listener, to ensure that pending I/O is not
|
|
// aborted/rejected too early.
|
|
initialClientReactController.signal.addEventListener('abort', ()=>{
|
|
initialClientRenderController.abort();
|
|
}, {
|
|
once: true
|
|
});
|
|
pendingInitialClientResult.catch((err)=>{
|
|
if (initialClientReactController.signal.aborted || isPrerenderInterruptedError(err)) {
|
|
// These are expected errors that might error the prerender. we ignore them.
|
|
} else if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
|
|
// We don't normally log these errors because we are going to retry anyway but
|
|
// it can be useful for debugging Next.js itself to get visibility here when needed
|
|
printDebugThrownValueForProspectiveRender(err, workStore.route, Phase.ProspectiveRender);
|
|
}
|
|
});
|
|
// This is mostly needed for dynamic `import()`s in client components.
|
|
// Promises passed to client were already awaited above (assuming that they came from cached functions)
|
|
trackPendingModules(cacheSignal);
|
|
await cacheSignal.cacheReady();
|
|
initialClientReactController.abort();
|
|
}
|
|
if (renderOpts.renderResumeDataCache) {
|
|
// Swap to the warmed cache so the final render uses entries produced during warmup.
|
|
renderResumeDataCache = createRenderResumeDataCache(prerenderResumeDataCache);
|
|
}
|
|
const finalServerReactController = new AbortController();
|
|
const finalServerRenderController = new AbortController();
|
|
const varyParamsAccumulator = createResponseVaryParamsAccumulator();
|
|
const finalServerPayloadPrerenderStore = {
|
|
type: 'prerender',
|
|
phase: 'render',
|
|
rootParams,
|
|
fallbackRouteParams,
|
|
implicitTags,
|
|
// While this render signal isn't going to be used to abort a React render while getting the RSC payload
|
|
// various request data APIs bind to this controller to reject after completion.
|
|
renderSignal: finalServerRenderController.signal,
|
|
// When we generate the RSC payload we might abort this controller due to sync IO
|
|
// but we don't actually care about sync IO in this phase so we use a throw away controller
|
|
// that isn't connected to anything
|
|
controller: new AbortController(),
|
|
// All caches we could read must already be filled so no tracking is necessary
|
|
cacheSignal: null,
|
|
dynamicTracking: null,
|
|
allowEmptyStaticShell,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
prerenderResumeDataCache,
|
|
renderResumeDataCache,
|
|
hmrRefreshHash: undefined,
|
|
varyParamsAccumulator
|
|
};
|
|
const finalAttemptRSCPayload = await workUnitAsyncStorage.run(finalServerPayloadPrerenderStore, getRSCPayload, tree, ctx, {
|
|
is404: res.statusCode === 404
|
|
});
|
|
let staleTimeIterable;
|
|
if (cachedNavigations) {
|
|
staleTimeIterable = new StaleTimeIterable();
|
|
finalAttemptRSCPayload.s = staleTimeIterable;
|
|
}
|
|
const serverDynamicTracking = createDynamicTrackingState(isDebugDynamicAccesses);
|
|
let serverIsDynamic = false;
|
|
const finalServerPrerenderStore = prerenderStore = {
|
|
type: 'prerender',
|
|
phase: 'render',
|
|
rootParams,
|
|
fallbackRouteParams,
|
|
implicitTags,
|
|
renderSignal: finalServerRenderController.signal,
|
|
controller: finalServerReactController,
|
|
// All caches we could read must already be filled so no tracking is necessary
|
|
cacheSignal: null,
|
|
dynamicTracking: serverDynamicTracking,
|
|
allowEmptyStaticShell,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
prerenderResumeDataCache,
|
|
renderResumeDataCache,
|
|
hmrRefreshHash: undefined,
|
|
varyParamsAccumulator
|
|
};
|
|
if (staleTimeIterable !== undefined) {
|
|
trackStaleTime(finalServerPrerenderStore, staleTimeIterable, selectStaleTime);
|
|
}
|
|
let prerenderIsPending = true;
|
|
const finalRSCPrerenderOptions = {
|
|
filterStackFrame,
|
|
onError: (err)=>{
|
|
return serverComponentsErrorHandler(err);
|
|
},
|
|
signal: finalServerReactController.signal
|
|
};
|
|
const finalRSCAbortCallback = async ()=>{
|
|
// Now that the prerendering is complete, we know the final stale
|
|
// time and vary params. Close the stale time iterable and resolve
|
|
// the vary params thenable so Flight can serialize their values
|
|
// into the stream. The timing here is important: both were
|
|
// included in the Flight payload, but they can only be serialized
|
|
// at the very end, after all the components have finished.
|
|
//
|
|
// We resolve these directly here instead of reading from the work
|
|
// unit store because this callback runs in a separate task (via
|
|
// setTimeout) and may not have access to the async storage context.
|
|
const pendingFinishes = [
|
|
finishAccumulatingVaryParams(varyParamsAccumulator)
|
|
];
|
|
if (staleTimeIterable !== undefined) {
|
|
pendingFinishes.push(finishStaleTimeTracking(staleTimeIterable));
|
|
}
|
|
await Promise.all(pendingFinishes);
|
|
if (finalServerReactController.signal.aborted) {
|
|
// If the server controller is already aborted we must have called something
|
|
// that required aborting the prerender synchronously such as with new Date()
|
|
serverIsDynamic = true;
|
|
return;
|
|
}
|
|
if (prerenderIsPending) {
|
|
// If prerenderIsPending then we have blocked for longer than a Task and we assume
|
|
// there is something unfinished.
|
|
serverIsDynamic = true;
|
|
}
|
|
finalServerReactController.abort();
|
|
};
|
|
const finalRSCPrerenderFn = async ()=>{
|
|
const pendingPrerenderResult = workUnitAsyncStorage.run(// The store to scope
|
|
finalServerPrerenderStore, // The function to run
|
|
getServerPrerender(ComponentMod), // ... the arguments for the function to run
|
|
finalAttemptRSCPayload, clientModules, finalRSCPrerenderOptions);
|
|
// The listener to abort our own render controller must be added
|
|
// after React has added its listener, to ensure that pending I/O
|
|
// is not aborted/rejected too early.
|
|
finalServerReactController.signal.addEventListener('abort', ()=>{
|
|
finalServerRenderController.abort();
|
|
}, {
|
|
once: true
|
|
});
|
|
const prerenderResult = await pendingPrerenderResult;
|
|
prerenderIsPending = false;
|
|
return prerenderResult;
|
|
};
|
|
const reactServerResult = reactServerPrerenderResult = await createReactServerPrerenderResult(runInSequentialTasks(finalRSCPrerenderFn, finalRSCAbortCallback));
|
|
const clientDynamicTracking = createDynamicTrackingState(isDebugDynamicAccesses);
|
|
const finalClientReactController = new AbortController();
|
|
const finalClientRenderController = new AbortController();
|
|
const finalClientPrerenderStore = {
|
|
type: 'prerender-client',
|
|
phase: 'render',
|
|
rootParams,
|
|
fallbackRouteParams,
|
|
implicitTags,
|
|
renderSignal: finalClientRenderController.signal,
|
|
controller: finalClientReactController,
|
|
// No APIs require a cacheSignal through the workUnitStore during the HTML prerender
|
|
cacheSignal: null,
|
|
dynamicTracking: clientDynamicTracking,
|
|
allowEmptyStaticShell,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
prerenderResumeDataCache,
|
|
renderResumeDataCache,
|
|
hmrRefreshHash: undefined,
|
|
// Client prerenders don't track server param access
|
|
varyParamsAccumulator: null
|
|
};
|
|
let dynamicValidation = createDynamicValidationState();
|
|
const finalClientOnHeaders = createOnHeadersCallback(appendHeader);
|
|
let { prelude: unprocessedPrelude, postponed } = await runInSequentialTasks(()=>{
|
|
const pendingFinalClientResult = workUnitAsyncStorage.run(finalClientPrerenderStore, getClientPrerender, // eslint-disable-next-line @next/internal/no-ambiguous-jsx
|
|
/*#__PURE__*/ _jsx(App, {
|
|
reactServerStream: reactServerResult.asUnclosingStream(),
|
|
reactDebugStream: undefined,
|
|
debugEndTime: undefined,
|
|
preinitScripts: preinitScripts,
|
|
ServerInsertedHTMLProvider: ServerInsertedHTMLProvider,
|
|
nonce: nonce,
|
|
images: ctx.renderOpts.images
|
|
}), {
|
|
signal: finalClientReactController.signal,
|
|
onError: (err, errorInfo)=>{
|
|
if (isPrerenderInterruptedError(err) || finalClientReactController.signal.aborted) {
|
|
const componentStack = errorInfo.componentStack;
|
|
if (typeof componentStack === 'string') {
|
|
trackAllowedDynamicAccess(workStore, componentStack, dynamicValidation, clientDynamicTracking);
|
|
}
|
|
return;
|
|
}
|
|
return htmlRendererErrorHandler(err, errorInfo);
|
|
},
|
|
onHeaders: finalClientOnHeaders,
|
|
maxHeadersLength: reactMaxHeadersLength,
|
|
bootstrapScripts: [
|
|
bootstrapScript
|
|
]
|
|
});
|
|
// The listener to abort our own render controller must be added
|
|
// after React has added its listener, to ensure that pending I/O is
|
|
// not aborted/rejected too early.
|
|
finalClientReactController.signal.addEventListener('abort', ()=>{
|
|
finalClientRenderController.abort();
|
|
}, {
|
|
once: true
|
|
});
|
|
return pendingFinalClientResult;
|
|
}, ()=>{
|
|
finalClientReactController.abort();
|
|
});
|
|
const { prelude, preludeIsEmpty } = await processPreludeOp(unprocessedPrelude);
|
|
// If we've disabled throwing on empty static shell, then we don't need to
|
|
// track any dynamic access that occurs above the suspense boundary because
|
|
// we'll do so in the route shell.
|
|
if (!allowEmptyStaticShell) {
|
|
throwIfDisallowedDynamic(workStore, preludeIsEmpty ? PreludeState.Empty : PreludeState.Full, dynamicValidation, serverDynamicTracking);
|
|
}
|
|
const getServerInsertedHTML = makeGetServerInsertedHTML({
|
|
polyfills,
|
|
renderServerInsertedHTML,
|
|
serverCapturedErrors: allCapturedErrors,
|
|
basePath,
|
|
tracingMetadata: tracingMetadata
|
|
});
|
|
metadata.flightData = await streamToBuffer(cachedNavigations ? prependIsPartialByte(reactServerResult.asStream(), serverIsDynamic) : reactServerResult.asStream());
|
|
// collectSegmentData needs the raw flight data without the marker byte.
|
|
const flightData = cachedNavigations ? metadata.flightData.subarray(1) : metadata.flightData;
|
|
await collectSegmentData(flightData, finalServerPrerenderStore, ComponentMod, renderOpts, ctx.pagePath, metadata);
|
|
if (serverIsDynamic) {
|
|
// Dynamic case
|
|
// We will always need to perform a "resume" render of some kind when this route is accessed
|
|
// because the RSC data itself is dynamic. We determine if there are any HTML holes or not
|
|
// but generally this is a "partial" prerender in that there will be a per-request compute
|
|
// concatenated to the static shell.
|
|
if (postponed != null) {
|
|
// Dynamic HTML case
|
|
metadata.postponed = await getDynamicHTMLPostponedState(postponed, preludeIsEmpty ? DynamicHTMLPreludeState.Empty : DynamicHTMLPreludeState.Full, fallbackRouteParams, prerenderResumeDataCache, cacheComponents);
|
|
} else {
|
|
// Dynamic Data case
|
|
metadata.postponed = await getDynamicDataPostponedState(prerenderResumeDataCache, cacheComponents);
|
|
}
|
|
reactServerResult.consume();
|
|
const continueDynamicPrerenderOpts = {
|
|
getServerInsertedHTML,
|
|
getServerInsertedMetadata,
|
|
deploymentId: ctx.sharedContext.deploymentId
|
|
};
|
|
return {
|
|
digestErrorsMap: reactServerErrorsByDigest,
|
|
ssrErrors: allCapturedErrors,
|
|
stream: await continueDynamicPrerender(prelude, continueDynamicPrerenderOpts),
|
|
dynamicAccess: consumeDynamicAccess(serverDynamicTracking, clientDynamicTracking),
|
|
// TODO: Should this include the SSR pass?
|
|
collectedRevalidate: finalServerPrerenderStore.revalidate,
|
|
collectedExpire: finalServerPrerenderStore.expire,
|
|
collectedStale: selectStaleTime(finalServerPrerenderStore.stale),
|
|
collectedTags: finalServerPrerenderStore.tags,
|
|
renderResumeDataCache: createRenderResumeDataCache(prerenderResumeDataCache)
|
|
};
|
|
} else {
|
|
// Static case
|
|
// We will not perform resumption per request. The result can be served statically to the requestor
|
|
// and if there was anything dynamic it will only be rendered in the browser.
|
|
if (workStore.forceDynamic) {
|
|
throw Object.defineProperty(new StaticGenBailoutError('Invariant: a Page with `dynamic = "force-dynamic"` did not trigger the dynamic pathway. This is a bug in Next.js'), "__NEXT_ERROR_CODE", {
|
|
value: "E598",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
let htmlStream = 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
|
|
/*#__PURE__*/ _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);
|
|
}
|
|
let finalStream;
|
|
const hasFallbackRouteParams = fallbackRouteParams && fallbackRouteParams.size > 0;
|
|
if (hasFallbackRouteParams) {
|
|
// This is a "static fallback" prerender: although the page didn't
|
|
// access any runtime params in a Server Component, it may have
|
|
// accessed a runtime param in a client segment.
|
|
//
|
|
// TODO: If there were no client segments, we can use the fully static
|
|
// path instead.
|
|
//
|
|
// Rather than use a dynamic server resume to fill in the params,
|
|
// we can rely on the client to parse the params from the URL and use
|
|
// that to hydrate the page.
|
|
//
|
|
// Send an empty InitialRSCPayload to the server component renderer
|
|
// The data will be fetched by the client instead.
|
|
// TODO: In the future, rather than defer the entire hydration payload
|
|
// to be fetched by the client, we should only defer the client
|
|
// segments, since those are the only ones whose data is not complete.
|
|
const emptyReactServerResult = await createReactServerPrerenderResultFromRender(renderToFlightStream(ComponentMod, [], clientModules, {
|
|
filterStackFrame,
|
|
onError: serverComponentsErrorHandler
|
|
}));
|
|
finalStream = await continueStaticFallbackPrerender(htmlStream, {
|
|
inlinedDataStream: createInlinedDataStream(emptyReactServerResult.consumeAsStream(), nonce, formState),
|
|
getServerInsertedHTML,
|
|
getServerInsertedMetadata,
|
|
deploymentId: ctx.sharedContext.deploymentId
|
|
});
|
|
} else {
|
|
// Normal static prerender case, no fallback param handling needed
|
|
finalStream = await continueStaticPrerender(htmlStream, {
|
|
inlinedDataStream: createInlinedDataStream(reactServerResult.consumeAsStream(), nonce, formState),
|
|
getServerInsertedHTML,
|
|
getServerInsertedMetadata,
|
|
deploymentId: ctx.sharedContext.deploymentId
|
|
});
|
|
}
|
|
return {
|
|
digestErrorsMap: reactServerErrorsByDigest,
|
|
ssrErrors: allCapturedErrors,
|
|
stream: finalStream,
|
|
dynamicAccess: consumeDynamicAccess(serverDynamicTracking, clientDynamicTracking),
|
|
// TODO: Should this include the SSR pass?
|
|
collectedRevalidate: finalServerPrerenderStore.revalidate,
|
|
collectedExpire: finalServerPrerenderStore.expire,
|
|
collectedStale: selectStaleTime(finalServerPrerenderStore.stale),
|
|
collectedTags: finalServerPrerenderStore.tags,
|
|
renderResumeDataCache: createRenderResumeDataCache(prerenderResumeDataCache)
|
|
};
|
|
}
|
|
} else if (experimental.isRoutePPREnabled) {
|
|
// We're statically generating with PPR and need to do dynamic tracking
|
|
let dynamicTracking = createDynamicTrackingState(isDebugDynamicAccesses);
|
|
const prerenderResumeDataCache = createPrerenderResumeDataCache();
|
|
const reactServerPrerenderStore = prerenderStore = {
|
|
type: 'prerender-ppr',
|
|
phase: 'render',
|
|
rootParams,
|
|
fallbackRouteParams,
|
|
implicitTags,
|
|
dynamicTracking,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
prerenderResumeDataCache
|
|
};
|
|
const RSCPayload = await workUnitAsyncStorage.run(reactServerPrerenderStore, getRSCPayload, tree, ctx, {
|
|
is404: res.statusCode === 404
|
|
});
|
|
let reactServerResult;
|
|
reactServerResult = reactServerPrerenderResult = await createReactServerPrerenderResultFromRender(workUnitAsyncStorage.run(reactServerPrerenderStore, renderToFlightStream, ComponentMod, RSCPayload, clientModules, {
|
|
filterStackFrame,
|
|
onError: serverComponentsErrorHandler
|
|
}));
|
|
const ssrPrerenderStore = {
|
|
type: 'prerender-ppr',
|
|
phase: 'render',
|
|
rootParams,
|
|
fallbackRouteParams,
|
|
implicitTags,
|
|
dynamicTracking,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
],
|
|
prerenderResumeDataCache
|
|
};
|
|
const pprOnHeaders = createOnHeadersCallback(appendHeader);
|
|
const { prelude: unprocessedPrelude, postponed } = await workUnitAsyncStorage.run(ssrPrerenderStore, getClientPrerender, // eslint-disable-next-line @next/internal/no-ambiguous-jsx
|
|
/*#__PURE__*/ _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, prerenderResumeDataCache, cacheComponents);
|
|
} else {
|
|
// Dynamic Data case.
|
|
metadata.postponed = await getDynamicDataPostponedState(prerenderResumeDataCache, 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: reactServerPrerenderStore.revalidate,
|
|
collectedExpire: reactServerPrerenderStore.expire,
|
|
collectedStale: selectStaleTime(reactServerPrerenderStore.stale),
|
|
collectedTags: reactServerPrerenderStore.tags
|
|
};
|
|
} else if (fallbackRouteParams && fallbackRouteParams.size > 0) {
|
|
// Rendering the fallback case.
|
|
metadata.postponed = await getDynamicDataPostponedState(prerenderResumeDataCache, 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: reactServerPrerenderStore.revalidate,
|
|
collectedExpire: reactServerPrerenderStore.expire,
|
|
collectedStale: selectStaleTime(reactServerPrerenderStore.stale),
|
|
collectedTags: reactServerPrerenderStore.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 Object.defineProperty(new StaticGenBailoutError('Invariant: a Page with `dynamic = "force-dynamic"` did not trigger the dynamic pathway. This is a bug in Next.js'), "__NEXT_ERROR_CODE", {
|
|
value: "E598",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
let htmlStream = 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
|
|
/*#__PURE__*/ _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: reactServerPrerenderStore.revalidate,
|
|
collectedExpire: reactServerPrerenderStore.expire,
|
|
collectedStale: selectStaleTime(reactServerPrerenderStore.stale),
|
|
collectedTags: reactServerPrerenderStore.tags
|
|
};
|
|
}
|
|
} else {
|
|
const prerenderLegacyStore = prerenderStore = {
|
|
type: 'prerender-legacy',
|
|
phase: 'render',
|
|
rootParams,
|
|
implicitTags,
|
|
revalidate: INFINITE_CACHE,
|
|
expire: INFINITE_CACHE,
|
|
stale: INFINITE_CACHE,
|
|
tags: [
|
|
...implicitTags.tags
|
|
]
|
|
};
|
|
// This is a regular static generation. We don't do dynamic tracking because we rely on
|
|
// the old-school dynamic error handling to bail out of static generation
|
|
const RSCPayload = await workUnitAsyncStorage.run(prerenderLegacyStore, getRSCPayload, tree, ctx, {
|
|
is404: res.statusCode === 404
|
|
});
|
|
let reactServerResult;
|
|
reactServerResult = reactServerPrerenderResult = await createReactServerPrerenderResultFromRender(workUnitAsyncStorage.run(prerenderLegacyStore, renderToFlightStream, ComponentMod, RSCPayload, clientModules, {
|
|
filterStackFrame,
|
|
onError: serverComponentsErrorHandler
|
|
}));
|
|
const { stream: htmlStream } = await workUnitAsyncStorage.run(prerenderLegacyStore, renderToFizzStream, // eslint-disable-next-line @next/internal/no-ambiguous-jsx
|
|
/*#__PURE__*/ _jsx(App, {
|
|
reactServerStream: reactServerResult.asUnclosingStream(),
|
|
reactDebugStream: undefined,
|
|
debugEndTime: undefined,
|
|
preinitScripts: preinitScripts,
|
|
ServerInsertedHTMLProvider: ServerInsertedHTMLProvider,
|
|
nonce: nonce,
|
|
images: ctx.renderOpts.images
|
|
}), {
|
|
onError: htmlRendererErrorHandler,
|
|
nonce,
|
|
bootstrapScripts: [
|
|
bootstrapScript
|
|
]
|
|
});
|
|
if (shouldGenerateStaticFlightData(workStore)) {
|
|
const flightData = await streamToBuffer(reactServerResult.asStream());
|
|
metadata.flightData = flightData;
|
|
await collectSegmentData(flightData, prerenderLegacyStore, ComponentMod, renderOpts, ctx.pagePath, metadata);
|
|
}
|
|
const getServerInsertedHTML = makeGetServerInsertedHTML({
|
|
polyfills,
|
|
renderServerInsertedHTML,
|
|
serverCapturedErrors: allCapturedErrors,
|
|
basePath,
|
|
tracingMetadata: tracingMetadata
|
|
});
|
|
return {
|
|
digestErrorsMap: reactServerErrorsByDigest,
|
|
ssrErrors: allCapturedErrors,
|
|
stream: await continueFizzStream(htmlStream, {
|
|
inlinedDataStream: createInlinedDataStream(reactServerResult.consumeAsStream(), nonce, formState),
|
|
isStaticGeneration: true,
|
|
getServerInsertedHTML,
|
|
getServerInsertedMetadata,
|
|
deploymentId: ctx.sharedContext.deploymentId
|
|
}),
|
|
// TODO: Should this include the SSR pass?
|
|
collectedRevalidate: prerenderLegacyStore.revalidate,
|
|
collectedExpire: prerenderLegacyStore.expire,
|
|
collectedStale: selectStaleTime(prerenderLegacyStore.stale),
|
|
collectedTags: prerenderLegacyStore.tags
|
|
};
|
|
}
|
|
} catch (err) {
|
|
if (isStaticGenBailoutError(err) || typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string' && err.message.includes('https://nextjs.org/docs/advanced-features/static-html-export')) {
|
|
// Ensure that "next dev" prints the red error overlay
|
|
throw err;
|
|
}
|
|
// If this is a static generation error, we need to throw it so that it
|
|
// can be handled by the caller if we're in static generation mode.
|
|
if (isDynamicServerError(err)) {
|
|
throw err;
|
|
}
|
|
// If a bailout made it to this point, it means it wasn't wrapped inside
|
|
// a suspense boundary.
|
|
const shouldBailoutToCSR = isBailoutToCSRError(err);
|
|
if (shouldBailoutToCSR) {
|
|
const stack = getStackWithoutErrorMessage(err);
|
|
error(`${err.reason} should be wrapped in a suspense boundary at page "${pagePath}". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout\n${stack}`);
|
|
throw err;
|
|
}
|
|
// If we errored when we did not have an RSC stream to read from. This is
|
|
// not just a render error, we need to throw early.
|
|
if (reactServerPrerenderResult === null) {
|
|
throw err;
|
|
}
|
|
let errorType;
|
|
if (isHTTPAccessFallbackError(err)) {
|
|
res.statusCode = getAccessFallbackHTTPStatus(err);
|
|
metadata.statusCode = res.statusCode;
|
|
errorType = getAccessFallbackErrorTypeByStatus(res.statusCode);
|
|
} else if (isRedirectError(err)) {
|
|
errorType = 'redirect';
|
|
res.statusCode = getRedirectStatusCodeFromError(err);
|
|
metadata.statusCode = res.statusCode;
|
|
const redirectUrl = addPathPrefix(getURLFromRedirectError(err), basePath);
|
|
setHeader('location', redirectUrl);
|
|
} else if (!shouldBailoutToCSR) {
|
|
res.statusCode = 500;
|
|
metadata.statusCode = res.statusCode;
|
|
}
|
|
const [errorPreinitScripts, errorBootstrapScript] = getRequiredScripts(buildManifest, assetPrefix, crossOrigin, subresourceIntegrityManifest, getAssetQueryString(ctx, false), nonce, '/_not-found/page');
|
|
const prerenderLegacyStore = prerenderStore = {
|
|
type: 'prerender-legacy',
|
|
phase: 'render',
|
|
rootParams,
|
|
implicitTags: implicitTags,
|
|
revalidate: typeof (prerenderStore == null ? void 0 : prerenderStore.revalidate) !== 'undefined' ? prerenderStore.revalidate : INFINITE_CACHE,
|
|
expire: typeof (prerenderStore == null ? void 0 : prerenderStore.expire) !== 'undefined' ? prerenderStore.expire : INFINITE_CACHE,
|
|
stale: typeof (prerenderStore == null ? void 0 : prerenderStore.stale) !== 'undefined' ? prerenderStore.stale : INFINITE_CACHE,
|
|
tags: [
|
|
...(prerenderStore == null ? void 0 : prerenderStore.tags) || implicitTags.tags
|
|
]
|
|
};
|
|
const errorRSCPayload = await workUnitAsyncStorage.run(prerenderLegacyStore, getErrorRSCPayload, tree, ctx, reactServerErrorsByDigest.has(err.digest) ? undefined : err, errorType);
|
|
const errorServerStream = workUnitAsyncStorage.run(prerenderLegacyStore, renderToFlightStream, ComponentMod, errorRSCPayload, clientModules, {
|
|
filterStackFrame,
|
|
onError: serverComponentsErrorHandler
|
|
});
|
|
try {
|
|
const { stream: errorHtmlStream } = await workUnitAsyncStorage.run(prerenderLegacyStore, renderToFizzStream, // eslint-disable-next-line @next/internal/no-ambiguous-jsx
|
|
/*#__PURE__*/ _jsx(ErrorApp, {
|
|
reactServerStream: errorServerStream,
|
|
ServerInsertedHTMLProvider: ServerInsertedHTMLProvider,
|
|
preinitScripts: errorPreinitScripts,
|
|
nonce: nonce,
|
|
images: ctx.renderOpts.images
|
|
}), {
|
|
nonce,
|
|
bootstrapScripts: [
|
|
errorBootstrapScript
|
|
],
|
|
formState
|
|
});
|
|
if (shouldGenerateStaticFlightData(workStore)) {
|
|
const flightData = await streamToBuffer(reactServerPrerenderResult.asStream());
|
|
metadata.flightData = flightData;
|
|
await collectSegmentData(flightData, prerenderLegacyStore, ComponentMod, renderOpts, ctx.pagePath, metadata);
|
|
}
|
|
// This is intentionally using the readable datastream from the main
|
|
// render rather than the flight data from the error page render
|
|
const flightStream = reactServerPrerenderResult.consumeAsStream();
|
|
return {
|
|
digestErrorsMap: reactServerErrorsByDigest,
|
|
ssrErrors: allCapturedErrors,
|
|
stream: await continueFizzStream(errorHtmlStream, {
|
|
inlinedDataStream: createInlinedDataStream(flightStream, nonce, formState),
|
|
isStaticGeneration: true,
|
|
getServerInsertedHTML: makeGetServerInsertedHTML({
|
|
polyfills,
|
|
renderServerInsertedHTML,
|
|
serverCapturedErrors: [],
|
|
basePath,
|
|
tracingMetadata: tracingMetadata
|
|
}),
|
|
getServerInsertedMetadata,
|
|
validateRootLayout: !!process.env.__NEXT_DEV_SERVER,
|
|
deploymentId: ctx.sharedContext.deploymentId
|
|
}),
|
|
dynamicAccess: null,
|
|
collectedRevalidate: prerenderStore !== null ? prerenderStore.revalidate : INFINITE_CACHE,
|
|
collectedExpire: prerenderStore !== null ? prerenderStore.expire : INFINITE_CACHE,
|
|
collectedStale: selectStaleTime(prerenderStore !== null ? prerenderStore.stale : INFINITE_CACHE),
|
|
collectedTags: prerenderStore !== null ? prerenderStore.tags : null
|
|
};
|
|
} catch (finalErr) {
|
|
if (process.env.__NEXT_DEV_SERVER && isHTTPAccessFallbackError(finalErr)) {
|
|
const { bailOnRootNotFound } = require('../../client/components/dev-root-http-access-fallback-boundary');
|
|
bailOnRootNotFound();
|
|
}
|
|
throw finalErr;
|
|
}
|
|
}
|
|
}
|
|
const getGlobalErrorStyles = async (tree, ctx)=>{
|
|
const globalErrorModule = parseLoaderTree(tree).modules['global-error'];
|
|
if (!globalErrorModule) {
|
|
throw Object.defineProperty(new Error('Invariant: global-error module is required but not found in loader tree'), "__NEXT_ERROR_CODE", {
|
|
value: "E983",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
const { componentMod: { createElement } } = ctx;
|
|
// Get the GlobalError component and styles from the loader tree
|
|
const [GlobalErrorComponent, styles] = await createComponentStylesAndScripts({
|
|
ctx,
|
|
filePath: globalErrorModule[1],
|
|
getComponent: globalErrorModule[0],
|
|
injectedCSS: new Set(),
|
|
injectedJS: new Set()
|
|
});
|
|
let globalErrorStyles = styles;
|
|
if (process.env.__NEXT_DEV_SERVER) {
|
|
const dir = (process.env.NEXT_RUNTIME === 'edge' ? process.env.__NEXT_EDGE_PROJECT_DIR : ctx.renderOpts.dir) || '';
|
|
const globalErrorModulePath = normalizeConventionFilePath(dir, globalErrorModule[1]);
|
|
if (globalErrorModulePath) {
|
|
const SegmentViewNode = ctx.componentMod.SegmentViewNode;
|
|
globalErrorStyles = // This will be rendered next to GlobalError component under ErrorBoundary,
|
|
// it requires a key to avoid React warning about duplicate keys.
|
|
createElement(SegmentViewNode, {
|
|
key: 'ge-svn',
|
|
type: 'global-error',
|
|
pagePath: globalErrorModulePath
|
|
}, globalErrorStyles);
|
|
}
|
|
}
|
|
return {
|
|
GlobalError: GlobalErrorComponent,
|
|
styles: globalErrorStyles
|
|
};
|
|
};
|
|
async function collectSegmentData(fullPageDataBuffer, prerenderStore, ComponentMod, renderOpts, pagePath, metadata) {
|
|
// Per-segment prefetch data
|
|
//
|
|
// All of the segments for a page are generated simultaneously, including
|
|
// during revalidations. This is to ensure consistency, because it's
|
|
// possible for a mismatch between a layout and page segment can cause the
|
|
// client to error during rendering. We want to preserve the ability of the
|
|
// client to recover from such a mismatch by re-requesting all the segments
|
|
// to get a consistent view of the page.
|
|
//
|
|
// For performance, we reuse the Flight output that was created when
|
|
// generating the initial page HTML. The Flight stream for the whole page is
|
|
// decomposed into a separate stream per segment.
|
|
const { clientModules, edgeRscModuleMapping, rscModuleMapping } = getClientReferenceManifest();
|
|
// Manifest passed to the Flight client for reading the full-page Flight
|
|
// stream. Based off similar code in use-cache-wrapper.ts.
|
|
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge';
|
|
const serverConsumerManifest = {
|
|
// moduleLoading must be null because we don't want to trigger preloads of ClientReferences
|
|
// to be added to the consumer. Instead, we'll wait for any ClientReference to be emitted
|
|
// which themselves will handle the preloading.
|
|
moduleLoading: null,
|
|
moduleMap: isEdgeRuntime ? edgeRscModuleMapping : rscModuleMapping,
|
|
serverModuleMap: getServerModuleMap()
|
|
};
|
|
const selectStaleTime = createSelectStaleTime(renderOpts.experimental);
|
|
const staleTime = selectStaleTime(prerenderStore.stale);
|
|
// Resolve prefetch hints. At runtime (next start / ISR), the precomputed
|
|
// hints are already loaded from the prefetch-hints.json manifest. During
|
|
// build, compute them by measuring segment gzip sizes and write them to
|
|
// metadata so the build pipeline can persist them to the manifest.
|
|
let hints;
|
|
const prefetchInlining = renderOpts.experimental.prefetchInlining;
|
|
if (!prefetchInlining) {
|
|
hints = null;
|
|
} else if (renderOpts.isBuildTimePrerendering) {
|
|
// Build time: compute fresh hints and store in metadata for the manifest.
|
|
hints = await ComponentMod.collectPrefetchHints(fullPageDataBuffer, staleTime, clientModules, serverConsumerManifest, prefetchInlining.maxSize, prefetchInlining.maxBundleSize);
|
|
metadata.prefetchHints = hints;
|
|
} else {
|
|
var _renderOpts_prefetchHints;
|
|
// Runtime: use hints from the manifest. Never compute fresh hints
|
|
// during ISR/revalidation.
|
|
hints = ((_renderOpts_prefetchHints = renderOpts.prefetchHints) == null ? void 0 : _renderOpts_prefetchHints[pagePath]) ?? null;
|
|
}
|
|
// Pass the resolved hints so collectSegmentData can union them into
|
|
// the TreePrefetch. During the initial build the FlightRouterState in
|
|
// the buffer doesn't have inlining hints yet (they were just computed
|
|
// above), so we need to merge them in here. At runtime/ISR the hints
|
|
// are already embedded in the FlightRouterState, so this is null.
|
|
metadata.segmentData = await ComponentMod.collectSegmentData(renderOpts.cacheComponents, fullPageDataBuffer, staleTime, clientModules, serverConsumerManifest, Boolean(renderOpts.experimental.prefetchInlining), hints);
|
|
}
|
|
function isBypassingCachesInDev(requestStore) {
|
|
return !!process.env.__NEXT_DEV_SERVER && requestStore.headers.get('cache-control') === 'no-cache';
|
|
}
|
|
function WarnForBypassCachesInDev({ route }) {
|
|
warnOnce(`Route ${route} is rendering with server caches disabled. For this navigation, Component Metadata in React DevTools will not accurately reflect what is statically prerenderable and runtime prefetchable. See more info here: https://nextjs.org/docs/messages/cache-bypass-in-dev`);
|
|
return null;
|
|
}
|
|
function nodeStreamFromReadableStream(stream) {
|
|
if (process.env.NEXT_RUNTIME === 'edge') {
|
|
throw Object.defineProperty(new InvariantError('nodeStreamFromReadableStream cannot be used in the edge runtime'), "__NEXT_ERROR_CODE", {
|
|
value: "E944",
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
} else {
|
|
const reader = stream.getReader();
|
|
const { Readable } = require('node:stream');
|
|
return new Readable({
|
|
read () {
|
|
reader.read().then(({ done, value })=>{
|
|
if (done) {
|
|
this.push(null);
|
|
} else {
|
|
this.push(value);
|
|
}
|
|
}).catch((err)=>this.destroy(err));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
//# sourceMappingURL=app-render.js.map
|