Cleanup Render Result (#58782)
This improves some of the typings around the `RenderResult` returned during renders. Previously it had a single large metadata object that was shared across both the pages and app render pipelines. To add more type safety, this splits the types used by each of the render pipelines into their own types while still allowing the default `RenderResult` to reference metadata from either render pipeline. This also improved the flight data generation for app renders. Previously, the promise was inlined, and errors were swallowed. With the advent of improvements in #58779 the postpone errors are no longer returned by the flight generation and are instead handled correctly inside the render by React so it can emit properly postponed flight data. Besides that there was some whitespace changes, so hiding whitespace differences during review should make it much clearer to review!
This commit is contained in:
parent
8d1c619ad6
commit
f4c14935aa
11 changed files with 285 additions and 203 deletions
|
@ -1127,10 +1127,16 @@ export async function buildStaticPaths({
|
|||
}
|
||||
}
|
||||
|
||||
export type AppConfigDynamic =
|
||||
| 'auto'
|
||||
| 'error'
|
||||
| 'force-static'
|
||||
| 'force-dynamic'
|
||||
|
||||
export type AppConfig = {
|
||||
revalidate?: number | false
|
||||
dynamicParams?: true | false
|
||||
dynamic?: 'auto' | 'error' | 'force-static' | 'force-dynamic'
|
||||
dynamic?: AppConfigDynamic
|
||||
fetchCache?: 'force-cache' | 'only-cache'
|
||||
preferredRegion?: string
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import type { AsyncLocalStorage } from 'async_hooks'
|
|||
import type { IncrementalCache } from '../../server/lib/incremental-cache'
|
||||
import type { DynamicServerError } from './hooks-server-context'
|
||||
import type { FetchMetrics } from '../../server/base-http'
|
||||
import type { Revalidate } from '../../server/lib/revalidate'
|
||||
|
||||
import { createAsyncLocalStorage } from './async-local-storage'
|
||||
|
||||
export interface StaticGenerationStore {
|
||||
|
@ -23,7 +25,7 @@ export interface StaticGenerationStore {
|
|||
| 'default-no-store'
|
||||
| 'only-no-store'
|
||||
|
||||
revalidate?: false | number
|
||||
revalidate?: Revalidate
|
||||
forceStatic?: boolean
|
||||
dynamicShouldError?: boolean
|
||||
pendingRevalidates?: Record<string, Promise<any>>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import type { AppConfigDynamic } from '../../build/utils'
|
||||
|
||||
import { DynamicServerError } from './hooks-server-context'
|
||||
import { maybePostpone } from './maybe-postpone'
|
||||
import { staticGenerationAsyncStorage } from './static-generation-async-storage.external'
|
||||
|
@ -6,7 +8,7 @@ class StaticGenBailoutError extends Error {
|
|||
code = 'NEXT_STATIC_GEN_BAILOUT'
|
||||
}
|
||||
|
||||
type BailoutOpts = { dynamic?: string; link?: string }
|
||||
type BailoutOpts = { dynamic?: AppConfigDynamic; link?: string }
|
||||
|
||||
export type StaticGenerationBailout = (
|
||||
reason: string,
|
||||
|
@ -23,7 +25,7 @@ function formatErrorMessage(reason: string, opts?: BailoutOpts) {
|
|||
|
||||
export const staticGenerationBailout: StaticGenerationBailout = (
|
||||
reason,
|
||||
opts
|
||||
{ dynamic, link } = {}
|
||||
) => {
|
||||
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
|
||||
if (!staticGenerationStore) return false
|
||||
|
@ -34,12 +36,12 @@ export const staticGenerationBailout: StaticGenerationBailout = (
|
|||
|
||||
if (staticGenerationStore.dynamicShouldError) {
|
||||
throw new StaticGenBailoutError(
|
||||
formatErrorMessage(reason, { ...opts, dynamic: opts?.dynamic ?? 'error' })
|
||||
formatErrorMessage(reason, { link, dynamic: dynamic ?? 'error' })
|
||||
)
|
||||
}
|
||||
|
||||
const message = formatErrorMessage(reason, {
|
||||
...opts,
|
||||
dynamic,
|
||||
// this error should be caught by Next to bail out of static generation
|
||||
// in case it's uncaught, this link provides some additional context as to why
|
||||
link: 'https://nextjs.org/docs/messages/dynamic-server-error',
|
||||
|
@ -51,7 +53,7 @@ export const staticGenerationBailout: StaticGenerationBailout = (
|
|||
// to 0.
|
||||
staticGenerationStore.revalidate = 0
|
||||
|
||||
if (!opts?.dynamic) {
|
||||
if (!dynamic) {
|
||||
// we can statically prefetch pages that opt into dynamic,
|
||||
// but not things like headers/cookies
|
||||
staticGenerationStore.staticPrefetchBailout = true
|
||||
|
|
|
@ -25,6 +25,7 @@ import { lazyRenderAppPage } from '../../server/future/route-modules/app-page/mo
|
|||
export const enum ExportedAppPageFiles {
|
||||
HTML = 'HTML',
|
||||
FLIGHT = 'FLIGHT',
|
||||
PREFETCH_FLIGHT = 'PREFETCH_FLIGHT',
|
||||
META = 'META',
|
||||
POSTPONED = 'POSTPONED',
|
||||
}
|
||||
|
@ -128,10 +129,8 @@ export async function exportAppPage(
|
|||
|
||||
const html = result.toUnchunkedString()
|
||||
|
||||
const {
|
||||
metadata: { pageData, revalidate = false, postponed, fetchTags },
|
||||
} = result
|
||||
const { metadata } = result
|
||||
const { flightData, revalidate = false, postponed, fetchTags } = metadata
|
||||
|
||||
// Ensure we don't postpone without having PPR enabled.
|
||||
if (postponed && !renderOpts.experimental.ppr) {
|
||||
|
@ -169,6 +168,11 @@ export async function exportAppPage(
|
|||
|
||||
return { revalidate: 0 }
|
||||
}
|
||||
// If page data isn't available, it means that the page couldn't be rendered
|
||||
// properly.
|
||||
else if (!flightData) {
|
||||
throw new Error(`Invariant: failed to get page data for ${path}`)
|
||||
}
|
||||
// If PPR is enabled, we want to emit a prefetch rsc file for the page
|
||||
// instead of the standard rsc. This is because the standard rsc will
|
||||
// contain the dynamic data.
|
||||
|
@ -176,16 +180,16 @@ export async function exportAppPage(
|
|||
// If PPR is enabled, we should emit the flight data as the prefetch
|
||||
// payload.
|
||||
await fileWriter(
|
||||
ExportedAppPageFiles.FLIGHT,
|
||||
ExportedAppPageFiles.PREFETCH_FLIGHT,
|
||||
htmlFilepath.replace(/\.html$/, RSC_PREFETCH_SUFFIX),
|
||||
pageData
|
||||
flightData
|
||||
)
|
||||
} else {
|
||||
// Writing the RSC payload to a file if we don't have PPR enabled.
|
||||
await fileWriter(
|
||||
ExportedAppPageFiles.FLIGHT,
|
||||
htmlFilepath.replace(/\.html$/, RSC_SUFFIX),
|
||||
pageData
|
||||
flightData
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -210,7 +210,8 @@ async function createRedirectRenderResult(
|
|||
console.error(`failed to get redirect response`, err)
|
||||
}
|
||||
}
|
||||
return new RenderResult(JSON.stringify({}))
|
||||
|
||||
return RenderResult.fromStatic('{}')
|
||||
}
|
||||
|
||||
// Used to compare Host header and Origin header.
|
||||
|
@ -607,7 +608,7 @@ To configure the body size limit for Server Actions, see: https://nextjs.org/doc
|
|||
res.statusCode = 303
|
||||
return {
|
||||
type: 'done',
|
||||
result: new RenderResult(''),
|
||||
result: RenderResult.fromStatic(''),
|
||||
}
|
||||
} else if (isNotFoundError(err)) {
|
||||
res.statusCode = 404
|
||||
|
|
|
@ -15,6 +15,7 @@ import type { NextParsedUrlQuery } from '../request-meta'
|
|||
import type { LoaderTree } from '../lib/app-dir-module'
|
||||
import type { AppPageModule } from '../future/route-modules/app-page/module'
|
||||
import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
|
||||
import type { Revalidate } from '../lib/revalidate'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
|
@ -22,7 +23,11 @@ import {
|
|||
createServerComponentRenderer,
|
||||
type ServerComponentRendererOptions,
|
||||
} from './create-server-components-renderer'
|
||||
import RenderResult, { type RenderResultMetadata } from '../render-result'
|
||||
import RenderResult, {
|
||||
type AppPageRenderResultMetadata,
|
||||
type RenderResultOptions,
|
||||
type RenderResultResponse,
|
||||
} from '../render-result'
|
||||
import {
|
||||
renderToInitialFizzStream,
|
||||
continueFizzStream,
|
||||
|
@ -106,7 +111,7 @@ export type AppRenderContext = AppRenderBaseContext & {
|
|||
appUsingSizeAdjustment: boolean
|
||||
providedFlightRouterState?: FlightRouterState
|
||||
requestId: string
|
||||
defaultRevalidate: StaticGenerationStore['revalidate']
|
||||
defaultRevalidate: Revalidate
|
||||
pagePath: string
|
||||
clientReferenceManifest: ClientReferenceManifest
|
||||
assetPrefix: string
|
||||
|
@ -303,6 +308,34 @@ async function generateFlight(
|
|||
return new FlightRenderResult(flightReadableStream)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a resolver that eagerly generates a flight payload that is then
|
||||
* resolved when the resolver is called.
|
||||
*/
|
||||
function createFlightDataResolver(ctx: AppRenderContext) {
|
||||
// Generate the flight data and as soon as it can, convert it into a string.
|
||||
const promise = generateFlight(ctx)
|
||||
.then(async (result) => ({
|
||||
flightData: await result.toUnchunkedString(true),
|
||||
}))
|
||||
// Otherwise if it errored, return the error.
|
||||
.catch((err) => ({ err }))
|
||||
|
||||
return async () => {
|
||||
// Resolve the promise to get the flight data or error.
|
||||
const result = await promise
|
||||
|
||||
// If the flight data failed to render due to an error, re-throw the error
|
||||
// here.
|
||||
if ('err' in result) {
|
||||
throw result.err
|
||||
}
|
||||
|
||||
// Otherwise, return the flight data.
|
||||
return result.flightData
|
||||
}
|
||||
}
|
||||
|
||||
type ServerComponentsRendererOptions = {
|
||||
ctx: AppRenderContext
|
||||
preinitScripts: () => void
|
||||
|
@ -430,7 +463,7 @@ async function renderToHTMLOrFlightImpl(
|
|||
globalThis.__next_chunk_load__ = ComponentMod.__next_app__.loadChunk
|
||||
}
|
||||
|
||||
const extraRenderResultMeta: RenderResultMetadata = {}
|
||||
const metadata: AppPageRenderResultMetadata = {}
|
||||
|
||||
const appUsingSizeAdjustment = !!nextFontManifest?.appUsingSizeAdjust
|
||||
|
||||
|
@ -469,7 +502,7 @@ async function renderToHTMLOrFlightImpl(
|
|||
const allCapturedErrors: Error[] = []
|
||||
const isNextExport = !!renderOpts.nextExport
|
||||
const { staticGenerationStore, requestStore } = baseCtx
|
||||
const isStaticGeneration = staticGenerationStore.isStaticGeneration
|
||||
const { isStaticGeneration } = staticGenerationStore
|
||||
// when static generation fails during PPR, we log the errors separately. We intentionally
|
||||
// silence the error logger in this case to avoid double logging.
|
||||
const silenceStaticGenerationErrors =
|
||||
|
@ -537,7 +570,7 @@ async function renderToHTMLOrFlightImpl(
|
|||
const { urlPathname } = staticGenerationStore
|
||||
|
||||
staticGenerationStore.fetchMetrics = []
|
||||
extraRenderResultMeta.fetchMetrics = staticGenerationStore.fetchMetrics
|
||||
metadata.fetchMetrics = staticGenerationStore.fetchMetrics
|
||||
|
||||
// don't modify original query object
|
||||
query = { ...query }
|
||||
|
@ -615,11 +648,14 @@ async function renderToHTMLOrFlightImpl(
|
|||
|
||||
const hasPostponed = typeof renderOpts.postponed === 'string'
|
||||
|
||||
let stringifiedFlightPayloadPromise = isStaticGeneration
|
||||
? generateFlight(ctx)
|
||||
.then((renderResult) => renderResult.toUnchunkedString(true))
|
||||
.catch(() => null)
|
||||
: Promise.resolve(null)
|
||||
// Create the resolver that can get the flight payload when it's ready or
|
||||
// throw the error if it occurred. If we are not generating static HTML, we
|
||||
// don't need to generate the flight payload because it's a dynamic request
|
||||
// which means we're either getting the flight payload only or just the
|
||||
// regular HTML.
|
||||
const flightDataResolver = isStaticGeneration
|
||||
? createFlightDataResolver(ctx)
|
||||
: null
|
||||
|
||||
// Get the nonce from the incoming request if it has one.
|
||||
const csp = req.headers['content-security-policy']
|
||||
|
@ -754,8 +790,8 @@ async function renderToHTMLOrFlightImpl(
|
|||
// result.
|
||||
if (isStaticGeneration) {
|
||||
headers.forEach((value, key) => {
|
||||
extraRenderResultMeta.headers ??= {}
|
||||
extraRenderResultMeta.headers[key] = value
|
||||
metadata.headers ??= {}
|
||||
metadata.headers[key] = value
|
||||
})
|
||||
|
||||
// Resolve the promise to continue the stream.
|
||||
|
@ -779,7 +815,7 @@ async function renderToHTMLOrFlightImpl(
|
|||
// If the stream was postponed, we need to add the result to the
|
||||
// metadata so that it can be resumed later.
|
||||
if (postponed) {
|
||||
extraRenderResultMeta.postponed = JSON.stringify(postponed)
|
||||
metadata.postponed = JSON.stringify(postponed)
|
||||
|
||||
// We don't need to "continue" this stream now as it's continued when
|
||||
// we resume the stream.
|
||||
|
@ -789,8 +825,7 @@ async function renderToHTMLOrFlightImpl(
|
|||
const options: ContinueStreamOptions = {
|
||||
inlinedDataStream:
|
||||
serverComponentsRenderOpts.inlinedDataTransformStream.readable,
|
||||
generateStaticHTML:
|
||||
staticGenerationStore.isStaticGeneration || generateStaticHTML,
|
||||
isStaticGeneration: isStaticGeneration || generateStaticHTML,
|
||||
getServerInsertedHTML: () => getServerInsertedHTML(allCapturedErrors),
|
||||
serverInsertedHTMLToHead: !renderOpts.postponed,
|
||||
// If this render generated a postponed state or this is a resume
|
||||
|
@ -968,7 +1003,7 @@ async function renderToHTMLOrFlightImpl(
|
|||
inlinedDataStream:
|
||||
serverErrorComponentsRenderOpts.inlinedDataTransformStream
|
||||
.readable,
|
||||
generateStaticHTML: staticGenerationStore.isStaticGeneration,
|
||||
isStaticGeneration,
|
||||
getServerInsertedHTML: () => getServerInsertedHTML([]),
|
||||
serverInsertedHTMLToHead: true,
|
||||
validateRootLayout,
|
||||
|
@ -1012,11 +1047,11 @@ async function renderToHTMLOrFlightImpl(
|
|||
tree: notFoundLoaderTree,
|
||||
formState,
|
||||
}),
|
||||
{ ...extraRenderResultMeta }
|
||||
{ metadata }
|
||||
)
|
||||
} else if (actionRequestResult.type === 'done') {
|
||||
if (actionRequestResult.result) {
|
||||
actionRequestResult.result.extendMetadata(extraRenderResultMeta)
|
||||
actionRequestResult.result.assignMetadata(metadata)
|
||||
return actionRequestResult.result
|
||||
} else if (actionRequestResult.formState) {
|
||||
formState = actionRequestResult.formState
|
||||
|
@ -1024,114 +1059,130 @@ async function renderToHTMLOrFlightImpl(
|
|||
}
|
||||
}
|
||||
|
||||
const renderResult = new RenderResult(
|
||||
await renderToStream({
|
||||
asNotFound: isNotFoundPath,
|
||||
tree: loaderTree,
|
||||
formState,
|
||||
}),
|
||||
{
|
||||
...extraRenderResultMeta,
|
||||
// Wait for and collect the flight payload data if we don't have it
|
||||
// already.
|
||||
pageData: await stringifiedFlightPayloadPromise,
|
||||
// If we have pending revalidates, wait until they are all resolved.
|
||||
waitUntil: staticGenerationStore.pendingRevalidates
|
||||
? Promise.all(Object.values(staticGenerationStore.pendingRevalidates))
|
||||
: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
addImplicitTags(staticGenerationStore)
|
||||
extraRenderResultMeta.fetchTags = staticGenerationStore.tags?.join(',')
|
||||
renderResult.extendMetadata({
|
||||
fetchTags: extraRenderResultMeta.fetchTags,
|
||||
})
|
||||
|
||||
if (staticGenerationStore.isStaticGeneration) {
|
||||
// Collect the entire render result to a string (by streaming it to a
|
||||
// string).
|
||||
const htmlResult = await renderResult.toUnchunkedString(true)
|
||||
|
||||
// Timeout after 1.5 seconds for the headers to write. If it takes
|
||||
// longer than this it's more likely that the stream has stalled and
|
||||
// there is a React bug. The headers will then be updated in the render
|
||||
// result below when the metadata is re-added to the new render result.
|
||||
const onTimeout = new DetachedPromise<never>()
|
||||
const timeout = setTimeout(() => {
|
||||
onTimeout.reject(
|
||||
new Error(
|
||||
'Timeout waiting for headers to be emitted, this is a bug in Next.js'
|
||||
)
|
||||
)
|
||||
}, 1500)
|
||||
|
||||
// Race against the timeout and the headers being written.
|
||||
await Promise.race([onHeadersFinished.promise, onTimeout.promise])
|
||||
|
||||
// It got here, which means it did not reject, so clear the timeout to avoid
|
||||
// it from rejecting again (which is a no-op anyways).
|
||||
clearTimeout(timeout)
|
||||
|
||||
if (
|
||||
// if PPR is enabled
|
||||
renderOpts.experimental.ppr &&
|
||||
// and a call to `maybePostpone` happened
|
||||
staticGenerationStore.postponeWasTriggered &&
|
||||
// but there's no postpone state
|
||||
!extraRenderResultMeta.postponed
|
||||
) {
|
||||
// a call to postpone was made but was caught and not detected by Next.js. We should fail the build immediately
|
||||
// as we won't be able to generate the static part
|
||||
warn('')
|
||||
error(
|
||||
`Prerendering ${urlPathname} needs to partially bail out because something dynamic was used. ` +
|
||||
`React throws a special object to indicate where we need to bail out but it was caught ` +
|
||||
`by a try/catch or a Promise was not awaited. These special objects should not be caught ` +
|
||||
`by your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`
|
||||
)
|
||||
|
||||
if (capturedErrors.length > 0) {
|
||||
warn(
|
||||
'The following error was thrown during build, and may help identify the source of the issue:'
|
||||
)
|
||||
|
||||
error(capturedErrors[0])
|
||||
}
|
||||
|
||||
throw new MissingPostponeDataError(
|
||||
`An unexpected error occurred while prerendering ${urlPathname}. Please check the logs above for more details.`
|
||||
)
|
||||
}
|
||||
|
||||
// if we encountered any unexpected errors during build
|
||||
// we fail the prerendering phase and the build
|
||||
if (capturedErrors.length > 0) {
|
||||
throw capturedErrors[0]
|
||||
}
|
||||
|
||||
if (staticGenerationStore.forceStatic === false) {
|
||||
staticGenerationStore.revalidate = 0
|
||||
}
|
||||
|
||||
// TODO-APP: derive this from same pass to prevent additional
|
||||
// render during static generation
|
||||
extraRenderResultMeta.pageData = await stringifiedFlightPayloadPromise
|
||||
extraRenderResultMeta.revalidate =
|
||||
staticGenerationStore.revalidate ?? ctx.defaultRevalidate
|
||||
|
||||
// provide bailout info for debugging
|
||||
if (extraRenderResultMeta.revalidate === 0) {
|
||||
extraRenderResultMeta.staticBailoutInfo = {
|
||||
description: staticGenerationStore.dynamicUsageDescription,
|
||||
stack: staticGenerationStore.dynamicUsageStack,
|
||||
}
|
||||
}
|
||||
|
||||
return new RenderResult(htmlResult, { ...extraRenderResultMeta })
|
||||
const options: RenderResultOptions = {
|
||||
metadata,
|
||||
}
|
||||
|
||||
return renderResult
|
||||
let response: RenderResultResponse = await renderToStream({
|
||||
asNotFound: isNotFoundPath,
|
||||
tree: loaderTree,
|
||||
formState,
|
||||
})
|
||||
|
||||
// If we have pending revalidates, wait until they are all resolved.
|
||||
if (staticGenerationStore.pendingRevalidates) {
|
||||
options.waitUntil = Promise.all(
|
||||
Object.values(staticGenerationStore.pendingRevalidates)
|
||||
)
|
||||
}
|
||||
|
||||
addImplicitTags(staticGenerationStore)
|
||||
|
||||
if (staticGenerationStore.tags) {
|
||||
metadata.fetchTags = staticGenerationStore.tags.join(',')
|
||||
}
|
||||
|
||||
// Create the new render result for the response.
|
||||
const result = new RenderResult(response, options)
|
||||
|
||||
// If we aren't performing static generation, we can return the result now.
|
||||
if (!isStaticGeneration) {
|
||||
return result
|
||||
}
|
||||
|
||||
// If this is static generation, we should read this in now rather than
|
||||
// sending it back to be sent to the client.
|
||||
response = await result.toUnchunkedString(true)
|
||||
|
||||
// Timeout after 1.5 seconds for the headers to write. If it takes
|
||||
// longer than this it's more likely that the stream has stalled and
|
||||
// there is a React bug. The headers will then be updated in the render
|
||||
// result below when the metadata is re-added to the new render result.
|
||||
const onTimeout = new DetachedPromise<never>()
|
||||
const timeout = setTimeout(() => {
|
||||
onTimeout.reject(
|
||||
new Error(
|
||||
'Timeout waiting for headers to be emitted, this is a bug in Next.js'
|
||||
)
|
||||
)
|
||||
}, 1500)
|
||||
|
||||
// Race against the timeout and the headers being written.
|
||||
await Promise.race([onHeadersFinished.promise, onTimeout.promise])
|
||||
|
||||
// It got here, which means it did not reject, so clear the timeout to avoid
|
||||
// it from rejecting again (which is a no-op anyways).
|
||||
clearTimeout(timeout)
|
||||
|
||||
if (
|
||||
// if PPR is enabled
|
||||
renderOpts.experimental.ppr &&
|
||||
// and a call to `maybePostpone` happened
|
||||
staticGenerationStore.postponeWasTriggered &&
|
||||
// but there's no postpone state
|
||||
!metadata.postponed
|
||||
) {
|
||||
// a call to postpone was made but was caught and not detected by Next.js. We should fail the build immediately
|
||||
// as we won't be able to generate the static part
|
||||
warn('')
|
||||
error(
|
||||
`Prerendering ${urlPathname} needs to partially bail out because something dynamic was used. ` +
|
||||
`React throws a special object to indicate where we need to bail out but it was caught ` +
|
||||
`by a try/catch or a Promise was not awaited. These special objects should not be caught ` +
|
||||
`by your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`
|
||||
)
|
||||
|
||||
if (capturedErrors.length > 0) {
|
||||
warn(
|
||||
'The following error was thrown during build, and may help identify the source of the issue:'
|
||||
)
|
||||
|
||||
error(capturedErrors[0])
|
||||
}
|
||||
|
||||
throw new MissingPostponeDataError(
|
||||
`An unexpected error occurred while prerendering ${urlPathname}. Please check the logs above for more details.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!flightDataResolver) {
|
||||
throw new Error(
|
||||
'Invariant: Flight data resolver is missing when generating static HTML'
|
||||
)
|
||||
}
|
||||
|
||||
// If we encountered any unexpected errors during build we fail the
|
||||
// prerendering phase and the build.
|
||||
if (capturedErrors.length > 0) {
|
||||
throw capturedErrors[0]
|
||||
}
|
||||
|
||||
// Wait for and collect the flight payload data if we don't have it
|
||||
// already
|
||||
const flightData = await flightDataResolver()
|
||||
if (flightData) {
|
||||
metadata.flightData = flightData
|
||||
}
|
||||
|
||||
// If force static is specifically set to false, we should not revalidate
|
||||
// the page.
|
||||
if (staticGenerationStore.forceStatic === false) {
|
||||
staticGenerationStore.revalidate = 0
|
||||
}
|
||||
|
||||
// Copy the revalidation value onto the render result metadata.
|
||||
metadata.revalidate =
|
||||
staticGenerationStore.revalidate ?? ctx.defaultRevalidate
|
||||
|
||||
// provide bailout info for debugging
|
||||
if (metadata.revalidate === 0) {
|
||||
metadata.staticBailoutInfo = {
|
||||
description: staticGenerationStore.dynamicUsageDescription,
|
||||
stack: staticGenerationStore.dynamicUsageStack,
|
||||
}
|
||||
}
|
||||
|
||||
return new RenderResult(response, options)
|
||||
}
|
||||
|
||||
export type AppPageRender = (
|
||||
|
@ -1140,7 +1191,7 @@ export type AppPageRender = (
|
|||
pagePath: string,
|
||||
query: NextParsedUrlQuery,
|
||||
renderOpts: RenderOpts
|
||||
) => Promise<RenderResult>
|
||||
) => Promise<RenderResult<AppPageRenderResultMetadata>>
|
||||
|
||||
export const renderToHTMLOrFlight: AppPageRender = (
|
||||
req,
|
||||
|
|
|
@ -6,6 +6,6 @@ import RenderResult from '../render-result'
|
|||
*/
|
||||
export class FlightRenderResult extends RenderResult {
|
||||
constructor(response: string | ReadableStream<Uint8Array>) {
|
||||
super(response, { contentType: RSC_CONTENT_TYPE_HEADER })
|
||||
super(response, { contentType: RSC_CONTENT_TYPE_HEADER, metadata: {} })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2406,7 +2406,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
// should return.
|
||||
|
||||
// Handle `isNotFound`.
|
||||
if (metadata.isNotFound) {
|
||||
if ('isNotFound' in metadata && metadata.isNotFound) {
|
||||
return { value: null, revalidate: metadata.revalidate }
|
||||
}
|
||||
|
||||
|
@ -2415,7 +2415,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
return {
|
||||
value: {
|
||||
kind: 'REDIRECT',
|
||||
props: metadata.pageData,
|
||||
props: metadata.pageData ?? metadata.flightData,
|
||||
},
|
||||
revalidate: metadata.revalidate,
|
||||
}
|
||||
|
@ -2431,7 +2431,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
value: {
|
||||
kind: 'PAGE',
|
||||
html: result,
|
||||
pageData: metadata.pageData,
|
||||
pageData: metadata.pageData ?? metadata.flightData,
|
||||
postponed: metadata.postponed,
|
||||
headers,
|
||||
status: isAppPath ? res.statusCode : undefined,
|
||||
|
@ -3224,7 +3224,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
if (this.renderOpts.dev && ctx.pathname === '/favicon.ico') {
|
||||
return {
|
||||
type: 'html',
|
||||
body: new RenderResult(''),
|
||||
body: RenderResult.fromStatic(''),
|
||||
}
|
||||
}
|
||||
const { res, query } = ctx
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { OutgoingHttpHeaders, ServerResponse } from 'http'
|
||||
import type { StaticGenerationStore } from '../client/components/static-generation-async-storage.external'
|
||||
import type { Revalidate } from './lib/revalidate'
|
||||
import type { FetchMetrics } from './base-http'
|
||||
|
||||
import {
|
||||
chainStreams,
|
||||
|
@ -11,38 +11,58 @@ import { isAbortError, pipeToNodeResponse } from './pipe-readable'
|
|||
|
||||
type ContentTypeOption = string | undefined
|
||||
|
||||
export type RenderResultMetadata = {
|
||||
pageData?: any
|
||||
export type AppPageRenderResultMetadata = {
|
||||
flightData?: string
|
||||
revalidate?: Revalidate
|
||||
staticBailoutInfo?: {
|
||||
stack?: string
|
||||
description?: string
|
||||
}
|
||||
assetQueryString?: string
|
||||
isNotFound?: boolean
|
||||
isRedirect?: boolean
|
||||
fetchMetrics?: StaticGenerationStore['fetchMetrics']
|
||||
fetchTags?: string
|
||||
waitUntil?: Promise<any>
|
||||
|
||||
/**
|
||||
* The headers to set on the response that were added by the render.
|
||||
*/
|
||||
headers?: OutgoingHttpHeaders
|
||||
|
||||
/**
|
||||
* The postponed state if the render had postponed and needs to be resumed.
|
||||
*/
|
||||
postponed?: string
|
||||
|
||||
/**
|
||||
* The headers to set on the response that were added by the render.
|
||||
*/
|
||||
headers?: OutgoingHttpHeaders
|
||||
fetchTags?: string
|
||||
fetchMetrics?: FetchMetrics
|
||||
}
|
||||
|
||||
type RenderResultResponse =
|
||||
export type PagesRenderResultMetadata = {
|
||||
pageData?: any
|
||||
revalidate?: Revalidate
|
||||
assetQueryString?: string
|
||||
isNotFound?: boolean
|
||||
isRedirect?: boolean
|
||||
}
|
||||
|
||||
export type StaticRenderResultMetadata = {}
|
||||
|
||||
export type RenderResultMetadata = AppPageRenderResultMetadata &
|
||||
PagesRenderResultMetadata &
|
||||
StaticRenderResultMetadata
|
||||
|
||||
export type RenderResultResponse =
|
||||
| ReadableStream<Uint8Array>[]
|
||||
| ReadableStream<Uint8Array>
|
||||
| string
|
||||
| null
|
||||
|
||||
export default class RenderResult {
|
||||
export type RenderResultOptions<
|
||||
Metadata extends RenderResultMetadata = RenderResultMetadata
|
||||
> = {
|
||||
contentType?: ContentTypeOption
|
||||
waitUntil?: Promise<unknown>
|
||||
metadata: Metadata
|
||||
}
|
||||
|
||||
export default class RenderResult<
|
||||
Metadata extends RenderResultMetadata = RenderResultMetadata
|
||||
> {
|
||||
/**
|
||||
* The detected content type for the response. This is used to set the
|
||||
* `Content-Type` header.
|
||||
|
@ -53,7 +73,7 @@ export default class RenderResult {
|
|||
* The metadata for the response. This is used to set the revalidation times
|
||||
* and other metadata.
|
||||
*/
|
||||
public readonly metadata: RenderResultMetadata
|
||||
public readonly metadata: Readonly<Metadata>
|
||||
|
||||
/**
|
||||
* The response itself. This can be a string, a stream, or null. If it's a
|
||||
|
@ -69,21 +89,15 @@ export default class RenderResult {
|
|||
* @param value the static response value
|
||||
* @returns a new RenderResult instance
|
||||
*/
|
||||
public static fromStatic(value: string): RenderResult {
|
||||
return new RenderResult(value)
|
||||
public static fromStatic(value: string) {
|
||||
return new RenderResult<StaticRenderResultMetadata>(value, { metadata: {} })
|
||||
}
|
||||
|
||||
private waitUntil?: Promise<void>
|
||||
private readonly waitUntil?: Promise<unknown>
|
||||
|
||||
constructor(
|
||||
response: RenderResultResponse,
|
||||
{
|
||||
contentType,
|
||||
waitUntil,
|
||||
...metadata
|
||||
}: {
|
||||
contentType?: ContentTypeOption
|
||||
} & RenderResultMetadata = {}
|
||||
{ contentType, waitUntil, metadata }: RenderResultOptions<Metadata>
|
||||
) {
|
||||
this.response = response
|
||||
this.contentType = contentType
|
||||
|
@ -91,7 +105,7 @@ export default class RenderResult {
|
|||
this.waitUntil = waitUntil
|
||||
}
|
||||
|
||||
public extendMetadata(metadata: RenderResultMetadata) {
|
||||
public assignMetadata(metadata: Metadata) {
|
||||
Object.assign(this.metadata, metadata)
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
|
|||
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
|
||||
import { getRequestMeta } from './request-meta'
|
||||
import { allowedStatusCodes, getRedirectStatus } from '../lib/redirect-status'
|
||||
import RenderResult, { type RenderResultMetadata } from './render-result'
|
||||
import RenderResult, { type PagesRenderResultMetadata } from './render-result'
|
||||
import isError from '../lib/is-error'
|
||||
import {
|
||||
streamFromString,
|
||||
|
@ -414,20 +414,20 @@ export async function renderToHTMLImpl(
|
|||
// Adds support for reading `cookies` in `getServerSideProps` when SSR.
|
||||
setLazyProp({ req: req as any }, 'cookies', getCookieParser(req.headers))
|
||||
|
||||
const renderResultMeta: RenderResultMetadata = {}
|
||||
const metadata: PagesRenderResultMetadata = {}
|
||||
|
||||
// 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.
|
||||
renderResultMeta.assetQueryString = renderOpts.dev
|
||||
metadata.assetQueryString = renderOpts.dev
|
||||
? renderOpts.assetQueryString || `?ts=${Date.now()}`
|
||||
: ''
|
||||
|
||||
// if deploymentId is provided we append it to all asset requests
|
||||
if (renderOpts.deploymentId) {
|
||||
renderResultMeta.assetQueryString += `${
|
||||
renderResultMeta.assetQueryString ? '&' : '?'
|
||||
}dpl=${renderOpts.deploymentId}`
|
||||
metadata.assetQueryString += `${metadata.assetQueryString ? '&' : '?'}dpl=${
|
||||
renderOpts.deploymentId
|
||||
}`
|
||||
}
|
||||
|
||||
// don't modify original query object
|
||||
|
@ -454,7 +454,7 @@ export async function renderToHTMLImpl(
|
|||
} = renderOpts
|
||||
const { App } = extra
|
||||
|
||||
const assetQueryString = renderResultMeta.assetQueryString
|
||||
const assetQueryString = metadata.assetQueryString
|
||||
|
||||
let Document = extra.Document
|
||||
|
||||
|
@ -892,7 +892,7 @@ export async function renderToHTMLImpl(
|
|||
)
|
||||
}
|
||||
|
||||
renderResultMeta.isNotFound = true
|
||||
metadata.isNotFound = true
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -916,12 +916,12 @@ export async function renderToHTMLImpl(
|
|||
if (typeof data.redirect.basePath !== 'undefined') {
|
||||
;(data as any).props.__N_REDIRECT_BASE_PATH = data.redirect.basePath
|
||||
}
|
||||
renderResultMeta.isRedirect = true
|
||||
metadata.isRedirect = true
|
||||
}
|
||||
|
||||
if (
|
||||
(dev || isBuildTimeSSG) &&
|
||||
!renderResultMeta.isNotFound &&
|
||||
!metadata.isNotFound &&
|
||||
!isSerializableProps(pathname, 'getStaticProps', (data as any).props)
|
||||
) {
|
||||
// this fn should throw an error instead of ever returning `false`
|
||||
|
@ -992,12 +992,12 @@ export async function renderToHTMLImpl(
|
|||
)
|
||||
|
||||
// pass up revalidate and props for export
|
||||
renderResultMeta.revalidate = revalidate
|
||||
renderResultMeta.pageData = props
|
||||
metadata.revalidate = revalidate
|
||||
metadata.pageData = props
|
||||
|
||||
// this must come after revalidate is added to renderResultMeta
|
||||
if (renderResultMeta.isNotFound) {
|
||||
return new RenderResult(null, renderResultMeta)
|
||||
if (metadata.isNotFound) {
|
||||
return new RenderResult(null, { metadata })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1110,8 +1110,8 @@ export async function renderToHTMLImpl(
|
|||
)
|
||||
}
|
||||
|
||||
renderResultMeta.isNotFound = true
|
||||
return new RenderResult(null, renderResultMeta)
|
||||
metadata.isNotFound = true
|
||||
return new RenderResult(null, { metadata })
|
||||
}
|
||||
|
||||
if ('redirect' in data && typeof data.redirect === 'object') {
|
||||
|
@ -1123,7 +1123,7 @@ export async function renderToHTMLImpl(
|
|||
if (typeof data.redirect.basePath !== 'undefined') {
|
||||
;(data as any).props.__N_REDIRECT_BASE_PATH = data.redirect.basePath
|
||||
}
|
||||
renderResultMeta.isRedirect = true
|
||||
metadata.isRedirect = true
|
||||
}
|
||||
|
||||
if (deferredContent) {
|
||||
|
@ -1141,7 +1141,7 @@ export async function renderToHTMLImpl(
|
|||
}
|
||||
|
||||
props.pageProps = Object.assign({}, props.pageProps, (data as any).props)
|
||||
renderResultMeta.pageData = props
|
||||
metadata.pageData = props
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -1158,8 +1158,10 @@ export async function renderToHTMLImpl(
|
|||
|
||||
// Avoid rendering page un-necessarily for getServerSideProps data request
|
||||
// and getServerSideProps/getStaticProps redirects
|
||||
if ((isDataReq && !isSSG) || renderResultMeta.isRedirect) {
|
||||
return new RenderResult(JSON.stringify(props), renderResultMeta)
|
||||
if ((isDataReq && !isSSG) || metadata.isRedirect) {
|
||||
return new RenderResult(JSON.stringify(props), {
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
// We don't call getStaticProps or getServerSideProps while generating
|
||||
|
@ -1169,7 +1171,7 @@ export async function renderToHTMLImpl(
|
|||
}
|
||||
|
||||
// the response might be finished on the getInitialProps call
|
||||
if (isResSent(res) && !isSSG) return new RenderResult(null, renderResultMeta)
|
||||
if (isResSent(res) && !isSSG) return new RenderResult(null, { metadata })
|
||||
|
||||
// we preload the buildManifest for auto-export dynamic pages
|
||||
// to speed up hydrating query values
|
||||
|
@ -1333,7 +1335,7 @@ export async function renderToHTMLImpl(
|
|||
return continueFizzStream(initialStream, {
|
||||
suffix,
|
||||
inlinedDataStream: serverComponentsInlinedTransformStream?.readable,
|
||||
generateStaticHTML: true,
|
||||
isStaticGeneration: true,
|
||||
// this must be called inside bodyResult so appWrappers is
|
||||
// up to date when `wrapApp` is called
|
||||
getServerInsertedHTML: () => {
|
||||
|
@ -1408,7 +1410,7 @@ export async function renderToHTMLImpl(
|
|||
async () => renderDocument()
|
||||
)
|
||||
if (!documentResult) {
|
||||
return new RenderResult(null, renderResultMeta)
|
||||
return new RenderResult(null, { metadata })
|
||||
}
|
||||
|
||||
const dynamicImportsIds = new Set<string | number>()
|
||||
|
@ -1567,7 +1569,7 @@ export async function renderToHTMLImpl(
|
|||
hybridAmp,
|
||||
})
|
||||
|
||||
return new RenderResult(optimizedHtml, renderResultMeta)
|
||||
return new RenderResult(optimizedHtml, { metadata })
|
||||
}
|
||||
|
||||
export type PagesRender = (
|
||||
|
|
|
@ -442,7 +442,7 @@ function chainTransformers<T>(
|
|||
|
||||
export type ContinueStreamOptions = {
|
||||
inlinedDataStream: ReadableStream<Uint8Array> | undefined
|
||||
generateStaticHTML: boolean
|
||||
isStaticGeneration: boolean
|
||||
getServerInsertedHTML: (() => Promise<string>) | undefined
|
||||
serverInsertedHTMLToHead: boolean
|
||||
validateRootLayout:
|
||||
|
@ -462,7 +462,7 @@ export async function continueFizzStream(
|
|||
{
|
||||
suffix,
|
||||
inlinedDataStream,
|
||||
generateStaticHTML,
|
||||
isStaticGeneration,
|
||||
getServerInsertedHTML,
|
||||
serverInsertedHTMLToHead,
|
||||
validateRootLayout,
|
||||
|
@ -475,7 +475,7 @@ export async function continueFizzStream(
|
|||
|
||||
// If we're generating static HTML and there's an `allReady` promise on the
|
||||
// stream, we need to wait for it to resolve before continuing.
|
||||
if (generateStaticHTML && 'allReady' in renderStream) {
|
||||
if (isStaticGeneration && 'allReady' in renderStream) {
|
||||
await renderStream.allReady
|
||||
}
|
||||
|
||||
|
@ -518,7 +518,7 @@ export async function continueFizzStream(
|
|||
type ContinuePostponedStreamOptions = Pick<
|
||||
ContinueStreamOptions,
|
||||
| 'inlinedDataStream'
|
||||
| 'generateStaticHTML'
|
||||
| 'isStaticGeneration'
|
||||
| 'getServerInsertedHTML'
|
||||
| 'serverInsertedHTMLToHead'
|
||||
>
|
||||
|
@ -527,7 +527,7 @@ export async function continuePostponedFizzStream(
|
|||
renderStream: ReactReadableStream,
|
||||
{
|
||||
inlinedDataStream,
|
||||
generateStaticHTML,
|
||||
isStaticGeneration,
|
||||
getServerInsertedHTML,
|
||||
serverInsertedHTMLToHead,
|
||||
}: ContinuePostponedStreamOptions
|
||||
|
@ -536,7 +536,7 @@ export async function continuePostponedFizzStream(
|
|||
|
||||
// If we're generating static HTML and there's an `allReady` promise on the
|
||||
// stream, we need to wait for it to resolve before continuing.
|
||||
if (generateStaticHTML && 'allReady' in renderStream) {
|
||||
if (isStaticGeneration && 'allReady' in renderStream) {
|
||||
await renderStream.allReady
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue