@outfitter/config
v0.1.0
Published
XDG-compliant config loading with schema validation for Outfitter
Maintainers
Readme
@outfitter/config
XDG-compliant configuration loading with schema validation for Outfitter applications.
Features
- XDG Base Directory Specification - Proper paths for config, data, cache, and state
- Multi-format support - TOML, YAML, JSON, and JSON5
- Schema validation - Zod-powered type-safe configuration
- Multi-source merging - Combine defaults, files, env vars, and CLI flags
- Deep merge - Intelligent merging of nested configuration objects
Installation
bun add @outfitter/configQuick Start
import { loadConfig, resolveConfig, getConfigDir } from "@outfitter/config";
import { z } from "zod";
// Define your configuration schema
const AppConfigSchema = z.object({
apiKey: z.string(),
timeout: z.number().default(5000),
features: z.object({
darkMode: z.boolean().default(false),
}),
});
// Load from XDG paths (~/.config/myapp/config.toml)
const result = await loadConfig("myapp", AppConfigSchema);
if (result.isOk()) {
console.log("Config loaded:", result.value);
} else {
console.error("Failed:", result.error.message);
}API Reference
Configuration Loading
loadConfig(appName, schema, options?)
Load configuration from XDG-compliant paths with schema validation.
const result = await loadConfig("myapp", AppConfigSchema);
if (result.isOk()) {
const config = result.value;
// Type-safe access to your config
}Parameters:
appName- Application name for XDG directory lookupschema- Zod schema for validationoptions.searchPaths- Custom search paths (overrides XDG defaults)
Search Order:
- Custom
searchPathsif provided $XDG_CONFIG_HOME/{appName}/config.{ext}~/.config/{appName}/config.{ext}
File Format Preference: .toml > .yaml > .yml > .json > .json5
Returns: Result<T, NotFoundError | ValidationError | ParseError>
resolveConfig(schema, sources)
Merge configuration from multiple sources with precedence rules.
const result = resolveConfig(AppSchema, {
defaults: { port: 3000, host: "localhost" },
file: loadedConfig,
env: { port: parseInt(process.env.PORT!) },
flags: cliArgs,
});Parameters:
schema- Zod schema for validationsources- Configuration sources to merge
Returns: Result<T, ValidationError | ParseError>
parseConfigFile(content, filename)
Parse configuration file content based on extension.
const toml = `
[server]
port = 3000
host = "localhost"
`;
const result = parseConfigFile(toml, "config.toml");
if (result.isOk()) {
console.log(result.value.server.port); // 3000
}Parameters:
content- Raw file contentfilename- Filename (extension determines parser)
Returns: Result<Record<string, unknown>, ParseError>
XDG Path Helpers
getConfigDir(appName)
Get the XDG config directory for an application.
getConfigDir("myapp");
// With XDG_CONFIG_HOME="/custom": "/custom/myapp"
// Default: "~/.config/myapp"getDataDir(appName)
Get the XDG data directory for an application.
getDataDir("myapp");
// With XDG_DATA_HOME="/custom": "/custom/myapp"
// Default: "~/.local/share/myapp"getCacheDir(appName)
Get the XDG cache directory for an application.
getCacheDir("myapp");
// With XDG_CACHE_HOME="/custom": "/custom/myapp"
// Default: "~/.cache/myapp"getStateDir(appName)
Get the XDG state directory for an application.
getStateDir("myapp");
// With XDG_STATE_HOME="/custom": "/custom/myapp"
// Default: "~/.local/state/myapp"Utilities
deepMerge(target, source)
Deep merge two objects with configurable semantics.
const defaults = { server: { port: 3000, host: "localhost" } };
const overrides = { server: { port: 8080 } };
const merged = deepMerge(defaults, overrides);
// { server: { port: 8080, host: "localhost" } }Merge Behavior:
- Recursively merges nested plain objects
- Arrays are replaced (not concatenated)
nullexplicitly replaces the target valueundefinedis skipped (does not override)
Types
ConfigSources<T>
Configuration sources for multi-layer resolution.
interface ConfigSources<T> {
defaults?: Partial<T>; // Lowest precedence
file?: Partial<T>; // From config file
env?: Partial<T>; // Environment variables
flags?: Partial<T>; // CLI flags (highest)
}LoadConfigOptions
Options for loadConfig().
interface LoadConfigOptions {
searchPaths?: string[]; // Custom search paths
}ParseError
Error thrown when configuration file parsing fails.
class ParseError {
readonly _tag = "ParseError";
readonly message: string;
readonly filename: string;
readonly line?: number;
readonly column?: number;
}XDG Base Directory Specification
This package follows the XDG Base Directory Specification for locating configuration files.
| Variable | macOS/Linux Default | Purpose |
|----------|---------------------|---------|
| XDG_CONFIG_HOME | ~/.config | User-specific configuration |
| XDG_DATA_HOME | ~/.local/share | User-specific data files |
| XDG_CACHE_HOME | ~/.cache | Non-essential cached data |
| XDG_STATE_HOME | ~/.local/state | Persistent state (logs, history) |
Override Precedence
Configuration sources are merged with the following precedence (highest to lowest):
+---------------------------------------+
| flags (CLI arguments) HIGHEST |
+---------------------------------------+
| env (environment variables) |
+---------------------------------------+
| file (config file) |
+---------------------------------------+
| defaults LOWEST |
+---------------------------------------+Higher precedence sources override lower ones. Nested objects are deep-merged.
Supported File Formats
| Extension | Parser | Notes |
|-----------|--------|-------|
| .toml | smol-toml | Preferred for configuration |
| .yaml, .yml | yaml | YAML anchors/aliases supported |
| .json | JSON.parse | Strict parsing |
| .json5 | json5 | Comments and trailing commas allowed |
Examples
Loading with Custom Paths
const result = await loadConfig("myapp", AppConfigSchema, {
searchPaths: ["/etc/myapp", "/opt/myapp/config"],
});Multi-Source Configuration
import { loadConfig, resolveConfig } from "@outfitter/config";
// Load base config from file
const fileResult = await loadConfig("myapp", RawConfigSchema);
const fileConfig = fileResult.isOk() ? fileResult.value : {};
// Resolve with all sources
const result = resolveConfig(AppConfigSchema, {
defaults: {
server: { port: 3000, host: "localhost" },
logging: { level: "info" },
},
file: fileConfig,
env: {
server: { port: parseInt(process.env.PORT || "3000") },
logging: { level: process.env.LOG_LEVEL },
},
flags: {
logging: { level: cliArgs.verbose ? "debug" : undefined },
},
});Error Handling
const result = await loadConfig("myapp", AppConfigSchema);
if (result.isErr()) {
switch (result.error._tag) {
case "NotFoundError":
console.log("Config file not found, using defaults");
break;
case "ValidationError":
console.error("Invalid config:", result.error.message);
break;
case "ParseError":
console.error("Parse error in", result.error.filename);
break;
}
}License
MIT
