chore(nuxt3): add tests, comments and example for components scan (#1455)

Co-authored-by: Pooya Parsa <pyapar@gmail.com>
This commit is contained in:
fgiraud 2021-11-15 11:22:46 -05:00 committed by GitHub
parent 0a74940bf6
commit 6b873f15bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 183 additions and 6 deletions

View File

@ -0,0 +1,5 @@
<template>
<div>
Awesome Component
</div>
</template>

View File

@ -1,5 +1,15 @@
import { defineNuxtConfig } from 'nuxt3' import { defineNuxtConfig } from 'nuxt3'
export default defineNuxtConfig({ export default defineNuxtConfig({
vite: false vite: false,
components: {
dirs: [
'~/components',
{
path: '~/other-components-folder',
extensions: ['vue'],
prefix: 'nuxt'
}
]
}
}) })

View File

@ -0,0 +1,5 @@
<template>
<div>
Awesome Component
</div>
</template>

View File

@ -40,6 +40,10 @@ export interface ScanDir {
* Prefix component name by it's path. * Prefix component name by it's path.
*/ */
pathPrefix?: boolean pathPrefix?: boolean
/**
* Ignore scanning this directory if set to `true`
*/
enabled?: boolean
/** /**
* Level is used to define a hint when overwriting the components which have the same name in two different directories. * Level is used to define a hint when overwriting the components which have the same name in two different directories.
*/ */

View File

@ -1,7 +1,7 @@
import { basename, extname, join, dirname, relative } from 'pathe' import { basename, extname, join, dirname, relative } from 'pathe'
import globby from 'globby' import globby from 'globby'
import { pascalCase, splitByCase } from 'scule' import { pascalCase, splitByCase } from 'scule'
import type { ScanDir, Component } from '@nuxt/kit' import type { ScanDir, Component, ComponentsDir } from '@nuxt/kit'
export function sortDirsByPathLength ({ path: pathA }: ScanDir, { path: pathB }: ScanDir): number { export function sortDirsByPathLength ({ path: pathA }: ScanDir, { path: pathB }: ScanDir): number {
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length
@ -13,12 +13,26 @@ function hyphenate (str: string):string {
return str.replace(/\B([A-Z])/g, '-$1').toLowerCase() return str.replace(/\B([A-Z])/g, '-$1').toLowerCase()
} }
export async function scanComponents (dirs: ScanDir[], srcDir: string): Promise<Component[]> { /**
* 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
const components: Component[] = [] const components: Component[] = []
// Keep resolved path to avoid duplicates
const filePaths = new Set<string>() const filePaths = new Set<string>()
// All scanned paths
const scannedPaths: string[] = [] const scannedPaths: string[] = []
for (const dir of dirs.sort(sortDirsByPathLength)) { for (const dir of dirs.sort(sortDirsByPathLength)) {
// A map from resolved path to component name (used for making duplicate warning message)
const resolvedNames = new Map<string, string>() const resolvedNames = new Map<string, string>()
for (const _file of await globby(dir.pattern!, { cwd: dir.path, ignore: dir.ignore })) { for (const _file of await globby(dir.pattern!, { cwd: dir.path, ignore: dir.ignore })) {
@ -28,18 +42,42 @@ export async function scanComponents (dirs: ScanDir[], srcDir: string): Promise<
continue continue
} }
// Avoid duplicate paths
if (filePaths.has(filePath)) { continue } if (filePaths.has(filePath)) { continue }
filePaths.add(filePath) filePaths.add(filePath)
// Resolve componentName /**
* 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']
*/
const prefixParts = ([] as string[]).concat( const prefixParts = ([] as string[]).concat(
dir.prefix ? splitByCase(dir.prefix) : [], dir.prefix ? splitByCase(dir.prefix) : [],
(dir.pathPrefix !== false) ? splitByCase(relative(dir.path, dirname(filePath))) : [] (dir.pathPrefix !== false) ? splitByCase(relative(dir.path, dirname(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 thid-components/Awesome.vue -> Awesome
*/
let fileName = basename(filePath, extname(filePath)) let fileName = basename(filePath, extname(filePath))
if (fileName.toLowerCase() === 'index') { if (fileName.toLowerCase() === 'index') {
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */ fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
} }
/**
* Array of fileName parts splitted by case, / or -
*
* @example third-component -> ['third', 'component']
* @example AwesomeComponent -> ['Awesome', 'Component']
*/
const fileNameParts = splitByCase(fileName) const fileNameParts = splitByCase(fileName)
const componentNameParts: string[] = [] const componentNameParts: string[] = []
@ -53,7 +91,6 @@ export async function scanComponents (dirs: ScanDir[], srcDir: string): Promise<
const componentName = pascalCase(componentNameParts) + pascalCase(fileNameParts) const componentName = pascalCase(componentNameParts) + pascalCase(fileNameParts)
if (resolvedNames.has(componentName)) { if (resolvedNames.has(componentName)) {
// eslint-disable-next-line no-console
console.warn(`Two component files resolving to the same name \`${componentName}\`:\n` + console.warn(`Two component files resolving to the same name \`${componentName}\`:\n` +
`\n - ${filePath}` + `\n - ${filePath}` +
`\n - ${resolvedNames.get(componentName)}` `\n - ${resolvedNames.get(componentName)}`
@ -92,7 +129,6 @@ export async function scanComponents (dirs: ScanDir[], srcDir: string): Promise<
components.push(component) components.push(component)
} }
} }
scannedPaths.push(dir.path) scannedPaths.push(dir.path)
} }

View File

@ -0,0 +1,5 @@
<template>
<div>
This is HelloWorld component!
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<b style="color: #00C58E">
From Nuxt 3
</b>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
Awesome Component
</div>
</template>

View File

@ -0,0 +1,102 @@
import { resolve } from 'path'
import { ComponentsDir } from '@nuxt/kit'
import { expect } from 'chai'
import { scanComponents } from '../src/components/scan'
const fixtureDir = resolve(__dirname, 'fixture')
const rFixture = (...p) => resolve(fixtureDir, ...p)
const dirs: ComponentsDir[] = [
{
path: rFixture('components'),
level: 0,
enabled: true,
extensions: [
'vue'
],
pattern: '**/*.{vue,}',
ignore: [
'**/*.stories.{js,ts,jsx,tsx}',
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}',
'**/*.d.ts'
],
transpile: false
},
{
path: rFixture('components'),
level: 0,
enabled: true,
extensions: [
'vue'
],
pattern: '**/*.{vue,}',
ignore: [
'**/*.stories.{js,ts,jsx,tsx}',
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}',
'**/*.d.ts'
],
transpile: false
},
{
path: rFixture('components'),
extensions: [
'vue'
],
prefix: 'nuxt',
level: 0,
enabled: true,
pattern: '**/*.{vue,}',
ignore: [
'**/*.stories.{js,ts,jsx,tsx}',
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}',
'**/*.d.ts'
],
transpile: false
}
]
const expectedComponents = [
{
pascalName: 'HelloWorld',
kebabName: 'hello-world',
chunkName: 'components/hello-world',
shortPath: 'components/HelloWorld.vue',
export: 'default',
global: undefined,
level: 0,
prefetch: false,
preload: false
},
{
pascalName: 'Nuxt3',
kebabName: 'nuxt3',
chunkName: 'components/nuxt3',
shortPath: 'components/Nuxt3.vue',
export: 'default',
global: undefined,
level: 0,
prefetch: false,
preload: false
},
{
pascalName: 'ParentFolder',
kebabName: 'parent-folder',
chunkName: 'components/parent-folder',
shortPath: 'components/parent-folder/index.vue',
export: 'default',
global: undefined,
level: 0,
prefetch: false,
preload: false
}
]
const srcDir = rFixture('.')
it('components:scanComponents', async () => {
const scannedComponents = await scanComponents(dirs, srcDir)
for (const c of scannedComponents) {
delete c.filePath
}
expect(scannedComponents).deep.eq(expectedComponents)
})