plumjs-config
Version:
A powerful Node.js configuration management library with YAML support and dynamic configuration loading
494 lines (412 loc) • 15.8 kB
JavaScript
/**
* 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;