From 054fc1219fa6686ffb42d7f33c46b1be84f35fa8 Mon Sep 17 00:00:00 2001 From: Corentin Thomasset Date: Sun, 21 Sep 2025 00:57:56 +0200 Subject: [PATCH] feat(env): support multiple environment variables --- README.md | 48 ++++++++++++++++++++++++++++++++++++++ src/figue.test.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++++ src/figue.ts | 12 ++++++++-- src/figue.types.ts | 2 +- 4 files changed, 116 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f237d38..7fee668 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,54 @@ const { config } = defineConfig( ); ``` +### Environment variable fallback + +You can specify multiple environment variable names for a single configuration field by providing an array of strings. Figue will use the first environment variable that is found in your environment sources. + +This feature is particularly useful when: +- Supporting multiple deployment environments with different environment variable naming conventions +- Migrating from legacy environment variable names while maintaining backward compatibility +- Providing fallback options for missing environment variables + +```typescript +const { config } = defineConfig( + { + port: { + doc: 'Application port to listen', + schema: z.coerce.number(), + default: 3000, + // Will use PORT if available, otherwise APP_PORT, otherwise SERVER_PORT + env: ['PORT', 'APP_PORT', 'SERVER_PORT'], + }, + database: { + host: { + doc: 'Database host', + schema: z.string(), + default: 'localhost', + // Useful for supporting legacy environment variable names + env: ['DATABASE_HOST', 'DB_HOST', 'LEGACY_DB_URL'], + }, + }, + workerId: { + doc: 'Worker identifier', + schema: z.string().optional(), + // Or using some plateform specific environment variable + env: ['WORKER_ID', 'HEROKU_DYNO_ID', 'RENDER_INSTANCE_ID'], + }, + }, + { + envSource: process.env, + }, +); +``` + + + +Some caveats: +- If none of the specified environment variables are found, Figue will fall back to the default as expected when no `env` key is present. +- If a variable is found but its value is nullish or falsy (like an empty string, or undefined), Figue will still consider it as set and use that value. If you want to ignore such values, you should handle that in your schema validation. +- Ensure that the order of environment variables in the array reflects their priority, as Figue will use the first one it finds. + ### Get defaults You can use the `getDefaults` key of the second argument of `defineConfig` to specify a function that will be called to get some defaults: diff --git a/src/figue.test.ts b/src/figue.test.ts index 1cf50d4..688646a 100644 --- a/src/figue.test.ts +++ b/src/figue.test.ts @@ -174,6 +174,63 @@ describe('figue tests', () => { }); }); + describe('the env option can be an array of env variables', () => { + test('the first env variable found is used', () => { + const { config } = defineConfig({ + value: { + schema: z.any(), + env: ['NOT_EXISTING', 'BAR', 'ALSO_NOT_EXISTING', 'FOO'], + default: 'default', + }, + }, { + envSource: { + FOO: 'foo', + BAR: 'bar', + }, + }); + + expect(config).toEqual({ + value: 'bar', + }); + }); + + test('if an env variable exists but is empty, it is used', () => { + const { config } = defineConfig({ + value: { + schema: z.any(), + env: ['NOT_EXISTING', 'BAR', 'ALSO_NOT_EXISTING', 'FOO'], + }, + }, { + envSource: { + FOO: 'foo', + BAR: undefined, + }, + }); + + expect(config).toEqual({ + value: undefined, + }); + }); + + test('if an env variable exists but is falsy, it is used', () => { + const { config } = defineConfig({ + value: { + schema: z.any(), + env: ['NOT_EXISTING', 'BAR', 'ALSO_NOT_EXISTING', 'FOO'], + }, + }, { + envSource: { + FOO: 'foo', + BAR: false, + }, + }); + + expect(config).toEqual({ + value: false, + }); + }); + }); + describe('any validation libraries can be used', () => { test('you can merge some schema from zod and some from valibot', () => { const { config } = defineConfig({ diff --git a/src/figue.ts b/src/figue.ts index 1ada6ee..f7e534b 100644 --- a/src/figue.ts +++ b/src/figue.ts @@ -105,8 +105,16 @@ function buildEnvConfig({ configDefinition, env }: { configDefinition: ConfigDef return undefined; } - const value = env[envKey as string]; - return value; + const envKeys = castArray(envKey); + + // Find the first environment variable that is set + const key = envKeys.find(key => key in env); + + if (key === undefined) { + return undefined; + } + + return env[key]; } else { return buildEnvConfig({ configDefinition: config, env }); } diff --git a/src/figue.types.ts b/src/figue.types.ts index 763a743..44a80ba 100644 --- a/src/figue.types.ts +++ b/src/figue.types.ts @@ -3,7 +3,7 @@ import type { Expand } from './types'; export type ConfigDefinitionElement = { schema: T; - env?: string; + env?: string | string[]; doc?: string; default?: StandardSchemaV1.InferOutput; };