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

Add error when document component isn't rendered #16459

Merged
merged 2 commits into from
Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 13 additions & 0 deletions errors/missing-document-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Missing Document Components

#### Why This Error Occurred

In your custom `pages/_document` an expected sub-component was not rendered.

#### Possible Ways to Fix It

Make sure to import and render all of the expected `Document` components.

### Useful Links

- [Custom Document Docs](https://nextjs.org/docs/advanced-features/custom-document)
6 changes: 6 additions & 0 deletions packages/next/next-server/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ export type DocumentInitialProps = RenderPageResult & {
export type DocumentProps = DocumentInitialProps & {
__NEXT_DATA__: NEXT_DATA
dangerousAsPath: string
docComponentsRendered: {
Html?: boolean
Main?: boolean
Head?: boolean
NextScript?: boolean
}
buildManifest: BuildManifest
ampPath: string
inAmpMode: boolean
Expand Down
30 changes: 30 additions & 0 deletions packages/next/next-server/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
loadGetInitialProps,
NextComponentType,
RenderPage,
DocumentProps,
} from '../lib/utils'
import { tryGetPreviewData, __ApiPreviewProps } from './api-utils'
import { denormalizePagePath } from './denormalize-page-path'
Expand Down Expand Up @@ -158,6 +159,7 @@ function renderDocument(
Document: DocumentType,
{
buildManifest,
docComponentsRendered,
props,
docProps,
pathname,
Expand Down Expand Up @@ -188,6 +190,7 @@ function renderDocument(
devOnlyCacheBusterQueryString,
}: RenderOpts & {
props: any
docComponentsRendered: DocumentProps['docComponentsRendered']
docProps: DocumentInitialProps
pathname: string
query: ParsedUrlQuery
Expand Down Expand Up @@ -233,6 +236,7 @@ function renderDocument(
appGip, // whether the _app has getInitialProps
},
buildManifest,
docComponentsRendered,
dangerousAsPath,
canonicalBase,
ampPath,
Expand Down Expand Up @@ -759,8 +763,11 @@ export async function renderToHTML(
renderOpts.inAmpMode = inAmpMode
renderOpts.hybridAmp = hybridAmp

const docComponentsRendered: DocumentProps['docComponentsRendered'] = {}

let html = renderDocument(Document, {
...renderOpts,
docComponentsRendered,
buildManifest: filteredBuildManifest,
// Only enabled in production as development mode has features relying on HMR (style injection for example)
unstable_runtimeJS:
Expand All @@ -787,6 +794,29 @@ export async function renderToHTML(
devOnlyCacheBusterQueryString,
})

if (process.env.NODE_ENV !== 'production') {
const nonRenderedComponents = []
const expectedDocComponents = ['Main', 'Head', 'NextScript', 'Html']

for (const comp of expectedDocComponents) {
if (!(docComponentsRendered as any)[comp]) {
nonRenderedComponents.push(comp)
}
}
const plural = nonRenderedComponents.length !== 1 ? 's' : ''

if (nonRenderedComponents.length) {
console.warn(
`Expected Document Component${plural} ${nonRenderedComponents.join(
', '
)} ${
plural ? 'were' : 'was'
} not rendered. Make sure you render them in your custom \`_document\`\n` +
`See more info here https://err.sh/next.js/missing-document-component`
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
)
}
}

if (inAmpMode && html) {
// inject HTML to AMP_RENDER_TARGET to allow rendering
// directly to body in AMP mode
Expand Down
19 changes: 17 additions & 2 deletions packages/next/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,12 @@ export function Html(
HTMLHtmlElement
>
) {
const { inAmpMode } = useContext(DocumentComponentContext)
const { inAmpMode, docComponentsRendered } = useContext(
DocumentComponentContext
)

docComponentsRendered.Html = true

return (
<html
{...props}
Expand Down Expand Up @@ -288,6 +293,8 @@ export class Head extends Component<
} = this.context
const disableRuntimeJS = unstable_runtimeJS === false

this.context.docComponentsRendered.Head = true

let { head } = this.context
let children = this.props.children
// show a warning if Head contains <title> (only in development)
Expand Down Expand Up @@ -501,7 +508,12 @@ export class Head extends Component<
}

export function Main() {
const { inAmpMode, html } = useContext(DocumentComponentContext)
const { inAmpMode, html, docComponentsRendered } = useContext(
DocumentComponentContext
)

docComponentsRendered.Main = true

if (inAmpMode) return <>{AMP_RENDER_TARGET}</>
return <div id="__next" dangerouslySetInnerHTML={{ __html: html }} />
}
Expand Down Expand Up @@ -642,10 +654,13 @@ export class NextScript extends Component<OriginProps> {
inAmpMode,
buildManifest,
unstable_runtimeJS,
docComponentsRendered,
devOnlyCacheBusterQueryString,
} = this.context
const disableRuntimeJS = unstable_runtimeJS === false

docComponentsRendered.NextScript = true

if (inAmpMode) {
if (process.env.NODE_ENV === 'production') {
return null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Index() {
return <p>Index page</p>
}
159 changes: 159 additions & 0 deletions test/integration/missing-document-component-error/test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/* eslint-env jest */

import fs from 'fs-extra'
import { join } from 'path'
import {
findPort,
killApp,
launchApp,
check,
renderViaHTTP,
} from 'next-test-utils'

jest.setTimeout(1000 * 60 * 2)

const appDir = join(__dirname, '..')
const docPath = join(appDir, 'pages/_document.js')
let appPort
let app

const checkMissing = async (missing = [], docContent) => {
await fs.writeFile(docPath, docContent)
let stderr = ''

appPort = await findPort()
app = await launchApp(appDir, appPort, {
onStderr(msg) {
stderr += msg || ''
},
})

await renderViaHTTP(appPort, '/')

await check(() => stderr, new RegExp(`missing-document-component`))
await check(() => stderr, new RegExp(`${missing.join(', ')}`))

await killApp(app)
await fs.remove(docPath)
}

describe('Missing _document components error', () => {
it('should detect missing Html component', async () => {
await checkMissing(
['Html'],
`
import Document, { Head, Main, NextScript } from 'next/document'

class MyDocument extends Document {
render() {
return (
<html>
<Head />
<body>
<Main />
<NextScript />
</body>
</html>
)
}
}

export default MyDocument
`
)
})

it('should detect missing Head component', async () => {
await checkMissing(
['Head'],
`
import Document, { Html, Main, NextScript } from 'next/document'

class MyDocument extends Document {
render() {
return (
<Html>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}

export default MyDocument
`
)
})

it('should detect missing Main component', async () => {
await checkMissing(
['Main'],
`
import Document, { Html, Head, NextScript } from 'next/document'

class MyDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<NextScript />
</body>
</Html>
)
}
}

export default MyDocument
`
)
})

it('should detect missing NextScript component', async () => {
await checkMissing(
['NextScript'],
`
import Document, { Html, Head, Main } from 'next/document'

class MyDocument extends Document {
render() {
return (
<Html>
<body>
<Main />
</body>
</Html>
)
}
}

export default MyDocument
`
)
})

it('should detect multiple missing document components', async () => {
await checkMissing(
['Head', 'NextScript'],
`
import Document, { Html, Main } from 'next/document'

class MyDocument extends Document {
render() {
return (
<Html>
<body>
<Main />
</body>
</Html>
)
}
}

export default MyDocument
`
)
})
})