@setu-ts/config-plugin
v0.10.0
Published
Configuration with env loading, variable expansion, and schema validation
Readme
@setu-ts/config-plugin
Configuration management plugin for Setu-TS with strict .env parsing, variable expansion, and
structurally compatible schema validation.
Installation
npm install @setu-ts/config-plugin
# Deno: deno add npm:@setu-ts/config-pluginQuick Start
import { createApplication } from '@setu-ts/kernel';
import { ConfigPlugin } from '@setu-ts/config-plugin';
import { RuntimePlugin } from '@setu-ts/runtime';
import { CAPABILITIES } from '@setu-ts/common';
import type { IConfig } from '@setu-ts/common';
const app = createApplication();
app.register(RuntimePlugin());
app.register(
ConfigPlugin({
envFilePath: ['.env.local', '.env'],
}),
);
await app.start();
// Access configuration
const config = app.services.get<IConfig>(CAPABILITIES.CONFIG);
const port = config.get('PORT', { default: '3000' });Configuration a plugin needs at construction
Factory arguments are evaluated before app.start() registers ConfigPlugin and loads .env. Load
a snapshot first when another plugin needs those values at construction, then register that same
snapshot with instance; it is used verbatim without loading again:
import { createApplication } from '@setu-ts/kernel';
import { createRuntimeServices, RuntimePlugin } from '@setu-ts/runtime';
import { ConfigPlugin, loadConfig } from '@setu-ts/config-plugin';
import { AuthPlugin } from '@setu-ts/auth-plugin';
const runtime = createRuntimeServices();
const config = await loadConfig(runtime, { envFilePath: '.env', envFileOptional: true });
const app = createApplication({
plugins: [
RuntimePlugin(),
ConfigPlugin({ instance: config }),
AuthPlugin({ jwt: { secret: config.getOrThrow<string>('JWT_SECRET') } }),
],
});
await app.start();Options
| Option | Type | Default | Description |
| ------------------ | ----------------------------------- | ----------- | ----------------------------------------------------------------------------- |
| envFilePath | string \| readonly string[] | undefined | Path(s) to .env files. No file loading when absent. |
| envFileOptional | boolean | false | Skip an ABSENT configured path instead of throwing (unreadable still throws). |
| validationSchema | StructuralSchema<T> | undefined | Zod-compatible whole-snapshot schema for startup validation. |
| sections | readonly ConfigSection<unknown>[] | undefined | Typed, declared-key sections to validate and cache at startup. |
| expandVariables | boolean | true | Expand ${NAME} references in values. |
| instance | IConfig | undefined | An already-loaded snapshot to register verbatim; nothing is read. |
| diagnostics | ConfigDiagnosticsOptions | undefined | Opt-in value-free provenance; absent registers a disabled source. |
Configuration Precedence
Values are merged in the following order (highest precedence first):
- Environment variables (
runtime.env) - Earlier file paths (
.env.localoverrides.env) - Later file paths
undefined entries in runtime.env are filtered out.
Dotenv Parsing
Configured files are parsed strictly at startup. Blank lines and comments are ignored; export
prefixes, quoted values, common double-quoted escapes, empty values, and whitespace-delimited inline
comments are supported. Malformed entries, invalid keys, and unterminated quotes fail startup with a
line number. Error messages never include the rejected value.
Variable Expansion
When expandVariables is true (default), ${NAME} references in values are resolved against the
final merged configuration:
# .env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_URL=postgresql://${DATABASE_HOST}:${DATABASE_PORT}/mydb- Supports recursive references.
- Resolves references once, after every file and
runtime.envhave been merged. - Detects cycles spanning any combination of sources and throws with a clear error.
- Fails for missing variable references.
- Never uses
evalorFunction.
Validation with Zod
Pass a Zod schema to validationSchema for type coercion and validation at startup:
import { z } from 'zod';
const AppConfigSchema = z.object({
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
DEBUG: z.coerce.boolean().default(false),
});
app.register(
ConfigPlugin({
envFilePath: ['.env.local', '.env'],
validationSchema: AppConfigSchema,
}),
);- Coercions and defaults are preserved in the stored configuration.
- The schema's parsed output must be a non-null, non-array object.
- Validation errors do not disclose secret values.
Typed Configuration Sections
Use a section when related settings should be returned with the type that its schema validated.
Sections are validated and cached during startup; getConfigSection never parses or asserts a value
at read time.
import { CAPABILITIES, type IConfig } from '@setu-ts/common';
import { ConfigPlugin, defineConfigSection, getConfigSection } from '@setu-ts/config-plugin';
import { z } from 'zod';
const database = defineConfigSection({
prefix: 'DATABASE_',
keys: ['URL', 'POOL_SIZE'],
schema: z.object({
URL: z.string().url(),
POOL_SIZE: z.coerce.number().int().positive(),
}),
});
app.register(ConfigPlugin({ sections: [database] }));
const config = app.services.get<IConfig>(CAPABILITIES.CONFIG);
const settings = getConfigSection(config, database);
// settings.POOL_SIZE is a number, validated before the application started.keys contains prefix-stripped names: prefix: 'DATABASE_' plus keys: ['URL'] reads the flat
DATABASE_URL key and supplies { URL } to the schema. The list is explicit because IConfig
supports named reads but deliberately has no key-enumeration method; this also lets sections
validate an arbitrary instance snapshot. A missing key is omitted, so the schema decides whether
it is optional. A section schema is not interchangeable with validationSchema: the latter receives
the whole flat snapshot, while the former receives only its declared, prefix-stripped keys.
Configuration Provenance
diagnostics is the explicit opt-in to value-free provenance for the approved keys only:
app.register(
ConfigPlugin({
envFilePath: ['.env.local', '.env'],
diagnostics: {
enabled: true, // the literal true: an acknowledgement, not a toggle
keys: { PORT: 'port', DATABASE_URL: 'database' },
files: { '.env.local': 'dotenv-local', '.env': 'dotenv' },
},
}),
);During the ONE load already performed — never a second pass — the loader records where each approved
key's final value was observed to come from: the source category and approved source aliases,
evidenced precedence displacement (overriddenSourceAliases, lowest first), ${NAME} expansion
references (expanded + approved referenceAliases), and a schema effect derived only from
input/output property presence. introduced reports that a key appeared only after schema parsing;
it never names a mechanism, because a default and a transform are indistinguishable by presence. No
value, hash, length, raw key name, or file path is ever retained, and unapproved keys are never
observed at all.
loadConfig(runtime, { diagnostics }) builds the record with the snapshot;
ConfigPlugin({ instance, diagnostics }) adopts that exact instance's record — keeping real origins
— or reports an opaque injected instance as unknown with no presence flag and no read of it. An
adopted record is served as the loader built it, under the loadConfig call's approvals; the
plugin's own keys and files then only enable the source and govern the opaque case, so pass the
same policy to both calls. Provenance adds no read to any configuration object. The plugin always
registers the source under CAPABILITIES.CONFIG_DIAGNOSTICS: absent diagnostics answers
disabled. The Diagnostics Connector serves it at GET /v1/config — see
docs/diagnostics-protocol.md.
Hot Reload
Deferred. The current runtime contract has no file-watching abstraction. Configuration is an immutable application-startup snapshot.
Edge Runtimes
On edge platforms (Cloudflare Workers, etc.) where runtime.fs is undefined, envFilePath must
not be set. Attempting to do so throws a clear startup error:
ConfigPlugin: envFilePath requires a runtime with filesystem support.
API Reference
ConfigPlugin(options?)
Creates the configuration plugin. Consumes CAPABILITIES.RUNTIME, provides CAPABILITIES.CONFIG.
ConfigPluginOptions
import type { IConfig } from '@setu-ts/common';
import type { ConfigSection, StructuralSchema } from '@setu-ts/config-plugin';
interface ConfigPluginOptions {
readonly envFilePath?: string | readonly string[];
readonly envFileOptional?: boolean;
readonly validationSchema?: StructuralSchema<unknown>;
readonly sections?: readonly ConfigSection<unknown>[];
readonly expandVariables?: boolean;
readonly instance?: IConfig;
}StructuralSchema<T>
Minimal schema interface compatible with Zod's parse(unknown) API.
ConfigSection<T>
The typed section declaration returned by defineConfigSection. It carries prefix,
prefix-stripped keys, and a StructuralSchema<T>. Pass it in ConfigPluginOptions.sections, then
retrieve its validated output with getConfigSection(config, section).
IConfig (from @setu-ts/common)
interface IConfig {
get<T>(key: string): T | undefined;
get<T>(key: string, options: { readonly default: T }): T;
getOrThrow<T>(key: string): T;
has(key: string): boolean;
}Without Schema
When validationSchema is not provided, all values remain as strings from the environment and
.env files.
Dependencies
@setu-ts/common(workspace)- Consumer-supplied structural schema such as Zod (optional; not a package dependency)
Exports
| Export | Kind |
| -------------------------- | --------- |
| ConfigPlugin | function |
| defineConfigSection | function |
| getConfigSection | function |
| loadConfig | function |
| ConfigDiagnosticsOptions | interface |
| ConfigPluginOptions | interface |
| ConfigSection | interface |
| StructuralSchema | interface |
Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it
drifts.
Full API
Every export and option is documented in PUBLIC_API.md.
