UNPKG

nxconfig-js

Version:

Next-generation configuration management for Node.js - Zero dependencies, TypeScript-first, production-ready

1,229 lines 56.5 kB
"use strict"; /** * nxconfig - Next-generation configuration management for Node.js * * Smart multi-config support with merge strategies and named configs * * @license MIT * @version 1.0.0 */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigurationHierarchy = exports.ConfigMergeError = exports.ConfigSchemaError = exports.ConfigFileError = exports.ConfigValidationError = exports.ConfigParseError = void 0; exports.getEnvVariables = getEnvVariables; exports.extractEnvTokens = extractEnvTokens; exports.maskSecrets = maskSecrets; exports.validateRequiredEnvVars = validateRequiredEnvVars; exports.generateDocumentation = generateDocumentation; exports.parseConfig = parseConfig; exports.parseMultiConfig = parseMultiConfig; exports.verify = verify; exports.initConfig = initConfig; exports.watchConfig = watchConfig; exports.createConfig = createConfig; const fs = __importStar(require("fs")); const path = __importStar(require("path")); // ============================================================================ // Constants // ============================================================================ // Patterns for multi-segment env file paths: ENV.TEST.VARIABLE, ENV.PROD.DATABASE.HOST const ENV_TOKEN_PATTERN = /^ENV\.([A-Za-z0-9_.]+)\.([A-Za-z0-9_]+)(?:\|\|(.+))?$/; const ENV_TOKEN_WITH_TYPE = /^ENV\.([A-Za-z0-9_.]+)\.([A-Za-z0-9_]+)(?::(\w+))?(?:\|\|(.+))?$/; // Patterns for simple env variables: ENV.VARIABLE (uses default .env file) const ENV_SIMPLE_TOKEN_PATTERN = /^ENV\.([A-Za-z0-9_]+)(?:\|\|(.+))?$/; const ENV_SIMPLE_TOKEN_WITH_TYPE = /^ENV\.([A-Za-z0-9_]+)(?::(\w+))?(?:\|\|(.+))?$/; const SECRET_FIELD_PATTERNS = [ /password/i, /secret/i, /key/i, /token/i, /credential/i, /auth/i, /api[_-]?key/i, /access[_-]?token/i, ]; const BUILTIN_FORMATS = { 'port': (val) => { const num = Number(val); return Number.isInteger(num) && num >= 0 && num <= 65535; }, 'nat': (val) => { const num = Number(val); return Number.isInteger(num) && num >= 0; }, 'int': (val) => { return Number.isInteger(Number(val)); }, 'ipaddress': (val) => { const ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; const ipv6 = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2})$/; return ipv4.test(val) || ipv6.test(val); }, 'duration': (val) => { return /^\d+[smhdw]$/.test(val); }, 'timestamp': (val) => { return !isNaN(Date.parse(val)); }, }; // ============================================================================ // Error Classes // ============================================================================ class ConfigParseError extends Error { constructor(message, variableName, path, filePath) { super(message); this.variableName = variableName; this.path = path; this.filePath = filePath; this.name = 'ConfigParseError'; Object.setPrototypeOf(this, ConfigParseError.prototype); } } exports.ConfigParseError = ConfigParseError; class ConfigValidationError extends Error { constructor(message, missingVars, emptyVars) { super(message); this.missingVars = missingVars; this.emptyVars = emptyVars; this.name = 'ConfigValidationError'; Object.setPrototypeOf(this, ConfigValidationError.prototype); } } exports.ConfigValidationError = ConfigValidationError; class ConfigFileError extends Error { constructor(message, filePath, originalError) { super(message); this.filePath = filePath; this.originalError = originalError; this.name = 'ConfigFileError'; Object.setPrototypeOf(this, ConfigFileError.prototype); } } exports.ConfigFileError = ConfigFileError; class ConfigSchemaError extends Error { constructor(message, field, expectedType) { super(message); this.field = field; this.expectedType = expectedType; this.name = 'ConfigSchemaError'; Object.setPrototypeOf(this, ConfigSchemaError.prototype); } } exports.ConfigSchemaError = ConfigSchemaError; class ConfigMergeError extends Error { constructor(message, sources) { super(message); this.sources = sources; this.name = 'ConfigMergeError'; Object.setPrototypeOf(this, ConfigMergeError.prototype); } } exports.ConfigMergeError = ConfigMergeError; // ============================================================================ // Environment File Cache and Dynamic Loading // ============================================================================ // Cache for loaded environment files to avoid reloading const envFileCache = new Map(); function loadEnvFileByToken(envPath, variableName) { // Construct the dotenv filename const dotenvFilename = `.env.${envPath.toLowerCase()}`; // Check cache first if (envFileCache.has(dotenvFilename)) { const envVars = envFileCache.get(dotenvFilename); return envVars[variableName]; } // Load the file if not cached try { const fullPath = path.isAbsolute(dotenvFilename) ? dotenvFilename : path.resolve(process.cwd(), dotenvFilename); if (!fs.existsSync(fullPath)) { return undefined; } const content = fs.readFileSync(fullPath, 'utf8'); const envVars = {}; const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const match = trimmed.match(/^([A-Za-z0-9_]+)=(.*)$/); if (match && match[1] && match[2] !== undefined) { let [, key, value] = match; if (value && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) { value = value.slice(1, -1); } if (key && value !== undefined) { envVars[key] = value; } } } // Cache the loaded file envFileCache.set(dotenvFilename, envVars); return envVars[variableName]; } catch (error) { // Silent fail, return undefined return undefined; } } function loadSimpleEnvVariable(variableName) { // For simple ENV.VARIABLE pattern, use process.env or the default dotenv file return process.env[variableName]; } function getEnvVariables(envPath) { const variables = []; // Determine which env file to read const dotenvFilename = envPath ? `.env.${envPath.toLowerCase()}` : '.env'; try { const fullPath = path.isAbsolute(dotenvFilename) ? dotenvFilename : path.resolve(process.cwd(), dotenvFilename); if (!fs.existsSync(fullPath)) { return variables; // Return empty array if file doesn't exist } const content = fs.readFileSync(fullPath, 'utf8'); const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const match = trimmed.match(/^([A-Za-z0-9_]+)=(.*)$/); if (match && match[1] && match[2] !== undefined) { let [, key, value] = match; // Remove quotes if present if (value && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) { value = value.slice(1, -1); } if (key && value !== undefined) { // Check if this is a sensitive field const isSensitive = isSecretField(key); const synthesizedValue = isSensitive ? maskSecret(value) : value; variables.push({ varName: key, length: value.length, synthesizedValue: synthesizedValue }); } } } } catch (error) { // Silent fail, return empty array } return variables; } // ============================================================================ // Utility Functions // ============================================================================ function isSecretField(fieldName) { return SECRET_FIELD_PATTERNS.some((pattern) => pattern.test(fieldName)); } function maskSecret(value) { if (value.length <= 4) return '***'; return value.substring(0, 2) + '***' + value.substring(value.length - 2); } function extractEnvTokens(config) { const tokens = new Set(); function traverse(obj) { if (typeof obj === 'string') { // Check for multi-segment pattern: ENV.TEST.VARIABLE const multiMatch = obj.match(ENV_TOKEN_PATTERN); if (multiMatch && multiMatch[2]) { tokens.add(multiMatch[2]); // Add the variable name } // Check for simple pattern: ENV.VARIABLE const simpleMatch = obj.match(ENV_SIMPLE_TOKEN_PATTERN); if (simpleMatch && simpleMatch[1]) { tokens.add(simpleMatch[1]); // Add the variable name } } else if (Array.isArray(obj)) { obj.forEach(traverse); } else if (obj !== null && typeof obj === 'object') { Object.values(obj).forEach(traverse); } } traverse(config); return Array.from(tokens); } function maskSecrets(config, path = '', schema) { if (typeof config === 'string') { const pathParts = path.split('.').filter(Boolean); const fieldName = pathParts[pathParts.length - 1] || ''; if (schema && schema[fieldName]?.sensitive) { return maskSecret(config); } if (isSecretField(fieldName)) { return maskSecret(config); } return config; } if (Array.isArray(config)) { return config.map((item, index) => maskSecrets(item, `${path}[${index}]`, schema)); } if (config !== null && typeof config === 'object') { const masked = {}; for (const [key, value] of Object.entries(config)) { const newPath = path ? `${path}.${key}` : key; masked[key] = maskSecrets(value, newPath, schema); } return masked; } return config; } function validateRequiredEnvVars(variableNames) { return verify(variableNames); } // ============================================================================ // Multi-Config Merge Strategies // ============================================================================ function deepMerge(target, ...sources) { if (!sources.length) return target; const source = sources.shift(); if (isObject(target) && isObject(source)) { for (const key in source) { if (isObject(source[key])) { if (!target[key]) Object.assign(target, { [key]: {} }); deepMerge(target[key], source[key]); } else if (Array.isArray(source[key])) { target[key] = [...(target[key] || []), ...source[key]]; } else { Object.assign(target, { [key]: source[key] }); } } } return deepMerge(target, ...sources); } function shallowMerge(target, ...sources) { return Object.assign({}, target, ...sources); } function overrideMerge(target, ...sources) { return sources[sources.length - 1] || target; } function appendMerge(target, ...sources) { if (Array.isArray(target)) { return [...target, ...sources.flat()]; } return deepMerge(target, ...sources); } function isObject(item) { return item && typeof item === 'object' && !Array.isArray(item); } function mergeConfigs(configs, strategy = 'deep') { if (configs.length === 0) return {}; if (configs.length === 1) return configs[0]; switch (strategy) { case 'deep': return configs.reduce((acc, config) => deepMerge(acc, config), {}); case 'shallow': return configs.reduce((acc, config) => shallowMerge(acc, config), {}); case 'override': return configs.reduce((acc, config) => overrideMerge(acc, config), {}); case 'append': return configs.reduce((acc, config) => appendMerge(acc, config), {}); default: return deepMerge({}, ...configs); } } // ============================================================================ // Command-line Arguments Parser // ============================================================================ function parseCommandLineArgs() { const args = {}; const argv = process.argv.slice(2); for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg && arg.startsWith('--')) { const key = arg.substring(2); const nextArg = argv[i + 1]; if (nextArg && !nextArg.startsWith('--')) { args[key] = nextArg; i++; } else { args[key] = true; } } } return args; } // ============================================================================ // Nested Key Support // ============================================================================ function getNestedValue(obj, key, separator = ':') { const keys = key.split(separator); let current = obj; for (const k of keys) { if (current === undefined || current === null) { return undefined; } current = current[k]; } return current; } function setNestedValue(obj, key, value, separator = ':') { const keys = key.split(separator); const lastKey = keys.pop(); let current = obj; for (const k of keys) { if (!(k in current) || typeof current[k] !== 'object') { current[k] = {}; } current = current[k]; } current[lastKey] = value; } // ============================================================================ // Type Coercion Functions // ============================================================================ function coerceType(value, type) { switch (type.toLowerCase()) { case 'number': const num = Number(value); if (isNaN(num)) throw new Error(`Cannot convert "${value}" to number`); return num; case 'int': const int = parseInt(value, 10); if (isNaN(int)) throw new Error(`Cannot convert "${value}" to integer`); return int; case 'nat': const nat = parseInt(value, 10); if (isNaN(nat) || nat < 0) throw new Error(`Cannot convert "${value}" to natural number`); return nat; case 'port': const port = parseInt(value, 10); if (isNaN(port) || port < 0 || port > 65535) { throw new Error(`Invalid port number: "${value}"`); } return port; case 'boolean': if (value.toLowerCase() === 'true' || value === '1') return true; if (value.toLowerCase() === 'false' || value === '0') return false; throw new Error(`Cannot convert "${value}" to boolean`); case 'bigint': try { return BigInt(value); } catch { throw new Error(`Cannot convert "${value}" to bigint`); } case 'json': try { return JSON.parse(value); } catch { throw new Error(`Cannot parse "${value}" as JSON`); } case 'array': return value.split(',').map(s => s.trim()); case 'url': try { new URL(value); return value; } catch { throw new Error(`Invalid URL: "${value}"`); } case 'email': const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailPattern.test(value)) { throw new Error(`Invalid email: "${value}"`); } return value; case 'regex': try { return new RegExp(value); } catch { throw new Error(`Invalid regex pattern: "${value}"`); } case 'duration': const match = value.match(/^(\d+)([smhdw])$/); if (!match) throw new Error(`Invalid duration: "${value}"`); const amount = parseInt(match[1] || '0', 10); const unit = match[2]; const multipliers = { 's': 1000, 'm': 60000, 'h': 3600000, 'd': 86400000, 'w': 604800000 }; return amount * (unit ? (multipliers[unit] || 1000) : 1000); case 'timestamp': const date = new Date(value); if (isNaN(date.getTime())) { throw new Error(`Invalid timestamp: "${value}"`); } return date.getTime(); case 'ipaddress': if (!BUILTIN_FORMATS.ipaddress(value)) { throw new Error(`Invalid IP address: "${value}"`); } return value; default: return value; } } // ============================================================================ // .env File Loading // ============================================================================ function loadDotenv(dotenvPath = '.env') { const envVars = {}; try { const fullPath = path.isAbsolute(dotenvPath) ? dotenvPath : path.resolve(process.cwd(), dotenvPath); if (!fs.existsSync(fullPath)) { return envVars; } const content = fs.readFileSync(fullPath, 'utf8'); const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const match = trimmed.match(/^([A-Za-z0-9_]+)=(.*)$/); if (match && match[1] && match[2] !== undefined) { let [, key, value] = match; if (value && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) { value = value.slice(1, -1); } if (key && value !== undefined) { envVars[key] = value; if (!(key in process.env)) { process.env[key] = value; } } } } } catch (error) { // Silent fail } return envVars; } // ============================================================================ // Path Validation // ============================================================================ function validatePath(pathValue, isDirectory, isFile) { if (!fs.existsSync(pathValue)) { return false; } const stats = fs.statSync(pathValue); if (isDirectory && !stats.isDirectory()) { return false; } if (isFile && !stats.isFile()) { return false; } return true; } function createDirectoryIfNeeded(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } // ============================================================================ // Documentation Generation // ============================================================================ function generateDocumentation(_config, resolvedVars, descriptions, schema) { const lines = []; lines.push('# Configuration Documentation\n'); lines.push('Auto-generated by nxconfig.\n'); lines.push('## Environment Variables\n'); const varsInfo = []; for (const varName of resolvedVars) { const schemaInfo = schema?.[varName]; const description = descriptions?.[varName] || schemaInfo?.description || schemaInfo?.doc; varsInfo.push({ name: varName, type: schemaInfo?.type || 'string', required: schemaInfo?.required ?? false, default: schemaInfo?.default, description: description || undefined, sensitive: schemaInfo?.sensitive || isSecretField(varName) }); } varsInfo.sort((a, b) => { if (a.required !== b.required) return a.required ? -1 : 1; return a.name.localeCompare(b.name); }); lines.push('| Variable | Type | Required | Default | Description | Sensitive |'); lines.push('|----------|------|----------|---------|-------------|-----------|'); for (const info of varsInfo) { const required = info.required ? '✅ Yes' : '❌ No'; const defaultVal = info.default !== undefined ? `\`${info.default}\`` : '-'; const desc = info.description || 'No description'; const sensitive = info.sensitive ? '🔒 Yes' : '❌ No'; lines.push(`| \`${info.name}\` | \`${info.type}\` | ${required} | ${defaultVal} | ${desc} | ${sensitive} |`); } lines.push('\n## Usage Example\n'); lines.push('```bash'); lines.push('# Set required environment variables'); for (const info of varsInfo.filter(v => v.required)) { lines.push(`export ${info.name}="your-value-here"`); } lines.push('```\n'); return lines.join('\n'); } // ============================================================================ // Prefix/Suffix Resolution // ============================================================================ function resolveEnvVarName(varName, prefix, suffix) { if (suffix) { const withSuffix = (prefix || '') + varName + suffix; if (process.env[withSuffix] !== undefined) { return withSuffix; } } const withPrefix = (prefix || '') + varName; if (process.env[withPrefix] !== undefined) { return withPrefix; } if (process.env[varName] !== undefined) { return varName; } return (prefix || '') + varName; } // ============================================================================ // Schema Validation // ============================================================================ function validateSchema(config, schema) { const errors = []; for (const [field, definition] of Object.entries(schema)) { const value = config[field]; if (definition.required && (value === undefined || value === null)) { errors.push(`Field "${field}" is required`); continue; } if (value === undefined || (value === null && !definition.nullable)) { if (value === null && !definition.nullable) { errors.push(`Field "${field}" cannot be null`); } continue; } if (definition.isDirectory) { if (!validatePath(value, true, false)) { errors.push(`Field "${field}" must be a valid directory path`); } } if (definition.isFile) { if (!validatePath(value, false, true)) { errors.push(`Field "${field}" must be a valid file path`); } } if (definition.format) { if (typeof definition.format === 'function') { if (!definition.format(value)) { errors.push(`Field "${field}" failed custom format validation`); } } else if (typeof definition.format === 'string') { const builtinValidator = BUILTIN_FORMATS[definition.format]; if (builtinValidator && !builtinValidator(value)) { errors.push(`Field "${field}" must be a valid ${definition.format}`); } } } const expectedType = definition.type; let actualType; if (Array.isArray(value)) { actualType = 'array'; } else if (value === null) { actualType = 'null'; } else if (typeof value === 'bigint') { actualType = 'bigint'; } else { actualType = typeof value; } switch (expectedType) { case 'number': case 'int': case 'nat': case 'port': if (typeof value !== 'number') { errors.push(`Field "${field}" must be a number, got ${actualType}`); } else { if (definition.min !== undefined && value < definition.min) { errors.push(`Field "${field}" must be >= ${definition.min}`); } if (definition.max !== undefined && value > definition.max) { errors.push(`Field "${field}" must be <= ${definition.max}`); } } break; case 'string': if (typeof value !== 'string') { errors.push(`Field "${field}" must be a string, got ${actualType}`); } else { if (definition.pattern && !definition.pattern.test(value)) { errors.push(`Field "${field}" does not match required pattern`); } } break; case 'boolean': if (typeof value !== 'boolean') { errors.push(`Field "${field}" must be a boolean, got ${actualType}`); } break; case 'bigint': if (typeof value !== 'bigint') { errors.push(`Field "${field}" must be a bigint, got ${actualType}`); } break; case 'array': if (!Array.isArray(value)) { errors.push(`Field "${field}" must be an array, got ${actualType}`); } break; } if (definition.enum && !definition.enum.includes(value)) { errors.push(`Field "${field}" must be one of: ${definition.enum.join(', ')}`); } } return { valid: errors.length === 0, errors }; } // ============================================================================ // File Operations // ============================================================================ function loadConfigFromFile(filePath, encoding = 'utf8') { try { const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath); if (!fs.existsSync(absolutePath)) { throw new ConfigFileError(`Configuration file not found: ${filePath}`, filePath); } const content = fs.readFileSync(absolutePath, encoding); return JSON.parse(content); } catch (error) { if (error instanceof ConfigFileError) throw error; const message = error instanceof Error ? error.message : 'Unknown error'; throw new ConfigFileError(`Failed to read or parse configuration file: ${message}`, filePath, error instanceof Error ? error : undefined); } } function normalizeConfigSource(config, encoding = 'utf8') { if (typeof config === 'string') { return { config: loadConfigFromFile(config, encoding), filePath: config, }; } if (config && typeof config === 'object' && 'filePath' in config) { return { config: loadConfigFromFile(config.filePath, encoding), filePath: config.filePath, }; } return { config }; } // ============================================================================ // Hierarchical Configuration Stores // ============================================================================ class ConfigurationHierarchy { constructor() { this.stores = new Map(); this.data = {}; this.stores.set('memory', {}); } set(key, value, separator = ':') { setNestedValue(this.data, key, value, separator); } get(key, separator = ':') { return getNestedValue(this.data, key, separator); } has(key, separator = ':') { return this.get(key, separator) !== undefined; } reset(key, separator = ':') { const keys = key.split(separator); const lastKey = keys.pop(); let current = this.data; for (const k of keys) { if (!(k in current)) return; current = current[k]; } delete current[lastKey]; } load(data) { this.data = { ...this.data, ...data }; } merge(data) { this.data = deepMerge(this.data, data); } save(callback) { if (callback) callback(); } toObject() { return { ...this.data }; } toString() { return JSON.stringify(maskSecrets(this.data), null, 2); } } exports.ConfigurationHierarchy = ConfigurationHierarchy; // ============================================================================ // Core Parse Config Function (Original Requirement Compatible) // ============================================================================ function parseConfig(config, options = {}) { const { strict = true, verbose = false, filePath: customFilePath, encoding = 'utf8', dotenvPath, defaults = {}, transform = {}, prefix, suffix, createDirectories = false, commandLineArgs = false, } = options; if (dotenvPath) { loadDotenv(dotenvPath); } const { config: configObj, filePath } = normalizeConfigSource(config, encoding); const effectiveFilePath = customFilePath || filePath; let cliArgs = {}; if (commandLineArgs) { cliArgs = parseCommandLineArgs(); } const resolvedVars = new Set(); const warnings = []; const errors = []; const directoriesToCreate = []; function parseValue(value, currentPath = '') { if (typeof value === 'string') { // Check for multi-segment pattern with type: ENV.TEST.VARIABLE:type||default let match = value.match(ENV_TOKEN_WITH_TYPE); if (match && match[1] && match[2]) { const [, envPath, varName, type, defaultValue] = match; let envValue = loadEnvFileByToken(envPath, varName); if (commandLineArgs && varName && cliArgs[varName] !== undefined) { envValue = cliArgs[varName]; } if (envValue === undefined && defaultValue !== undefined) { envValue = defaultValue; warnings.push(`Using default value for ${varName} at ${currentPath || 'root'}`); } if (envValue === undefined && varName && defaults[varName] !== undefined) { envValue = defaults[varName]; warnings.push(`Using options default for ${varName} at ${currentPath || 'root'}`); } if (envValue === undefined) { const errorMessage = `Missing environment variable: ${varName} in .env.${envPath.toLowerCase()}`; const fullMessage = currentPath ? `${errorMessage} at path: ${currentPath}` : errorMessage; if (strict) { throw new ConfigParseError(fullMessage, varName, currentPath, effectiveFilePath); } else { errors.push(fullMessage); return value; } } if (varName) { resolvedVars.add(varName); } let finalValue = envValue; if (type) { try { finalValue = coerceType(envValue, type); } catch (error) { const msg = error instanceof Error ? error.message : 'Type coercion failed'; throw new ConfigParseError(`${msg} for ${varName} at ${currentPath}`, varName, currentPath, effectiveFilePath); } } if (varName && transform[varName]) { finalValue = transform[varName](finalValue); } if (createDirectories && currentPath.toLowerCase().includes('dir')) { directoriesToCreate.push(finalValue); } if (verbose) { const pathParts = currentPath.split('.').filter(Boolean); const fieldName = pathParts[pathParts.length - 1] || ''; const displayValue = isSecretField(fieldName) ? maskSecret(String(finalValue)) : finalValue; console.log(`[nxconfig] Resolved ${varName} from .env.${envPath.toLowerCase()} at ${currentPath || 'root'}: ${displayValue}`); } if (currentPath) { const pathParts = currentPath.split('.').filter(Boolean); const fieldName = pathParts[pathParts.length - 1] || ''; if (isSecretField(fieldName)) { warnings.push(`Detected secret field at path: ${currentPath}`); } } return finalValue; } // Check for multi-segment pattern without type: ENV.TEST.VARIABLE||default match = value.match(ENV_TOKEN_PATTERN); if (match && match[1] && match[2]) { const [, envPath, varName, defaultValue] = match; let envValue = loadEnvFileByToken(envPath, varName); if (commandLineArgs && varName && cliArgs[varName] !== undefined) { envValue = cliArgs[varName]; } if (envValue === undefined && defaultValue !== undefined) { envValue = defaultValue; warnings.push(`Using default value for ${varName} at ${currentPath || 'root'}`); } if (envValue === undefined && varName && defaults[varName] !== undefined) { envValue = defaults[varName]; warnings.push(`Using options default for ${varName} at ${currentPath || 'root'}`); } if (envValue === undefined) { const errorMessage = `Missing environment variable: ${varName} in .env.${envPath.toLowerCase()}`; const fullMessage = currentPath ? `${errorMessage} at path: ${currentPath}` : errorMessage; if (strict) { throw new ConfigParseError(fullMessage, varName, currentPath, effectiveFilePath); } else { errors.push(fullMessage); return value; } } if (varName) { resolvedVars.add(varName); } let finalValue = envValue; if (varName && transform[varName]) { finalValue = transform[varName](finalValue); } if (createDirectories && currentPath.toLowerCase().includes('dir')) { directoriesToCreate.push(finalValue); } if (verbose) { const pathParts = currentPath.split('.').filter(Boolean); const fieldName = pathParts[pathParts.length - 1] || ''; const displayValue = isSecretField(fieldName) ? maskSecret(String(finalValue)) : finalValue; console.log(`[nxconfig] Resolved ${varName} from .env.${envPath.toLowerCase()} at ${currentPath || 'root'}: ${displayValue}`); } if (currentPath) { const pathParts = currentPath.split('.').filter(Boolean); const fieldName = pathParts[pathParts.length - 1] || ''; if (isSecretField(fieldName)) { warnings.push(`Detected secret field at path: ${currentPath}`); } } return finalValue; } // Check for simple pattern with type: ENV.VARIABLE:type||default match = value.match(ENV_SIMPLE_TOKEN_WITH_TYPE); if (match && match[1]) { const [, varName, type, defaultValue] = match; const resolvedVarName = resolveEnvVarName(varName, prefix, suffix); let envValue = loadSimpleEnvVariable(resolvedVarName); if (commandLineArgs && varName && cliArgs[varName] !== undefined) { envValue = cliArgs[varName]; } if (envValue === undefined && defaultValue !== undefined) { envValue = defaultValue; warnings.push(`Using default value for ${varName} at ${currentPath || 'root'}`); } if (envValue === undefined && varName && defaults[varName] !== undefined) { envValue = defaults[varName]; warnings.push(`Using options default for ${varName} at ${currentPath || 'root'}`); } if (envValue === undefined) { const errorMessage = `Missing environment variable: ${resolvedVarName}`; const fullMessage = currentPath ? `${errorMessage} at path: ${currentPath}` : errorMessage; if (strict) { throw new ConfigParseError(fullMessage, resolvedVarName, currentPath, effectiveFilePath); } else { errors.push(fullMessage); return value; } } if (varName) { resolvedVars.add(varName); } let finalValue = envValue; if (type) { try { finalValue = coerceType(envValue, type); } catch (error) { const msg = error instanceof Error ? error.message : 'Type coercion failed'; throw new ConfigParseError(`${msg} for ${varName} at ${currentPath}`, varName, currentPath, effectiveFilePath); } } if (varName && transform[varName]) { finalValue = transform[varName](finalValue); } if (createDirectories && currentPath.toLowerCase().includes('dir')) { directoriesToCreate.push(finalValue); } if (verbose) { const pathParts = currentPath.split('.').filter(Boolean); const fieldName = pathParts[pathParts.length - 1] || ''; const displayValue = isSecretField(fieldName) ? maskSecret(String(finalValue)) : finalValue; console.log(`[nxconfig] Resolved ${resolvedVarName} at ${currentPath || 'root'}: ${displayValue}`); } if (currentPath) { const pathParts = currentPath.split('.').filter(Boolean); const fieldName = pathParts[pathParts.length - 1] || ''; if (isSecretField(fieldName)) { warnings.push(`Detected secret field at path: ${currentPath}`); } } return finalValue; } // Check for simple pattern without type: ENV.VARIABLE||default match = value.match(ENV_SIMPLE_TOKEN_PATTERN); if (match && match[1]) { const [, varName, defaultValue] = match; const resolvedVarName = resolveEnvVarName(varName, prefix, suffix); let envValue = loadSimpleEnvVariable(resolvedVarName); if (commandLineArgs && varName && cliArgs[varName] !== undefined) { envValue = cliArgs[varName]; } if (envValue === undefined && defaultValue !== undefined) { envValue = defaultValue; warnings.push(`Using default value for ${varName} at ${currentPath || 'root'}`); } if (envValue === undefined && varName && defaults[varName] !== undefined) { envValue = defaults[varName]; warnings.push(`Using options default for ${varName} at ${currentPath || 'root'}`); } if (envValue === undefined) { const errorMessage = `Missing environment variable: ${resolvedVarName}`; const fullMessage = currentPath ? `${errorMessage} at path: ${currentPath}` : errorMessage; if (strict) { throw new ConfigParseError(fullMessage, resolvedVarName, currentPath, effectiveFilePath); } else { errors.push(fullMessage); return value; } } if (varName) { resolvedVars.add(varName); } let finalValue = envValue; if (varName && transform[varName]) { finalValue = transform[varName](finalValue); } if (createDirectories && currentPath.toLowerCase().includes('dir')) { directoriesToCreate.push(finalValue); } if (verbose) { const pathParts = currentPath.split('.').filter(Boolean); const fieldName = pathParts[pathParts.length - 1] || ''; const displayValue = isSecretField(fieldName) ? maskSecret(String(finalValue)) : finalValue; console.log(`[nxconfig] Resolved ${resolvedVarName} at ${currentPath || 'root'}: ${displayValue}`); } if (currentPath) { const pathParts = currentPath.split('.').filter(Boolean); const fieldName = pathParts[pathParts.length - 1] || ''; if (isSecretField(fieldName)) { warnings.push(`Detected secret field at path: ${currentPath}`); } } return finalValue; } return value; } if (Array.isArray(value)) { return value.map((item, index) => { const itemPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`; return parseValue(item, itemPath); }); } if (value !== null && typeof value === 'object') { const parsed = {}; for (const [key, val] of Object.entries(value)) { const newPath = currentPath ? `${currentPath}.${key}` : key; parsed[key] = parseValue(val, newPath); } return parsed; } return value; } const parsedConfig = parseValue(configObj); if (createDirectories) { for (const dir of directoriesToCreate) { try { createDirectoryIfNeeded(dir); if (verbose) { console.log(`[nxconfig] Created directory: ${dir}`); } } catch (error) { warnings.push(`Failed to create directory: ${dir}`); } } } return { config: parsedConfig, resolvedVars: Array.from(resolvedVars), warnings, errors, }; } // ============================================================================ // SMART MULTI-CONFIG FUNCTION (NEW!) // ============================================================================ function parseMultiConfig(options) { const { sources, mergeStrategy = 'deep', keepSeparate = false, names = [], extends: baseConfigPath, priority = 'last', ...parseOptions } = options; if (sources.length === 0) { throw new ConfigMergeError('No configuration sources provided', []); } const allResolvedVars = new Set(); const allWarnings = []; const allErrors = []; const parsedConfigs = []; const namedConfigs = {}; // Load base config if extends is specified if (baseConfigPath) { try { const baseResult = parseConfig(baseConfigPath, parseOptions); parsedConfigs.push(baseResult.config); baseResult.resolvedVars.forEach(v => allResolvedVars.add(v)); allWarnings.push(...baseResult.warnings); allErrors.push(...baseResult.errors); if (parseOptions.verbose) { console.log(`[nxconfig] Loaded base config from: ${baseConfigPath}`); } } catch (error) { throw new ConfigMergeError(`Failed to load base config: ${baseConfigPath}`, [baseConfigPath]); } } // Parse all sources for (let i = 0; i < sources.length; i++) { const source = sources[i]; const configName = names[i] || `config_${i}`; if (!source) { throw new ConfigMergeError(`Empty config source at index ${i}`, [`config_${i}`]); } try { const result = parseConfig(source, parseOptions); parsedConfigs.push(result.config); if (keepSeparate) { namedConfigs[configName] = result.config; } result.resolvedVars.forEach(v => allResolvedVars.add(v)); allWarnings.push(...result.warnings); allErrors.push(...result.errors); if (parseOptions.verbose) { console.log(`[nxconfig] Loaded config: ${configName}`); } } catch (error) { const sourceName = typeof source === 'string' ? source : `config_${i}`; throw new ConfigMergeError(`Failed to parse config source: ${sourceName}`, [sourceName]); } } // Apply priority if (priority === 'first') { parsedConfigs.reverse(); } // Merge or keep separate const mergedConfig = keepSeparate ? namedConfigs : mergeConfigs(parsedConfigs, mergeStrategy); return { config: mergedConfig, configs: namedConfigs, resolvedVars: Array.from(allResolvedVars), warnings: allWarnings, errors: allErrors, }; } // ============================================================================ // Verify Function // ============================================================================ function verify(variableNames) { const missing = []; const empty = []; const present = []; for (const varName of variableNames) { const value = process.env[varName]; if (value === undefined) { missing.push(varName); } else if (value === '') { empty.push(varName); } else { present.push(varName); } } return { validated: missing.length === 0 && empty.length === 0, missing, empty, present, }; } // ============================================================================ // Init Config Function (Original Requirement Compatible) // ============================================================================ function initConfig(config, options = {}) { const { requiredVars = [], validateOnInit = true, throwOnMissing = true, schema, generateDocs = fal