Environment variables
How seniority reads the environment: only declared options, named PREFIX_OPTION_NAME in SCREAMING_SNAKE, one boolean spelling each way, PREFIX_NO_X refused — and the environment always an argument.
The environment is a layer like any other, and it is passed in: resolve(specs, { env }).
Nothing in resolve reads process.env, so a test hands it an object and a program hands it
process.env.
The rules
import { resolve } from 'seniority';
const specs = { dryRun: { type: 'boolean' }, logLevel: { type: 'string' } };
const run = (env) => {
try {
return JSON.stringify(resolve(specs, { flags: {}, env, envPrefix: 'APP' }).values);
} catch (error) {
return `${error.message} (${error.hint})`;
}
};
console.log(run({ APP_DRY_RUN: 'yes', APP_LOG_LEVEL: 'debug' }));
console.log(run({ APP_DRY_RUN: 'false' }));
console.log(run({ APP_DRY_RUN: 'maybe' }));
console.log(run({ APP_NO_DRY_RUN: '1' }));
console.log(run({ APP_SECRET_FLAG: 'x', APP_LOGLEVEL: 'debug' }));{"dryRun":true,"logLevel":"debug"}
{"dryRun":false}
APP_DRY_RUN="maybe" is not a boolean (use APP_DRY_RUN=true or APP_DRY_RUN=false)
APP_NO_DRY_RUN is not supported (set APP_DRY_RUN=false instead)
{}Line by line:
- Names. With
envPrefix: 'APP', an option readsAPP_plus its name in SCREAMING_SNAKE.dryRunisAPP_DRY_RUN, andlogLevelandlog-levelare bothAPP_LOG_LEVEL. A name is never camel-cased back from a variable.- An option with an explicit
env: 'MY_TOKEN'reads that variable instead, prefix or not. - With no prefix and no
env:, an option reads no variable at all.
- An option with an explicit
- Booleans.
1,trueandyes, or0,falseandno, in any case. The string"false"is never silently truthy. - Anything else is a CONFIG error naming the variable, the value and the spelling to use.
APP_NO_Xis refused, pointing atAPP_X=false. Two spellings of one fact is how a configuration becomes unreadable.- Only declared options.
APP_SECRET_FLAGsets nothing, because no option declares it, andAPP_LOGLEVELis notAPP_LOG_LEVEL. The environment cannot inject a key the running command did not ask for, including a sibling command's option.
dotenv files
seniority/dotenv is dotenv's API (config, parse, populate), graded by dotenv's own
suite. Give config the object to fill as processEnv, and the .env file joins the
environment layer without touching process.env:
import { writeFileSync } from 'node:fs';
import { resolve } from 'seniority';
import dotenv from 'seniority/dotenv';
writeFileSync('.env', 'APP_REGION=eu-2\nAPP_DRY_RUN=yes\n');
const env = { APP_REGION: 'us-9' }; // already set: dotenv does not override it
dotenv.config({ path: '.env', processEnv: env, quiet: true });
const { values, provenance } = resolve({ region: { type: 'string' }, dryRun: { type: 'boolean' } }, { flags: {}, env, envPrefix: 'APP' });
console.log(values, provenance.region.location);{ region: 'us-9', dryRun: true } APP_REGIONCalled with no processEnv, config() populates process.env, as dotenv does. The encrypted
.env.vault and dotenvx's features are declined: see Compatibility.
What is tested
precedence.test.ts:- env applies only to declared options, by their own names;
- the six boolean spellings;
- anything else is a CONFIG error naming the fix;
PREFIX_NO_Xis rejected.
dotenv.test.ts:- parsing as dotenv 17.4.2 does;
configpopulates the object it was given.
Config files
discover() finds a config file in a fixed order, follows extends, and records each key's line; search() walks upward only when asked, bounded by stopAt, a depth limit and the root; loaders bring any format.
Validation
validate() returns every violation at once, each naming the rule, the value and the source that set it — file and line, flag or variable — and check() throws them as one CONFIG error.