Building on envapt
Read config from a library, framework, or CLI that depends on envapt, so the source, environment, and .env files the app sets up reach your reads.
Your package reads a variable or two, and the app that installs it decides where config comes from. The app may bind a source with useSource, pin the environment, or point envapt at another folder for its .env files, and it often does that after it imports your package. seedcord, a Discord framework built on envapt, follows each pattern here.
Declare envapt as a peer dependency
envapt keeps its source, its environment, and its cache in module state. Each installed copy keeps its own. If your package installs a second copy, the app's useSource call binds the app's copy, and your reads go through the other one. List envapt under peerDependencies, and the package manager installs one copy for both:
{
"peerDependencies": {
"envapt": "^9.0.0"
}
}Read inside your functions
A read at the top of a module runs when the app imports your package, which is before the app's own setup code. Put the read inside the function that needs it:
export function (): string {
return .('ACME_API_URL', 'https://api.acme.dev');
}If the app binds a PortableSource after importing your package, apiBaseUrl() reads from it. The same read at module scope ran first, against the default source, and keeps that answer.
Branch on the environment the same way. A check like Envapter.isProduction inside a function reads the environment the app pinned, while a module-scope constant holds the one detected at import.
Throw your own error for a missing key
getRequired throws envapt's MissingEnvValue error. If your package documents its own error codes, check the key with has first, so your users get your error:
export function (): string {
if (!.('ACME_API_KEY')) throw new ('MissingApiKey');
return .('ACME_API_KEY', .);
}has and getRequired decide whether a key is present the same way, so has returns false in exactly the cases where getRequired would throw MissingEnvValue.
Set the environment before you load the app
A CLI that starts the user's app often picks the mode itself, such as production for a build. Assign Envapter.environment before you import the user's code:
. = 'production';
await import();The assignment reloads the .env files on Node, so .env.production loads even though envapt read the files for development when your CLI imported it. Modules the user's entry imports then read the production values.
Bind a source for one dev session
A dev command runs in Node, and sometimes it needs values that the user's code gets from a file envapt doesn't read, like a platform's local vars file. Layer those values over FileSource with merge. Then put the call that undoes it on the AsyncDisposableStack that closes the rest of the session:
export async function (: ): <void> {
const = await ();
.((new (), new ()));
.(() => .(new ()));
}vars comes second in merge, so a key from that file wins over the same key from FileSource. The defer line binds a fresh FileSource when the stack is disposed. That's the same source the node build binds on import. envapt doesn't expose the current source, so you restore it by binding the default again.
The stack runs that line when the session stops. It also runs it if a later startup step throws and your code disposes the stack early. If your tool restarts the session in the same process, or runs more code after it ends, every later read goes through the default source again.
Restoring the default fits a process your tool starts, like a CLI. If the app bound its own source before your code runs, the defer line replaces that source with the default.
Set defaults the app can override
If your package already has the path to the app's .env files, set Envapter.baseDir when your entry module loads:
. = ;The app imports your package before its own code runs, so a baseDir the app assigns later replaces yours. Reading from a fixed directory shows what baseDir changes.
Write the bind into generated entries
On Workers and edge runtimes, envapt reads nothing until something calls useSource. If your build step writes the app's entry file, generate the bind as a module of its own:
export const = `import { env } from 'cloudflare:workers';
import { Envapter, PortableSource } from 'envapt';
Envapter.useSource(new PortableSource(env));
`;Import that module first in the entry you generate, above the import of the app. JavaScript runs every import before the statements in the same file, so a useSource line written into the entry itself would run after the app's modules have loaded and read. With the bind imported first, the app's code reads config with no setup of its own, the same as on Node. Cloudflare Workers explains the bind.
Testing your package
Your tests bind a PortableSource with the values each test needs, the same as an app's tests. If your test runner shares modules between files, envapt's state carries over too. Testing has the vitest setup for that.