diff --git a/src/core/options.ts b/src/core/options.ts index ec2b5fb0..8693e643 100644 --- a/src/core/options.ts +++ b/src/core/options.ts @@ -33,26 +33,30 @@ function resolveGlobsExclude(root: string, glob: string) { export function resolveOptions(options: Options, root: string): ResolvedOptions { const resolved = Object.assign({}, defaultOptions, options) as ResolvedOptions resolved.resolvers = normalizeResolvers(resolved.resolvers) - resolved.extensions = toArray(resolved.extensions) + resolved.resolvedExtensions = toArray(resolved.extensions) + .map(i => i.startsWith('.') ? i : `.${i}`) + // sort extensions by length to ensure that the longest one is used first + // e.g. ['.vue', '.page.vue'] -> ['.page.vue', '.vue'] as both would match and order matters + .sort((a, b) => b.length - a.length) if (resolved.globs) { resolved.globs = toArray(resolved.globs).map((glob: string) => slash(resolveGlobsExclude(root, glob))) resolved.resolvedDirs = [] } else { - const extsGlob = resolved.extensions.length === 1 - ? resolved.extensions - : `{${resolved.extensions.join(',')}}` + const extsGlob = resolved.resolvedExtensions.length === 1 + ? resolved.resolvedExtensions + : `{${resolved.resolvedExtensions.join(',')}}` resolved.dirs = toArray(resolved.dirs) resolved.resolvedDirs = resolved.dirs.map(i => slash(resolveGlobsExclude(root, i))) resolved.globs = resolved.resolvedDirs.map(i => resolved.deep - ? slash(join(i, `**/*.${extsGlob}`)) - : slash(join(i, `*.${extsGlob}`)), + ? slash(join(i, `**/*${extsGlob}`)) + : slash(join(i, `*${extsGlob}`)), ) - if (!resolved.extensions.length) + if (!resolved.resolvedExtensions.length) throw new Error('[unplugin-vue-components] `extensions` option is required to search for components') } diff --git a/src/core/utils.ts b/src/core/utils.ts index eb5c8f3f..ea6693c6 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,7 +1,7 @@ import type { FilterPattern } from '@rollup/pluginutils' import type { ComponentInfo, ImportInfo, ImportInfoLegacy, Options, ResolvedOptions } from '../types' import type { Context } from './context' -import { parse } from 'node:path' +import { basename, parse } from 'node:path' import process from 'node:process' import { slash, toArray } from '@antfu/utils' import { @@ -114,7 +114,7 @@ export function stringifyComponentImport({ as: name, from: path, name: importNam } export function getNameFromFilePath(filePath: string, options: ResolvedOptions): string { - const { resolvedDirs, directoryAsNamespace, globalNamespaces, collapseSamePrefixes, root } = options + const { resolvedDirs, directoryAsNamespace, globalNamespaces, collapseSamePrefixes, root, resolvedExtensions } = options const parsedFilePath = parse(slash(filePath)) @@ -129,11 +129,14 @@ export function getNameFromFilePath(filePath: string, options: ResolvedOptions): } let folders = strippedPath.slice(1).split('/').filter(Boolean) - let filename = parsedFilePath.name + // when using `globs` option, `resolvedDirs` will always empty, and ignoring extensions is the expected behavior + let filename = isEmpty(resolvedDirs) + ? parsedFilePath.name + : basename(parsedFilePath.base, resolvedExtensions?.find(ext => parsedFilePath.base.endsWith(ext))) // set parent directory as filename if it is index if (filename === 'index' && !directoryAsNamespace) { - // when use `globs` option, `resolvedDirs` will always empty, and `folders` will also empty + // when using `globs` option, `resolvedDirs` will always empty, and `folders` will also empty if (isEmpty(folders)) folders = parsedFilePath.dir.slice(root.length + 1).split('/').filter(Boolean) diff --git a/src/types.ts b/src/types.ts index 69dceae1..b4348348 100644 --- a/src/types.ts +++ b/src/types.ts @@ -189,6 +189,7 @@ export type ResolvedOptions = Omit< > & { resolvers: ComponentResolverObject[] extensions: string[] + resolvedExtensions: string[] dirs: string[] resolvedDirs: string[] globs: string[] diff --git a/test/utils.test.ts b/test/utils.test.ts index cdabab87..c96ea341 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,14 +1,16 @@ import type { ResolvedOptions } from '../src' import { describe, expect, it } from 'vitest' +import { resolveOptions } from '../src/core/options' import { getNameFromFilePath } from '../src/core/utils' describe('getNameFromFilePath', () => { - const options: Partial = { + const options: Partial = resolveOptions({ directoryAsNamespace: true, globalNamespaces: [], collapseSamePrefixes: false, - resolvedDirs: ['/src/components'], - } + dirs: ['/src/components'], + extensions: ['vue', 'ce.vue'], + }, '/') it('normal name', () => { const inComponentFilePath = '/src/components/a/b.vue' @@ -19,4 +21,14 @@ describe('getNameFromFilePath', () => { const inComponentFilePath = '/src/components/[a1]/b_2/c 3/d.4/[...ef]/ghi.vue' expect(getNameFromFilePath(inComponentFilePath, options as ResolvedOptions)).toBe('a1-b2-c3-d4-ef-ghi') }) + + it(('long extensions'), () => { + const inComponentFilePath = '/src/components/b.ce.vue' + expect(getNameFromFilePath(inComponentFilePath, options as ResolvedOptions)).toBe('b') + }) + + it(('long extensions and nested'), () => { + const inComponentFilePath = '/src/components/a/b.ce.vue' + expect(getNameFromFilePath(inComponentFilePath, options as ResolvedOptions)).toBe('a-b') + }) })