You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

135 lines
4.3 KiB

import {
defineNuxtModule,
addComponent,
addImports,
createResolver,
} from '@nuxt/kit'
import { readdirSync, existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
export interface ModuleOptions {
/**
* Whether to import global base styles (CSS variables / design tokens).
* @default true
*/
importStyles?: boolean
}
function kebabCase(str: string): string {
return str.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
}
// ── 从源码中提取 export function / export const 名称 ─────────
function extractExports(filePath: string): string[] {
try {
const content = readFileSync(filePath, 'utf-8')
const names: string[] = []
const re = /export\s+(?:function|const|let|var)\s+(\w+)/g
let m: RegExpExecArray | null
while ((m = re.exec(content)) !== null) names.push(m[1] as any)
return names
} catch {
return []
}
}
function isComposable(name: string): boolean {
return name.startsWith('use') || name === 'createContext'
}
// ── Vite 插件:自动注入组件样式 ────────────────────────────
function BoltUiStylePlugin(styleMap: Record<string, string>) {
return {
name: 'bolt-ui:style-inject',
enforce: 'post',
transform(code: string, id: string) {
if (!/\.(vue|tsx?|jsx?)$/.test(id)) return null
if (id.includes('node_modules')) return null
const matchedStyles = Object.entries(styleMap)
.filter(([name]) => code.includes(name))
.map(([, style]) => style)
if (matchedStyles.length === 0) return null
const imports = [...new Set(matchedStyles)]
.map(p => `import '${p}';`)
.join('\n')
return {
code: imports + '\n' + code,
map: null,
}
},
}
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'bolt-ui',
configKey: 'boltUi',
},
defaults: {
importStyles: true,
},
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
// ── CSS 变量层(design tokens)────────────────────────────
nuxt.options.css.push(resolver.resolve('./theme-chalk/src/theme/index.scss'))
if (options.importStyles) {}
// ── 自动扫描组件目录 ────────────────────────────────────
const componentsDir = resolver.resolve('./components')
const componentStyleMap: Record<string, string> = {}
for (const entry of readdirSync(componentsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const name = entry.name
const indexPath = join(componentsDir, name, 'index.ts')
if (!existsSync(indexPath)) continue
// 注册组件
addComponent({
name: `Bo${name}`,
export: `Bo${name}`,
filePath: resolver.resolve(`./components/${name}/index.ts`),
})
// 构建样式映射(约定:theme-chalk/src/${kebab}.scss)
const scssPath = join(componentsDir, '..', 'theme-chalk/src', `${kebabCase(name)}.scss`)
if (existsSync(scssPath)) {
componentStyleMap[`Bo${name}`] = `bolt-ui/theme-chalk/src/${kebabCase(name)}.scss`
}
}
// ── Vite 插件:按需注入组件样式 ─────────────────────────
nuxt.hook('vite:extendConfig', (config) => {
// @ts-ignore
config.plugins = config.plugins || []
config.plugins.push(BoltUiStylePlugin(componentStyleMap) as any)
})
// ── 自动扫描 composable 目录 ──────────────────────────
for (const scanDir of ['hooks', 'utils', 'locales']) {
const absDir = resolver.resolve(`./${scanDir}`)
if (!existsSync(absDir)) continue
const walk = (dir: string) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
walk(fullPath)
} else if (entry.name === 'index.ts') {
const names = extractExports(fullPath).filter(isComposable)
for (const name of names) {
addImports({ name, as: name, from: fullPath })
}
}
}
}
walk(absDir)
}
},
})