diff --git a/packages/nuxt/src/components/scan.ts b/packages/nuxt/src/components/scan.ts index 7a0065c5743f..4c755eec9d5a 100644 --- a/packages/nuxt/src/components/scan.ts +++ b/packages/nuxt/src/components/scan.ts @@ -72,24 +72,8 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */ } - /** - * Array of fileName parts splitted by case, / or - - * - * @example third-component -> ['third', 'component'] - * @example AwesomeComponent -> ['Awesome', 'Component'] - */ - const fileNameParts = splitByCase(fileName) - - const componentNameParts: string[] = [] - - while (prefixParts.length && - (prefixParts[0] || '').toLowerCase() !== (fileNameParts[0] || '').toLowerCase() - ) { - componentNameParts.push(prefixParts.shift()!) - } - - const componentName = pascalCase(componentNameParts) + pascalCase(fileNameParts) const suffix = (mode !== 'all' ? `-${mode}` : '') + const componentName = resolveComponentName(fileName, prefixParts) if (resolvedNames.has(componentName + suffix) || resolvedNames.has(componentName)) { console.warn(`Two component files resolving to the same name \`${componentName}\`:\n` + @@ -137,3 +121,30 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr 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((prefixParts[index] || '').toLowerCase()) + if (fileNamePartsContent.startsWith(matchedSuffix.join('')) || + // 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) +} diff --git a/packages/nuxt/test/fixture/components/same-name/same/Same.vue b/packages/nuxt/test/fixture/components/same-name/same/Same.vue new file mode 100644 index 000000000000..671ce6550ca7 --- /dev/null +++ b/packages/nuxt/test/fixture/components/same-name/same/Same.vue @@ -0,0 +1,7 @@ + + diff --git a/packages/nuxt/test/scan-components.test.ts b/packages/nuxt/test/scan-components.test.ts index 5a1a26b05f35..0c4025f2a0fd 100644 --- a/packages/nuxt/test/scan-components.test.ts +++ b/packages/nuxt/test/scan-components.test.ts @@ -1,8 +1,8 @@ import { resolve } from 'node:path' -import { expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import type { ComponentsDir } from 'nuxt/schema' -import { scanComponents } from '../src/components/scan' +import { resolveComponentName, scanComponents } from '../src/components/scan' const fixtureDir = resolve(__dirname, 'fixture') const rFixture = (...p: string[]) => resolve(fixtureDir, ...p) @@ -192,6 +192,19 @@ const expectedComponents = [ preload: false, priority: 1 }, + { + chunkName: 'components/same-name-same', + export: 'default', + global: undefined, + island: undefined, + kebabName: 'same-name-same', + mode: 'all', + pascalName: 'SameNameSame', + prefetch: false, + preload: false, + priority: 1, + shortPath: 'components/same-name/same/Same.vue' + }, { chunkName: 'components/some-glob', export: 'default', @@ -230,3 +243,25 @@ 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'] +] + +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) + }) +})