nx-config2
Version:
Next-generation configuration management for Node.js - Zero dependencies, TypeScript-first, production-ready
1,242 lines • 117 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 2.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.remoteConfig = remoteConfig;
exports.watchConfig = watchConfig;
exports.createConfig = createConfig;
exports.autoLoadConfig = autoLoadConfig;
exports.inspectSharedEnvFiles = inspectSharedEnvFiles;
exports.getVariableSourceMap = getVariableSourceMap;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const nx_remote_json_1 = require("nx-remote-json");
// ============================================================================
// 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;
}
// ============================================================================
// Heuristic JSON Parsing Helper
// ============================================================================
function tryParseJsonFileReference(value, basePath) {
const trimmed = value.trim();
// Check for file reference pattern: {{path.json}}
if (trimmed.startsWith('{{') && trimmed.endsWith('}}')) {
// Extract the file path (remove {{ and }})
const filePath = trimmed.slice(2, -2).trim();
// Only process if it ends with .json
if (filePath.endsWith('.json')) {
try {
// Resolve the path relative to the basePath or current working directory
const resolvedPath = basePath
? path.resolve(path.dirname(basePath), filePath)
: path.resolve(process.cwd(), filePath);
// Check if file exists
if (fs.existsSync(resolvedPath)) {
// Read and parse the JSON file
const fileContent = fs.readFileSync(resolvedPath, 'utf8');
const parsed = JSON.parse(fileContent);
return { success: true, result: parsed };
}
}
catch {
// Fall through to return failure (file doesn't exist or invalid JSON)
}
}
}
return { success: false, result: value };
}
function tryParseJson(value) {
const trimmed = value.trim();
// Check for object-like pattern: starts with { and ends with }
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
try {
const parsed = JSON.parse(value);
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
return { success: true, result: parsed };
}
}
catch {
// Fall through to return failure
}
}
// Check for array-like pattern: starts with [ and ends with ]
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return { success: true, result: parsed };
}
}
catch {
// Fall through to return failure
}
}
return { success: false, result: value };
}
// ============================================================================
// Type Coercion Functions
// ============================================================================
function coerceType(value, type, lenientJson = false) {
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 {
if (lenientJson) {
return value;
}
throw new Error(`Cannot parse "${value}" as JSON`);
}
case 'array':
// First try to parse as JSON array if it looks like one
const trimmed = value.trim();
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed;
}
}
catch {
// Fall through to comma-split behavior
}
}
// Fallback to comma-split behavior for non-JSON arrays
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;
}
}
/**
* Parse shared .env references from a .env file
*/
function parseSharedEnvReferences(envFilePath) {
if (!fs.existsSync(envFilePath)) {
return [];
}
const content = fs.readFileSync(envFilePath, 'utf-8');
const references = [];
// Pattern 1: .env=path (required)
// Pattern 2: .env.name=path (required, named)
// Pattern 3: .env?=path (optional)
// Pattern 4: .env.name?=path (optional, named)
const sharedEnvPattern = /^\.env(?:\.([a-zA-Z0-9_-]+))?(\?)?=(.+)$/gm;
let match;
while ((match = sharedEnvPattern.exec(content)) !== null) {
const name = match[1]; // Optional name (e.g., "global", "company")
const isOptional = match[2] === '?'; // Optional marker
const refPath = match[3]?.trim();
if (!refPath)
continue;
// Remove quotes if present
const cleanPath = refPath.replace(/^['"](.+)['"]$/, '$1');
references.push({
path: cleanPath,
...(name && { name }),
required: !isOptional // Required unless marked with ?
});
}
return references;
}
/**
* Resolve environment variables in path string
*/
function resolveEnvVarsInPath(pathStr) {
// Replace ${VAR_NAME} with environment variable value
return pathStr.replace(/\$\{([^}]+)\}/g, (match, varName) => {
return process.env[varName] || match;
});
}
/**
* Resolve shared .env path relative to the referencing .env file
*/
function resolveSharedEnvPath(referencingFile, sharedPath) {
// Resolve environment variables in path
let resolvedPath = resolveEnvVarsInPath(sharedPath);
// If absolute path, use as-is
if (path.isAbsolute(resolvedPath)) {
return path.normalize(resolvedPath);
}
// If relative path, resolve relative to the referencing file's directory
const referencingDir = path.dirname(referencingFile);
return path.resolve(referencingDir, resolvedPath);
}
/**
* Parse .env file content into key-value pairs
*/
function parseDotenvContent(content) {
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) {
const hasDoubleQuotes = value.startsWith('"') && value.endsWith('"');
const hasSingleQuotes = value.startsWith("'") && value.endsWith("'");
if (hasDoubleQuotes || hasSingleQuotes) {
value = value.slice(1, -1);
}
}
if (key && value !== undefined) {
envVars[key] = value;
}
}
}
return envVars;
}
/**
* Load shared .env files with circular reference protection
*/
function loadSharedEnvFiles(envFilePath, visited = new Set(), verbose = false) {
// Normalize path for circular reference check
const normalizedPath = path.normalize(envFilePath);
// Check for circular reference
if (visited.has(normalizedPath)) {
if (verbose) {
console.warn(`[nx-config2] Circular reference detected: ${normalizedPath}`);
}
return {};
}
visited.add(normalizedPath);
// Check if file exists
if (!fs.existsSync(envFilePath)) {
if (verbose) {
console.warn(`[nx-config2] .env file not found: ${envFilePath}`);
}
return {};
}
// Parse shared references from this file
const sharedRefs = parseSharedEnvReferences(envFilePath);
// Merged environment variables (priority: first loaded = lowest priority)
let mergedEnv = {};
// Load shared files first (in order)
for (const ref of sharedRefs) {
const resolvedPath = resolveSharedEnvPath(envFilePath, ref.path);
if (verbose) {
const namePart = ref.name ? `.${ref.name}` : '';
console.log(`[nx-config2] Loading shared .env${namePart}: ${resolvedPath}`);
}
if (!fs.existsSync(resolvedPath)) {
if (ref.required) {
throw new Error(`[nx-config2] Required shared .env file not found: ${resolvedPath}`);
}
if (verbose) {
console.warn(`[nx-config2] Optional shared .env file not found: ${resolvedPath}`);
}
continue;
}
// Recursively load shared files (supports nested sharing)
const sharedEnv = loadSharedEnvFiles(resolvedPath, visited, verbose);
// Merge shared env (current file overrides previous shared files)
mergedEnv = { ...mergedEnv, ...sharedEnv };
}
// Load current file and merge (current file overrides all shared files)
const content = fs.readFileSync(envFilePath, 'utf-8');
const currentEnv = parseDotenvContent(content);
// Remove .env= references from final merged env
const cleanedEnv = Object.fromEntries(Object.entries(currentEnv).filter(([key]) => !key.startsWith('.env')));
mergedEnv = { ...mergedEnv, ...cleanedEnv };
return mergedEnv;
}
function loadDotenv(dotenvPath = '.env', enableSharedEnv = true, verbose = false) {
const resolvedPath = path.isAbsolute(dotenvPath)
? dotenvPath
: path.resolve(process.cwd(), dotenvPath);
if (!fs.existsSync(resolvedPath)) {
if (verbose) {
console.warn(`[nx-config2] .env file not found: ${resolvedPath}`);
}
return {};
}
if (enableSharedEnv) {
if (verbose) {
console.log(`[nx-config2] Loading .env with shared file support: ${resolvedPath}`);
}
// Load with shared files
const mergedEnv = loadSharedEnvFiles(resolvedPath, new Set(), verbose);
// Apply to process.env
for (const [key, value] of Object.entries(mergedEnv)) {
if (!process.env[key]) {
process.env[key] = value;
}
}
return mergedEnv;
}
else {
// Original behavior (no shared support)
const content = fs.readFileSync(resolvedPath, 'utf8');
const envVars = parseDotenvContent(content);
for (const [key, value] of Object.entries(envVars)) {
if (!(key in process.env)) {
process.env[key] = value;
}
}
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, heuristicJsonParsing = true, lenientJson = false, enableSharedEnv = true, sharedEnvRequired = true, } = options;
if (dotenvPath) {
try {
loadDotenv(dotenvPath, enableSharedEnv, verbose);
}
catch (error) {
if (sharedEnvRequired) {
throw error;
}
if (verbose) {
console.warn(`[nx-config2] Error loading shared .env files:`, error);
}
}
}
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, lenientJson);
}
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;
// Apply heuristic parsing if enabled
if (heuristicJsonParsing && typeof finalValue === 'string') {
// First check for JSON file reference pattern: {{path.json}}
const fileRefResult = tryParseJsonFileReference(finalValue, effectiveFilePath);
if (fileRefResult.success) {
finalValue = fileRefResult.result;
}
else {
// Then check for inline JSON patterns: {...} or [...]
const jsonResult = tryParseJson(finalValue);
if (jsonResult.success) {
finalValue = jsonResult.result;
}
}
}
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;
// Check for JSON file reference pattern: {{path.json}}
if (heuristicJsonParsing && typeof finalValue === 'string') {
const fileRefResult = tryParseJsonFileReference(finalValue, effectiveFilePath);
if (fi