mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
feat(nuxt): scanComponents named exports
This commit is contained in:
parent
416d76ab80
commit
7ce0b8754f
@ -6,6 +6,7 @@ import { isIgnored, logger, useNuxt } from '@nuxt/kit'
|
|||||||
import { withTrailingSlash } from 'ufo'
|
import { withTrailingSlash } from 'ufo'
|
||||||
import type { Component, ComponentsDir } from 'nuxt/schema'
|
import type { Component, ComponentsDir } from 'nuxt/schema'
|
||||||
|
|
||||||
|
import { resolveModuleExportNames } from 'mlly'
|
||||||
import { resolveComponentNameSegments } from '../core/utils'
|
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))) : [],
|
(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
|
* In case we have index as filename the component become the parent path
|
||||||
* @example third-components/index.vue -> third-component
|
* @example third-components/index.vue -> third-component
|
||||||
* if not take the filename
|
* if not take the filename
|
||||||
* @example third-components/Awesome.vue -> Awesome
|
* @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 island = /\.(island)(\.global)?$/.test(fileName) || dir.island
|
||||||
const global = /\.(global)(\.island)?$/.test(fileName) || dir.global
|
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 */
|
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getComponents = async (exportName: string): Promise<Component | null> => {
|
||||||
|
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 suffix = (mode !== 'all' ? `-${mode}` : '')
|
||||||
const componentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts)
|
const defaultComponentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts)
|
||||||
const pascalName = pascalCase(componentNameSegments)
|
|
||||||
|
|
||||||
if (resolvedNames.has(pascalName + suffix) || resolvedNames.has(pascalName)) {
|
const componentsDefinitions = fileExt === '.vue'
|
||||||
warnAboutDuplicateComponent(pascalName, filePath, resolvedNames.get(pascalName) || resolvedNames.get(pascalName + suffix)!)
|
? [await getComponents('default')]
|
||||||
continue
|
: await Promise.all((await resolveModuleExportNames(filePath, {
|
||||||
}
|
extensions: ['.ts', '.js', '.tsx', '.jsx'],
|
||||||
resolvedNames.set(pascalName + suffix, filePath)
|
})).map(getComponents))
|
||||||
|
|
||||||
const kebabName = kebabCase(componentNameSegments)
|
components.push(...componentsDefinitions.filter<Component>((c): c is Component => Boolean(c)))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
scannedPaths.push(dir.path)
|
scannedPaths.push(dir.path)
|
||||||
}
|
}
|
||||||
|
13
packages/nuxt/test/fixture/components/Named.ts
Normal file
13
packages/nuxt/test/fixture/components/Named.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export const NamedExport = defineComponent({
|
||||||
|
setup () {
|
||||||
|
return () => 'hello'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup () {
|
||||||
|
return () => 'default'
|
||||||
|
},
|
||||||
|
})
|
@ -47,8 +47,9 @@ const dirs: ComponentsDir[] = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
extensions: [
|
extensions: [
|
||||||
'vue',
|
'vue',
|
||||||
|
'ts',
|
||||||
],
|
],
|
||||||
pattern: '**/*.{vue,}',
|
pattern: '**/*.{vue,ts}',
|
||||||
ignore: [
|
ignore: [
|
||||||
'**/*.stories.{js,ts,jsx,tsx}',
|
'**/*.stories.{js,ts,jsx,tsx}',
|
||||||
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}',
|
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}',
|
||||||
@ -61,8 +62,9 @@ const dirs: ComponentsDir[] = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
extensions: [
|
extensions: [
|
||||||
'vue',
|
'vue',
|
||||||
|
'ts',
|
||||||
],
|
],
|
||||||
pattern: '**/*.{vue,}',
|
pattern: '**/*.{vue,ts}',
|
||||||
ignore: [
|
ignore: [
|
||||||
'**/*.stories.{js,ts,jsx,tsx}',
|
'**/*.stories.{js,ts,jsx,tsx}',
|
||||||
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}',
|
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}',
|
||||||
@ -74,10 +76,11 @@ const dirs: ComponentsDir[] = [
|
|||||||
path: rFixture('components'),
|
path: rFixture('components'),
|
||||||
extensions: [
|
extensions: [
|
||||||
'vue',
|
'vue',
|
||||||
|
'ts',
|
||||||
],
|
],
|
||||||
prefix: 'nuxt',
|
prefix: 'nuxt',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
pattern: '**/*.{vue,}',
|
pattern: '**/*.{vue,ts}',
|
||||||
ignore: [
|
ignore: [
|
||||||
'**/*.stories.{js,ts,jsx,tsx}',
|
'**/*.stories.{js,ts,jsx,tsx}',
|
||||||
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}',
|
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}',
|
||||||
@ -127,6 +130,32 @@ const expectedComponents = [
|
|||||||
preload: false,
|
preload: false,
|
||||||
priority: 1,
|
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',
|
mode: 'client',
|
||||||
pascalName: 'Nuxt3',
|
pascalName: 'Nuxt3',
|
||||||
|
Loading…
Reference in New Issue
Block a user