UNPKG

plumjs-config

Version:

A powerful Node.js configuration management library with YAML support and dynamic configuration loading

494 lines (412 loc) 15.8 kB
#!/usr/bin/env node /** * plumjs-config CLI tool for generating external types * 外部类型生成CLI工具 */ const fs = require('fs'); const path = require('path'); // Try to load js-yaml from the plumjs-config package let yaml; try { // First try to load from the current package yaml = require('js-yaml'); } catch (error) { try { // If that fails, try to load from the plumjs-config package const packagePath = path.dirname(__filename); yaml = require(path.join(packagePath, '../node_modules/js-yaml')); } catch (error2) { console.error('❌ 无法加载 js-yaml。请确保 plumjs-config 包正确安装。'); process.exit(1); } } class ExternalTypeGenerator { constructor() { this.interfaces = new Map(); this.configItems = new Set(); this.generatedTypes = new Set(); // 跟踪已生成的类型(基于路径) this.currentPath = ''; // 用于跟踪当前生成接口的路径上下文 } /** * Find plumjs-config installation path * 查找 plumjs-config 安装路径 */ findPackagePath() { let currentDir = process.cwd(); // First try to find in current project's node_modules const localPath = path.join(currentDir, 'node_modules', 'plumjs-config'); if (fs.existsSync(localPath)) { return localPath; } // Try to find in parent directories while (currentDir !== path.dirname(currentDir)) { const nodeModulesPath = path.join(currentDir, 'node_modules', 'plumjs-config'); if (fs.existsSync(nodeModulesPath)) { return nodeModulesPath; } currentDir = path.dirname(currentDir); } // Try global installation try { return path.dirname(require.resolve('plumjs-config/package.json')); } catch (error) { throw new Error('❌ 找不到 plumjs-config 包。请确保已安装该包。'); } } /** * Find configuration directory * 查找配置目录 */ findConfigDirectory(customConfigPath = null) { // If a custom config path is provided, use it if (customConfigPath) { const configPath = path.resolve(customConfigPath); if (fs.existsSync(configPath)) { console.log(`📁 使用指定配置目录: ${configPath}`); return configPath; } else { throw new Error(`❌ 指定的配置目录不存在: ${configPath}`); } } // Otherwise, try the default paths const possiblePaths = [ path.join(process.cwd(), 'config'), path.join(process.cwd(), 'configs'), path.join(process.cwd(), 'src', 'config'), path.join(process.cwd(), 'src', 'configs'), ]; for (const configPath of possiblePaths) { if (fs.existsSync(configPath)) { console.log(`📁 找到配置目录: ${configPath}`); return configPath; } } throw new Error('❌ 找不到配置目录。请确保存在 config/ 或 configs/ 目录,或使用 -c 参数指定配置目录路径。'); } /** * Process configuration files and generate merged configuration object * 处理配置文件并生成合并的配置对象 */ async processConfigFiles(configDir) { const files = fs.readdirSync(configDir); const yamlFiles = files.filter(file => file.endsWith('.yml') || file.endsWith('.yaml')); console.log(`📝 处理配置文件: ${yamlFiles.join(', ')}`); let mergedConfig = {}; for (const file of yamlFiles) { const filePath = path.join(configDir, file); const content = fs.readFileSync(filePath, 'utf8'); try { const configs = yaml.loadAll(content); for (const config of configs) { if (config && typeof config === 'object') { mergedConfig = this.deepMerge(mergedConfig, config); } } } catch (error) { console.warn(`⚠️ 解析 ${file} 时出错: ${error.message}`); } } // 使用路径感知的类型生成(避免同名配置冲突) this.generateTypesFromConfigWithPaths(mergedConfig); } /** * Deep merge objects */ deepMerge(target, source) { const isObject = (obj) => obj && typeof obj === 'object' && !Array.isArray(obj); if (!isObject(target) || !isObject(source)) { return source; } const result = { ...target }; Object.keys(source).forEach(key => { const targetValue = result[key]; const sourceValue = source[key]; if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { result[key] = [...targetValue, ...sourceValue]; } else if (isObject(targetValue) && isObject(sourceValue)) { result[key] = this.deepMerge(targetValue, sourceValue); } else { result[key] = sourceValue; } }); return result; } /** * Generate TypeScript types from merged configuration object with path awareness * 从合并的配置对象生成 TypeScript 类型(路径感知,避免同名冲突) */ generateTypesFromConfigWithPaths(config) { // 为每个顶层配置键生成接口,使用完整路径避免冲突 for (const [key, value] of Object.entries(config)) { if (typeof value === 'object' && value !== null && !Array.isArray(value)) { const interfaceName = this.generateUniqueInterfaceName(key); this.generateInterfaceForObjectWithPath(value, interfaceName, key); } } // Generate all possible paths for type safety this.generateAllPaths(config, ''); } /** * Generate unique interface name based on full path * 基于完整路径生成唯一的接口名称 * 例如: 'application.redis' -> 'ApplicationRedisConfig' * 'datasource.redis' -> 'DatasourceRedisConfig' */ generateUniqueInterfaceName(path) { const parts = path.split('.'); const name = parts .map(part => this.capitalizeFirst(part)) .join('') + 'Config'; return name; } /** * Capitalize first letter * 首字母大写 */ capitalizeFirst(str) { if (!str) return 'Unknown'; return str.charAt(0).toUpperCase() + str.slice(1); } /** * Generate interface for object with full path tracking * 为对象生成接口(带完整路径跟踪,避免同名配置冲突) */ generateInterfaceForObjectWithPath(obj, interfaceName, currentPath = '') { // 使用完整路径作为唯一标识 const uniqueKey = currentPath || interfaceName; if (this.generatedTypes.has(uniqueKey)) { return interfaceName; } this.generatedTypes.add(uniqueKey); const properties = []; // 保存当前路径上下文 const previousPath = this.currentPath; this.currentPath = currentPath; for (const [key, value] of Object.entries(obj)) { let type; if (typeof value === 'object' && value !== null && !Array.isArray(value)) { // 为嵌套对象生成接口,使用完整路径 const fullPath = currentPath ? `${currentPath}.${key}` : key; const nestedInterfaceName = this.generateUniqueInterfaceName(fullPath); this.generateInterfaceForObjectWithPath(value, nestedInterfaceName, fullPath); type = nestedInterfaceName; } else { type = this.inferType(value); } // Check if property is optional const isOptional = value === null || value === undefined; const propertyName = isOptional ? `${key}?` : key; properties.push(` ${propertyName}: ${type};`); } // 恢复之前的路径上下文 this.currentPath = previousPath; const interfaceDefinition = `export interface ${interfaceName} {\n${properties.join('\n')}\n}`; this.interfaces.set(uniqueKey, interfaceDefinition); return interfaceName; } /** * Generate all possible configuration paths recursively * 递归生成所有可能的配置路径 */ generateAllPaths(obj, prefix = '') { if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { return; } Object.keys(obj).forEach(key => { const fullPath = prefix ? `${prefix}.${key}` : key; this.configItems.add(fullPath); if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { this.generateAllPaths(obj[key], fullPath); } }); } /** * Infer TypeScript type from value */ inferType(value) { if (value === null || value === undefined) { return 'any'; } if (typeof value === 'boolean') { return 'boolean'; } if (typeof value === 'number') { return 'number'; } if (typeof value === 'string') { // Check for common enum patterns const lowerValue = value.toLowerCase(); if (['development', 'staging', 'production', 'test'].includes(lowerValue)) { return `'${lowerValue}'`; } if (['true', 'false'].includes(lowerValue)) { return 'boolean'; } if (['debug', 'info', 'warn', 'error'].includes(lowerValue)) { return `'${lowerValue}'`; } return 'string'; } if (Array.isArray(value)) { if (value.length === 0) { return 'any[]'; } const firstType = this.inferType(value[0]); const allSameType = value.every(item => this.inferType(item) === firstType); return allSameType ? `${firstType}[]` : 'any[]'; } if (typeof value === 'object') { return 'object'; } return 'any'; } /** * Generate user configuration types with enhanced type support * 生成用户配置类型(增强类型支持) */ generateUserConfigTypes() { const interfaces = Array.from(this.interfaces.values()); const allPaths = Array.from(this.configItems); // Generate union type for all configuration paths const configPathsUnion = allPaths.map(path => `'${path}'`).join(' | '); return `/** * 用户配置类型定义 * * 这个文件是通过扫描用户的 YAML 配置文件自动生成的。 * 每次运行 plumjs-config types 命令都会重新生成。 * 生成时间: ${new Date().toISOString()} * * 注意:使用路径感知的类型生成,避免同名配置冲突。 * 例如:application.redis 和 datasource.redis 会生成不同的类型: * - ApplicationRedisConfig * - DatasourceRedisConfig */ ${interfaces.join('\n\n')} /** * All possible configuration paths * 所有可能的配置路径 */ export type ConfigPath = ${configPathsUnion || 'string'}; /** * Deep path type helper for nested configuration access * 深层路径类型助手,用于嵌套配置访问 */ type PathValue<T, P extends string> = P extends keyof T ? T[P] : P extends \`\${infer K}.\${infer Rest}\` ? K extends keyof T ? PathValue<T[K], Rest> : any : any; /** * Main configuration interface (aggregated from all top-level configs) * 主配置接口(从所有顶层配置聚合) */ export interface AppConfiguration { ${Array.from(this.interfaces.keys()) .filter(key => !key.includes('.')) // 只包含顶层配置 .map(key => { const interfaceName = this.generateUniqueInterfaceName(key); return ` ${key}: ${interfaceName};`; }) .join('\n')} } /** * Get configuration value type by path with enhanced inference * 根据路径获取配置值类型(增强推断) */ export type ConfigValue<T extends string> = T extends ConfigPath ? PathValue<AppConfiguration, T> : any; /** * Main configuration type (return type of getConfig() with no parameters) * 主配置类型(getConfig() 无参数调用的返回类型) */ export type Configuration = AppConfiguration; `; } /** * Update package type definitions * 更新包的类型定义(每次都重新生成) */ updatePackageTypes(packagePath, userTypes) { const distPath = path.join(packagePath, 'dist'); const indexDtsPath = path.join(distPath, 'index.d.ts'); if (!fs.existsSync(indexDtsPath)) { throw new Error(`❌ 找不到类型定义文件: ${indexDtsPath}`); } // Read current type definitions let indexContent = fs.readFileSync(indexDtsPath, 'utf8'); // Create/overwrite user types file (每次都重新生成) const userTypesPath = path.join(distPath, 'user-config-types.d.ts'); fs.writeFileSync(userTypesPath, userTypes); console.log(`✅ 已生成用户配置类型文件: ${userTypesPath}`); // Update index.d.ts to include user types const userTypeImport = `import { ConfigPath, ConfigValue, Configuration } from './user-config-types';`; if (!indexContent.includes('user-config-types')) { // Add import at the top indexContent = userTypeImport + '\n' + indexContent; // Update getConfig function signature const newGetConfig = `export declare function getConfig<T extends ConfigPath>(path: T): ConfigValue<T> | undefined; export declare function getConfig<T extends ConfigPath>(path: T, defaultValue: ConfigValue<T>): ConfigValue<T>; export declare function getConfig(): Configuration | undefined; export declare function getConfig<T = any>(path?: string, defaultValue?: T): T | undefined;`; // Try to find and replace existing getConfig signatures const getConfigPattern = /export declare function getConfig[^;]+;(\s*export declare function getConfig[^;]+;)*/g; if (getConfigPattern.test(indexContent)) { indexContent = indexContent.replace(getConfigPattern, newGetConfig); } else { // If getConfig doesn't exist, add it indexContent += `\n${newGetConfig}\n`; } fs.writeFileSync(indexDtsPath, indexContent); console.log(`✅ 已更新主类型定义文件: ${indexDtsPath}`); } else { console.log(`ℹ️ 主类型定义文件已包含用户类型导入,跳过更新`); } } /** * Run the type generation process */ async run(customConfigPath = null) { try { console.log('🚀 开始生成配置类型...\n'); // Find package path const packagePath = this.findPackagePath(); console.log(`📦 plumjs-config 包路径: ${packagePath}`); // Find config directory const configDir = this.findConfigDirectory(customConfigPath); // Process configuration files await this.processConfigFiles(configDir); if (this.interfaces.size === 0) { console.log('⚠️ 没有找到有效的配置定义'); return; } // Generate user types const userTypes = this.generateUserConfigTypes(); // Update package types (每次都重新生成) this.updatePackageTypes(packagePath, userTypes); console.log(`\n✅ 类型生成完成!`); console.log(`📊 生成了 ${this.interfaces.size} 个接口`); console.log(`🔧 配置路径数量: ${this.configItems.size}`); console.log(`📁 类型文件: ${path.join(packagePath, 'dist/user-config-types.d.ts')}`); console.log('\n💡 提示:每次运行 plumjs-config types 都会重新扫描配置文件并更新类型'); console.log('🎯 示例用法:'); console.log(' - getConfig() // 获取完整配置对象'); console.log(' - getConfig("app.name", "默认应用") // 获取应用名称'); console.log(' - getConfig("database.port", 5432) // 获取数据库端口'); } catch (error) { console.error(`\n❌ 类型生成失败: ${error.message}`); if (error.stack) { console.error(error.stack); } process.exit(1); } } } // CLI entry point if (require.main === module) { const generator = new ExternalTypeGenerator(); generator.run(); } module.exports = ExternalTypeGenerator;