import { z } from "zod"; import { type DeepPartial } from "./utils/deep-merge.js"; import { type EnvField, type IntrospectOptions } from "./introspect.js"; /** * A {@link SettingsLoader} whose generics are erased to broad bounds, * suitable for the `extends` array. */ export type AnySettingsLoader = SettingsLoader, object, object>; type UnionToIntersection = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never; type ExtractEnv = L extends SettingsLoader ? z.infer : never; type ExtractConfig = L extends SettingsLoader, infer C, object> ? C : never; /** * Type-level merge of every parent loader's env type with the child's * own env type. Falls back to the child's env type alone when the * extends list is empty. */ export type MergedEnv = TExtends extends readonly [] ? TSelfEnv : UnionToIntersection> & TSelfEnv; /** * Type-level merge of every parent loader's config type with the * child's own config type. Mirrors the runtime `deepMerge` semantics. */ export type MergedConfig = TExtends extends readonly [] ? TSelfConfig : UnionToIntersection> & TSelfConfig; /** * Options for {@link defineSettings}. * * @typeParam TSchema - The zod env schema (must be a `z.object({...})`). * @typeParam TConfig - Shape of the layered, non-env "config" object. * @typeParam TSettings - Final settings shape returned by `build`. * @typeParam TExtends - Tuple of parent loaders this one extends. */ export interface DefineSettingsOptions, TConfig extends object, TSettings extends object, TExtends extends readonly AnySettingsLoader[] = readonly []> { /** * Parent loaders whose env schema, defaults, and perEnv are inherited. * The merge order is `extends[0]`, `extends[1]`, ..., then this loader's * own values on top — later layers win on key collisions. * * Modeled after `t3-oss/env`'s `extends` field. Use for monorepo * composition where multiple packages share a common base. */ extends?: TExtends; /** Zod schema that validates `process.env`. Must be a `z.object({...})`. */ envSchema: TSchema; /** * The env key whose value selects the `perEnv` branch to apply. * Typical choice: `'APP_ENV'` or `'NODE_ENV'`. May reference a key * supplied by a parent in `extends`. */ envKey: keyof MergedEnv> & string; /** * Optional env key that, when set, contains a JSON-encoded partial * config used as the highest-priority override layer. */ overrideEnvKey?: keyof MergedEnv> & string; /** * Explicit, first-class env-to-config overrides. Maps an env var name * (a key of `envSchema`) to a dot-path in the config. When that env var * is present at runtime, its validated value replaces the config value * at the given path — making "this env var overrides this config field" * a declared contract instead of an implicit `{ ...config, ...env }` * spread hidden inside `build()`. * * Applied as layer C: after `defaults` (A) and `perEnv` (B), but below * the `overrideEnvKey` JSON blob (D), which remains the final word. * Env vars that are absent (or `undefined` after parsing) are skipped, * so an unset override never clobbers a configured value. * * @example * ```ts * defineSettings({ * envSchema: z.object({ * APP_ENV: z.enum(['local', 'prod']), * TIMEOUT: z.coerce.number().optional(), * }), * envKey: 'APP_ENV', * defaults: { timeout: 3000 }, * perEnv: { local: {}, prod: { timeout: 5000 } }, * // CI can set TIMEOUT to override the per-env value without a code change. * envOverrides: { TIMEOUT: 'timeout' }, * build: (env, config) => config, * }); * ``` */ envOverrides?: Partial> & string, string>>; /** Defaults applied first (layer A). */ defaults: TConfig; /** Per-env overrides keyed by the value of `envKey` (layer B). */ perEnv: Record>>; /** * Optional validator for the JSON override layer. */ validateOverride?: (parsed: unknown) => DeepPartial>>; /** * Map the validated env and the final layered config into the public * settings object that the application consumes. With `extends`, the * `env` and `config` parameters contain the merged shape (parent + * child). */ build: (env: MergedEnv>, config: NoInfer>) => TSettings; /** * Called once when a JSON override is applied. Useful for emitting an * operational audit log without giving this package a logger dep. */ onOverride?: (overrides: DeepPartial>>, envValue: string) => void; /** * Customise which fields are flagged as secrets. See * {@link IntrospectOptions} for the defaults. */ secretPatterns?: IntrospectOptions["secretPatterns"]; } /** * Resolved view of a loader's effective options after merging any * `extends` parents. Generators and the CLI's check command read these * values rather than the user-supplied `opts`. */ export interface ResolvedSettings { /** Merged zod schema (parent schemas combined via `.merge(...)`). */ envSchema: z.ZodObject; /** The selected env key (child wins if both parent and child set it). */ envKey: string; /** Optional override env key. */ overrideEnvKey: string | undefined; /** Merged env-to-config overrides across the extends chain. */ envOverrides: Record; /** Merged defaults across the extends chain. */ defaults: Record; /** Merged per-env overrides across the extends chain. */ perEnv: Record>; } /** * A settings loader function with tooling metadata attached. */ export interface SettingsLoader = z.ZodObject, TConfig extends object = object, TSettings extends object = object> { (rawEnv: Record): TSettings; /** The options this loader was defined with (not the merged values). */ readonly opts: Readonly>; /** Introspected env fields for the *resolved* schema (parents + self). */ readonly envFields: readonly EnvField[]; /** The effective merged schema / defaults / perEnv used at runtime. */ readonly resolved: Readonly; } /** * Define a settings loader. * * @example * ```ts * import { z } from 'zod'; * import { defineSettings } from '@env-kit/node-settings'; * * export const settings = defineSettings({ * envSchema: z.object({ * APP_ENV: z.enum(['local', 'dev', 'prod']).default('local'), * DB_HOST: z.string(), * }), * envKey: 'APP_ENV', * defaults: { bucket: '' }, * perEnv: { * local: { bucket: 'local-bucket' }, * dev: { bucket: 'dev-bucket' }, * prod: { bucket: 'prod-bucket' }, * }, * build: (env, config) => ({ * dbHost: env.DB_HOST, * bucket: config.bucket, * }), * }); * ``` * * @example Monorepo composition with `extends`: * ```ts * // packages/shared/settings.base.ts * export const base = defineSettings({ * envSchema: z.object({ DB_HOST: z.string(), APP_ENV: z.enum(['local','prod']).default('local') }), * envKey: 'APP_ENV', * defaults: { region: 'us-east-1', logLevel: 'info' }, * perEnv: { local: { logLevel: 'debug' }, prod: {} }, * build: (env, config) => ({ dbHost: env.DB_HOST, ...config }), * }); * * // packages/content-api/settings.config.ts * export default defineSettings({ * extends: [base], * envSchema: z.object({ CONTENT_BUCKET: z.string() }), * envKey: 'APP_ENV', * defaults: { bucket: '' }, * perEnv: { * local: { bucket: 'local-content' }, * prod: { bucket: 'prod-content' }, * }, * build: (env, config) => ({ * contentBucket: env.CONTENT_BUCKET, * dbHost: env.DB_HOST, // inherited from base * bucket: config.bucket, * region: config.region, // inherited from base * }), * }); * ``` */ export declare function defineSettings, TConfig extends object, TSettings extends object, const TExtends extends readonly AnySettingsLoader[] = readonly []>(opts: DefineSettingsOptions): SettingsLoader; export {}; //# sourceMappingURL=define-settings.d.ts.map