Configorama

January 29, 2026 · View on GitHub

Dynamic configuration values with variable support for yml, json, toml, hcl (Terraform), and other config formats.

Configorama extends your configuration with a powerful variable system that resolves values from CLI options, environment variables, file references, TypeScript/JavaScript files, git data, self-references, conditional expressions, and any custom source you define.

Key Features

  • Multiple file formats - yml, yaml, json, toml, ini, hcl (Terraform), TypeScript, JavaScript, markdown
  • Rich variable sources - env vars, CLI flags, file refs, git data, cron expressions, eval/if expressions
  • Async/sync function execution - Import and execute JavaScript/TypeScript files with argument passing
  • Self-referencing - Reference other values within the same config using dot notation
  • Custom variable sources - Pluggable architecture to add your own variable resolvers
  • Filters and functions - Transform and combine values with built-in or custom operators
  • Metadata extraction - Analyze configs without resolving them, or get full resolution history
  • Circular dependency detection - Helpful error messages instead of infinite loops
  • TypeScript support - Full type definitions and TypeScript file execution via tsx/ts-node

Table of Contents

Click to expand

Getting Started

Installation

As a library dependency:

npm install configorama

As a global CLI tool:

npm install -g configorama

Quick Start

Async 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 config

Sync 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 config

Example 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}

Running Examples

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.js

How It Works

Resolution Flow

Configorama 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:

  1. Load - Read config file from disk or accept JavaScript object
  2. Parse - Convert to JavaScript object (format auto-detected by extension)
  3. Preprocess - Identify all variables and build dependency graph
  4. Traverse - Recursively resolve variables in dependency order
  5. Post-process - Apply filters and functions
  6. Return - Fully resolved configuration object

Analyzing Without Resolving

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:

  • Validate config structure before deployment
  • Generate documentation of required environment variables
  • Build dependency graphs for complex configs
  • Audit what external resources a config depends on

Getting Metadata

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 path

Metadata 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 }
    ]
  }
}

Variable Sources

Configorama supports multiple variable sources out of the box. All variable syntax follows the pattern ${type:value} or ${type(value)}.

Summary Table

VariableSyntaxDescriptionExample
env${env:VAR}Environment variables${env:NODE_ENV}
opt${opt:flag}CLI option flags${opt: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')}

Environment Variables

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:

  • Reads from process.env at resolution time
  • Supports default values with comma syntax
  • Throws error if env var not found and no default provided (unless allowUnresolvedVariables is set)

CLI usage:

# Set env var then run
SECRET_KEY=abc123 node app.js

# Or export first
export SECRET_KEY=abc123
node app.js

CLI Option Flags

Access 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:

  • Reads from the options object passed to configorama
  • Typically populated from CLI args using minimist or similar parser
  • Supports dot-notation for nested option paths

Example:

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-1

Parameter Values

Access parameter values via ${param:key}. Parameters follow a resolution hierarchy:

  1. CLI params (--param="key=value") - highest priority
  2. Stage-specific params (stages.<stage>.params)
  3. Default 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: 3306

CLI 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'

Self References

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:

  • Uses dot-notation for nested object access
  • Supports array index access with bracket notation
  • Resolves in dependency order (referenced values resolved first)
  • Detects circular references and throws helpful errors

File References

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):

TypeExtensions
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:

  • Relative paths resolved from config file's directory
  • Absolute paths supported
  • ~ 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)}

Sync/Async File References

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 = fetchSecretsFromRemoteStore

Sync function example (sync-value.js):

function getEnvironmentConfig() {
  return {
    timeout: 5000,
    retries: 3,
    logLevel: process.env.NODE_ENV === 'production' ? 'error' : 'debug'
  }
}

module.exports = getEnvironmentConfig

Passing Arguments to Functions

You 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 = fetchSecrets

ConfigContext

The ctx parameter (always the last argument) provides access to:

