Environment Composition
Compose independent server contexts through one canonical extension path.
Fragments let each module own its values and runtime options:
import {
createEnv,
extendEnv,
port,
postgresUrl,
server,
shared,
} from "@ayronforge/envil";
export const baseEnv = createEnv(
shared({ APP_NAME: "My App" }),
server({ DATABASE_URL: postgresUrl }),
);
export const serviceEnv = createEnv(
server({ SERVICE_NAME: "payments" }),
);
export const appEnv = baseEnv.pipe(
extendEnv(serviceEnv),
extendEnv(server({ PORT: port })),
);
extendEnv accepts AppEnv values and individual fragments. It is the canonical
way to compose an existing environment; createEnv accepts only fragments.
This keeps ordering explicit and gives pipe one predictable extension model.
Last definition wins
Composition is applied from left to right:
const appEnv = baseEnv.pipe(
extendEnv(server({ PORT: port })),
extendEnv(
server(
{ PORT: positiveInt },
{ runtimeEnv: deploymentEnv, prefix: "APP_" },
),
),
);
The second PORT is the entire winning definition. Its schema, source,
runtimeEnv, prefix, redaction, errors, and Effect requirements replace the
earlier definition. Envil does not merge pieces of duplicate definitions, and
a resolver owned only by a shadowed definition is never initialized.
Server and client may use the same property name. A server output includes both
targets, so the later fragment among server(...), client(...), and
shared(...) wins for that key there. A client output considers only client and
shared fragments.
No file-layout convention
Fragments may be declared inline, exported from feature modules, returned by
ordinary factories, or assembled with spreads. Envil imposes no filename or
directory convention. The compiler only uses the explicit server(...)
boundary; it does not interpret the expressions inside it.