feat: components discovery (#243)

Co-authored-by: Daniel Roe <daniel@roe.dev>
This commit is contained in:
pooya parsa 2021-06-18 18:50:03 +02:00 committed by GitHub
parent 1ed3387243
commit a0f81cd1fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 370 additions and 1 deletions

View File

@ -2,3 +2,4 @@ dist
node_modules
_templates
schema
**/*.tmpl.*

View File

@ -0,0 +1,5 @@
<template>
<div>
<hello-world />
</div>
</template>

View File

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

View File

@ -0,0 +1,4 @@
import { defineNuxtConfig } from '@nuxt/kit'
export default defineNuxtConfig({
})

View File

@ -0,0 +1,12 @@
{
"name": "example-with-components",
"private": true,
"devDependencies": {
"nuxt3": "latest"
},
"scripts": {
"dev": "nu dev",
"build": "nu build",
"start": "node .output/server"
}
}

View File

@ -0,0 +1,9 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
declaration: true,
entries: [
'src/module',
{ input: 'src/runtime/', outDir: 'dist/runtime', format: 'esm' }
]
})

View File

@ -0,0 +1 @@
module.exports = require('./dist/module')

View File

@ -0,0 +1,27 @@
{
"name": "@nuxt/component-discovery",
"version": "0.1.0",
"repository": "nuxt/framework",
"license": "MIT",
"types": "./dist/module.d.ts",
"files": [
"dist",
"module.js"
],
"scripts": {
"prepack": "unbuild"
},
"dependencies": {
"@nuxt/kit": "^0.6.3",
"globby": "^11.0.3",
"scule": "^0.2.1",
"ufo": "^0.7.5",
"upath": "^2.0.1"
},
"devDependencies": {
"unbuild": "^0.3.1"
},
"peerDependencies": {
"vue": "3.1.1"
}
}

View File

@ -0,0 +1,88 @@
import fs from 'fs'
import { defineNuxtModule, resolveAlias } from '@nuxt/kit'
import { resolve, dirname } from 'upath'
import { scanComponents } from './scan'
import type { ComponentsDir } from './types'
const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
const getDir = (p: string) => fs.statSync(p).isDirectory() ? p : dirname(p)
export default defineNuxtModule({
name: 'components',
defaults: {
dirs: ['~/components']
},
setup (options, nuxt) {
let componentDirs = []
// Resolve dirs
nuxt.hook('app:resolve', async () => {
await nuxt.callHook('components:dirs', options.dirs)
componentDirs = options.dirs.filter(isPureObjectOrString).map((dir) => {
const dirOptions: ComponentsDir = typeof dir === 'object' ? dir : { path: dir }
const dirPath = getDir(resolveAlias(dirOptions.path, nuxt.options.alias))
const transpile = typeof dirOptions.transpile === 'boolean' ? dirOptions.transpile : 'auto'
const extensions = dirOptions.extensions || ['vue'] // TODO: nuxt extensions and strip leading dot
dirOptions.level = Number(dirOptions.level || 0)
const enabled = fs.existsSync(dirPath)
if (!enabled && dirOptions.path !== '~/components') {
// eslint-disable-next-line no-console
console.warn('Components directory not found: `' + dirPath + '`')
}
return {
...dirOptions,
enabled,
path: dirPath,
extensions,
pattern: dirOptions.pattern || `**/*.{${extensions.join(',')},}`,
ignore: [
'**/*.stories.{js,ts,jsx,tsx}', // ignore storybook files
'**/*{M,.m,-m}ixin.{js,ts,jsx,tsx}', // ignore mixins
'**/*.d.ts', // .d.ts files
// TODO: support nuxt ignore patterns
...(dirOptions.ignore || [])
],
transpile: (transpile === 'auto' ? dirPath.includes('node_modules') : transpile)
}
}).filter(d => d.enabled)
nuxt.options.build!.transpile!.push(...componentDirs.filter(dir => dir.transpile).map(dir => dir.path))
})
// Scan components and add to plugin
nuxt.hook('app:templates', async (app) => {
const components = await scanComponents(componentDirs, nuxt.options.srcDir!)
await nuxt.callHook('components:extend', components)
if (!components.length) {
return
}
app.templates.push({
path: 'components.mjs',
src: resolve(__dirname, 'runtime/components.tmpl.mjs'),
data: { components }
})
app.templates.push({
path: 'components.d.ts',
src: resolve(__dirname, 'runtime/components.tmpl.d.ts'),
data: { components }
})
app.plugins.push({ src: '#build/components' })
})
// Watch for changes
nuxt.hook('builder:watch', async (event, path) => {
if (!['add', 'unlink'].includes(event)) {
return
}
const fPath = resolve(nuxt.options.rootDir, path)
if (componentDirs.find(dir => fPath.startsWith(dir.path))) {
await nuxt.callHook('builder:generateApp')
}
})
}
})

View File

@ -0,0 +1,7 @@
declare module 'vue' {
export interface GlobalComponents {
<%= components.map(c => {
return ` ${c.pascalName}: typeof import('${c.filePath}')['${c.export}']`
}).join(',\n') %>
}
}

View File

@ -0,0 +1,22 @@
import { defineAsyncComponent } from 'vue'
const components = {
<%= components.map(c => {
const exp = c.export === 'default' ? `c.default || c` : `c['${c.export}']`
const magicComments = [
`webpackChunkName: "${c.chunkName}"`,
c.prefetch === true || typeof c.prefetch === 'number' ? `webpackPrefetch: ${c.prefetch}` : false,
c.preload === true || typeof c.preload === 'number' ? `webpackPreload: ${c.preload}` : false,
].filter(Boolean).join(', ')
return ` ${c.pascalName}: defineAsyncComponent(() => import('${c.filePath}' /* ${magicComments} */).then(c => ${exp}))`
}).join(',\n') %>
}
export default function (nuxt) {
for (const name in components) {
nuxt.app.component(name, components[name])
nuxt.app.component('Lazy' + name, components[name])
}
}

View File

@ -0,0 +1,100 @@
import { basename, extname, join, dirname, relative } from 'upath'
import globby from 'globby'
import { pascalCase, splitByCase } from 'scule'
import type { ScanDir, Component } from './types'
export function sortDirsByPathLength ({ path: pathA }: ScanDir, { path: pathB }: ScanDir): number {
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length
}
// vue@2 src/shared/util.js
// TODO: update to vue3?
function hyphenate (str: string):string {
return str.replace(/\B([A-Z])/g, '-$1').toLowerCase()
}
export async function scanComponents (dirs: ScanDir[], srcDir: string): Promise<Component[]> {
const components: Component[] = []
const filePaths = new Set<string>()
const scannedPaths: string[] = []
for (const { path, pattern, ignore = [], prefix, extendComponent, pathPrefix, level, prefetch = false, preload = false } of dirs.sort(sortDirsByPathLength)) {
const resolvedNames = new Map<string, string>()
for (const _file of await globby(pattern!, { cwd: path, ignore })) {
const filePath = join(path, _file)
if (scannedPaths.find(d => filePath.startsWith(d))) {
continue
}
if (filePaths.has(filePath)) { continue }
filePaths.add(filePath)
// Resolve componentName
const prefixParts = ([] as string[]).concat(
prefix ? splitByCase(prefix) : [],
(pathPrefix !== false) ? splitByCase(relative(path, dirname(filePath))) : []
)
let fileName = basename(filePath, extname(filePath))
if (fileName.toLowerCase() === 'index') {
fileName = pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
}
const fileNameParts = splitByCase(fileName)
const componentNameParts: string[] = []
while (prefixParts.length &&
(prefixParts[0] || '').toLowerCase() !== (fileNameParts[0] || '').toLowerCase()
) {
componentNameParts.push(prefixParts.shift()!)
}
const componentName = pascalCase(componentNameParts) + pascalCase(fileNameParts)
if (resolvedNames.has(componentName)) {
// eslint-disable-next-line no-console
console.warn(`Two component files resolving to the same name \`${componentName}\`:\n` +
`\n - ${filePath}` +
`\n - ${resolvedNames.get(componentName)}`
)
continue
}
resolvedNames.set(componentName, filePath)
const pascalName = pascalCase(componentName)
const kebabName = hyphenate(componentName)
const shortPath = relative(srcDir, filePath)
const chunkName = 'components/' + kebabName
let component: Component = {
filePath,
pascalName,
kebabName,
chunkName,
shortPath,
export: 'default',
global: Boolean(global),
level: Number(level),
prefetch: Boolean(prefetch),
preload: Boolean(preload)
}
if (typeof extendComponent === 'function') {
component = (await extendComponent(component)) || component
}
// Check if component is already defined, used to overwite if level is inferiour
const definedComponent = components.find(c => c.pascalName === component.pascalName)
if (definedComponent && component.level < definedComponent.level) {
Object.assign(definedComponent, component)
} else if (!definedComponent) {
components.push(component)
}
}
scannedPaths.push(path)
}
return components
}

