mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-13 09:33:54 +00:00
feat(nuxt): scan and register layouts in nested folders (#20190)
Co-authored-by: Daniel Roe <daniel@roe.dev>
This commit is contained in:
parent
b52548d915
commit
00917a1bd8
@ -178,3 +178,24 @@ definePageMeta({
|
|||||||
::alert{type=warning}
|
::alert{type=warning}
|
||||||
If you use `<NuxtLayout>` within your pages, make sure it is not the root element (or disable layout/page transitions).
|
If you use `<NuxtLayout>` within your pages, make sure it is not the root element (or disable layout/page transitions).
|
||||||
::
|
::
|
||||||
|
|
||||||
|
## Layout Names
|
||||||
|
|
||||||
|
If you have a layout in nested directories such as:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
| layouts/
|
||||||
|
--| base/
|
||||||
|
----| foo/
|
||||||
|
------| Layout.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
... then the layout's name will be based on its own path directory and filename, with duplicate segments being removed. Therefore, the layout's name will be:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<BaseFooLayout />
|
||||||
|
```
|
||||||
|
|
||||||
|
::alert
|
||||||
|
For clarity, we recommend that the layout's filename matches its name. (So, in the example above, you could rename `Layout.vue` to be `BaseFooLayout.vue`.)
|
||||||
|
::
|
||||||
|
@ -8,6 +8,8 @@ import { hyphenate } from '@vue/shared'
|
|||||||
import { withTrailingSlash } from 'ufo'
|
import { withTrailingSlash } from 'ufo'
|
||||||
import type { Component, ComponentsDir } from 'nuxt/schema'
|
import type { Component, ComponentsDir } from 'nuxt/schema'
|
||||||
|
|
||||||
|
import { resolveComponentName } from '../core/utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan the components inside different components folders
|
* Scan the components inside different components folders
|
||||||
* and return a unique list of components
|
* and return a unique list of components
|
||||||
@ -157,33 +159,6 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
|
|||||||
return components
|
return components
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveComponentName (fileName: string, prefixParts: string[]) {
|
|
||||||
/**
|
|
||||||
* Array of fileName parts splitted by case, / or -
|
|
||||||
* @example third-component -> ['third', 'component']
|
|
||||||
* @example AwesomeComponent -> ['Awesome', 'Component']
|
|
||||||
*/
|
|
||||||
const fileNameParts = splitByCase(fileName)
|
|
||||||
const fileNamePartsContent = fileNameParts.join('/').toLowerCase()
|
|
||||||
const componentNameParts: string[] = [...prefixParts]
|
|
||||||
let index = prefixParts.length - 1
|
|
||||||
const matchedSuffix: string[] = []
|
|
||||||
while (index >= 0) {
|
|
||||||
matchedSuffix.unshift(...splitByCase(prefixParts[index] || '').map(p => p.toLowerCase()))
|
|
||||||
const matchedSuffixContent = matchedSuffix.join('/')
|
|
||||||
if ((fileNamePartsContent === matchedSuffixContent || fileNamePartsContent.startsWith(matchedSuffixContent + '/')) ||
|
|
||||||
// e.g Item/Item/Item.vue -> Item
|
|
||||||
(prefixParts[index].toLowerCase() === fileNamePartsContent &&
|
|
||||||
prefixParts[index + 1] &&
|
|
||||||
prefixParts[index] === prefixParts[index + 1])) {
|
|
||||||
componentNameParts.length = index
|
|
||||||
}
|
|
||||||
index--
|
|
||||||
}
|
|
||||||
|
|
||||||
return pascalCase(componentNameParts) + pascalCase(fileNameParts)
|
|
||||||
}
|
|
||||||
|
|
||||||
function warnAboutDuplicateComponent (componentName: string, filePath: string, duplicatePath: string) {
|
function warnAboutDuplicateComponent (componentName: string, filePath: string, duplicatePath: string) {
|
||||||
logger.warn(`Two component files resolving to the same name \`${componentName}\`:\n` +
|
logger.warn(`Two component files resolving to the same name \`${componentName}\`:\n` +
|
||||||
`\n - ${filePath}` +
|
`\n - ${filePath}` +
|
||||||
|
@ -110,9 +110,9 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
|
|||||||
app.layouts = {}
|
app.layouts = {}
|
||||||
for (const config of nuxt.options._layers.map(layer => layer.config)) {
|
for (const config of nuxt.options._layers.map(layer => layer.config)) {
|
||||||
const layoutDir = (config.rootDir === nuxt.options.rootDir ? nuxt.options : config).dir?.layouts || 'layouts'
|
const layoutDir = (config.rootDir === nuxt.options.rootDir ? nuxt.options : config).dir?.layouts || 'layouts'
|
||||||
const layoutFiles = await resolveFiles(config.srcDir, `${layoutDir}/*{${nuxt.options.extensions.join(',')}}`)
|
const layoutFiles = await resolveFiles(config.srcDir, `${layoutDir}/**/*{${nuxt.options.extensions.join(',')}}`)
|
||||||
for (const file of layoutFiles) {
|
for (const file of layoutFiles) {
|
||||||
const name = getNameFromPath(file)
|
const name = getNameFromPath(file, resolve(config.srcDir, layoutDir))
|
||||||
app.layouts[name] = app.layouts[name] || { name, file }
|
app.layouts[name] = app.layouts[name] || { name, file }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,43 @@
|
|||||||
import { basename, extname } from 'pathe'
|
import { basename, dirname, extname, normalize } from 'pathe'
|
||||||
import { kebabCase } from 'scule'
|
import { kebabCase, pascalCase, splitByCase } from 'scule'
|
||||||
|
import { withTrailingSlash } from 'ufo'
|
||||||
|
|
||||||
export function getNameFromPath (path: string) {
|
export function getNameFromPath (path: string, relativeTo?: string) {
|
||||||
return kebabCase(basename(path).replace(extname(path), '')).replace(/["']/g, '')
|
const relativePath = relativeTo
|
||||||
|
? normalize(path).replace(withTrailingSlash(normalize(relativeTo)), '')
|
||||||
|
: basename(path)
|
||||||
|
const prefixParts = splitByCase(dirname(relativePath))
|
||||||
|
const fileName = basename(relativePath, extname(relativePath))
|
||||||
|
return kebabCase(resolveComponentName(fileName.toLowerCase() === 'index' ? '' : fileName, prefixParts)).replace(/["']/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasSuffix (path: string, suffix: string) {
|
export function hasSuffix (path: string, suffix: string) {
|
||||||
return basename(path).replace(extname(path), '').endsWith(suffix)
|
return basename(path).replace(extname(path), '').endsWith(suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveComponentName (fileName: string, prefixParts: string[]) {
|
||||||
|
/**
|
||||||
|
* Array of fileName parts splitted by case, / or -
|
||||||
|
* @example third-component -> ['third', 'component']
|
||||||
|
* @example AwesomeComponent -> ['Awesome', 'Component']
|
||||||
|
*/
|
||||||
|
const fileNameParts = splitByCase(fileName)
|
||||||
|
const fileNamePartsContent = fileNameParts.join('/').toLowerCase()
|
||||||
|
const componentNameParts: string[] = [...prefixParts]
|
||||||
|
let index = prefixParts.length - 1
|
||||||
|
const matchedSuffix: string[] = []
|
||||||
|
while (index >= 0) {
|
||||||
|
matchedSuffix.unshift(...splitByCase(prefixParts[index] || '').map(p => p.toLowerCase()))
|
||||||
|
const matchedSuffixContent = matchedSuffix.join('/')
|
||||||
|
if ((fileNamePartsContent === matchedSuffixContent || fileNamePartsContent.startsWith(matchedSuffixContent + '/')) ||
|
||||||
|
// e.g Item/Item/Item.vue -> Item
|
||||||
|
(prefixParts[index].toLowerCase() === fileNamePartsContent &&
|
||||||
|
prefixParts[index + 1] &&
|
||||||
|
prefixParts[index] === prefixParts[index + 1])) {
|
||||||
|
componentNameParts.length = index
|
||||||
|
}
|
||||||
|
index--
|
||||||
|
}
|
||||||
|
|
||||||
|
return pascalCase(componentNameParts) + pascalCase(fileNameParts)
|
||||||
|
}
|
||||||
|
@ -179,6 +179,55 @@ describe('resolveApp', () => {
|
|||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('resolves nested layouts correctly', async () => {
|
||||||
|
const app = await getResolvedApp([
|
||||||
|
'layouts/default.vue',
|
||||||
|
'layouts/some/layout.vue',
|
||||||
|
'layouts/SomeOther.vue',
|
||||||
|
'layouts/SomeOther/Thing/Index.vue',
|
||||||
|
'layouts/thing/thing/thing.vue',
|
||||||
|
'layouts/desktop-base/base.vue',
|
||||||
|
'layouts/some.vue',
|
||||||
|
'layouts/SomeOther/layout.ts'
|
||||||
|
])
|
||||||
|
expect(app.layouts).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
"file": "<rootDir>/layouts/default.vue",
|
||||||
|
"name": "default",
|
||||||
|
},
|
||||||
|
"desktop-base": {
|
||||||
|
"file": "<rootDir>/layouts/desktop-base/base.vue",
|
||||||
|
"name": "desktop-base",
|
||||||
|
},
|
||||||
|
"some": {
|
||||||
|
"file": "<rootDir>/layouts/some.vue",
|
||||||
|
"name": "some",
|
||||||
|
},
|
||||||
|
"some-layout": {
|
||||||
|
"file": "<rootDir>/layouts/some/layout.vue",
|
||||||
|
"name": "some-layout",
|
||||||
|
},
|
||||||
|
"some-other": {
|
||||||
|
"file": "<rootDir>/layouts/SomeOther.vue",
|
||||||
|
"name": "some-other",
|
||||||
|
},
|
||||||
|
"some-other-layout": {
|
||||||
|
"file": "<rootDir>/layouts/SomeOther/layout.ts",
|
||||||
|
"name": "some-other-layout",
|
||||||
|
},
|
||||||
|
"some-other-thing": {
|
||||||
|
"file": "<rootDir>/layouts/SomeOther/Thing/Index.vue",
|
||||||
|
"name": "some-other-thing",
|
||||||
|
},
|
||||||
|
"thing": {
|
||||||
|
"file": "<rootDir>/layouts/thing/thing/thing.vue",
|
||||||
|
"name": "thing",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function getResolvedApp (files: Array<string | { name: string, contents: string }>) {
|
async function getResolvedApp (files: Array<string | { name: string, contents: string }>) {
|
||||||
|
@ -2,7 +2,8 @@ import { resolve } from 'node:path'
|
|||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import type { ComponentsDir } from 'nuxt/schema'
|
import type { ComponentsDir } from 'nuxt/schema'
|
||||||
|
|
||||||
import { resolveComponentName, scanComponents } from '../src/components/scan'
|
import { scanComponents } from '../src/components/scan'
|
||||||
|
import { resolveComponentName } from '../src/core/utils'
|
||||||
|
|
||||||
const fixtureDir = resolve(__dirname, 'fixture')
|
const fixtureDir = resolve(__dirname, 'fixture')
|
||||||
const rFixture = (...p: string[]) => resolve(fixtureDir, ...p)
|
const rFixture = (...p: string[]) => resolve(fixtureDir, ...p)
|
||||||
|
Loading…
Reference in New Issue
Block a user