Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nuxt): scan named exports with addComponentsDir #27155

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
123 changes: 68 additions & 55 deletions packages/nuxt/src/components/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -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
Expand All @@ -92,67 +94,78 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
}

const suffix = (mode !== 'all' ? `-${mode}` : '')
const componentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts)
const pascalName = pascalCase(componentNameSegments)

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 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 getComponents = async (exportName: string): Promise<Component | null> => {
const componentNameSegment = exportName === 'default' ? defaultComponentNameSegments : resolveComponentNameSegments(exportName, defaultComponentNameSegments)
const pascalName = pascalCase(componentNameSegment)

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
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,
}

// Replace component if priority is higher
if (newPriority > existingPriority) {
components.splice(components.indexOf(existingComponent), 1, component)
if (typeof dir.extendComponent === 'function') {
component = (await dir.extendComponent(component)) || 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)

// 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
}

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)
}

return null
}
return component
}

components.push(component)
const suffix = (mode !== 'all' ? `-${mode}` : '')
const defaultComponentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts)

const componentsDefinitions = fileExt === '.vue'
? [await getComponents('default')]
: await Promise.all((await resolveModuleExportNames(filePath, {
extensions: ['.ts', '.js', '.tsx', '.jsx'],
})).map(getComponents))

components.push(...componentsDefinitions.filter<Component>((c): c is Component => Boolean(c)))
}
scannedPaths.push(dir.path)
}
Expand Down
13 changes: 13 additions & 0 deletions packages/nuxt/test/fixture/components/Named.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineComponent } from 'vue'

export const NamedExport = defineComponent({
setup () {
return () => 'hello'
},
})

export default defineComponent({
setup () {
return () => 'default'
},
})
35 changes: 32 additions & 3 deletions packages/nuxt/test/scan-components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}',
Expand All @@ -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}',
Expand All @@ -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}',
Expand Down Expand Up @@ -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',
Expand Down