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>/) + }) + }) +})