Skip to content

Instantly share code, notes, and snippets.

@kylefox
Last active March 31, 2016 20:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kylefox/4dc10aa5beaf4409ce76 to your computer and use it in GitHub Desktop.
Save kylefox/4dc10aa5beaf4409ce76 to your computer and use it in GitHub Desktop.

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.

Goals

  • 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).

Multiple Environments

config.json may define as many environments as needed:

{
  "default": {
    "constants": {
      "ANSWER": 42
    }
  },
  "special": {
    "constants": {
      "ANSWER": 84
    }
  }
}

Extending Environments

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

Including Additional Files

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",
    ]
  }
}

Dynamic Replacements

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:

  1. Environment Variable Replacements (prefixed with "_$")
  2. Function Replacements (prefixed with "_#")

Environment Variable Replacements

Allows placeholders to be replaced by environment variables. _$KEY will be replaced with the environment variable KEY

{
  "development": {
    "constants": {
      "API_TOKEN": "_$API_TOKEN"
    }
  }
}

Function Replacements

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:

  1. env: current environment name (ex: "default")
  2. config: an object representing the configuration as processed so far (ex: {ANSWER: 42})

Environment Objects

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.

Merge order

Each subsequent step will override any keys defined at an earlier stage.

  1. extends: Load parent configuration.
  2. constants: Merge static constants.
  3. replacements: Process & merge replacements (environment variables and functions).
  4. paths: Merge any additional files in the order they are specified.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment