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)