omniconfig.js
TypeScript icon, indicating that this package has built-in type declarations

1.2.0 • Public • Published

OmniConfig

OmniConfig is a universal runtime configuration loader and validator.
Define schema and configuration sources. Use merged and valid configuration object for your application.

Key features:

  • simple, universal and predictable
  • load, normalize and merge configuration from multiple sources (environment variables, and .env, JSON, YAML, JS files)
  • validate configuration object using Yup or JSON/JTD schema (through Ajv)
  • get meaningful error messages
    • for invalid values: where the value comes from
    • for missing values: how the value can be defined
    • optionally display a pretty error message
  • leverage TypeScript support including type inference from the schema
  • extend the library and use your own loader or validator
  • minimal footprint - only install dependencies that you need

Example Open in StackBlitz

Load and merge configuration (in order) from .env, .env.local and process.env.
Use APP_ prefix for environment variables. Validate merged object using Yup.

import * as yup from 'yup'
import OmniConfig from 'omniconfig.js'

const schema = yup.object({
  debug: yup.boolean().default(false),

  db: yup.object({
    host: yup.string().required(),
    port: yup.number().min(0).default(5432),
    user: yup.string().required(),
    pass: yup.string()
  })
})

const config = OmniConfig
  .withYup(schema)
  .useEnvironmentVariables({
    processEnv: true,
    envMapper:  { prefix: 'APP_' },
    dotEnv:     '.env[.local]',
  })
  .resolveSync()

console.log(config)

Get normalized and merged config object - like this:

{
  debug: true,
  db: { 
    host: 'localhost', 
    port: 5432, 
    user: 'some_user', 
    pass: 'foo'
  },
}

...or meaningful error messages:

Missing value:

Configuration error: db.user is a required field
The value can be defined in:
  - .env as APP_DB_USER
  - .env.local as APP_DB_USER
  - Environment variables as APP_DB_USER

Invalid value:

Configuration error: db.port must be greater than or equal to 0
The causing value is defined in .env.local as APP_DB_PORT

Check full code of this example.
You can find more examples in examples directory.

Table of Contents

Installation

npm i omniconfig.js --save # this library

npm i dotenv --save        # optional .env file support 
npm i js-yaml --save       # optional YAML file support 
npm i yup --save           # optional Yup support 
npm i ajv --save           # optional JSON schema and JDT schema support 
npm i chalk@^4.1.2 --save  # optional error message coloring 

or

yarn add omniconfig.js # this library

yarn add dotenv        # optional .env file support 
yarn add js-yaml       # optional YAML file support 
yarn add yup           # optional Yup support 
yarn add ajv           # optional JSON schema and JDT schema support 
yarn add chalk@^4.1.2  # optional error message coloring 

High level API

OmniConfig

High level class with builder-like API.

import { OmniConfig } from 'omniconfig.js'

const config = new OmniConfig()
  .withModel(/*...*/)
  .useLoader(/*...*/)
  .useLoader(/*...*/)
  // ...

Ready-to-use instance if also exported using a default export.

import OmniConfig from 'omniconfig.js'

const config = OmniConfig
  .withModel(/*...*/)
  .useLoader(/*...*/)
  .useLoader(/*...*/)
  // ...

.withModel(model?: Model): OmniConfig

Set model to validate configuration against. If model is not set, validation will not be performed. You can use one of built-in models like YupModel or AjvModel or create a custom one. Check the Model interface for the details.

.useLoader(loader: Loader): OmniConfig

Adds new loader to the end of loader list. Values loaded with it overwrite the previously loaded values.

Built-in loaders:

.useOptionalLoader(loader: Loader): OmniConfig

Adds new optional loader to the end of loader list. Values loaded with it overwrite the previously loaded values.

.withYup(schema: yup.ObjectSchema, options?: yup.ValidateOptions): OmniConfig

Required dependency: Yup

Sets Yup object schema as a validation model. Dynamic schemas are not supported.

import * as yup from 'yup'
import OmniConfig from 'omniconfig.js'

const schema = yup.object({
  debug: yup.boolean().default(false),

  db: yup.object({
    host: yup.string().required(),
    port: yup.number().min(0).default(5432),
    user: yup.string().required(),
    pass: yup.string()
  })
})

const config = OmniConfig
  .withYup(schema)
 // ...

.withJsonSchema(schema: ajv.JSONSchemaType, options?: ajv.Options, context?: ajv.DataValidationCxt): OmniConfig

Required dependency: Ajv

Sets JSON schema as a validation model. Using following default options for Ajv:

{
  coerceTypes: true,
  useDefaults: true,
  removeAdditional: true,
}

Example that uses Ajv default JSON schema version:

import OmniConfig from 'omniconfig.js'