PropertyDescription
originalConfigThe original unresolved configuration object
currentConfigThe current (partially resolved) configuration
optionsOptions passed to configorama (populates ${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 = fetchSecrets

Functions Without Arguments

If 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 = getSecrets

TypeScript File References

Execute 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-dev

Usage 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 = createConfig

TypeScript 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 = fetchSecretsFromVault

Complete 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:

  • Modern tsx execution (fast, no compilation) with ts-node fallback
  • Support for both sync and async TypeScript functions
  • Function argument passing via config variables
  • Full TypeScript interface support
  • Comprehensive error handling with helpful dependency messages

Terraform HCL Support

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/hcl2json

Supported file types:

  • .tf - Terraform configuration files
  • .hcl - Generic HCL files
  • .tf.json - Terraform JSON configuration files

Example:

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)    // Outputs

Importing 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.


Git References

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:

  • Reads git data from .git directory in current working directory or parent directories
  • Executes git commands via child process
  • Throws error if not in a git repository

Cron Values

Convert 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/days
  • at HH:MM [am/pm]
  • on [weekday] at HH:MM
  • midnight, noon, weekdays
  • Standard cron syntax (passed through unchanged)

Eval Expressions

Evaluate 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})}  # true

Supported operators:

CategoryOperators
Arithmetic+ - * / %
Comparison== != === !== > < >= <=
Logical&& || !
Grouping( )

Security:

  • Does NOT use JavaScript's eval()
  • Uses safe expression parser (subscript)
  • No access to global scope or functions
  • Only mathematical and logical operations allowed

If Expressions

Conditional expressions using ternary syntax. This is an alias for eval with a more intuitive 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)} # false

Supported operators:

CategoryOperators
Comparison== != === !== > < >= <=
Logical&& || !
Nullish??
Ternarycondition ? "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}

Filters (Experimental)

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 uppercase
  • toLowerCase - Convert to lowercase
  • toKebabCase - Convert to kebab-case
  • toCamelCase - Convert to camelCase

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 '

Functions (Experimental)

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 objects
  • concat(str1, str2, ...) - Concatenate strings
  • join(array, separator) - Join array elements

Custom 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)}  # true

API Reference

Async API

The primary async API for resolving configurations.

Signature:

function configorama<T = any>(
  configPathOrObject: string | object,
  settings?: ConfigoramaSettings
): Promise<T | ConfigoramaResult<T>>

Parameters:

ParameterTypeRequiredDescription
configPathOrObjectstring | objectYesPath to config file or raw JavaScript object
settingsConfigoramaSettingsNoConfiguration 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:

  • If returnMetadata: false (default): Promise<T> - Resolved config object
  • If returnMetadata: true: Promise<ConfigoramaResult<T>> - Object with config and metadata

Example:

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 steps

Sync API

Synchronous API for blocking config resolution.

Signature:

function configorama.sync<T = any>(
  configPathOrObject: string | object,
  settings?: ConfigoramaSettings
): T

Parameters:

Same as async API, but dynamicArgs cannot be a function (must be serializable).

Returns:

T - Resolved config object (synchronously)

Limitations:

  • Cannot use async functions in JavaScript/TypeScript file references
  • dynamicArgs must be serializable (not a function)
  • CLI args automatically parsed from process.argv if options not provided

Example:

const configorama = require('configorama')

// Basic sync usage
const config = configorama.sync('./config.yml')

// With options
const config = configorama.sync('./config.yml', {
  options: { stage: 'dev' }
})

Analyze API

Analyze config structure without resolving variables.

Signature:

function configorama.analyze(
  configPathOrObject: string | object,
  settings?: ConfigoramaSettings
): Promise<AnalyzeResult>

Returns:

interface AnalyzeResult {
  originalConfig: object         // Raw config object
  variables: Variable[]          // All variables found
  uniqueVariables: Record<string, Variable[]>  // Variables grouped by name
  fileDependencies: string[]     // File references
}

interface Variable {
  variable: string               // Full variable syntax (e.g., '${env:KEY}')
  variableType: string           // Type (e.g., 'env', 'opt', 'file')
  variableName: string           // Name/path (e.g., 'KEY')
  variablePath: string           // Location in config (e.g., 'database.host')
  defaultValue: any              // Default value if provided
  hasDefault: boolean            // Whether default exists
}

Example:

const configorama = require('configorama')

const analysis = await configorama.analyze('./config.yml')

console.log(`Found ${analysis.variables.length} variables`)
console.log(`File dependencies:`, analysis.fileDependencies)

// List all environment variables required
const envVars = analysis.variables
  .filter(v => v.variableType === 'env' && !v.hasDefault)
  .map(v => v.variableName)

console.log('Required env vars:', envVars)

Use cases:

  • Generate documentation of required environment variables
  • Validate config structure in CI/CD
  • Build dependency graphs
  • Audit external dependencies before resolution

