rsnext/packages/next/next-server/server/render.tsx
Gerald Monaco 039eb817e1
Remove next-head-count (#16758)
Removes `next-head-count`, improving support for 3rd party libraries that insert or append new elements to `<head>`.

---

This is more or less what a solution with a `data-` attribute would look like, except that instead of directly searching for elements with that attribute, we serialize the elements expected in `<head>` and then find them/assume ownership of them during initialization (in a manner similar to React's reconciliation) based on their properties.

There are two main assumptions here:
1. Content is served with compression, so duplicate serialization of e.g. inline script or style tags doesn't have a meaningful impact. Storing a hash would be a potential optimization.
2. 3rd party libraries primarily only insert new, unique elements to head. Libraries trying to actively manage elements that overlap with those that Next.js claims ownership of will still be unsupported.

The reason for this roundabout approach is that I'd really like to avoid `data-` if possible, for maximum compatibility. Implicitly adding an attribute could be a breaking change for some class of tools or crawlers and makes it otherwise impossible to insert raw HTML into `<head>`. Adding an unexpected attribute is why the original `class="next-head"` approach was problematic in the first place!

That said, while I don't expect this to be more problematic than `next-head-count` (anything that would break in this new model also should have broken in the old model), if that does end up being the case, it might make sense to just bite the bullet.

Fixes #11012
Closes #16707

---

cc @Timer @timneutkens
2020-09-09 01:41:04 +00:00

972 lines
28 KiB
TypeScript

import { IncomingMessage, ServerResponse } from 'http'
import { ParsedUrlQuery } from 'querystring'
import React from 'react'
import { renderToStaticMarkup, renderToString } from 'react-dom/server'
import { UnwrapPromise } from '../../lib/coalesced-function'
import {
GSP_NO_RETURNED_VALUE,
GSSP_COMPONENT_MEMBER_ERROR,
GSSP_NO_RETURNED_VALUE,
PAGES_404_GET_INITIAL_PROPS_ERROR,
SERVER_PROPS_GET_INIT_PROPS_CONFLICT,
SERVER_PROPS_SSG_CONFLICT,
SSG_GET_INITIAL_PROPS_CONFLICT,
UNSTABLE_REVALIDATE_RENAME_ERROR,
} from '../../lib/constants'
import { isSerializableProps } from '../../lib/is-serializable-props'
import { GetServerSideProps, GetStaticProps } from '../../types'
import { isInAmpMode } from '../lib/amp'
import { AmpStateContext } from '../lib/amp-context'
import {
AMP_RENDER_TARGET,
SERVER_PROPS_ID,
STATIC_PROPS_ID,
PERMANENT_REDIRECT_STATUS,
TEMPORARY_REDIRECT_STATUS,
} from '../lib/constants'
import { defaultHead } from '../lib/head'
import { HeadManagerContext } from '../lib/head-manager-context'
import Loadable from '../lib/loadable'
import { LoadableContext } from '../lib/loadable-context'
import mitt, { MittEmitter } from '../lib/mitt'
import postProcess from '../lib/post-process'
import { RouterContext } from '../lib/router-context'
import { NextRouter } from '../lib/router/router'
import { isDynamicRoute } from '../lib/router/utils/is-dynamic'
import {
AppType,
ComponentsEnhancer,
DocumentInitialProps,
DocumentType,
getDisplayName,
isResSent,
loadGetInitialProps,
NextComponentType,
RenderPage,
DocumentProps,
} from '../lib/utils'
import { tryGetPreviewData, __ApiPreviewProps } from './api-utils'
import { denormalizePagePath } from './denormalize-page-path'
import { FontManifest, getFontDefinitionFromManifest } from './font-utils'
import { LoadComponentsReturnType, ManifestItem } from './load-components'
import { normalizePagePath } from './normalize-page-path'
import optimizeAmp from './optimize-amp'
function noRouter() {
const message =
'No router instance found. you should only use "next/router" inside the client side of your app. https://err.sh/vercel/next.js/no-router-instance'
throw new Error(message)
}
class ServerRouter implements NextRouter {
route: string
pathname: string
query: ParsedUrlQuery
asPath: string
basePath: string
events: any
isFallback: boolean
// TODO: Remove in the next major version, as this would mean the user is adding event listeners in server-side `render` method
static events: MittEmitter = mitt()
constructor(
pathname: string,
query: ParsedUrlQuery,
as: string,
{ isFallback }: { isFallback: boolean },
basePath: string
) {
this.route = pathname.replace(/\/$/, '') || '/'
this.pathname = pathname
this.query = query
this.asPath = as
this.isFallback = isFallback
this.basePath = basePath
}
push(): any {
noRouter()
}
replace(): any {
noRouter()
}
reload() {
noRouter()
}
back() {
noRouter()
}
prefetch(): any {
noRouter()
}
beforePopState() {
noRouter()
}
}
function enhanceComponents(
options: ComponentsEnhancer,
App: AppType,
Component: NextComponentType
): {
App: AppType
Component: NextComponentType
} {
// For backwards compatibility
if (typeof options === 'function') {
return {
App,
Component: options(Component),
}
}
return {
App: options.enhanceApp ? options.enhanceApp(App) : App,
Component: options.enhanceComponent
? options.enhanceComponent(Component)
: Component,
}
}
export type RenderOptsPartial = {
buildId: string
canonicalBase: string
runtimeConfig?: { [key: string]: any }
assetPrefix?: string
err?: Error | null
autoExport?: boolean
nextExport?: boolean
dev?: boolean
ampMode?: any
ampPath?: string
inAmpMode?: boolean
hybridAmp?: boolean
ErrorDebug?: React.ComponentType<{ error: Error }>
ampValidator?: (html: string, pathname: string) => Promise<void>
ampSkipValidation?: boolean
ampOptimizerConfig?: { [key: string]: any }
isDataReq?: boolean
params?: ParsedUrlQuery
previewProps: __ApiPreviewProps
basePath: string
unstable_runtimeJS?: false
optimizeFonts: boolean
fontManifest?: FontManifest
optimizeImages: boolean
devOnlyCacheBusterQueryString?: string
}
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
function renderDocument(
Document: DocumentType,
{
buildManifest,
docComponentsRendered,
props,
docProps,
pathname,
query,
buildId,
canonicalBase,
assetPrefix,
runtimeConfig,
nextExport,
autoExport,
isFallback,
dynamicImportsIds,
dangerousAsPath,
err,
dev,
ampPath,
ampState,
inAmpMode,
hybridAmp,
dynamicImports,
headTags,
gsp,
gssp,
customServer,
gip,
appGip,
unstable_runtimeJS,
devOnlyCacheBusterQueryString,
}: RenderOpts & {
props: any
docComponentsRendered: DocumentProps['docComponentsRendered']
docProps: DocumentInitialProps
pathname: string
query: ParsedUrlQuery
dangerousAsPath: string
ampState: any
ampPath: string
inAmpMode: boolean
hybridAmp: boolean
dynamicImportsIds: string[]
dynamicImports: ManifestItem[]
headTags: any
isFallback?: boolean
gsp?: boolean
gssp?: boolean
customServer?: boolean
gip?: boolean
appGip?: boolean
devOnlyCacheBusterQueryString: string
}
): string {
return (
'<!DOCTYPE html>' +
renderToStaticMarkup(
<AmpStateContext.Provider value={ampState}>
{Document.renderDocument(Document, {
__NEXT_DATA__: {
props, // The result of getInitialProps
page: pathname, // The rendered page
query, // querystring parsed / passed by the user
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
nextExport, // If this is a page exported by `next export`
autoExport, // If this is an auto exported page
isFallback,
dynamicIds:
dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
gsp, // whether the page is getStaticProps
gssp, // whether the page is getServerSideProps
customServer, // whether the user is using a custom server
gip, // whether the page has getInitialProps
appGip, // whether the _app has getInitialProps
head: React.Children.toArray(docProps.head || [])
.map((elem) => {
const { children } = elem?.props
return [
elem?.type,
{
...elem?.props,
children: children
? typeof children === 'string'
? children
: children.join('')
: undefined,
},
]
})
.filter(Boolean) as any,
},
buildManifest,
docComponentsRendered,
dangerousAsPath,
canonicalBase,
ampPath,
inAmpMode,
isDevelopment: !!dev,
hybridAmp,
dynamicImports,
assetPrefix,
headTags,
unstable_runtimeJS,
devOnlyCacheBusterQueryString,
...docProps,
})}
</AmpStateContext.Provider>
)
)
}
const invalidKeysMsg = (methodName: string, invalidKeys: string[]) => {
return (
`Additional keys were returned from \`${methodName}\`. Properties intended for your component must be nested under the \`props\` key, e.g.:` +
`\n\n\treturn { props: { title: 'My Title', content: '...' } }` +
`\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.` +
`\nRead more: https://err.sh/next.js/invalid-getstaticprops-value`
)
}
type Redirect = {
permanent: boolean
destination: string
}
function checkRedirectValues(redirect: Redirect, req: IncomingMessage) {
const { destination, permanent } = redirect
let invalidPermanent = typeof permanent !== 'boolean'
let invalidDestination = typeof destination !== 'string'
if (invalidPermanent || invalidDestination) {
throw new Error(
`Invalid redirect object returned from getStaticProps for ${req.url}\n` +
`Expected${
invalidPermanent
? ` \`permanent\` to be boolean but received ${typeof permanent}`
: ''
}${invalidPermanent && invalidDestination ? ' and' : ''}${
invalidDestination
? ` \`destinatino\` to be string but received ${typeof destination}`
: ''
}\n` +
`See more info here: https://err.sh/vercel/next.js/invalid-redirect-gssp`
)
}
}
function handleRedirect(res: ServerResponse, redirect: Redirect) {
const statusCode = redirect.permanent
? PERMANENT_REDIRECT_STATUS
: TEMPORARY_REDIRECT_STATUS
if (redirect.permanent) {
res.setHeader('Refresh', `0;url=${redirect.destination}`)
}
res.statusCode = statusCode
res.setHeader('Location', redirect.destination)
res.end()
}
export async function renderToHTML(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
query: ParsedUrlQuery,
renderOpts: RenderOpts
): Promise<string | null> {
// In dev we invalidate the cache by appending a timestamp to the resource URL.
// This is a workaround to fix https://github.com/vercel/next.js/issues/5860
// TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed.
renderOpts.devOnlyCacheBusterQueryString = renderOpts.dev
? renderOpts.devOnlyCacheBusterQueryString || `?ts=${Date.now()}`
: ''
const {
err,
dev = false,
ampPath = '',
App,
Document,
pageConfig = {},
Component,
buildManifest,
fontManifest,
reactLoadableManifest,
ErrorDebug,
getStaticProps,
getStaticPaths,
getServerSideProps,
isDataReq,
params,
previewProps,
basePath,
devOnlyCacheBusterQueryString,
} = renderOpts
const getFontDefinition = (url: string): string => {
if (fontManifest) {
return getFontDefinitionFromManifest(url, fontManifest)
}
return ''
}
const callMiddleware = async (method: string, args: any[], props = false) => {
let results: any = props ? {} : []
if ((Document as any)[`${method}Middleware`]) {
let middlewareFunc = await (Document as any)[`${method}Middleware`]
middlewareFunc = middlewareFunc.default || middlewareFunc
const curResults = await middlewareFunc(...args)
if (props) {
for (const result of curResults) {
results = {
...results,
...result,
}
}
} else {
results = curResults
}
}
return results
}
const headTags = (...args: any) => callMiddleware('headTags', args)
const isFallback = !!query.__nextFallback
delete query.__nextFallback
const isSSG = !!getStaticProps
const isBuildTimeSSG = isSSG && renderOpts.nextExport
const defaultAppGetInitialProps =
App.getInitialProps === (App as any).origGetInitialProps
const hasPageGetInitialProps = !!(Component as any).getInitialProps
const pageIsDynamic = isDynamicRoute(pathname)
const isAutoExport =
!hasPageGetInitialProps &&
defaultAppGetInitialProps &&
!isSSG &&
!getServerSideProps
for (const methodName of [
'getStaticProps',
'getServerSideProps',
'getStaticPaths',
]) {
if ((Component as any)[methodName]) {
throw new Error(
`page ${pathname} ${methodName} ${GSSP_COMPONENT_MEMBER_ERROR}`
)
}
}
if (hasPageGetInitialProps && isSSG) {
throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT + ` ${pathname}`)
}
if (hasPageGetInitialProps && getServerSideProps) {
throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT + ` ${pathname}`)
}
if (getServerSideProps && isSSG) {
throw new Error(SERVER_PROPS_SSG_CONFLICT + ` ${pathname}`)
}
if (!!getStaticPaths && !isSSG) {
throw new Error(
`getStaticPaths was added without a getStaticProps in ${pathname}. Without getStaticProps, getStaticPaths does nothing`
)
}
if (isSSG && pageIsDynamic && !getStaticPaths) {
throw new Error(
`getStaticPaths is required for dynamic SSG pages and is missing for '${pathname}'.` +
`\nRead more: https://err.sh/next.js/invalid-getstaticpaths-value`
)
}
if (dev) {
const { isValidElementType } = require('react-is')
if (!isValidElementType(Component)) {
throw new Error(
`The default export is not a React Component in page: "${pathname}"`
)
}
if (!isValidElementType(App)) {
throw new Error(
`The default export is not a React Component in page: "/_app"`
)
}
if (!isValidElementType(Document)) {
throw new Error(
`The default export is not a React Component in page: "/_document"`
)
}
if (isAutoExport || isFallback) {
// remove query values except ones that will be set during export
query = {
...(query.amp
? {
amp: query.amp,
}
: {}),
}
req.url = pathname
renderOpts.nextExport = true
}
if (pathname === '/404' && (hasPageGetInitialProps || getServerSideProps)) {
throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR)
}
}
if (isAutoExport) renderOpts.autoExport = true
if (isSSG) renderOpts.nextExport = false
await Loadable.preloadAll() // Make sure all dynamic imports are loaded
// url will always be set
const asPath: string = req.url as string
const router = new ServerRouter(
pathname,
query,
asPath,
{
isFallback: isFallback,
},
basePath
)
const ctx = {
err,
req: isAutoExport ? undefined : req,
res: isAutoExport ? undefined : res,
pathname,
query,
asPath,
AppTree: (props: any) => {
return (
<AppContainer>
<App {...props} Component={Component} router={router} />
</AppContainer>
)
},
}
let props: any
const ampState = {
ampFirst: pageConfig.amp === true,
hasQuery: Boolean(query.amp),
hybrid: pageConfig.amp === 'hybrid',
}
const inAmpMode = isInAmpMode(ampState)
const reactLoadableModules: string[] = []
let head: JSX.Element[] = defaultHead(inAmpMode)
const AppContainer = ({ children }: any) => (
<RouterContext.Provider value={router}>
<AmpStateContext.Provider value={ampState}>
<HeadManagerContext.Provider
value={{
updateHead: (state) => {
head = state
},
mountedInstances: new Set(),
}}
>
<LoadableContext.Provider
value={(moduleName) => reactLoadableModules.push(moduleName)}
>
{children}
</LoadableContext.Provider>
</HeadManagerContext.Provider>
</AmpStateContext.Provider>
</RouterContext.Provider>
)
try {
props = await loadGetInitialProps(App, {
AppTree: ctx.AppTree,
Component,
router,
ctx,
})
if (isSSG) {
props[STATIC_PROPS_ID] = true
}
let previewData: string | false | object | undefined
if ((isSSG || getServerSideProps) && !isFallback) {
// 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.
previewData = tryGetPreviewData(req, res, previewProps)
}
if (isSSG && !isFallback) {
let data: UnwrapPromise<ReturnType<GetStaticProps>>
try {
data = await getStaticProps!({
...(pageIsDynamic ? { params: query as ParsedUrlQuery } : undefined),
...(previewData !== false
? { preview: true, previewData: previewData }
: undefined),
})
} catch (staticPropsError) {
// remove not found error code to prevent triggering legacy
// 404 rendering
if (staticPropsError.code === 'ENOENT') {
delete staticPropsError.code
}
throw staticPropsError
}
if (data == null) {
throw new Error(GSP_NO_RETURNED_VALUE)
}
const invalidKeys = Object.keys(data).filter(
(key) =>
key !== 'revalidate' && key !== 'props' && key !== 'unstable_redirect'
)
if (invalidKeys.includes('unstable_revalidate')) {
throw new Error(UNSTABLE_REVALIDATE_RENAME_ERROR)
}
if (invalidKeys.length) {
throw new Error(invalidKeysMsg('getStaticProps', invalidKeys))
}
if (
data.unstable_redirect &&
typeof data.unstable_redirect === 'object'
) {
checkRedirectValues(data.unstable_redirect, req)
if (isBuildTimeSSG) {
throw new Error(
`\`redirect\` can not be returned from getStaticProps during prerendering (${req.url})\n` +
`See more info here: https://err.sh/next.js/gsp-redirect-during-prerender`
)
}
if (isDataReq) {
data.props = {
__N_REDIRECT: data.unstable_redirect.destination,
}
} else {
handleRedirect(res, data.unstable_redirect)
return null
}
}
if (
(dev || isBuildTimeSSG) &&
!isSerializableProps(pathname, 'getStaticProps', data.props)
) {
// this fn should throw an error instead of ever returning `false`
throw new Error(
'invariant: getStaticProps did not return valid props. Please report this.'
)
}
if (typeof data.revalidate === 'number') {
if (!Number.isInteger(data.revalidate)) {
throw new Error(
`A page's revalidate option must be seconds expressed as a natural number. Mixed numbers, such as '${data.revalidate}', cannot be used.` +
`\nTry changing the value to '${Math.ceil(
data.revalidate
)}' or using \`Math.ceil()\` if you're computing the value.`
)
} else if (data.revalidate <= 0) {
throw new Error(
`A page's revalidate option can not be less than or equal to zero. A revalidate option of zero means to revalidate after _every_ request, and implies stale data cannot be tolerated.` +
`\n\nTo never revalidate, you can set revalidate to \`false\` (only ran once at build-time).` +
`\nTo revalidate as soon as possible, you can set the value to \`1\`.`
)
} else if (data.revalidate > 31536000) {
// if it's greater than a year for some reason error
console.warn(
`Warning: A page's revalidate option was set to more than a year. This may have been done in error.` +
`\nTo only run getStaticProps at build-time and not revalidate at runtime, you can set \`revalidate\` to \`false\`!`
)
}
} else if (data.revalidate === true) {
// When enabled, revalidate after 1 second. This value is optimal for
// the most up-to-date page possible, but without a 1-to-1
// request-refresh ratio.
data.revalidate = 1
} else {
// By default, we never revalidate.
data.revalidate = false
}
props.pageProps = Object.assign({}, props.pageProps, data.props)
// pass up revalidate and props for export
// TODO: change this to a different passing mechanism
;(renderOpts as any).revalidate = data.revalidate
;(renderOpts as any).pageData = props
}
if (getServerSideProps) {
props[SERVER_PROPS_ID] = true
}
if (getServerSideProps && !isFallback) {
let data: UnwrapPromise<ReturnType<GetServerSideProps>>
try {
data = await getServerSideProps({
req,
res,
query,
...(pageIsDynamic ? { params: params as ParsedUrlQuery } : undefined),
...(previewData !== false
? { preview: true, previewData: previewData }
: undefined),
})
} catch (serverSidePropsError) {
// remove not found error code to prevent triggering legacy
// 404 rendering
if (serverSidePropsError.code === 'ENOENT') {
delete serverSidePropsError.code
}
throw serverSidePropsError
}
if (data == null) {
throw new Error(GSSP_NO_RETURNED_VALUE)
}
const invalidKeys = Object.keys(data).filter(
(key) => key !== 'props' && key !== 'unstable_redirect'
)
if (invalidKeys.length) {
throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys))
}
if (
data.unstable_redirect &&
typeof data.unstable_redirect === 'object'
) {
checkRedirectValues(data.unstable_redirect, req)
if (isDataReq) {
data.props = {
__N_REDIRECT: data.unstable_redirect.destination,
}
} else {
handleRedirect(res, data.unstable_redirect)
return null
}
}
if (
(dev || isBuildTimeSSG) &&
!isSerializableProps(pathname, 'getServerSideProps', data.props)
) {
// this fn should throw an error instead of ever returning `false`
throw new Error(
'invariant: getServerSideProps did not return valid props. Please report this.'
)
}
props.pageProps = Object.assign({}, props.pageProps, data.props)
;(renderOpts as any).pageData = props
}
} catch (dataFetchError) {
if (isDataReq || !dev || !dataFetchError) throw dataFetchError
ctx.err = dataFetchError
renderOpts.err = dataFetchError
console.error(dataFetchError)
}
if (
!isSSG && // we only show this warning for legacy pages
!getServerSideProps &&
process.env.NODE_ENV !== 'production' &&
Object.keys(props?.pageProps || {}).includes('url')
) {
console.warn(
`The prop \`url\` is a reserved prop in Next.js for legacy reasons and will be overridden on page ${pathname}\n` +
`See more info here: https://err.sh/vercel/next.js/reserved-page-prop`
)
}
// We only need to do this if we want to support calling
// _app's getInitialProps for getServerSideProps if not this can be removed
if (isDataReq && !isSSG) return props
// We don't call getStaticProps or getServerSideProps while generating
// the fallback so make sure to set pageProps to an empty object
if (isFallback) {
props.pageProps = {}
}
// the response might be finished on the getInitialProps call
if (isResSent(res) && !isSSG) return null
// we preload the buildManifest for auto-export dynamic pages
// to speed up hydrating query values
let filteredBuildManifest = buildManifest
if (isAutoExport && pageIsDynamic) {
const page = denormalizePagePath(normalizePagePath(pathname))
// This code would be much cleaner using `immer` and directly pushing into
// the result from `getPageFiles`, we could maybe consider that in the
// future.
if (page in filteredBuildManifest.pages) {
filteredBuildManifest = {
...filteredBuildManifest,
pages: {
...filteredBuildManifest.pages,
[page]: [
...filteredBuildManifest.pages[page],
...filteredBuildManifest.lowPriorityFiles.filter((f) =>
f.includes('_buildManifest')
),
],
},
lowPriorityFiles: filteredBuildManifest.lowPriorityFiles.filter(
(f) => !f.includes('_buildManifest')
),
}
}
}
const renderPage: RenderPage = (
options: ComponentsEnhancer = {}
): { html: string; head: any } => {
if (ctx.err && ErrorDebug) {
return { html: renderToString(<ErrorDebug error={ctx.err} />), head }
}
if (dev && (props.router || props.Component)) {
throw new Error(
`'router' and 'Component' can not be returned in getInitialProps from _app.js https://err.sh/vercel/next.js/cant-override-next-props`
)
}
const {
App: EnhancedApp,
Component: EnhancedComponent,
} = enhanceComponents(options, App, Component)
const html = renderToString(
<AppContainer>
<EnhancedApp Component={EnhancedComponent} router={router} {...props} />
</AppContainer>
)
return { html, head }
}
const documentCtx = { ...ctx, renderPage }
const docProps: DocumentInitialProps = await loadGetInitialProps(
Document,
documentCtx
)
// the response might be finished on the getInitialProps call
if (isResSent(res) && !isSSG) return null
if (!docProps || typeof docProps.html !== 'string') {
const message = `"${getDisplayName(
Document
)}.getInitialProps()" should resolve to an object with a "html" prop set with a valid html string`
throw new Error(message)
}
const dynamicImportIdsSet = new Set<string>()
const dynamicImports: ManifestItem[] = []
for (const mod of reactLoadableModules) {
const manifestItem: ManifestItem[] = reactLoadableManifest[mod]
if (manifestItem) {
manifestItem.forEach((item) => {
dynamicImports.push(item)
dynamicImportIdsSet.add(item.id as string)
})
}
}
const dynamicImportsIds = [...dynamicImportIdsSet]
const hybridAmp = ampState.hybrid
// update renderOpts so export knows current state
renderOpts.inAmpMode = inAmpMode
renderOpts.hybridAmp = hybridAmp
const docComponentsRendered: DocumentProps['docComponentsRendered'] = {}
let html = renderDocument(Document, {
...renderOpts,
docComponentsRendered,
buildManifest: filteredBuildManifest,
// Only enabled in production as development mode has features relying on HMR (style injection for example)
unstable_runtimeJS:
process.env.NODE_ENV === 'production'
? pageConfig.unstable_runtimeJS
: undefined,
dangerousAsPath: router.asPath,
ampState,
props,
headTags: await headTags(documentCtx),
isFallback,
docProps,
pathname,
ampPath,
query,
inAmpMode,
hybridAmp,
dynamicImportsIds,
dynamicImports,
gsp: !!getStaticProps ? true : undefined,
gssp: !!getServerSideProps ? true : undefined,
gip: hasPageGetInitialProps ? true : undefined,
appGip: !defaultAppGetInitialProps ? true : undefined,
devOnlyCacheBusterQueryString,
})
if (process.env.NODE_ENV !== 'production') {
const nonRenderedComponents = []
const expectedDocComponents = ['Main', 'Head', 'NextScript', 'Html']
for (const comp of expectedDocComponents) {
if (!(docComponentsRendered as any)[comp]) {
nonRenderedComponents.push(comp)
}
}
const plural = nonRenderedComponents.length !== 1 ? 's' : ''
if (nonRenderedComponents.length) {
console.warn(
`Expected Document Component${plural} ${nonRenderedComponents.join(
', '
)} ${
plural ? 'were' : 'was'
} not rendered. Make sure you render them in your custom \`_document\`\n` +
`See more info here https://err.sh/next.js/missing-document-component`
)
}
}
if (inAmpMode && html) {
// inject HTML to AMP_RENDER_TARGET to allow rendering
// directly to body in AMP mode
const ampRenderIndex = html.indexOf(AMP_RENDER_TARGET)
html =
html.substring(0, ampRenderIndex) +
`<!-- __NEXT_DATA__ -->${docProps.html}` +
html.substring(ampRenderIndex + AMP_RENDER_TARGET.length)
html = await optimizeAmp(html, renderOpts.ampOptimizerConfig)
if (!renderOpts.ampSkipValidation && renderOpts.ampValidator) {
await renderOpts.ampValidator(html, pathname)
}
}
html = await postProcess(
html,
{
getFontDefinition,
},
{
optimizeFonts: renderOpts.optimizeFonts,
optimizeImages: renderOpts.optimizeImages,
}
)
if (inAmpMode || hybridAmp) {
// fix &amp being escaped for amphtml rel link
html = html.replace(/&amp;amp=1/g, '&amp=1')
}
return html
}
function errorToJSON(err: Error): Error {
const { name, message, stack } = err
return { name, message, stack }
}
function serializeError(
dev: boolean | undefined,
err: Error
): Error & { statusCode?: number } {
if (dev) {
return errorToJSON(err)
}
return {
name: 'Internal Server Error.',
message: '500 - Internal Server Error.',
statusCode: 500,
}
}