No description
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| .githooks | ||
| src | ||
| .gitignore | ||
| .npmignore | ||
| ARCHITECTURE.md | ||
| example.ts | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsup.config.ts | ||
@transquinnftw/yaml-config
Type-safe YAML configuration loader with Zod validation, environment variable overrides, hot reload support, and variable interpolation.
Features
- Type Safety: Full TypeScript support with Zod schema validation
- Variable Interpolation: Use
${variable.path}syntax in YAML files - Path Expansion: Automatic
~expansion to home directory - Environment Overrides: Override any config value via environment variables
- Hot Reload: Optional file watching for automatic config reloading
- Deep Merging: Intelligent merging of nested configuration objects
- Fallback Paths: Search multiple locations for config files
- Validation Errors: Clear, formatted error messages for invalid configs
Installation
pnpm add @transquinnftw/yaml-config
Quick Start
import { createConfigLoader, z } from '@transquinnftw/yaml-config';
// Define your config schema
const schema = z.object({
port: z.number().default(8000),
database: z.object({
host: z.string(),
port: z.number(),
url: z.string(), // Can use interpolation in YAML
}),
});
// Create loader
const loader = createConfigLoader({
path: './config.yaml',
schema,
envPrefix: 'APP_',
});
// Load config
const config = loader.load();
// Type-safe access
console.log(config.port); // number
console.log(config.database.host); // string
YAML Configuration
# config.yaml
port: 8000
database:
host: localhost
port: 5432
# Variable interpolation
url: "postgresql://${database.host}:${database.port}/mydb"
Environment Variable Overrides
With envPrefix: 'APP_', you can override any config value:
APP_PORT=9000 node app.js
APP_DATABASE_HOST=prod.example.com node app.js
APP_DATABASE_PORT=5433 node app.js
The loader automatically:
- Converts env var names to config paths (underscores to dots)
- Coerces types based on schema (strings to numbers/booleans)
- Deep merges with file-based config
Advanced Usage
Hot Reload
const loader = createConfigLoader({
path: './config.yaml',
schema,
watch: true,
onReload: (newConfig) => {
console.log('Config reloaded:', newConfig);
// Update application state
},
});
const config = loader.load();
// Later: stop watching
loader.stopWatching();
Fallback Paths
const loader = createConfigLoader({
path: './config.yaml',
fallbackPaths: [
'/etc/myapp/config.yaml',
'~/.config/myapp/config.yaml',
],
schema,
});
The loader will search paths in order and use the first one found.
Path Expansion
Any key named path or ending with Path will have ~ automatically expanded:
# config.yaml
service:
logPath: "~/logs/app.log" # Expands to /home/user/logs/app.log
dataPath: "~/data" # Expands to /home/user/data
Common Schema Patterns
import { z, commonSchemas } from '@transquinnftw/yaml-config';
const schema = z.object({
// Use built-in patterns
port: commonSchemas.port, // 1-65535
apiUrl: commonSchemas.url, // Valid URL
email: commonSchemas.email, // Valid email
dbPath: commonSchemas.filePath, // Non-empty string
// Or define custom
logLevel: z.enum(['debug', 'info', 'warn', 'error']),
retries: z.number().int().min(0).max(10),
});
Error Handling
const loader = createConfigLoader({
path: './config.yaml',
schema,
onError: (error) => {
if (error instanceof ValidationError) {
console.error('Validation failed:');
console.error(error.formatErrors());
} else {
console.error('Failed to load config:', error.message);
}
// Use defaults or exit
process.exit(1);
},
});
Manual Reload
const loader = createConfigLoader({ path: './config.yaml', schema });
const config = loader.load();
// Later: reload without watching
const updatedConfig = loader.reload();
// Or: get cached config without reloading
const cached = loader.get(); // Returns null if not loaded yet
API Reference
createConfigLoader(options)
Creates a new configuration loader instance.
Options:
path: string- Path to YAML config file (required)schema: ZodSchema- Zod schema for validation (required)envPrefix?: string- Prefix for environment variable overridesfallbackPaths?: string[]- Additional paths to searchwatch?: boolean- Enable file watching (default: false)onReload?: (config) => void- Callback on reloadlog?: (message) => void- Custom log function (default: console.log)onError?: (error) => void- Custom error handler
Returns: ConfigLoader instance
ConfigLoader
load(): Config- Load and validate configurationreload(): Config- Reload configuration from diskget(): Config | null- Get cached config without reloadingstopWatching(): void- Stop file watchinggetConfigPath(): string | null- Get resolved config file path
commonSchemas
Pre-built Zod schemas for common config patterns:
url- Valid URLport- Port number (1-65535)filePath- Non-empty file pathdirPath- Non-empty directory pathemail- Valid email addressnonEmptyString- Non-empty stringpositiveInt- Positive integernonNegativeInt- Non-negative integerbooleanFromString- Coerce string to booleannumberFromString- Coerce string to number
ValidationError
Extended Error class for validation failures:
errors: ZodError- Original Zod errorformatErrors(): string- Format errors as human-readable string
Examples
Microservice Configuration
import { createConfigLoader, z, commonSchemas } from '@transquinnftw/yaml-config';
const schema = z.object({
service: z.object({
name: z.string(),
port: commonSchemas.port,
host: z.string().default('0.0.0.0'),
}),
database: z.object({
host: z.string(),
port: commonSchemas.port,
name: z.string(),
url: z.string(), // Constructed via interpolation
}),
redis: z.object({
host: z.string(),
port: commonSchemas.port,
url: z.string(),
}),
logging: z.object({
level: z.enum(['debug', 'info', 'warn', 'error']),
path: commonSchemas.filePath,
}),
});
const loader = createConfigLoader({
path: './config.yaml',
schema,
envPrefix: 'SERVICE_',
fallbackPaths: [
'/etc/myservice/config.yaml',
],
});
export const config = loader.load();
# config.yaml
service:
name: "my-service"
port: 8000
host: "0.0.0.0"
database:
host: "localhost"
port: 5432
name: "mydb"
url: "postgresql://${database.host}:${database.port}/${database.name}"
redis:
host: "localhost"
port: 6379
url: "redis://${redis.host}:${redis.port}"
logging:
level: "info"
path: "~/logs/service.log"
Desktop Application
import { createConfigLoader, z } from '@transquinnftw/yaml-config';
const schema = z.object({
app: z.object({
name: z.string(),
version: z.string(),
}),
window: z.object({
width: z.number().default(1200),
height: z.number().default(800),
minWidth: z.number().default(800),
minHeight: z.number().default(600),
}),
theme: z.enum(['light', 'dark', 'system']).default('system'),
dataPath: z.string(),
});
const loader = createConfigLoader({
path: './config.yaml',
schema,
watch: true,
onReload: (config) => {
// Update app theme when config changes
updateTheme(config.theme);
},
});
export const config = loader.load();
License
MIT
Author
Quinn quinn@transquinnftw.dev