import type { FlagKey, FlagRegistry, FlagSnapshot } from "./define-flags"; import { resolveEnvironment, type EnvSource } from "./env"; const warnedInvalidOverrides = new Set(); /** * Options used when resolving flag values. * * @example * ```ts * const options: ResolveOptions = { * env: { NODE_ENV: "staging", FLAG_NEW_CHECKOUT: "true" }, * }; * ``` */ export type ResolveOptions = { /** * Environment variable source. Defaults to `process.env`. */ readonly env?: EnvSource; /** * Receives invalid local override warnings. Defaults to `console.warn`. */ readonly warn?: (message: string) => void; }; function getFlagEnvName(flagKey: string): string { return `FLAG_${flagKey}`; } function parseOverride(value: string): boolean | undefined { const normalized = value.trim().toLowerCase(); if (normalized === "true") { return true; } if (normalized === "false") { return false; } return undefined; } function warnInvalidOverrideOnce( envName: string, value: string, warn: (message: string) => void ) { const warningKey = `${envName}:${value}`; if (warnedInvalidOverrides.has(warningKey)) { return; } warnedInvalidOverrides.add(warningKey); warn( `[flagkeeper] Ignoring invalid ${envName} value "${value}". Expected "true" or "false".` ); } /** * Resolves one flag from a registry. * * Local `FLAG_` overrides take precedence over environment defaults when * set to `true` or `false`. * * @example * ```ts * const enabled = resolveFlag(flags, "NEW_CHECKOUT", { * env: { NODE_ENV: "production" }, * }); * ``` */ export function resolveFlag< TRegistry extends FlagRegistry, TKey extends FlagKey, >(registry: TRegistry, key: TKey, options: ResolveOptions = {}): boolean { const env = options.env ?? process.env; const defaults = registry[key].defaults; const environment = resolveEnvironment(env, defaults); const defaultValue = defaults[environment] ?? defaults.production; const envName = getFlagEnvName(key); const overrideValue = env[envName]; if (overrideValue === undefined) { return defaultValue; } const parsedOverride = parseOverride(overrideValue); if (parsedOverride !== undefined) { return parsedOverride; } warnInvalidOverrideOnce(envName, overrideValue, options.warn ?? console.warn); return defaultValue; } /** * Resolves every flag in a registry into a typed boolean snapshot. * * @example * ```ts * const snapshot = resolveAllFlags(flags, { * env: { NODE_ENV: "staging" }, * }); * ``` */ export function resolveAllFlags( registry: TRegistry, options: ResolveOptions = {} ): FlagSnapshot { const snapshot = {} as Record, boolean>; for (const key of Object.keys(registry) as FlagKey[]) { snapshot[key] = resolveFlag(registry, key, options); } return snapshot as FlagSnapshot; }