rsnext/packages/next/server/app-render.tsx
Josh Story f012eab00f
use a function expression to access arguments binding (#43987)
arrow function was trying to use arguments binding but wanted arguments
of the lambda not the nearest normal function. The narrowest change is
to convert to function expression. We could also use varargs rest syntax
since this runs in runtimes that should all support that I think. I
don't think it really matters though.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
2022-12-12 14:39:12 -08:00

1954 lines
61 KiB
TypeScript

import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'http'
import type { LoadComponentsReturnType } from './load-components'
import type { ServerRuntime } from '../types'
import type { FontLoaderManifest } from '../build/webpack/plugins/font-loader-manifest-plugin'
// Import builtin react directly to avoid require cache conflicts
import React, { use } from 'next/dist/compiled/react'
import { NotFound as DefaultNotFound } from '../client/components/error'
// this needs to be required lazily so that `next-server` can set
// the env before we require
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 {
readableStreamTee,
encodeText,
decodeText,
renderToInitialStream,
createBufferedTransformStream,
continueFromInitialStream,
streamToString,
} from './node-web-streams-helper'
import { ESCAPE_REGEX, htmlEscapeJsonString } from './htmlescape'
import { matchSegment } from '../client/components/match-segments'
import {
FlightCSSManifest,
FlightManifest,
} from '../build/webpack/plugins/flight-manifest-plugin'
import { ServerInsertedHTMLContext } from '../shared/lib/server-inserted-html'
import { stripInternalQueries } from './internal-utils'
import type { ComponentsType } from '../build/webpack/loaders/next-app-loader'
import { REDIRECT_ERROR_CODE } from '../client/components/redirect'
import { RequestCookies } from './web/spec-extension/cookies'
import { DYNAMIC_ERROR_CODE } from '../client/components/hooks-server-context'
import { NOT_FOUND_ERROR_CODE } from '../client/components/not-found'
import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/no-ssr-error'
import { HeadManagerContext } from '../shared/lib/head-manager-context'
import { Writable } from 'stream'
import stringHash from 'next/dist/compiled/string-hash'
import {
NEXT_ROUTER_PREFETCH,
NEXT_ROUTER_STATE_TREE,
RSC,
FLIGHT_PARAMETERS,
} from '../client/components/app-router-headers'
import type { StaticGenerationStore } from '../client/components/static-generation-async-storage'
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'
function preloadComponent(Component: any, props: any) {
const prev = console.error
// Hide invalid hook call warning when calling component
console.error = function (msg) {
if (msg.startsWith('Invalid hook call..')) {
// ignore
} else {
// @ts-expect-error argument is defined
prev.apply(console, arguments)
}
}
try {
let result = Component(props)
if (result && typeof result.then === 'function') {
// Catch promise rejections to prevent unhandledRejection errors
result.then(
() => {},
() => {}
)
}
return function () {
// We know what this component will render already.
return result
}
} catch (x) {
// something suspended or errored, try again later
} finally {
console.error = prev
}
return Component
}
const CACHE_ONE_YEAR = 31536000
const INTERNAL_HEADERS_INSTANCE = Symbol('internal for headers readonly')
function readonlyHeadersError() {
return new Error('ReadonlyHeaders cannot be modified')
}
export class ReadonlyHeaders {
[INTERNAL_HEADERS_INSTANCE]: Headers
entries: Headers['entries']
forEach: Headers['forEach']
get: Headers['get']
has: Headers['has']
keys: Headers['keys']
values: Headers['values']
constructor(headers: IncomingHttpHeaders) {
// Since `new Headers` uses `this.append()` to fill the headers object ReadonlyHeaders can't extend from Headers directly as it would throw.
const headersInstance = new Headers(headers as any)
this[INTERNAL_HEADERS_INSTANCE] = headersInstance
this.entries = headersInstance.entries.bind(headersInstance)
this.forEach = headersInstance.forEach.bind(headersInstance)
this.get = headersInstance.get.bind(headersInstance)
this.has = headersInstance.has.bind(headersInstance)
this.keys = headersInstance.keys.bind(headersInstance)
this.values = headersInstance.values.bind(headersInstance)
}
[Symbol.iterator]() {
return this[INTERNAL_HEADERS_INSTANCE][Symbol.iterator]()
}
append() {
throw readonlyHeadersError()
}
delete() {
throw readonlyHeadersError()
}
set() {
throw readonlyHeadersError()
}
}
const INTERNAL_COOKIES_INSTANCE = Symbol('internal for cookies readonly')
class ReadonlyRequestCookiesError extends Error {
message =
'ReadonlyRequestCookies cannot be modified. Read more: https://nextjs.org/api-reference/cookies'
}
export class ReadonlyRequestCookies {
[INTERNAL_COOKIES_INSTANCE]: RequestCookies
get: RequestCookies['get']
getAll: RequestCookies['getAll']
has: RequestCookies['has']
constructor(request: {
headers: {
get(key: 'cookie'): string | null | undefined
}
}) {
// Since `new Headers` uses `this.append()` to fill the headers object ReadonlyHeaders can't extend from Headers directly as it would throw.
// Request overridden to not have to provide a fully request object.
const cookiesInstance = new RequestCookies(request.headers as Headers)
this[INTERNAL_COOKIES_INSTANCE] = cookiesInstance
this.get = cookiesInstance.get.bind(cookiesInstance)
this.getAll = cookiesInstance.getAll.bind(cookiesInstance)
this.has = cookiesInstance.has.bind(cookiesInstance)
}
[Symbol.iterator]() {
return (this[INTERNAL_COOKIES_INSTANCE] as any)[Symbol.iterator]()
}
clear() {
throw new ReadonlyRequestCookiesError()
}
delete() {
throw new ReadonlyRequestCookiesError()
}
set() {
throw new ReadonlyRequestCookiesError()
}
}
export type RenderOptsPartial = {
err?: Error | null
dev?: boolean
serverComponentManifest?: FlightManifest
serverCSSManifest?: FlightCSSManifest
supportsDynamicHTML?: boolean
runtime?: ServerRuntime
serverComponents?: boolean
assetPrefix?: string
fontLoaderManifest?: FontLoaderManifest
isBot?: boolean
incrementalCache?: import('./lib/incremental-cache').IncrementalCache
isRevalidate?: boolean
}
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
/**
* Flight Response is always set to application/octet-stream to ensure it does not get interpreted as HTML.
*/
class FlightRenderResult extends RenderResult {
constructor(response: string | ReadableStream<Uint8Array>) {
super(response, { contentType: 'application/octet-stream' })
}
}
/**
* Interop between "export default" and "module.exports".
*/
function interopDefault(mod: any) {
return mod.default || mod
}
// tolerate dynamic server errors during prerendering so console
// isn't spammed with unactionable errors
/**
* Create error handler for renderers.
*/
function createErrorHandler(
/**
* Used for debugging
*/
_source: string,
capturedErrors: Error[],
allCapturedErrors?: Error[]
) {
return (err: any): string => {
if (allCapturedErrors) allCapturedErrors.push(err)
if (
err &&
(err.digest === DYNAMIC_ERROR_CODE ||
err.digest === NOT_FOUND_ERROR_CODE ||
err.digest === NEXT_DYNAMIC_NO_SSR_CODE ||
err.digest?.startsWith(REDIRECT_ERROR_CODE))
) {
return err.digest
}
// Used for debugging error source
// console.error(_source, err)
console.error(err)
capturedErrors.push(err)
// TODO-APP: look at using webcrypto instead. Requires a promise to be awaited.
return stringHash(err.message + err.stack + (err.digest || '')).toString()
}
}
let isFetchPatched = false
// we patch fetch to collect cache information used for
// determining if a page is static or not
function patchFetch(ComponentMod: any) {
if (isFetchPatched) return
isFetchPatched = true
const { DynamicServerError } =
ComponentMod.serverHooks as typeof import('../client/components/hooks-server-context')
const staticGenerationAsyncStorage = ComponentMod.staticGenerationAsyncStorage
const originFetch = globalThis.fetch
globalThis.fetch = async (input, init) => {
const staticGenerationStore =
('getStore' in staticGenerationAsyncStorage
? staticGenerationAsyncStorage.getStore()
: staticGenerationAsyncStorage) || {}
const {
isStaticGeneration,
fetchRevalidate,
pathname,
incrementalCache,
isRevalidate,
} = (staticGenerationStore || {}) as StaticGenerationStore
let revalidate: number | undefined | boolean
if (typeof init?.next?.revalidate === 'number') {
revalidate = init.next.revalidate
}
if (init?.next?.revalidate === false) {
revalidate = CACHE_ONE_YEAR
}
if (
!staticGenerationStore.fetchRevalidate ||
(typeof revalidate === 'number' &&
revalidate < staticGenerationStore.fetchRevalidate)
) {
staticGenerationStore.fetchRevalidate = revalidate
}
let cacheKey: string | undefined
const doOriginalFetch = async () => {
return originFetch(input, init).then(async (res) => {
if (
incrementalCache &&
cacheKey &&
typeof revalidate === 'number' &&
revalidate > 0
) {
const clonedRes = res.clone()
let base64Body = ''
if (process.env.NEXT_RUNTIME === 'edge') {
let string = ''
new Uint8Array(await clonedRes.arrayBuffer()).forEach((byte) => {
string += String.fromCharCode(byte)
})
base64Body = btoa(string)
} else {
base64Body = Buffer.from(await clonedRes.arrayBuffer()).toString(
'base64'
)
}
await incrementalCache.set(
cacheKey,
{
kind: 'FETCH',
isStale: false,
age: 0,
data: {
headers: Object.fromEntries(clonedRes.headers.entries()),
body: base64Body,
},
revalidate,
},
revalidate,
true
)
}
return res
})
}
if (incrementalCache && typeof revalidate === 'number' && revalidate > 0) {
cacheKey = await incrementalCache?.fetchCacheKey(input.toString(), init)
const entry = await incrementalCache.get(cacheKey, true)
if (entry?.value && entry.value.kind === 'FETCH') {
// when stale and is revalidating we wait for fresh data
// so the revalidated entry has the updated data
if (!isRevalidate || !entry.isStale) {
if (entry.isStale) {
if (!staticGenerationStore.pendingRevalidates) {
staticGenerationStore.pendingRevalidates = []
}
staticGenerationStore.pendingRevalidates.push(
doOriginalFetch().catch(console.error)
)
}
const resData = entry.value.data
let decodedBody = ''
// TODO: handle non-text response bodies
if (process.env.NEXT_RUNTIME === 'edge') {
decodedBody = atob(resData.body)
} else {
decodedBody = Buffer.from(resData.body, 'base64').toString()
}
return new Response(decodedBody, {
headers: resData.headers,
status: resData.status,
})
}
}
}
if (staticGenerationStore && isStaticGeneration) {
if (init && typeof init === 'object') {
const cache = init.cache
// Delete `cache` property as Cloudflare Workers will throw an error
if (isEdgeRuntime) {
delete init.cache
}
if (cache === 'no-store') {
staticGenerationStore.fetchRevalidate = 0
// TODO: ensure this error isn't logged to the user
// seems it's slipping through currently
throw new DynamicServerError(
`no-store fetch ${input}${pathname ? ` ${pathname}` : ''}`
)
}
const hasNextConfig = 'next' in init
const next = init.next || {}
if (
typeof next.revalidate === 'number' &&
(typeof fetchRevalidate === 'undefined' ||
next.revalidate < fetchRevalidate)
) {
const forceDynamic = staticGenerationStore.forceDynamic
if (!forceDynamic || next.revalidate !== 0) {
staticGenerationStore.fetchRevalidate = next.revalidate
}
if (!forceDynamic && next.revalidate === 0) {
throw new DynamicServerError(
`revalidate: ${next.revalidate} fetch ${input}${
pathname ? ` ${pathname}` : ''
}`
)
}
}
if (hasNextConfig) delete init.next
}
}
return doOriginalFetch()
}
}
interface FlightResponseRef {
current: Promise<JSX.Element> | null
}
/**
* Render Flight stream.
* This is only used for renderToHTML, the Flight response does not need additional wrappers.
*/
function useFlightResponse(
writable: WritableStream<Uint8Array>,
req: ReadableStream<Uint8Array>,
serverComponentManifest: any,
rscChunks: Uint8Array[],
flightResponseRef: FlightResponseRef,
nonce?: string
): Promise<JSX.Element> {
if (flightResponseRef.current !== null) {
return flightResponseRef.current
}
const {
createFromReadableStream,
} = require('next/dist/compiled/react-server-dom-webpack/client')
const [renderStream, forwardStream] = readableStreamTee(req)
const res = createFromReadableStream(renderStream, {
moduleMap: isEdgeRuntime
? serverComponentManifest.__edge_ssr_module_mapping__
: serverComponentManifest.__ssr_module_mapping__,
})
flightResponseRef.current = res
let bootstrapped = false
// We only attach CSS chunks to the inlined data.
const forwardReader = forwardStream.getReader()
const writer = writable.getWriter()
const startScriptTag = nonce
? `<script nonce=${JSON.stringify(nonce)}>`
: '<script>'
function read() {
forwardReader.read().then(({ done, value }) => {
if (value) {
rscChunks.push(value)
}
if (!bootstrapped) {
bootstrapped = true
writer.write(
encodeText(
`${startScriptTag}(self.__next_f=self.__next_f||[]).push(${htmlEscapeJsonString(
JSON.stringify([0])
)})</script>`
)
)
}
if (done) {
flightResponseRef.current = null
writer.close()
} else {
const responsePartial = decodeText(value)
const scripts = `${startScriptTag}self.__next_f.push(${htmlEscapeJsonString(
JSON.stringify([1, responsePartial])
)})</script>`
writer.write(encodeText(scripts))
read()
}
})
}
read()
return res
}
/**
* Create a component that renders the Flight stream.
* This is only used for renderToHTML, the Flight response does not need additional wrappers.
*/
function createServerComponentRenderer(
ComponentToRender: any,
ComponentMod: {
renderToReadableStream: any
__next_app_webpack_require__?: any
},
{
transformStream,
serverComponentManifest,
serverContexts,
rscChunks,
}: {
transformStream: TransformStream<Uint8Array, Uint8Array>
serverComponentManifest: NonNullable<RenderOpts['serverComponentManifest']>
serverContexts: Array<
[ServerContextName: string, JSONValue: Object | number | string]
>
rscChunks: Uint8Array[]
},
serverComponentsErrorHandler: ReturnType<typeof createErrorHandler>,
nonce?: string
): () => JSX.Element {
// We need to expose the `__webpack_require__` API globally for
// react-server-dom-webpack. This is a hack until we find a better way.
if (ComponentMod.__next_app_webpack_require__) {
// @ts-ignore
globalThis.__next_require__ = ComponentMod.__next_app_webpack_require__
// @ts-ignore
globalThis.__next_chunk_load__ = () => Promise.resolve()
}
let RSCStream: ReadableStream<Uint8Array>
const createRSCStream = () => {
if (!RSCStream) {
RSCStream = ComponentMod.renderToReadableStream(
<ComponentToRender />,
serverComponentManifest,
{
context: serverContexts,
onError: serverComponentsErrorHandler,
}
)
}
return RSCStream
}
const flightResponseRef: FlightResponseRef = { current: null }
const writable = transformStream.writable
return function ServerComponentWrapper(): JSX.Element {
const reqStream = createRSCStream()
const response = useFlightResponse(
writable,
reqStream,
serverComponentManifest,
rscChunks,
flightResponseRef,
nonce
)
return use(response)
}
}
type DynamicParamTypes = 'catchall' | 'optional-catchall' | 'dynamic'
// c = catchall
// oc = optional catchall
// d = dynamic
export type DynamicParamTypesShort = 'c' | 'oc' | 'd'
/**
* Shorten the dynamic param in order to make it smaller when transmitted to the browser.
*/
function getShortDynamicParamType(
type: DynamicParamTypes
): DynamicParamTypesShort {
switch (type) {
case 'catchall':
return 'c'
case 'optional-catchall':
return 'oc'
case 'dynamic':
return 'd'
default:
throw new Error('Unknown dynamic param type')
}
}
/**
* Segment in the router state.
*/
export type Segment =
| string
| [param: string, value: string, type: DynamicParamTypesShort]
/**
* LoaderTree is generated in next-app-loader.
*/
type LoaderTree = [
segment: string,
parallelRoutes: { [parallelRouterKey: string]: LoaderTree },
components: ComponentsType
]
/**
* Router state
*/
export type FlightRouterState = [
segment: Segment,
parallelRoutes: { [parallelRouterKey: string]: FlightRouterState },
url?: string,
refresh?: 'refetch',
isRootLayout?: boolean
]
/**
* Individual Flight response path
*/
export type FlightSegmentPath =
// Uses `any` as repeating pattern can't be typed.
| any[]
// Looks somewhat like this
| [
segment: Segment,
parallelRouterKey: string,
segment: Segment,
parallelRouterKey: string,
segment: Segment,
parallelRouterKey: string
]
export type FlightDataPath =
// Uses `any` as repeating pattern can't be typed.
| any[]
// Looks somewhat like this
| [
// Holds full path to the segment.
...FlightSegmentPath[],
/* segment of the rendered slice: */ Segment,
/* treePatch */ FlightRouterState,
/* subTreeData: */ React.ReactNode | null, // Can be null during prefetch if there's no loading component
/* head */ React.ReactNode | null
]
/**
* The Flight response data
*/
export type FlightData = Array<FlightDataPath> | string
/**
* Property holding the current subTreeData.
*/
export type ChildProp = {
/**
* Null indicates that the tree is partial
*/
current: React.ReactNode | null
segment: Segment
}
/**
* Parse dynamic route segment to type of parameter
*/
function getSegmentParam(segment: string): {
param: string
type: DynamicParamTypes
} | null {
if (segment.startsWith('[[...') && segment.endsWith(']]')) {
return {
type: 'optional-catchall',
param: segment.slice(5, -2),
}
}
if (segment.startsWith('[...') && segment.endsWith(']')) {
return {
type: 'catchall',
param: segment.slice(4, -1),
}
}
if (segment.startsWith('[') && segment.endsWith(']')) {
return {
type: 'dynamic',
param: segment.slice(1, -1),
}
}
return null
}
/**
* Get inline <link> tags based on server CSS manifest. Only used when rendering to HTML.
*/
function getCssInlinedLinkTags(
serverComponentManifest: FlightManifest,
serverCSSManifest: FlightCSSManifest,
filePath: string,
serverCSSForEntries: string[]
): string[] {
const layoutOrPageCss =
serverCSSManifest[filePath] ||
serverComponentManifest.__client_css_manifest__?.[filePath]
if (!layoutOrPageCss) {
return []
}
const chunks = new Set<string>()
for (const css of layoutOrPageCss) {
// We only include the CSS if it's a global CSS, or it is used by this
// entrypoint.
if (serverCSSForEntries.includes(css) || !/\.module\.css/.test(css)) {
const mod = serverComponentManifest[css]
if (mod) {
for (const chunk of mod.default.chunks) {
chunks.add(chunk)
}
}
}
}
return [...chunks]
}
function getServerCSSForEntries(
serverCSSManifest: FlightCSSManifest,
entries: string[]
) {
const css = []
for (const entry of entries) {
const entryName = entry.replace(/\.[^.]+$/, '')
if (
serverCSSManifest.__entry_css__ &&
serverCSSManifest.__entry_css__[entryName]
) {
css.push(...serverCSSManifest.__entry_css__[entryName])
}
}
return css
}
/**
* Get inline <link rel="preload" as="font"> tags based on server CSS manifest and font loader manifest. Only used when rendering to HTML.
*/
function getPreloadedFontFilesInlineLinkTags(
serverComponentManifest: FlightManifest,
serverCSSManifest: FlightCSSManifest,
fontLoaderManifest: FontLoaderManifest | undefined,
serverCSSForEntries: string[],
filePath?: string
): string[] {
if (!fontLoaderManifest || !filePath) {
return []
}
const layoutOrPageCss =
serverCSSManifest[filePath] ||
serverComponentManifest.__client_css_manifest__?.[filePath]
if (!layoutOrPageCss) {
return []
}
const fontFiles = new Set<string>()
for (const css of layoutOrPageCss) {
// We only include the CSS if it is used by this entrypoint.
if (serverCSSForEntries.includes(css)) {
const preloadedFontFiles = fontLoaderManifest.app[css]
if (preloadedFontFiles) {
for (const fontFile of preloadedFontFiles) {
fontFiles.add(fontFile)
}
}
}
}
return [...fontFiles]
}
function getScriptNonceFromHeader(cspHeaderValue: string): string | undefined {
const directives = cspHeaderValue
// Directives are split by ';'.
.split(';')
.map((directive) => directive.trim())
// First try to find the directive for the 'script-src', otherwise try to
// fallback to the 'default-src'.
const directive =
directives.find((dir) => dir.startsWith('script-src')) ||
directives.find((dir) => dir.startsWith('default-src'))
// If no directive could be found, then we're done.
if (!directive) {
return
}
// Extract the nonce from the directive
const nonce = directive
.split(' ')
// Remove the 'strict-src'/'default-src' string, this can't be the nonce.
.slice(1)
.map((source) => source.trim())
// Find the first source with the 'nonce-' prefix.
.find(
(source) =>
source.startsWith("'nonce-") &&
source.length > 8 &&
source.endsWith("'")
)
// Grab the nonce by trimming the 'nonce-' prefix.
?.slice(7, -1)
// If we could't find the nonce, then we're done.
if (!nonce) {
return
}
// Don't accept the nonce value if it contains HTML escape characters.
// Technically, the spec requires a base64'd value, but this is just an
// extra layer.
if (ESCAPE_REGEX.test(nonce)) {
throw new Error(
'Nonce value from Content-Security-Policy contained HTML escape characters.\nLearn more: https://nextjs.org/docs/messages/nonce-contained-invalid-characters'
)
}
return nonce
}
function headersWithoutFlight(headers: IncomingHttpHeaders) {
const newHeaders = { ...headers }
for (const param of FLIGHT_PARAMETERS) {
delete newHeaders[param.toString().toLowerCase()]
}
return newHeaders
}
async function renderToString(element: React.ReactElement) {
const renderStream = await ReactDOMServer.renderToReadableStream(element)
await renderStream.allReady
return streamToString(renderStream)
}
export async function renderToHTMLOrFlight(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
query: NextParsedUrlQuery,
renderOpts: RenderOpts
): Promise<RenderResult | null> {
/**
* Rules of Static & Dynamic HTML:
*
* 1.) We must generate static HTML unless the caller explicitly opts
* in to dynamic HTML support.
*
* 2.) If dynamic HTML support is requested, we must honor that request
* or throw an error. It is the sole responsibility of the caller to
* ensure they aren't e.g. requesting dynamic HTML for an AMP page.
*
* These rules help ensure that other existing features like request caching,
* coalescing, and ISR continue working as intended.
*/
const isStaticGeneration =
renderOpts.supportsDynamicHTML !== true && !renderOpts.isBot
const isFlight = req.headers[RSC.toLowerCase()] !== undefined
const capturedErrors: Error[] = []
const allCapturedErrors: Error[] = []
const serverComponentsErrorHandler = createErrorHandler(
'serverComponentsRenderer',
capturedErrors
)
const flightDataRendererErrorHandler = createErrorHandler(
'flightDataRenderer',
capturedErrors
)
const htmlRendererErrorHandler = createErrorHandler(
'htmlRenderer',
capturedErrors,
allCapturedErrors
)
const {
buildManifest,
subresourceIntegrityManifest,
serverComponentManifest,
serverCSSManifest = {},
ComponentMod,
dev,
fontLoaderManifest,
supportsDynamicHTML,
} = renderOpts
patchFetch(ComponentMod)
const generateStaticHTML = supportsDynamicHTML !== true
const staticGenerationAsyncStorage = ComponentMod.staticGenerationAsyncStorage
const requestAsyncStorage = ComponentMod.requestAsyncStorage
if (
staticGenerationAsyncStorage &&
!('getStore' in staticGenerationAsyncStorage) &&
staticGenerationAsyncStorage.inUse
) {
throw new Error(
`Invariant: A separate worker must be used for each render when AsyncLocalStorage is not available`
)
}
// we wrap the render in an AsyncLocalStorage context
const wrappedRender = async () => {
const staticGenerationStore: StaticGenerationStore =
'getStore' in staticGenerationAsyncStorage
? staticGenerationAsyncStorage.getStore()
: staticGenerationAsyncStorage
// don't modify original query object
query = Object.assign({}, query)
const isPrefetch =
req.headers[NEXT_ROUTER_PREFETCH.toLowerCase()] !== undefined
// TODO-APP: verify the tree is valid
// TODO-APP: verify query param is single value (not an array)
// TODO-APP: verify tree can't grow out of control
/**
* Router state provided from the client-side router. Used to handle rendering from the common layout down.
*/
let providedFlightRouterState: FlightRouterState = isFlight
? req.headers[NEXT_ROUTER_STATE_TREE.toLowerCase()]
? JSON.parse(
req.headers[NEXT_ROUTER_STATE_TREE.toLowerCase()] as string
)
: undefined
: undefined
/**
* The tree created in next-app-loader that holds component segments and modules
*/
const loaderTree: LoaderTree = ComponentMod.tree
stripInternalQueries(query)
const LayoutRouter =
ComponentMod.LayoutRouter as typeof import('../client/components/layout-router').default
const RenderFromTemplateContext =
ComponentMod.RenderFromTemplateContext as typeof import('../client/components/render-from-template-context').default
/**
* Server Context is specifically only available in Server Components.
* It has to hold values that can't change while rendering from the common layout down.
* An example of this would be that `headers` are available but `searchParams` are not because that'd mean we have to render from the root layout down on all requests.
*/
const serverContexts: Array<[string, any]> = [
['WORKAROUND', null], // TODO-APP: First value has a bug currently where the value is not set on the second request: https://github.com/facebook/react/issues/24849
]
type CreateSegmentPath = (child: FlightSegmentPath) => FlightSegmentPath
/**
* Dynamic parameters. E.g. when you visit `/dashboard/vercel` which is rendered by `/dashboard/[slug]` the value will be {"slug": "vercel"}.
*/
const pathParams = (renderOpts as any).params as ParsedUrlQuery
/**
* Parse the dynamic segment and return the associated value.
*/
const getDynamicParamFromSegment = (
// [slug] / [[slug]] / [...slug]
segment: string
): {
param: string
value: string | string[] | null
treeSegment: Segment
type: DynamicParamTypesShort
} | null => {
const segmentParam = getSegmentParam(segment)
if (!segmentParam) {
return null
}
const key = segmentParam.param
let value = pathParams[key]
if (Array.isArray(value)) {
value = value.map((i) => encodeURIComponent(i))
} else if (typeof value === 'string') {
value = encodeURIComponent(value)
}
if (!value) {
// Handle case where optional catchall does not have a value, e.g. `/dashboard/[...slug]` when requesting `/dashboard`
if (segmentParam.type === 'optional-catchall') {
const type = getShortDynamicParamType(segmentParam.type)
return {
param: key,
value: null,
type: type,
// This value always has to be a string.
treeSegment: [key, '', type],
}
}
return null
}
const type = getShortDynamicParamType(segmentParam.type)
return {
param: key,
// The value that is passed to user code.
value: value,
// The value that is rendered in the router tree.
treeSegment: [
key,
Array.isArray(value) ? value.join('/') : value,
type,
],
type: type,
}
}
async function resolveHead(
[segment, parallelRoutes, { head }]: LoaderTree,
parentParams: { [key: string]: any }
): Promise<React.ReactNode> {
// Handle dynamic segment params.
const segmentParam = getDynamicParamFromSegment(segment)
/**
* Create object holding the parent params and current params
*/
const currentParams =
// Handle null case where dynamic param is optional
segmentParam && segmentParam.value !== null
? {
...parentParams,
[segmentParam.param]: segmentParam.value,
}
: // Pass through parent params to children
parentParams
for (const key in parallelRoutes) {
const childTree = parallelRoutes[key]
const returnedHead = await resolveHead(childTree, currentParams)
if (returnedHead) {
return returnedHead
}
}
if (head) {
const Head = await interopDefault(await head[0]())
return <Head params={currentParams} />
}
return null
}
const createFlightRouterStateFromLoaderTree = (
[segment, parallelRoutes, { layout }]: LoaderTree,
rootLayoutIncluded = false
): FlightRouterState => {
const dynamicParam = getDynamicParamFromSegment(segment)
const segmentTree: FlightRouterState = [
dynamicParam ? dynamicParam.treeSegment : segment,
{},
]
if (!rootLayoutIncluded && typeof layout !== 'undefined') {
rootLayoutIncluded = true
segmentTree[4] = true
}
segmentTree[1] = Object.keys(parallelRoutes).reduce(
(existingValue, currentValue) => {
existingValue[currentValue] = createFlightRouterStateFromLoaderTree(
parallelRoutes[currentValue],
rootLayoutIncluded
)
return existingValue
},
{} as FlightRouterState[1]
)
return segmentTree
}
let defaultRevalidate: false | undefined | number = false
// Collect all server CSS imports used by this specific entry (or entries, for parallel routes).
// Not that we can't rely on the CSS manifest because it tracks CSS imports per module,
// which can be used by multiple entries and cannot be tree-shaked in the module graph.
// More info: https://github.com/vercel/next.js/issues/41018
const serverCSSForEntries = getServerCSSForEntries(
serverCSSManifest!,
ComponentMod.pages
)
const assetPrefix = renderOpts.assetPrefix || ''
const createComponentAndStyles = async ({
filePath,
getComponent,
shouldPreload,
}: {
filePath: string
getComponent: () => any
shouldPreload?: boolean
}): Promise<any> => {
const cssHrefs = getCssInlinedLinkTags(
serverComponentManifest,
serverCSSManifest,
filePath,
serverCSSForEntries
)
const styles = cssHrefs
? cssHrefs.map((href, index) => (
<link
rel="stylesheet"
// In dev, Safari will wrongly cache the resource if you preload it:
// - https://github.com/vercel/next.js/issues/5860
// - https://bugs.webkit.org/show_bug.cgi?id=187726
// We used to add a `?ts=` query for resources in `pages` to bypass it,
// but in this case it is fine as we don't need to preload the styles.
href={`${assetPrefix}/_next/${href}`}
// @ts-ignore
precedence={shouldPreload ? 'high' : undefined}
key={index}
/>
))
: null
const Comp = await interopDefault(await getComponent())
return [Comp, styles]
}
/**
* Use the provided loader tree to create the React Component tree.
*/
const createComponentTree = async ({
createSegmentPath,
loaderTree: [
segment,
parallelRoutes,
{ layout, template, error, loading, page, 'not-found': notFound },
],
parentParams,
firstItem,
rootLayoutIncluded,
}: {
createSegmentPath: CreateSegmentPath
loaderTree: LoaderTree
parentParams: { [key: string]: any }
rootLayoutIncluded?: boolean
firstItem?: boolean
}): Promise<{ Component: React.ComponentType }> => {
const layoutOrPagePath = layout?.[1] || page?.[1]
const stylesheets: string[] = layoutOrPagePath
? getCssInlinedLinkTags(
serverComponentManifest,
serverCSSManifest!,
layoutOrPagePath,
serverCSSForEntries
)
: []
const preloadedFontFiles = layoutOrPagePath
? getPreloadedFontFilesInlineLinkTags(
serverComponentManifest,
serverCSSManifest!,
fontLoaderManifest,
serverCSSForEntries,
layoutOrPagePath
)
: []
const [Template, templateStyles] = template
? await createComponentAndStyles({
filePath: template[1],
getComponent: template[0],
shouldPreload: true,
})
: [React.Fragment]
const [ErrorComponent, errorStyles] = error
? await createComponentAndStyles({
filePath: error[1],
getComponent: error[0],
})
: []
const [Loading, loadingStyles] = loading
? await createComponentAndStyles({
filePath: loading[1],
getComponent: loading[0],
})
: []
const isLayout = typeof layout !== 'undefined'
const isPage = typeof page !== 'undefined'
const layoutOrPageMod = isLayout
? await layout[0]()
: isPage
? await page[0]()
: undefined
/**
* Checks if the current segment is a root layout.
*/
const rootLayoutAtThisLevel = isLayout && !rootLayoutIncluded
/**
* Checks if the current segment or any level above it has a root layout.
*/
const rootLayoutIncludedAtThisLevelOrAbove =
rootLayoutIncluded || rootLayoutAtThisLevel
const [NotFound, notFoundStyles] = notFound
? await createComponentAndStyles({
filePath: notFound[1],
getComponent: notFound[0],
})
: rootLayoutAtThisLevel
? [DefaultNotFound]
: []
if (typeof layoutOrPageMod?.dynamic === 'string') {
// the nested most config wins so we only force-static
// if it's configured above any parent that configured
// otherwise
if (layoutOrPageMod.dynamic === 'force-static') {
staticGenerationStore.forceStatic = true
} else if (layoutOrPageMod.dynamic !== 'error') {
staticGenerationStore.forceStatic = false
}
}
if (typeof layoutOrPageMod?.revalidate === 'number') {
defaultRevalidate = layoutOrPageMod.revalidate
if (isStaticGeneration && defaultRevalidate === 0) {
const { DynamicServerError } =
ComponentMod.serverHooks as typeof import('../client/components/hooks-server-context')
throw new DynamicServerError(`revalidate: 0 configured ${segment}`)
}
}
/**
* The React Component to render.
*/
let Component = layoutOrPageMod
? interopDefault(layoutOrPageMod)
: undefined
if (dev) {
const { isValidElementType } = require('next/dist/compiled/react-is')
if (
(isPage || typeof Component !== 'undefined') &&
!isValidElementType(Component)
) {
throw new Error(
`The default export is not a React Component in page: "${pathname}"`
)
}
if (
typeof ErrorComponent !== 'undefined' &&
!isValidElementType(ErrorComponent)
) {
throw new Error(
`The default export of error is not a React Component in page: ${segment}`
)
}
if (typeof Loading !== 'undefined' && !isValidElementType(Loading)) {
throw new Error(
`The default export of loading is not a React Component in ${segment}`
)
}
if (typeof NotFound !== 'undefined' && !isValidElementType(NotFound)) {
throw new Error(
`The default export of notFound is not a React Component in ${segment}`
)
}
}
// Handle dynamic segment params.
const segmentParam = getDynamicParamFromSegment(segment)
/**
* Create object holding the parent params and current params
*/
const currentParams =
// Handle null case where dynamic param is optional
segmentParam && segmentParam.value !== null
? {
...parentParams,
[segmentParam.param]: segmentParam.value,
}
: // Pass through parent params to children
parentParams
// Resolve the segment param
const actualSegment = segmentParam ? segmentParam.treeSegment : segment
// This happens outside of rendering in order to eagerly kick off data fetching for layouts / the page further down
const parallelRouteMap = await Promise.all(
Object.keys(parallelRoutes).map(
async (parallelRouteKey): Promise<[string, React.ReactNode]> => {
const currentSegmentPath: FlightSegmentPath = firstItem
? [parallelRouteKey]
: [actualSegment, parallelRouteKey]
const childSegment = parallelRoutes[parallelRouteKey][0]
const childSegmentParam = getDynamicParamFromSegment(childSegment)
if (isPrefetch && Loading) {
const childProp: ChildProp = {
// Null indicates the tree is not fully rendered
current: null,
segment: childSegmentParam
? childSegmentParam.treeSegment
: childSegment,
}
// This is turned back into an object below.
return [
parallelRouteKey,
<LayoutRouter
parallelRouterKey={parallelRouteKey}
segmentPath={createSegmentPath(currentSegmentPath)}
loading={Loading ? <Loading /> : undefined}
loadingStyles={loadingStyles}
hasLoading={Boolean(Loading)}
error={ErrorComponent}
errorStyles={errorStyles}
template={
<Template>
<RenderFromTemplateContext />
</Template>
}
templateStyles={templateStyles}
notFound={NotFound ? <NotFound /> : undefined}
notFoundStyles={notFoundStyles}
childProp={childProp}
rootLayoutIncluded={rootLayoutIncludedAtThisLevelOrAbove}
/>,
]
}
// Create the child component
const { Component: ChildComponent } = await createComponentTree({
createSegmentPath: (child) => {
return createSegmentPath([...currentSegmentPath, ...child])
},
loaderTree: parallelRoutes[parallelRouteKey],
parentParams: currentParams,
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
})
const childProp: ChildProp = {
current: <ChildComponent />,
segment: childSegmentParam
? childSegmentParam.treeSegment
: childSegment,
}
const segmentPath = createSegmentPath(currentSegmentPath)
// This is turned back into an object below.
return [
parallelRouteKey,
<LayoutRouter
parallelRouterKey={parallelRouteKey}
segmentPath={segmentPath}
error={ErrorComponent}
errorStyles={errorStyles}
loading={Loading ? <Loading /> : undefined}
loadingStyles={loadingStyles}
// TODO-APP: Add test for loading returning `undefined`. This currently can't be tested as the `webdriver()` tab will wait for the full page to load before returning.
hasLoading={Boolean(Loading)}
template={
<Template>
<RenderFromTemplateContext />
</Template>
}
templateStyles={templateStyles}
notFound={NotFound ? <NotFound /> : undefined}
notFoundStyles={notFoundStyles}
childProp={childProp}
rootLayoutIncluded={rootLayoutIncludedAtThisLevelOrAbove}
/>,
]
}
)
)
// Convert the parallel route map into an object after all promises have been resolved.
const parallelRouteComponents = parallelRouteMap.reduce(
(list, [parallelRouteKey, Comp]) => {
list[parallelRouteKey] = Comp
return list
},
{} as { [key: string]: React.ReactNode }
)
// When the segment does not have a layout or page we still have to add the layout router to ensure the path holds the loading component
if (!Component) {
return {
Component: () => <>{parallelRouteComponents.children}</>,
}
}
const props = {
...parallelRouteComponents,
// TODO-APP: params and query have to be blocked parallel route names. Might have to add a reserved name list.
// Params are always the current params that apply to the layout
// If you have a `/dashboard/[team]/layout.js` it will provide `team` as a param but not anything further down.
params: currentParams,
// Query is only provided to page
...(isPage ? { searchParams: query } : {}),
}
// Eagerly execute layout/page component to trigger fetches early.
Component = await Promise.resolve().then(() => {
return preloadComponent(Component, props)
})
return {
Component: () => {
return (
<>
{preloadedFontFiles.map((fontFile) => {
const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFile)![1]
return (
<link
key={fontFile}
rel="preload"
href={`${assetPrefix}/_next/${fontFile}`}
as="font"
type={`font/${ext}`}
crossOrigin="anonymous"
/>
)
})}
{stylesheets
? stylesheets.map((href, index) => (
<link
rel="stylesheet"
// In dev, Safari will wrongly cache the resource if you preload it:
// - https://github.com/vercel/next.js/issues/5860
// - https://bugs.webkit.org/show_bug.cgi?id=187726
// We used to add a `?ts=` query for resources in `pages` to bypass it,
// but in this case it is fine as we don't need to preload the styles.
href={`${assetPrefix}/_next/${href}`}
// `Precedence` is an opt-in signal for React to handle
// resource loading and deduplication, etc:
// https://github.com/facebook/react/pull/25060
// @ts-ignore
precedence="high"
key={index}
/>
))
: null}
<Component {...props} />
{/* {HeadTags ? <HeadTags /> : null} */}
</>
)
},
}
}
const streamToBufferedResult = async (
renderResult: RenderResult
): Promise<string> => {
const renderChunks: Buffer[] = []
const writable = new Writable({
write(chunk, _encoding, callback) {
renderChunks.push(chunk)
callback()
},
})
await renderResult.pipe(writable)
return Buffer.concat(renderChunks).toString()
}
// Handle Flight render request. This is only used when client-side navigating. E.g. when you `router.push('/dashboard')` or `router.reload()`.
const generateFlight = async (): Promise<RenderResult> => {
// TODO-APP: throw on invalid flightRouterState
/**
* Use router state to decide at what common layout to render the page.
* This can either be the common layout between two pages or a specific place to start rendering from using the "refetch" marker in the tree.
*/
const walkTreeWithFlightRouterState = async ({
createSegmentPath,
loaderTreeToFilter,
parentParams,
isFirst,
flightRouterState,
parentRendered,
rscPayloadHead,
}: {
createSegmentPath: CreateSegmentPath
loaderTreeToFilter: LoaderTree
parentParams: { [key: string]: string | string[] }
isFirst: boolean
flightRouterState?: FlightRouterState
parentRendered?: boolean
rscPayloadHead: React.ReactNode
}): Promise<FlightDataPath> => {
const [segment, parallelRoutes] = loaderTreeToFilter
const parallelRoutesKeys = Object.keys(parallelRoutes)
// Because this function walks to a deeper point in the tree to start rendering we have to track the dynamic parameters up to the point where rendering starts
const segmentParam = getDynamicParamFromSegment(segment)
const currentParams =
// Handle null case where dynamic param is optional
segmentParam && segmentParam.value !== null
? {
...parentParams,
[segmentParam.param]: segmentParam.value,
}
: parentParams
const actualSegment: Segment = segmentParam
? segmentParam.treeSegment
: segment
/**
* Decide if the current segment is where rendering has to start.
*/
const renderComponentsOnThisLevel =
// No further router state available
!flightRouterState ||
// Segment in router state does not match current segment
!matchSegment(actualSegment, flightRouterState[0]) ||
// Last item in the tree
parallelRoutesKeys.length === 0 ||
// Explicit refresh
flightRouterState[3] === 'refetch'
if (!parentRendered && renderComponentsOnThisLevel) {
return [
actualSegment,
// Create router state using the slice of the loaderTree
createFlightRouterStateFromLoaderTree(loaderTreeToFilter),
// Check if one level down from the common layout has a loading component. If it doesn't only provide the router state as part of the Flight data.
isPrefetch && !Boolean(loaderTreeToFilter[2].loading)
? null
: // Create component tree using the slice of the loaderTree
// @ts-expect-error TODO-APP: fix async component type
React.createElement(async () => {
const { Component } = await createComponentTree(
// This ensures flightRouterPath is valid and filters down the tree
{
createSegmentPath: (child) => {
return createSegmentPath(child)
},
loaderTree: loaderTreeToFilter,
parentParams: currentParams,
firstItem: isFirst,
}
)
return <Component />
}),
isPrefetch && !Boolean(loaderTreeToFilter[2].loading) ? null : (
<>{rscPayloadHead}</>
),
]
}
// Walk through all parallel routes.
for (const parallelRouteKey of parallelRoutesKeys) {
const parallelRoute = parallelRoutes[parallelRouteKey]
const currentSegmentPath: FlightSegmentPath = isFirst
? [parallelRouteKey]
: [actualSegment, parallelRouteKey]
const path = await walkTreeWithFlightRouterState({
createSegmentPath: (child) => {
return createSegmentPath([...currentSegmentPath, ...child])
},
loaderTreeToFilter: parallelRoute,
parentParams: currentParams,
flightRouterState:
flightRouterState && flightRouterState[1][parallelRouteKey],
parentRendered: parentRendered || renderComponentsOnThisLevel,
isFirst: false,
rscPayloadHead,
})
if (typeof path[path.length - 1] !== 'string') {
return [actualSegment, parallelRouteKey, ...path]
}
}
return [actualSegment]
}
const rscPayloadHead = await resolveHead(loaderTree, {})
// 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.
const flightData: FlightData = [
(
await walkTreeWithFlightRouterState({
createSegmentPath: (child) => child,
loaderTreeToFilter: loaderTree,
parentParams: {},
flightRouterState: providedFlightRouterState,
isFirst: true,
rscPayloadHead,
})
).slice(1),
]
// For app dir, use the bundled version of Fizz renderer (renderToReadableStream)
// which contains the subset React.
const readable = ComponentMod.renderToReadableStream(
flightData,
serverComponentManifest,
{
context: serverContexts,
onError: flightDataRendererErrorHandler,
}
).pipeThrough(createBufferedTransformStream())
return new FlightRenderResult(readable)
}
if (isFlight && !isStaticGeneration) {
return generateFlight()
}
// Below this line is handling for rendering to HTML.
// AppRouter is provided by next-app-loader
const AppRouter =
ComponentMod.AppRouter as typeof import('../client/components/app-router').default
let serverComponentsInlinedTransformStream: TransformStream<
Uint8Array,
Uint8Array
> = new TransformStream()
// TODO-APP: validate req.url as it gets passed to render.
const initialCanonicalUrl = req.url!
// Get the nonce from the incomming request if it has one.
const csp = req.headers['content-security-policy']
let nonce: string | undefined
if (csp && typeof csp === 'string') {
nonce = getScriptNonceFromHeader(csp)
}
const serverComponentsRenderOpts = {
transformStream: serverComponentsInlinedTransformStream,
serverComponentManifest,
serverContexts,
rscChunks: [],
}
const validateRootLayout = dev
? {
validateRootLayout: {
assetPrefix: renderOpts.assetPrefix,
getTree: () => createFlightRouterStateFromLoaderTree(loaderTree),
},
}
: {}
const initialHead = await resolveHead(loaderTree, {})
/**
* A new React Component that renders the provided React Component
* using Flight which can then be rendered to HTML.
*/
const ServerComponentsRenderer = createServerComponentRenderer(
async () => {
// Create full component tree from root to leaf.
const { Component: ComponentTree } = await createComponentTree({
createSegmentPath: (child) => child,
loaderTree: loaderTree,
parentParams: {},
firstItem: true,
})
const initialTree = createFlightRouterStateFromLoaderTree(loaderTree)
return (
<AppRouter
assetPrefix={assetPrefix}
initialCanonicalUrl={initialCanonicalUrl}
initialTree={initialTree}
initialHead={initialHead}
>
<ComponentTree />
</AppRouter>
)
},
ComponentMod,
serverComponentsRenderOpts,
serverComponentsErrorHandler,
nonce
)
const serverInsertedHTMLCallbacks: Set<() => React.ReactNode> = new Set()
function InsertedHTML({ children }: { children: JSX.Element }) {
// Reset addInsertedHtmlCallback on each render
serverInsertedHTMLCallbacks.clear()
const addInsertedHtml = React.useCallback(
(handler: () => React.ReactNode) => {
serverInsertedHTMLCallbacks.add(handler)
},
[]
)
return (
<HeadManagerContext.Provider
value={{
appDir: true,
nonce,
}}
>
<ServerInsertedHTMLContext.Provider value={addInsertedHtml}>
{children}
</ServerInsertedHTMLContext.Provider>
</HeadManagerContext.Provider>
)
}
const bodyResult = async () => {
const polyfills = buildManifest.polyfillFiles
.filter(
(polyfill) =>
polyfill.endsWith('.js') && !polyfill.endsWith('.module.js')
)
.map((polyfill) => ({
src: `${assetPrefix}/_next/${polyfill}`,
integrity: subresourceIntegrityManifest?.[polyfill],
}))
const content = (
<InsertedHTML>
<ServerComponentsRenderer />
</InsertedHTML>
)
let polyfillsFlushed = false
const getServerInsertedHTML = (): Promise<string> => {
const flushed = renderToString(
<>
{Array.from(serverInsertedHTMLCallbacks).map((callback) =>
callback()
)}
{polyfillsFlushed
? null
: polyfills?.map((polyfill) => {
return (
<script
key={polyfill.src}
src={polyfill.src}
integrity={polyfill.integrity}
noModule={true}
nonce={nonce}
/>
)
})}
</>
)
polyfillsFlushed = true
return flushed
}
try {
const renderStream = await renderToInitialStream({
ReactDOMServer,
element: content,
streamOptions: {
onError: htmlRendererErrorHandler,
nonce,
// Include hydration scripts in the HTML
bootstrapScripts: [
...(subresourceIntegrityManifest
? buildManifest.rootMainFiles.map((src) => ({
src: `${assetPrefix}/_next/` + src,
integrity: subresourceIntegrityManifest[src],
}))
: buildManifest.rootMainFiles.map(
(src) => `${assetPrefix}/_next/` + src
)),
],
},
})
const result = await continueFromInitialStream(renderStream, {
dataStream: serverComponentsInlinedTransformStream?.readable,
generateStaticHTML: isStaticGeneration || generateStaticHTML,
getServerInsertedHTML,
serverInsertedHTMLToHead: true,
...validateRootLayout,
})
return result
} catch (err: any) {
const shouldNotIndex = err.digest === NOT_FOUND_ERROR_CODE
if (err.digest === NOT_FOUND_ERROR_CODE) {
res.statusCode = 404
}
const renderStream = await renderToInitialStream({
ReactDOMServer,
element: (
<html id="__next_error__">
<head>
{shouldNotIndex ? (
<meta name="robots" content="noindex" />
) : null}
</head>
<body></body>
</html>
),
streamOptions: {
nonce,
// Include hydration scripts in the HTML
bootstrapScripts: subresourceIntegrityManifest
? buildManifest.rootMainFiles.map((src) => ({
src: `${assetPrefix}/_next/` + src,
integrity: subresourceIntegrityManifest[src],
}))
: buildManifest.rootMainFiles.map(
(src) => `${assetPrefix}/_next/` + src
),
},
})
return await continueFromInitialStream(renderStream, {
dataStream: serverComponentsInlinedTransformStream?.readable,
generateStaticHTML: isStaticGeneration,
getServerInsertedHTML,
serverInsertedHTMLToHead: true,
...validateRootLayout,
})
}
}
const renderResult = new RenderResult(await bodyResult())
if (staticGenerationStore.pendingRevalidates) {
await Promise.all(staticGenerationStore.pendingRevalidates)
}
if (isStaticGeneration) {
const htmlResult = await streamToBufferedResult(renderResult)
// if we encountered any unexpected errors during build
// we fail the prerendering phase and the build
if (capturedErrors.length > 0) {
throw capturedErrors[0]
}
// const before = Buffer.concat(
// serverComponentsRenderOpts.rscChunks
// ).toString()
// TODO-APP: derive this from same pass to prevent additional
// render during static generation
const filteredFlightData = await streamToBufferedResult(
await generateFlight()
)
if (staticGenerationStore?.forceStatic === false) {
staticGenerationStore.fetchRevalidate = 0
}
;(renderOpts as any).pageData = filteredFlightData
;(renderOpts as any).revalidate =
typeof staticGenerationStore?.fetchRevalidate === 'undefined'
? defaultRevalidate
: staticGenerationStore?.fetchRevalidate
return new RenderResult(htmlResult)
}
return renderResult
}
const initialStaticGenerationStore = {
isStaticGeneration,
inUse: true,
pathname,
incrementalCache: renderOpts.incrementalCache,
isRevalidate: renderOpts.isRevalidate,
}
const tryGetPreviewData =
process.env.NEXT_RUNTIME === 'edge'
? () => false
: require('./api-utils/node').tryGetPreviewData
// Reads of this are cached on the `req` object, so this should resolve
// instantly. There's no need to pass this data down from a previous
// invoke, where we'd have to consider server & serverless.
const previewData = tryGetPreviewData(
req,
res,
(renderOpts as any).previewProps
)
let cachedHeadersInstance: ReadonlyHeaders | undefined
let cachedCookiesInstance: ReadonlyRequestCookies | undefined
const requestStore = {
get headers() {
if (!cachedHeadersInstance) {
cachedHeadersInstance = new ReadonlyHeaders(
headersWithoutFlight(req.headers)
)
}
return cachedHeadersInstance
},
get cookies() {
if (!cachedCookiesInstance) {
cachedCookiesInstance = new ReadonlyRequestCookies({
headers: {
get: (key) => {
if (key !== 'cookie') {
throw new Error('Only cookie header is supported')
}
return req.headers.cookie
},
},
})
}
return cachedCookiesInstance
},
previewData,
}
function handleRequestStoreRun<T>(fn: () => T): Promise<T> {
if ('getStore' in requestAsyncStorage) {
return new Promise((resolve, reject) => {
requestAsyncStorage.run(requestStore, () => {
return Promise.resolve(fn()).then(resolve).catch(reject)
})
})
} else {
Object.assign(requestAsyncStorage, requestStore)
return Promise.resolve(fn())
}
}
function handleStaticGenerationStoreRun<T>(fn: () => T): Promise<T> {
if ('getStore' in staticGenerationAsyncStorage) {
return new Promise((resolve, reject) => {
staticGenerationAsyncStorage.run(initialStaticGenerationStore, () => {
return Promise.resolve(fn()).then(resolve).catch(reject)
})
})
} else {
Object.assign(staticGenerationAsyncStorage, initialStaticGenerationStore)
return Promise.resolve(fn()).finally(() => {
staticGenerationAsyncStorage.inUse = false
})
}
}
return handleRequestStoreRun(() =>
handleStaticGenerationStoreRun(() => wrappedRender())
)
}