Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide default fallback _document and _app for for concurrent mode #30642

Merged
merged 6 commits into from
Oct 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})
}