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

Create root layout #41523

Merged
merged 14 commits into from Oct 23, 2022
@@ -1,4 +1,4 @@
import * as React from 'react'
import React from 'react'
import {
Dialog,
DialogBody,
Expand Down Expand Up @@ -32,16 +32,16 @@ export const RootLayoutError: React.FC<RootLayoutErrorProps> =
<DialogContent>
<DialogHeader className="nextjs-container-root-layout-error-header">
<h4 id="nextjs__container_root_layout_error_label">
Root layout error
Missing required tags
</h4>
</DialogHeader>
<DialogBody className="nextjs-container-root-layout-error-body">
<Terminal content={message} />
<footer>
<p id="nextjs__container_root_layout_error_desc">
<small>
This error occurred during the build process and can only be
dismissed by fixing the error.
This error and can only be dismissed by providing all
required tags.
</small>
</p>
</footer>
Expand Down
85 changes: 85 additions & 0 deletions packages/next/lib/verifyRootLayout.ts
@@ -0,0 +1,85 @@
import path from 'path'
import { promises as fs } from 'fs'
import chalk from 'next/dist/compiled/chalk'
import * as Log from '../build/output/log'

const globOrig =
require('next/dist/compiled/glob') as typeof import('next/dist/compiled/glob')
const glob = (cwd: string, pattern: string): Promise<string[]> => {
return new Promise((resolve, reject) => {
globOrig(pattern, { cwd }, (err, files) => {
if (err) {
return reject(err)
}
resolve(files)
})
})
}

function getRootLayout(isTs: boolean) {
if (isTs) {
return `export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
`
}

return `export default function RootLayout({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
`
}

export async function verifyRootLayout({
dir,
appDir,
tsconfigPath,
}: {
dir: string
appDir: string
tsconfigPath: string
}) {
try {
// Only create root layout if no other layout exists
const layoutFiles = await glob(dir, 'app/**/layout.{js,jsx,ts,tsx}')
const hasLayout = layoutFiles.length !== 0
if (!hasLayout) {
const resolvedTsConfigPath = path.join(dir, tsconfigPath)
const hasTsConfig = await fs.access(resolvedTsConfigPath).then(
() => true,
() => false
)

const rootLayoutPath = path.join(
appDir,
`layout.${hasTsConfig ? 'tsx' : 'js'}`
)
await fs.writeFile(rootLayoutPath, getRootLayout(hasTsConfig))
console.log(
chalk.green(
`${chalk.bold(
'appDir'
)} is enabled but you're missing a root layout, we created ${chalk.bold(
`app/layout.${hasTsConfig ? 'tsx' : 'js'}`
)} for you.`
) + '\n'
)
}
} catch (error) {
Log.error('Failed to create root layout', error)
}
}
8 changes: 8 additions & 0 deletions packages/next/server/dev/next-dev-server.ts
Expand Up @@ -26,6 +26,7 @@ import { findPagesDir } from '../../lib/find-pages-dir'
import loadCustomRoutes from '../../lib/load-custom-routes'
import { verifyTypeScriptSetup } from '../../lib/verifyTypeScriptSetup'
import { verifyPartytownSetup } from '../../lib/verify-partytown-setup'
import { verifyRootLayout } from '../../lib/verifyRootLayout'
import {
PHASE_DEVELOPMENT_SERVER,
CLIENT_STATIC_FILES_PATH,
Expand Down Expand Up @@ -658,6 +659,13 @@ export default class DevServer extends Server {
setGlobal('phase', PHASE_DEVELOPMENT_SERVER)

await this.verifyTypeScript()
if (this.appDir) {
await verifyRootLayout({
dir: this.dir,
appDir: this.appDir,
tsconfigPath: this.nextConfig.typescript.tsconfigPath,
})
}
this.customRoutes = await loadCustomRoutes(this.nextConfig)

// reload router
Expand Down
112 changes: 112 additions & 0 deletions test/e2e/app-dir/create-root-layout.test.ts
@@ -0,0 +1,112 @@
import path from 'path'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import webdriver from 'next-webdriver'

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

if (!isDev) {
it('should only run in dev', () => {})
return
}

if (process.env.NEXT_TEST_REACT_VERSION === '^17') {
it('should skip for react v17', () => {})
return
}
let next: NextInstance

describe('page.js', () => {
beforeAll(async () => {
next = await createNext({
files: {
'app/page.js': new FileRef(
path.join(__dirname, 'create-root-layout/app/page.js')
),
'next.config.js': new FileRef(
path.join(__dirname, 'create-root-layout/next.config.js')
),
},
dependencies: {
react: 'experimental',
'react-dom': 'experimental',
},
})
})
afterAll(() => next.destroy())

it('create root layout', async () => {
const outputIndex = next.cliOutput.length
const browser = await webdriver(next.url, '/')

expect(await browser.elementById('page-text').text()).toBe('Hello world!')

expect(next.cliOutput.slice(outputIndex)).toInclude(
"appDir is enabled but you're missing a root layout, we created app/layout.js for you."
)

expect(await next.readFile('app/layout.js')).toMatchInlineSnapshot(`
"export default function RootLayout({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
"
`)
})
})

describe('page.tsx', () => {
beforeAll(async () => {
next = await createNext({
files: {
'app/page.tsx': new FileRef(
path.join(__dirname, 'create-root-layout/app/page.js')
),
'next.config.js': new FileRef(
path.join(__dirname, 'create-root-layout/next.config.js')
),
},
dependencies: {
react: 'experimental',
'react-dom': 'experimental',
typescript: 'latest',
'@types/react': 'latest',
'@types/node': 'latest',
},
})
})
afterAll(() => next.destroy())

it('create root layout', async () => {
const outputIndex = next.cliOutput.length
const browser = await webdriver(next.url, '/')

expect(await browser.elementById('page-text').text()).toBe('Hello world!')

expect(next.cliOutput.slice(outputIndex)).toInclude(
"appDir is enabled but you're missing a root layout, we created app/layout.tsx for you."
)

expect(await next.readFile('app/layout.tsx')).toMatchInlineSnapshot(`
"export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
"
`)
})
})
})
3 changes: 3 additions & 0 deletions test/e2e/app-dir/create-root-layout/app/page.js
@@ -0,0 +1,3 @@
export default function Page() {
return <p id="page-text">Hello world!</p>
}
5 changes: 5 additions & 0 deletions test/e2e/app-dir/create-root-layout/next.config.js
@@ -0,0 +1,5 @@
module.exports = {
experimental: {
appDir: true,
},
}