omniconfig.js
v1.2.0
Published
Load, merge and validate configuration from environment variables and/or .env, JS, JSON, YAML files
Downloads
1,233
Maintainers
Readme
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
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:
Invalid value:
Check full code of this example.
You can find more examples in examples directory.
Table of Contents
- Installation
- High level API
- OmniConfig
.withModel(model?: Model): OmniConfig
.useLoader(loader: Loader): OmniConfig
.useOptionalLoader(loader: Loader): OmniConfig
.withYup(schema: yup.ObjectSchema, options?: yup.ValidateOptions): OmniConfig
.withJsonSchema(schema: ajv.JSONSchemaType, options?: ajv.Options, context?: ajv.DataValidationCxt): OmniConfig
.withJTDSchema(schema: ajv.JDTSchema, options?: ajv.JTDOptions, context?: ajv.DataValidationCxt): OmniConfig
.useEnvironmentVariables(options?: OmniConfigEnvOptions): OmniConfig
.useJsonFiles(templateOrOptions: string | ConfigFileVariantFn | OmniConfigFileOptions): OmniConfig
.useYamlFiles(template: string | ConfigFileVariantFn | OmniConfigFileOptions): OmniConfig
.useJsFiles(template: string | ConfigFileVariantFn): OmniConfig
.resolve(options?: OmniConfigResolveOptions): Promise<Config>
.resolveSync(options?: OmniConfigResolveOptions): Config
- File name template syntax
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:
- ProcessEnvLoader - loads process environment variables
- DotEnvLoader - loads environment variables .env files (requires dotenv)
- ModuleLoader - loads JS files
- JsonFileLoader - loads JSON files
- YamlFileLoader - loads YAML files (requires js-yaml)
- ValueLoader - static value loader
- OptionalLoader - loader wrapper that ignores errors thrown by inner loader
.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 directorystring
- 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):
- CamelCaseEnvMapper - to map camelcase object keys to environment variables
- SnakeCaseEnvMapper - to map snakecase object keys to environment variables
.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 (likefoo.bar
to load propertybar
that is nested in propertyfoo
)string[]
- where each element represents property (like['foo', 'bar']
to load propertybar
that is nested in propertyfoo
)
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 (likefoo.bar
to load propertybar
that is nested in propertyfoo
)string[]
- where each element represents property (like['foo', 'bar']
to load propertybar
that is nested in propertyfoo
)
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 onprocess.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:.env
template
.env[.local]
loads:.env
.env.local
template
app.json[.dist]
loads:app.json.dist
app.json
template
app[.node_env].json
loads:app.json
app.development.json
(ifNODE_ENV=development
)
template
config/[node_env.]app[.local].yml
loads:config/app.yml
config/app.local.yml
config/development.app.yml
(ifNODE_ENV=development
)config/development.app.local.yml
(ifNODE_ENV=development
)