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

Check required root layout tags #41120

Merged
merged 5 commits into from Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/next/server/app-render.tsx
Expand Up @@ -685,6 +685,7 @@ export async function renderToHTMLOrFlight(
serverCSSManifest = {},
supportsDynamicHTML,
ComponentMod,
dev,
} = renderOpts

patchFetch(ComponentMod)
Expand Down Expand Up @@ -1370,6 +1371,7 @@ export async function renderToHTMLOrFlight(
flushEffectHandler,
flushEffectsToHead: true,
polyfills,
dev,
})
} catch (err: any) {
// TODO-APP: show error overlay in development. `element` should probably be wrapped in AppRouter for this case.
Expand Down Expand Up @@ -1401,6 +1403,7 @@ export async function renderToHTMLOrFlight(
flushEffectHandler,
flushEffectsToHead: true,
polyfills,
dev,
})
}
}
Expand Down
41 changes: 41 additions & 0 deletions packages/next/server/node-web-streams-helper.ts
Expand Up @@ -257,16 +257,56 @@ export function createSuffixStream(
})
}

export function createRootLayoutValidatorStream(): TransformStream<
Uint8Array,
Uint8Array
> {
let foundHtml = false
let foundHead = false
let foundBody = false

return new TransformStream({
async transform(chunk, controller) {
const content = decodeText(chunk)
hanneslund marked this conversation as resolved.
Show resolved Hide resolved
if (!foundHtml && content.includes('<html')) {
foundHtml = true
}
if (!foundHead && content.includes('<head')) {
foundHead = true
}
if (!foundBody && content.includes('<body')) {
foundBody = true
}
controller.enqueue(chunk)
},
flush(controller) {
const missingTags = [
foundHtml ? null : 'html',
foundHead ? null : 'head',
foundBody ? null : 'body',
].filter(nonNullable)

if (missingTags.length > 0) {
controller.error(
'Missing required root layout tags: ' + missingTags.join(', ')
)
}
},
})
}

export async function continueFromInitialStream(
renderStream: ReactReadableStream,
{
dev,
suffix,
dataStream,
generateStaticHTML,
flushEffectHandler,
flushEffectsToHead,
polyfills,
}: {
dev?: boolean
suffix?: string
dataStream?: ReadableStream<Uint8Array>
generateStaticHTML: boolean
Expand Down Expand Up @@ -312,6 +352,7 @@ export async function continueFromInitialStream(
: ''
return polyfillScripts + flushEffectsContent
}),
dev ? createRootLayoutValidatorStream() : null,
].filter(nonNullable)

return transforms.reduce(
Expand Down
Expand Up @@ -2,8 +2,11 @@ import path from 'path'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import webdriver from 'next-webdriver'
import { check, renderViaHTTP } from 'next-test-utils'

describe('app-dir root layout', () => {
const isDev = (global as any).isNextDev

describe('app-dir mpa navigation', () => {
if ((global as any).isNextDeploy) {
it('should skip next deploy for now', () => {})
return
Expand All @@ -18,9 +21,9 @@ describe('app-dir mpa navigation', () => {
beforeAll(async () => {
next = await createNext({
files: {
app: new FileRef(path.join(__dirname, 'mpa-navigation/app')),
app: new FileRef(path.join(__dirname, 'root-layout/app')),
'next.config.js': new FileRef(
path.join(__dirname, 'mpa-navigation/next.config.js')
path.join(__dirname, 'root-layout/next.config.js')
),
},
dependencies: {
Expand All @@ -31,6 +34,30 @@ describe('app-dir mpa navigation', () => {
})
afterAll(() => next.destroy())

if (isDev) {
describe('Missing required tags', () => {
it('should error on page load', async () => {
const outputIndex = next.cliOutput.length
renderViaHTTP(next.url, '/missing-tags').catch(() => {})
await check(
() => next.cliOutput.slice(outputIndex),
/Missing required root layout tags: html, head, body/
)
})

it('should error on page navigation', async () => {
const outputIndex = next.cliOutput.length
const browser = await webdriver(next.url, '/has-tags')
await browser.elementByCss('a').click()

await check(
() => next.cliOutput.slice(outputIndex),
/Missing required root layout tags: html, head, body/
)
})
})
}

describe('Should do a mpa navigation when switching root layout', () => {
it('should work with basic routes', async () => {
const browser = await webdriver(next.url, '/basic-route')
Expand Down
@@ -0,0 +1,10 @@
export default function Root({ children }) {
return (
<html>
<head>
<title>Hello World</title>
</head>
<body>{children}</body>
</html>
)
}
@@ -0,0 +1,5 @@
import Link from 'next/link'

export default function Page() {
return <Link href="/missing-tags">To incorrect root layout</Link>
}
@@ -0,0 +1,3 @@
export default function Root({ children }) {
return children
}
@@ -0,0 +1,3 @@
export default function Page() {
return <p>WORLD!</p>
}