Skip to content

Commit

Permalink
Create root layout
Browse files Browse the repository at this point in the history
  • Loading branch information
Hannes Bornö committed Oct 19, 2022
1 parent 2d1f729 commit 7afa5e1
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 4 deletions.
@@ -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,
},
}

0 comments on commit 7afa5e1

Please sign in to comment.