File size: 4,634 Bytes
7421850 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | /**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Resolves environment variables in a string.
* Replaces $VAR_NAME, ${VAR_NAME}, and ${VAR_NAME:-DEFAULT_VALUE} with their corresponding
* environment variable values. If the environment variable is not defined and no default
* value is provided, the original placeholder is preserved.
*
* @param value - The string that may contain environment variable placeholders
* @param customEnv - Optional record of environment variables to use before process.env
* @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("URL: ${MISSING_VAR:-https://default.com}") // Returns "URL: https://default.com"
* resolveEnvVarsInString("Missing: $UNDEFINED_VAR") // Returns "Missing: $UNDEFINED_VAR"
*/
export function resolveEnvVarsInString(
value: string,
customEnv?: Record<string, string>,
): string {
// Regex matches $VAR_NAME, ${VAR_NAME}, and ${VAR_NAME:-DEFAULT_VALUE}
const envVarRegex = /\$(?:(\w+)|{([^}]+?)(?::-([^}]*))?})/g;
return value.replace(
envVarRegex,
(
match: string,
varName1?: string,
varName2?: string,
defaultValue?: string,
): string => {
const varName: string = varName1 || varName2 || '';
if (!varName) {
return match;
}
if (customEnv && typeof customEnv[varName] === 'string') {
return customEnv[varName];
}
if (process && process.env) {
const val = process.env[varName];
if (typeof val === 'string') {
return val;
}
}
if (defaultValue !== undefined) {
return defaultValue;
}
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<T>(
obj: T,
customEnv?: Record<string, string>,
): T {
return resolveEnvVarsInObjectInternal(obj, new WeakSet(), customEnv);
}
/**
* 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<T>(
obj: T,
visited: WeakSet<object>,
customEnv?: Record<string, string>,
): T {
if (
obj === null ||
obj === undefined ||
typeof obj === 'boolean' ||
typeof obj === 'number'
) {
return obj;
}
if (typeof obj === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return resolveEnvVarsInString(obj, customEnv) as unknown as T;
}
if (Array.isArray(obj)) {
// Check for circular reference
if (visited.has(obj)) {
// Return a shallow copy to break the cycle
const copy: unknown = [...obj];
const isTArray = (val: unknown): val is T => Array.isArray(val);
if (isTArray(copy)) return copy;
throw new Error('Unreachable');
}
visited.add(obj);
const mapped: unknown = obj.map((item: unknown) =>
resolveEnvVarsInObjectInternal(item, visited, customEnv),
);
visited.delete(obj);
const isTArray = (val: unknown): val is T => Array.isArray(val);
if (isTArray(mapped)) return mapped;
throw new Error('Unreachable');
}
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,
customEnv,
);
}
}
visited.delete(obj as object);
return newObj;
}
return obj;
}
|