View File

@ -0,0 +1,62 @@
export interface Component {
pascalName: string
kebabName: string
export: string
filePath: string
shortPath: string
chunkName: string
level: number
prefetch: boolean
preload: boolean
/** @deprecated */
import?: string
/** @deprecated */
asyncImport?: string
/** @deprecated */
global?: boolean
/** @deprecated */
async?: boolean
}
export interface ScanDir {
path: string
pattern?: string | string[]
ignore?: string[]
prefix?: string
pathPrefix?: boolean
level?: number
prefetch?: boolean
preload?: boolean
extendComponent?: (component: Component) => Promise<Component | void> | (Component | void)
/** @deprecated */
global?: boolean | 'dev'
}
export interface ComponentsDir extends ScanDir {
watch?: boolean
extensions?: string[]
transpile?: 'auto' | boolean
}
type componentsDirHook = (dirs: ComponentsDir[]) => void | Promise<void>
type componentsExtendHook = (components: (ComponentsDir | ScanDir)[]) => void | Promise<void>
export interface Options {
dirs: (string | ComponentsDir)[]
loader: Boolean
}
declare module '@nuxt/kit' {
interface NuxtOptions {
components: boolean | Options | Options['dirs']
}
interface NuxtOptionsHooks {
'components:dirs'?: componentsDirHook
'components:extend'?: componentsExtendHook
components?: {
dirs?: componentsDirHook
extend?: componentsExtendHook
}
}
}

