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) { 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({ 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 = {} 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) } }, })