diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index fd045c7dcb6f8b6..25b1dffb72871ec 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -1268,12 +1268,17 @@ export async function renderToHTML( } } - // We make it a function component to enable streaming. - if (hasConcurrentFeatures && builtinDocument) { - Document = builtinDocument + if ((isServerComponent || process.browser) && Document.getInitialProps) { + if (builtinDocument) { + Document = builtinDocument + } else { + throw new Error( + '`getInitialProps` in Document component is not supported with React Server Components.' + ) + } } - if (!hasConcurrentFeatures && Document.getInitialProps) { + async function documentInitialProps() { const renderPage: RenderPage = ( options: ComponentsEnhancer = {} ): RenderPageResult | Promise => { @@ -1323,80 +1328,43 @@ export async function renderToHTML( throw new Error(message) } - return { - bodyResult: (suffix: string) => - streamFromArray([docProps.html, suffix]), - documentElement: (htmlProps: HtmlProps) => ( - - ), - head: docProps.head, - headTags: await headTags(documentCtx), - styles: docProps.styles, - } - } else { - let bodyResult - - const renderContent = () => { - return ctx.err && ErrorDebug ? ( - - - - ) : ( - - - {isServerComponent && AppMod.__next_rsc__ ? ( - // _app.server.js is used. - - ) : ( - - )} - - - ) - } - - if (hasConcurrentFeatures) { - let renderStream: any - - // We start rendering the shell earlier, before returning the head tags - // to `documentResult`. - const content = renderContent() - renderStream = await renderToInitialStream({ - ReactDOMServer, - element: content, - }) + return { docProps, documentCtx } + } - bodyResult = async (suffix: string) => { - // this must be called inside bodyResult so appWrappers is - // up to date when getWrappedApp is called - - const flushEffectHandler = async () => { - const allFlushEffects = [ - styledJsxFlushEffect, - ...(flushEffects || []), - ] - const flushEffectStream = await renderToStream({ - ReactDOMServer, - element: ( - <> - {allFlushEffects.map((flushEffect, i) => ( - {flushEffect()} - ))} - - ), - generateStaticHTML: true, - }) - const flushed = await streamToString(flushEffectStream) - return flushed - } + const renderContent = () => { + return ctx.err && ErrorDebug ? ( + + + + ) : ( + + + {isServerComponent && AppMod.__next_rsc__ ? ( + // _app.server.js is used. + + ) : ( + + )} + + + ) + } - return await continueFromInitialStream({ - renderStream, - suffix, - dataStream: serverComponentsInlinedTransformStream?.readable, - generateStaticHTML: generateStaticHTML || !hasConcurrentFeatures, - flushEffectHandler, - }) + if (!hasConcurrentFeatures) { + if (Document.getInitialProps) { + const documentInitialPropsRes = await documentInitialProps() + if (documentInitialPropsRes === null) return null + const { docProps, documentCtx } = documentInitialPropsRes + + return { + bodyResult: (suffix: string) => + streamFromArray([docProps.html, suffix]), + documentElement: (htmlProps: HtmlProps) => ( + + ), + head: docProps.head, + headTags: await headTags(documentCtx), + styles: docProps.styles, } } else { const content = renderContent() @@ -1404,15 +1372,86 @@ export async function renderToHTML( // before _document so that updateHead is called/collected before // rendering _document's head const result = ReactDOMServer.renderToString(content) - bodyResult = (suffix: string) => streamFromArray([result, suffix]) + const bodyResult = (suffix: string) => streamFromArray([result, suffix]) + + const styles = jsxStyleRegistry.styles() + jsxStyleRegistry.flush() + + return { + bodyResult, + documentElement: () => (Document as any)(), + head, + headTags: [], + styles, + } + } + } else { + let bodyResult + + let renderStream: any + + // We start rendering the shell earlier, before returning the head tags + // to `documentResult`. + const content = renderContent() + renderStream = await renderToInitialStream({ + ReactDOMServer, + element: content, + }) + + bodyResult = async (suffix: string) => { + // this must be called inside bodyResult so appWrappers is + // up to date when getWrappedApp is called + + const flushEffectHandler = async () => { + const allFlushEffects = [ + styledJsxFlushEffect, + ...(flushEffects || []), + ] + const flushEffectStream = await renderToStream({ + ReactDOMServer, + element: ( + <> + {allFlushEffects.map((flushEffect, i) => ( + {flushEffect()} + ))} + + ), + generateStaticHTML: true, + }) + const flushed = await streamToString(flushEffectStream) + return flushed + } + + return await continueFromInitialStream({ + renderStream, + suffix, + dataStream: serverComponentsInlinedTransformStream?.readable, + generateStaticHTML: generateStaticHTML || !hasConcurrentFeatures, + flushEffectHandler, + }) } const styles = jsxStyleRegistry.styles() jsxStyleRegistry.flush() + const documentInitialPropsRes = + isServerComponent || process.browser || !Document.getInitialProps + ? {} + : await documentInitialProps() + if (documentInitialPropsRes === null) return null + + const documentElement = () => { + if (isServerComponent || process.browser) { + return (Document as any)() + } + + const { docProps } = (documentInitialPropsRes as any) || {} + return + } + return { bodyResult, - documentElement: () => (Document as any)(), + documentElement, head, headTags: [], styles, diff --git a/test/development/basic/styled-components.test.ts b/test/development/basic/styled-components.test.ts index c17cc8f7be609d5..6e350c3c473a05d 100644 --- a/test/development/basic/styled-components.test.ts +++ b/test/development/basic/styled-components.test.ts @@ -17,6 +17,8 @@ describe('styled-components SWC transform', () => { }, dependencies: { 'styled-components': '5.3.3', + react: 'latest', + 'react-dom': 'latest', }, }) }) @@ -34,6 +36,7 @@ describe('styled-components SWC transform', () => { }) return foundLog } + it('should not have hydration mismatch with styled-components transform enabled', async () => { let browser try { @@ -56,4 +59,14 @@ describe('styled-components SWC transform', () => { } } }) + + it('should render the page with correct styles', async () => { + const browser = await webdriver(next.appPort, '/') + + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('#btn')).color` + ) + ).toBe('rgb(255, 255, 255)') + }) }) diff --git a/test/development/basic/styled-components/next.config.js b/test/development/basic/styled-components/next.config.js index 91f693fda5258b9..b03e60f7ad06dbd 100644 --- a/test/development/basic/styled-components/next.config.js +++ b/test/development/basic/styled-components/next.config.js @@ -1,5 +1,17 @@ -module.exports = { +const path = require('path') + +let withReact18 = (config) => config + +try { + // only used when running inside of the monorepo not when isolated + withReact18 = require(path.join( + __dirname, + '../../../integration/react-18/test/with-react-18' + )) +} catch (_) {} + +module.exports = withReact18({ compiler: { styledComponents: true, }, -} +}) diff --git a/test/development/basic/styled-components/pages/index.js b/test/development/basic/styled-components/pages/index.js index 31bd1226e76891b..31c5716fbf10b95 100644 --- a/test/development/basic/styled-components/pages/index.js +++ b/test/development/basic/styled-components/pages/index.js @@ -32,7 +32,9 @@ export default function Home() { GitHub - + ) } diff --git a/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index 4bdd329708b9e27..bb52a23a2b83389 100644 --- a/test/integration/react-streaming-and-server-components/test/index.test.js +++ b/test/integration/react-streaming-and-server-components/test/index.test.js @@ -241,11 +241,17 @@ const cssSuite = { } const documentSuite = { - runTests: (context) => { - it('should error when custom _document has getInitialProps method', async () => { - const res = await fetchViaHTTP(context.appPort, '/') - expect(res.status).toBe(500) - }) + runTests: (context, env) => { + if (env === 'dev') { + it('should error when custom _document has getInitialProps method', async () => { + const res = await fetchViaHTTP(context.appPort, '/') + expect(res.status).toBe(500) + }) + } else { + it('should failed building', async () => { + expect(context.code).toBe(1) + }) + } }, beforeAll: () => documentPage.write(documentWithGip), afterAll: () => documentPage.delete(), @@ -270,7 +276,7 @@ function runSuite(suiteName, env, options) { options.beforeAll?.() if (env === 'prod') { context.appPort = await findPort() - await nextBuild(context.appDir) + context.code = (await nextBuild(context.appDir)).code context.server = await nextStart(context.appDir, context.appPort) } if (env === 'dev') {