interface Config {
  debug: boolean
  db: {
    host: string
    port: number
    user: string
    pass?: string
  }
}

const config = OmniConfig
  .withJsonSchema<Config>({
    type:     'object',
    required: ['db'],

    properties: {
      debug: {
        type:    'boolean',
        default: false,
      },

      db: {
        type:     'object',
        required: ['host', 'user', 'port'],

        properties: {
          host: { type: 'string' },
          port: { type: 'number', default: 5432 },
          user: { type: 'string' },
          pass: { type: 'string', nullable: true },
        }
      }
    }
  })

You can also customize Ajv behaviour (change schema, add keywords, etc...):

import Ajv from 'ajv'
import OmniConfig from 'omniconfig.js'
import { AjvModel } from './ajvModel'

const ajv = new Ajv({
  // your options
})

ajv.addSchema(/*...*/)
ajv.addFormat(/*...*/)
ajv.addKeyword(/*...*/)

const customFn = ajv.compile({
  // your schema
})

const config = OmniConfig
  .withModel(new AjvModel({ fn: customFn }))

.withJTDSchema(schema: ajv.JDTSchema, options?: ajv.JTDOptions, context?: ajv.DataValidationCxt): OmniConfig

Required dependency: Ajv

Sets JTD schema as a validation model.

.useValue(value: object, sourceOrFrameIndex?: string | number): OmniConfig

Loads configuration from a static value.
The library will attempt to determine file name and line number where this method is called using a stack trace. You can specify number of stack frames to skip (e.g. if you can some additional facade) or specify the source name as a string.

import OmniConfig from 'omniconfig.js'

const config = OmniConfig
  //...
  .useValue({
    myOption: 'myValue',
    
    some: {
      nested: {
        value: true,
      }
    }
  })
//...

.useEnvironmentVariables(options?: OmniConfigEnvOptions): OmniConfig

Loads configuration from environment variables and optionally .env files.

Options

Default options:

{
  processEnv: true,

  // MetadataBasedEnvMapperOptions
  envMapper: {
    prefix:        '',
    separator:     '_',
    wordSeparator: '_',
  }
}
processEnv: boolean = true

