rsnext/packages/next/server/app-render.tsx
JJ Kasper 0c756bee15
Update cache handling for app (#43659)
This updates the app directory caching. 

x-ref: [slack thread
](https://vercel.slack.com/archives/C042LHPJ1NX/p1669231119199339)

## 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)

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ]
[e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] 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`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
2022-12-02 23:32:49 -08:00

1953 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 { 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 = (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.digest === DYNAMIC_ERROR_CODE ||
err.digest === NOT_FOUND_ERROR_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 = [
// TODO-APP: change walk to output without ''
(
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
}
// TODO-APP: show error overlay in development. `element` should probably be wrapped in AppRouter for this case.
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())
)
}