Env
Typed, validated environment variables as a Context, documented in help and man pages.
@crustjs/env declares environment variables with the same vocabulary as flags and exposes the validated values as a Context.
Install
npm install @crustjs/env@crustjs/core is a required peer. TypeScript is an optional peer.
Quick example
import { Crust, CrustError } from "@crustjs/core";
import { defineEnv } from "@crustjs/env";
const env = defineEnv("env", {
DATABASE_URL: { type: "url", required: true, description: "Postgres connection" },
PORT: { type: "number", default: 3000 },
LOG_LEVEL: { type: "string", choices: ["debug", "info", "warn"], default: "info" },
TAGS: { type: "string", multiple: true, delimiter: "," },
});
const app = new Crust("app").provide(env()).command("serve", (cmd) =>
cmd.action(async ({ ctx }) => {
const { , PORT } = await ctx.env;
console.log(`listening on ${PORT} for ${DATABASE_URL.host}`);
}),
);defineEnv(name, vars, options?) returns an ordinary Context factory. Provide it at the root for the whole tree, or on a subcommand for only that subtree. Setup is lazy: the variables are read and validated on the first ctx.env read, so commands that never read it never fail. Read it first thing in the action, as with every Context.
Variable definitions
Keys are literal variable names. Each value is a flag definition without the argv-only keys short, aliases, noNegate, negatable, and hidden, and without env; those keys are type errors, and defineEnv() throws a DEFINITION CrustError if an untyped caller passes one. type, choices, required, default, multiple, delimiter, parse, description, and Standard Schema schema behave as they do for a flag read from its environment fallback, and the value types are inferred the same way.
defineEnv()checks and copies each definition asdefineFlag()does, so a default outsidechoicesor an emptydelimiterthrows aDEFINITIONCrustErrorwhen the Context is defined. Help and validation both use that copy; changing the definition object afterwards has no effect.- Values run through Core's flag pipeline:
choices, thenparseor built-in coercion, then defaults and requiredness, then Standard Schemas (async schemas are awaited). type: "boolean"istruefortrueor1andfalsefor any other text. A booleanschemareceives that boolean, not the raw text.multiple: truetreats the text as one occurrence. Withdelimiter, the text is split and empty segments are dropped; zero segments counts as unset.- Only own properties of the source are read, so a name like
constructornever readsObject.prototype.
Options
Prop
Type
const testEnv = defineEnv(
"env",
{ PORT: { type: "number", default: 3000 } },
{ source: { PORT: "8080" } },
);Option settings are read once, when defineEnv() is called. The values in source are read at first access, so tests can fill the record after defining the Context.
An empty string is a value by default: PORT="" fails type: "number", and NAME="" is "" rather than its default. Opt in to emptyStringAsUndefined when blank variables mean unset.
skipValidation: true is an escape hatch for build steps that run without the real environment. A literal true types every declared variable as string | undefined; a non-literal boolean, or options that may be undefined, types the value as either the raw or the validated shape. Undeclared variables are never returned.
Errors
A failed read rejects with one CrustError whose code is "ENV". details.issues lists every missing or invalid variable in declaration order as { name, expected, received }:
const outcome = await app.run(["serve"]);
if (outcome.status === "failed" && outcome.error instanceof CrustError && outcome.error.is("ENV")) {
for (const { name, expected, received } of outcome.error.details.issues) {
console.error(`${name}: ${received} (expected ${expected})`);
}
}received is "missing" or "invalid", and expected describes the declaration ("number", "one of: debug, info, warn", "list of string", "value accepted by schema"). Values never appear in the message, details, or toJSON(), and the error carries no cause, because parser and schema messages can quote a secret.
Help and man pages
.provide(env()) adds an "Environment" section (man ENVIRONMENT) to the providing command, listing each variable with its description, default, and choices:
Environment:
DATABASE_URL Postgres connection
PORT [default: 3000]
LOG_LEVEL [default: "info"] [choices: debug, info, warn]
TAGSThe section appears only on the command where the Context is provided, not on its descendants, and never shows source values.
Out of scope
Loading .env files, a prefix option (names are literal), and a warn-only mode are not supported.