Skip to content

Commit fcf84e1

Browse files
committed
Add warning for custom documents
1 parent b1335de commit fcf84e1

File tree

8 files changed

+83
-0
lines changed

8 files changed

+83
-0
lines changed

errors/custom-document-rsc.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Custom Document must be a functional Server Component
2+
3+
#### Why This Error Occurred
4+
5+
Your `next.config.js` has `concurrentFeatures: true` and your `pages/_document` page doesn't export a React Server Component.
6+
7+
#### Possible Ways to Fix It
8+
9+
Set `concurrentFeatures: false` or convert `pages/_document` to a React Server Component.

packages/next/build/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import formatWebpackMessages from '../client/dev/error-overlay/format-webpack-me
1313
import {
1414
STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR,
1515
PUBLIC_DIR_MIDDLEWARE_CONFLICT,
16+
CUSTOM_DOCUMENT_RSC_ERROR,
1617
} from '../lib/constants'
1718
import { fileExists } from '../lib/file-exists'
1819
import { findPagesDir } from '../lib/find-pages-dir'
@@ -85,6 +86,7 @@ import {
8586
printCustomRoutes,
8687
printTreeView,
8788
getCssFilePaths,
89+
hasFunctionalComponentExport,
8890
} from './utils'
8991
import getBaseWebpackConfig from './webpack-config'
9092
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
@@ -690,6 +692,18 @@ export default async function build(
690692
)
691693
}
692694

695+
if (config.experimental?.concurrentFeatures) {
696+
const customDocumentFunctional = await hasFunctionalComponentExport(
697+
'/_document',
698+
distDir,
699+
isLikeServerless,
700+
runtimeEnvConfig
701+
)
702+
if (!customDocumentFunctional) {
703+
throw new Error(CUSTOM_DOCUMENT_RSC_ERROR)
704+
}
705+
}
706+
693707
await Promise.all(
694708
pageKeys.map(async (page) => {
695709
const checkPageSpan = staticCheckSpan.traceChild('check-page', {

packages/next/build/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,22 @@ export async function hasCustomGetInitialProps(
868868
return mod.getInitialProps !== mod.origGetInitialProps
869869
}
870870

871+
export async function hasFunctionalComponentExport(
872+
page: string,
873+
distDir: string,
874+
isLikeServerless: boolean,
875+
runtimeEnvConfig: any
876+
): Promise<boolean> {
877+
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
878+
879+
const components = await loadComponents(distDir, page, isLikeServerless)
880+
let mod = components.ComponentMod
881+
mod = mod.default || mod
882+
mod = await mod
883+
884+
return typeof mod === 'function' && !mod.prototype
885+
}
886+
871887
export async function getNamedExports(
872888
page: string,
873889
distDir: string,

packages/next/lib/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,5 @@ export const GSSP_COMPONENT_MEMBER_ERROR = `can not be attached to a page's comp
4848
export const NON_STANDARD_NODE_ENV = `You are using a non-standard "NODE_ENV" value in your environment. This creates inconsistencies in the project and is strongly advised against. Read more: https://nextjs.org/docs/messages/non-standard-node-env`
4949

5050
export const SSG_FALLBACK_EXPORT_ERROR = `Pages with \`fallback\` enabled in \`getStaticPaths\` can not be exported. See more info here: https://nextjs.org/docs/messages/ssg-fallback-true-export`
51+
52+
export const CUSTOM_DOCUMENT_RSC_ERROR = `Custom Document must be a functional Server Component. See more info here: https://nextjs.org/docs/messages/custom-document-rsc`

packages/next/next-server/server/render.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
SERVER_PROPS_SSG_CONFLICT,
1414
SSG_GET_INITIAL_PROPS_CONFLICT,
1515
UNSTABLE_REVALIDATE_RENAME_ERROR,
16+
CUSTOM_DOCUMENT_RSC_ERROR,
1617
} from '../../lib/constants'
1718
import { isSerializableProps } from '../../lib/is-serializable-props'
1819
import { GetServerSideProps, GetStaticProps, PreviewData } from '../../types'
@@ -459,6 +460,10 @@ export async function renderToHTML(
459460
!isSSG &&
460461
!getServerSideProps
461462

463+
if (process.env.NEXT_CONCURRENT_FEATURES && typeof Document !== 'function') {
464+
throw new Error(CUSTOM_DOCUMENT_RSC_ERROR + ` ${pathname}`)
465+
}
466+
462467
for (const methodName of [
463468
'getStaticProps',
464469
'getServerSideProps',
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
experimental: {
3+
concurrentFeatures: true,
4+
},
5+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Document, { Html, Head, Main, NextScript } from 'next/document'
2+
3+
class MyDocument extends Document {
4+
render() {
5+
return (
6+
<Html>
7+
<Head />
8+
<body>
9+
<Main />
10+
<NextScript />
11+
</body>
12+
</Html>
13+
)
14+
}
15+
}
16+
17+
export default MyDocument
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* eslint-env jest */
2+
3+
import { join } from 'path'
4+
import { nextBuild } from 'next-test-utils'
5+
import { CUSTOM_DOCUMENT_RSC_ERROR } from 'next/dist/lib/constants'
6+
7+
jest.setTimeout(1000 * 60)
8+
const appDir = join(__dirname, '..')
9+
10+
describe('Concurrent Document Component Errors', () => {
11+
it('should error when exporting a class component', async () => {
12+
const { stderr } = await nextBuild(appDir, [], { stderr: true })
13+
expect(stderr).toContain(CUSTOM_DOCUMENT_RSC_ERROR)
14+
})
15+
})

0 commit comments

Comments
 (0)