Format Utilities

Parse various config formats to JavaScript objects.

Available parsers:

const { format } = require('configorama')

// Parse YAML
const yamlObj = format.yaml.parse('key: value')

// Parse JSON5
const jsonObj = format.json5.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" }')

Parser methods:

Each parser has:

  • parse(content) - Parse string to JavaScript object
  • stringify(obj) - Convert JavaScript object to format string (if supported)

Configuration Options

Custom Variable Syntax

Use 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', 'stageVariables']
): string

The buildVariableSyntax() function:

  • Automatically excludes suffix characters from the allowed character class (prevents parsing issues)
  • Supports nested variables by excluding $ and { from values
  • Third parameter excludePatterns is an array of strings to exclude via negative lookahead

Example 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)}}

allowUnknownVariableTypes

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 error

Use cases:

  • Multi-stage resolution (local resolution, then cloud provider resolves remaining vars)
  • Serverless Framework integration (let framework resolve SSM, CloudFormation refs)
  • Gradual migration (allow unknown types during transition period)

allowUnresolvedVariables

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:

  • This option does NOT apply to self: or dotProp variables (e.g., ${foo.bar.baz})
  • Self-references are local config errors, not external dependencies
  • Useful for multi-stage resolution pipelines

Use cases:

  • Serverless Dashboard resolves params after local resolution
  • Gradual migration with optional external dependencies
  • Development mode where some services are unavailable

Complete Options Reference

OptionTypeDefaultDescription
optionsobject{}CLI options/flags to populate ${opt:xyz} variables
syntaxstring | RegExp${...}Custom variable syntax regex pattern
configDirstringdirectory of config fileWorking directory for relative file paths
variableSourcesVariableSource[][]Custom variable sources (see below)
filtersRecord<string, Function>{}Custom filter functions for pipe operator
functionsRecord<string, Function>{}Custom functions for ${fn(...)} syntax
allowUnknownVariableTypesboolean | string[]falseAllow unknown variable types to pass through
allowUnresolvedVariablesboolean | string[]falseAllow known types that can't resolve to pass through
allowUndefinedValuesbooleanfalseAllow undefined as a valid end result
returnMetadatabooleanfalseReturn both config and metadata about variables
mergeKeysstring[][]Keys to merge in arrays of objects
filePathOverridesRecord<string, string>{}Map of file paths to override (for testing/mocking)

Legacy options (deprecated):

Legacy OptionNew Equivalent
allowUnknownVarsallowUnknownVariableTypes
allowUnknownVariablesallowUnknownVariableTypes
allowUnknownParamsallowUnresolvedVariables: ['param']
allowUnknownFileRefsallowUnresolvedVariables: ['file']

Custom Variable Sources

Configorama allows you to bring your own variable sources.

Variable Source Types

The source property defines how the config wizard handles each variable type:

SourceDescriptionWizard BehaviorExamples
'user'Values provided by user at runtimePrompt user for valueenv, opt
'config'Values from config files or self-referencesCheck existence, can createself, file, text
'remote'Values from external servicesFetch, prompt if missing, can write backssm, vault, consul
'readonly'Computed or system-derived valuesDisplay only, cannot modifygit, cron, eval

Built-in variable sources and their types:

VariableSource TypeDescription
${env:VAR}userEnvironment variables
${opt:flag}userCLI option flags
${param:key}userParameter values
${self:key}configSelf references
${file(path)}configFile references
${text(path)}configRaw text file references
${git:branch}readonlyGit repository data
${cron(expr)}readonlyCron expression conversion
${eval(expr)}readonlyMath/logic evaluation
${if(expr)}readonlyConditional expressions

Creating a Custom Resolver

There are 2 ways to resolve variables from custom sources:

  1. Use built-in JavaScript method for sync or async resolution.

  2. 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 AWS = require('aws-sdk')
const ssm = new AWS.SSM()

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.getParameter({
          Name: paramPath,
          WithDecryption: true
        }).promise()

        return result.Parameter.Value
      } catch (err) {
        if (options.allowUnresolvedVariables) {
          return `\${${variable}}` // Pass through unresolved
        }
        throw new Error(`SSM parameter not found: ${paramPath}`)
      }
    }
  }]
})
# config.yml
database:
  password: ${ssm:/myapp/prod/db-password}
  apiKey: ${ssm:/myapp/prod/api-key}

