Skip to content

DavidWells/configorama

Repository files navigation

Configorama

Dynamic configuration values with variable support.

Works with yml, json, toml config formats and anything that parsed down to a plain ol' javascript object

About

Configorama extends your configuration with a powerful variable system. It resolves configuration variables from:

  • CLI options
  • ENV variables
  • File references
  • TypeScript file references
  • Self references (other keys/values in config)
  • Git references
  • Cron values
  • Async/sync JS functions
  • Filters (experimental)
  • Functions (experimental)
  • Any source you'd like...

See tests for more examples.

Table of Contents

Click to expand

Usage

Async API:

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

const config = await configorama(myConfigFilePath, {
  options: args
})

Sync API:

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

const config = configorama.sync(myConfigFilePath, {
  options: cliFlags
})

Variable Sources

Environment variables

apiKey: ${env:SECRET_KEY}

# Fallback to default value if env var not found
apiKeyWithFallback: ${env:SECRET_KEY, 'defaultApiKey'}

CLI option flags

# CLI option. Example `cmd --stage dev` makes `bar: dev`
bar: ${opt:stage}

# Composed example makes `foo: dev-hello`
foo: ${opt:stage}-hello

# You can also provide a default value. If no --stage flag is provided, it will use 'dev'
foo: ${opt:stage, 'dev'}

Self references

foo: bar

zaz:
  matazaz: 1
  wow:
    cool: 2

# Self file reference. Resolves to `bar`
one: ${self:foo}

# Shorthand self reference. Resolves to `bar`
two: ${foo} 

# Dot prop reference will traverse the object. Resolves to `2`
three: ${zaz.wow.cool}

File references

# Import full yml/json/toml file via relative path
fileRef: ${file(./subFile.yml)}

# Import sub values from files. This imports other-config.yml `topLevel:` value
fileValue: ${file(./other-config.yml):topLevel}

# Import sub values from files. This imports other-config.json `nested.value` value
fileValueSubKey: ${file(./other-config.json):nested.value}

# Fallback to default value if file not found
fallbackValueExample: ${file(./not-found.yml), 'fall back value'}

Sync/Async file references

asyncJSValue: ${file(./async-value.js)}
# resolves to 'asyncval'

${file(./asyncValue.js)} will call into async-value and run/resolve the async function with values. These values can be strings, objects, arrays, whatever.

/* async-value.js */
function delay(t, v) {
  return new Promise((resolve) => setTimeout(resolve.bind(null, v), t))
}

async function fetchSecretsFromRemoteStore(config) {
  await delay(1000)
  return 'asyncval'
}

module.exports = fetchSecretsFromRemoteStore

TypeScript file references

Configure with full TypeScript support using modern tsx execution engine with ts-node fallback.

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

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: '${opt:stage, "development"}',
    database: {
      host: '${env:DB_HOST, "localhost"}',
      port: parseInt('${env:DB_PORT, "5432"}'),
      database: '${env:DB_NAME, "myapp"}',
      ssl: '${env:NODE_ENV}' === 'production'
    },
    api: {
      baseUrl: '${env:API_BASE_URL, "http://localhost:3000"}',
      timeout: 5000,
      retries: 3
    },
    features: {
      enableNewFeature: '${opt:stage}' === 'production',
      debugMode: '${env:DEBUG, "false"}' === '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 like fetching from 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"}
  
  # You can also 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"
  timestamp: ${timestamp}
  
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}

Installation Requirements:

TypeScript support requires either tsx (recommended) or ts-node:

# Recommended: Modern, fast TypeScript execution
npm install tsx --save-dev

# Alternative: Traditional ts-node approach
npm install ts-node typescript --save-dev

Features:

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

Git references

Resolve values from cwd 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')}

Cron Values

Convert human-readable time expressions into cron expressions. Supports single quotes for values.

# 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
customCron: ${cron('15 2 * * *')}           # 15 2 * * *

Filters (experimental)

Filters will transform the resolved variables

toUpperCaseString: ${'value' | toUpperCase }

toKebabCaseString: ${'valueHere' | toKebabCase }

key: lol_hi

keyTwo: lol_hi

toKebabCase: ${key | toKebabCase }

toCamelCase: ${keyTwo | toCamelCase }

Functions (experimental)

Functions will convert resolved config values with various methods.

object:
  one: once
  two: twice

objectTwo:
  three: third
  four: fourth

mergeObjects: ${merge(${object}, ${objectTwo})}

More Examples

See the tests folder for a bunch of examples!

Custom Variable Sources

Configorama allows you to bring your own variable sources.

There are 2 ways to resolve variables from custom sources.

  1. Use the baked in javascript method for sync or aysnc resolution.

  2. Add your own variable syntax and resolver.

    const config = configorama('path/to/configFile', {
      variableSources: [{
        // Match variables ${consul:xyz}
        match: RegExp(/^consul:/g),
        // Custom variable source. Must return a promise
        resolver: (varToProcess, opts, currentObject) => {
          // Make remote call to consul
          return Promise.resolve(varToProcess)
        }
      }]
    })
    console.log(config)

    This would match the following config:

    key: ${consul:xyz}

FAQ

Q: Why should I use this?

Never rendering a stale configuration file again!

Q: Does this work with serverless.yml

Yes it does. Using serverless.js as your main entry point!

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

Whats new

How is this different than the serverless variable system?

  1. You can use it with any other tool you'd like. Just include configorama and go nuts.

  2. It's pluggable. Add whatever variable syntax/sources you wish.

  3. Filters! You can filter values before they are resolved.

    key: ${opt:stage | toUpperCase}
  4. Cleaner self references

    keyOne:
      subKey: hi
    
    # Before
    key: ${self:keyOne.subKey}
    
    # Now
    key: ${keyOne.subKey}
  5. Numbers as defaults are supported

    key: ${env:whatever, 2}
  6. TOML, YML, JSON, etc support

    Configorama will work on any configuration format that can be converted into a JS object.

    Parse any config format and pass it into configorama.

  7. Configorama has a number of built-in functions.

    Build in functions can be used within expressions as another way to transform and combine values. These are similar to the operators but all follow a common syntax:

    <FUNCTION NAME>(<ARGUMENT 1>, <ARGUMENT 2>)
    

    example:

    ${merge('one', 'two')} => 'onetwo'
    

Alt libs

Inspiration

This is forked out of 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:

About

⚙️ ${variable} support for config files

Resources

Security policy

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Contributors 2

  •  
  •