-
Notifications
You must be signed in to change notification settings - Fork 2
Utils Config
Multi-format configuration file loader with environment variable substitution and directory filtering.
The Config utility provides a powerful system for loading and managing application configuration from multiple file formats (JSON, YAML, TOML) with support for:
-
Multiple Formats:
.json,.js,.yaml/.yml,.toml— see Supported File Formats for the exact extension list the loader scans for. -
Environment Variables: Opt-in
${VAR}substitution, via theenvoption - Directory Filtering: Include/exclude patterns for selective loading
- Type Safety: Full TypeScript support with generic types
- Nested Access: Dot notation for deep object traversal
- Validation: Built-in checks for duplicate and malformed configs
deno add @tundralibs/utilsLoads configuration files from a directory and returns a Config object.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
options.path |
string |
Yes | Directory path containing config files |
options.env |
boolean | string |
No |
true loads .env from options.path; a string loads it from that path; omitted means no substitution |
options.include |
RegExp[] |
No | Patterns to include specific files |
options.exclude |
RegExp[] |
No | Patterns to exclude specific files |
env selects a source, it is not a bag of variables — pass true or a
path, never a Record. Substitution is off unless you ask for it.
When it is on, values come from the system environment, the .env file,
and Docker secrets, merged in that order (see
envArgs).
Returns: Promise<ConfigType> - Configuration object with typed access methods
Resolves a dot-separated path and casts the result to T. Two overloads,
one difference: what happens when the path does not resolve.
-
Without a default,
getthrows —Config set "…" does not existfor an unknown set,Config item "…" does not exist in set "…"for anything deeper. - With a default, it returns the default instead of throwing.
The default applies in exactly the cases has
reports as false: an unknown set, a missing segment, a path running
through a null intermediate or a primitive, and a key that exists but
holds undefined. So config.get(p, d) is config.has(p) ? config.get(p) : d.
A stored null is a value the config author wrote down, so it is
returned as-is — as are the falsy 0, '' and false. The default
replaces missing, not falsy.
Both overloads return T; passing a default never widens the result to
T | undefined.
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
// Required — throws if 'server.port' is not configured.
const port = config.get<number>('server.port');
// Optional — falls back to 3000 when it is not.
const fallbackPort = config.get<number>('server.port', 3000);Checks whether a path resolves to a defined value. Never throws — an
unknown set, a missing segment and a key holding undefined all return
false.
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
if (config.has('database.host')) {
// Connect to database
}Returns list of all root configuration keys.
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
const configs = config.list(); // ['database', 'server', 'logging']Iterates the direct entries of one set — the top level of a single
config file. The first argument is a set name, not a path: passing
'server.hosts' throws Config set "server.hosts" does not exist. The
callback receives the key and the value as two arguments.
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
// 'database' is a set — i.e. the file database.json / .yaml / .toml.
config.forEach('database', (key, value) => {
console.log(key, value);
});Returns the direct keys of one set. Like forEach, it takes a set name
— required, and top-level only — and throws if the set is unknown. Use
list() for the set names themselves, and get() to reach anything
deeper.
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
const dbKeys = config.keys('database'); // ['host', 'port', 'name']import { loadConfig } from '@tundralibs/utils';
// Load all config files from directory
const config = await loadConfig({
path: './config',
});
// Access configuration values
const dbHost = config.get<string>('database.host');
const dbPort = config.get<number>('database.port');Config file (config.json):
{
"database": {
"host": "${DB_HOST}",
"port": "${DB_PORT}",
"password": "${DB_PASSWORD}"
}
}TypeScript:
import { loadConfig } from '@tundralibs/utils';
// `env: true` reads .env from the config directory, plus system env
const config = await loadConfig({ path: './config', env: true });
// Or point at a specific .env file
const custom = await loadConfig({
path: './config',
env: './config/.env',
});
console.log(custom.get('database.host')); // 'localhost'import { loadConfig } from '@tundralibs/utils';
// Load only database configurations
const config = await loadConfig({
path: './config',
include: [/database/i, /db/i],
});
// Exclude test configurations
const withoutTests = await loadConfig({
path: './config',
exclude: [/test/i, /mock/i],
});The loader automatically detects and parses different formats:
config.json:
{
"app": {
"name": "MyApp",
"version": "1.0.0"
}
}database.yaml:
host: localhost
port: 5432
ssl: truelogging.toml:
[console]
level = "info"
colors = true
[file]
path = "/var/log/app.log"import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
// Access from any format
console.log(config.get('app.name')); // From JSON
console.log(config.get('database.host')); // From YAML
console.log(config.get('logging.console.level')); // From TOMLimport { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
// Deep object traversal with dot notation
const timeout = config.get<number>('server.http.options.timeout');
// Iterate the direct entries of a set
config.forEach('server', (key, value) => {
console.log(`${key} = ${value}`);
});
// Check nested paths
if (config.has('server.http.ssl.enabled')) {
// Setup SSL
}import { loadConfig } from '@tundralibs/utils';
interface DatabaseConfig {
host: string;
port: number;
ssl: boolean;
}
const config = await loadConfig({ path: './config' });
// Type-safe retrieval
const dbConfig: DatabaseConfig = {
host: config.get<string>('database.host'),
port: config.get<number>('database.port'),
ssl: config.get<boolean>('database.ssl'),
};config.json:
{
"servers": [
{ "name": "web1", "ip": "10.0.0.1" },
{ "name": "web2", "ip": "10.0.0.2" }
]
}The file is the set, so the array sits at config.servers. An array is
an ordinary value — forEach iterates a set's direct entries, not the
contents of one of them, so iterate the array itself:
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
type Server = { name: string; ip: string };
const servers = config.get<Array<Server>>('config.servers', []);
for (const server of servers) {
console.log(`${server.name}: ${server.ip}`);
}Directory-read failures surface as the typed errors loadConfig's
underlying readDir call (@tundralibs/compat/file) throws —
FileNotFound and FileAccessDenied — not generic Errors with a
particular substring. loadConfig itself throws plain Error for a
duplicate basename or a parse failure:
import { loadConfig } from '@tundralibs/utils';
import { FileAccessDenied, FileNotFound } from '@tundralibs/compat/file';
try {
const config = await loadConfig({ path: './config' });
} catch (err) {
if (err instanceof FileNotFound) {
console.error('Configuration directory not found:', err.path);
} else if (err instanceof FileAccessDenied) {
console.error('Insufficient permissions to read config:', err.path);
} else if (
err instanceof Error && err.message.includes('Duplicate config file')
) {
console.error('Multiple files with the same basename found');
} else if (err instanceof Error && err.message.includes('Error parsing')) {
console.error('Invalid configuration file format:', err.cause);
} else {
throw err;
}
}config/
├── database.json # Database settings
├── server.yaml # Server configuration
├── logging.toml # Logging setup
└── features.json # Feature flags
Deno.env / process.env are runtime-specific globals; use
envArgs to read the selector portably:
import { envArgs, loadConfig } from '@tundralibs/utils';
const env = envArgs().get('ENVIRONMENT') ?? 'development';
const config = await loadConfig({
path: `./config/${env}`,
include: [/^(?!test)/], // Exclude test configs
});import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
const port = config.get<number>('server.port', 3000);
const host = config.get<string>('server.host', '0.0.0.0');
const workers = config.get<number>('server.workers', 4);Each call returns number / string, not number | undefined — the
default is part of the result type, so there is nothing to narrow
afterwards. Reserve the no-default form for values the application
cannot start without, and let it throw.
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
const requiredKeys = ['database.host', 'api.key', 'server.port'];
for (const key of requiredKeys) {
if (!config.has(key)) {
throw new Error(`Missing required configuration: ${key}`);
}
}| Format | Extensions scanned | Parser | Features |
|---|---|---|---|
| JSON |
.json, .js
|
@std/jsonc |
// and /* */ comments tolerated |
| YAML |
.yaml, .yml
|
@std/yaml |
Anchors, aliases |
| TOML | .toml |
@std/toml |
Sections, nested tables |
.jsonfiles may contain JSONC-style comments — the parser used for.json/.jsis@std/jsoncregardless of extension — but the directory scan itself only picks up the five extensions above. A file literally named*.jsoncis invisible toloadConfig(silently not loaded, no error): put comments in a.json-extension file instead. Likewise.jsis scanned and parsed as JSONC, not executed.
- Async Loading: All file operations are asynchronous
- Lazy Evaluation: Environment variable substitution happens at load time
- Caching: Loaded configs are returned as an object (not cached between calls)
- File Filtering: Uses optimized directory reading from compat layer
Problem: Variable shows as ${VAR} instead of value
import { loadConfig } from '@tundralibs/utils';
// Wrong - substitution is off unless you ask for it
const config = await loadConfig({ path: './config' });Solution:
import { loadConfig } from '@tundralibs/utils';
// Correct - env enabled
const config = await loadConfig({ path: './config', env: true });Problem: Getting undefined for existing config
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
// Wrong - incorrect key path
const value = config.get('database-host'); // throwsSolution:
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
// Correct - use dot notation
const value = config.get('database.host');Problem: Runtime type mismatch
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
// Wrong - assuming type
const port = config.get<number>('server.port') + 100; // Could be string!Solution:
import { loadConfig } from '@tundralibs/utils';
const config = await loadConfig({ path: './config' });
// Correct - explicit type and validation
const port = config.get<number>('server.port');
if (typeof port !== 'number') {
throw new Error('Invalid port configuration');
}- envArgs - Load environment variables and .env files
- variableReplacer - Template variable substitution