diff --git a/packages/next-swc/crates/next-core/js/src/entry/app-renderer.tsx b/packages/next-swc/crates/next-core/js/src/entry/app-renderer.tsx index 8abb6612dda01..87aba4ce6c6cc 100644 --- a/packages/next-swc/crates/next-core/js/src/entry/app-renderer.tsx +++ b/packages/next-swc/crates/next-core/js/src/entry/app-renderer.tsx @@ -259,7 +259,8 @@ async function runOperation(renderData: RenderData) { renderOpt as any as RenderOpts ); - if (!result) throw new Error("rendering was not successful"); + if (!result || result.isNull()) + throw new Error("rendering was not successful"); let body; if (result.isDynamic()) { diff --git a/packages/next-swc/crates/next-core/js/src/internal/page-server-handler.tsx b/packages/next-swc/crates/next-core/js/src/internal/page-server-handler.tsx index fb085568d5ea1..2b1a81462aa54 100644 --- a/packages/next-swc/crates/next-core/js/src/internal/page-server-handler.tsx +++ b/packages/next-swc/crates/next-core/js/src/internal/page-server-handler.tsx @@ -239,17 +239,17 @@ export default function startHandler({ ); // Set when `getStaticProps` returns `notFound: true`. - const isNotFound = (renderOpts as any).isNotFound; + const isNotFound = renderResult.metadata().isNotFound; if (isNotFound) { return createNotFoundResponse(isDataReq); } // Set when `getStaticProps` returns `redirect: { destination, permanent, statusCode }`. - const isRedirect = (renderOpts as any).isRedirect; + const isRedirect = renderResult.metadata().isRedirect; if (isRedirect && !isDataReq) { - const pageProps = (renderOpts as any).pageData.pageProps; + const pageProps = renderResult.metadata().pageData.pageProps; const redirect = { destination: pageProps.__N_REDIRECT, statusCode: pageProps.__N_REDIRECT_STATUS, @@ -283,7 +283,7 @@ export default function startHandler({ if (isDataReq) { // TODO(from next.js): change this to a different passing mechanism - const pageData = (renderOpts as any).pageData; + const pageData = renderResult.metadata().pageData; return { type: "response", statusCode, @@ -293,14 +293,14 @@ export default function startHandler({ }; } - if (!renderResult) { + if (!renderResult || renderResult.isNull()) { throw new Error("no render result returned"); } const body = renderResult.toUnchunkedString(); // TODO: handle revalidate - // const sprRevalidate = (renderOpts as any).revalidate; + // const sprRevalidate = renderResult.metadata().revalidate; return { type: "response", diff --git a/packages/next/src/build/webpack/loaders/next-serverless-loader/page-handler.ts b/packages/next/src/build/webpack/loaders/next-serverless-loader/page-handler.ts index 0c98bcde24754..c18de99fc18ca 100644 --- a/packages/next/src/build/webpack/loaders/next-serverless-loader/page-handler.ts +++ b/packages/next/src/build/webpack/loaders/next-serverless-loader/page-handler.ts @@ -308,10 +308,11 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { ), renderOpts ) + const renderResultMeta = result.metadata() if (!renderMode) { if (_nextData || getStaticProps || getServerSideProps) { - if (renderOpts.isNotFound) { + if (renderResultMeta.isNotFound) { res.statusCode = 404 if (_nextData) { @@ -343,22 +344,24 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { sendRenderResult({ req, res, - result: result2 ?? RenderResult.empty, + result: result2.isNull() ? RenderResult.empty : result2, type: 'html', generateEtags, poweredByHeader, options: { private: isPreviewMode || page === '/404', stateful: !!getServerSideProps, - revalidate: renderOpts.revalidate, + revalidate: renderResultMeta.revalidate, }, }) return null - } else if (renderOpts.isRedirect && !_nextData) { + } else if (renderResultMeta.isRedirect && !_nextData) { const redirect = { - destination: renderOpts.pageData.pageProps.__N_REDIRECT, - statusCode: renderOpts.pageData.pageProps.__N_REDIRECT_STATUS, - basePath: renderOpts.pageData.pageProps.__N_REDIRECT_BASE_PATH, + destination: renderResultMeta.pageData.pageProps.__N_REDIRECT, + statusCode: + renderResultMeta.pageData.pageProps.__N_REDIRECT_STATUS, + basePath: + renderResultMeta.pageData.pageProps.__N_REDIRECT_BASE_PATH, } const statusCode = getRedirectStatus(redirect) @@ -383,15 +386,19 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { req, res, result: _nextData - ? RenderResult.fromStatic(JSON.stringify(renderOpts.pageData)) - : result ?? RenderResult.empty, + ? RenderResult.fromStatic( + JSON.stringify(renderResultMeta.pageData) + ) + : result.isNull() + ? RenderResult.empty + : result, type: _nextData ? 'json' : 'html', generateEtags, poweredByHeader, options: { private: isPreviewMode || renderOpts.is404Page, stateful: !!getServerSideProps, - revalidate: renderOpts.revalidate, + revalidate: renderResultMeta.revalidate, }, }) return null @@ -405,7 +412,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { } if (renderMode) return { html: result, renderOpts } - return result ? result.toUnchunkedString() : null + return result.isNull() ? null : result.toUnchunkedString() } catch (err) { if (!parsedUrl!) { parsedUrl = parseUrl(req.url!, true) @@ -467,7 +474,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { err: res.statusCode === 404 ? undefined : err, }) ) - return result2 ? result2.toUnchunkedString() : null + return result2.isNull() ? null : result2.toUnchunkedString() } } diff --git a/packages/next/src/client/components/router-reducer/apply-router-state-patch-to-tree.ts b/packages/next/src/client/components/router-reducer/apply-router-state-patch-to-tree.ts index 655794d040fa4..00d5d2a9f6fea 100644 --- a/packages/next/src/client/components/router-reducer/apply-router-state-patch-to-tree.ts +++ b/packages/next/src/client/components/router-reducer/apply-router-state-patch-to-tree.ts @@ -1,4 +1,4 @@ -import { +import type { FlightRouterState, FlightSegmentPath, } from '../../../server/app-render' diff --git a/packages/next/src/client/components/router-reducer/create-optimistic-tree.ts b/packages/next/src/client/components/router-reducer/create-optimistic-tree.ts index 3074d8b2f1651..82b1d697cd91f 100644 --- a/packages/next/src/client/components/router-reducer/create-optimistic-tree.ts +++ b/packages/next/src/client/components/router-reducer/create-optimistic-tree.ts @@ -1,4 +1,4 @@ -import { FlightRouterState } from '../../../server/app-render' +import type { FlightRouterState } from '../../../server/app-render' import { matchSegment } from '../match-segments' /** diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index e14b067be0671..56cd3246016af 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -1,7 +1,7 @@ 'use client' import { createFromFetch } from 'next/dist/compiled/react-server-dom-webpack/client' -import { FlightRouterState, FlightData } from '../../../server/app-render' +import type { FlightRouterState, FlightData } from '../../../server/app-render' import { NEXT_ROUTER_PREFETCH, NEXT_ROUTER_STATE_TREE, diff --git a/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts b/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts index 75bd1771db76f..1cecc6e728312 100644 --- a/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts +++ b/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts @@ -1,5 +1,5 @@ import { CacheNode, CacheStates } from '../../../shared/lib/app-router-context' -import { FlightDataPath } from '../../../server/app-render' +import type { FlightDataPath } from '../../../server/app-render' import { invalidateCacheByRouterState } from './invalidate-cache-by-router-state' import { fillLazyItemsTillLeafWithHead } from './fill-lazy-items-till-leaf-with-head' diff --git a/packages/next/src/client/components/router-reducer/fill-lazy-items-till-leaf-with-head.ts b/packages/next/src/client/components/router-reducer/fill-lazy-items-till-leaf-with-head.ts index 2c81f37b9108c..55734e08e6861 100644 --- a/packages/next/src/client/components/router-reducer/fill-lazy-items-till-leaf-with-head.ts +++ b/packages/next/src/client/components/router-reducer/fill-lazy-items-till-leaf-with-head.ts @@ -1,5 +1,5 @@ import { CacheNode, CacheStates } from '../../../shared/lib/app-router-context' -import { FlightRouterState } from '../../../server/app-render' +import type { FlightRouterState } from '../../../server/app-render' export function fillLazyItemsTillLeafWithHead( newCache: CacheNode, diff --git a/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.ts b/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.ts index 875fc7ed00252..ba97b2b262c0a 100644 --- a/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.ts +++ b/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.ts @@ -1,5 +1,5 @@ -import { CacheNode } from '../../../shared/lib/app-router-context' -import { FlightSegmentPath } from '../../../server/app-render' +import type { CacheNode } from '../../../shared/lib/app-router-context' +import type { FlightSegmentPath } from '../../../server/app-render' /** * Fill cache up to the end of the flightSegmentPath, invalidating anything below it. diff --git a/packages/next/src/client/components/router-reducer/invalidate-cache-by-router-state.ts b/packages/next/src/client/components/router-reducer/invalidate-cache-by-router-state.ts index b52b636439b8a..e3a8d8da30817 100644 --- a/packages/next/src/client/components/router-reducer/invalidate-cache-by-router-state.ts +++ b/packages/next/src/client/components/router-reducer/invalidate-cache-by-router-state.ts @@ -1,5 +1,5 @@ -import { CacheNode } from '../../../shared/lib/app-router-context' -import { FlightRouterState } from '../../../server/app-render' +import type { CacheNode } from '../../../shared/lib/app-router-context' +import type { FlightRouterState } from '../../../server/app-render' /** * Invalidate cache one level down from the router state. diff --git a/packages/next/src/client/components/router-reducer/is-navigating-to-new-root-layout.ts b/packages/next/src/client/components/router-reducer/is-navigating-to-new-root-layout.ts index e94cb7e89f232..5948dcbc8b67c 100644 --- a/packages/next/src/client/components/router-reducer/is-navigating-to-new-root-layout.ts +++ b/packages/next/src/client/components/router-reducer/is-navigating-to-new-root-layout.ts @@ -1,4 +1,4 @@ -import { FlightRouterState } from '../../../server/app-render' +import type { FlightRouterState } from '../../../server/app-render' export function isNavigatingToNewRootLayout( currentTree: FlightRouterState, diff --git a/packages/next/src/client/components/router-reducer/reducers/find-head-in-cache.ts b/packages/next/src/client/components/router-reducer/reducers/find-head-in-cache.ts index fa14078359082..b861b2a167b39 100644 --- a/packages/next/src/client/components/router-reducer/reducers/find-head-in-cache.ts +++ b/packages/next/src/client/components/router-reducer/reducers/find-head-in-cache.ts @@ -1,5 +1,5 @@ -import { FlightRouterState } from '../../../../server/app-render' -import { ChildSegmentMap } from '../../../../shared/lib/app-router-context' +import type { FlightRouterState } from '../../../../server/app-render' +import type { ChildSegmentMap } from '../../../../shared/lib/app-router-context' export function findHeadInCache( childSegmentMap: ChildSegmentMap, diff --git a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts index 76fe666c6f134..79eac34cd5f9a 100644 --- a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts @@ -18,7 +18,7 @@ import { createOptimisticTree } from '../create-optimistic-tree' import { applyRouterStatePatchToTree } from '../apply-router-state-patch-to-tree' import { shouldHardNavigate } from '../should-hard-navigate' import { isNavigatingToNewRootLayout } from '../is-navigating-to-new-root-layout' -import { +import type { Mutable, NavigateAction, ReadonlyReducerState, diff --git a/packages/next/src/client/components/router-reducer/router-reducer-types.ts b/packages/next/src/client/components/router-reducer/router-reducer-types.ts index 27d5b69f4ded5..013754f3862bf 100644 --- a/packages/next/src/client/components/router-reducer/router-reducer-types.ts +++ b/packages/next/src/client/components/router-reducer/router-reducer-types.ts @@ -1,5 +1,5 @@ -import { CacheNode } from '../../../shared/lib/app-router-context' -import { FlightRouterState, FlightData } from '../../../server/app-render' +import type { CacheNode } from '../../../shared/lib/app-router-context' +import type { FlightRouterState, FlightData } from '../../../server/app-render' import { fetchServerResponse } from './fetch-server-response' export const ACTION_REFRESH = 'refresh' diff --git a/packages/next/src/client/components/router-reducer/should-hard-navigate.ts b/packages/next/src/client/components/router-reducer/should-hard-navigate.ts index 82fa72fc8f258..5ea520ca3f198 100644 --- a/packages/next/src/client/components/router-reducer/should-hard-navigate.ts +++ b/packages/next/src/client/components/router-reducer/should-hard-navigate.ts @@ -1,4 +1,4 @@ -import { +import type { FlightRouterState, FlightDataPath, Segment, diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index 20352a557616f..86dc5ba80efba 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -293,9 +293,10 @@ export default async function exportPage({ let htmlFilepath = join(outDir, htmlFilename) await promises.mkdir(baseDir, { recursive: true }) - let renderResult + let renderResult: RenderResult | undefined let curRenderOpts: RenderOpts = {} - const { renderToHTML } = require('../server/render') + const { renderToHTML } = + require('../server/render') as typeof import('../server/render') let renderMethod = renderToHTML let inAmpMode = false, hybridAmp = false @@ -467,9 +468,10 @@ export default async function exportPage({ query, curRenderOpts as any ) - const html = result?.toUnchunkedString() - const flightData = (curRenderOpts as any).pageData - const revalidate = (curRenderOpts as any).revalidate + const html = result.toUnchunkedString() + const renderResultMeta = result.metadata() + const flightData = renderResultMeta.pageData + const revalidate = renderResultMeta.revalidate results.fromBuildExportRevalidate = revalidate if (revalidate !== 0) { @@ -484,7 +486,7 @@ export default async function exportPage({ ) } - const { staticBailoutInfo = {} } = curRenderOpts as any + const staticBailoutInfo = renderResultMeta.staticBailoutInfo || {} if ( revalidate === 0 && @@ -574,7 +576,7 @@ export default async function exportPage({ } } - results.ssgNotFound = (curRenderOpts as any).isNotFound + results.ssgNotFound = renderResult?.metadata().isNotFound const validateAmp = async ( rawAmpHtml: string, @@ -597,7 +599,13 @@ export default async function exportPage({ } } - const html = renderResult ? renderResult.toUnchunkedString() : '' + const html = + renderResult && !renderResult.isNull() + ? renderResult.toUnchunkedString() + : '' + + let ampRenderResult: Awaited> | undefined + if (inAmpMode && !curRenderOpts.ampSkipValidation) { if (!results.ssgNotFound) { await validateAmp(html, path, curRenderOpts.ampValidatorPath) @@ -614,7 +622,6 @@ export default async function exportPage({ try { await promises.access(ampHtmlFilepath) } catch (_) { - let ampRenderResult // make sure it doesn't exist from manual mapping try { ampRenderResult = await renderMethod( @@ -631,9 +638,10 @@ export default async function exportPage({ } } - const ampHtml = ampRenderResult - ? ampRenderResult.toUnchunkedString() - : '' + const ampHtml = + ampRenderResult && !ampRenderResult.isNull() + ? ampRenderResult.toUnchunkedString() + : '' if (!curRenderOpts.ampSkipValidation) { await validateAmp(ampHtml, page + '?amp=1') } @@ -642,7 +650,9 @@ export default async function exportPage({ } } - if ((curRenderOpts as any).pageData) { + const renderResultMeta = + renderResult?.metadata() || ampRenderResult?.metadata() || {} + if (renderResultMeta.pageData) { const dataFile = join( pagesDataDir, htmlFilename.replace(/\.html$/, '.json') @@ -651,19 +661,19 @@ export default async function exportPage({ await promises.mkdir(dirname(dataFile), { recursive: true }) await promises.writeFile( dataFile, - JSON.stringify((curRenderOpts as any).pageData), + JSON.stringify(renderResultMeta.pageData), 'utf8' ) if (hybridAmp) { await promises.writeFile( dataFile.replace(/\.json$/, '.amp.json'), - JSON.stringify((curRenderOpts as any).pageData), + JSON.stringify(renderResultMeta.pageData), 'utf8' ) } } - results.fromBuildExportRevalidate = (curRenderOpts as any).revalidate + results.fromBuildExportRevalidate = renderResultMeta.revalidate if (!results.ssgNotFound) { // don't attempt writing to disk if getStaticProps returned not found diff --git a/packages/next/src/server/app-render.tsx b/packages/next/src/server/app-render.tsx index ecf5c6447b64f..98468b5734b53 100644 --- a/packages/next/src/server/app-render.tsx +++ b/packages/next/src/server/app-render.tsx @@ -12,7 +12,7 @@ import { NotFound as DefaultNotFound } from '../client/components/error' import ReactDOMServer from 'next/dist/compiled/react-dom/server.browser' import { ParsedUrlQuery } from 'querystring' import { NextParsedUrlQuery } from './request-meta' -import RenderResult from './render-result' +import RenderResult, { type RenderResultMetadata } from './render-result' import { readableStreamTee, encodeText, @@ -807,7 +807,7 @@ export async function renderToHTMLOrFlight( pathname: string, query: NextParsedUrlQuery, renderOpts: RenderOpts -): Promise { +): Promise { const isFlight = req.headers[RSC.toLowerCase()] !== undefined const { @@ -2071,22 +2071,20 @@ export async function renderToHTMLOrFlight( staticGenerationStore.revalidate = 0 } - // TODO: investigate why `pageData` is not in RenderOpts - ;(renderOpts as any).pageData = filteredFlightData - - // TODO: investigate why `revalidate` is not in RenderOpts - ;(renderOpts as any).revalidate = - staticGenerationStore.revalidate ?? defaultRevalidate + const extraRenderResultMeta: RenderResultMetadata = { + pageData: filteredFlightData, + revalidate: staticGenerationStore.revalidate ?? defaultRevalidate, + } // provide bailout info for debugging - if ((renderOpts as any).revalidate === 0) { - ;(renderOpts as any).staticBailoutInfo = { + if (extraRenderResultMeta.revalidate === 0) { + extraRenderResultMeta.staticBailoutInfo = { description: staticGenerationStore.dynamicUsageDescription, stack: staticGenerationStore.dynamicUsageStack, } } - return new RenderResult(htmlResult) + return new RenderResult(htmlResult, { ...extraRenderResultMeta }) } return renderResult diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 586f55e33c450..b87e921da9101 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -306,7 +306,7 @@ export default abstract class Server { pathname: string, query: NextParsedUrlQuery, renderOpts: RenderOpts - ): Promise + ): Promise protected abstract handleCompression( req: BaseNextRequest, @@ -1488,7 +1488,7 @@ export default abstract class Server { } let pageData: any - let body: RenderResult | null + let body: RenderResult let isrRevalidate: number | false let isNotFound: boolean | undefined let isRedirect: boolean | undefined @@ -1550,11 +1550,13 @@ export default abstract class Server { ) body = renderResult - // TODO: change this to a different passing mechanism - pageData = (renderOpts as any).pageData - isrRevalidate = (renderOpts as any).revalidate - isNotFound = (renderOpts as any).isNotFound - isRedirect = (renderOpts as any).isRedirect + + const renderResultMeta = renderResult.metadata() + + pageData = renderResultMeta.pageData + isrRevalidate = renderResultMeta.revalidate + isNotFound = renderResultMeta.isNotFound + isRedirect = renderResultMeta.isRedirect // we don't throw static to dynamic errors in dev as isSSG // is a best guess in dev since we don't have the prerender pass @@ -1563,7 +1565,7 @@ export default abstract class Server { const staticBailoutInfo: { stack?: string description?: string - } = (renderOpts as any).staticBailoutInfo || {} + } = renderResultMeta.staticBailoutInfo || {} const err = new Error( `Page changed from static to dynamic at runtime ${urlPathname}${ @@ -1588,7 +1590,7 @@ export default abstract class Server { } else if (isRedirect) { value = { kind: 'REDIRECT', props: pageData } } else { - if (!body) { + if (body.isNull()) { return null } value = { kind: 'PAGE', html: body, pageData } diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 1f8462868ed8a..c73f99040a35b 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -838,7 +838,7 @@ export default class NextNodeServer extends BaseServer { pathname: string, query: NextParsedUrlQuery, renderOpts: RenderOpts - ): Promise { + ): Promise { return getTracer().trace(NextNodeServerSpan.renderHTML, async () => this.renderHTMLImpl(req, res, pathname, query, renderOpts) ) @@ -850,7 +850,7 @@ export default class NextNodeServer extends BaseServer { pathname: string, query: NextParsedUrlQuery, renderOpts: RenderOpts - ): Promise { + ): Promise { // Due to the way we pass data by mutating `renderOpts`, we can't extend the // object here but only updating its `serverComponentManifest` field. // https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952 diff --git a/packages/next/src/server/render-result.ts b/packages/next/src/server/render-result.ts index df0c693738fc6..e39edd9ce6e58 100644 --- a/packages/next/src/server/render-result.ts +++ b/packages/next/src/server/render-result.ts @@ -3,16 +3,42 @@ import { Writable } from 'stream' type ContentTypeOption = string | undefined +export type RenderResultMetadata = { + pageData?: any + revalidate?: any + staticBailoutInfo?: any + devOnlyCacheBusterQueryString?: string + isNotFound?: boolean + isRedirect?: boolean +} + export default class RenderResult { - private _result: string | ReadableStream + private _result: string | ReadableStream | null private _contentType: ContentTypeOption + // Extra render result meta fields + private _metadata: RenderResultMetadata + constructor( - response: string | ReadableStream, - { contentType }: { contentType?: ContentTypeOption } = {} + response: string | ReadableStream | null, + { + contentType, + ...metadata + }: { + contentType?: ContentTypeOption + } & RenderResultMetadata = {} ) { this._result = response this._contentType = contentType + this._metadata = metadata + } + + metadata() { + return this._metadata + } + + isNull(): boolean { + return this._result === null } contentType(): ContentTypeOption { @@ -29,6 +55,9 @@ export default class RenderResult { } pipe(res: ServerResponse | Writable): Promise { + if (this._result === null) { + throw new Error('invariant: response is null. This is a bug in Next.js') + } if (typeof this._result === 'string') { throw new Error( 'invariant: static responses cannot be piped. This is a bug in Next.js' diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx index 2a271d287d51a..4caa42b212fa6 100644 --- a/packages/next/src/server/render.tsx +++ b/packages/next/src/server/render.tsx @@ -68,7 +68,7 @@ import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' import { getRequestMeta, NextParsedUrlQuery } from './request-meta' import { allowedStatusCodes, getRedirectStatus } from '../lib/redirect-status' -import RenderResult from './render-result' +import RenderResult, { type RenderResultMetadata } from './render-result' import isError from '../lib/is-error' import { streamFromArray, @@ -372,11 +372,13 @@ export async function renderToHTML( pathname: string, query: NextParsedUrlQuery, renderOpts: RenderOpts -): Promise { +): Promise { + const renderResultMeta: RenderResultMetadata = {} + // In dev we invalidate the cache by appending a timestamp to the resource URL. // This is a workaround to fix https://github.com/vercel/next.js/issues/5860 // TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed. - renderOpts.devOnlyCacheBusterQueryString = renderOpts.dev + renderResultMeta.devOnlyCacheBusterQueryString = renderOpts.dev ? renderOpts.devOnlyCacheBusterQueryString || `?ts=${Date.now()}` : '' @@ -398,12 +400,14 @@ export async function renderToHTML( params, previewProps, basePath, - devOnlyCacheBusterQueryString, images, runtime: globalRuntime, App, } = renderOpts + const devOnlyCacheBusterQueryString = + renderResultMeta.devOnlyCacheBusterQueryString + let Document = renderOpts.Document // Component will be wrapped by ServerComponentWrapper for RSC @@ -814,7 +818,7 @@ export async function renderToHTML( ) } - ;(renderOpts as any).isNotFound = true + renderResultMeta.isNotFound = true } if ( @@ -838,12 +842,12 @@ export async function renderToHTML( if (typeof data.redirect.basePath !== 'undefined') { ;(data as any).props.__N_REDIRECT_BASE_PATH = data.redirect.basePath } - ;(renderOpts as any).isRedirect = true + renderResultMeta.isRedirect = true } if ( (dev || isBuildTimeSSG) && - !(renderOpts as any).isNotFound && + !renderResultMeta.isNotFound && !isSerializableProps(pathname, 'getStaticProps', (data as any).props) ) { // this fn should throw an error instead of ever returning `false` @@ -909,14 +913,13 @@ export async function renderToHTML( ) // pass up revalidate and props for export - // TODO: change this to a different passing mechanism - ;(renderOpts as any).revalidate = + renderResultMeta.revalidate = 'revalidate' in data ? data.revalidate : undefined - ;(renderOpts as any).pageData = props + renderResultMeta.pageData = props - // this must come after revalidate is added to renderOpts - if ((renderOpts as any).isNotFound) { - return null + // this must come after revalidate is added to renderResultMeta + if (renderResultMeta.isNotFound) { + return new RenderResult(null, renderResultMeta) } } @@ -1022,8 +1025,8 @@ export async function renderToHTML( ) } - ;(renderOpts as any).isNotFound = true - return null + renderResultMeta.isNotFound = true + return new RenderResult(null, renderResultMeta) } if ('redirect' in data && typeof data.redirect === 'object') { @@ -1035,7 +1038,7 @@ export async function renderToHTML( if (typeof data.redirect.basePath !== 'undefined') { ;(data as any).props.__N_REDIRECT_BASE_PATH = data.redirect.basePath } - ;(renderOpts as any).isRedirect = true + renderResultMeta.isRedirect = true } if (deferredContent) { @@ -1053,7 +1056,7 @@ export async function renderToHTML( } props.pageProps = Object.assign({}, props.pageProps, (data as any).props) - ;(renderOpts as any).pageData = props + renderResultMeta.pageData = props } if ( @@ -1070,8 +1073,8 @@ export async function renderToHTML( // Avoid rendering page un-necessarily for getServerSideProps data request // and getServerSideProps/getStaticProps redirects - if ((isDataReq && !isSSG) || (renderOpts as any).isRedirect) { - return RenderResult.fromStatic(JSON.stringify(props)) + if ((isDataReq && !isSSG) || renderResultMeta.isRedirect) { + return new RenderResult(JSON.stringify(props), renderResultMeta) } // We don't call getStaticProps or getServerSideProps while generating @@ -1081,7 +1084,7 @@ export async function renderToHTML( } // the response might be finished on the getInitialProps call - if (isResSent(res) && !isSSG) return null + if (isResSent(res) && !isSSG) return new RenderResult(null, renderResultMeta) // we preload the buildManifest for auto-export dynamic pages // to speed up hydrating query values @@ -1324,7 +1327,7 @@ export async function renderToHTML( async () => renderDocument() ) if (!documentResult) { - return null + return new RenderResult(null, renderResultMeta) } const dynamicImportsIds = new Set() @@ -1479,13 +1482,14 @@ export async function renderToHTML( if (generateStaticHTML) { const html = await streamToString(chainStreams(streams)) const optimizedHtml = await postOptimize(html) - return new RenderResult(optimizedHtml) + return new RenderResult(optimizedHtml, renderResultMeta) } return new RenderResult( chainStreams(streams).pipeThrough( createBufferedTransformStream(postOptimize) - ) + ), + renderResultMeta ) } diff --git a/packages/next/src/server/web-server.ts b/packages/next/src/server/web-server.ts index 5c9ff4b43faa3..f408ea34f9a3e 100644 --- a/packages/next/src/server/web-server.ts +++ b/packages/next/src/server/web-server.ts @@ -377,7 +377,7 @@ export default class NextWebServer extends BaseServer { pathname: string, query: NextParsedUrlQuery, renderOpts: RenderOpts - ): Promise { + ): Promise { const { pagesRenderToHTML, appRenderToHTML } = this.serverOptions.webServerConfig const curRenderToHTML = pagesRenderToHTML || appRenderToHTML