Spaces:
Sleeping
Sleeping
| /** | |
| * Environment variable substitution for config values. | |
| * | |
| * Supports `${VAR_NAME}` syntax in string values, substituted at config load time. | |
| * - Only uppercase env vars are matched: `[A-Z_][A-Z0-9_]*` | |
| * - Escape with `$${}` to output literal `${}` | |
| * - Missing env vars throw `MissingEnvVarError` with context | |
| * | |
| * @example | |
| * ```json5 | |
| * { | |
| * models: { | |
| * providers: { | |
| * "vercel-gateway": { | |
| * apiKey: "${VERCEL_GATEWAY_API_KEY}" | |
| * } | |
| * } | |
| * } | |
| * } | |
| * ``` | |
| */ | |
| // Pattern for valid uppercase env var names: starts with letter or underscore, | |
| // followed by letters, numbers, or underscores (all uppercase) | |
| const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/; | |
| export class MissingEnvVarError extends Error { | |
| constructor( | |
| public readonly varName: string, | |
| public readonly configPath: string, | |
| ) { | |
| super(`Missing env var "${varName}" referenced at config path: ${configPath}`); | |
| this.name = "MissingEnvVarError"; | |
| } | |
| } | |
| function isPlainObject(value: unknown): value is Record<string, unknown> { | |
| return ( | |
| typeof value === "object" && | |
| value !== null && | |
| !Array.isArray(value) && | |
| Object.prototype.toString.call(value) === "[object Object]" | |
| ); | |
| } | |
| function substituteString(value: string, env: NodeJS.ProcessEnv, configPath: string): string { | |
| if (!value.includes("$")) { | |
| return value; | |
| } | |
| const chunks: string[] = []; | |
| for (let i = 0; i < value.length; i += 1) { | |
| const char = value[i]; | |
| if (char !== "$") { | |
| chunks.push(char); | |
| continue; | |
| } | |
| const next = value[i + 1]; | |
| const afterNext = value[i + 2]; | |
| // Escaped: $${VAR} -> ${VAR} | |
| if (next === "$" && afterNext === "{") { | |
| const start = i + 3; | |
| const end = value.indexOf("}", start); | |
| if (end !== -1) { | |
| const name = value.slice(start, end); | |
| if (ENV_VAR_NAME_PATTERN.test(name)) { | |
| chunks.push(`\${${name}}`); | |
| i = end; | |
| continue; | |
| } | |
| } | |
| } | |
| // Substitution: ${VAR} -> value | |
| if (next === "{") { | |
| const start = i + 2; | |
| const end = value.indexOf("}", start); | |
| if (end !== -1) { | |
| const name = value.slice(start, end); | |
| if (ENV_VAR_NAME_PATTERN.test(name)) { | |
| const envValue = env[name]; | |
| if (envValue === undefined || envValue === "") { | |
| throw new MissingEnvVarError(name, configPath); | |
| } | |
| chunks.push(envValue); | |
| i = end; | |
| continue; | |
| } | |
| } | |
| } | |
| // Leave untouched if not a recognized pattern | |
| chunks.push(char); | |
| } | |
| return chunks.join(""); | |
| } | |
| function substituteAny(value: unknown, env: NodeJS.ProcessEnv, path: string): unknown { | |
| if (typeof value === "string") { | |
| return substituteString(value, env, path); | |
| } | |
| if (Array.isArray(value)) { | |
| return value.map((item, index) => substituteAny(item, env, `${path}[${index}]`)); | |
| } | |
| if (isPlainObject(value)) { | |
| const result: Record<string, unknown> = {}; | |
| for (const [key, val] of Object.entries(value)) { | |
| const childPath = path ? `${path}.${key}` : key; | |
| result[key] = substituteAny(val, env, childPath); | |
| } | |
| return result; | |
| } | |
| // Primitives (number, boolean, null) pass through unchanged | |
| return value; | |
| } | |
| /** | |
| * Resolves `${VAR_NAME}` environment variable references in config values. | |
| * | |
| * @param obj - The parsed config object (after JSON5 parse and $include resolution) | |
| * @param env - Environment variables to use for substitution (defaults to process.env) | |
| * @returns The config object with env vars substituted | |
| * @throws {MissingEnvVarError} If a referenced env var is not set or empty | |
| */ | |
| export function resolveConfigEnvVars(obj: unknown, env: NodeJS.ProcessEnv = process.env): unknown { | |
| return substituteAny(obj, env, ""); | |
| } | |