211 lines
5.9 KiB
JavaScript
211 lines
5.9 KiB
JavaScript
const fs = require('fs-extra');
|
|
const yaml = require('js-yaml');
|
|
const path = require('node:path');
|
|
|
|
/**
|
|
* Configuration utility class
|
|
*/
|
|
class Config {
|
|
/**
|
|
* Load a YAML configuration file
|
|
* @param {string} configPath - Path to config file
|
|
* @returns {Object} Parsed configuration
|
|
*/
|
|
async loadYaml(configPath) {
|
|
if (!(await fs.pathExists(configPath))) {
|
|
throw new Error(`Configuration file not found: ${configPath}`);
|
|
}
|
|
|
|
const content = await fs.readFile(configPath, 'utf8');
|
|
return yaml.load(content);
|
|
}
|
|
|
|
/**
|
|
* Save configuration to YAML file
|
|
* @param {string} configPath - Path to config file
|
|
* @param {Object} config - Configuration object
|
|
*/
|
|
async saveYaml(configPath, config) {
|
|
const yamlContent = yaml.dump(config, {
|
|
indent: 2,
|
|
lineWidth: 120,
|
|
noRefs: true,
|
|
});
|
|
|
|
await fs.ensureDir(path.dirname(configPath));
|
|
await fs.writeFile(configPath, yamlContent, 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Process configuration file (replace placeholders)
|
|
* @param {string} configPath - Path to config file
|
|
* @param {Object} replacements - Replacement values
|
|
*/
|
|
async processConfig(configPath, replacements = {}) {
|
|
let content = await fs.readFile(configPath, 'utf8');
|
|
|
|
// Standard replacements
|
|
const standardReplacements = {
|
|
'{project-root}': replacements.root || '',
|
|
'{module}': replacements.module || '',
|
|
'{version}': replacements.version || '5.0.0',
|
|
'{date}': new Date().toISOString().split('T')[0],
|
|
};
|
|
|
|
// Apply all replacements
|
|
const allReplacements = { ...standardReplacements, ...replacements };
|
|
|
|
for (const [placeholder, value] of Object.entries(allReplacements)) {
|
|
if (typeof placeholder === 'string' && typeof value === 'string') {
|
|
const regex = new RegExp(placeholder.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`), 'g');
|
|
content = content.replace(regex, value);
|
|
}
|
|
}
|
|
|
|
await fs.writeFile(configPath, content, 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Merge configurations
|
|
* @param {Object} base - Base configuration
|
|
* @param {Object} override - Override configuration
|
|
* @returns {Object} Merged configuration
|
|
*/
|
|
mergeConfigs(base, override) {
|
|
return this.deepMerge(base, override);
|
|
}
|
|
|
|
/**
|
|
* Deep merge two objects
|
|
* @param {Object} target - Target object
|
|
* @param {Object} source - Source object
|
|
* @returns {Object} Merged object
|
|
*/
|
|
deepMerge(target, source) {
|
|
const output = { ...target };
|
|
|
|
if (this.isObject(target) && this.isObject(source)) {
|
|
for (const key of Object.keys(source)) {
|
|
if (this.isObject(source[key])) {
|
|
if (key in target) {
|
|
output[key] = this.deepMerge(target[key], source[key]);
|
|
} else {
|
|
output[key] = source[key];
|
|
}
|
|
} else {
|
|
output[key] = source[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Check if value is an object
|
|
* @param {*} item - Item to check
|
|
* @returns {boolean} True if object
|
|
*/
|
|
isObject(item) {
|
|
return item && typeof item === 'object' && !Array.isArray(item);
|
|
}
|
|
|
|
/**
|
|
* Validate configuration against schema
|
|
* @param {Object} config - Configuration to validate
|
|
* @param {Object} schema - Validation schema
|
|
* @returns {Object} Validation result
|
|
*/
|
|
validateConfig(config, schema) {
|
|
const errors = [];
|
|
const warnings = [];
|
|
|
|
// Check required fields
|
|
if (schema.required) {
|
|
for (const field of schema.required) {
|
|
if (!(field in config)) {
|
|
errors.push(`Missing required field: ${field}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check field types
|
|
if (schema.properties) {
|
|
for (const [field, spec] of Object.entries(schema.properties)) {
|
|
if (field in config) {
|
|
const value = config[field];
|
|
const expectedType = spec.type;
|
|
|
|
if (expectedType === 'array' && !Array.isArray(value)) {
|
|
errors.push(`Field '${field}' should be an array`);
|
|
} else if (expectedType === 'object' && !this.isObject(value)) {
|
|
errors.push(`Field '${field}' should be an object`);
|
|
} else if (expectedType === 'string' && typeof value !== 'string') {
|
|
errors.push(`Field '${field}' should be a string`);
|
|
} else if (expectedType === 'number' && typeof value !== 'number') {
|
|
errors.push(`Field '${field}' should be a number`);
|
|
} else if (expectedType === 'boolean' && typeof value !== 'boolean') {
|
|
errors.push(`Field '${field}' should be a boolean`);
|
|
}
|
|
|
|
// Check enum values
|
|
if (spec.enum && !spec.enum.includes(value)) {
|
|
errors.push(`Field '${field}' must be one of: ${spec.enum.join(', ')}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get configuration value with fallback
|
|
* @param {Object} config - Configuration object
|
|
* @param {string} path - Dot-notation path to value
|
|
* @param {*} defaultValue - Default value if not found
|
|
* @returns {*} Configuration value
|
|
*/
|
|
getValue(config, path, defaultValue = null) {
|
|
const keys = path.split('.');
|
|
let current = config;
|
|
|
|
for (const key of keys) {
|
|
if (current && typeof current === 'object' && key in current) {
|
|
current = current[key];
|
|
} else {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Set configuration value
|
|
* @param {Object} config - Configuration object
|
|
* @param {string} path - Dot-notation path to value
|
|
* @param {*} value - Value to set
|
|
*/
|
|
setValue(config, path, value) {
|
|
const keys = path.split('.');
|
|
const lastKey = keys.pop();
|
|
let current = config;
|
|
|
|
for (const key of keys) {
|
|
if (!(key in current) || typeof current[key] !== 'object') {
|
|
current[key] = {};
|
|
}
|
|
current = current[key];
|
|
}
|
|
|
|
current[lastKey] = value;
|
|
}
|
|
}
|
|
|
|
module.exports = { Config };
|