Skip to content

Commit 622a1a5

Browse files
authoredOct 30, 2021
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
1 parent c12ae5e commit 622a1a5

File tree

5 files changed

+156
-61
lines changed

5 files changed

+156
-61
lines changed
 

‎packages/next/build/webpack-config.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -361,11 +361,16 @@ export default async function getBaseWebpackConfig(
361361
const hasReactRoot: boolean =
362362
config.experimental.reactRoot || hasReact18 || isReactExperimental
363363

364-
if (config.experimental.reactRoot && !(hasReact18 || isReactExperimental)) {
364+
// Only inform during one of the builds
365+
if (
366+
!isServer &&
367+
config.experimental.reactRoot &&
368+
!(hasReact18 || isReactExperimental)
369+
) {
365370
// It's fine to only mention React 18 here as we don't recommend people to try experimental.
366371
Log.warn('You have to use React 18 to use `experimental.reactRoot`.')
367372
}
368-
if (config.experimental.concurrentFeatures && !hasReactRoot) {
373+
if (!isServer && config.experimental.concurrentFeatures && !hasReactRoot) {
369374
throw new Error(
370375
'`experimental.concurrentFeatures` requires `experimental.reactRoot` to be enabled along with React 18.'
371376
)

‎packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts

+63-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
11
import loaderUtils from 'next/dist/compiled/loader-utils'
22
import { getStringifiedAbsolutePath } from './utils'
33

4-
export default function middlewareRSCLoader(this: any) {
4+
const fallbackDocumentPage = `
5+
import { Html, Head, Main, NextScript } from 'next/document'
6+
7+
function Document() {
8+
return (
9+
createElement(Html, null,
10+
createElement(Head),
11+
createElement('body', null,
12+
createElement(Main),
13+
createElement(NextScript),
14+
)
15+
)
16+
)
17+
}
18+
`
19+
20+
function hasModule(path: string) {
21+
let has
22+
try {
23+
has = !!require.resolve(path)
24+
} catch (_) {
25+
has = false
26+
}
27+
return has
28+
}
29+
30+
export default async function middlewareRSCLoader(this: any) {
531
const {
632
absolutePagePath,
733
basePath,
@@ -22,6 +48,21 @@ export default function middlewareRSCLoader(this: any) {
2248
'./pages/_app'
2349
)
2450

51+
const hasProvidedAppPage = hasModule(JSON.parse(stringifiedAbsoluteAppPath))
52+
const hasProvidedDocumentPage = hasModule(
53+
JSON.parse(stringifiedAbsoluteDocumentPath)
54+
)
55+
56+
let appDefinition = `const App = require(${
57+
hasProvidedAppPage
58+
? stringifiedAbsoluteAppPath
59+
: JSON.stringify('next/dist/pages/_app')
60+
}).default`
61+
62+
let documentDefinition = hasProvidedDocumentPage
63+
? `const Document = require(${stringifiedAbsoluteDocumentPath}).default`
64+
: fallbackDocumentPage
65+
2566
const transformed = `
2667
import { adapter } from 'next/dist/server/web/adapter'
2768
@@ -38,30 +79,35 @@ export default function middlewareRSCLoader(this: any) {
3879
: ''
3980
}
4081
41-
var {
82+
${documentDefinition}
83+
${appDefinition}
84+
85+
const {
4286
default: Page,
4387
config,
4488
getStaticProps,
4589
getServerSideProps,
4690
getStaticPaths
4791
} = require(${stringifiedAbsolutePagePath})
48-
var Document = require(${stringifiedAbsoluteDocumentPath}).default
49-
var App = require(${stringifiedAbsoluteAppPath}).default
5092
5193
const buildManifest = self.__BUILD_MANIFEST
5294
const reactLoadableManifest = self.__REACT_LOADABLE_MANIFEST
5395
const rscManifest = self._middleware_rsc_manifest
5496
5597
if (typeof Page !== 'function') {
56-
throw new Error('Your page must export a \`default\` component');
98+
throw new Error('Your page must export a \`default\` component')
99+
}
100+
101+
function renderError(err, status) {
102+
return new Response(err.toString(), {status})
57103
}
58104
59-
function wrapReadable (readable) {
60-
var encoder = new TextEncoder()
61-
var transformStream = new TransformStream()
62-
var writer = transformStream.writable.getWriter()
63-
var reader = readable.getReader()
64-
var process = () => {
105+
function wrapReadable(readable) {
106+
const encoder = new TextEncoder()
107+
const transformStream = new TransformStream()
108+
const writer = transformStream.writable.getWriter()
109+
const reader = readable.getReader()
110+
const process = () => {
65111
reader.read().then(({ done, value }) => {
66112
if (!done) {
67113
writer.write(typeof value === 'string' ? encoder.encode(value) : value)
@@ -82,7 +128,7 @@ export default function middlewareRSCLoader(this: any) {
82128
83129
let responseCache
84130
const FlightWrapper = props => {
85-
var response = responseCache
131+
let response = responseCache
86132
if (!response) {
87133
responseCache = response = createFromReadableStream(renderFlight(props))
88134
}
@@ -103,6 +149,11 @@ export default function middlewareRSCLoader(this: any) {
103149
const url = request.nextUrl
104150
const query = Object.fromEntries(url.searchParams)
105151
152+
if (Document.getInitialProps) {
153+
const err = new Error('Document.getInitialProps is not supported with server components, please remove it from pages/_document')
154+
return renderError(err, 500)
155+
}
156+
106157
// Preflight request
107158
if (request.method === 'HEAD') {
108159
return new Response('OK.', {

‎test/integration/react-rsc-basic/app/pages/_app.js

-7
This file was deleted.

‎test/integration/react-rsc-basic/app/pages/_document.js

-13
This file was deleted.

‎test/integration/react-rsc-basic/test/index.test.js

+86-27
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { join } from 'path'
55
import fs from 'fs-extra'
66

77
import {
8+
File,
9+
fetchViaHTTP,
810
findPort,
911
killApp,
1012
launchApp,
@@ -18,6 +20,38 @@ import css from './css'
1820
const nodeArgs = ['-r', join(__dirname, '../../react-18/test/require-hook.js')]
1921
const appDir = join(__dirname, '../app')
2022
const distDir = join(__dirname, '../app/.next')
23+
const documentPage = new File(join(appDir, 'pages/_document.js'))
24+
const appPage = new File(join(appDir, 'pages/_app.js'))
25+
26+
const documentWithGip = `
27+
import { Html, Head, Main, NextScript } from 'next/document'
28+
29+
export default function Document() {
30+
return (
31+
<Html>
32+
<Head />
33+
<body>
34+
<Main />
35+
<NextScript />
36+
</body>
37+
</Html>
38+
)
39+
}
40+
41+
Document.getInitialProps = (ctx) => {
42+
return ctx.defaultGetInitialProps(ctx)
43+
}
44+
`
45+
46+
const appWithGlobalCss = `
47+
import '../styles.css'
48+
49+
function App({ Component, pageProps }) {
50+
return <Component {...pageProps} />
51+
}
52+
53+
export default App
54+
`
2155

2256
async function nextBuild(dir) {
2357
return await _nextBuild(dir, [], {
@@ -99,7 +133,7 @@ describe('RSC prod', () => {
99133
expect(content.clientInfo).toContainEqual(item)
100134
}
101135
})
102-
runTests(context)
136+
runBasicTests(context)
103137
})
104138

105139
describe('RSC dev', () => {
@@ -112,39 +146,38 @@ describe('RSC dev', () => {
112146
afterAll(async () => {
113147
await killApp(context.server)
114148
})
115-
runTests(context)
149+
runBasicTests(context)
116150
})
117151

118-
describe('CSS prod', () => {
119-
const context = { appDir }
120-
121-
beforeAll(async () => {
122-
context.appPort = await findPort()
123-
await nextBuild(context.appDir)
124-
context.server = await nextStart(context.appDir, context.appPort)
125-
})
126-
afterAll(async () => {
127-
await killApp(context.server)
128-
})
152+
const cssSuite = {
153+
runTests: css,
154+
before: () => appPage.write(appWithGlobalCss),
155+
after: () => appPage.delete(),
156+
}
129157

130-
css(context)
131-
})
158+
runSuite('CSS', 'dev', cssSuite)
159+
runSuite('CSS', 'prod', cssSuite)
132160

133-
describe('CSS dev', () => {
134-
const context = { appDir }
161+
const documentSuite = {
162+
runTests: (context) => {
163+
it('should error when custom _document has getInitialProps method', async () => {
164+
const res = await fetchViaHTTP(context.appPort, '/')
165+
const html = await res.text()
135166

136-
beforeAll(async () => {
137-
context.appPort = await findPort()
138-
context.server = await nextDev(context.appDir, context.appPort)
139-
})
140-
afterAll(async () => {
141-
await killApp(context.server)
142-
})
167+
expect(res.status).toBe(500)
168+
expect(html).toContain(
169+
'Document.getInitialProps is not supported with server components, please remove it from pages/_document'
170+
)
171+
})
172+
},
173+
before: () => documentPage.write(documentWithGip),
174+
after: () => documentPage.delete(),
175+
}
143176

144-
css(context)
145-
})
177+
runSuite('document', 'dev', documentSuite)
178+
runSuite('document', 'prod', documentSuite)
146179

147-
async function runTests(context) {
180+
async function runBasicTests(context) {
148181
it('should render the correct html', async () => {
149182
const homeHTML = await renderViaHTTP(context.appPort, '/')
150183

@@ -181,3 +214,29 @@ async function runTests(context) {
181214
expect(imageTag.attr('src')).toContain('data:image')
182215
})
183216
}
217+
218+
function runSuite(suiteName, env, { runTests, before, after }) {
219+
const context = { appDir }
220+
describe(`${suiteName} ${env}`, () => {
221+
if (env === 'prod') {
222+
beforeAll(async () => {
223+
before?.()
224+
context.appPort = await findPort()
225+
context.server = await nextDev(context.appDir, context.appPort)
226+
})
227+
}
228+
if (env === 'dev') {
229+
beforeAll(async () => {
230+
before?.()
231+
context.appPort = await findPort()
232+
context.server = await nextDev(context.appDir, context.appPort)
233+
})
234+
}
235+
afterAll(async () => {
236+
after?.()
237+
await killApp(context.server)
238+
})
239+
240+
runTests(context)
241+
})
242+
}

0 commit comments

Comments
 (0)
Please sign in to comment.