CLI Usage

Configorama includes a CLI tool for resolving configs from the command line.

Basic Commands

# 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

# Extract specific path from config
configorama config.yml database.host

# Output as YAML
configorama config.yml --format yaml

Command Options

Usage:
  configorama [options] <file> [path]

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, or js (default: json)
  -d, --debug               Enable debug mode
  -i, --info                Show info about the config
  -V, --verify              Verify the config
  --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

Path Extraction:
  configorama config.yml database.host    Extract specific value
  configorama config.yml functions[0]     Extract from array

CLI Examples

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
configorama config.yml database.host
# 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 yaml

Show 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.yml

Verify 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_PASSWORD

Testing

Running Tests

# 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 typecheck

Test Structure

tests/
├── _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 categories

Writing Tests

Configorama uses the uvu test framework. Tests can be run directly with Node.js:

# Run a single test file
node tests/api/api.test.js

Example 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')

Deployment

Using with Serverless Framework

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: nodejs18.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: ANY

Deploy:

# Deploy to dev
serverless deploy --stage dev

# Deploy to production
serverless deploy --stage prod --region us-west-2

Docker Deployment

Dockerfile:

FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application files
COPY . .

# Set environment variables (can be overridden at runtime)
ENV NODE_ENV=production
ENV STAGE=prod

# Run config resolution at build time (optional)
# RUN node -e "require('configorama').sync('./config.yml', { options: { stage: process.env.STAGE } })"

CMD ["node", "index.js"]

docker-compose.yml:

version: '3.8'

services:
  app:
    build: .
    environment:
      - NODE_ENV=production
      - STAGE=prod
      - DB_HOST=postgres
      - DB_PASSWORD=${DB_PASSWORD}
      - API_KEY=${API_KEY}
    depends_on:
      - postgres

  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}

Usage:

# Build
docker build -t myapp .

# Run with environment variables
docker run \
  -e DB_HOST=mydb.example.com \
  -e DB_PASSWORD=secret \
  -e API_KEY=abc123 \
  myapp

CI/CD Integration

GitHub Actions example (.github/workflows/deploy.yml):

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - 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:18
  script:
    - npm ci
    - npx configorama config.yml --verify --stage $CI_ENVIRONMENT_NAME
  variables:
    STAGE: $CI_ENVIRONMENT_NAME

test:
  stage: test
  image: node:18
  script:
    - npm ci
    - npm test

deploy-production:
  stage: deploy
  image: node:18
  script:
    - npm ci
    - npm run deploy
  environment:
    name: production
  only:
    - main
  variables:
    STAGE: prod

Troubleshooting

Common Issues

Q: Variable not resolving

# Problem
apiKey: ${env:API_KEY}
# Error: Environment variable 'API_KEY' not found

Solutions:

  1. Ensure environment variable is set: echo $API_KEY
  2. Check variable name spelling
  3. Use default value: ${env:API_KEY, 'default'}
  4. Allow unresolved vars: allowUnresolvedVariables: ['env']

Q: Circular dependency error

# Problem
a: ${self:b}
b: ${self:a}
# Error: Circular variable dependency detected: b → a → b

Solutions:

  1. Restructure config to remove circular reference
  2. Use intermediate values to break the cycle
  3. Consider if one value should be a constant instead

Q: File not found

# Problem
secrets: ${file(./secrets.yml)}
# Error: File not found: ./secrets.yml

Solutions:

  1. Check file path is correct relative to config file
  2. Ensure file exists: ls -la secrets.yml
  3. Use absolute path: ${file(/absolute/path/to/secrets.yml)}
  4. Add default: ${file(./secrets.yml), {}}

Q: TypeScript file execution fails

# Problem
config: ${file(./config.ts)}
# Error: Cannot find module 'tsx'

Solutions:

  1. Install tsx: npm install tsx --save-dev
  2. Or install ts-node: npm install ts-node typescript --save-dev
  3. Ensure TypeScript file exports correctly: export = value or module.exports = value

Q: HCL parsing fails

# Problem
terraform: ${file(./main.tf)}
# Error: HCL parsing requires @cdktf/hcl2json

Solutions:

  1. Install dependency: npm install @cdktf/hcl2json
  2. Ensure HCL file is valid Terraform syntax
  3. Check file extension is .tf, .hcl, or .tf.json

Debug Mode