View File

@ -18,6 +18,7 @@
},
"dependencies": {
"@nuxt/app": "^0.4.3",
"@nuxt/component-discovery": "^0.1.0",
"@nuxt/kit": "^0.6.3",
"@nuxt/nitro": "^0.9.0",
"@nuxt/pages": "^0.2.3",

View File

@ -50,6 +50,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
options._majorVersion = 3
options.alias.vue = require.resolve('vue/dist/vue.esm-bundler.js')
options.buildModules.push(require.resolve('@nuxt/pages/module'))
options.buildModules.push(require.resolve('@nuxt/component-discovery/module'))
const nuxt = createNuxt(options)

View File

@ -1,6 +1,6 @@
<template>
<div>
Hello
<HelloWorld />
</div>
</template>

View File

@ -1648,6 +1648,21 @@ __metadata:
languageName: unknown
linkType: soft
"@nuxt/component-discovery@^0.1.0, @nuxt/component-discovery@workspace:packages/components":
version: 0.0.0-use.local
resolution: "@nuxt/component-discovery@workspace:packages/components"
dependencies:
"@nuxt/kit": ^0.6.3
globby: ^11.0.3
scule: ^0.2.1
ufo: ^0.7.5
unbuild: ^0.3.1
upath: ^2.0.1
peerDependencies:
vue: 3.1.1
languageName: unknown
linkType: soft
"@nuxt/devalue@npm:^1.2.5":
version: 1.2.5
resolution: "@nuxt/devalue@npm:1.2.5"
@ -6000,6 +6015,14 @@ __metadata:
languageName: unknown
linkType: soft
"example-with-components@workspace:examples/with-components":
version: 0.0.0-use.local
resolution: "example-with-components@workspace:examples/with-components"
dependencies:
nuxt3: latest
languageName: unknown
linkType: soft
"example-with-vite@workspace:examples/with-vite":
version: 0.0.0-use.local
resolution: "example-with-vite@workspace:examples/with-vite"
@ -9796,6 +9819,7 @@ __metadata:
resolution: "nuxt3@workspace:packages/nuxt3"
dependencies:
"@nuxt/app": ^0.4.3
"@nuxt/component-discovery": ^0.1.0
"@nuxt/kit": ^0.6.3
"@nuxt/nitro": ^0.9.0
"@nuxt/pages": ^0.2.3