diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts index 6dc7e46ce0e3..353ccb3f70d9 100644 --- a/packages/next/server/node-web-streams-helper.ts +++ b/packages/next/server/node-web-streams-helper.ts @@ -1,5 +1,9 @@ import { nonNullable } from '../lib/non-nullable' +export type ReactReadableStream = ReadableStream & { + allReady?: Promise | undefined +} + export function readableStreamTee( readable: ReadableStream ): [ReadableStream, ReadableStream] { @@ -138,29 +142,24 @@ export function renderToInitialStream({ }: { ReactDOMServer: any element: React.ReactElement -}): Promise< - ReadableStream & { - allReady?: Promise - } -> { +}): Promise { return ReactDOMServer.renderToReadableStream(element) } -export async function continueFromInitialStream({ - suffix, - dataStream, - generateStaticHTML, - flushEffectHandler, - renderStream, -}: { - suffix?: string - dataStream?: ReadableStream - generateStaticHTML: boolean - flushEffectHandler?: () => string - renderStream: ReadableStream & { - allReady?: Promise +export async function continueFromInitialStream( + renderStream: ReactReadableStream, + { + suffix, + dataStream, + generateStaticHTML, + flushEffectHandler, + }: { + suffix?: string + dataStream?: ReadableStream + generateStaticHTML: boolean + flushEffectHandler?: () => string } -}): Promise> { +): Promise> { const closeTag = '' const suffixUnclosed = suffix ? suffix.split(closeTag)[0] : null @@ -198,12 +197,11 @@ export async function renderToStream({ flushEffectHandler?: () => string }): Promise> { const renderStream = await renderToInitialStream({ ReactDOMServer, element }) - return continueFromInitialStream({ + return continueFromInitialStream(renderStream, { suffix, dataStream, generateStaticHTML, flushEffectHandler, - renderStream, }) } diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index ca0b4c5111b3..fb2e9f9e3639 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -20,6 +20,7 @@ import type { FontManifest } from './font-utils' import type { LoadComponentsReturnType, ManifestItem } from './load-components' import type { GetServerSideProps, GetStaticProps, PreviewData } from '../types' import type { UnwrapPromise } from '../lib/coalesced-function' +import type { ReactReadableStream } from './node-web-streams-helper' import React from 'react' import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack' @@ -1340,11 +1341,11 @@ export async function renderToHTML( } } - async function documentInitialProps( + async function loadDocumentInitialProps( renderShell?: ( _App: AppType, _Component: NextComponentType - ) => Promise + ) => Promise ) { const renderPage: RenderPage = ( options: ComponentsEnhancer = {} @@ -1373,11 +1374,13 @@ export async function renderToHTML( enhanceComponents(options, App, Component) if (renderShell) { - return renderShell(EnhancedApp, EnhancedComponent).then(() => { - // When using concurrent features, we don't have or need the full - // html so it's fine to return nothing here. - return { html: '', head } - }) + return renderShell(EnhancedApp, EnhancedComponent).then( + async (stream) => { + const forwardStream = readableStreamTee(stream)[1] + const html = await streamToString(forwardStream) + return { html, head } + } + ) } const html = ReactDOMServer.renderToString( @@ -1438,9 +1441,9 @@ export async function renderToHTML( if (!process.env.__NEXT_REACT_ROOT) { // Enabling react legacy rendering mode: __NEXT_REACT_ROOT = false if (Document.getInitialProps) { - const documentInitialPropsRes = await documentInitialProps() - if (documentInitialPropsRes === null) return null - const { docProps, documentCtx } = documentInitialPropsRes + const documentInitialProps = await loadDocumentInitialProps() + if (documentInitialProps === null) return null + const { docProps, documentCtx } = documentInitialProps return { bodyResult: (suffix: string) => @@ -1473,80 +1476,75 @@ export async function renderToHTML( } } else { // Enabling react concurrent rendering mode: __NEXT_REACT_ROOT = true - let renderStream: ReadableStream & { - allReady?: Promise | undefined - } - const renderShell = async ( EnhancedApp: AppType, EnhancedComponent: NextComponentType ) => { const content = renderContent(EnhancedApp, EnhancedComponent) - renderStream = await renderToInitialStream({ + return await renderToInitialStream({ ReactDOMServer, element: content, }) } - const bodyResult = async (suffix: string) => { - // this must be called inside bodyResult so appWrappers is - // up to date when `wrapApp` is called - - const flushEffectHandler = (): string => { - const allFlushEffects = [ - styledJsxFlushEffect, - ...(flushEffects || []), - ] - const flushed = ReactDOMServer.renderToString( - <> - {allFlushEffects.map((flushEffect, i) => ( - {flushEffect()} - ))} - - ) - return flushed - } + const createBodyResult = + (initialStream: ReactReadableStream) => (suffix: string) => { + // this must be called inside bodyResult so appWrappers is + // up to date when `wrapApp` is called + const flushEffectHandler = (): string => { + const allFlushEffects = [ + styledJsxFlushEffect, + ...(flushEffects || []), + ] + const flushed = ReactDOMServer.renderToString( + <> + {allFlushEffects.map((flushEffect, i) => ( + {flushEffect()} + ))} + + ) + return flushed + } - // Handle static data for server components. - async function generateStaticFlightDataIfNeeded() { - if (serverComponentsPageDataTransformStream) { - // If it's a server component with the Node.js runtime, we also - // statically generate the page data. - let data = '' - - const readable = serverComponentsPageDataTransformStream.readable - const reader = readable.getReader() - const textDecoder = new TextDecoder() - - while (true) { - const { done, value } = await reader.read() - if (done) { - break + // Handle static data for server components. + async function generateStaticFlightDataIfNeeded() { + if (serverComponentsPageDataTransformStream) { + // If it's a server component with the Node.js runtime, we also + // statically generate the page data. + let data = '' + + const readable = serverComponentsPageDataTransformStream.readable + const reader = readable.getReader() + const textDecoder = new TextDecoder() + + while (true) { + const { done, value } = await reader.read() + if (done) { + break + } + data += decodeText(value, textDecoder) } - data += decodeText(value, textDecoder) - } - ;(renderOpts as any).pageData = { - ...(renderOpts as any).pageData, - __flight__: data, + ;(renderOpts as any).pageData = { + ...(renderOpts as any).pageData, + __flight__: data, + } + return data } - return data } - } - // @TODO: A potential improvement would be to reuse the inlined - // data stream, or pass a callback inside as this doesn't need to - // be streamed. - // Do not use `await` here. - generateStaticFlightDataIfNeeded() - return await continueFromInitialStream({ - renderStream, - suffix, - dataStream: serverComponentsInlinedTransformStream?.readable, - generateStaticHTML, - flushEffectHandler, - }) - } + // @TODO: A potential improvement would be to reuse the inlined + // data stream, or pass a callback inside as this doesn't need to + // be streamed. + // Do not use `await` here. + generateStaticFlightDataIfNeeded() + return continueFromInitialStream(initialStream, { + suffix, + dataStream: serverComponentsInlinedTransformStream?.readable, + generateStaticHTML, + flushEffectHandler, + }) + } const hasDocumentGetInitialProps = !( isServerComponent || @@ -1554,16 +1552,21 @@ export async function renderToHTML( !Document.getInitialProps ) + let bodyResult: (s: string) => Promise> + // If it has getInitialProps, we will render the shell in `renderPage`. // Otherwise we do it right now. let documentInitialPropsRes: | {} - | Awaited> + | Awaited> if (hasDocumentGetInitialProps) { - documentInitialPropsRes = await documentInitialProps(renderShell) + documentInitialPropsRes = await loadDocumentInitialProps(renderShell) if (documentInitialPropsRes === null) return null + const { docProps } = documentInitialPropsRes as any + bodyResult = createBodyResult(streamFromArray([docProps.html])) } else { - await renderShell(App, Component) + const stream = await renderShell(App, Component) + bodyResult = createBodyResult(stream) documentInitialPropsRes = {} } diff --git a/packages/next/server/view-render.tsx b/packages/next/server/view-render.tsx index 8952c5f493aa..68c24818978e 100644 --- a/packages/next/server/view-render.tsx +++ b/packages/next/server/view-render.tsx @@ -506,8 +506,7 @@ export async function renderToHTML( // Do not use `await` here. // generateStaticFlightDataIfNeeded() - return await continueFromInitialStream({ - renderStream, + return await continueFromInitialStream(renderStream, { suffix: '', dataStream: serverComponentsInlinedTransformStream?.readable, generateStaticHTML: generateStaticHTML || !hasConcurrentFeatures, diff --git a/test/integration/amphtml-fragment-style/next.config.js b/test/integration/amphtml-fragment-style/next.config.js deleted file mode 100644 index 9c1c065bdfe3..000000000000 --- a/test/integration/amphtml-fragment-style/next.config.js +++ /dev/null @@ -1,2 +0,0 @@ -const path = require('path') -module.exports = require(path.join(__dirname, '../../lib/with-react-17.js'))({}) diff --git a/test/integration/amphtml-fragment-style/test/index.test.js b/test/integration/amphtml-fragment-style/test/index.test.js index 3bccdd030339..e7e83e705316 100644 --- a/test/integration/amphtml-fragment-style/test/index.test.js +++ b/test/integration/amphtml-fragment-style/test/index.test.js @@ -12,19 +12,14 @@ import { } from 'next-test-utils' const appDir = join(__dirname, '../') -const nodeArgs = ['-r', join(appDir, '../../lib/react-17-require-hook.js')] let appPort let app describe('AMP Fragment Styles', () => { beforeAll(async () => { - await nextBuild(appDir, [], { - nodeArgs, - }) + await nextBuild(appDir, []) appPort = await findPort() - app = await nextStart(appDir, appPort, { - nodeArgs, - }) + app = await nextStart(appDir, appPort) }) afterAll(() => killApp(app)) diff --git a/test/integration/app-document/pages/_document.js b/test/integration/app-document/pages/_document.js index e78e002ffcea..c6a9c36e4143 100644 --- a/test/integration/app-document/pages/_document.js +++ b/test/integration/app-document/pages/_document.js @@ -42,6 +42,7 @@ export default class MyDocument extends Document { return { ...result, + cssInJsCount: (result.html.match(/css-in-js-class/g) || []).length, customProperty: 'Hello Document', withCSP: ctx.query.withCSP, } @@ -74,6 +75,7 @@ export default class MyDocument extends Document {

Hello Document HMR

+
{this.props.cssInJsCount}
) diff --git a/test/integration/app-document/pages/index.js b/test/integration/app-document/pages/index.js index c4ab35053abe..f21ecbe1dd7a 100644 --- a/test/integration/app-document/pages/index.js +++ b/test/integration/app-document/pages/index.js @@ -2,8 +2,10 @@ import Link from 'next/link' export default () => (
index
+ about +
) diff --git a/test/integration/app-document/test/rendering.js b/test/integration/app-document/test/rendering.js index 541594eef04d..71514580fef7 100644 --- a/test/integration/app-document/test/rendering.js +++ b/test/integration/app-document/test/rendering.js @@ -22,6 +22,14 @@ export default function ({ app }, suiteName, render, fetch) { expect($('body').hasClass('custom_class')).toBe(true) }) + it('Document.getInitialProps returns html prop representing app shell', async () => { + // Extract css-in-js-class from the rendered HTML, which is returned by Document.getInitialProps + const $index = await get$('/') + const $about = await get$('/about') + expect($index('#css-in-cjs-count').text()).toBe('2') + expect($about('#css-in-cjs-count').text()).toBe('0') + }) + test('It injects custom head tags', async () => { const $ = await get$('/') expect($('head').text()).toMatch('body { margin: 0 }') diff --git a/test/integration/styled-jsx-module/app/next.config.js b/test/integration/styled-jsx-module/app/next.config.js deleted file mode 100644 index 40b089d571fe..000000000000 --- a/test/integration/styled-jsx-module/app/next.config.js +++ /dev/null @@ -1,4 +0,0 @@ -const path = require('path') -module.exports = require(path.join(__dirname, '../../../lib/with-react-17.js'))( - {} -) diff --git a/test/integration/styled-jsx-module/test/index.test.js b/test/integration/styled-jsx-module/test/index.test.js index 0243f07266eb..3bad07cb13c4 100644 --- a/test/integration/styled-jsx-module/test/index.test.js +++ b/test/integration/styled-jsx-module/test/index.test.js @@ -12,7 +12,6 @@ import { } from 'next-test-utils' const appDir = join(__dirname, '../app') -const nodeArgs = ['-r', join(appDir, '../../../lib/react-17-require-hook.js')] let appPort let app @@ -49,13 +48,9 @@ function runTests() { describe('styled-jsx using in node_modules', () => { describe('Production', () => { beforeAll(async () => { - await nextBuild(appDir, undefined, { - nodeArgs, - }) + await nextBuild(appDir) appPort = await findPort() - app = await nextStart(appDir, appPort, { - nodeArgs, - }) + app = await nextStart(appDir, appPort) }) afterAll(() => killApp(app)) @@ -65,9 +60,7 @@ describe('styled-jsx using in node_modules', () => { describe('Development', () => { beforeAll(async () => { appPort = await findPort() - app = await launchApp(appDir, appPort, { - nodeArgs, - }) + app = await launchApp(appDir, appPort) }) afterAll(() => killApp(app))