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'
|
2023-09-19 21:26:15 +00:00
|
|
|
import { isIgnored, logger, 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
|
|
|
|
2023-10-31 13:30:54 +00:00
|
|
|
import { resolveComponentNameSegments } from '../core/utils'
|
2023-10-16 21:58:40 +00:00
|
|
|
|
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) {
|
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
|
|
|
|
2022-11-24 12:24:14 +00:00
|
|
|
const island = /\.(island)(\.global)?$/.test(fileName) || dir.island
|
|
|
|
const global = /\.(global)(\.island)?$/.test(fileName) || dir.global
|
|
|
|
const mode = island ? 'server' : (fileName.match(/(?<=\.)(client|server)(\.global|\.island)*$/)?.[1] || 'all') as 'client' | 'server' | 'all'
|
|
|
|
fileName = fileName.replace(/(\.(client|server))?(\.global|\.island)*$/, '')
|
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}` : '')
|
2023-10-31 13:30:54 +00:00
|
|
|
const componentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts)
|
|
|
|
const pascalName = pascalCase(componentNameSegments)
|
2021-06-18 16:50:03 +00:00
|
|
|
|
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,
|
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
|
|
|
)
|
|
|
|
}
|