mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-16 13:48:13 +00:00
fix(nuxt): preserve hyphens in component/layout kebab names (#23902)
This commit is contained in:
parent
f6deb518c2
commit
7500f27235
@ -1,14 +1,12 @@
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import { basename, dirname, extname, join, relative } from 'pathe'
|
||||
import { globby } from 'globby'
|
||||
import { pascalCase, splitByCase } from 'scule'
|
||||
import { kebabCase, pascalCase, splitByCase } from 'scule'
|
||||
import { isIgnored, logger, useNuxt } from '@nuxt/kit'
|
||||
// eslint-disable-next-line vue/prefer-import-from-vue
|
||||
import { hyphenate } from '@vue/shared'
|
||||
import { withTrailingSlash } from 'ufo'
|
||||
import type { Component, ComponentsDir } from 'nuxt/schema'
|
||||
|
||||
import { resolveComponentName } from '../core/utils'
|
||||
import { resolveComponentNameSegments } from '../core/utils'
|
||||
|
||||
/**
|
||||
* Scan the components inside different components folders
|
||||
@ -92,16 +90,16 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
|
||||
}
|
||||
|
||||
const suffix = (mode !== 'all' ? `-${mode}` : '')
|
||||
const componentName = resolveComponentName(fileName, prefixParts)
|
||||
const componentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts)
|
||||
const pascalName = pascalCase(componentNameSegments)
|
||||
|
||||
if (resolvedNames.has(componentName + suffix) || resolvedNames.has(componentName)) {
|
||||
warnAboutDuplicateComponent(componentName, filePath, resolvedNames.get(componentName) || resolvedNames.get(componentName + suffix)!)
|
||||
if (resolvedNames.has(pascalName + suffix) || resolvedNames.has(pascalName)) {
|
||||
warnAboutDuplicateComponent(pascalName, filePath, resolvedNames.get(pascalName) || resolvedNames.get(pascalName + suffix)!)
|
||||
continue
|
||||
}
|
||||
resolvedNames.set(componentName + suffix, filePath)
|
||||
resolvedNames.set(pascalName + suffix, filePath)
|
||||
|
||||
const pascalName = pascalCase(componentName).replace(/["']/g, '')
|
||||
const kebabName = hyphenate(componentName)
|
||||
const kebabName = kebabCase(componentNameSegments)
|
||||
const shortPath = relative(srcDir, filePath)
|
||||
const chunkName = 'components/' + kebabName + suffix
|
||||
|
||||
@ -128,7 +126,7 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
|
||||
}
|
||||
|
||||
// Ignore files like `~/components/index.vue` which end up not having a name at all
|
||||
if (!componentName) {
|
||||
if (!pascalName) {
|
||||
logger.warn(`Component did not resolve to a file name in \`~/${relative(srcDir, filePath)}\`.`)
|
||||
continue
|
||||
}
|
||||
@ -145,7 +143,7 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
|
||||
}
|
||||
// Warn if a user-defined (or prioritized) component conflicts with a previously scanned component
|
||||
if (newPriority > 0 && newPriority === existingPriority) {
|
||||
warnAboutDuplicateComponent(componentName, filePath, existingComponent.filePath)
|
||||
warnAboutDuplicateComponent(pascalName, filePath, existingComponent.filePath)
|
||||
}
|
||||
|
||||
continue
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { basename, dirname, extname, normalize } from 'pathe'
|
||||
import { kebabCase, pascalCase, splitByCase } from 'scule'
|
||||
import { kebabCase, splitByCase } from 'scule'
|
||||
import { withTrailingSlash } from 'ufo'
|
||||
|
||||
export function getNameFromPath (path: string, relativeTo?: string) {
|
||||
@ -8,14 +8,15 @@ export function getNameFromPath (path: string, relativeTo?: string) {
|
||||
: basename(path)
|
||||
const prefixParts = splitByCase(dirname(relativePath))
|
||||
const fileName = basename(relativePath, extname(relativePath))
|
||||
return kebabCase(resolveComponentName(fileName.toLowerCase() === 'index' ? '' : fileName, prefixParts)).replace(/["']/g, '')
|
||||
const segments = resolveComponentNameSegments(fileName.toLowerCase() === 'index' ? '' : fileName, prefixParts).filter(Boolean)
|
||||
return kebabCase(segments).replace(/["']/g, '')
|
||||
}
|
||||
|
||||
export function hasSuffix (path: string, suffix: string) {
|
||||
return basename(path).replace(extname(path), '').endsWith(suffix)
|
||||
}
|
||||
|
||||
export function resolveComponentName (fileName: string, prefixParts: string[]) {
|
||||
export function resolveComponentNameSegments (fileName: string, prefixParts: string[]) {
|
||||
/**
|
||||
* Array of fileName parts splitted by case, / or -
|
||||
* @example third-component -> ['third', 'component']
|
||||
@ -38,6 +39,5 @@ export function resolveComponentName (fileName: string, prefixParts: string[]) {
|
||||
}
|
||||
index--
|
||||
}
|
||||
|
||||
return pascalCase(componentNameParts) + pascalCase(fileNameParts)
|
||||
return [...componentNameParts, ...fileNameParts]
|
||||
}
|
||||
|
42
packages/nuxt/test/naming.test.ts
Normal file
42
packages/nuxt/test/naming.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { pascalCase } from 'scule'
|
||||
import { getNameFromPath, resolveComponentNameSegments } from '../src/core/utils'
|
||||
|
||||
describe('getNameFromPath', () => {
|
||||
const cases: Record<string, string> = {
|
||||
'base.vue': 'base',
|
||||
'base/base.vue': 'base',
|
||||
'base/base-layout.vue': 'base-layout',
|
||||
'base-1-layout': 'base-1-layout'
|
||||
}
|
||||
it.each(Object.keys(cases))('correctly deduplicates segments - %s', (filename) => {
|
||||
expect(getNameFromPath(filename)).toEqual(cases[filename])
|
||||
})
|
||||
})
|
||||
|
||||
const tests: Array<[string, string[], string]> = [
|
||||
['WithClientOnlySetup', ['Client'], 'ClientWithClientOnlySetup'],
|
||||
['ItemHolderItem', ['Item', 'Holder', 'Item'], 'ItemHolderItem'],
|
||||
['Item', ['Item'], 'Item'],
|
||||
['Item', ['Item', 'Item'], 'Item'],
|
||||
['ItemTest', ['Item', 'Test'], 'ItemTest'],
|
||||
['ThingItemTest', ['Item', 'Thing'], 'ItemThingItemTest'],
|
||||
['Item', ['Thing', 'Item'], 'ThingItem'],
|
||||
['Item', ['Item', 'Holder', 'Item'], 'ItemHolderItem'],
|
||||
['ItemHolder', ['Item', 'Holder', 'Item'], 'ItemHolderItemHolder'],
|
||||
['ThingItemTest', ['Item', 'Thing', 'Foo'], 'ItemThingFooThingItemTest'],
|
||||
['ItemIn', ['Item', 'Holder', 'Item', 'In'], 'ItemHolderItemIn'],
|
||||
['Item', ['Item', 'Holder', 'Test'], 'ItemHolderTestItem'],
|
||||
['ItemHolderItem', ['Item', 'Holder', 'Item', 'Holder'], 'ItemHolderItemHolderItem'],
|
||||
['Icones', ['Icon'], 'IconIcones'],
|
||||
['Icon', ['Icones'], 'IconesIcon'],
|
||||
['IconHolder', ['IconHolder'], 'IconHolder'],
|
||||
['GameList', ['Desktop', 'ShareGame', 'Review', 'Detail'], 'DesktopShareGameReviewDetailGameList'],
|
||||
['base-1-layout', [], 'Base1Layout']
|
||||
]
|
||||
|
||||
describe('components:resolveComponentNameSegments', () => {
|
||||
it.each(tests)('resolves %s with prefix parts %s and filename %s', (fileName, prefixParts: string[], result) => {
|
||||
expect(pascalCase(resolveComponentNameSegments(fileName, prefixParts))).toBe(result)
|
||||
})
|
||||
})
|
@ -1,9 +1,8 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import type { ComponentsDir } from 'nuxt/schema'
|
||||
|
||||
import { scanComponents } from '../src/components/scan'
|
||||
import { resolveComponentName } from '../src/core/utils'
|
||||
|
||||
const fixtureDir = resolve(__dirname, 'fixture')
|
||||
const rFixture = (...p: string[]) => resolve(fixtureDir, ...p)
|
||||
@ -244,29 +243,3 @@ it('components:scanComponents', async () => {
|
||||
}
|
||||
expect(scannedComponents).deep.eq(expectedComponents)
|
||||
})
|
||||
|
||||
const tests: Array<[string, string[], string]> = [
|
||||
['WithClientOnlySetup', ['Client'], 'ClientWithClientOnlySetup'],
|
||||
['ItemHolderItem', ['Item', 'Holder', 'Item'], 'ItemHolderItem'],
|
||||
['Item', ['Item'], 'Item'],
|
||||
['Item', ['Item', 'Item'], 'Item'],
|
||||
['ItemTest', ['Item', 'Test'], 'ItemTest'],
|
||||
['ThingItemTest', ['Item', 'Thing'], 'ItemThingItemTest'],
|
||||
['Item', ['Thing', 'Item'], 'ThingItem'],
|
||||
['Item', ['Item', 'Holder', 'Item'], 'ItemHolderItem'],
|
||||
['ItemHolder', ['Item', 'Holder', 'Item'], 'ItemHolderItemHolder'],
|
||||
['ThingItemTest', ['Item', 'Thing', 'Foo'], 'ItemThingFooThingItemTest'],
|
||||
['ItemIn', ['Item', 'Holder', 'Item', 'In'], 'ItemHolderItemIn'],
|
||||
['Item', ['Item', 'Holder', 'Test'], 'ItemHolderTestItem'],
|
||||
['ItemHolderItem', ['Item', 'Holder', 'Item', 'Holder'], 'ItemHolderItemHolderItem'],
|
||||
['Icones', ['Icon'], 'IconIcones'],
|
||||
['Icon', ['Icones'], 'IconesIcon'],
|
||||
['IconHolder', ['IconHolder'], 'IconHolder'],
|
||||
['GameList', ['Desktop', 'ShareGame', 'Review', 'Detail'], 'DesktopShareGameReviewDetailGameList']
|
||||
]
|
||||
|
||||
describe('components:resolveComponentName', () => {
|
||||
it.each(tests)('resolves %s with prefix parts %s and filename %s', (fileName, prefixParts: string[], result) => {
|
||||
expect(resolveComponentName(fileName, prefixParts)).toBe(result)
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user