Skip to content

Commit

Permalink
Provide default fallback _document and _app for for concurrent mode (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
huozhi authored Oct 30, 2021
1 parent c12ae5e commit 622a1a5
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 61 deletions.
9 changes: 7 additions & 2 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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'
Expand All @@ -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)
Expand All @@ -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))
}
Expand All @@ -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.', {
Expand Down
7 changes: 0 additions & 7 deletions test/integration/react-rsc-basic/app/pages/_app.js

This file was deleted.

13 changes: 0 additions & 13 deletions test/integration/react-rsc-basic/app/pages/_document.js

This file was deleted.

113 changes: 86 additions & 27 deletions test/integration/react-rsc-basic/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { join } from 'path'
import fs from 'fs-extra'

import {
File,
fetchViaHTTP,
findPort,
killApp,
launchApp,
Expand All @@ -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 (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
Document.getInitialProps = (ctx) => {
return ctx.defaultGetInitialProps(ctx)
}
`

const appWithGlobalCss = `
import '../styles.css'
function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default App
`

async function nextBuild(dir) {
return await _nextBuild(dir, [], {
Expand Down Expand Up @@ -99,7 +133,7 @@ describe('RSC prod', () => {
expect(content.clientInfo).toContainEqual(item)
}
})
runTests(context)
runBasicTests(context)
})

describe('RSC dev', () => {
Expand All @@ -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, '/')

Expand Down Expand Up @@ -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)
})
}

0 comments on commit 622a1a5

Please sign in to comment.