A proposal for a framework-agnostic way of building Javascript configuration objects based on different environments.
This proposal focuses on the conventions that will allow for the creation of a plain old JavaScript object. For example:
var environment = new Environment("development");
environment.name; // "development"
environment.config.API_ROOT; // "http://localhost:3000/"
Additional libraries could be built on top of this base functionality, for example a gulp plugin, a library which constructs an Angular constant, etc.
- A default implementation must work "out of the box" (opt-in to complexity)
- It must allow for multiple "environments" (development, staging, production).
- It must keep configuration DRY across multiple environments.
- It must allow third-party configuration to be included relatively easily.
- It must allow for both static (hard-coded) and dynamic (determined at compile time) settings.
- It must be tool/framework agnostic.
config.json
resides in the project root and defines how configuration should be built for a given environment.
A basic config.json
is shown below. It consists of a single environment object.
{
"default": {
"constants": {
"ANSWER": 42
}
}
}
This will be the default configuration loaded when no environment is specified. Constants can be defined directly in config.json
and can either be static literals (ex: 42
) or can be dynamic replacements (see further down).
config.json
may define as many environments as needed:
{
"default": {
"constants": {
"ANSWER": 42
}
},
"special": {
"constants": {
"ANSWER": 84
}
}
}
Environments may extend an existing environment. This allows environments to inherit configuration while adding or overwriting only what's necessary. This keeps environments DRY but flexible.
{
"default": {
"constants": {
"OPBEAT_ORG_ID": "ZXCV09",
"OPBEAT_APP_ID": "ABC123"
}
},
"development": {
"extends": "default",
"constants": {
"API_ROOT": "http://localhost:3000/"
}
},
"staging": {
"extends": "default",
"constants": {
"API_ROOT": "http://staging-api.example.com/",
}
},
"production": {
"extends": "default",
"constants": {
"API_ROOT": "http://api.example.com/",
"OPBEAT_APP_ID": "XYZ789"
}
}
}
Use paths
for scenarios where you may want additional files or globs to be merged into an environment.
For example, Ionic Framework uses .io-config.json
to specify settings for ionic.io platform integrations. To merge these values into your environment(s):
{
"default": {
"constants": {
"ANSWER": 42
},
"paths": [
".io-config.json",
]
}
}
In teams of multiple developers, developers may wish to overwrite specific settings for their local machine. These files are often excluded from version control to avoid interfering with the rest of the team's local setup.
{
"development": {
"paths": [
"./config/development.json",
"./config/development.local.json",
]
}
}
In this example, development settings common to all team members would be checked into version control under ./config/development.json
, while ./config/development.local.json
would be added .gitignore
, allowing each team member to override any particular setting they wish.
Additionally, globbing could be used to allow whole patterns to be extended in to the environment. This allows the developer to break their configuration cleanly into different files if they wish:
{
"development": {
"paths": [
"./config/development.json",
"./config/development.*.json",
]
}
}
Not all constants can be hard-coded into a file. For example, you may want to inject the "version"
attribute from package.json
into your configuration as 'VERSION'
. There are two ways to define dynamic replacements in constants
:
- Environment Variable Replacements (prefixed with
"_$"
) - Function Replacements (prefixed with
"_#"
)
Allows placeholders to be replaced by environment variables. _$KEY
will be replaced with the environment variable KEY
{
"development": {
"constants": {
"API_TOKEN": "_$API_TOKEN"
}
}
}
Allows placeholders to be replaced by a function. _#KEY
will be replaced with the result of REPLACEMENT_FUNCTION(env, config)
When using function replacements, use replacements
to define the location of your replacment functions.
{
"default": {
"constants": {
"ANSWER": 42,
"VERSION": "_#VERSION_FUNC"
},
"replacements": [
"./config/replacements.js"
]
}
}
Replacement files consist of an object whose keys are equal to the placeholder being replaced (excluding the "_#"
prefix) and whose values are the replacement function.
So to dynamically replace "VERSION"
above, our ./config/replacements.js
file would be:
module.exports = {
VERSION_FUNC: function(env, config) {
return package.version;
}
}
The arguments passed to the replacement function are:
env
: current environment name (ex:"default"
)config
: an object representing the configuration as processed so far (ex:{ANSWER: 42}
)
The basic format of config.json
is {ENV_NAME: ENVIRONMENT_DEFINITION}
. The environment configuration is the final object that results from processing the environment definition.
contsants
: object representing key/value pairs (either static or dynamic).extends
: specifies a parent configuration into which this configuration is merged.paths
: additional JSON files which should be merged into this configuration.replacements
: paths to a modules which define replacement functions for dynamic settings.
Each subsequent step will override any keys defined at an earlier stage.
extends
: Load parent configuration.constants
: Merge static constants.replacements
: Process & merge replacements (environment variables and functions).paths
: Merge any additional files in the order they are specified.