From e8ca334d424c4939b73d22eba19d925db73fc66f Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 19 Nov 2021 13:41:19 -0600 Subject: [PATCH] Fix non-concurrent function _document (#31628) This ensures functional `_document` is rendered correctly when not using concurrent mode. ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` Fixes: https://github.com/vercel/next.js/issues/31593 x-ref: https://github.com/vercel/next.js/pull/30156 --- packages/next/server/render.tsx | 54 +++++++++++++------ test/e2e/next-head/app/components/meta.js | 12 +++++ test/e2e/next-head/app/pages/_document.js | 13 +++++ test/e2e/next-head/app/pages/index.js | 15 ++++++ test/e2e/next-head/index.test.ts | 41 ++++++++++++++ .../{ => app}/lib/context.js | 0 .../app/next.config.js | 19 +++++++ .../app/package.json | 12 +++++ .../{ => app}/pages/_document.js | 0 .../{ => app}/pages/index.js | 0 .../tests/index.test.js | 5 +- 11 files changed, 153 insertions(+), 18 deletions(-) create mode 100644 test/e2e/next-head/app/components/meta.js create mode 100644 test/e2e/next-head/app/pages/_document.js create mode 100644 test/e2e/next-head/app/pages/index.js create mode 100644 test/e2e/next-head/index.test.ts rename test/integration/document-functional-render-prop/{ => app}/lib/context.js (100%) create mode 100644 test/integration/document-functional-render-prop/app/next.config.js create mode 100644 test/integration/document-functional-render-prop/app/package.json rename test/integration/document-functional-render-prop/{ => app}/pages/_document.js (100%) rename test/integration/document-functional-render-prop/{ => app}/pages/index.js (100%) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 91fdb62bd3699b6..c5af9cdc527e9ec 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -1148,30 +1148,52 @@ export async function renderToHTML( styles: docProps.styles, } } else { - const bodyResult = async () => { - const content = ( - - {ctx.err && ErrorDebug ? ( - + let bodyResult + + if (concurrentFeatures) { + bodyResult = async () => { + // this must be called inside bodyResult so appWrappers is + // up to date when getWrappedApp is called + const content = + ctx.err && ErrorDebug ? ( + + + ) : ( - getWrappedApp( - - ) - )} - - ) - - return concurrentFeatures - ? process.browser + + {getWrappedApp( + + )} + + ) + return process.browser ? await renderToWebStream(content) : await renderToNodeStream(content, generateStaticHTML) - : piperFromArray([ReactDOMServer.renderToString(content)]) + } + } else { + const content = + ctx.err && ErrorDebug ? ( + + + + ) : ( + + {getWrappedApp( + + )} + + ) + // for non-concurrent rendering we need to ensure App is rendered + // before _document so that updateHead is called/collected before + // rendering _document's head + const result = piperFromArray([ReactDOMServer.renderToString(content)]) + bodyResult = () => result } return { bodyResult, documentElement: () => (Document as any)(), - useMainContent: (fn?: (content: JSX.Element) => JSX.Element) => { + useMainContent: (fn?: (_content: JSX.Element) => JSX.Element) => { if (fn) { appWrappers.push(fn) } diff --git a/test/e2e/next-head/app/components/meta.js b/test/e2e/next-head/app/components/meta.js new file mode 100644 index 000000000000000..3a121375fd3f60b --- /dev/null +++ b/test/e2e/next-head/app/components/meta.js @@ -0,0 +1,12 @@ +import Head from 'next/head' + +export function Meta(props) { + return ( + <> + + + + + + ) +} diff --git a/test/e2e/next-head/app/pages/_document.js b/test/e2e/next-head/app/pages/_document.js new file mode 100644 index 000000000000000..7ee4a282756f025 --- /dev/null +++ b/test/e2e/next-head/app/pages/_document.js @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from 'next/document' + +export default function MyDocument() { + return ( + + + +
+ + + + ) +} diff --git a/test/e2e/next-head/app/pages/index.js b/test/e2e/next-head/app/pages/index.js new file mode 100644 index 000000000000000..c887c8738691356 --- /dev/null +++ b/test/e2e/next-head/app/pages/index.js @@ -0,0 +1,15 @@ +import Head from 'next/head' +import { Meta } from '../components/meta' + +export default function Page(props) { + return ( + <> + + + + + +

index page

+ + ) +} diff --git a/test/e2e/next-head/index.test.ts b/test/e2e/next-head/index.test.ts new file mode 100644 index 000000000000000..479a67a63bc7ada --- /dev/null +++ b/test/e2e/next-head/index.test.ts @@ -0,0 +1,41 @@ +import { createNext, FileRef } from 'e2e-utils' +import { renderViaHTTP } from 'next-test-utils' +import cheerio from 'cheerio' +import webdriver from 'next-webdriver' +import { NextInstance } from 'test/lib/next-modes/base' +import { join } from 'path' + +describe('should set-up next', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef(join(__dirname, 'app/pages')), + components: new FileRef(join(__dirname, 'app/components')), + }, + }) + }) + afterAll(() => next.destroy()) + + it('should have correct head tags in initial document', async () => { + const html = await renderViaHTTP(next.url, '/') + const $ = cheerio.load(html) + + for (let i = 1; i < 5; i++) { + expect($(`meta[name="test-head-${i}"]`).attr()['content']).toBe('hello') + } + }) + + it('should have correct head tags after hydration', async () => { + const browser = await webdriver(next.url, '/') + + for (let i = 1; i < 5; i++) { + expect( + await browser + .elementByCss(`meta[name="test-head-${i}"]`) + .getAttribute('content') + ).toBe('hello') + } + }) +}) diff --git a/test/integration/document-functional-render-prop/lib/context.js b/test/integration/document-functional-render-prop/app/lib/context.js similarity index 100% rename from test/integration/document-functional-render-prop/lib/context.js rename to test/integration/document-functional-render-prop/app/lib/context.js diff --git a/test/integration/document-functional-render-prop/app/next.config.js b/test/integration/document-functional-render-prop/app/next.config.js new file mode 100644 index 000000000000000..a866ec0085c1013 --- /dev/null +++ b/test/integration/document-functional-render-prop/app/next.config.js @@ -0,0 +1,19 @@ +module.exports = { + experimental: { + reactRoot: true, + concurrentFeatures: true, + }, + webpack(config) { + const { alias } = config.resolve + // FIXME: resolving react/jsx-runtime https://github.com/facebook/react/issues/20235 + alias['react/jsx-dev-runtime'] = 'react/jsx-dev-runtime.js' + alias['react/jsx-runtime'] = 'react/jsx-runtime.js' + + // Use react 18 + alias['react'] = 'react-18' + alias['react-dom'] = 'react-dom-18' + alias['react-dom/server'] = 'react-dom-18/server' + + return config + }, +} diff --git a/test/integration/document-functional-render-prop/app/package.json b/test/integration/document-functional-render-prop/app/package.json new file mode 100644 index 000000000000000..f9dafc993a79cae --- /dev/null +++ b/test/integration/document-functional-render-prop/app/package.json @@ -0,0 +1,12 @@ +{ + "scripts": { + "next": "node -r ../test/require-hook.js ../../../../packages/next/dist/bin/next", + "dev": "yarn next dev", + "build": "yarn next build", + "start": "yarn next start" + }, + "dependencies": { + "react": "*", + "react-dom": "*" + } +} diff --git a/test/integration/document-functional-render-prop/pages/_document.js b/test/integration/document-functional-render-prop/app/pages/_document.js similarity index 100% rename from test/integration/document-functional-render-prop/pages/_document.js rename to test/integration/document-functional-render-prop/app/pages/_document.js diff --git a/test/integration/document-functional-render-prop/pages/index.js b/test/integration/document-functional-render-prop/app/pages/index.js similarity index 100% rename from test/integration/document-functional-render-prop/pages/index.js rename to test/integration/document-functional-render-prop/app/pages/index.js diff --git a/test/integration/document-functional-render-prop/tests/index.test.js b/test/integration/document-functional-render-prop/tests/index.test.js index d7cb027d41f7edb..c843082feed935e 100644 --- a/test/integration/document-functional-render-prop/tests/index.test.js +++ b/test/integration/document-functional-render-prop/tests/index.test.js @@ -3,7 +3,8 @@ import { join } from 'path' import { findPort, launchApp, killApp, renderViaHTTP } from 'next-test-utils' -const appDir = join(__dirname, '..') +const nodeArgs = ['-r', join(__dirname, '../../react-18/test/require-hook.js')] +const appDir = join(__dirname, '../app') let appPort let app @@ -11,7 +12,7 @@ describe('Functional Custom Document', () => { describe('development mode', () => { beforeAll(async () => { appPort = await findPort() - app = await launchApp(appDir, appPort) + app = await launchApp(appDir, appPort, { nodeArgs }) }) afterAll(() => killApp(app))