From 1877f7329be75515f4ba02d41faa72a6b508a3c8 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 30 Oct 2021 01:28:19 +0200 Subject: [PATCH 1/5] Provide default fallback _document and _app for for rsc --- .../next-middleware-ssr-loader/index.ts | 54 +++++++++++++++---- .../react-rsc-basic/app/pages/_app.js | 7 --- .../react-rsc-basic/app/pages/_document.js | 13 ----- 3 files changed, 45 insertions(+), 29 deletions(-) delete mode 100644 test/integration/react-rsc-basic/app/pages/_app.js delete mode 100644 test/integration/react-rsc-basic/app/pages/_document.js diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index 562009149e308..d40f02cf1a7bb 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -1,6 +1,28 @@ import loaderUtils from 'next/dist/compiled/loader-utils' import { getStringifiedAbsolutePath } from './utils' +const fallbackDocumentPage = ` +import { Html, Head, Main, NextScript } from 'next/document' + +function Document() { + return ( + jsx(Html, null, + jsx(Head), + jsx('body', null, + jsx(Main), + jsx(NextScript), + ), + ) + ) +} +` + +const fallbackAppPage = ` +function App({ Component, pageProps }) { + return jsx(Component, pageProps) +} +` + export default function middlewareRSCLoader(this: any) { const { absolutePagePath, @@ -22,6 +44,18 @@ export default function middlewareRSCLoader(this: any) { './pages/_app' ) + const fs = this.fs.fileSystem + const hasProvidedAppPage = fs.existsSync(stringifiedAbsoluteAppPath) + const hasProvidedDocumentPage = fs.existsSync(stringifiedAbsoluteDocumentPath) + + let appDefinition = hasProvidedDocumentPage + ? `const Document = require(${stringifiedAbsoluteAppPath})}).default` + : fallbackAppPage + + let documentDefinition = hasProvidedAppPage + ? `const Document = require(${stringifiedAbsoluteDocumentPath})}).default` + : fallbackDocumentPage + const transformed = ` import { adapter } from 'next/dist/server/web/adapter' @@ -38,15 +72,17 @@ export default function middlewareRSCLoader(this: any) { : '' } - var { + const jsx = createElement + ${appDefinition} + ${documentDefinition} + + const { default: Page, config, getStaticProps, getServerSideProps, getStaticPaths } = require(${stringifiedAbsolutePagePath}) - var Document = require(${stringifiedAbsoluteDocumentPath}).default - var App = require(${stringifiedAbsoluteAppPath}).default const buildManifest = self.__BUILD_MANIFEST const reactLoadableManifest = self.__REACT_LOADABLE_MANIFEST @@ -57,11 +93,11 @@ export default function middlewareRSCLoader(this: any) { } function wrapReadable (readable) { - var encoder = new TextEncoder() - var transformStream = new TransformStream() - var writer = transformStream.writable.getWriter() - var reader = readable.getReader() - var process = () => { + const encoder = new TextEncoder() + const transformStream = new TransformStream() + const writer = transformStream.writable.getWriter() + const reader = readable.getReader() + const process = () => { reader.read().then(({ done, value }) => { if (!done) { writer.write(typeof value === 'string' ? encoder.encode(value) : value) @@ -82,7 +118,7 @@ export default function middlewareRSCLoader(this: any) { let responseCache const FlightWrapper = props => { - var response = responseCache + let response = responseCache if (!response) { responseCache = response = createFromReadableStream(renderFlight(props)) } diff --git a/test/integration/react-rsc-basic/app/pages/_app.js b/test/integration/react-rsc-basic/app/pages/_app.js deleted file mode 100644 index eee8821812081..0000000000000 --- a/test/integration/react-rsc-basic/app/pages/_app.js +++ /dev/null @@ -1,7 +0,0 @@ -import '../styles.css' - -function App({ Component, pageProps }) { - return -} - -export default App diff --git a/test/integration/react-rsc-basic/app/pages/_document.js b/test/integration/react-rsc-basic/app/pages/_document.js deleted file mode 100644 index bff2b1b2821cb..0000000000000 --- a/test/integration/react-rsc-basic/app/pages/_document.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Html, Head, Main, NextScript } from 'next/document' - -export default function Document() { - return ( - - - -
- - - - ) -} From 16e9aa067a3403aa3826d6dd9983fdd5b3c83a06 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 30 Oct 2021 18:37:49 +0200 Subject: [PATCH 2/5] fallback inline doc, dist default app --- packages/next/build/webpack-config.ts | 9 ++- .../next-middleware-ssr-loader/index.ts | 55 +++++++++++-------- .../react-rsc-basic/app/pages/_document.js | 17 ++++++ 3 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 test/integration/react-rsc-basic/app/pages/_document.js diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index b3b3e394abd69..13b4d2ebc308e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -361,11 +361,16 @@ export default async function getBaseWebpackConfig( const hasReactRoot: boolean = config.experimental.reactRoot || hasReact18 || isReactExperimental - if (config.experimental.reactRoot && !(hasReact18 || isReactExperimental)) { + // Only inform during one of the builds + if ( + !isServer && + config.experimental.reactRoot && + !(hasReact18 || isReactExperimental) + ) { // It's fine to only mention React 18 here as we don't recommend people to try experimental. Log.warn('You have to use React 18 to use `experimental.reactRoot`.') } - if (config.experimental.concurrentFeatures && !hasReactRoot) { + if (!isServer && config.experimental.concurrentFeatures && !hasReactRoot) { throw new Error( '`experimental.concurrentFeatures` requires `experimental.reactRoot` to be enabled along with React 18.' ) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index d40f02cf1a7bb..a805362d33924 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -6,24 +6,28 @@ import { Html, Head, Main, NextScript } from 'next/document' function Document() { return ( - jsx(Html, null, - jsx(Head), - jsx('body', null, - jsx(Main), - jsx(NextScript), - ), + createElement(Html, null, + createElement(Head), + createElement('body', null, + createElement(Main), + createElement(NextScript), + ) ) ) } ` -const fallbackAppPage = ` -function App({ Component, pageProps }) { - return jsx(Component, pageProps) +function hasModule(path: string) { + let has + try { + has = !!require.resolve(path) + } catch (_) { + has = false + } + return has } -` -export default function middlewareRSCLoader(this: any) { +export default async function middlewareRSCLoader(this: any) { const { absolutePagePath, basePath, @@ -44,16 +48,17 @@ export default function middlewareRSCLoader(this: any) { './pages/_app' ) - const fs = this.fs.fileSystem - const hasProvidedAppPage = fs.existsSync(stringifiedAbsoluteAppPath) - const hasProvidedDocumentPage = fs.existsSync(stringifiedAbsoluteDocumentPath) + const hasProvidedAppPage = hasModule(JSON.parse(stringifiedAbsoluteAppPath)) + const hasProvidedDocumentPage = hasModule( + JSON.parse(stringifiedAbsoluteDocumentPath) + ) - let appDefinition = hasProvidedDocumentPage - ? `const Document = require(${stringifiedAbsoluteAppPath})}).default` - : fallbackAppPage + let appDefinition = `const App = require('${ + hasProvidedAppPage ? stringifiedAbsoluteAppPath : 'next/dist/pages/_app' + }').default` - let documentDefinition = hasProvidedAppPage - ? `const Document = require(${stringifiedAbsoluteDocumentPath})}).default` + let documentDefinition = hasProvidedDocumentPage + ? `const Document = require(${stringifiedAbsoluteDocumentPath}).default` : fallbackDocumentPage const transformed = ` @@ -72,9 +77,15 @@ export default function middlewareRSCLoader(this: any) { : '' } - const jsx = createElement - ${appDefinition} ${documentDefinition} + ${appDefinition} + + // console.log('Document.getInitialProps', Document.getInitialProps) + let hasWarnedGip = false + if (!hasWarnedGip && Document.getInitialProps) { + hasWarnedGip = true + throw new Error('Document.getInitialProps is not supported when \`experimental.concurrentFeatures\` is enabled') + } const { default: Page, @@ -89,7 +100,7 @@ export default function middlewareRSCLoader(this: any) { const rscManifest = self._middleware_rsc_manifest if (typeof Page !== 'function') { - throw new Error('Your page must export a \`default\` component'); + throw new Error('Your page must export a \`default\` component') } function wrapReadable (readable) { diff --git a/test/integration/react-rsc-basic/app/pages/_document.js b/test/integration/react-rsc-basic/app/pages/_document.js new file mode 100644 index 0000000000000..9c87b34c2dc50 --- /dev/null +++ b/test/integration/react-rsc-basic/app/pages/_document.js @@ -0,0 +1,17 @@ +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + + + +
+ + + + ) +} + +Document.getInitialProps = (ctx) => { + return ctx.defaultGetInitialProps(ctx) +} From 6e387290f220bf4130c437a28353661b4c26aaea Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 30 Oct 2021 22:19:43 +0200 Subject: [PATCH 3/5] add test --- .../next-middleware-ssr-loader/index.ts | 18 ++-- .../react-rsc-basic/app/pages/_document.js | 17 ---- .../react-rsc-basic/test/index.test.js | 98 ++++++++++++++----- 3 files changed, 81 insertions(+), 52 deletions(-) delete mode 100644 test/integration/react-rsc-basic/app/pages/_document.js diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index a805362d33924..05b792741f48e 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -80,13 +80,6 @@ export default async function middlewareRSCLoader(this: any) { ${documentDefinition} ${appDefinition} - // console.log('Document.getInitialProps', Document.getInitialProps) - let hasWarnedGip = false - if (!hasWarnedGip && Document.getInitialProps) { - hasWarnedGip = true - throw new Error('Document.getInitialProps is not supported when \`experimental.concurrentFeatures\` is enabled') - } - const { default: Page, config, @@ -103,7 +96,11 @@ export default async function middlewareRSCLoader(this: any) { throw new Error('Your page must export a \`default\` component') } - function wrapReadable (readable) { + function renderError(err, status) { + return new Response(err.toString(), {status}) + } + + function wrapReadable(readable) { const encoder = new TextEncoder() const transformStream = new TransformStream() const writer = transformStream.writable.getWriter() @@ -150,6 +147,11 @@ export default async function middlewareRSCLoader(this: any) { const url = request.nextUrl const query = Object.fromEntries(url.searchParams) + if (Document.getInitialProps) { + const err = new Error('Document.getInitialProps is not supported with server components, please remove it from pages/_document') + return renderError(err, 500) + } + // Preflight request if (request.method === 'HEAD') { return new Response('OK.', { diff --git a/test/integration/react-rsc-basic/app/pages/_document.js b/test/integration/react-rsc-basic/app/pages/_document.js deleted file mode 100644 index 9c87b34c2dc50..0000000000000 --- a/test/integration/react-rsc-basic/app/pages/_document.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Html, Head, Main, NextScript } from 'next/document' - -export default function Document() { - return ( - - - -
- - - - ) -} - -Document.getInitialProps = (ctx) => { - return ctx.defaultGetInitialProps(ctx) -} diff --git a/test/integration/react-rsc-basic/test/index.test.js b/test/integration/react-rsc-basic/test/index.test.js index 1a1d373818d6c..4e133fe365cfc 100644 --- a/test/integration/react-rsc-basic/test/index.test.js +++ b/test/integration/react-rsc-basic/test/index.test.js @@ -5,6 +5,8 @@ import { join } from 'path' import fs from 'fs-extra' import { + File, + fetchViaHTTP, findPort, killApp, launchApp, @@ -18,6 +20,27 @@ import css from './css' const nodeArgs = ['-r', join(__dirname, '../../react-18/test/require-hook.js')] const appDir = join(__dirname, '../app') const distDir = join(__dirname, '../app/.next') +const documentPage = new File(join(appDir, 'pages/_document.js')) + +const documentWithGip = ` +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + + + +
+ + + + ) +} + +Document.getInitialProps = (ctx) => { + return ctx.defaultGetInitialProps(ctx) +} +` async function nextBuild(dir) { return await _nextBuild(dir, [], { @@ -99,7 +122,7 @@ describe('RSC prod', () => { expect(content.clientInfo).toContainEqual(item) } }) - runTests(context) + runBasicTests(context) }) describe('RSC dev', () => { @@ -112,39 +135,34 @@ describe('RSC dev', () => { afterAll(async () => { await killApp(context.server) }) - runTests(context) + runBasicTests(context) }) -describe('CSS prod', () => { - const context = { appDir } +const cssSuite = { runTests: css } - beforeAll(async () => { - context.appPort = await findPort() - await nextBuild(context.appDir) - context.server = await nextStart(context.appDir, context.appPort) - }) - afterAll(async () => { - await killApp(context.server) - }) - - css(context) -}) +runSuite('CSS', 'dev', cssSuite) +runSuite('CSS', 'prod', cssSuite) -describe('CSS dev', () => { - const context = { appDir } +const documentSuite = { + runTests: (context) => { + it('should error when custom _document has getInitialProps method', async () => { + const res = await fetchViaHTTP(context.appPort, '/') + const html = await res.text() - beforeAll(async () => { - context.appPort = await findPort() - context.server = await nextDev(context.appDir, context.appPort) - }) - afterAll(async () => { - await killApp(context.server) - }) + expect(res.status).toBe(500) + expect(html).toContain( + 'Document.getInitialProps is not supported with server components, please remove it from pages/_document' + ) + }) + }, + before: () => documentPage.write(documentWithGip), + after: () => documentPage.delete(), +} - css(context) -}) +runSuite('document', 'dev', documentSuite) +runSuite('document', 'prod', documentSuite) -async function runTests(context) { +async function runBasicTests(context) { it('should render the correct html', async () => { const homeHTML = await renderViaHTTP(context.appPort, '/') @@ -181,3 +199,29 @@ async function runTests(context) { expect(imageTag.attr('src')).toContain('data:image') }) } + +function runSuite(suiteName, env, { runTests, before, after }) { + const context = { appDir } + describe(`${suiteName} ${env}`, () => { + if (env === 'prod') { + beforeAll(async () => { + before?.() + context.appPort = await findPort() + context.server = await nextDev(context.appDir, context.appPort) + }) + } + if (env === 'dev') { + beforeAll(async () => { + before?.() + context.appPort = await findPort() + context.server = await nextDev(context.appDir, context.appPort) + }) + } + afterAll(async () => { + after?.() + await killApp(context.server) + }) + + runTests(context) + }) +} From 5a94c4f44d51f35dc4b131847a210dbc48613d04 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 30 Oct 2021 22:25:00 +0200 Subject: [PATCH 4/5] fix style tests --- test/integration/react-rsc-basic/app/pages/_app.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 test/integration/react-rsc-basic/app/pages/_app.js diff --git a/test/integration/react-rsc-basic/app/pages/_app.js b/test/integration/react-rsc-basic/app/pages/_app.js new file mode 100644 index 0000000000000..eee8821812081 --- /dev/null +++ b/test/integration/react-rsc-basic/app/pages/_app.js @@ -0,0 +1,7 @@ +import '../styles.css' + +function App({ Component, pageProps }) { + return +} + +export default App From ff330734dde354b0d4bf62ceb8d4c2eba05f3da4 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sat, 30 Oct 2021 22:30:08 +0200 Subject: [PATCH 5/5] improve css tests --- .../loaders/next-middleware-ssr-loader/index.ts | 8 +++++--- .../react-rsc-basic/app/pages/_app.js | 7 ------- .../react-rsc-basic/test/index.test.js | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 11 deletions(-) delete mode 100644 test/integration/react-rsc-basic/app/pages/_app.js diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index 05b792741f48e..0cfc7305c7339 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -53,9 +53,11 @@ export default async function middlewareRSCLoader(this: any) { JSON.parse(stringifiedAbsoluteDocumentPath) ) - let appDefinition = `const App = require('${ - hasProvidedAppPage ? stringifiedAbsoluteAppPath : 'next/dist/pages/_app' - }').default` + let appDefinition = `const App = require(${ + hasProvidedAppPage + ? stringifiedAbsoluteAppPath + : JSON.stringify('next/dist/pages/_app') + }).default` let documentDefinition = hasProvidedDocumentPage ? `const Document = require(${stringifiedAbsoluteDocumentPath}).default` diff --git a/test/integration/react-rsc-basic/app/pages/_app.js b/test/integration/react-rsc-basic/app/pages/_app.js deleted file mode 100644 index eee8821812081..0000000000000 --- a/test/integration/react-rsc-basic/app/pages/_app.js +++ /dev/null @@ -1,7 +0,0 @@ -import '../styles.css' - -function App({ Component, pageProps }) { - return -} - -export default App diff --git a/test/integration/react-rsc-basic/test/index.test.js b/test/integration/react-rsc-basic/test/index.test.js index 4e133fe365cfc..943af329657e7 100644 --- a/test/integration/react-rsc-basic/test/index.test.js +++ b/test/integration/react-rsc-basic/test/index.test.js @@ -21,6 +21,7 @@ const nodeArgs = ['-r', join(__dirname, '../../react-18/test/require-hook.js')] const appDir = join(__dirname, '../app') const distDir = join(__dirname, '../app/.next') const documentPage = new File(join(appDir, 'pages/_document.js')) +const appPage = new File(join(appDir, 'pages/_app.js')) const documentWithGip = ` import { Html, Head, Main, NextScript } from 'next/document' @@ -42,6 +43,16 @@ Document.getInitialProps = (ctx) => { } ` +const appWithGlobalCss = ` +import '../styles.css' + +function App({ Component, pageProps }) { + return +} + +export default App +` + async function nextBuild(dir) { return await _nextBuild(dir, [], { stdout: true, @@ -138,7 +149,11 @@ describe('RSC dev', () => { runBasicTests(context) }) -const cssSuite = { runTests: css } +const cssSuite = { + runTests: css, + before: () => appPage.write(appWithGlobalCss), + after: () => appPage.delete(), +} runSuite('CSS', 'dev', cssSuite) runSuite('CSS', 'prod', cssSuite)