From 7ce0b8754f05fea466eec45f6bec63e27bdf4083 Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Fri, 10 May 2024 15:24:44 +0200 Subject: [PATCH] feat(nuxt): scanComponents named exports --- packages/nuxt/src/components/scan.ts | 131 ++++++++++-------- .../nuxt/test/fixture/components/Named.ts | 13 ++ packages/nuxt/test/scan-components.test.ts | 35 ++++- 3 files changed, 117 insertions(+), 62 deletions(-) create mode 100644 packages/nuxt/test/fixture/components/Named.ts diff --git a/packages/nuxt/src/components/scan.ts b/packages/nuxt/src/components/scan.ts index dc3eb4f2a6..3507955a63 100644 --- a/packages/nuxt/src/components/scan.ts +++ b/packages/nuxt/src/components/scan.ts @@ -6,6 +6,7 @@ import { isIgnored, logger, useNuxt } from '@nuxt/kit' import { withTrailingSlash } from 'ufo' import type { Component, ComponentsDir } from 'nuxt/schema' +import { resolveModuleExportNames } from 'mlly' import { resolveComponentNameSegments } from '../core/utils' /** @@ -75,13 +76,14 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr (dir.pathPrefix !== false) ? splitByCase(relative(dir.path, dirname(filePath))) : [], ) + const fileExt = extname(filePath) /** * In case we have index as filename the component become the parent path * @example third-components/index.vue -> third-component * if not take the filename * @example third-components/Awesome.vue -> Awesome */ - let fileName = basename(filePath, extname(filePath)) + let fileName = basename(filePath, fileExt) const island = /\.(island)(\.global)?$/.test(fileName) || dir.island const global = /\.(global)(\.island)?$/.test(fileName) || dir.global @@ -92,67 +94,78 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */ } + const getComponents = async (exportName: string): Promise => { + const componentNameSegment = exportName === 'default' ? defaultComponentNameSegments : resolveComponentNameSegments(exportName, defaultComponentNameSegments) + const pascalName = pascalCase(componentNameSegment) + + if (resolvedNames.has(pascalName + suffix) || resolvedNames.has(pascalName)) { + warnAboutDuplicateComponent(pascalName, filePath, resolvedNames.get(pascalName) || resolvedNames.get(pascalName + suffix)!) + return null + } + resolvedNames.set(pascalName + suffix, filePath) + + const kebabName = kebabCase(componentNameSegment) + const shortPath = relative(srcDir, filePath) + const chunkName = 'components/' + kebabName + suffix + + let component: Component = { + // inheritable from directory configuration + mode, + global, + island, + prefetch: Boolean(dir.prefetch), + preload: Boolean(dir.preload), + // specific to the file + filePath, + pascalName, + kebabName, + chunkName, + shortPath, + export: exportName, + // by default, give priority to scanned components + priority: dir.priority ?? 1, + } + + if (typeof dir.extendComponent === 'function') { + component = (await dir.extendComponent(component)) || component + } + + // Ignore files like `~/components/index.vue` which end up not having a name at all + if (!pascalName) { + logger.warn(`Component did not resolve to a file name in \`~/${relative(srcDir, filePath)}\`.`) + return null + } + + const existingComponent = components.find(c => c.pascalName === component.pascalName && ['all', component.mode].includes(c.mode)) + // Ignore component if component is already defined (with same mode) + if (existingComponent) { + const existingPriority = existingComponent.priority ?? 0 + const newPriority = component.priority ?? 0 + + // Replace component if priority is higher + if (newPriority > existingPriority) { + components.splice(components.indexOf(existingComponent), 1, component) + } + // Warn if a user-defined (or prioritized) component conflicts with a previously scanned component + if (newPriority > 0 && newPriority === existingPriority) { + warnAboutDuplicateComponent(pascalName, filePath, existingComponent.filePath) + } + + return null + } + return component + } + const suffix = (mode !== 'all' ? `-${mode}` : '') - const componentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts) - const pascalName = pascalCase(componentNameSegments) + const defaultComponentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts) - if (resolvedNames.has(pascalName + suffix) || resolvedNames.has(pascalName)) { - warnAboutDuplicateComponent(pascalName, filePath, resolvedNames.get(pascalName) || resolvedNames.get(pascalName + suffix)!) - continue - } - resolvedNames.set(pascalName + suffix, filePath) + const componentsDefinitions = fileExt === '.vue' + ? [await getComponents('default')] + : await Promise.all((await resolveModuleExportNames(filePath, { + extensions: ['.ts', '.js', '.tsx', '.jsx'], + })).map(getComponents)) - const kebabName = kebabCase(componentNameSegments) - const shortPath = relative(srcDir, filePath) - const chunkName = 'components/' + kebabName + suffix - - let component: Component = { - // inheritable from directory configuration - mode, - global, - island, - prefetch: Boolean(dir.prefetch), - preload: Boolean(dir.preload), - // specific to the file - filePath, - pascalName, - kebabName, - chunkName, - shortPath, - export: 'default', - // by default, give priority to scanned components - priority: dir.priority ?? 1, - } - - if (typeof dir.extendComponent === 'function') { - component = (await dir.extendComponent(component)) || component - } - - // Ignore files like `~/components/index.vue` which end up not having a name at all - if (!pascalName) { - logger.warn(`Component did not resolve to a file name in \`~/${relative(srcDir, filePath)}\`.`) - continue - } - - const existingComponent = components.find(c => c.pascalName === component.pascalName && ['all', component.mode].includes(c.mode)) - // Ignore component if component is already defined (with same mode) - if (existingComponent) { - const existingPriority = existingComponent.priority ?? 0 - const newPriority = component.priority ?? 0 - - // Replace component if priority is higher - if (newPriority > existingPriority) { - components.splice(components.indexOf(existingComponent), 1, component) - } - // Warn if a user-defined (or prioritized) component conflicts with a previously scanned component - if (newPriority > 0 && newPriority === existingPriority) { - warnAboutDuplicateComponent(pascalName, filePath, existingComponent.filePath) - } - - continue - } - - components.push(component) + components.push(...componentsDefinitions.filter((c): c is Component => Boolean(c))) } scannedPaths.push(dir.path) } diff --git a/packages/nuxt/test/fixture/components/Named.ts b/packages/nuxt/test/fixture/components/Named.ts new file mode 100644 index 0000000000..baeed7a7d5 --- /dev/null +++ b/packages/nuxt/test/fixture/components/Named.ts @@ -0,0 +1,13 @@ +import { defineComponent } from 'vue' + +export const NamedExport = defineComponent({ + setup () { + return () => 'hello' + }, +}) + +export default defineComponent({ + setup () { + return () => 'default' + }, +}) diff --git a/packages/nuxt/test/scan-components.test.ts b/packages/nuxt/test/scan-components.test.ts index a4e8f172da..9322707b56 100644 --- a/packages/nuxt/test/scan-components.test.ts +++ b/packages/nuxt/test/scan-components.test.ts @@ -47,8 +47,9 @@ const dirs: ComponentsDir[] = [ enabled: true, extensions: [ 'vue', + 'ts', ], - pattern: '**/*.{vue,}', + pattern: '**/*.{vue,ts}', ignore: [ '**/*.stories.{js,ts,jsx,tsx}', '**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}', @@ -61,8 +62,9 @@ const dirs: ComponentsDir[] = [ enabled: true, extensions: [ 'vue', + 'ts', ], - pattern: '**/*.{vue,}', + pattern: '**/*.{vue,ts}', ignore: [ '**/*.stories.{js,ts,jsx,tsx}', '**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}', @@ -74,10 +76,11 @@ const dirs: ComponentsDir[] = [ path: rFixture('components'), extensions: [ 'vue', + 'ts', ], prefix: 'nuxt', enabled: true, - pattern: '**/*.{vue,}', + pattern: '**/*.{vue,ts}', ignore: [ '**/*.stories.{js,ts,jsx,tsx}', '**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}', @@ -127,6 +130,32 @@ const expectedComponents = [ preload: false, priority: 1, }, + { + chunkName: 'components/named-export', + export: 'NamedExport', + global: undefined, + island: undefined, + kebabName: 'named-export', + mode: 'all', + pascalName: 'NamedExport', + prefetch: false, + preload: false, + priority: 1, + shortPath: 'components/Named.ts', + }, + { + chunkName: 'components/named', + export: 'default', + global: undefined, + island: undefined, + kebabName: 'named', + mode: 'all', + pascalName: 'Named', + prefetch: false, + preload: false, + priority: 1, + shortPath: 'components/Named.ts', + }, { mode: 'client', pascalName: 'Nuxt3',