Package Exports
- yassa
- yassa/package.json
Readme
yassa
Resolve environment-aware config file hierarchy for Node.js applications.
yassa is the modernized successor of
resolve-node-configs-hierarchy,
maintained by the same developers.
yassa helps you find:
- The full precedence chain of existing config files (most specific first), or
- The single most authoritative file for the current or explicit environment.
It is designed for predictable config loading in environments like development, test, and production.
Table of Contents
- Installation
- When to Use
- How Resolution Works
- Quick Start
- API Reference
- Runtime Environment (
process.env.NODE_ENV) - Explicit Environment Factories
- Behavior Details
localIgnoredEnvironmentssemantics- Error behavior
- Common Patterns
- Edge Cases
- FAQ
- Why the Name "Yassa"
Installation
npm install yassaRequirements
- Node.js
>=16(runtime) - Works with ESM and CJS consumers via package exports
When to Use
Use yassa when your project has layered config files such as:
.env.env.production.env.local.env.production.local
and you need deterministic precedence logic while safely ignoring missing files.
Typical use cases:
- Environment variable bootstrap
- JSON/YAML config file selection
- Team-safe test config loading (ignore local overrides in tests)
How Resolution Works
Given a base file (for example .env) and an environment (development), yassa builds candidates in this order:
<name>.<environment>.local<ext><name>.local<ext><name>.<environment><ext><name><ext>
Then it returns only candidates that are:
- Existing
- Readable
- Regular files (directories are excluded)
Important details:
- Paths are resolved from
realpath(process.cwd()). - Symlinks to files are supported (resolved via
stat). - Missing candidates are ignored (not treated as errors).
- Return order is always most specific first.
This precedence model is inspired by the Ruby on Rails-style .env layering documented in dotenv:
https://github.com/bkeepers/dotenv?tab=readme-ov-file#customizing-rails
Quick Start
Resolve chain for current NODE_ENV
import { resolveConfigChain } from "yassa";
process.env.NODE_ENV = "development";
const files = await resolveConfigChain(".env");
// Example result:
// [
// "/app/.env.development.local",
// "/app/.env.local",
// "/app/.env.development",
// "/app/.env"
// ]Resolve only the most authoritative file
import { resolveConfigFile } from "yassa";
process.env.NODE_ENV = "production";
const file = await resolveConfigFile("config/app.json");
// "/app/config/app.production.local.json" | ... | undefinedExplicit environment (independent from NODE_ENV)
import { resolveConfigChainFor, resolveConfigFileFor } from "yassa";
const resolveStagingChain = resolveConfigChainFor("staging");
const resolveStagingFile = resolveConfigFileFor("staging");
const chain = await resolveStagingChain(".env");
const top = await resolveStagingFile(".env");Team-reproducible test mode (ignore local files for test)
import { resolveConfigChainFor } from "yassa";
const resolveTestChain = resolveConfigChainFor("test");
const chain = await resolveTestChain(".env", ["test"]);
// For test env, local overrides are excluded:
// ["/app/.env.test", "/app/.env"]API Reference
Runtime Environment (process.env.NODE_ENV)
resolveConfigChain(file, localIgnoredEnvironments?)
- Type:
(file: string, localIgnoredEnvironments?: string[]) => Promise<string[]> - Resolves existing chain for current
NODE_ENV. - Throws if
NODE_ENVis missing or empty.
resolveConfigChainSync(file, localIgnoredEnvironments?)
- Type:
(file: string, localIgnoredEnvironments?: string[]) => string[] - Synchronous counterpart of
resolveConfigChain. - Throws if
NODE_ENVis missing or empty.
resolveConfigFile(file, localIgnoredEnvironments?)
- Type:
(file: string, localIgnoredEnvironments?: string[]) => Promise<string | undefined> - Returns the top file from
resolveConfigChain. - Returns
undefinedwhen no candidates exist. - Throws if
NODE_ENVis missing or empty.
resolveConfigFileSync(file, localIgnoredEnvironments?)
- Type:
(file: string, localIgnoredEnvironments?: string[]) => string | undefined - Synchronous counterpart of
resolveConfigFile. - Throws if
NODE_ENVis missing or empty.
Explicit Environment Factories
resolveConfigChainFor(environment)
- Type:
(environment: string) => (file: string, localIgnoredEnvironments?: string[]) => Promise<string[]> - Curried async factory for environment-bound chain resolution.
resolveConfigChainForSync(environment)
- Type:
(environment: string) => (file: string, localIgnoredEnvironments?: string[]) => string[] - Synchronous counterpart of
resolveConfigChainFor.
resolveConfigFileFor(environment)
- Type:
(environment: string) => (file: string, localIgnoredEnvironments?: string[]) => Promise<string | undefined> - Curried async factory for single-file resolution.
resolveConfigFileForSync(environment)
- Type:
(environment: string) => (file: string, localIgnoredEnvironments?: string[]) => string | undefined - Synchronous counterpart of
resolveConfigFileFor.
Behavior Details
localIgnoredEnvironments semantics
This parameter does not disable the whole hierarchy.
It disables only .local variants for the matching environment.
For base .env, environment test, and localIgnoredEnvironments = ["test"]:
- Excluded:
.env.test.local,.env.local - Still considered:
.env.test,.env
This is useful for keeping local machine overrides out of team/shared test execution.
Error behavior
resolveConfig*(runtime wrappers) throw ifNODE_ENVis absent or blank.resolveConfig*Forthrow if explicitenvironmentis blank/whitespace.- Missing files do not throw.
Common Patterns
Load env files with a precedence chain
import { config as dotenvConfig } from "dotenv";
import { resolveConfigChain } from "yassa";
process.env.NODE_ENV = process.env.NODE_ENV || "development";
for (const file of await resolveConfigChain(".env", ["test"])) {
dotenvConfig({ path: file, override: false });
}Load one authoritative JSON config
import { readFile } from "node:fs/promises";
import { resolveConfigFileFor } from "yassa";
const resolveProdConfig = resolveConfigFileFor("production");
const configPath = await resolveProdConfig("config/app.json");
if (!configPath) {
throw new Error("No production config file found");
}
const config = JSON.parse(await readFile(configPath, "utf8"));Build reusable resolvers
import { resolveConfigChainForSync } from "yassa";
const resolveTestChainSync = resolveConfigChainForSync("test");
const files = resolveTestChainSync(".env", ["test"]);Edge Cases
yassa supports:
- Dotted filenames with extension, e.g.
.env.json.env.staging.local.json,.env.local.json,.env.staging.json,.env.json
- Filenames ending with a dot, e.g.
index.index.qa.local.,index.local.,index.qa.,index.
- Absolute and relative input paths
- Symlinked files
Directories that happen to match candidate names are excluded.
FAQ
Why return undefined instead of throwing when nothing is found?
Missing config layers are normal in hierarchy-based loading. undefined/empty array keeps the API composable and explicit.
Should I use sync or async API?
- Use async in long-running apps and CLI tools where startup can be async.
- Use sync in bootstrapping paths that are already sync.
Should I use runtime wrappers or explicit factories?
- Use
resolveConfig*whenNODE_ENVis your source of truth. - Use
resolveConfig*Forin libraries/tests where environment must be explicit and decoupled from process globals.
Why the Name "Yassa"
The library name points to a hierarchy-and-authority model.
Historically, Yassa (also written as Yasa / Yasaq / Jasagh) is commonly described as Genghis Khan’s body of binding decrees and rulings for the Mongol state and army. A key scholarly nuance is that no single complete official text survives; what is called “the Yassa” is reconstructed from scattered references in medieval sources and later scholarship.
Why this maps to the library:
- Single authoritative decision point
resolveConfigFile*returns the top-priority effective file.
- Deterministic hierarchy
- Candidates are always resolved in a strict, predictable order.
- Command-chain behavior
resolveConfigChain*exposes the full precedence stack used to derive authority.
- Controlled local override policy
localIgnoredEnvironmentslets teams deliberately suppress local overrides in specific environments (for exampletest) for reproducibility.
The name is used as a conceptual metaphor for precedence and authoritative resolution, not as a legal or historical claim about a complete codified text.