Configorama adds variable support to configuration files so environment-specific values can be composed without turning every config into custom JavaScript.
It is the kind of utility that comes from building repeatable systems: keep the config readable, make the dynamic parts explicit, and let the same structure work across local, staging, and production environments.
Resolve dynamic config values from environment variables, CLI flags, files, git data, expressions, and custom sources. Works with YAML, JSON, TOML, INI, HCL, Markdown, JavaScript, and TypeScript.
npm install configorama
npx configorama config.yml --stage prodConfigorama is a framework-agnostic variable engine for configuration files. Use it to resolve a config at runtime, inspect missing values before resolution, audit risky references, draw dependency graphs, run an interactive setup flow, or emit requirements JSON for agents and automation.
Deployment configs usually pull values from several places: env vars, CLI flags, local files, generated JavaScript, git metadata, stage-specific maps, and secret stores. Most config parsers stop at parsing, while framework-specific variable systems tend to stay tied to that framework.
Configorama loads a config file, finds variable references, resolves them in dependency order, applies filters/functions, and returns a plain JavaScript object. It can also report what the config needs before resolution.
Common use cases:
| Need | Support | Example |
|---|---|---|
| Resolve values from many sources | Built-in env, option/opt, self, file, text, git, cron, eval, and if sources | ${env:API_KEY}, ${option:stage}, ${file(./secrets.yml)} |
| Keep config portable | Runs outside any framework | Use the same resolver in a CLI, build script, deploy job, or app bootstrap |
| Prompt for missing inputs | Interactive setup wizard with type-aware prompts and masked secrets | configorama setup config.yml |
| Tell agents what to provide | Requirements JSON with schemaVersion, requirements[], and ask[] | configorama inspect config.yml --view requirements |
| Inspect before resolving | Full inspection model plus focused requirements, audit, and graph views | configorama inspect config.yml |
| Document variables near the config | help() plus comment annotations for descriptions, obtain hints, examples, groups, sensitivity, and deprecation warnings | # @from Stripe dashboard > Developers > API keys |
| Enforce runtime constraints | Type filters and oneOf(...) validation | ${option:threads \| Number \| oneOf(1, 2, 4)} |
# config.yml
service: billing-api
# Deployment stage
stage: ${option:stage | oneOf("dev", "staging", "prod")}
secrets:
# Stripe live secret key
# @from Stripe dashboard > Developers > API keys
# @example sk_live_...
# @sensitive true
# @group Payments
stripeSecret: ${env:STRIPE_SECRET_KEY}
database:
host: ${env:DB_HOST, "localhost"}
port: ${env:DB_PORT, 5432 | Number}
name: ${self:service}-${self:stage}# Resolve the config
STRIPE_SECRET_KEY=sk_live_xxx npx configorama config.yml --stage prod
# Walk through missing variables interactively
npx configorama setup config.yml
# Print requirements for agents or automation
npx configorama inspect config.yml --view requirements
# Inspect requirements, dependency graph, and audit report together
npx configorama inspect config.yml| Area | Added |
|---|---|
| Normalized requirements model | ConfigRequirements groups occurrences by variable, normalizes ${opt:...} and ${option:...} as variableType: "option", and tracks paths, defaults, types, allowed values, sensitivity, and conflicts. |
| Unified inspection CLI | configorama inspect config.yml emits requirements, graph, and audit output without resolving missing values. Use --view requirements, --view audit, or --view graph for one slice. |
| Requirements JSON | configorama inspect config.yml --view requirements, configorama requirements config.yml, and configorama config.yml --requirements emit schemaVersion: 1, summary, requirements[], and environment-aware ask[]. |
| Safe inspection | inspect, audit, and graph run in safe mode by default. Use --unsafe to opt out or --safe-root <dir> to restrict file/text references. |
| Agent-friendly CLI contract | configorama capabilities prints commands, aliases, formats, flags, error codes, and exit codes as JSON. |
| Path extraction polish | Jq-style paths, --raw, and --copy make scalar extraction usable in scripts: configorama -r --copy config.yml .database.host. |
| Conflict handling | Conflicting type/default/allowed-value/annotation metadata is deterministic in the wizard. Requirements serialization fails on conflicts so agents get a clean contract. |
| Setup wizard migration | The wizard now consumes prompt descriptors derived from the requirements model, supports enum selects from oneOf, displays annotation details, and redacts sensitive values in setup summaries and setup stdout. |
oneOf(...) validation | Runtime filter for inline literal sets and resolved list variables, including type-filter-first behavior such as ${option:threads \| Number \| oneOf(1, 2, 4)}. |
| More type filters | Array and Object filters now validate/coerce arrays, comma-separated lists, JSON/JSON5 arrays, and JSON/JSON5 objects. |
| Option alias | ${option:name} is supported alongside the existing ${opt:name} shorthand. |
| Comment metadata | Leading/inline comments become help fallback; structured tags add @description, @from, @example, @default, @sensitive, @group, and @deprecated. |
| Structured CLI errors | Inspection commands default to JSON errors on stderr; --error-format human is available for terminal use. |
As a library dependency:
npm install configoramaAs a global CLI tool:
npm install -g configoramaAsync API (recommended for most use cases):
const path = require('path')
const configorama = require('configorama')
const cliFlags = require('minimist')(process.argv.slice(2))
// Path to yaml/json/toml config
const myConfigFilePath = path.join(__dirname, 'config.yml')
// Execute config resolution asynchronously
const config = await configorama(myConfigFilePath, { options: cliFlags })
console.log(config) // resolved configSync API (for synchronous execution contexts):
const path = require('path')
const configorama = require('configorama')
const cliFlags = require('minimist')(process.argv.slice(2))
// Path to yaml/json/toml config
const myConfigFilePath = path.join(__dirname, 'config.yml')
// Execute config resolution synchronously
const config = configorama.sync(myConfigFilePath, { options: cliFlags })
console.log(config) // resolved configExample configuration file (config.yml):
# Environment variable
apiKey: ${env:API_KEY}
# CLI option (e.g., --stage prod)
environment: ${opt:stage, 'dev'}
# Self-reference to other values
service: my-app
fullName: ${service}-api
# File reference
secrets: ${file(./secrets.yml)}
# Git information
branch: ${git:branch}
commit: ${git:sha1}
# Conditional logic
memorySize: ${if(${environment} === 'prod' ? 1024 : 512)}
# Nested references
database:
host: ${env:DB_HOST, 'localhost'}
port: ${env:DB_PORT, 5432}
name: ${service}-${environment}The project includes example files demonstrating various features:
# Clone the repository
git clone https://github.com/DavidWells/configorama
cd configorama
# Install dependencies
npm install
# Run async API example
node examples/using-async-api.js --stage prod
# Run sync API example
node examples/using-sync-api.js --stage dev
# Run zero-config example
node examples/zero-config.js
# Run TypeScript example
node examples/typescript/using-typescript.jsConfigorama creates a dependency graph of your config file and all its dependencies, then resolves values based on their variable sources. The resolution process follows this flow:
flowchart TD
A[Load config file] --> B[Parse yml/json/toml/hcl to object]
B --> C[Preprocess: raw config file]
C --> D{Return metadata only?}
D -->|Yes| E[Collect variable metadata]
E --> F[Return found variable metadata + original config]
D -->|No| G[Traverse & resolve variables recursively]
G --> H[Post-process: runs filters and functions]
H --> I[Return resolved config]Resolution process:
Analyze config structure and variables without actually resolving them:
const result = await configorama.analyze('config.yml')
// Returns metadata about variables without resolving them
console.log(result.originalConfig) // Raw config object
console.log(result.variables) // All variables found
console.log(result.uniqueVariables) // Variables grouped by name
console.log(result.fileDependencies) // File references found
Use cases:
Resolve config and get detailed metadata about the resolution process:
const result = await configorama('config.yml', {
returnMetadata: true,
options: { stage: 'prod' }
})
// Returns both resolved config and metadata
console.log(result.config) // Fully resolved config
console.log(result.originalConfig) // Raw config object
console.log(result.metadata.variables) // Variable info with resolution details
console.log(result.metadata.fileDependencies) // All file dependencies
console.log(result.metadata.summary) // { totalVariables, requiredVariables, variablesWithDefaults }
console.log(result.resolutionHistory) // Step-by-step resolution for each pathMetadata structure:
{
config: { /* resolved config */ },
originalConfig: { /* raw config */ },
metadata: {
variables: [
{
variable: '${env:API_KEY}',
variableType: 'env',
variableName: 'API_KEY',
variablePath: 'apiKey',
defaultValue: null,
hasDefault: false,
resolved: true,
resolvedValue: 'secret-key-123'
},
// ... more variables
],
summary: {
totalVariables: 15,
requiredVariables: 8,
variablesWithDefaults: 7
},
fileDependencies: ['./secrets.yml', './config.ts']
},
resolutionHistory: {
'apiKey': [
{ step: 1, value: '${env:API_KEY}', type: 'env' },
{ step: 2, value: 'secret-key-123', resolved: true }
]
}
}┌────────���─────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ Input │───▶│ Configorama core │───▶│ Output │
│ │ │ │ │ │
│ • Config file │ │ parser registry │ │ Resolved config │
│ • JS/TS object │ │ (yaml, json, toml, │ │ (+ metadata if │
│ • Inline opts │ │ ini, hcl, md, …) │ │ requested) │
└──────────────────┘ │ │ └──────────────────┘
│ preProcess() │
│ ↓ │
│ populateObject() │◀───┐ iterates until
│ ↓ │ │ no variables
│ resolve leaf vars │────┘ remain
│ ↓ │
│ apply filters/funcs│
│ ↓ │
│ return │
└──────────┬──────────┘
│
▼
┌──────────────────────────────────────┐
│ Variable Sources │
│ ┌────────┐ ┌────────┐ ┌────────────┐ │
│ │ env │ │ opt │ │ file/text │ │
│ │ self │ │ param │ │ git/cron │ │
│ │ eval │ │ if │ │ + plugins │ │
│ └────────┘ └────────┘ └────────────┘ │
│ │
│ Bundled plugins: │
│ • plugins/cloudformation │
│ │
│ Custom: variableSources: [{…}] │
└──────────────────────────────────────┘Resolution is a fixed-point loop: each pass resolves what it can, then populateObject() runs again until no ${…} references remain. Built-in resolvers run first; custom resolvers from variableSources are tried in order.
A typical 21KB serverless-style config resolves in ~3ms on warm Node 22.
0.9.17 baseline: PERF.mdscripts/bench.jsnode scripts/bench.js # local
node scripts/bench.js /path/to/another/configorama # A/BIf your config is slow, please open an issue with the config (or a redacted reproduction). We're happy to profile and tighten the hot path.
Configorama supports multiple variable sources. All variable syntax follows the pattern ${type:value} or ${type(value)}.
| Variable | Syntax | Description | Example |
|---|---|---|---|
| env | ${env:VAR} | Environment variables | ${env:NODE_ENV} |
| option | ${option:flag} or ${opt:flag} | CLI option flags (opt is shorthand) | ${option:stage} |
| param | ${param:key} | Parameter values | ${param:domain} |
| self | ${key} or ${self:key} | Self references | ${database.host} |
| file | ${file(path)} | File references | ${file(./secrets.yml)} |
| text | ${text(path)} | Raw text file | ${text(./README.md)} |
| git | ${git:value} | Git data | ${git:branch} |
| cron | ${cron(expr)} | Cron expressions | ${cron('every 5 minutes')} |
| eval | ${eval(expr)} | Math/logic expressions | ${eval(10 + 5)} |
| if | ${if(expr)} | Conditional expressions | ${if(x > 5 ? 'yes' : 'no')} |
Access values from process.env environment variables.
# Basic env var
apiKey: ${env:SECRET_KEY}
# With fallback default if env var not found
apiKeyWithFallback: ${env:SECRET_KEY, 'defaultApiKey'}
# Common patterns
nodeEnv: ${env:NODE_ENV, 'development'}
port: ${env:PORT, 3000}
debug: ${env:DEBUG, false}
How it works:
process.env at resolution timeallowUnresolvedVariables is set)CLI usage:
# Set env var then run
SECRET_KEY=abc123 node app.js
# Or export first
export SECRET_KEY=abc123
node app.jsAccess values from command line arguments passed via the options parameter.
# CLI option. Example `cmd --stage dev` makes `bar: dev`
bar: ${opt:stage}
# Composed example makes `foo: dev-hello`
foo: ${opt:stage}-hello
# With default value. If no --stage flag, uses 'dev'
environment: ${opt:stage, 'dev'}
# Boolean flags
verbose: ${opt:verbose, false}
# Nested paths
region: ${opt:aws.region, 'us-east-1'}How it works:
options object passed to configoramaminimist or similar parserExample:
const minimist = require('minimist')
const configorama = require('configorama')
const argv = minimist(process.argv.slice(2))
// argv = { stage: 'prod', verbose: true, aws: { region: 'eu-west-1' } }
const config = await configorama('config.yml', { options: argv })# Command line
node app.js --stage prod --verbose --aws.region eu-west-1Access parameter values via ${param:key}. Parameters follow a resolution hierarchy:
--param="key=value") - highest prioritystages.<stage>.params)stages.default.params)# Direct parameter reference
appDomain: ${param:domain}
# Parameter with fallback
apiKey: ${param:apiKey, 'default-api-key'}
# Stage-specific parameters defined in config
stages:
dev:
params:
domain: dev.myapp.com
dbHost: localhost
prod:
params:
domain: myapp.com
dbHost: prod-db.myapp.com
default:
params:
domain: default.myapp.com
dbPort: 3306CLI Usage:
# Single param
node app.js --param="domain=example.com"
# Multiple params
node app.js --param="domain=example.com" --param="apiKey=secret123"
# With stage selection
node app.js --stage prod --param="domain=cli-override.com"Code Usage:
const config = await configorama('config.yml', {
options: {
stage: 'prod',
param: ['domain=cli-override.com', 'apiKey=secret']
}
})Resolution order example:
stages:
prod:
params:
domain: prod.myapp.com # 2. Stage-specific
default:
params:
domain: default.myapp.com # 3. Default fallback
appUrl: ${param:domain}# CLI override (highest priority)
node app.js --stage prod --param="domain=cli.myapp.com"
# Result: appUrl = 'cli.myapp.com'
# Stage param (no CLI override)
node app.js --stage prod
# Result: appUrl = 'prod.myapp.com'
# Default param (no CLI override, no stage match)
node app.js --stage staging
# Result: appUrl = 'default.myapp.com'Reference values from other key paths in the same configuration file using dot notation.
foo: bar
zaz:
matazaz: 1
wow:
cool: 2
# Shorthand dot.prop reference
two: ${foo} # Resolves to 'bar'
# Explicit self file reference
one: ${self:foo} # Resolves to 'bar'
# Dot prop reference traverses objects
three: ${zaz.wow.cool} # Resolves to 2
# Complex nested references
database:
host: localhost
port: 5432
name: mydb
connectionString: postgres://${database.host}:${database.port}/${database.name}
# Resolves to: postgres://localhost:5432/mydb
# Array access
items:
- first
- second
- third
selectedItem: ${items[1]} # Resolves to 'second'How it works:
Import values from external yml, json, toml, hcl, or other supported files by relative path.
# Import full yml/json/toml/hcl file via relative path
fileRef: ${file(./subFile.yml)}
# Import sub values from files (topLevel key from other-config.yml)
fileValue: ${file(./other-config.yml):topLevel}
# Import nested sub values (nested.value from other-config.json)
fileValueSubKey: ${file(./other-config.json):nested.value}
# Fallback to default value if file not found
fallbackValueExample: ${file(./not-found.yml), 'fall back value'}
# Relative paths from config file location
secrets: ${file(../shared/secrets.yml)}
# Import from subdirectory
dbConfig: ${file(./config/database.yml):production}Supported file types (extensions are case-insensitive):
| Type | Extensions |
|---|---|
| TypeScript | .ts, .tsx, .mts, .cts |
| JavaScript | .js, .cjs |
| ESM | .mjs, .esm |
| YAML | .yml, .yaml |
| TOML | .toml, .tml |
| INI | .ini |
| JSON | .json, .json5, .jsonc |
| HCL (Terraform) | .tf, .hcl, .tf.json |
| Markdown | .md, .mdx, .markdown, .mdown, .mkdn, .mkd |
Path resolution:
~ home directory expansion NOT supported (use absolute paths)Example file structure:
project/
├── config.yml # Main config
├── secrets.yml # Secrets file
└── environments/
├── dev.yml
└── prod.yml# config.yml
secrets: ${file(./secrets.yml)}
environment: ${file(./environments/${opt:stage}.yml)}Execute JavaScript files and use their exported function's return value. Functions can be synchronous or asynchronous and receive arguments from your config.
# Async function execution
asyncJSValue: ${file(./async-value.js)}
# Sync function execution
syncJSValue: ${file(./sync-value.js)}
# With arguments (resolved before being passed)
secrets: ${file(./fetch-secrets.js, ${self:environment}, ${self:region})}JavaScript file example (async-value.js):
async function fetchSecretsFromRemoteStore() {
// Simulate async operation (AWS Secrets Manager, HashiCorp Vault, etc.)
await new Promise(resolve => setTimeout(resolve, 1000))
return {
apiKey: 'secret-key-123',
dbPassword: 'db-password-456'
}
}
module.exports = fetchSecretsFromRemoteStoreSync function example (sync-value.js):
function getEnvironmentConfig() {
return {
timeout: 5000,
retries: 3,
logLevel: process.env.NODE_ENV === 'production' ? 'error' : 'debug'
}
}
module.exports = getEnvironmentConfigYou can pass resolved values from your config as arguments to JavaScript/TypeScript functions:
foo: bar
baz:
qux: quux
# Pass resolved values as arguments
secrets: ${file(./fetch-secrets.js, ${self:foo}, ${self:baz})}Arguments are passed in order, with the config context always last:
/**
* @param {string} foo - First arg from YAML ('bar')
* @param {object} baz - Second arg from YAML ({ qux: 'quux' })
* @param {import('configorama').ConfigContext} ctx - Config context (always last)
*/
async function fetchSecrets(foo, baz, ctx) {
console.log(foo) // 'bar'
console.log(baz) // { qux: 'quux' }
// Access config context
console.log(ctx.originalConfig) // Original unresolved config
console.log(ctx.currentConfig) // Current partially-resolved config
console.log(ctx.options) // Options passed to configorama
return { secret: 'value' }
}
module.exports = fetchSecretsThe ctx parameter (always the last argument) provides access to:
| Property | Description |
|---|---|
originalConfig | The original unresolved configuration object |
currentConfig | The current (partially resolved) configuration |
options | Options passed to configorama (populates ${option:xyz} / ${opt:xyz} variables) |
TypeScript users can import the type:
import type { ConfigContext } from 'configorama'
async function fetchSecrets(
foo: string,
baz: { qux: string },
ctx: ConfigContext
): Promise<string> {
// Full type support for ctx properties
return 'secret-value'
}
export = fetchSecretsIf you don't need arguments, the function still receives ctx as its only parameter:
// No args - ctx is the only parameter
async function getSecrets(ctx) {
return ctx.options.stage === 'prod'
? 'prod-secret'
: 'dev-secret'
}
module.exports = getSecretsExecute TypeScript files using tsx (recommended) or ts-node.
Installation:
# Recommended: Modern, fast TypeScript execution
npm install tsx --save-dev
# Alternative: Traditional ts-node approach
npm install ts-node typescript --save-devUsage in config:
# TypeScript configuration object
config: ${file(./config.ts)}
# TypeScript async function
secrets: ${file(./async-secrets.ts)}
# Specific property from TypeScript export
database: ${file(./config.ts):database}
# With arguments
apiConfig: ${file(./config.ts, ${opt:stage})}TypeScript Object Export (typescript-config.ts):
interface DatabaseConfig {
host: string
port: number
database: string
ssl: boolean
}
interface ApiConfig {
baseUrl: string
timeout: number
retries: number
}
interface ConfigObject {
environment: string
database: DatabaseConfig
api: ApiConfig
features: {
enableNewFeature: boolean
debugMode: boolean
}
}
function createConfig(): ConfigObject {
return {
environment: process.env.STAGE || 'development',
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'myapp',
ssl: process.env.NODE_ENV === 'production'
},
api: {
baseUrl: process.env.API_BASE_URL || 'http://localhost:3000',
timeout: 5000,
retries: 3
},
features: {
enableNewFeature: process.env.STAGE === 'production',
debugMode: process.env.DEBUG === 'true'
}
}
}
export = createConfigTypeScript Async Function (typescript-async.ts):
interface SecretStore {
apiKey: string
dbPassword: string
jwtSecret: string
}
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function fetchSecretsFromVault(): Promise<SecretStore> {
console.log('Fetching secrets from vault...')
// Simulate async operations (AWS Secrets Manager, HashiCorp Vault, etc.)
await delay(100)
return {
apiKey: process.env.API_KEY || 'dev-api-key',
dbPassword: process.env.DB_PASSWORD || 'dev-password',
jwtSecret: process.env.JWT_SECRET || 'dev-jwt-secret'
}
}
export = fetchSecretsFromVaultComplete Example Configuration:
# config-with-typescript.yml
service: my-awesome-app
# Load configuration from TypeScript file
provider: ${file(./typescript-config.ts)}
# Load secrets asynchronously from TypeScript file
secrets: ${file(./typescript-async.ts)}
# Mix TypeScript with other configuration
custom:
stage: ${opt:stage, "dev"}
region: ${opt:region, "us-east-1"}
# Use TypeScript files for specific sections
databaseConfig: ${file(./typescript-config.ts):database}
# Environment-specific overrides
stageVariables:
dev:
logLevel: debug
prod:
logLevel: info
# Regular configuration values
resources:
description: "Configuration loaded with TypeScript support"
functions:
hello:
handler: handler.hello
environment:
LOG_LEVEL: ${self:custom.stageVariables.${self:custom.stage}.logLevel}
DB_HOST: ${self:provider.database.host}
API_KEY: ${self:secrets.apiKey}Features:
Configorama supports Terraform HCL (HashiCorp Configuration Language) files, allowing you to parse .tf, .tf.json, and .hcl files.
Installation:
HCL parsing requires the optional @cdktf/hcl2json package:
npm install @cdktf/hcl2jsonSupported file types:
.tf - Terraform configuration files.hcl - Generic HCL files.tf.json - Terraform JSON configuration filesExample:
const configorama = require('configorama')
// Parse a Terraform configuration file
const terraformConfig = await configorama('./main.tf')
// Access Terraform variables, resources, locals, etc.
console.log(terraformConfig.variable) // Variables defined in the file
console.log(terraformConfig.resource) // Resources
console.log(terraformConfig.locals) // Local values
console.log(terraformConfig.output) // OutputsImporting Terraform files:
# Import Terraform variables from a .tf file
terraformVars: ${file(./terraform/variables.tf)}
# Import specific variable from Terraform file
region: ${file(./terraform/variables.tf):variable.region[0].default}Variable syntax:
When loading .tf or .hcl files directly, configorama automatically uses $[...] syntax instead of ${...} to avoid conflicts with Terraform's native ${var.name} interpolation. Terraform expressions like ${var.environment} and ${map(string)} are preserved as-is.
// Loading .tf directly - uses $[...] syntax automatically
const config = await configorama('./main.tf')
// config.locals[0].app_name = "myapp-${var.environment}" (preserved)
// Use $[...] for configorama variables in .tf files
// myvar: $[env:MY_VAR]
// myref: $[file(./other.yml)] # referenced files also use $[...]When importing .tf files from other config formats (yml, json, etc.) via ${file()}, the parent file's syntax applies. Use allowUnknownVariableTypes: true if the imported .tf contains Terraform interpolations:
const config = await configorama('./config.yml', {
allowUnknownVariableTypes: true
})Read-only support:
Currently, HCL files can be read and parsed, but writing/generating HCL files is not supported.
See tests/hclTests for example Terraform files.
Access repository information from the current working directory's git data.
########################
# Git Variables
########################
# Repo owner/name. E.g. DavidWells/configorama
repo: ${git:repo}
repository: ${git:repository}
# Repo owner. E.g. DavidWells
owner: ${git:owner}
repoOwner: ${git:repoOwner}
repoOwnerDashed: ${git:repo-owner}
# Url. E.g. https://github.com/DavidWells/configorama
url: ${git:url}
repoUrl: ${git:repoUrl}
repoUrlDashed: ${git:repo-url}
# Directory. E.g. https://github.com/DavidWells/configorama/tree/master/tests/gitVariables
dir: ${git:dir}
directory: ${git:directory}
# Branch
branch: ${git:branch}
# Commits. E.g. 785fa6b982d67b079d53099d57c27fa87c075211
commit: ${git:commit}
# Sha1. E.g. 785fa6b
sha1: ${git:sha1}
# Message. E.g. 'Initial commit'
message: ${git:message}
# Remotes. E.g. https://github.com/DavidWells/configorama
remote: ${git:remote}
remoteDefined: ${git:remote('origin')}
remoteDefinedNoQuotes: ${git:remote(origin)}
# Tags. E.g. v0.5.2-1-g785fa6b
tag: ${git:tag}
# Describe. E.g. v0.5.2-1-g785fa6b
describe: ${git:describe}
# Timestamp. E.g. 2025-01-28T07:28:53.000Z
gitTimestampRelativePath: ${git:timestamp('../../package.json')}
# Timestamp. E.g. 2025-01-28T07:28:53.000Z
gitTimestampAbsolutePath: ${git:timestamp('package.json')}How it works:
.git directory in current working directory or parent directoriesConvert human-readable time expressions into standard cron syntax.
# Basic patterns
everyMinute: ${cron('every minute')} # * * * * *
everyHour: ${cron('every hour')} # 0 * * * *
everyDay: ${cron('every day')} # 0 0 * * *
weekdays: ${cron('weekdays')} # 0 0 * * 1-5
midnight: ${cron('midnight')} # 0 0 * * *
noon: ${cron('noon')} # 0 12 * * *
# Interval patterns
every5Minutes: ${cron('every 5 minutes')} # */5 * * * *
every15Minutes: ${cron('every 15 minutes')} # */15 * * * *
every2Hours: ${cron('every 2 hours')} # 0 */2 * * *
every3Days: ${cron('every 3 days')} # 0 0 */3 * *
# Specific times
at930: ${cron('at 9:30')} # 30 9 * * *
at930pm: ${cron('at 9:30 pm')} # 30 21 * * *
at1200: ${cron('at 12:00')} # 0 12 * * *
at1230am: ${cron('at 12:30 am')} # 30 0 * * *
# Weekday patterns
mondayMorning: ${cron('on monday at 9:00')} # 0 9 * * 1
fridayEvening: ${cron('on friday at 17:00')} # 0 17 * * 5
sundayNoon: ${cron('on sunday at 12:00')} # 0 12 * * 0
# Pre-existing cron expressions (pass through)
customCron: ${cron('15 2 * * *')} # 15 2 * * *Supported expressions:
every N minutes/hours/daysat HH:MM [am/pm]on [weekday] at HH:MMmidnight, noon, weekdaysEvaluate mathematical and logical expressions safely (without using JavaScript's eval). Uses the subscript library for safe expression evaluation.
# Math operations
sum: ${eval(10 + 5)} # 15
multiply: ${eval(10 * 3)} # 30
divide: ${eval(100 / 4)} # 25
modulo: ${eval(17 % 5)} # 2
# Comparisons (returns boolean)
isGreater: ${eval(200 > 100)} # true
isLess: ${eval(100 > 200)} # false
isEqual: ${eval(10 == 10)} # true
# String comparisons
isEqual: ${eval("hello" == "hello")} # true
strictEqual: ${eval("foo" === "foo")} # true
notEqual: ${eval("a" != "b")} # true
# Complex expressions
complex: ${eval((10 + 5) * 2)} # 30
percentage: ${eval((75 / 100) * 200)} # 150
# With variables
threshold: 50
value: 75
aboveThreshold: ${eval(${value} > ${threshold})} # trueSupported operators:
| Category | Operators |
|---|---|
| Arithmetic | + - * / % |
| Comparison | == != === !== > < >= <= |
| Logical | && \|\| ! |
| Grouping | ( ) |
Security:
eval()Conditional expressions using ternary syntax. This is an alias for eval with a clearer name for conditionals.
# Basic ternary (condition ? "yes" : "no")
status: ${if(5 > 3 ? "yes" : "no")} # "yes"
# With variables
threshold: 50
value: 75
result: ${if(${value} > ${threshold} ? "above" : "below")} # "above"
# Nested ternary (if/else if/else)
score: 85
grade: ${if(${score} >= 90 ? "A" : ${score} >= 80 ? "B" : "C")} # "B"
# Boolean result (no ternary needed)
isValid: ${if(${value} > 0)} # true
# Logical operators
enabled: true
count: 5
canProceed: ${if(${enabled} && ${count} > 0)} # true
hasIssues: ${if(!${enabled} || ${count} == 0)} # falseSupported operators:
| Category | Operators |
|---|---|
| Comparison | == != === !== > < >= <= |
| Logical | && \|\| ! |
| Nullish | ?? |
| Ternary | condition ? "yes" : "no" |
Serverless deployment examples:
service: my-service
provider:
name: aws
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'us-east-1'}
custom:
# Different memory by stage
memorySize: ${if(${provider.stage} === "prod" ? 1024 : 512)}
# Different log retention by stage
logRetention: ${if(${provider.stage} === "prod" ? 30 : 7)}
# Enable features per environment
enableDebugEndpoints: ${if(${provider.stage} !== "prod")}
enableMetrics: ${if(${provider.stage} === "prod")}
# Regional settings
replicaCount: ${if(${provider.region} === "us-east-1" ? 3 : 1)}
# Conditional IAM role (use predefined role in prod, inline in dev)
useExternalRole: ${if(${provider.stage} === "prod")}
role: ${if(${custom.useExternalRole} ? "arn:aws:iam::123:role/prod-role" : null)}
functions:
api:
handler: handler.api
memorySize: ${custom.memorySize}
# Debug function - only deployed in non-prod
debug:
handler: handler.debug
enabled: ${custom.enableDebugEndpoints}
# Metrics processor - only in prod
metricsProcessor:
handler: handler.metrics
enabled: ${custom.enableMetrics}Pipe resolved values through transformation functions like case conversion.
# String transformations
toUpperCaseString: ${'value' | toUpperCase } # 'VALUE'
toLowerCaseString: ${'VALUE' | toLowerCase } # 'value'
# Case conversions
toKebabCaseString: ${'valueHere' | toKebabCase } # 'value-here'
toCamelCaseString: ${'value-here' | toCamelCase } # 'valueHere'
# Chaining filters
key: lol_hi
transformed: ${key | toKebabCase | toUpperCase } # 'LOL-HI'
# With variables
serviceName: MyServiceName
serviceSlug: ${serviceName | toKebabCase} # 'my-service-name'Built-in filters:
toUpperCase - Convert to uppercasetoLowerCase - Convert to lowercasetoKebabCase - Convert to kebab-casetoCamelCase - Convert to camelCaseString, Number, Boolean, Array, Object, Json - Validate/coerce resolved valuesoneOf(...) - Restrict a value to inline literals or a resolved list variablehelp('text') - Attach guidance to a variable for the config wizard; returns the value unchangedThe help() filter is an identity filter: it leaves the value untouched but records prompt/agent description text.
apiKey: ${env:API_KEY | help('The Stripe live secret key')}
stage: ${option:stage | toUpperCase | help('Deployment stage')}
dbPort: ${env:DB_PORT, 5432 | Number | help('The Postgres port')}oneOf() is a runtime constraint. It throws if the resolved value is not in the allowed set. Put type filters first when coercion matters:
stage: ${option:stage | oneOf('dev', 'staging', 'prod')}
threads: ${option:threads | Number | oneOf(1, 2, 4)}
allowedStages:
- dev
- prod
stageFromList: ${option:stage | oneOf(${self:allowedStages})}Array accepts existing arrays, JSON/JSON5 array strings, and comma-separated text. Object accepts existing objects and JSON/JSON5 object strings.
Comments are used as help fallback when help() is absent. Precedence is help() first, then trailing inline comments, then a leading comment block:
# Used by deploy jobs
deployToken: ${env:DEPLOY_TOKEN}
region: ${option:region, 'us-east-1'} # AWS regionUse comment annotations for human and agent metadata. Filters affect runtime values; comments describe values:
secrets:
# Stripe live secret key
# @from Stripe dashboard > Developers > API keys
# @example sk_live_...
# @default Set in CI or local shell profile
# @sensitive true
# @group Payments
# @deprecated Use STRIPE_RESTRICTED_KEY instead
stripeSecret: ${env:STRIPE_SECRET_KEY}Supported annotation tags:
@description ... - Explicit description; overrides normal comment text and help()@from ... - Where to obtain the value; appears as obtainHint@example ... - Example value; can appear multiple times@default ... - Documentation-only default hint; does not resolve the variable@sensitive true|false - Override name-based masking detection@group ... - Wizard display group label@deprecated ... - Warning text for requirements JSON and prompt descriptorsfrom() and meta() are not built-in filters. JSON files cannot use comment annotations because JSON has no comments; use JSON5/JSONC or another commented format if metadata is needed.
Custom filters:
const config = await configorama('config.yml', {
filters: {
// Custom filter
reverse: (value) => value.split('').reverse().join(''),
// Filter with options
truncate: (value, length = 10) => value.substring(0, length)
}
})# Using custom filters
reversed: ${'hello' | reverse} # 'olleh'
truncated: ${'very long string' | truncate(5)} # 'very 'Apply built-in functions to combine, transform, or manipulate values.
object:
one: once
two: twice
objectTwo:
three: third
four: fourth
# Merge objects
mergeObjects: ${merge(${object}, ${objectTwo})}
# Result: { one: 'once', two: 'twice', three: 'third', four: 'fourth' }
# String concatenation
fullName: ${concat(${firstName}, ' ', ${lastName})}
# Array operations
items:
- a
- b
- c
joinedItems: ${join(${items}, ', ')} # 'a, b, c'Built-in functions:
merge(obj1, obj2, ...) - Merge multiple objectsconcat(str1, str2, ...) - Concatenate stringsjoin(array, separator) - Join array elementsCustom functions:
const config = await configorama('config.yml', {
functions: {
// Custom function
add: (a, b) => a + b,
// Function with multiple args
between: (val, min, max) => val >= min && val <= max
}
})# Using custom functions
sum: ${add(5, 10)} # 15
value: 75
inRange: ${between(${value}, 50, 100)} # truePlugins ship in the repo under plugins/ and are opt-in: install their peer dependencies, then wire them into variableSources. Plugins are not required dependencies of configorama itself, so consumers who don't need them aren't paying for them.
Resolves CloudFormation stack output values. Single-region, multi-region, and multi-account.
# Default region, default AWS credentials
apiUrl: ${cf:my-stack.ApiUrl}
# Explicit region
westUrl: ${cf(us-west-2):west-stack.ApiUrl}
# Cross-account: 'prod' matches PROD_AWS_ACCESS_KEY_ID env vars
prodUrl: ${cf(prod:us-west-2):prod-stack.ApiUrl}const configorama = require('configorama')
const createCloudFormationResolver = require('configorama/plugins/cloudformation')
const cfResolver = createCloudFormationResolver({
defaultRegion: 'us-east-1',
})
const config = await configorama('config.yml', {
variableSources: [cfResolver]
})Full docs: plugins/cloudformation/README.md. Covers the env-var-prefix alias convention, the refcounted credential mutex for parallel-safe deploys, and the skipResolution mode for CI metadata extraction.
Peer dependency (install separately):
npm install @aws-sdk/client-cloudformation @aws-sdk/credential-providersThe primary async API for resolving configurations.
Signature:
function configorama<T = any>(
configPathOrObject: string | object,
settings?: ConfigoramaSettings
): Promise<T | ConfigoramaResult<T>>Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
configPathOrObject | string \| object | Yes | Path to config file or raw JavaScript object |
settings | ConfigoramaSettings | No | Configuration options |
Settings object:
interface ConfigoramaSettings {
options?: Record<string, any> // CLI flags for ${opt:xyz}
syntax?: string | RegExp // Custom variable syntax
configDir?: string // Working directory for relative paths
variableSources?: VariableSource[] // Custom variable resolvers
filters?: Record<string, Function> // Custom filter functions
functions?: Record<string, Function> // Custom functions
allowUnknownVariableTypes?: boolean | string[] // Allow unknown var types
allowUnresolvedVariables?: boolean | string[] // Allow unresolved vars
allowUndefinedValues?: boolean // Allow undefined in output
returnMetadata?: boolean // Return metadata with config
mergeKeys?: string[] // Keys to merge in arrays
filePathOverrides?: Record<string, string> // Override file paths
}Returns:
returnMetadata: false (default): Promise<T> - Resolved config objectreturnMetadata: true: Promise<ConfigoramaResult<T>> - Object with config and metadataExample:
const configorama = require('configorama')
// Basic usage
const config = await configorama('./config.yml')
// With options
const config = await configorama('./config.yml', {
options: { stage: 'prod', region: 'us-east-1' },
allowUnknownVariableTypes: ['ssm', 'cf']
})
// With metadata
const result = await configorama('./config.yml', {
returnMetadata: true,
options: { stage: 'prod' }
})
console.log(result.config) // Resolved config
console.log(result.metadata) // Variable metadata
console.log(result.resolutionHistory) // Resolution stepsSynchronous API for blocking config resolution.
Signature:
function configorama.sync<T = any>(
configPathOrObject: string | object,
settings?: ConfigoramaSettings
): TParameters:
Same as async API, but dynamicArgs cannot be a function (must be serializable).
Returns:
T - Resolved config object (synchronously)
Limitations:
dynamicArgs must be serializable (not a function)process.argv if options not providedExample:
const configorama = require('configorama')
// Basic sync usage
const config = configorama.sync('./config.yml')
// With options
const config = configorama.sync('./config.yml', {
options: { stage: 'dev' }
})const configorama = require('configorama')
const config = await configorama('config.yml')
const result = await configorama('config.yml', { returnMetadata: true })
const requirements = await configorama.inspect('config.yml', { view: 'requirements' })
const graph = await configorama.inspect('config.yml', { view: 'graph', format: 'mermaid' })
const configSync = configorama.sync('config.yml')The stable public surface is intentionally small:
| Method | Use it for |
|---|---|
configorama(fileOrObject, opts) | Async resolution. Set returnMetadata: true when you need metadata.variables, metadata.uniqueVariables, or metadata.fileDependencies. |
configorama.sync(fileOrObject, opts) | Synchronous resolution for blocking contexts. |
configorama.inspect(fileOrObject, opts) | Pre-resolution inspection. Use view: "requirements", view: "audit", or view: "graph" for one projection. |
configorama.format | Parser utilities for YAML, JSON/JSON5, TOML, INI, HCL, and Markdown frontmatter. |
Lower-level helpers still exist for compatibility: analyze(), introspect(), audit(), graph(), buildVariableSyntax(), and Configorama. New code should start with configorama(), configorama.sync(), or configorama.inspect().
returnMetadata: true remains the right API for tools that need resolved config plus dependency metadata:
const result = await configorama('serverless.yml', {
returnMetadata: true,
allowUnknownVariableTypes: true,
allowUnresolvedVariables: true,
options: { stage: 'prod' }
})
console.log(result.config)
console.log(result.metadata.variables)
console.log(result.metadata.uniqueVariables)
console.log(result.metadata.fileDependencies.resolvedPaths)
console.log(result.metadata.fileDependencies.globPatterns)That metadata shape is the same path used by the newer inspection APIs, so tools can keep consuming it without switching to inspect().
Inspect config structure without resolving missing user inputs.
Signature:
function configorama.inspect(
configPathOrObject: string | object,
settings?: ConfigoramaSettings & {
view?: 'requirements' | 'audit' | 'graph'
format?: 'json' | 'mermaid' | 'dot'
}
): Promise<object | string>With no view, inspect() returns the full model:
const model = await configorama.inspect('./config.yml')
console.log(model.requirements)
console.log(model.graph)
console.log(model.audit)Use view for one projection:
const configorama = require('configorama')
const requirements = await configorama.inspect('./config.yml', { view: 'requirements' })
const audit = await configorama.inspect('./config.yml', { view: 'audit' })
const graph = await configorama.inspect('./config.yml', { view: 'graph', format: 'mermaid' })The older analyze(), introspect(), audit(), and graph() helpers map to the same underlying inspection paths. They are kept for existing consumers.
Parse various config formats to JavaScript objects.
Available parsers:
const { format } = require('configorama')
// Parse YAML
const yamlObj = format.yaml.parse('key: value')
// Parse JSON (handles JSON5/JSONC too: comments, trailing commas)
const jsonObj = format.json.parse('{ key: "value", }')
// Parse TOML
const tomlObj = format.toml.parse('key = "value"')
// Parse INI
const iniObj = format.ini.parse('[section]\nkey=value')
// Parse HCL (requires @cdktf/hcl2json)
const hclObj = await format.hcl.parse('variable "example" { default = "value" }')Available parsers: format.json, format.yaml, format.toml, format.ini, format.hcl, format.markdown.
Each has at minimum a parse(content) method; dump(obj) / stringify(obj) and cross-format converters (e.g. format.yaml.toJson, format.toml.toYaml) are available where the underlying format supports them. format.markdown is a frontmatter parser; see Markdown Files below.
Real use cases for format:
const { format } = require('configorama')
const fs = require('fs')
const raw = format.yaml.parse(fs.readFileSync('config.yml', 'utf8'))
// raw is the YAML structure with ${...} strings intactMarkdown configs (.md, .mdx, .markdown, .mdown, .mkdn, .mkd) are parsed as YAML/TOML/JSON frontmatter + body. The frontmatter becomes top-level keys; the body is exposed as _content on the resolved config (or _body if the frontmatter used that key explicitly).
---
service: my-service
stage: ${opt:stage, 'dev'}
---
# Service Docs
This is the body content.Resolves to:
{
service: 'my-service',
stage: 'dev',
_content: '# Service Docs\nThis is the body content.'
}The body is detached during variable resolution (so ${…} inside the body text is left alone) and re-attached afterward; only frontmatter keys get variable expansion.
buildVariableSyntax(prefix, suffix, excludePatterns?)Helper for building a properly-escaped regex source string to pass to the syntax option. Handles regex-special characters in your delimiters without you having to escape them yourself.
const { buildVariableSyntax } = require('configorama')
// Use {{ ... }} instead of ${ ... }
const syntax = buildVariableSyntax('{{', '}}')
const config = await configorama('config.yml', { syntax })Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
prefix | string | '${' | Opening delimiter |
suffix | string | '}' | Closing delimiter |
excludePatterns | string[] | ['AWS', 'aws:', 'stageVariables'] | Patterns to exclude via negative lookahead (so e.g. ${AWS::Region} and ${aws:username} are left untouched by CloudFormation users) |
Configorama ClassFor advanced use cases (long-lived instances, hooking into init/resolve lifecycle, accessing partial state) the underlying class is exported.
const { Configorama } = require('configorama')
const instance = new Configorama('config.yml', { options: { stage: 'dev' } })
await instance.init({ stage: 'dev' })
const resolved = await instance.populateObject(instance.config)
const metadata = instance.collectVariableMetadata()Most users should prefer the top-level configorama() / .sync() / .analyze() functions, which are thin wrappers around this class.
configorama/parse-file SubpathFor tools that want to parse a config file (auto-detecting format from extension or contents) without going through variable resolution:
const { parseFile, parseFileContents } = require('configorama/parse-file')
// Read from disk
const raw = parseFile('./config.yml')
// returns the parsed object with ${...} strings intact
// Or parse already-loaded contents
const fromString = parseFileContents({
contents: 'service: my-app\nstage: ${opt:stage}',
filePath: 'in-memory.yml'
})Both are synchronous. Useful for build tools that inspect or rewrite configs before handing them to configorama.
Type definitions are bundled (index.d.ts). TypeScript users get:
configorama<MyConfig>('config.yml') returns Promise<MyConfig>ConfigoramaSettings and ConfigoramaResultimport configorama, { ConfigoramaSettings } from 'configorama'
interface MyConfig {
service: string
stage: string
database: { host: string; port: number }
}
const config = await configorama<MyConfig>('config.yml', { options: { stage: 'prod' } })
// config.database.port is typed as numberUse the syntax option to change the variable delimiters. You can provide a regex string directly or use buildVariableSyntax() to generate one with proper character escaping:
const configorama = require('configorama')
const { buildVariableSyntax } = require('configorama')
// Using buildVariableSyntax helper (recommended)
const config = await configorama(configFile, {
syntax: buildVariableSyntax('{{', '}}'), // Mustache-style: {{env:FOO}}
options: { stage: 'dev' }
})
// Other examples:
buildVariableSyntax('${{', '}}') // ${{env:FOO}}
buildVariableSyntax('#{', '}') // #{env:FOO}
buildVariableSyntax('[[', ']]') // [[env:FOO]]
buildVariableSyntax('<', '>') // <env:FOO>Function signature:
function buildVariableSyntax(
prefix: string = '${',
suffix: string = '}',
excludePatterns: string[] = ['AWS', 'aws:', 'stageVariables']
): stringThe buildVariableSyntax() function:
It is the kind of utility that comes from building repeatable systems: keep the config readable, make the dynamic parts explicit, and let the same structure work across local, staging, and production environments.
Resolve dynamic config values from environment variables, CLI flags, files, git data, expressions, and custom sources. Works with YAML, JSON, TOML, INI, HCL, Markdown, JavaScript, and TypeScript.
npm install configorama
npx configorama config.yml --stage prodConfigorama is a framework-agnostic variable engine for configuration files. Use it to resolve a config at runtime, inspect missing values before resolution, audit risky references, draw dependency graphs, run an interactive setup flow, or emit requirements JSON for agents and automation.
Deployment configs usually pull values from several places: env vars, CLI flags, local files, generated JavaScript, git metadata, stage-specific maps, and secret stores. Most config parsers stop at parsing, while framework-specific variable systems tend to stay tied to that framework.
Configorama loads a config file, finds variable references, resolves them in dependency order, applies filters/functions, and returns a plain JavaScript object. It can also report what the config needs before resolution.
Common use cases:
| Need | Support | Example |
|---|---|---|
| Resolve values from many sources | Built-in env, option/opt, self, file, text, git, cron, eval, and if sources | ${env:API_KEY}, ${option:stage}, ${file(./secrets.yml)} |
| Keep config portable | Runs outside any framework | Use the same resolver in a CLI, build script, deploy job, or app bootstrap |
| Prompt for missing inputs | Interactive setup wizard with type-aware prompts and masked secrets | configorama setup config.yml |
| Tell agents what to provide | Requirements JSON with schemaVersion, requirements[], and ask[] | configorama inspect config.yml --view requirements |
| Inspect before resolving | Full inspection model plus focused requirements, audit, and graph views | configorama inspect config.yml |
| Document variables near the config | help() plus comment annotations for descriptions, obtain hints, examples, groups, sensitivity, and deprecation warnings | # @from Stripe dashboard > Developers > API keys |
| Enforce runtime constraints | Type filters and oneOf(...) validation | ${option:threads \| Number \| oneOf(1, 2, 4)} |
# config.yml
service: billing-api
# Deployment stage
stage: ${option:stage | oneOf("dev", "staging", "prod")}
secrets:
# Stripe live secret key
# @from Stripe dashboard > Developers > API keys
# @example sk_live_...
# @sensitive true
# @group Payments
stripeSecret: ${env:STRIPE_SECRET_KEY}
database:
host: ${env:DB_HOST, "localhost"}
port: ${env:DB_PORT, 5432 | Number}
name: ${self:service}-${self:stage}# Resolve the config
STRIPE_SECRET_KEY=sk_live_xxx npx configorama config.yml --stage prod
# Walk through missing variables interactively
npx configorama setup config.yml
# Print requirements for agents or automation
npx configorama inspect config.yml --view requirements
# Inspect requirements, dependency graph, and audit report together
npx configorama inspect config.yml| Area | Added |
|---|---|
| Normalized requirements model | ConfigRequirements groups occurrences by variable, normalizes ${opt:...} and ${option:...} as variableType: "option", and tracks paths, defaults, types, allowed values, sensitivity, and conflicts. |
| Unified inspection CLI | configorama inspect config.yml emits requirements, graph, and audit output without resolving missing values. Use --view requirements, --view audit, or --view graph for one slice. |
| Requirements JSON | configorama inspect config.yml --view requirements, configorama requirements config.yml, and configorama config.yml --requirements emit schemaVersion: 1, summary, requirements[], and environment-aware ask[]. |
| Safe inspection | inspect, audit, and graph run in safe mode by default. Use --unsafe to opt out or --safe-root <dir> to restrict file/text references. |
| Agent-friendly CLI contract | configorama capabilities prints commands, aliases, formats, flags, error codes, and exit codes as JSON. |
| Path extraction polish | Jq-style paths, --raw, and --copy make scalar extraction usable in scripts: configorama -r --copy config.yml .database.host. |
| Conflict handling | Conflicting type/default/allowed-value/annotation metadata is deterministic in the wizard. Requirements serialization fails on conflicts so agents get a clean contract. |
| Setup wizard migration | The wizard now consumes prompt descriptors derived from the requirements model, supports enum selects from oneOf, displays annotation details, and redacts sensitive values in setup summaries and setup stdout. |
oneOf(...) validation | Runtime filter for inline literal sets and resolved list variables, including type-filter-first behavior such as ${option:threads \| Number \| oneOf(1, 2, 4)}. |
| More type filters | Array and Object filters now validate/coerce arrays, comma-separated lists, JSON/JSON5 arrays, and JSON/JSON5 objects. |
| Option alias | ${option:name} is supported alongside the existing ${opt:name} shorthand. |
| Comment metadata | Leading/inline comments become help fallback; structured tags add @description, @from, @example, @default, @sensitive, @group, and @deprecated. |
| Structured CLI errors | Inspection commands default to JSON errors on stderr; --error-format human is available for terminal use. |
As a library dependency:
npm install configoramaAs a global CLI tool:
npm install -g configoramaAsync API (recommended for most use cases):
const path = require('path')
const configorama = require('configorama')
const cliFlags = require('minimist')(process.argv.slice(2))
// Path to yaml/json/toml config
const myConfigFilePath = path.join(__dirname, 'config.yml')
// Execute config resolution asynchronously
const config = await configorama(myConfigFilePath, { options: cliFlags })
console.log(config) // resolved configSync API (for synchronous execution contexts):
const path = require('path')
const configorama = require('configorama')
const cliFlags = require('minimist')(process.argv.slice(2))
// Path to yaml/json/toml config
const myConfigFilePath = path.join(__dirname, 'config.yml')
// Execute config resolution synchronously
const config = configorama.sync(myConfigFilePath, { options: cliFlags })
console.log(config) // resolved configExample configuration file (config.yml):
# Environment variable
apiKey: ${env:API_KEY}
# CLI option (e.g., --stage prod)
environment: ${opt:stage, 'dev'}
# Self-reference to other values
service: my-app
fullName: ${service}-api
# File reference
secrets: ${file(./secrets.yml)}
# Git information
branch: ${git:branch}
commit: ${git:sha1}
# Conditional logic
memorySize: ${if(${environment} === 'prod' ? 1024 : 512)}
# Nested references
database:
host: ${env:DB_HOST, 'localhost'}
port: ${env:DB_PORT, 5432}
name: ${service}-${environment}The project includes example files demonstrating various features:
# Clone the repository
git clone https://github.com/DavidWells/configorama
cd configorama
# Install dependencies
npm install
# Run async API example
node examples/using-async-api.js --stage prod
# Run sync API example
node examples/using-sync-api.js --stage dev
# Run zero-config example
node examples/zero-config.js
# Run TypeScript example
node examples/typescript/using-typescript.jsConfigorama creates a dependency graph of your config file and all its dependencies, then resolves values based on their variable sources. The resolution process follows this flow:
flowchart TD
A[Load config file] --> B[Parse yml/json/toml/hcl to object]
B --> C[Preprocess: raw config file]
C --> D{Return metadata only?}
D -->|Yes| E[Collect variable metadata]
E --> F[Return found variable metadata + original config]
D -->|No| G[Traverse & resolve variables recursively]
G --> H[Post-process: runs filters and functions]
H --> I[Return resolved config]Resolution process:
Analyze config structure and variables without actually resolving them:
const result = await configorama.analyze('config.yml')
// Returns metadata about variables without resolving them
console.log(result.originalConfig) // Raw config object
console.log(result.variables) // All variables found
console.log(result.uniqueVariables) // Variables grouped by name
console.log(result.fileDependencies) // File references found
Use cases:
Resolve config and get detailed metadata about the resolution process:
const result = await configorama('config.yml', {
returnMetadata: true,
options: { stage: 'prod' }
})
// Returns both resolved config and metadata
console.log(result.config) // Fully resolved config
console.log(result.originalConfig) // Raw config object
console.log(result.metadata.variables) // Variable info with resolution details
console.log(result.metadata.fileDependencies) // All file dependencies
console.log(result.metadata.summary) // { totalVariables, requiredVariables, variablesWithDefaults }
console.log(result.resolutionHistory) // Step-by-step resolution for each pathMetadata structure:
{
config: { /* resolved config */ },
originalConfig: { /* raw config */ },
metadata: {
variables: [
{
variable: '${env:API_KEY}',
variableType: 'env',
variableName: 'API_KEY',
variablePath: 'apiKey',
defaultValue: null,
hasDefault: false,
resolved: true,
resolvedValue: 'secret-key-123'
},
// ... more variables
],
summary: {
totalVariables: 15,
requiredVariables: 8,
variablesWithDefaults: 7
},
fileDependencies: ['./secrets.yml', './config.ts']
},
resolutionHistory: {
'apiKey': [
{ step: 1, value: '${env:API_KEY}', type: 'env' },
{ step: 2, value: 'secret-key-123', resolved: true }
]
}
}┌────────���─────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ Input │───▶│ Configorama core │───▶│ Output │
│ │ │ │ │ │
│ • Config file │ │ parser registry │ │ Resolved config │
│ • JS/TS object │ │ (yaml, json, toml, │ │ (+ metadata if │
│ • Inline opts │ │ ini, hcl, md, …) │ │ requested) │
└──────────────────┘ │ │ └──────────────────┘
│ preProcess() │
│ ↓ │
│ populateObject() │◀───┐ iterates until
│ ↓ │ │ no variables
│ resolve leaf vars │────┘ remain
│ ↓ │
│ apply filters/funcs│
│ ↓ │
│ return │
└──────────┬──────────┘
│
▼
┌──────────────────────────────────────┐
│ Variable Sources │
│ ┌────────┐ ┌────────┐ ┌────────────┐ │
│ │ env │ │ opt │ │ file/text │ │
│ │ self │ │ param │ │ git/cron │ │
│ │ eval │ │ if │ │ + plugins │ │
│ └────────┘ └────────┘ └────────────┘ │
│ │
│ Bundled plugins: │
│ • plugins/cloudformation │
│ │
│ Custom: variableSources: [{…}] │
└──────────────────────────────────────┘Resolution is a fixed-point loop: each pass resolves what it can, then populateObject() runs again until no ${…} references remain. Built-in resolvers run first; custom resolvers from variableSources are tried in order.
A typical 21KB serverless-style config resolves in ~3ms on warm Node 22.
0.9.17 baseline: PERF.mdscripts/bench.jsnode scripts/bench.js # local
node scripts/bench.js /path/to/another/configorama # A/BIf your config is slow, please open an issue with the config (or a redacted reproduction). We're happy to profile and tighten the hot path.
Configorama supports multiple variable sources. All variable syntax follows the pattern ${type:value} or ${type(value)}.
| Variable | Syntax | Description | Example |
|---|---|---|---|
| env | ${env:VAR} | Environment variables | ${env:NODE_ENV} |
| option | ${option:flag} or ${opt:flag} | CLI option flags (opt is shorthand) | ${option:stage} |
| param | ${param:key} | Parameter values | ${param:domain} |
| self | ${key} or ${self:key} | Self references | ${database.host} |
| file | ${file(path)} | File references | ${file(./secrets.yml)} |
| text | ${text(path)} | Raw text file | ${text(./README.md)} |
| git | ${git:value} | Git data | ${git:branch} |
| cron | ${cron(expr)} | Cron expressions | ${cron('every 5 minutes')} |
| eval | ${eval(expr)} | Math/logic expressions | ${eval(10 + 5)} |
| if | ${if(expr)} | Conditional expressions | ${if(x > 5 ? 'yes' : 'no')} |
Access values from process.env environment variables.
# Basic env var
apiKey: ${env:SECRET_KEY}
# With fallback default if env var not found
apiKeyWithFallback: ${env:SECRET_KEY, 'defaultApiKey'}
# Common patterns
nodeEnv: ${env:NODE_ENV, 'development'}
port: ${env:PORT, 3000}
debug: ${env:DEBUG, false}
How it works:
process.env at resolution timeallowUnresolvedVariables is set)CLI usage:
# Set env var then run
SECRET_KEY=abc123 node app.js
# Or export first
export SECRET_KEY=abc123
node app.jsAccess values from command line arguments passed via the options parameter.
# CLI option. Example `cmd --stage dev` makes `bar: dev`
bar: ${opt:stage}
# Composed example makes `foo: dev-hello`
foo: ${opt:stage}-hello
# With default value. If no --stage flag, uses 'dev'
environment: ${opt:stage, 'dev'}
# Boolean flags
verbose: ${opt:verbose, false}
# Nested paths
region: ${opt:aws.region, 'us-east-1'}How it works:
options object passed to configoramaminimist or similar parserExample:
const minimist = require('minimist')
const configorama = require('configorama')
const argv = minimist(process.argv.slice(2))
// argv = { stage: 'prod', verbose: true, aws: { region: 'eu-west-1' } }
const config = await configorama('config.yml', { options: argv })# Command line
node app.js --stage prod --verbose --aws.region eu-west-1Access parameter values via ${param:key}. Parameters follow a resolution hierarchy:
--param="key=value") - highest prioritystages.<stage>.params)stages.default.params)# Direct parameter reference
appDomain: ${param:domain}
# Parameter with fallback
apiKey: ${param:apiKey, 'default-api-key'}
# Stage-specific parameters defined in config
stages:
dev:
params:
domain: dev.myapp.com
dbHost: localhost
prod:
params:
domain: myapp.com
dbHost: prod-db.myapp.com
default:
params:
domain: default.myapp.com
dbPort: 3306CLI Usage:
# Single param
node app.js --param="domain=example.com"
# Multiple params
node app.js --param="domain=example.com" --param="apiKey=secret123"
# With stage selection
node app.js --stage prod --param="domain=cli-override.com"Code Usage:
const config = await configorama('config.yml', {
options: {
stage: 'prod',
param: ['domain=cli-override.com', 'apiKey=secret']
}
})Resolution order example:
stages:
prod:
params:
domain: prod.myapp.com # 2. Stage-specific
default:
params:
domain: default.myapp.com # 3. Default fallback
appUrl: ${param:domain}# CLI override (highest priority)
node app.js --stage prod --param="domain=cli.myapp.com"
# Result: appUrl = 'cli.myapp.com'
# Stage param (no CLI override)
node app.js --stage prod
# Result: appUrl = 'prod.myapp.com'
# Default param (no CLI override, no stage match)
node app.js --stage staging
# Result: appUrl = 'default.myapp.com'Reference values from other key paths in the same configuration file using dot notation.
foo: bar
zaz:
matazaz: 1
wow:
cool: 2
# Shorthand dot.prop reference
two: ${foo} # Resolves to 'bar'
# Explicit self file reference
one: ${self:foo} # Resolves to 'bar'
# Dot prop reference traverses objects
three: ${zaz.wow.cool} # Resolves to 2
# Complex nested references
database:
host: localhost
port: 5432
name: mydb
connectionString: postgres://${database.host}:${database.port}/${database.name}
# Resolves to: postgres://localhost:5432/mydb
# Array access
items:
- first
- second
- third
selectedItem: ${items[1]} # Resolves to 'second'How it works:
Import values from external yml, json, toml, hcl, or other supported files by relative path.
# Import full yml/json/toml/hcl file via relative path
fileRef: ${file(./subFile.yml)}
# Import sub values from files (topLevel key from other-config.yml)
fileValue: ${file(./other-config.yml):topLevel}
# Import nested sub values (nested.value from other-config.json)
fileValueSubKey: ${file(./other-config.json):nested.value}
# Fallback to default value if file not found
fallbackValueExample: ${file(./not-found.yml), 'fall back value'}
# Relative paths from config file location
secrets: ${file(../shared/secrets.yml)}
# Import from subdirectory
dbConfig: ${file(./config/database.yml):production}Supported file types (extensions are case-insensitive):
| Type | Extensions |
|---|---|
| TypeScript | .ts, .tsx, .mts, .cts |
| JavaScript | .js, .cjs |
| ESM | .mjs, .esm |
| YAML | .yml, .yaml |
| TOML | .toml, .tml |
| INI | .ini |
| JSON | .json, .json5, .jsonc |
| HCL (Terraform) | .tf, .hcl, .tf.json |
| Markdown | .md, .mdx, .markdown, .mdown, .mkdn, .mkd |
Path resolution:
~ home directory expansion NOT supported (use absolute paths)Example file structure:
project/
├── config.yml # Main config
├── secrets.yml # Secrets file
└── environments/
├── dev.yml
└── prod.yml# config.yml
secrets: ${file(./secrets.yml)}
environment: ${file(./environments/${opt:stage}.yml)}Execute JavaScript files and use their exported function's return value. Functions can be synchronous or asynchronous and receive arguments from your config.
# Async function execution
asyncJSValue: ${file(./async-value.js)}
# Sync function execution
syncJSValue: ${file(./sync-value.js)}
# With arguments (resolved before being passed)
secrets: ${file(./fetch-secrets.js, ${self:environment}, ${self:region})}JavaScript file example (async-value.js):
async function fetchSecretsFromRemoteStore() {
// Simulate async operation (AWS Secrets Manager, HashiCorp Vault, etc.)
await new Promise(resolve => setTimeout(resolve, 1000))
return {
apiKey: 'secret-key-123',
dbPassword: 'db-password-456'
}
}
module.exports = fetchSecretsFromRemoteStoreSync function example (sync-value.js):
function getEnvironmentConfig() {
return {
timeout: 5000,
retries: 3,
logLevel: process.env.NODE_ENV === 'production' ? 'error' : 'debug'
}
}
module.exports = getEnvironmentConfigYou can pass resolved values from your config as arguments to JavaScript/TypeScript functions:
foo: bar
baz:
qux: quux
# Pass resolved values as arguments
secrets: ${file(./fetch-secrets.js, ${self:foo}, ${self:baz})}Arguments are passed in order, with the config context always last:
/**
* @param {string} foo - First arg from YAML ('bar')
* @param {object} baz - Second arg from YAML ({ qux: 'quux' })
* @param {import('configorama').ConfigContext} ctx - Config context (always last)
*/
async function fetchSecrets(foo, baz, ctx) {
console.log(foo) // 'bar'
console.log(baz) // { qux: 'quux' }
// Access config context
console.log(ctx.originalConfig) // Original unresolved config
console.log(ctx.currentConfig) // Current partially-resolved config
console.log(ctx.options) // Options passed to configorama
return { secret: 'value' }
}
module.exports = fetchSecretsThe ctx parameter (always the last argument) provides access to:
| Property | Description |
|---|---|
originalConfig | The original unresolved configuration object |
currentConfig | The current (partially resolved) configuration |
options | Options passed to configorama (populates ${option:xyz} / ${opt:xyz} variables) |
TypeScript users can import the type:
import type { ConfigContext } from 'configorama'
async function fetchSecrets(
foo: string,
baz: { qux: string },
ctx: ConfigContext
): Promise<string> {
// Full type support for ctx properties
return 'secret-value'
}
export = fetchSecretsIf you don't need arguments, the function still receives ctx as its only parameter:
// No args - ctx is the only parameter
async function getSecrets(ctx) {
return ctx.options.stage === 'prod'
? 'prod-secret'
: 'dev-secret'
}
module.exports = getSecretsExecute TypeScript files using tsx (recommended) or ts-node.
Installation:
# Recommended: Modern, fast TypeScript execution
npm install tsx --save-dev
# Alternative: Traditional ts-node approach
npm install ts-node typescript --save-devUsage in config:
# TypeScript configuration object
config: ${file(./config.ts)}
# TypeScript async function
secrets: ${file(./async-secrets.ts)}
# Specific property from TypeScript export
database: ${file(./config.ts):database}
# With arguments
apiConfig: ${file(./config.ts, ${opt:stage})}TypeScript Object Export (typescript-config.ts):
interface DatabaseConfig {
host: string
port: number
database: string
ssl: boolean
}
interface ApiConfig {
baseUrl: string
timeout: number
retries: number
}
interface ConfigObject {
environment: string
database: DatabaseConfig
api: ApiConfig
features: {
enableNewFeature: boolean
debugMode: boolean
}
}
function createConfig(): ConfigObject {
return {
environment: process.env.STAGE || 'development',
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'myapp',
ssl: process.env.NODE_ENV === 'production'
},
api: {
baseUrl: process.env.API_BASE_URL || 'http://localhost:3000',
timeout: 5000,
retries: 3
},
features: {
enableNewFeature: process.env.STAGE === 'production',
debugMode: process.env.DEBUG === 'true'
}
}
}
export = createConfigTypeScript Async Function (typescript-async.ts):
interface SecretStore {
apiKey: string
dbPassword: string
jwtSecret: string
}
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function fetchSecretsFromVault(): Promise<SecretStore> {
console.log('Fetching secrets from vault...')
// Simulate async operations (AWS Secrets Manager, HashiCorp Vault, etc.)
await delay(100)
return {
apiKey: process.env.API_KEY || 'dev-api-key',
dbPassword: process.env.DB_PASSWORD || 'dev-password',
jwtSecret: process.env.JWT_SECRET || 'dev-jwt-secret'
}
}
export = fetchSecretsFromVaultComplete Example Configuration:
# config-with-typescript.yml
service: my-awesome-app
# Load configuration from TypeScript file
provider: ${file(./typescript-config.ts)}
# Load secrets asynchronously from TypeScript file
secrets: ${file(./typescript-async.ts)}
# Mix TypeScript with other configuration
custom:
stage: ${opt:stage, "dev"}
region: ${opt:region, "us-east-1"}
# Use TypeScript files for specific sections
databaseConfig: ${file(./typescript-config.ts):database}
# Environment-specific overrides
stageVariables:
dev:
logLevel: debug
prod:
logLevel: info
# Regular configuration values
resources:
description: "Configuration loaded with TypeScript support"
functions:
hello:
handler: handler.hello
environment:
LOG_LEVEL: ${self:custom.stageVariables.${self:custom.stage}.logLevel}
DB_HOST: ${self:provider.database.host}
API_KEY: ${self:secrets.apiKey}Features:
Configorama supports Terraform HCL (HashiCorp Configuration Language) files, allowing you to parse .tf, .tf.json, and .hcl files.
Installation:
HCL parsing requires the optional @cdktf/hcl2json package:
npm install @cdktf/hcl2jsonSupported file types:
.tf - Terraform configuration files.hcl - Generic HCL files.tf.json - Terraform JSON configuration filesExample:
const configorama = require('configorama')
// Parse a Terraform configuration file
const terraformConfig = await configorama('./main.tf')
// Access Terraform variables, resources, locals, etc.
console.log(terraformConfig.variable) // Variables defined in the file
console.log(terraformConfig.resource) // Resources
console.log(terraformConfig.locals) // Local values
console.log(terraformConfig.output) // OutputsImporting Terraform files:
# Import Terraform variables from a .tf file
terraformVars: ${file(./terraform/variables.tf)}
# Import specific variable from Terraform file
region: ${file(./terraform/variables.tf):variable.region[0].default}Variable syntax:
When loading .tf or .hcl files directly, configorama automatically uses $[...] syntax instead of ${...} to avoid conflicts with Terraform's native ${var.name} interpolation. Terraform expressions like ${var.environment} and ${map(string)} are preserved as-is.
// Loading .tf directly - uses $[...] syntax automatically
const config = await configorama('./main.tf')
// config.locals[0].app_name = "myapp-${var.environment}" (preserved)
// Use $[...] for configorama variables in .tf files
// myvar: $[env:MY_VAR]
// myref: $[file(./other.yml)] # referenced files also use $[...]When importing .tf files from other config formats (yml, json, etc.) via ${file()}, the parent file's syntax applies. Use allowUnknownVariableTypes: true if the imported .tf contains Terraform interpolations:
const config = await configorama('./config.yml', {
allowUnknownVariableTypes: true
})Read-only support:
Currently, HCL files can be read and parsed, but writing/generating HCL files is not supported.
See tests/hclTests for example Terraform files.
Access repository information from the current working directory's git data.
########################
# Git Variables
########################
# Repo owner/name. E.g. DavidWells/configorama
repo: ${git:repo}
repository: ${git:repository}
# Repo owner. E.g. DavidWells
owner: ${git:owner}
repoOwner: ${git:repoOwner}
repoOwnerDashed: ${git:repo-owner}
# Url. E.g. https://github.com/DavidWells/configorama
url: ${git:url}
repoUrl: ${git:repoUrl}
repoUrlDashed: ${git:repo-url}
# Directory. E.g. https://github.com/DavidWells/configorama/tree/master/tests/gitVariables
dir: ${git:dir}
directory: ${git:directory}
# Branch
branch: ${git:branch}
# Commits. E.g. 785fa6b982d67b079d53099d57c27fa87c075211
commit: ${git:commit}
# Sha1. E.g. 785fa6b
sha1: ${git:sha1}
# Message. E.g. 'Initial commit'
message: ${git:message}
# Remotes. E.g. https://github.com/DavidWells/configorama
remote: ${git:remote}
remoteDefined: ${git:remote('origin')}
remoteDefinedNoQuotes: ${git:remote(origin)}
# Tags. E.g. v0.5.2-1-g785fa6b
tag: ${git:tag}
# Describe. E.g. v0.5.2-1-g785fa6b
describe: ${git:describe}
# Timestamp. E.g. 2025-01-28T07:28:53.000Z
gitTimestampRelativePath: ${git:timestamp('../../package.json')}
# Timestamp. E.g. 2025-01-28T07:28:53.000Z
gitTimestampAbsolutePath: ${git:timestamp('package.json')}How it works:
.git directory in current working directory or parent directoriesConvert human-readable time expressions into standard cron syntax.
# Basic patterns
everyMinute: ${cron('every minute')} # * * * * *
everyHour: ${cron('every hour')} # 0 * * * *
everyDay: ${cron('every day')} # 0 0 * * *
weekdays: ${cron('weekdays')} # 0 0 * * 1-5
midnight: ${cron('midnight')} # 0 0 * * *
noon: ${cron('noon')} # 0 12 * * *
# Interval patterns
every5Minutes: ${cron('every 5 minutes')} # */5 * * * *
every15Minutes: ${cron('every 15 minutes')} # */15 * * * *
every2Hours: ${cron('every 2 hours')} # 0 */2 * * *
every3Days: ${cron('every 3 days')} # 0 0 */3 * *
# Specific times
at930: ${cron('at 9:30')} # 30 9 * * *
at930pm: ${cron('at 9:30 pm')} # 30 21 * * *
at1200: ${cron('at 12:00')} # 0 12 * * *
at1230am: ${cron('at 12:30 am')} # 30 0 * * *
# Weekday patterns
mondayMorning: ${cron('on monday at 9:00')} # 0 9 * * 1
fridayEvening: ${cron('on friday at 17:00')} # 0 17 * * 5
sundayNoon: ${cron('on sunday at 12:00')} # 0 12 * * 0
# Pre-existing cron expressions (pass through)
customCron: ${cron('15 2 * * *')} # 15 2 * * *Supported expressions:
every N minutes/hours/daysat HH:MM [am/pm]on [weekday] at HH:MMmidnight, noon, weekdaysEvaluate mathematical and logical expressions safely (without using JavaScript's eval). Uses the subscript library for safe expression evaluation.
# Math operations
sum: ${eval(10 + 5)} # 15
multiply: ${eval(10 * 3)} # 30
divide: ${eval(100 / 4)} # 25
modulo: ${eval(17 % 5)} # 2
# Comparisons (returns boolean)
isGreater: ${eval(200 > 100)} # true
isLess: ${eval(100 > 200)} # false
isEqual: ${eval(10 == 10)} # true
# String comparisons
isEqual: ${eval("hello" == "hello")} # true
strictEqual: ${eval("foo" === "foo")} # true
notEqual: ${eval("a" != "b")} # true
# Complex expressions
complex: ${eval((10 + 5) * 2)} # 30
percentage: ${eval((75 / 100) * 200)} # 150
# With variables
threshold: 50
value: 75
aboveThreshold: ${eval(${value} > ${threshold})} # trueSupported operators:
| Category | Operators |
|---|---|
| Arithmetic | + - * / % |
| Comparison | == != === !== > < >= <= |
| Logical | && \|\| ! |
| Grouping | ( ) |
Security:
eval()Conditional expressions using ternary syntax. This is an alias for eval with a clearer name for conditionals.
# Basic ternary (condition ? "yes" : "no")
status: ${if(5 > 3 ? "yes" : "no")} # "yes"
# With variables
threshold: 50
value: 75
result: ${if(${value} > ${threshold} ? "above" : "below")} # "above"
# Nested ternary (if/else if/else)
score: 85
grade: ${if(${score} >= 90 ? "A" : ${score} >= 80 ? "B" : "C")} # "B"
# Boolean result (no ternary needed)
isValid: ${if(${value} > 0)} # true
# Logical operators
enabled: true
count: 5
canProceed: ${if(${enabled} && ${count} > 0)} # true
hasIssues: ${if(!${enabled} || ${count} == 0)} # falseSupported operators:
| Category | Operators |
|---|---|
| Comparison | == != === !== > < >= <= |
| Logical | && \|\| ! |
| Nullish | ?? |
| Ternary | condition ? "yes" : "no" |
Serverless deployment examples:
service: my-service
provider:
name: aws
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'us-east-1'}
custom:
# Different memory by stage
memorySize: ${if(${provider.stage} === "prod" ? 1024 : 512)}
# Different log retention by stage
logRetention: ${if(${provider.stage} === "prod" ? 30 : 7)}
# Enable features per environment
enableDebugEndpoints: ${if(${provider.stage} !== "prod")}
enableMetrics: ${if(${provider.stage} === "prod")}
# Regional settings
replicaCount: ${if(${provider.region} === "us-east-1" ? 3 : 1)}
# Conditional IAM role (use predefined role in prod, inline in dev)
useExternalRole: ${if(${provider.stage} === "prod")}
role: ${if(${custom.useExternalRole} ? "arn:aws:iam::123:role/prod-role" : null)}
functions:
api:
handler: handler.api
memorySize: ${custom.memorySize}
# Debug function - only deployed in non-prod
debug:
handler: handler.debug
enabled: ${custom.enableDebugEndpoints}
# Metrics processor - only in prod
metricsProcessor:
handler: handler.metrics
enabled: ${custom.enableMetrics}Pipe resolved values through transformation functions like case conversion.
# String transformations
toUpperCaseString: ${'value' | toUpperCase } # 'VALUE'
toLowerCaseString: ${'VALUE' | toLowerCase } # 'value'
# Case conversions
toKebabCaseString: ${'valueHere' | toKebabCase } # 'value-here'
toCamelCaseString: ${'value-here' | toCamelCase } # 'valueHere'
# Chaining filters
key: lol_hi
transformed: ${key | toKebabCase | toUpperCase } # 'LOL-HI'
# With variables
serviceName: MyServiceName
serviceSlug: ${serviceName | toKebabCase} # 'my-service-name'Built-in filters:
toUpperCase - Convert to uppercasetoLowerCase - Convert to lowercasetoKebabCase - Convert to kebab-casetoCamelCase - Convert to camelCaseString, Number, Boolean, Array, Object, Json - Validate/coerce resolved valuesoneOf(...) - Restrict a value to inline literals or a resolved list variablehelp('text') - Attach guidance to a variable for the config wizard; returns the value unchangedThe help() filter is an identity filter: it leaves the value untouched but records prompt/agent description text.
apiKey: ${env:API_KEY | help('The Stripe live secret key')}
stage: ${option:stage | toUpperCase | help('Deployment stage')}
dbPort: ${env:DB_PORT, 5432 | Number | help('The Postgres port')}oneOf() is a runtime constraint. It throws if the resolved value is not in the allowed set. Put type filters first when coercion matters:
stage: ${option:stage | oneOf('dev', 'staging', 'prod')}
threads: ${option:threads | Number | oneOf(1, 2, 4)}
allowedStages:
- dev
- prod
stageFromList: ${option:stage | oneOf(${self:allowedStages})}Array accepts existing arrays, JSON/JSON5 array strings, and comma-separated text. Object accepts existing objects and JSON/JSON5 object strings.
Comments are used as help fallback when help() is absent. Precedence is help() first, then trailing inline comments, then a leading comment block:
# Used by deploy jobs
deployToken: ${env:DEPLOY_TOKEN}
region: ${option:region, 'us-east-1'} # AWS regionUse comment annotations for human and agent metadata. Filters affect runtime values; comments describe values:
secrets:
# Stripe live secret key
# @from Stripe dashboard > Developers > API keys
# @example sk_live_...
# @default Set in CI or local shell profile
# @sensitive true
# @group Payments
# @deprecated Use STRIPE_RESTRICTED_KEY instead
stripeSecret: ${env:STRIPE_SECRET_KEY}Supported annotation tags:
@description ... - Explicit description; overrides normal comment text and help()@from ... - Where to obtain the value; appears as obtainHint@example ... - Example value; can appear multiple times@default ... - Documentation-only default hint; does not resolve the variable@sensitive true|false - Override name-based masking detection@group ... - Wizard display group label@deprecated ... - Warning text for requirements JSON and prompt descriptorsfrom() and meta() are not built-in filters. JSON files cannot use comment annotations because JSON has no comments; use JSON5/JSONC or another commented format if metadata is needed.
Custom filters:
const config = await configorama('config.yml', {
filters: {
// Custom filter
reverse: (value) => value.split('').reverse().join(''),
// Filter with options
truncate: (value, length = 10) => value.substring(0, length)
}
})# Using custom filters
reversed: ${'hello' | reverse} # 'olleh'
truncated: ${'very long string' | truncate(5)} # 'very 'Apply built-in functions to combine, transform, or manipulate values.
object:
one: once
two: twice
objectTwo:
three: third
four: fourth
# Merge objects
mergeObjects: ${merge(${object}, ${objectTwo})}
# Result: { one: 'once', two: 'twice', three: 'third', four: 'fourth' }
# String concatenation
fullName: ${concat(${firstName}, ' ', ${lastName})}
# Array operations
items:
- a
- b
- c
joinedItems: ${join(${items}, ', ')} # 'a, b, c'Built-in functions:
merge(obj1, obj2, ...) - Merge multiple objectsconcat(str1, str2, ...) - Concatenate stringsjoin(array, separator) - Join array elementsCustom functions:
const config = await configorama('config.yml', {
functions: {
// Custom function
add: (a, b) => a + b,
// Function with multiple args
between: (val, min, max) => val >= min && val <= max
}
})# Using custom functions
sum: ${add(5, 10)} # 15
value: 75
inRange: ${between(${value}, 50, 100)} # truePlugins ship in the repo under plugins/ and are opt-in: install their peer dependencies, then wire them into variableSources. Plugins are not required dependencies of configorama itself, so consumers who don't need them aren't paying for them.
Resolves CloudFormation stack output values. Single-region, multi-region, and multi-account.
# Default region, default AWS credentials
apiUrl: ${cf:my-stack.ApiUrl}
# Explicit region
westUrl: ${cf(us-west-2):west-stack.ApiUrl}
# Cross-account: 'prod' matches PROD_AWS_ACCESS_KEY_ID env vars
prodUrl: ${cf(prod:us-west-2):prod-stack.ApiUrl}const configorama = require('configorama')
const createCloudFormationResolver = require('configorama/plugins/cloudformation')
const cfResolver = createCloudFormationResolver({
defaultRegion: 'us-east-1',
})
const config = await configorama('config.yml', {
variableSources: [cfResolver]
})Full docs: plugins/cloudformation/README.md. Covers the env-var-prefix alias convention, the refcounted credential mutex for parallel-safe deploys, and the skipResolution mode for CI metadata extraction.
Peer dependency (install separately):
npm install @aws-sdk/client-cloudformation @aws-sdk/credential-providersThe primary async API for resolving configurations.
Signature:
function configorama<T = any>(
configPathOrObject: string | object,
settings?: ConfigoramaSettings
): Promise<T | ConfigoramaResult<T>>Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
configPathOrObject | string \| object | Yes | Path to config file or raw JavaScript object |
settings | ConfigoramaSettings | No | Configuration options |
Settings object:
interface ConfigoramaSettings {
options?: Record<string, any> // CLI flags for ${opt:xyz}
syntax?: string | RegExp // Custom variable syntax
configDir?: string // Working directory for relative paths
variableSources?: VariableSource[] // Custom variable resolvers
filters?: Record<string, Function> // Custom filter functions
functions?: Record<string, Function> // Custom functions
allowUnknownVariableTypes?: boolean | string[] // Allow unknown var types
allowUnresolvedVariables?: boolean | string[] // Allow unresolved vars
allowUndefinedValues?: boolean // Allow undefined in output
returnMetadata?: boolean // Return metadata with config
mergeKeys?: string[] // Keys to merge in arrays
filePathOverrides?: Record<string, string> // Override file paths
}Returns:
returnMetadata: false (default): Promise<T> - Resolved config objectreturnMetadata: true: Promise<ConfigoramaResult<T>> - Object with config and metadataExample:
const configorama = require('configorama')
// Basic usage
const config = await configorama('./config.yml')
// With options
const config = await configorama('./config.yml', {
options: { stage: 'prod', region: 'us-east-1' },
allowUnknownVariableTypes: ['ssm', 'cf']
})
// With metadata
const result = await configorama('./config.yml', {
returnMetadata: true,
options: { stage: 'prod' }
})
console.log(result.config) // Resolved config
console.log(result.metadata) // Variable metadata
console.log(result.resolutionHistory) // Resolution stepsSynchronous API for blocking config resolution.
Signature:
function configorama.sync<T = any>(
configPathOrObject: string | object,
settings?: ConfigoramaSettings
): TParameters:
Same as async API, but dynamicArgs cannot be a function (must be serializable).
Returns:
T - Resolved config object (synchronously)
Limitations:
dynamicArgs must be serializable (not a function)process.argv if options not providedExample:
const configorama = require('configorama')
// Basic sync usage
const config = configorama.sync('./config.yml')
// With options
const config = configorama.sync('./config.yml', {
options: { stage: 'dev' }
})const configorama = require('configorama')
const config = await configorama('config.yml')
const result = await configorama('config.yml', { returnMetadata: true })
const requirements = await configorama.inspect('config.yml', { view: 'requirements' })
const graph = await configorama.inspect('config.yml', { view: 'graph', format: 'mermaid' })
const configSync = configorama.sync('config.yml')The stable public surface is intentionally small:
| Method | Use it for |
|---|---|
configorama(fileOrObject, opts) | Async resolution. Set returnMetadata: true when you need metadata.variables, metadata.uniqueVariables, or metadata.fileDependencies. |
configorama.sync(fileOrObject, opts) | Synchronous resolution for blocking contexts. |
configorama.inspect(fileOrObject, opts) | Pre-resolution inspection. Use view: "requirements", view: "audit", or view: "graph" for one projection. |
configorama.format | Parser utilities for YAML, JSON/JSON5, TOML, INI, HCL, and Markdown frontmatter. |
Lower-level helpers still exist for compatibility: analyze(), introspect(), audit(), graph(), buildVariableSyntax(), and Configorama. New code should start with configorama(), configorama.sync(), or configorama.inspect().
returnMetadata: true remains the right API for tools that need resolved config plus dependency metadata:
const result = await configorama('serverless.yml', {
returnMetadata: true,
allowUnknownVariableTypes: true,
allowUnresolvedVariables: true,
options: { stage: 'prod' }
})
console.log(result.config)
console.log(result.metadata.variables)
console.log(result.metadata.uniqueVariables)
console.log(result.metadata.fileDependencies.resolvedPaths)
console.log(result.metadata.fileDependencies.globPatterns)That metadata shape is the same path used by the newer inspection APIs, so tools can keep consuming it without switching to inspect().
Inspect config structure without resolving missing user inputs.
Signature:
function configorama.inspect(
configPathOrObject: string | object,
settings?: ConfigoramaSettings & {
view?: 'requirements' | 'audit' | 'graph'
format?: 'json' | 'mermaid' | 'dot'
}
): Promise<object | string>With no view, inspect() returns the full model:
const model = await configorama.inspect('./config.yml')
console.log(model.requirements)
console.log(model.graph)
console.log(model.audit)Use view for one projection:
const configorama = require('configorama')
const requirements = await configorama.inspect('./config.yml', { view: 'requirements' })
const audit = await configorama.inspect('./config.yml', { view: 'audit' })
const graph = await configorama.inspect('./config.yml', { view: 'graph', format: 'mermaid' })The older analyze(), introspect(), audit(), and graph() helpers map to the same underlying inspection paths. They are kept for existing consumers.
Parse various config formats to JavaScript objects.
Available parsers:
const { format } = require('configorama')
// Parse YAML
const yamlObj = format.yaml.parse('key: value')
// Parse JSON (handles JSON5/JSONC too: comments, trailing commas)
const jsonObj = format.json.parse('{ key: "value", }')
// Parse TOML
const tomlObj = format.toml.parse('key = "value"')
// Parse INI
const iniObj = format.ini.parse('[section]\nkey=value')
// Parse HCL (requires @cdktf/hcl2json)
const hclObj = await format.hcl.parse('variable "example" { default = "value" }')Available parsers: format.json, format.yaml, format.toml, format.ini, format.hcl, format.markdown.
Each has at minimum a parse(content) method; dump(obj) / stringify(obj) and cross-format converters (e.g. format.yaml.toJson, format.toml.toYaml) are available where the underlying format supports them. format.markdown is a frontmatter parser; see Markdown Files below.
Real use cases for format:
const { format } = require('configorama')
const fs = require('fs')
const raw = format.yaml.parse(fs.readFileSync('config.yml', 'utf8'))
// raw is the YAML structure with ${...} strings intactMarkdown configs (.md, .mdx, .markdown, .mdown, .mkdn, .mkd) are parsed as YAML/TOML/JSON frontmatter + body. The frontmatter becomes top-level keys; the body is exposed as _content on the resolved config (or _body if the frontmatter used that key explicitly).
---
service: my-service
stage: ${opt:stage, 'dev'}
---
# Service Docs
This is the body content.Resolves to:
{
service: 'my-service',
stage: 'dev',
_content: '# Service Docs\nThis is the body content.'
}The body is detached during variable resolution (so ${…} inside the body text is left alone) and re-attached afterward; only frontmatter keys get variable expansion.
buildVariableSyntax(prefix, suffix, excludePatterns?)Helper for building a properly-escaped regex source string to pass to the syntax option. Handles regex-special characters in your delimiters without you having to escape them yourself.
const { buildVariableSyntax } = require('configorama')
// Use {{ ... }} instead of ${ ... }
const syntax = buildVariableSyntax('{{', '}}')
const config = await configorama('config.yml', { syntax })Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
prefix | string | '${' | Opening delimiter |
suffix | string | '}' | Closing delimiter |
excludePatterns | string[] | ['AWS', 'aws:', 'stageVariables'] | Patterns to exclude via negative lookahead (so e.g. ${AWS::Region} and ${aws:username} are left untouched by CloudFormation users) |
Configorama ClassFor advanced use cases (long-lived instances, hooking into init/resolve lifecycle, accessing partial state) the underlying class is exported.
const { Configorama } = require('configorama')
const instance = new Configorama('config.yml', { options: { stage: 'dev' } })
await instance.init({ stage: 'dev' })
const resolved = await instance.populateObject(instance.config)
const metadata = instance.collectVariableMetadata()Most users should prefer the top-level configorama() / .sync() / .analyze() functions, which are thin wrappers around this class.
configorama/parse-file SubpathFor tools that want to parse a config file (auto-detecting format from extension or contents) without going through variable resolution:
const { parseFile, parseFileContents } = require('configorama/parse-file')
// Read from disk
const raw = parseFile('./config.yml')
// returns the parsed object with ${...} strings intact
// Or parse already-loaded contents
const fromString = parseFileContents({
contents: 'service: my-app\nstage: ${opt:stage}',
filePath: 'in-memory.yml'
})Both are synchronous. Useful for build tools that inspect or rewrite configs before handing them to configorama.
Type definitions are bundled (index.d.ts). TypeScript users get:
configorama<MyConfig>('config.yml') returns Promise<MyConfig>ConfigoramaSettings and ConfigoramaResultimport configorama, { ConfigoramaSettings } from 'configorama'
interface MyConfig {
service: string
stage: string
database: { host: string; port: number }
}
const config = await configorama<MyConfig>('config.yml', { options: { stage: 'prod' } })
// config.database.port is typed as numberUse the syntax option to change the variable delimiters. You can provide a regex string directly or use buildVariableSyntax() to generate one with proper character escaping:
const configorama = require('configorama')
const { buildVariableSyntax } = require('configorama')
// Using buildVariableSyntax helper (recommended)
const config = await configorama(configFile, {
syntax: buildVariableSyntax('{{', '}}'), // Mustache-style: {{env:FOO}}
options: { stage: 'dev' }
})
// Other examples:
buildVariableSyntax('${{', '}}') // ${{env:FOO}}
buildVariableSyntax('#{', '}') // #{env:FOO}
buildVariableSyntax('[[', ']]') // [[env:FOO]]
buildVariableSyntax('<', '>') // <env:FOO>Function signature:
function buildVariableSyntax(
prefix: string = '${',
suffix: string = '}',
excludePatterns: string[] = ['AWS', 'aws:', 'stageVariables']
): stringThe buildVariableSyntax() function:
{ from valuesexcludePatterns is an array of strings to exclude via negative lookaheadExample with custom syntax:
const config = await configorama('config.yml', {
syntax: buildVariableSyntax('{{', '}}')
})# config.yml with {{ }} syntax
apiKey: {{env:API_KEY}}
stage: {{opt:stage, 'dev'}}
database: {{file(./db.yml)}}Controls what happens when encountering unregistered variable types (e.g., ${ssm:path} when ssm isn't a registered resolver).
Type: boolean | string[]
Default: false
Behavior:
// Allow ALL unknown types to pass through
const config = await configorama(configFile, {
allowUnknownVariableTypes: true,
options: { stage: 'dev' }
})
// Input: { key: '${ssm:/path/to/secret}' }
// Output: { key: '${ssm:/path/to/secret}' }
// Allow only SPECIFIC unknown types
const config = await configorama(configFile, {
allowUnknownVariableTypes: ['ssm', 'cf'], // only these pass through
options: { stage: 'dev' }
})
// ${ssm:path} and ${cf:stack.output} pass through
// ${custom:thing} throws an errorUse cases:
CloudFormation refs (
${cf:…},${cf(region):…},${cf(account:region):…}) are now resolved natively by the bundledplugins/cloudformation/plugin; no external resolver required.
Controls what happens when a known resolver can't find a value (missing env vars, missing files, etc.).
Type: boolean | string[]
Default: false
Behavior:
// Allow ALL unresolved variables to pass through
const config = await configorama(configFile, {
allowUnresolvedVariables: true,
options: { stage: 'dev' }
})
// Input: { key: '${env:MISSING_VAR}' }
// Output: { key: '${env:MISSING_VAR}' }
// Allow only SPECIFIC types to be unresolved
const config = await configorama(configFile, {
allowUnresolvedVariables: ['param', 'file'], // only these pass through
options: { stage: 'prod' }
})
// Input: { paramKey: '${param:x}', fileKey: '${file(missing.yml)}' }
// Output: { paramKey: '${param:x}', fileKey: '${file(missing.yml)}' }
// Mixed scenario
const config = await configorama(configFile, {
allowUnresolvedVariables: ['param', 'file'],
options: { stage: 'prod' }
})
// Input: {
// key: '${env:MISSING_VAR}',
// paramKey: '${param:x}',
// fileKey: '${file(missing.yml)}'
// }
// Output: Error thrown because ${env:MISSING_VAR} cannot resolve
// (param and file pass through, but env vars must resolve)Important notes:
self: or dotProp variables (e.g., ${foo.bar.baz})Use cases:
| Option | Type | Default | Description |
|---|---|---|---|
options | object | {} | CLI options/flags to populate ${option:xyz} / ${opt:xyz} variables |
syntax | string \| RegExp | ${...} | Custom variable syntax regex pattern |
configDir | string | directory of config file | Working directory for relative file paths |
variableSources | VariableSource[] | [] | Custom variable sources (see below) |
filters | Record<string, Function> | {} | Custom filter functions for pipe operator |
functions | Record<string, Function> | {} | Custom functions for ${fn(...)} syntax |
allowUnknownVariableTypes | boolean \| string[] | false | Allow unknown variable types to pass through |
allowUnresolvedVariables | boolean \| string[] | false | Allow known types that can't resolve to pass through |
allowUndefinedValues | boolean | false | Allow undefined as a valid end result |
ignorePaths | string[] | Built-in CloudFormation/code paths | Glob-like config paths whose values should be left verbatim |
skipResolutionPaths | string[] | [] | Alias for ignorePaths |
disableDefaultIgnorePaths | boolean | false | Disable the built-in CloudFormation/code ignore paths |
returnMetadata | boolean | false | Return { config, metadata } instead of just the resolved config |
returnPreResolvedVariableDetails | boolean | false | Return metadata about variables without resolving them (used by analyze()) |
useDotEnvFiles | boolean | false | Auto-load .env, .env.{stage}, etc. into process.env before resolution (via env-stage-loader) |
dotEnvSilent | boolean | true (unless --verbose) | Suppress the env-stage-loader log lines when useDotEnvFiles is on |
dotEnvDebug | boolean | false | Enable env-stage-loader debug output |
dynamicArgs | object \| Function | undefined | Values passed into .js/.ts config files when they're imported as the root config |
mergeKeys | string[] | [] | Keys to merge in arrays of objects |
filePathOverrides | Record<string, string> | {} | Map of file paths to override (for testing/mocking) |
The config file itself can also set
useDotenv: true(oruseDotEnv: true) at the top level to trigger dotenv loading. Useful when you want the behavior intrinsic to the config rather than the JS caller.
Legacy options (deprecated):
| Legacy Option | New Equivalent |
|---|---|
allowUnknownVars | allowUnknownVariableTypes |
allowUnknownVariables | allowUnknownVariableTypes |
allowUnknownParams | allowUnresolvedVariables: ['param'] |
allowUnknownFileRefs | allowUnresolvedVariables: ['file'] |
Bring your own variable sources.
The source property defines how the config wizard handles each variable type:
| Source | Description | Wizard Behavior | Examples |
|---|---|---|---|
'user' | Values provided by user at runtime | Prompt user for value | env, option / opt |
'config' | Values from config files or self-references | Check existence, can create | self, file, text |
'remote' | Values from external services | Fetch, prompt if missing, can write back | ssm, vault, consul |
'readonly' | Computed or system-derived values | Display only, cannot modify | git, cron, eval |
Built-in variable sources and their types:
| Variable | Source Type | Description |
|---|---|---|
${env:VAR} | user | Environment variables |
${option:flag} / ${opt:flag} | user | CLI option flags |
${param:key} | user | Parameter values |
${self:key} | config | Self references |
${file(path)} | config | File references |
${text(path)} | config | Raw text file references |
${git:branch} | readonly | Git repository data |
${cron(expr)} | readonly | Cron expression conversion |
${eval(expr)} | readonly | Math/logic evaluation |
${if(expr)} | readonly | Conditional expressions |
There are 2 ways to resolve variables from custom sources:
Use built-in JavaScript method for sync or async resolution.
Add your own variable syntax and resolver:
const configorama = require('configorama')
const config = await configorama('path/to/configFile', {
variableSources: [{
// Variable type name (used in metadata)
type: 'consul',
// Source type for config wizard behavior
source: 'remote',
// Prefix shown in syntax examples
prefix: 'consul',
// Example syntax for documentation
syntax: '${consul:path/to/key}',
// Description for help text
description: 'Resolves values from Consul KV store',
// Match variables ${consul:xyz}
match: RegExp(/^consul:/g),
// Custom variable source. Must return a promise
resolver: async (varToProcess, opts, currentObject) => {
// varToProcess = 'consul:path/to/key'
const consulPath = varToProcess.replace(/^consul:/, '')
// Make remote call to consul
const consulClient = require('consul')()
const result = await consulClient.kv.get(consulPath)
return result.Value
}
}]
})
console.log(config)This would match:
key: ${consul:path/to/my/key}Variable source interface:
interface VariableSource {
type: string // Type name (e.g., 'consul', 'ssm')
source: 'user' | 'config' | 'remote' | 'readonly'
prefix?: string // Prefix for examples (defaults to type)
syntax: string // Example syntax (e.g., '${consul:key}')
description?: string // Help text description
match: RegExp // Regex to match variables
resolver: ( // Resolution function
variable: string, // Variable string (e.g., 'consul:key')
options: object, // Options from configorama call
currentConfig: object // Current partially-resolved config
) => Promise<any>
collectMetadata?: () => any // Optional: collect custom metadata
metadataKey?: string // Optional: key for custom metadata
}Advanced example with AWS SSM:
const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm')
const ssm = new SSMClient({})
const config = await configorama('config.yml', {
variableSources: [{
type: 'ssm',
source: 'remote',
syntax: '${ssm:/path/to/parameter}',
description: 'Resolves values from AWS Systems Manager Parameter Store',
match: /^ssm:/,
resolver: async (variable, options, currentConfig) => {
const paramPath = variable.replace(/^ssm:/, '')
try {
const result = await ssm.send(new GetParameterCommand({
Name: paramPath,
WithDecryption: true
}))
return result.Parameter.Value
} catch (err) {
if (options.allowUnresolvedVariables) {
return `\${${variable}}` // Pass through unresolved
}
throw new Error(`SSM parameter not found: ${paramPath}`)
}
}
}]
})See also: the bundled
plugins/cloudformation/plugin is a working example of asource: 'remote'resolver. It handles multi-region and multi-account credential swapping, plus per-instance client and output caching.
# config.yml
database:
password: ${ssm:/myapp/prod/db-password}
apiKey: ${ssm:/myapp/prod/api-key}The config wizard walks you through every variable your config needs that isn't resolved yet, prompting for each one and then resolving the config with your answers.
Trigger it with the --setup flag, the setup subcommand, or the setup library option:
configorama config.yml --setup
configorama setup config.ymlconst config = await configorama('config.yml', { setup: true })The wizard groups unresolved variables by source and prompts for each:
${option:...} / ${opt:...} - CLI option flags (opt is shorthand)${env:...} - environment variables (shows the current process.env value if set)${self:...} and dot-prop references - values defined elsewhere in the configVariables whose names look sensitive (secret, password, token, key, etc.) are prompted with a masked password input. Use comment annotations for new metadata, or annotate any variable with the backward-compatible help() filter to show guidance during the prompt:
# Stripe live secret key
# @from Stripe dashboard > Developers > API keys
# @sensitive true
apiKey: ${env:API_KEY}
stage: ${opt:stage | help('Deployment stage, e.g. dev or prod')}The Variable Source Types table describes how the wizard treats each source.
Experimental: the wizard fills in values for the current resolution run only. It does not write your answers back to the config file yet.
Use the requirements inspection view when an agent or script needs to know what a config is missing without running the full resolver:
configorama inspect config.yml --view requirementsThe older requirements command and --requirements flag still work:
configorama requirements config.yml
configorama config.yml --requirementsAll three forms use analyze mode and print JSON. Missing required variables do not fail requirements output. The result is environment-aware: if process.env.API_KEY is already set, ${env:API_KEY} is removed from ask[].
{
"schemaVersion": 1,
"config": "config.yml",
"summary": {
"total": 2,
"required": 1,
"optional": 1,
"sensitive": 1
},
"requirements": [
{
"name": "API_KEY",
"variable": "env:API_KEY",
"variableType": "env",
"sourceClass": "user",
"type": "string",
"description": "Stripe live secret key",
"descriptionSource": "commentTag",
"allowedValues": null,
"sensitive": true,
"sensitiveSource": "commentTag",
"required": true,
"default": null,
"defaultHint": "Set in CI or local shell profile",
"obtainHint": "Stripe dashboard > Developers > API keys",
"examples": ["sk_live_..."],
"group": "Payments",
"deprecationMessage": "Use STRIPE_RESTRICTED_KEY instead",
"paths": ["apiKey"],
"conflicts": []
}
],
"ask": [
{
"name": "API_KEY",
"variable": "env:API_KEY",
"variableType": "env",
"type": "string",
"sensitive": true,
"description": "Stripe live secret key",
"obtainHint": "Stripe dashboard > Developers > API keys",
"examples": ["sk_live_..."],
"defaultHint": "Set in CI or local shell profile",
"group": "Payments",
"deprecationMessage": "Use STRIPE_RESTRICTED_KEY instead",
"paths": ["apiKey"],
"how": "Set environment variable API_KEY"
}
]
}variableType uses normalized names. CLI flags are reported as option for both ${option:stage} and the backward-compatible ${opt:stage} shorthand. Concrete missing ${file(...)} and ${text(...)} dependencies appear in ask[]; dynamic paths such as ${file(./${option:stage}.yml)} ask for the inner variables first.
inspect is the current CLI surface for pre-resolution analysis. It does not resolve missing user inputs. By default it returns the full model:
configorama inspect config.yml{
"schemaVersion": 1,
"config": "config.yml",
"requirements": {},
"graph": {},
"audit": {}
}Use --view when a script wants one projection:
# Required inputs and ask[] contract
configorama inspect config.yml --view requirements
# Static risk report for executable or external surfaces
configorama inspect config.yml --view audit
# Dependency graph as JSON, Mermaid, or Graphviz DOT
configorama inspect config.yml --view graph --format json
configorama inspect config.yml --view graph --format mermaid
configorama inspect config.yml --view graph --format dotInspection commands default to safe mode, which blocks executable and config-mutating surfaces during analysis. Use --unsafe only when you intentionally want the resolver to execute those surfaces during inspection. Use --safe-root <dir> or --file-root <dir> to restrict file and text references to known directories.
Agents can discover the CLI contract without scraping this README:
configorama capabilitiesThat command prints JSON containing the documented commands, compatibility aliases, views, output formats, flags, error codes, and exit codes.
Configorama includes a CLI tool for resolving configs, extracting paths, and inspecting config requirements before resolution.
# Resolve a config file
configorama config.yml
# Resolve and write to output file
configorama config.yml --output resolved.json
# Resolve with CLI options
configorama config.yml --stage prod --region us-east-1
# Show info about variables
configorama config.yml --info
# Verify config (check for errors without resolving)
configorama config.yml --verify
# Walk through unresolved variables interactively (experimental)
configorama config.yml --setup
configorama setup config.yml
# Inspect requirements, graph, and audit together
configorama inspect config.yml
# Print agent requirements JSON without resolving missing values
configorama inspect config.yml --view requirements
# Compatibility forms for requirements JSON
configorama requirements config.yml
configorama config.yml --requirements
# Audit risky references without resolving missing values
configorama inspect config.yml --view audit
# Print a dependency graph
configorama inspect config.yml --view graph --format mermaid
# Print the machine-readable CLI contract
configorama capabilities
# Extract a specific path from config
configorama config.yml .database.host
# Print an extracted scalar without JSON quotes
configorama config.yml .database.host --raw
# Copy the formatted output to your clipboard
configorama config.yml .database.host --raw --copy
# Output as YAML
configorama config.yml --format yamlUsage:
configorama [options] <file> [path]
configorama <command> <file> [options]
Commands:
(default) Resolve <file> and print the result
inspect <file> Introspect a config without resolving it
--view requirements|audit|graph for one slice
setup <file> Run the interactive config wizard (experimental)
capabilities Print the machine-readable CLI contract as JSON
Options:
-h, --help Show this help message
-v, --version Show version number
-o, --output <file> Write output to file instead of stdout
-f, --format <format> Output format: json, yaml, js (resolve);
json, mermaid, dot (graph)
--view <view> inspect view: requirements, audit, or graph
-r, --raw Print extracted scalar values without JSON quoting
-c, --copy Copy the formatted output to the clipboard
-d, --debug Enable debug mode
-i, --info Show info about the config
-V, --verify Verify the config
--safe Block executable/config-mutating surfaces during resolution
--unsafe Disable inspect/audit/graph default safe inspection
--safe-root <dir> Restrict file/text references to an allowed root
--file-root <dir> Alias for --safe-root
--error-format <fmt> Error format on stderr: json or human
--param <key=value> Pass parameter values (can be used multiple times)
--allow-unknown Allow unknown variables to pass through
--allow-undefined Allow undefined values in the final output
--setup Alias for configorama setup <file>
--requirements Compatibility form for requirements JSON
Path Extraction:
configorama config.yml .database.host Extract a nested value
configorama config.yml '.functions[0]' Extract from an array
configorama -r config.yml .stage Print raw scalar output
configorama -r -c config.yml .stage Print and copy raw scalar outputPath extraction uses jq-style paths. JSON remains the default output format, so extracted strings are quoted by default:
configorama config.yml .stage
# "prod"
configorama config.yml .stage --raw
# prod--copy copies exactly the formatted value that the CLI prints. It uses native clipboard commands where available: pbcopy on macOS, clip on Windows, and wl-copy, xclip, or xsel on Linux.
Structured commands (inspect, requirements, audit, graph, and capabilities) default to JSON errors on stderr. Use --error-format human for boxed terminal errors, or --error-format json to force machine-readable errors in resolve mode.
Basic resolution:
# Input: config.yml
apiKey: ${env:API_KEY}
stage: ${opt:stage, 'dev'}
# Command
export API_KEY=secret123
configorama config.yml --stage prod
# Output
{
"apiKey": "secret123",
"stage": "prod"
}With parameters:
configorama config.yml \
--stage prod \
--param "domain=myapp.com" \
--param "apiKey=secret123"Extract specific path:
# config.yml
database:
host: localhost
port: 5432
# Extract database.host as JSON
configorama config.yml .database.host
# Output: "localhost"
# Extract database.host as a raw scalar
configorama config.yml .database.host --raw
# Output: localhost
# Extract and copy the raw scalar
configorama config.yml .database.host --raw --copy
# Output: localhost
# Extract database config as JSON
configorama config.yml .database --format json
# Output: {"host":"localhost","port":5432}Output to file:
configorama config.yml --output resolved.json
configorama config.yml --output resolved.yml --format yamlShow variable info:
configorama config.yml --info
# Output:
# Found 15 variables
# env: 5
# opt: 3
# self: 4
# file: 2
# git: 1
#
# Required environment variables:
# - API_KEY
# - DB_HOST
# - DB_PASSWORD
#
# File dependencies:
# - ./secrets.yml
# - ./config/database.ymlVerify without resolving:
configorama config.yml --verify
# Output:
# ✓ Config structure valid
# ✓ No circular dependencies
# ✓ All file references exist
# ! Warning: 3 environment variables not set
# - API_KEY
# - DB_HOST
# - DB_PASSWORDAllow unknown variable types to pass through unresolved:
# ${ssm:...} and ${custom:...} stay as literal ${ssm:...} strings
# (typical for multi-stage pipelines where another tool resolves them)
configorama config.yml --allow-unknownAllow undefined values in the final output:
# Don't error on values that resolved to undefined; emit them as nulls
# (useful for downstream tooling that does its own validation)
configorama config.yml --allow-undefined# Run all tests
npm test
# Run only fast tests (excludes slow tests)
npm run test:lib
# Run API tests
npm run test:api
# Run tests in a specific directory
npm run test:tests
# Run slow tests only
npm run test:slow
# Watch mode (reruns on file changes)
npm run watch
# Type checking
npm run typechecktests/
├── _fixtures/ # Shared test fixtures
├── api/ # API tests
├── asyncValues/ # Async function resolution tests
├── syncValues/ # Sync function resolution tests
├── cronValues/ # Cron expression tests
├── gitVariables/ # Git variable tests
├── filePathOverrides/ # File path override tests
├── filterTests/ # Filter tests
├── hclTests/ # Terraform HCL tests
├── iniTests/ # INI format tests
├── tomlTests/ # TOML format tests
├── jsTests/ # JavaScript file tests
└── ... # More test categoriesConfigorama uses the uvu test framework. Tests can be run directly with Node.js:
# Run a single test file
node tests/api/api.test.jsExample test:
const { test } = require('uvu')
const assert = require('uvu/assert')
const path = require('path')
const configorama = require('../src')
test('resolves environment variables', async () => {
process.env.TEST_VAR = 'test-value'
const config = await configorama({
key: '${env:TEST_VAR}'
})
assert.equal(config.key, 'test-value')
delete process.env.TEST_VAR
})
test('handles missing env vars with defaults', async () => {
const config = await configorama({
key: '${env:MISSING_VAR, "default"}'
})
assert.equal(config.key, 'default')
})
test.run()Test utilities available at tests/utils.js:
const { getFixturePath, loadFixture } = require('./tests/utils')
// Get path to fixture file
const fixturePath = getFixturePath('config.yml')
// Load and parse fixture
const fixtureData = loadFixture('config.yml')Configorama can be used as a drop-in replacement for the Serverless Framework variable system.
serverless.js:
const path = require('path')
const configorama = require('configorama')
const args = require('minimist')(process.argv.slice(2))
// Path to serverless config to be parsed
const yamlFile = path.join(__dirname, 'serverless.config.yml')
module.exports = configorama.sync(yamlFile, { options: args })serverless.config.yml:
service: my-service
provider:
name: aws
runtime: nodejs22.x
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'us-east-1'}
# Environment-specific config
environment:
STAGE: ${opt:stage}
DB_HOST: ${env:DB_HOST}
API_KEY: ${ssm:/my-service/${opt:stage}/api-key}
custom:
# Load stage-specific config
stageConfig: ${file(./config/${opt:stage}.yml)}
# Git info for tracking
deploymentInfo:
branch: ${git:branch}
commit: ${git:sha1}
timestamp: ${timestamp}
functions:
api:
handler: handler.api
memorySize: ${if(${provider.stage} === 'prod' ? 1024 : 512)}
events:
- http:
path: /
method: ANYDeploy:
# Deploy to dev
serverless deploy --stage dev
# Deploy to production
serverless deploy --stage prod --region us-west-2GitHub Actions example (.github/workflows/deploy.yml):
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Verify config
run: npx configorama config.yml --verify
env:
STAGE: prod
DB_HOST: ${{ secrets.DB_HOST }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
API_KEY: ${{ secrets.API_KEY }}
- name: Run tests
run: npm test
- name: Deploy to production
run: npm run deploy
env:
STAGE: prod
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}GitLab CI example (.gitlab-ci.yml):
stages:
- verify
- test
- deploy
verify-config:
stage: verify
image: node:22
script:
- npm ci
- npx configorama config.yml --verify --stage $CI_ENVIRONMENT_NAME
variables:
STAGE: $CI_ENVIRONMENT_NAME
test:
stage: test
image: node:22
script:
- npm ci
- npm test
deploy-production:
stage: deploy
image: node:22
script:
- npm ci
- npm run deploy
environment:
name: production
only:
- main
variables:
STAGE: prodQ: Variable not resolving
# Problem
apiKey: ${env:API_KEY}
# Error: Environment variable 'API_KEY' not foundSolutions:
echo $API_KEY${env:API_KEY, 'default'}allowUnresolvedVariables: ['env']Q: Circular dependency error
# Problem
a: ${self:b}
b: ${self:a}
# Error: Circular variable dependency detected: b → a → bSolutions:
Q: File not found
# Problem
secrets: ${file(./secrets.yml)}
# Error: File not found: ./secrets.ymlSolutions:
ls -la secrets.yml${file(/absolute/path/to/secrets.yml)}${file(./secrets.yml), {}}Q: TypeScript file execution fails
# Problem
config: ${file(./config.ts)}
# Error: Cannot find module 'tsx'Solutions:
npm install tsx --save-devnpm install ts-node typescript --save-devexport = value or module.exports = valueQ: HCL parsing fails
# Problem
terraform: ${file(./main.tf)}
# Error: HCL parsing requires @cdktf/hcl2jsonSolutions:
npm install @cdktf/hcl2json.tf, .hcl, or .tf.jsonEnable debug mode to see detailed resolution steps:
CLI:
configorama config.yml --debugProgrammatic:
const config = await configorama('config.yml', {
returnMetadata: true
})
// Inspect resolution history
console.log(config.resolutionHistory)Environment variable:
DEBUG=configorama:* node app.jsOutput example:
configorama:resolve Resolving variable: ${env:API_KEY}
configorama:resolve Type: env, Name: API_KEY
configorama:resolve Resolved to: secret-key-123
configorama:resolve Resolving variable: ${opt:stage}
configorama:resolve Type: opt, Name: stage
configorama:resolve Resolved to: prodConfigorama detects circular dependencies and provides helpful error messages:
# Direct cycle
a: ${self:b}
b: ${self:a}Error:
Circular variable dependency detected: b → a → b
Resolution path:
1. Started resolving 'b'
2. Required 'a' (from ${self:b})
3. Required 'b' (from ${self:a})
4. Circular dependency detected
To fix this, restructure your config to break the circular reference.How to fix:
# Before (circular)
a: ${self:b}
b: ${self:a}
# After (fixed)
base: value
a: ${self:base}
b: ${self:base}# Before (circular)
apiUrl: ${self:baseUrl}/api
baseUrl: ${self:apiUrl}/v1
# After (fixed)
baseUrl: https://example.com
apiUrl: ${self:baseUrl}/api/v1# Before (circular)
database:
connectionString: postgres://${database.host}:${database.port}/${database.name}
host: ${self:database.connectionString}
# After (fixed)
database:
host: localhost
port: 5432
name: mydb
connectionString: postgres://${database.host}:${database.port}/${database.name}Q: What happens with circular variable dependencies?
Configorama detects circular dependencies and throws a helpful error instead of hanging forever. See Circular Dependencies section for examples and fixes.
Q: Why should I use this?
Configs resolve fresh on every run, so values stay in sync with your environment variables, CLI flags, file contents, and custom sources instead of going stale.
Q: Does this work with serverless.yml?
Yes! Use serverless.js as your main entry point. See Using with Serverless Framework for full example.
Q: Can I use this with other frameworks/tools?
Yes! Configorama is framework-agnostic. It works with any tool that accepts a JavaScript object or can import a .js file. Examples:
webpack.config.jsvite.config.jsjest.config.jseslint.config.jsQ: How do I handle secrets securely?
Best practices:
apiKey: ${env:API_KEY}secrets: ${file(./fetch-secrets.js)}// fetch-secrets.js
const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm')
const ssm = new SSMClient({})
module.exports = async () => {
const result = await ssm.send(new GetParameterCommand({
Name: '/myapp/api-key',
WithDecryption: true
}))
return result.Parameter.Value
}.gitignore for secret filesQ: Can I use variables in variable syntax?
Yes! Variables are resolved recursively:
stage: prod
configFile: config-${stage}.yml
config: ${file(${configFile})}
# Resolves: ${file(config-prod.yml)}Q: How do I migrate from Serverless Framework variables?
Configorama is mostly compatible with Serverless Framework variable syntax. Key differences:
# Serverless
key: ${self:other.key}
# Configorama (both work)
key: ${self:other.key}
key: ${other.key}# Configorama supports numeric defaults
timeout: ${env:TIMEOUT, 30}${cron()} - Cron expressions${eval()} - Math expressions${if()} - Conditionals${git:} - Git dataResolve configs in multiple stages, allowing external systems to handle remaining variables:
// Stage 1: Local resolution (resolve env, opt, file, etc.)
const partiallyResolved = await configorama('config.yml', {
options: { stage: 'prod' },
allowUnresolvedVariables: ['ssm', 'cf'],
allowUnknownVariableTypes: ['ssm', 'cf']
})
// Stage 2: External system resolves SSM and any other refs
// (e.g., Serverless Dashboard, secrets manager, etc.)
const fullyResolved = await externalResolver(partiallyResolved)Use case: Serverless Framework + Serverless Dashboard workflow.
For CloudFormation refs specifically, the bundled CF plugin resolves them natively in Stage 1, so you don't need a second pass.
Pass dynamic data from your config to JavaScript/TypeScript functions:
environment: prod
region: us-east-1
features:
enableMetrics: true
# Pass resolved config values as arguments
secrets: ${file(./get-secrets.js, ${environment}, ${region}, ${features})}get-secrets.js:
async function getSecrets(env, region, features, ctx) {
// Arguments from YAML
console.log(env) // 'prod'
console.log(region) // 'us-east-1'
console.log(features) // { enableMetrics: true }
// Context (always last argument)
console.log(ctx.options) // CLI options
console.log(ctx.originalConfig) // Original config
console.log(ctx.currentConfig) // Partially resolved config
// Fetch secrets based on arguments
if (env === 'prod') {
return await fetchProdSecrets(region)
}
return await fetchDevSecrets()
}
module.exports = getSecretsCustom variable resolver:
const configorama = require('configorama')
// Add custom AWS SSM resolver
const config = await configorama('config.yml', {
variableSources: [{
type: 'ssm',
source: 'remote',
syntax: '${ssm:/path}',
description: 'AWS Systems Manager Parameter Store',
match: /^ssm:/,
resolver: async (variable) => {
const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm')
const ssm = new SSMClient({})
const paramName = variable.replace(/^ssm:/, '')
const result = await ssm.send(new GetParameterCommand({
Name: paramName,
WithDecryption: true
}))
return result.Parameter.Value
}
}]
})Custom filters:
const config = await configorama('config.yml', {
filters: {
// Custom string transformation
slugify: (str) => str.toLowerCase().replace(/\s+/g, '-'),
// Custom formatting
currency: (amount) => `$${parseFloat(amount).toFixed(2)}`,
// Chained filters work
upperSnake: (str) => str.toUpperCase().replace(/\s+/g, '_')
}
})# Usage
projectName: My Awesome Project
slug: ${projectName | slugify} # 'my-awesome-project'
price: 19.99
displayPrice: ${price | currency} # '$19.99'
constantName: my constant
constName: ${constantName | upperSnake} # 'MY_CONSTANT'Custom functions:
const config = await configorama('config.yml', {
functions: {
// Timestamp generator
timestamp: () => new Date().toISOString(),
// Random ID generator
uuid: () => require('crypto').randomUUID(),
// Environment-based selector
selectByEnv: (prodValue, devValue, env) => {
return env === 'prod' ? prodValue : devValue
}
}
})# Usage
createdAt: ${timestamp()}
id: ${uuid()}
environment: ${opt:stage, 'dev'}
timeout: ${selectByEnv(30, 5, ${environment})}Configorama was forked from the Serverless Framework variable system and extended. The differences:
| Capability | Serverless | Configorama |
|---|---|---|
| Framework-agnostic; use outside Serverless | ❌ Serverless-only | ✅ Any tool, any framework |
| Pluggable variable sources | ❌ Hardcoded | ✅ Custom resolvers, custom syntax |
self: prefix optional in self-refs | ❌ Required | ✅ ${foo.bar} works without self: |
| Numbers as defaults | ❌ Coerced to string | ✅ ${env:TIMEOUT, 30} stays numeric |
| Format support | YAML/JSON | YAML, JSON/JSON5/JSONC, TOML, INI, HCL, Markdown, TS, JS |
| Filters (pipe transforms) | ❌ | ✅ ${value \| toUpperCase} |
| Built-in functions | ❌ | ✅ merge(), custom user functions |
| Conditional expressions | ❌ | ✅ ${if(cond ? a : b)} |
| Eval/math expressions | ❌ | ✅ ${eval(2 + 2)} |
| TypeScript file refs | ❌ | ✅ ${file(./config.ts)} |
| Git data refs | ❌ | ✅ ${git:branch}, ${git:sha1}, etc. |
| Cron expression refs | ❌ | ✅ ${cron(every monday at 9am)} |
| Metadata extraction (analyze without resolving) | ❌ | ✅ configorama.analyze(...) |
| Multi-account CloudFormation refs | ❌ | ✅ via bundled CF plugin |
| Circular dependency detection | ❌ Hangs | ✅ Helpful error |
How configorama compares to other variable-substitution libraries:
| Library | Formats | Variable sources | Custom resolvers | Async | TypeScript |
|---|---|---|---|---|---|
| configorama | YAML, JSON5, TOML, INI, HCL, MD, TS, JS | env, opt, file, self, git, cron, eval, if, custom | ✅ | ✅ | ✅ |
| sls-yaml | YAML | env, opt, file, self | ❌ | ❌ | ❌ |
| yaml-boost | YAML | env, file, self, function | partial | ❌ | ❌ |
| serverless-merge-config | YAML | merge-focused | ❌ | ❌ | ❌ |
| serverless-terraform-variables | YAML + .tfvars | terraform-focused | ❌ | ❌ | ❌ |
Version history lives in CHANGELOG.md. It covers everything from 0.9.9 onward; older releases are in git log.
This is forked from the Serverless Framework variable system.
erikerikson, eahefnawy, HyperBrain, ac360, gcphost, pmuens, horike37, lorengordon, AndrewFarley, tobyhede, johncmckim, mangas, e-e-e, BasileTrujillo, miltador, sammarks, RafalWilinski, indieisaconcept, svdgraaf, infiniteluke, j0k3r, craigw, bsdkurt, aoskotsky-amplify, and all the other folks who contributed to the variable system.
Additionally these tools were very helpful:
MIT © David Wells
Bug reports and reproductions are very welcome. Please open an issue with a minimal failing config. PRs are reviewed case-by-case; small targeted fixes with a test case are most likely to land quickly.