/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * Resolves environment variables in a string. * Replaces $VAR_NAME and ${VAR_NAME} with their corresponding environment variable values. * If the environment variable is not defined, the original placeholder is preserved. * * @param value - The string that may contain environment variable placeholders * @returns The string with environment variables resolved * * @example * resolveEnvVarsInString("Token: $API_KEY") // Returns "Token: secret-123" * resolveEnvVarsInString("URL: ${BASE_URL}/api") // Returns "URL: https://api.example.com/api" * resolveEnvVarsInString("Missing: $UNDEFINED_VAR") // Returns "Missing: $UNDEFINED_VAR" */ export function resolveEnvVarsInString(value: string): string { // Find $VAR_NAME or ${VAR_NAME}. The pattern is passed to RegExp via an // identifier so it is not a static literal flagged by sonarjs/regular-expr. const envVarPattern = '\\$(?:(\\w+)|{([^}]+)})'; const envVarRegex = new RegExp(envVarPattern, 'g'); return value.replace(envVarRegex, (match, varName1, varName2) => { const varName = typeof varName1 === 'string' && varName1 !== '' ? varName1 : (varName2 as string); const envValue = process.env[varName]; if (typeof envValue === 'string') { return envValue; } return match; }); } /** * Recursively resolves environment variables in an object of any type. * Handles strings, arrays, nested objects, and preserves other primitive types. * Protected against circular references using a WeakSet to track visited objects. * * @param obj - The object to process for environment variable resolution * @returns A new object with environment variables resolved * * @example * const config = { * server: { * host: "$HOST", * port: "${PORT}", * enabled: true, * tags: ["$ENV", "api"] * } * }; * const resolved = resolveEnvVarsInObject(config); */ export function resolveEnvVarsInObject(obj: T): T { return resolveEnvVarsInObjectInternal(obj, new WeakSet()); } /** * Internal implementation of resolveEnvVarsInObject with circular reference protection. * * @param obj - The object to process * @param visited - WeakSet to track visited objects and prevent circular references * @returns A new object with environment variables resolved */ function resolveEnvVarsInObjectInternal( obj: T, visited: WeakSet, ): T { if ( obj === null || obj === undefined || typeof obj === 'boolean' || typeof obj === 'number' ) { return obj; } if (typeof obj === 'string') { return resolveEnvVarsInString(obj) as T; } if (Array.isArray(obj)) { // Check for circular reference if (visited.has(obj)) { // Return a shallow copy to break the cycle return [...obj] as T; } visited.add(obj); const result = obj.map((item) => resolveEnvVarsInObjectInternal(item, visited), ) as T; visited.delete(obj); return result; } if (typeof obj === 'object') { // Check for circular reference if (visited.has(obj as object)) { // Return a shallow copy to break the cycle return { ...obj } as T; } visited.add(obj as object); const newObj = { ...obj } as T; for (const key in newObj) { if (Object.prototype.hasOwnProperty.call(newObj, key)) { newObj[key] = resolveEnvVarsInObjectInternal(newObj[key], visited); } } visited.delete(obj as object); return newObj; } return obj; }