diff --git a/packages/next/lib/verifyRootLayout.ts b/packages/next/lib/verifyRootLayout.ts
index b20e69accb149c2..144184b784f6541 100644
--- a/packages/next/lib/verifyRootLayout.ts
+++ b/packages/next/lib/verifyRootLayout.ts
@@ -26,7 +26,7 @@ function getRootLayout(isTs: boolean) {
}) {
return (
-
+ {children}
)
@@ -37,7 +37,7 @@ function getRootLayout(isTs: boolean) {
return `export default function RootLayout({ children }) {
return (
-
+ {children}
)
@@ -45,6 +45,19 @@ function getRootLayout(isTs: boolean) {
`
}
+function getHead() {
+ return `export default function Head() {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+`
+}
+
export async function verifyRootLayout({
dir,
appDir,
@@ -63,15 +76,38 @@ export async function verifyRootLayout({
appDir,
`**/layout.{${pageExtensions.join(',')}}`
)
- const hasLayout = layoutFiles.length !== 0
-
const normalizedPagePath = pagePath.replace(`${APP_DIR_ALIAS}/`, '')
- const firstSegmentValue = normalizedPagePath.split('/')[0]
- const pageRouteGroup = firstSegmentValue.startsWith('(')
- ? firstSegmentValue
- : undefined
+ const pagePathSegments = normalizedPagePath.split('/')
+
+ // Find an available dir to place the layout file in, the layout file can't affect any other layout.
+ // Place the layout as close to app/ as possible.
+ let availableDir: string | undefined
+
+ if (layoutFiles.length === 0) {
+ // If there's no other layout file we can place the layout file in the app dir.
+ // However, if the page is within a route group directly under app (e.g. app/(routegroup)/page.js)
+ // prefer creating the root layout in that route group.
+ const firstSegmentValue = pagePathSegments[0]
+ availableDir = firstSegmentValue.startsWith('(') ? firstSegmentValue : ''
+ } else {
+ pagePathSegments.pop() // remove the page from segments
+
+ let currentSegments: string[] = []
+ for (const segment of pagePathSegments) {
+ currentSegments.push(segment)
+ // Find the dir closest to app/ where a layout can be created without affecting other layouts.
+ if (
+ !layoutFiles.some((file) =>
+ file.startsWith(currentSegments.join('/'))
+ )
+ ) {
+ availableDir = currentSegments.join('/')
+ break
+ }
+ }
+ }
- if (pageRouteGroup || !hasLayout) {
+ if (typeof availableDir === 'string') {
const resolvedTsConfigPath = path.join(dir, tsconfigPath)
const hasTsConfig = await fs.access(resolvedTsConfigPath).then(
() => true,
@@ -80,19 +116,35 @@ export async function verifyRootLayout({
const rootLayoutPath = path.join(
appDir,
- // If the page is within a route group directly under app (e.g. app/(routegroup)/page.js)
- // prefer creating the root layout in that route group. Otherwise create the root layout in the app root.
- pageRouteGroup ? pageRouteGroup : '',
+ availableDir,
`layout.${hasTsConfig ? 'tsx' : 'js'}`
)
await fs.writeFile(rootLayoutPath, getRootLayout(hasTsConfig))
+ const headPath = path.join(
+ appDir,
+ availableDir,
+ `head.${hasTsConfig ? 'tsx' : 'js'}`
+ )
+ const hasHead = await fs.access(headPath).then(
+ () => true,
+ () => false
+ )
+
+ if (!hasHead) {
+ await fs.writeFile(headPath, getHead())
+ }
+
console.log(
chalk.green(
`\nYour page ${chalk.bold(
`app/${normalizedPagePath}`
)} did not have a root layout, we created ${chalk.bold(
`app${rootLayoutPath.replace(appDir, '')}`
- )} for you.`
+ )}${
+ !hasHead
+ ? ` and ${chalk.bold(`app${headPath.replace(appDir, '')}`)}`
+ : ''
+ } for you.`
) + '\n'
)
diff --git a/test/e2e/app-dir/create-root-layout.test.ts b/test/e2e/app-dir/create-root-layout.test.ts
index 66eb1e85e2eaa5c..da4eeab95e6404c 100644
--- a/test/e2e/app-dir/create-root-layout.test.ts
+++ b/test/e2e/app-dir/create-root-layout.test.ts
@@ -2,6 +2,7 @@ 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 } from 'next-test-utils'
describe('app-dir create root layout', () => {
const isDev = (global as any).isNextDev
@@ -23,9 +24,7 @@ describe('app-dir create root layout', () => {
beforeAll(async () => {
next = await createNext({
files: {
- 'app/page.js': new FileRef(
- path.join(__dirname, 'create-root-layout/app/page.js')
- ),
+ app: new FileRef(path.join(__dirname, 'create-root-layout/app')),
'next.config.js': new FileRef(
path.join(__dirname, 'create-root-layout/next.config.js')
),
@@ -40,27 +39,44 @@ describe('app-dir create root layout', () => {
it('create root layout', async () => {
const outputIndex = next.cliOutput.length
- const browser = await webdriver(next.url, '/')
+ const browser = await webdriver(next.url, '/route')
expect(await browser.elementById('page-text').text()).toBe(
'Hello world!'
)
- expect(next.cliOutput.slice(outputIndex)).toInclude(
- 'Your page app/page.js did not have a root layout, we created app/layout.js for you.'
+ await check(
+ () => next.cliOutput.slice(outputIndex),
+ /did not have a root layout/
+ )
+ expect(next.cliOutput.slice(outputIndex)).toMatch(
+ 'Your page app/route/page.js did not have a root layout, we created app/layout.js and app/head.js for you.'
)
expect(await next.readFile('app/layout.js')).toMatchInlineSnapshot(`
- "export default function RootLayout({ children }) {
- return (
-
-
- {children}
-
- )
- }
- "
- `)
+ "export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ )
+ }
+ "
+ `)
+
+ expect(await next.readFile('app/head.js')).toMatchInlineSnapshot(`
+ "export default function Head() {
+ return (
+ <>
+
+
+
+ >
+ )
+ }
+ "
+ `)
})
})
@@ -85,28 +101,113 @@ describe('app-dir create root layout', () => {
it('create root layout', async () => {
const outputIndex = next.cliOutput.length
- const browser = await webdriver(next.url, '/path2')
+ const browser = await webdriver(next.url, '/')
expect(await browser.elementById('page-text').text()).toBe(
- 'Hello world 2'
+ 'Hello world'
)
+ await check(
+ () => next.cliOutput.slice(outputIndex),
+ /did not have a root layout/
+ )
expect(next.cliOutput.slice(outputIndex)).toInclude(
- 'Your page app/(group2)/path2/page.js did not have a root layout, we created app/(group2)/layout.js for you.'
+ 'Your page app/(group)/page.js did not have a root layout, we created app/(group)/layout.js and app/(group)/head.js for you.'
)
- expect(await next.readFile('app/(group2)/layout.js'))
+ expect(await next.readFile('app/(group)/layout.js'))
+ .toMatchInlineSnapshot(`
+ "export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ )
+ }
+ "
+ `)
+
+ expect(await next.readFile('app/(group)/head.js'))
.toMatchInlineSnapshot(`
- "export default function RootLayout({ children }) {
- return (
-
-
- {children}
-
+ "export default function Head() {
+ return (
+ <>
+
+
+
+ >
+ )
+ }
+ "
+ `)
+ })
+ })
+
+ describe('find available dir', () => {
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ app: new FileRef(
+ path.join(
+ __dirname,
+ 'create-root-layout/app-find-available-dir'
+ )
+ ),
+ '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, '/route/second/inner')
+
+ expect(await browser.elementById('page-text').text()).toBe(
+ 'Hello world'
)
- }
- "
- `)
+
+ await check(
+ () => next.cliOutput.slice(outputIndex),
+ /did not have a root layout/
+ )
+ expect(next.cliOutput.slice(outputIndex)).toInclude(
+ 'Your page app/(group)/route/second/inner/page.js did not have a root layout, we created app/(group)/route/second/layout.js and app/(group)/route/second/head.js for you.'
+ )
+
+ expect(await next.readFile('app/(group)/route/second/layout.js'))
+ .toMatchInlineSnapshot(`
+ "export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ )
+ }
+ "
+ `)
+
+ expect(await next.readFile('app/(group)/route/second/head.js'))
+ .toMatchInlineSnapshot(`
+ "export default function Head() {
+ return (
+ <>
+
+
+
+ >
+ )
+ }
+ "
+ `)
})
})
})
@@ -116,7 +217,7 @@ describe('app-dir create root layout', () => {
next = await createNext({
files: {
'app/page.tsx': new FileRef(
- path.join(__dirname, 'create-root-layout/app/page.js')
+ path.join(__dirname, 'create-root-layout/app/route/page.js')
),
'next.config.js': new FileRef(
path.join(__dirname, 'create-root-layout/next.config.js')
@@ -141,25 +242,42 @@ describe('app-dir create root layout', () => {
'Hello world!'
)
+ await check(
+ () => next.cliOutput.slice(outputIndex),
+ /did not have a root layout/
+ )
expect(next.cliOutput.slice(outputIndex)).toInclude(
- 'Your page app/page.tsx did not have a root layout, we created app/layout.tsx for you.'
+ 'Your page app/page.tsx did not have a root layout, we created app/layout.tsx and app/head.tsx for you.'
)
expect(await next.readFile('app/layout.tsx')).toMatchInlineSnapshot(`
- "export default function RootLayout({
- children,
- }: {
- children: React.ReactNode
- }) {
- return (
-
-
- {children}
-
- )
- }
- "
- `)
+ "export default function RootLayout({
+ children,
+ }: {
+ children: React.ReactNode
+ }) {
+ return (
+
+ {children}
+
+ )
+ }
+ "
+ `)
+
+ expect(await next.readFile('app/head.tsx')).toMatchInlineSnapshot(`
+ "export default function Head() {
+ return (
+ <>
+
+
+
+ >
+ )
+ }
+ "
+ `)
})
})
} else {
@@ -169,7 +287,7 @@ describe('app-dir create root layout', () => {
skipStart: true,
files: {
'app/page.js': new FileRef(
- path.join(__dirname, 'create-root-layout/app/page.js')
+ path.join(__dirname, 'create-root-layout/app/route/page.js')
),
'next.config.js': new FileRef(
path.join(__dirname, 'create-root-layout/next.config.js')
diff --git a/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/first/head.js b/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/first/head.js
new file mode 100644
index 000000000000000..772e640e75cefac
--- /dev/null
+++ b/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/first/head.js
@@ -0,0 +1,9 @@
+export default function Head() {
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/test/e2e/app-dir/create-root-layout/app-group-layout/(group1)/layout.js b/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/first/layout.js
similarity index 85%
rename from test/e2e/app-dir/create-root-layout/app-group-layout/(group1)/layout.js
rename to test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/first/layout.js
index 747270b45987a2b..803f17d863c8ad8 100644
--- a/test/e2e/app-dir/create-root-layout/app-group-layout/(group1)/layout.js
+++ b/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/first/layout.js
@@ -1,7 +1,6 @@
export default function RootLayout({ children }) {
return (
-
{children}
)
diff --git a/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/first/page.js b/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/first/page.js
new file mode 100644
index 000000000000000..faaea962365bb7e
--- /dev/null
+++ b/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/first/page.js
@@ -0,0 +1,3 @@
+export default function Page() {
+ return Hello world
+}
diff --git a/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/second/inner/page.js b/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/second/inner/page.js
new file mode 100644
index 000000000000000..faaea962365bb7e
--- /dev/null
+++ b/test/e2e/app-dir/create-root-layout/app-find-available-dir/(group)/route/second/inner/page.js
@@ -0,0 +1,3 @@
+export default function Page() {
+ return Hello world
+}
diff --git a/test/e2e/app-dir/create-root-layout/app-group-layout/(group)/page.js b/test/e2e/app-dir/create-root-layout/app-group-layout/(group)/page.js
new file mode 100644
index 000000000000000..faaea962365bb7e
--- /dev/null
+++ b/test/e2e/app-dir/create-root-layout/app-group-layout/(group)/page.js
@@ -0,0 +1,3 @@
+export default function Page() {
+ return Hello world
+}
diff --git a/test/e2e/app-dir/create-root-layout/app-group-layout/(group1)/path1/page.js b/test/e2e/app-dir/create-root-layout/app-group-layout/(group1)/path1/page.js
deleted file mode 100644
index 2e60ca9ad638683..000000000000000
--- a/test/e2e/app-dir/create-root-layout/app-group-layout/(group1)/path1/page.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Page() {
- return Hello world 1
-}
diff --git a/test/e2e/app-dir/create-root-layout/app-group-layout/(group2)/path2/page.js b/test/e2e/app-dir/create-root-layout/app-group-layout/(group2)/path2/page.js
deleted file mode 100644
index 4a8d52f9706a252..000000000000000
--- a/test/e2e/app-dir/create-root-layout/app-group-layout/(group2)/path2/page.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Page() {
- return Hello world 2
-}
diff --git a/test/e2e/app-dir/create-root-layout/app/page.js b/test/e2e/app-dir/create-root-layout/app/route/page.js
similarity index 100%
rename from test/e2e/app-dir/create-root-layout/app/page.js
rename to test/e2e/app-dir/create-root-layout/app/route/page.js