Enables (default) or disables loading configuration from process environment variables (`process.env). When enables, this loader is always added after .env files, so process environment variables always overwrite variables from .env files.

import OmniConfig from 'omniconfig.js'

const config = OmniConfig
  //...
  .useEnvironmentVariables({ processEnv: true }) // same as .useEnvironmentVariables()
  //...
dotEnv: true | string | ConfigFileVariantFn

Required dependency: dotenv

Enable loading of .env files. Supports following value:

  • true - load only .env file from current working directory
  • string - file name template for .env files (syntax)
  • ConfigFileVariantFn - function returns path to file for given context
import OmniConfig from 'omniconfig.js'

const config = OmniConfig
  //...
  .useEnvironmentVariables({
    dotenv: '.env[.local]'
    // dotenv: true
    // dotenv: ({ local, dist, nodeEnv }) => local ? '.env.very-custom-local-name' : `.env`
  })
//...
envMapper: EnvMapper | Partial<MetadataBasedEnvMapperOptions>

Accepts environment variable mapper instance or options for MetadataBasedEnvMapper.

By default, MetadataBasedEnvMapper is used. This mapper leverages metadata generated from the model (both Yup and Ajv support it) to map environment variables to configuration object keys. This approach allows to use same separator for configuration levels and camelcase names.

import * as yup from 'yup'
import OmniConfig from 'omniconfig.js'

const schema = yup.object({
  db: yup.object({
    host: yup.string().required(),
    // ...
  }),

  someService: yup.object({
    nestedSection: yup.object({
      option: yup.number(),
    })
  }),
})


const config = OmniConfig
  
  // Reads following environment variables names and maps to the above schema:
  //   - DB_HOST
  //   - SOME_SERVICE_NESTED_SECTION_OPTION
        
  .useEnvironmentVariables({
    envMapper: {
      prefix:        '',  // defaults
      separator:     '_',
      wordSeparator: '_',
    }
  })
  //...

const config2 = OmniConfig
  
  // Reads following environment variables names and maps to the above schema:
  //   - APP__DB__HOST
  //   - APP__SOME_SERVICE__NESTED_SECTION__OPTION
        
  .useEnvironmentVariables({
    envMapper: {
      prefix:        'APP__',
      separator:     '__',
      wordSeparator: '_',
    }
  })
  //...

Alternatively, you can use mappers that does not rely on the metadata (so you can use dynamic schemas):

.useJsonFiles(templateOrOptions: string | ConfigFileVariantFn | OmniConfigFileOptions): OmniConfig

Loads configuration from JSON files.

Options

template: string | ConfigFileVariantFn

As the template, you can pass:

  • string - file name template for JSON files (syntax)
  • ConfigFileVariantFn - function returns path to file for given context
section?: string | string[]

Optional section of file to load. Useful to load options from a key of package.json.

Section can be provided as:

  • string - dot separated list of properties (like foo.bar to load property bar that is nested in property foo)
  • string[] - where each element represents property (like ['foo', 'bar'] to load property bar that is nested in property foo)
import OmniConfig from 'omniconfig.js'

const config = OmniConfig
  //...
        
  // load JSON files with NODE_ENV based variants and dist variants        
  .useJsonFiles('config/app[.node_env].json[.dist]')
  
  // load JSON files returned by a custom function
  .useJsonFiles({
    template: ({ local, dist, nodeEnv }) => local ? 'very-custom-local-name.json' : 'app.json',
  })
        
  // load configuration from `custom.myApp` in `package.json`
  .useJsonFiles({
    template: 'package.json',
    section: 'custom.myApp', // same as ['custom', 'myApp']
  })
  //...

.useYamlFiles(template: string | ConfigFileVariantFn | OmniConfigFileOptions): OmniConfig

Required dependency: js-yaml

Loads configuration from YAML files.

Options

template: string | ConfigFileVariantFn

As the template, you can pass:

  • string - file name template for YAML files (syntax)
  • ConfigFileVariantFn - function returns path to file for given context
section?: string | string[]

Optional section of file to load. Useful to load options from a nested property of the file.

Section can be provided as:

  • string - dot separated list of properties (like foo.bar to load property bar that is nested in property foo)
  • string[] - where each element represents property (like ['foo', 'bar'] to load property bar that is nested in property foo)
import OmniConfig from 'omniconfig.js'

const config = OmniConfig
  //...

  // load YAML files with NODE_ENV based variants and dist variants   
  .useYamlFiles('config/app[.node_env].yml[.dist]')

  // load YAML files returned by a custom function
  .useYamlFiles({
    template: ({ local, dist, nodeEnv }) => local ? 'very-custom-local-name.yml' : 'app.yml',
  })
        
  // load options from `someKey` of `app.yml` file
  .useYamlFiles({
    template: 'app.yml',
    section:  'someKey' // same as ['someKey']
  })
  //...

.useJsFiles(template: string | ConfigFileVariantFn): OmniConfig

Loads configuration from JavaScript files.

As the template, you can pass:

  • string - file name template for JS files (syntax)
  • ConfigFileVariantFn - function returns path to file for given context

JS file path should be absolute or relative to the current working directory.

import OmniConfig from 'omniconfig.js'

const config = OmniConfig
  //...
  .useJsFiles('config/app[.node_env].js[.dist]')
  //.useJsFiles(({ local, dist, nodeEnv }) => local ? 'very-custom-local-name.js' : 'app.js')
  //...

.resolve(options?: OmniConfigResolveOptions): Promise<Config>

Asynchronously loads, merges, and validates configuration object. Optionally prints a formatted error message in the console.

Options

logger: OmniConfigResolveErrorLogger

Logger instance used to print error messages.
Default: console

formatter: ErrorFormatter

Instance of error formatter that formats validation error before it is passed to the logger. Default: ChalkErrorFormatter if chalk is available, otherwise: TextErrorFormatter

exitCode: number

Exit code. If provided, will be passed to process.exit(). Otherwise, process.exit() will not be called.

Default: undefined

.resolveSync(options?: OmniConfigResolveOptions): Config

Synchronously loads, merges, and validates configuration object. Optionally prints a formatted error message in the console.

See .resolve() for options reference.

File name template syntax

File name templates allows to customize source file name, location and variants that should be loaded.

Templates support following placeholders:

  • [local] - for local file variant (loaded AFTER the main file)
  • [dist] - for dist file variant (loaded BEFORE the main file)
  • [node_env] - environment-specific file variant (basing on process.env.NODE_ENV variable)

Additionally, you can add an arbitrary character after [ or before ] that should be inserted in the final name.

Examples:

  • template .env loads:

    1. .env
  • template .env[.local] loads:

    1. .env
    2. .env.local
  • template app.json[.dist] loads:

    1. app.json.dist
    2. app.json
  • template app[.node_env].json loads:

    1. app.json
    2. app.development.json (if NODE_ENV=development)
  • template config/[node_env.]app[.local].yml loads:

    1. config/app.yml
    2. config/app.local.yml
    3. config/development.app.yml (if NODE_ENV=development)
    4. config/development.app.local.yml (if NODE_ENV=development)

Package Sidebar

Install

npm i omniconfig.js

Weekly Downloads

110

Version

1.2.0

License

MIT

Unpacked Size

164 kB

Total Files

115

Last publish

Collaborators

  • mckacz