nxconfig-js
Version:
Next-generation configuration management for Node.js - Zero dependencies, TypeScript-first, production-ready
1,229 lines • 56.5 kB
JavaScript
"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