2023-05-22 10:04:02 +00:00
import { readdir } from 'node:fs/promises'
2023-04-07 16:02:47 +00:00
import { basename , dirname , extname , join , relative } from 'pathe'
2022-01-13 18:21:49 +00:00
import { globby } from 'globby'
2023-10-31 13:30:54 +00:00
import { kebabCase , pascalCase , splitByCase } from 'scule'
2024-12-29 23:25:29 +00:00
import { isIgnored , useNuxt } from '@nuxt/kit'
2022-07-25 10:13:54 +00:00
import { withTrailingSlash } from 'ufo'
2023-02-13 22:42:04 +00:00
import type { Component , ComponentsDir } from 'nuxt/schema'
2021-06-18 16:50:03 +00:00
2024-10-22 13:39:50 +00:00
import { QUOTE_RE , resolveComponentNameSegments } from '../core/utils'
2024-12-29 23:25:29 +00:00
import { logger } from '../utils'
2023-10-16 21:58:40 +00:00
2024-10-22 13:39:50 +00:00
const ISLAND_RE = /\.island(?:\.global)?$/
const GLOBAL_RE = /\.global(?:\.island)?$/
const COMPONENT_MODE_RE = /(?<=\.)(client|server)(\.global|\.island)*$/
const MODE_REPLACEMENT_RE = /(\.(client|server))?(\.global|\.island)*$/
2021-11-15 16:22:46 +00:00
/ * *
* Scan the components inside different components folders
* and return a unique list of components
* @param dirs all folders where components are defined
* @param srcDir src path of your app
* @returns { Promise } Component found promise
* /
export async function scanComponents ( dirs : ComponentsDir [ ] , srcDir : string ) : Promise < Component [ ] > {
// All scanned components
2021-06-18 16:50:03 +00:00
const components : Component [ ] = [ ]
2021-11-15 16:22:46 +00:00
// Keep resolved path to avoid duplicates
2021-06-18 16:50:03 +00:00
const filePaths = new Set < string > ( )
2021-11-15 16:22:46 +00:00
// All scanned paths
2021-06-18 16:50:03 +00:00
const scannedPaths : string [ ] = [ ]
2022-02-07 20:48:25 +00:00
for ( const dir of dirs ) {
2024-04-23 12:19:12 +00:00
if ( dir . enabled === false ) {
continue
}
2021-11-15 16:22:46 +00:00
// A map from resolved path to component name (used for making duplicate warning message)
2021-06-18 16:50:03 +00:00
const resolvedNames = new Map < string , string > ( )
2022-07-21 10:46:50 +00:00
const files = ( await globby ( dir . pattern ! , { cwd : dir.path , ignore : dir.ignore } ) ) . sort ( )
2023-05-22 10:04:02 +00:00
// Check if the directory exists (globby will otherwise read it case insensitively on MacOS)
if ( files . length ) {
const siblings = await readdir ( dirname ( dir . path ) ) . catch ( ( ) = > [ ] as string [ ] )
const directory = basename ( dir . path )
if ( ! siblings . includes ( directory ) ) {
2023-08-23 16:58:10 +00:00
const directoryLowerCase = directory . toLowerCase ( )
const caseCorrected = siblings . find ( sibling = > sibling . toLowerCase ( ) === directoryLowerCase )
2023-05-22 10:04:02 +00:00
if ( caseCorrected ) {
const nuxt = useNuxt ( )
const original = relative ( nuxt . options . srcDir , dir . path )
const corrected = relative ( nuxt . options . srcDir , join ( dirname ( dir . path ) , caseCorrected ) )
2023-09-19 21:26:15 +00:00
logger . warn ( ` Components not scanned from \` ~/ ${ corrected } \` . Did you mean to name the directory \` ~/ ${ original } \` instead? ` )
2023-05-22 10:04:02 +00:00
continue
}
}
}
2022-07-21 10:46:50 +00:00
for ( const _file of files ) {
2021-10-29 11:36:55 +00:00
const filePath = join ( dir . path , _file )
2021-06-18 16:50:03 +00:00
2022-07-25 10:13:54 +00:00
if ( scannedPaths . find ( d = > filePath . startsWith ( withTrailingSlash ( d ) ) ) || isIgnored ( filePath ) ) {
2021-06-18 16:50:03 +00:00
continue
}
2021-11-15 16:22:46 +00:00
// Avoid duplicate paths
2021-06-18 16:50:03 +00:00
if ( filePaths . has ( filePath ) ) { continue }
2021-11-15 16:22:46 +00:00
2021-06-18 16:50:03 +00:00
filePaths . add ( filePath )
2021-11-15 16:22:46 +00:00
/ * *
* Create an array of prefixes base on the prefix config
* Empty prefix will be an empty array
* @example prefix : 'nuxt' - > [ 'nuxt' ]
* @example prefix : 'nuxt-test' - > [ 'nuxt' , 'test' ]
* /
2021-06-18 16:50:03 +00:00
const prefixParts = ( [ ] as string [ ] ) . concat (
2021-10-29 11:36:55 +00:00
dir . prefix ? splitByCase ( dir . prefix ) : [ ] ,
2024-04-05 18:08:32 +00:00
( dir . pathPrefix !== false ) ? splitByCase ( relative ( dir . path , dirname ( filePath ) ) ) : [ ] ,
2021-06-18 16:50:03 +00:00
)
2021-11-15 16:22:46 +00:00
/ * *
* 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
2022-08-22 10:12:02 +00:00
* @example third - components / Awesome . vue - > Awesome
2021-11-15 16:22:46 +00:00
* /
2021-06-18 16:50:03 +00:00
let fileName = basename ( filePath , extname ( filePath ) )
2021-11-15 16:22:46 +00:00
2024-10-22 13:39:50 +00:00
const island = ISLAND_RE . test ( fileName ) || dir . island
const global = GLOBAL_RE . test ( fileName ) || dir . global
const mode = island ? 'server' : ( fileName . match ( COMPONENT_MODE_RE ) ? . [ 1 ] || 'all' ) as 'client' | 'server' | 'all'
fileName = fileName . replace ( MODE_REPLACEMENT_RE , '' )
2022-04-19 19:13:55 +00:00
2021-06-18 16:50:03 +00:00
if ( fileName . toLowerCase ( ) === 'index' ) {
2021-10-29 11:36:55 +00:00
fileName = dir . pathPrefix === false ? basename ( dirname ( filePath ) ) : '' /* inherits from path */
2021-06-18 16:50:03 +00:00
}
2021-11-15 16:22:46 +00:00
2022-04-19 19:13:55 +00:00
const suffix = ( mode !== 'all' ? ` - ${ mode } ` : '' )
2024-10-22 13:39:50 +00:00
const componentNameSegments = resolveComponentNameSegments ( fileName . replace ( QUOTE_RE , '' ) , prefixParts )
2023-10-31 13:30:54 +00:00
const pascalName = pascalCase ( componentNameSegments )
2021-06-18 16:50:03 +00:00
2024-06-27 13:16:17 +00:00
if ( LAZY_COMPONENT_NAME_REGEX . test ( pascalName ) ) {
logger . warn ( ` The component \` ${ pascalName } \` (in \` ${ filePath } \` ) is using the reserved "Lazy" prefix used for dynamic imports, which may cause it to break at runtime. ` )
}
2023-10-31 13:30:54 +00:00
if ( resolvedNames . has ( pascalName + suffix ) || resolvedNames . has ( pascalName ) ) {
warnAboutDuplicateComponent ( pascalName , filePath , resolvedNames . get ( pascalName ) || resolvedNames . get ( pascalName + suffix ) ! )
2021-06-18 16:50:03 +00:00
continue
}
2023-10-31 13:30:54 +00:00
resolvedNames . set ( pascalName + suffix , filePath )
2021-06-18 16:50:03 +00:00
2023-10-31 13:30:54 +00:00
const kebabName = kebabCase ( componentNameSegments )
2021-06-18 16:50:03 +00:00
const shortPath = relative ( srcDir , filePath )
2022-04-19 19:13:55 +00:00
const chunkName = 'components/' + kebabName + suffix
2021-06-18 16:50:03 +00:00
let component : Component = {
2022-07-27 13:05:34 +00:00
// inheritable from directory configuration
mode ,
global ,
2022-11-24 12:24:14 +00:00
island ,
2022-07-27 13:05:34 +00:00
prefetch : Boolean ( dir . prefetch ) ,
preload : Boolean ( dir . preload ) ,
// specific to the file
2021-06-18 16:50:03 +00:00
filePath ,
pascalName ,
kebabName ,
chunkName ,
shortPath ,
2023-03-06 11:33:40 +00:00
export : 'default' ,
// by default, give priority to scanned components
2024-04-05 18:08:32 +00:00
priority : dir.priority ? ? 1 ,
2024-09-05 15:38:43 +00:00
// @ts-expect-error untyped property
_scanned : true ,
2021-06-18 16:50:03 +00:00
}
2021-10-29 11:36:55 +00:00
if ( typeof dir . extendComponent === 'function' ) {
component = ( await dir . extendComponent ( component ) ) || component
2021-06-18 16:50:03 +00:00
}
2023-07-14 13:50:14 +00:00
// Ignore files like `~/components/index.vue` which end up not having a name at all
2023-10-31 13:30:54 +00:00
if ( ! pascalName ) {
2023-09-19 21:26:15 +00:00
logger . warn ( ` Component did not resolve to a file name in \` ~/ ${ relative ( srcDir , filePath ) } \` . ` )
2023-07-14 13:50:14 +00:00
continue
}
2023-08-23 15:23:17 +00:00
const existingComponent = components . find ( c = > c . pascalName === component . pascalName && [ 'all' , component . mode ] . includes ( c . mode ) )
2023-08-29 22:06:41 +00:00
// Ignore component if component is already defined (with same mode)
2023-08-23 15:23:17 +00:00
if ( existingComponent ) {
2023-08-29 22:06:41 +00:00
const existingPriority = existingComponent . priority ? ? 0
const newPriority = component . priority ? ? 0
2023-09-12 20:47:42 +00:00
// Replace component if priority is higher
if ( newPriority > existingPriority ) {
components . splice ( components . indexOf ( existingComponent ) , 1 , component )
}
2023-10-10 11:14:55 +00:00
// Warn if a user-defined (or prioritized) component conflicts with a previously scanned component
2023-09-12 20:47:42 +00:00
if ( newPriority > 0 && newPriority === existingPriority ) {
2023-10-31 13:30:54 +00:00
warnAboutDuplicateComponent ( pascalName , filePath , existingComponent . filePath )
2023-08-29 22:06:41 +00:00
}
2023-08-23 15:23:17 +00:00
continue
2021-06-18 16:50:03 +00:00
}
2023-08-23 15:23:17 +00:00
components . push ( component )
2021-06-18 16:50:03 +00:00
}
2021-10-29 11:36:55 +00:00
scannedPaths . push ( dir . path )
2021-06-18 16:50:03 +00:00
}
return components
}
2023-05-15 12:34:04 +00:00
2023-08-23 15:23:17 +00:00
function warnAboutDuplicateComponent ( componentName : string , filePath : string , duplicatePath : string ) {
2023-09-19 21:26:15 +00:00
logger . warn ( ` Two component files resolving to the same name \` ${ componentName } \` : \ n ` +
2023-08-23 15:23:17 +00:00
` \ n - ${ filePath } ` +
2024-04-05 18:08:32 +00:00
` \ n - ${ duplicatePath } ` ,
2023-08-23 15:23:17 +00:00
)
}
2024-06-27 13:16:17 +00:00
const LAZY_COMPONENT_NAME_REGEX = /^Lazy(?=[A-Z])/