Skip to content

Commit

Permalink
Check required root layout tags (vercel#41120)
Browse files Browse the repository at this point in the history
Inspects the stream of rendered HTML in dev to make sure all required tags are rendered. Since navigating to a new root layout causes a full page navigation we don't have to worry about flight requests.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
Hannes Bornö authored and Kikobeats committed Oct 24, 2022
1 parent 10f8a37 commit 6b88b49
Show file tree
Hide file tree
Showing 24 changed files with 105 additions and 3 deletions.
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 @@ -1374,6 +1375,7 @@ export async function renderToHTMLOrFlight(
getServerInsertedHTML,
serverInsertedHTMLToHead: 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 @@ -1405,6 +1407,7 @@ export async function renderToHTMLOrFlight(
getServerInsertedHTML,
serverInsertedHTMLToHead: true,
polyfills,
dev,
})
}
}
Expand Down
43 changes: 43 additions & 0 deletions packages/next/server/node-web-streams-helper.ts
Expand Up @@ -257,16 +257,58 @@ 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) {
if (!foundHtml || !foundHead || !foundBody) {
const content = decodeText(chunk)
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,
getServerInsertedHTML,
serverInsertedHTMLToHead,
polyfills,
}: {
dev?: boolean
suffix?: string
dataStream?: ReadableStream<Uint8Array>
generateStaticHTML: boolean
Expand Down Expand Up @@ -312,6 +354,7 @@ export async function continueFromInitialStream(
: ''
return polyfillScripts + serverInsertedHTML
}),
dev ? createRootLayoutValidatorStream() : null,
].filter(nonNullable)

return transforms.reduce(
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/app-dir/app-alias/app/layout.tsx
@@ -0,0 +1,8 @@
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
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>
}
File renamed without changes.

0 comments on commit 6b88b49

Please sign in to comment.