From 622a1a554933b1bf7aca21af5248746c92747cc7 Mon Sep 17 00:00:00 2001
From: Jiachi Liu
Date: Sat, 30 Oct 2021 23:35:31 +0200
Subject: [PATCH] Provide default fallback _document and _app for for
concurrent mode (#30642)
* if _app is not provided, fallback to default _app page
* If _document is not provided, fallback to inline functional components version or use the default
* if Document gIP is provided, error
Closes #30654
---
packages/next/build/webpack-config.ts | 9 +-
.../next-middleware-ssr-loader/index.ts | 75 ++++++++++--
.../react-rsc-basic/app/pages/_app.js | 7 --
.../react-rsc-basic/app/pages/_document.js | 13 --
.../react-rsc-basic/test/index.test.js | 113 +++++++++++++-----
5 files changed, 156 insertions(+), 61 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-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 562009149e308..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
@@ -1,7 +1,33 @@
import loaderUtils from 'next/dist/compiled/loader-utils'
import { getStringifiedAbsolutePath } from './utils'
-export default function middlewareRSCLoader(this: any) {
+const fallbackDocumentPage = `
+import { Html, Head, Main, NextScript } from 'next/document'
+
+function Document() {
+ return (
+ createElement(Html, null,
+ createElement(Head),
+ createElement('body', null,
+ createElement(Main),
+ createElement(NextScript),
+ )
+ )
+ )
+}
+`
+
+function hasModule(path: string) {
+ let has
+ try {
+ has = !!require.resolve(path)
+ } catch (_) {
+ has = false
+ }
+ return has
+}
+
+export default async function middlewareRSCLoader(this: any) {
const {
absolutePagePath,
basePath,
@@ -22,6 +48,21 @@ export default function middlewareRSCLoader(this: any) {
'./pages/_app'
)
+ const hasProvidedAppPage = hasModule(JSON.parse(stringifiedAbsoluteAppPath))
+ const hasProvidedDocumentPage = hasModule(
+ JSON.parse(stringifiedAbsoluteDocumentPath)
+ )
+
+ let appDefinition = `const App = require(${
+ hasProvidedAppPage
+ ? stringifiedAbsoluteAppPath
+ : JSON.stringify('next/dist/pages/_app')
+ }).default`
+
+ let documentDefinition = hasProvidedDocumentPage
+ ? `const Document = require(${stringifiedAbsoluteDocumentPath}).default`
+ : fallbackDocumentPage
+
const transformed = `
import { adapter } from 'next/dist/server/web/adapter'
@@ -38,30 +79,35 @@ export default function middlewareRSCLoader(this: any) {
: ''
}
- var {
+ ${documentDefinition}
+ ${appDefinition}
+
+ 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
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 renderError(err, status) {
+ return new Response(err.toString(), {status})
}
- function wrapReadable (readable) {
- var encoder = new TextEncoder()
- var transformStream = new TransformStream()
- var writer = transformStream.writable.getWriter()
- var reader = readable.getReader()
- var process = () => {
+ function wrapReadable(readable) {
+ 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 +128,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))
}
@@ -103,6 +149,11 @@ export default 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/_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 (
-
-
-
-
-
-
-
- )
-}
diff --git a/test/integration/react-rsc-basic/test/index.test.js b/test/integration/react-rsc-basic/test/index.test.js
index 1a1d373818d6c..943af329657e7 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,38 @@ 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 appPage = new File(join(appDir, 'pages/_app.js'))
+
+const documentWithGip = `
+import { Html, Head, Main, NextScript } from 'next/document'
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+ )
+}
+
+Document.getInitialProps = (ctx) => {
+ return ctx.defaultGetInitialProps(ctx)
+}
+`
+
+const appWithGlobalCss = `
+import '../styles.css'
+
+function App({ Component, pageProps }) {
+ return
+}
+
+export default App
+`
async function nextBuild(dir) {
return await _nextBuild(dir, [], {
@@ -99,7 +133,7 @@ describe('RSC prod', () => {
expect(content.clientInfo).toContainEqual(item)
}
})
- runTests(context)
+ runBasicTests(context)
})
describe('RSC dev', () => {
@@ -112,39 +146,38 @@ describe('RSC dev', () => {
afterAll(async () => {
await killApp(context.server)
})
- runTests(context)
+ runBasicTests(context)
})
-describe('CSS prod', () => {
- const context = { appDir }
-
- beforeAll(async () => {
- context.appPort = await findPort()
- await nextBuild(context.appDir)
- context.server = await nextStart(context.appDir, context.appPort)
- })
- afterAll(async () => {
- await killApp(context.server)
- })
+const cssSuite = {
+ runTests: css,
+ before: () => appPage.write(appWithGlobalCss),
+ after: () => appPage.delete(),
+}
- 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 +214,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)
+ })
+}