Enable debug mode to see detailed resolution steps:

CLI:

configorama config.yml --debug

Programmatic:

const config = await configorama('config.yml', {
  returnMetadata: true
})

// Inspect resolution history
console.log(config.resolutionHistory)

Environment variable:

DEBUG=configorama:* node app.js

Output 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: prod

Circular Dependencies

Configorama 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:

  1. Use intermediate values:
# Before (circular)
a: ${self:b}
b: ${self:a}

# After (fixed)
base: value
a: ${self:base}
b: ${self:base}
  1. Make one value a constant:
# Before (circular)
apiUrl: ${self:baseUrl}/api
baseUrl: ${self:apiUrl}/v1

# After (fixed)
baseUrl: https://example.com
apiUrl: ${self:baseUrl}/api/v1
  1. Restructure dependencies:
# 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}

FAQ

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?

Never render a stale configuration file again! Configorama ensures your configs are always up-to-date with the latest environment variables, CLI flags, file contents, and custom sources.


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: webpack.config.js
  • Vite: vite.config.js
  • Jest: jest.config.js
  • ESLint: eslint.config.js
  • Docker Compose: Generate yaml from resolved config
  • Kubernetes: Generate manifests from resolved config

Q: How do I handle secrets securely?

Best practices:

  1. Use environment variables:
apiKey: ${env:API_KEY}
  1. Fetch from secret managers:
secrets: ${file(./fetch-secrets.js)}
// fetch-secrets.js
const AWS = require('aws-sdk')
const ssm = new AWS.SSM()

module.exports = async () => {
  const result = await ssm.getParameter({
    Name: '/myapp/api-key',
    WithDecryption: true
  }).promise()

  return result.Parameter.Value
}
  1. Never commit secrets to version control
  2. Use .gitignore for secret files
  3. Rotate secrets regularly

Q: 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:

  1. Cleaner self-references:
# Serverless
key: ${self:other.key}

# Configorama (both work)
key: ${self:other.key}
key: ${other.key}
  1. Numbers as defaults:
# Configorama supports numeric defaults
timeout: ${env:TIMEOUT, 30}
  1. Additional variable types:
  • ${cron()} - Cron expressions
  • ${eval()} - Math expressions
  • ${if()} - Conditionals
  • ${git:} - Git data

Advanced Usage

Multi-Stage Resolution

Resolve 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 CloudFormation refs
// (e.g., Serverless Dashboard, AWS CloudFormation, etc.)
const fullyResolved = await externalResolver(partiallyResolved)

Use case: Serverless Framework + Serverless Dashboard workflow.


Function Arguments and Context

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 = getSecrets

Programmatic Usage

Custom 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 AWS = require('aws-sdk')
      const ssm = new AWS.SSM()

      const paramName = variable.replace(/^ssm:/, '')
      const result = await ssm.getParameter({
        Name: paramName,
        WithDecryption: true
      }).promise()

      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})}

What's New

How is this different than the Serverless Framework variable system?

  1. Framework-agnostic - Use with any tool, not just Serverless Framework

  2. Pluggable - Add custom variable syntax and sources easily

  3. Filters - Transform values before resolution:

    key: ${opt:stage | toUpperCase}
    
  4. Cleaner self-references - No need for self: prefix:

    keyOne:
      subKey: hi
    
    # Before
    key: ${self:keyOne.subKey}
    
    # Now (both work)
    key: ${keyOne.subKey}
    key: ${self:keyOne.subKey}
    
  5. Numbers as defaults - Numeric defaults fully supported:

    timeout: ${env:TIMEOUT, 30}
    port: ${opt:port, 3000}
    
  6. Multiple format support - TOML, YML, JSON, INI, HCL, etc.

  7. Built-in functions - Combine and transform values:

    merged: ${merge(${obj1}, ${obj2})}
    
  8. TypeScript support - Execute TypeScript files directly:

    config: ${file(./config.ts)}
    
  9. Conditional expressions - If/else logic in configs:

    memory: ${if(${stage} === 'prod' ? 1024 : 512)}
    
  10. Metadata extraction - Analyze configs without resolving:

    const meta = await configorama.analyze('config.yml')
    

Alternative Libraries


Inspiration

This is forked from the Serverless Framework variable system.

Mad props to:

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:


License

MIT © David Wells

Contributing

Contributions welcome! Please read the contributing guidelines first.

Support