diff --git a/bench/nested-deps/next.config.js b/bench/nested-deps/next.config.js
index d06b18cf4e29c..004e6c18198b6 100644
--- a/bench/nested-deps/next.config.js
+++ b/bench/nested-deps/next.config.js
@@ -1,9 +1,9 @@
+const idx = process.execArgv.indexOf('--cpu-prof')
+if (idx >= 0) process.execArgv.splice(idx, 1)
+
module.exports = {
eslint: {
ignoreDuringBuilds: true,
},
- experimental: {
- swcLoader: true,
- swcMinify: true,
- },
+ swcMinify: true,
}
diff --git a/bench/nested-deps/package.json b/bench/nested-deps/package.json
index d025ab2b2ad3c..ad48206bed2d1 100644
--- a/bench/nested-deps/package.json
+++ b/bench/nested-deps/package.json
@@ -1,11 +1,11 @@
{
"scripts": {
- "prepare": "rm -rf components && mkdir components && node ./fuzzponent.js -d 2 -s 206 -o components",
- "dev": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 ../../node_modules/.bin/next dev",
- "build": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 ../../node_modules/.bin/next build",
- "start": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 ../../node_modules/.bin/next start",
- "dev-nocache": "rm -rf .next && yarn dev",
- "dev-cpuprofile-nocache": "rm -rf .next && cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 node --cpu-prof ../../node_modules/.bin/next",
- "build-nocache": "rm -rf .next && yarn build"
+ "prepare": "rimraf components && mkdir components && node ./fuzzponent.js -d 2 -s 206 -o components",
+ "dev": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 node ../../node_modules/next/dist/bin/next dev",
+ "build": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 node ../../node_modules/next/dist/bin/next build",
+ "start": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 node ../../node_modules/next/dist/bin/next start",
+ "dev-nocache": "rimraf .next && yarn dev",
+ "dev-cpuprofile-nocache": "rimraf .next && cross-env NEXT_PRIVATE_LOCAL_WEBPACK5=1 node --cpu-prof ../../node_modules/next/dist/bin/next",
+ "build-nocache": "rimraf .next && yarn build"
}
}
diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts
index be2f0b3e8236e..e2c478c0e834c 100644
--- a/packages/next/build/entries.ts
+++ b/packages/next/build/entries.ts
@@ -65,7 +65,7 @@ export function createPagesMapping(
// we alias these in development and allow webpack to
// allow falling back to the correct source file so
// that HMR can work properly when a file is added/removed
- const documentPage = `_document${hasServerComponents ? '.web' : ''}`
+ const documentPage = `_document${hasServerComponents ? '-web' : ''}`
if (isDev) {
pages['/_app'] = `${PAGES_DIR_ALIAS}/_app`
pages['/_error'] = `${PAGES_DIR_ALIAS}/_error`
diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index d67f9cc3d8f94..b3288143b2011 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -629,7 +629,7 @@ export default async function getBaseWebpackConfig(
prev.push(path.join(pagesDir, `_document.${ext}`))
return prev
}, [] as string[]),
- `next/dist/pages/_document${hasServerComponents ? '.web' : ''}.js`,
+ `next/dist/pages/_document${hasServerComponents ? '-web' : ''}.js`,
]
}
diff --git a/packages/next/pages/_document.web.tsx b/packages/next/pages/_document-web.tsx
similarity index 85%
rename from packages/next/pages/_document.web.tsx
rename to packages/next/pages/_document-web.tsx
index 92c6462ae923e..2442154129350 100644
--- a/packages/next/pages/_document.web.tsx
+++ b/packages/next/pages/_document-web.tsx
@@ -1,3 +1,5 @@
+// Default _document page for web runtime
+
import React from 'react'
import { Html, Head, Main, NextScript } from './_document'
diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx
index a1c855bef1d70..a69b919b16709 100644
--- a/packages/next/pages/_document.tsx
+++ b/packages/next/pages/_document.tsx
@@ -1,8 +1,5 @@
import React, { Component, ReactElement, ReactNode, useContext } from 'react'
-import {
- BODY_RENDER_TARGET,
- OPTIMIZED_FONT_PROVIDERS,
-} from '../shared/lib/constants'
+import { OPTIMIZED_FONT_PROVIDERS } from '../shared/lib/constants'
import {
DocumentContext,
DocumentInitialProps,
@@ -763,13 +760,18 @@ export class Head extends Component<
}
}
-export function Main() {
- const { inAmpMode, docComponentsRendered } = useContext(HtmlContext)
-
+export function Main({
+ children,
+}: {
+ children?: (content: JSX.Element) => JSX.Element
+}) {
+ const { inAmpMode, docComponentsRendered, useMainContent } =
+ useContext(HtmlContext)
+ const content = useMainContent(children)
docComponentsRendered.Main = true
- if (inAmpMode) return <>{BODY_RENDER_TARGET}>
- return
{BODY_RENDER_TARGET}
+ if (inAmpMode) return content
+ return {content}
}
export class NextScript extends Component {
diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx
index 53fa578287d73..fd44dbc070cef 100644
--- a/packages/next/server/render.tsx
+++ b/packages/next/server/render.tsx
@@ -20,7 +20,6 @@ import { GetServerSideProps, GetStaticProps, PreviewData } from '../types'
import { isInAmpMode } from '../shared/lib/amp'
import { AmpStateContext } from '../shared/lib/amp-context'
import {
- BODY_RENDER_TARGET,
SERVER_PROPS_ID,
STATIC_PROPS_ID,
STATIC_STATUS_PAGES,
@@ -935,6 +934,20 @@ export async function renderToHTML(
}
}
+ const appWrappers: Array<(content: JSX.Element) => JSX.Element> = []
+ const getWrappedApp = (app: JSX.Element) => {
+ // Prevent wrappers from reading/writing props by rendering inside an
+ // opaque component. Wrappers should use context instead.
+ const InnerApp = () => app
+ return (
+
+ {appWrappers.reduce((innerContent, fn) => {
+ return fn(innerContent)
+ }, )}
+
+ )
+ }
+
/**
* Rules of Static & Dynamic HTML:
*
@@ -976,13 +989,13 @@ export async function renderToHTML(
enhanceComponents(options, App, Component)
const html = ReactDOMServer.renderToString(
-
+ getWrappedApp(
-
+ )
)
return { html, head }
}
@@ -1002,33 +1015,51 @@ export async function renderToHTML(
}
return {
- bodyResult: piperFromArray([docProps.html]),
+ bodyResult: () => piperFromArray([docProps.html]),
documentElement: (htmlProps: HtmlProps) => (
),
+ useMainContent: (fn?: (content: JSX.Element) => JSX.Element) => {
+ if (fn) {
+ throw new Error(
+ 'The `children` property is not supported by non-functional custom Document components'
+ )
+ }
+ // @ts-ignore
+ return
+ },
head: docProps.head,
headTags: await headTags(documentCtx),
styles: docProps.styles,
}
} else {
- const content =
- ctx.err && ErrorDebug ? (
-
- ) : (
-
-
-
- )
+ const bodyResult = async () => {
+ const content =
+ ctx.err && ErrorDebug ? (
+
+ ) : (
+ getWrappedApp(
+
+ )
+ )
- const bodyResult = concurrentFeatures
- ? process.browser
- ? await renderToReadableStream(content)
- : await renderToNodeStream(content, generateStaticHTML)
- : piperFromArray([ReactDOMServer.renderToString(content)])
+ return concurrentFeatures
+ ? process.browser
+ ? await renderToReadableStream(content)
+ : await renderToNodeStream(content, generateStaticHTML)
+ : piperFromArray([ReactDOMServer.renderToString(content)])
+ }
return {
bodyResult,
documentElement: () => (Document as any)(),
+ useMainContent: (fn?: (content: JSX.Element) => JSX.Element) => {
+ if (fn) {
+ appWrappers.push(fn)
+ }
+ // @ts-ignore
+ return
+ },
head,
headTags: [],
styles: jsxStyleRegistry.styles(),
@@ -1056,8 +1087,8 @@ export async function renderToHTML(
}
const hybridAmp = ampState.hybrid
-
const docComponentsRendered: DocumentProps['docComponentsRendered'] = {}
+
const {
assetPrefix,
buildId,
@@ -1123,6 +1154,7 @@ export async function renderToHTML(
head: documentResult.head,
headTags: documentResult.headTags,
styles: documentResult.styles,
+ useMainContent: documentResult.useMainContent,
useMaybeDeferContent,
}
@@ -1181,20 +1213,20 @@ export async function renderToHTML(
}
}
- const renderTargetIdx = documentHTML.indexOf(BODY_RENDER_TARGET)
+ const [renderTargetPrefix, renderTargetSuffix] = documentHTML.split(
+ /<\/next-js-internal-body-render-target>/
+ )
const prefix: Array = []
prefix.push('')
- prefix.push(documentHTML.substring(0, renderTargetIdx))
+ prefix.push(renderTargetPrefix)
if (inAmpMode) {
prefix.push('')
}
let pipers: Array = [
piperFromArray(prefix),
- documentResult.bodyResult,
- piperFromArray([
- documentHTML.substring(renderTargetIdx + BODY_RENDER_TARGET.length),
- ]),
+ await documentResult.bodyResult(),
+ piperFromArray([renderTargetSuffix]),
]
const postProcessors: Array<((html: string) => Promise) | null> = (
diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts
index e181c24d03ecf..ba3a5f7e93c9e 100644
--- a/packages/next/shared/lib/constants.ts
+++ b/packages/next/shared/lib/constants.ts
@@ -23,7 +23,6 @@ export const BLOCKED_PAGES = ['/_document', '/_app', '/_error']
export const CLIENT_PUBLIC_FILES_PATH = 'public'
export const CLIENT_STATIC_FILES_PATH = 'static'
export const CLIENT_STATIC_FILES_RUNTIME = 'runtime'
-export const BODY_RENDER_TARGET = '__NEXT_BODY_RENDER_TARGET__'
export const STRING_LITERAL_DROP_BUNDLE = '__NEXT_DROP_CLIENT_FILE__'
// server/middleware-flight-manifest.js
diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts
index 3582203816499..a74499fdba9b8 100644
--- a/packages/next/shared/lib/utils.ts
+++ b/packages/next/shared/lib/utils.ts
@@ -221,6 +221,7 @@ export type HtmlProps = {
styles?: React.ReactElement[] | React.ReactFragment
head?: Array
useMaybeDeferContent: MaybeDeferContentHook
+ useMainContent: (fn?: (content: JSX.Element) => JSX.Element) => JSX.Element
}
/**
diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js
index acd995ed7b17c..cd5c74b37d42a 100644
--- a/packages/next/taskfile.js
+++ b/packages/next/taskfile.js
@@ -1123,7 +1123,7 @@ export async function pages_document(task, opts) {
export async function pages_document_server(task, opts) {
await task
- .source('pages/_document.web.tsx')
+ .source('pages/_document-web.tsx')
.swc('client', { dev: opts.dev })
.target('dist/pages')
}
diff --git a/test/integration/document-functional-render-prop/lib/context.js b/test/integration/document-functional-render-prop/lib/context.js
new file mode 100644
index 0000000000000..229e13653eb1c
--- /dev/null
+++ b/test/integration/document-functional-render-prop/lib/context.js
@@ -0,0 +1,3 @@
+import { createContext } from 'react'
+
+export default createContext(null)
diff --git a/test/integration/document-functional-render-prop/pages/_document.js b/test/integration/document-functional-render-prop/pages/_document.js
new file mode 100644
index 0000000000000..6f7199c4e6d00
--- /dev/null
+++ b/test/integration/document-functional-render-prop/pages/_document.js
@@ -0,0 +1,20 @@
+import { Html, Head, Main, NextScript } from 'next/document'
+import Context from '../lib/context'
+
+export default function Document() {
+ return (
+
+
+
+
+ {(children) => (
+
+ {children}
+
+ )}
+
+
+
+
+ )
+}
diff --git a/test/integration/document-functional-render-prop/pages/index.js b/test/integration/document-functional-render-prop/pages/index.js
new file mode 100644
index 0000000000000..d00fe7fc70a6b
--- /dev/null
+++ b/test/integration/document-functional-render-prop/pages/index.js
@@ -0,0 +1,7 @@
+import { useContext } from 'react'
+import Context from '../lib/context'
+
+export default function MainRenderProp() {
+ const value = useContext(Context)
+ return {value}
+}
diff --git a/test/integration/document-functional-render-prop/tests/index.test.js b/test/integration/document-functional-render-prop/tests/index.test.js
new file mode 100644
index 0000000000000..d7cb027d41f7e
--- /dev/null
+++ b/test/integration/document-functional-render-prop/tests/index.test.js
@@ -0,0 +1,24 @@
+/* eslint-env jest */
+
+import { join } from 'path'
+import { findPort, launchApp, killApp, renderViaHTTP } from 'next-test-utils'
+
+const appDir = join(__dirname, '..')
+let appPort
+let app
+
+describe('Functional Custom Document', () => {
+ describe('development mode', () => {
+ beforeAll(async () => {
+ appPort = await findPort()
+ app = await launchApp(appDir, appPort)
+ })
+
+ afterAll(() => killApp(app))
+
+ it('supports render props', async () => {
+ const html = await renderViaHTTP(appPort, '/')
+ expect(html).toMatch(/from render prop<\/span>/)
+ })
+ })
+})