├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'es6': true, 4 | 'node': true 5 | }, 6 | 'extends': 'eslint:recommended', 7 | 'rules': { 8 | 'indent': [ 9 | 'error', 10 | 'tab' 11 | ], 12 | 'linebreak-style': [ 13 | 'error', 14 | 'unix' 15 | ], 16 | 'quotes': [ 17 | 'error', 18 | 'single' 19 | ], 20 | 'semi': [ 21 | 'error', 22 | 'never' 23 | ], 24 | 'require-yield': 'off', 25 | 'no-console': 'off' 26 | } 27 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "4" 5 | after_success: 6 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Fedor Korshunov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [npm-image]: https://img.shields.io/npm/v/no-config.svg?style=flat-square 2 | [npm-url]: https://npmjs.org/package/no-config 3 | [travis-image]: https://img.shields.io/travis/fedor/node-no-config.svg?style=flat-square 4 | [travis-url]: https://travis-ci.org/fedor/node-no-config 5 | [codecov-image]: https://img.shields.io/codecov/c/github/fedor/node-no-config.svg?style=flat-square 6 | [codecov-url]: https://codecov.io/gh/fedor/node-no-config 7 | [david-image]: https://img.shields.io/david/fedor/node-no-config.svg?style=flat-square 8 | [david-dev-image]: https://img.shields.io/david/dev/fedor/node-no-config.svg?style=flat-square 9 | [david-url]: https://david-dm.org/fedor/node-no-config 10 | [![NPM version][npm-image]][npm-url] 11 | [![Build Status][travis-image]][travis-url] 12 | [![Test Coverage][codecov-image]][codecov-url] 13 | [![Dependency Status][david-image]][david-url] 14 | [![Dependency Status][david-dev-image]][david-url] 15 | [Intro](#intro) | [Quick start](#quick-start) | [Quick start (ES6)](#quick-start-es6) | [API](#api) | [Contributors](#contributors) 16 | ## Intro 17 | > Why not `config`? 18 | 19 | [**Answer**](https://medium.com/@fedorHK/no-config-b3f1171eecd5). TL;DR: `config` separates data to different files based on `NODE_ENV`, not resources. 20 | ``` 21 | $ npm install no-config 22 | ``` 23 | ## Quick start 24 | ```js 25 | // config.js 26 | module.exports = { 27 | redis: { 28 | init: function (params) { 29 | return require('redis').createClient(params) 30 | }, 31 | default: { 32 | db: 0, 33 | port: 6379 34 | }, 35 | development: { 36 | host: '127.0.0.1' 37 | }, 38 | production: { 39 | db: 1, 40 | host: '192.168.0.10' 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | ```js 47 | // index.js 48 | require('no-config')({ 49 | config: require('./config') 50 | }).then( 51 | function(conf) { 52 | console.log('ENV', conf.env) 53 | console.log('Redis:', conf.redis.host+':'+conf.redis.port) 54 | conf.redis.instance.set('hello', 'world') 55 | } 56 | ) 57 | ``` 58 | ``` 59 | $ NODE_ENV=development node index.js 60 | ENV development 61 | Redis: 127.0.0.1:6379 62 | ``` 63 | ## Quick start (ES6) 64 | Since no-config returns a promise it is much better to use ES6 generators, arrow functions and [co](https://github.com/tj/co). 65 | **If you are not familiar with co, check this step-by-step [tutorial](https://github.com/fedor/co_demo)** 66 | 67 | ```js 68 | // config.js 69 | module.exports = { 70 | redis: { 71 | init: params => require('redis').createClient(params), 72 | default: { 73 | db: 0, 74 | port: 6379 75 | }, 76 | development: { 77 | host: '127.0.0.1' 78 | }, 79 | production: { 80 | db: 1, 81 | host: '192.168.0.10' 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ```js 88 | // index.js 89 | 'use strict' 90 | const co = require('co') 91 | co(function* () { 92 | let config = require('./config') 93 | let conf = yield require('no-config')({config}) 94 | 95 | console.log('ENV', conf.env) 96 | console.log('Redis:', conf.redis.host+':'+conf.redis.port) 97 | conf.redis.instance.set('hello', 'world') 98 | }) 99 | ``` 100 | ## API 101 | 102 | ### Loader 103 | ```js 104 | require('no-config')(parameters) 105 | ``` 106 | Loads resources from `parameters.config` based on `NODE_ENV` environment variable. Returns a Promise which resolves ones all resources are initialized. 107 | 108 | **Parameters** 109 | 110 | | Name | Required? | Type | Default | Description | 111 | | -------------- | --------- | --------------- | ------------- | -------------------------------------------------------- | 112 | | `config` | Required | Object | | [Configuration object](#configuration-object) | 113 | | `init` | Optional | List of strings | All Resources | Resources to initialize | 114 | | `verbose` | Optional | Boolean | `false` | Print resource input prior to call its `init()` function | 115 | | `mask_secrets` | Optional | Boolean | `true` | if `verbose === true` will hide input value if its key contains substrings: `secret`, `token`, `key`, `pass` or `pwd` | 116 | 117 | ### Configuration object 118 | Every high-level key in configuration object is a resource name. 119 | 120 | | Name | Required? | Type | Default | Description. Handling | 121 | | ------------------- | --------- | ---------- | ------------------ | --------------------------------------------------- | 122 | | `` | Optional | Object | | Resource configuration | 123 | | `.default` | Optional | Object | `{}` | Default values | 124 | | `.` | Optional | Object | `{}` | ENV specific values. If a key duplicates `default` key, env-specific value is used | 125 | | `.init` | Optional | Function, Generator function | | Called to initalize resource, `.init(result)`. If returns Promise or Generator, it got resolved with [co](https://github.com/tj/co). Result is saved to `result.instance`. | 126 | 127 | ## Contributors 128 | Fedor Korshunov - [view contributions](https://github.com/fedor/node-no-config/commits?author=fedor) 129 | Anurag Sharma - [view contributions](https://github.com/fedor/node-no-config/commits?author=anuragCES) 130 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var maskKeys = ['secret', 'token', 'key', 'pass', 'pwd'] 3 | module.exports = require('co').wrap(function* (params) { 4 | var get = function (val, def) { 5 | return (val !== undefined) ? val : def 6 | } 7 | 8 | var env = process.env.NODE_ENV 9 | var config = params.config 10 | var resourcesList = Object.keys(config) 11 | var init = get(params.init, resourcesList) 12 | var verbose = get(params.verbose, false) 13 | var maskSecrets = get(params.mask_secrets, true) 14 | 15 | var mask = function (key, val) { 16 | if (!maskSecrets) { 17 | return val 18 | } 19 | var matches = maskKeys.filter(function(maskKey) { 20 | return (key.toLowerCase().indexOf(maskKey) !== -1) 21 | }) 22 | if (matches.length) { 23 | return '********************' 24 | } 25 | return val 26 | } 27 | 28 | var result = {} 29 | resourcesList.forEach(function (resourceName) { 30 | var resource = config[resourceName] 31 | var default_config = resource.default || {} 32 | var env_config = resource[env] || {} 33 | result[resourceName] = Object.assign({}, default_config, env_config) 34 | }) 35 | for (var i = 0; i < init.length; i++) { 36 | var resourceName = init[i] 37 | var resource = config[resourceName] 38 | if (!resource.init) { 39 | continue 40 | } 41 | var resourceConfig = result[resourceName] 42 | if (verbose) { 43 | console.log('[' + resourceName + ']: starting') 44 | Object.keys(resourceConfig).forEach(function (key) { 45 | var value = mask(key, resourceConfig[key]) 46 | var padding = ' '.repeat(Math.max(1, 15 - key.length)) 47 | console.log('[' + resourceName + ']: ' + key + ':' + padding + value) 48 | }) 49 | console.log() 50 | } 51 | try { 52 | resourceConfig.instance = resource.init(resourceConfig) 53 | resourceConfig.instance = yield resourceConfig.instance 54 | } catch (e) { 55 | // if TypeError === co error, resourceConfig.instance must not be "yieldable" 56 | if (!(e instanceof TypeError)) { 57 | throw e 58 | } 59 | } 60 | } 61 | result.env = env 62 | return result 63 | }) 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-config", 3 | "version": "1.1.2", 4 | "description": "config and resource loader", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/eslint/bin/eslint.js *.js && istanbul cover -x test.js tape test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/fedor/node-no-config.git" 12 | }, 13 | "keywords": [ 14 | "environment", 15 | "env", 16 | "config-node", 17 | "node-config", 18 | "configuration", 19 | "config", 20 | "conf", 21 | "node_env", 22 | "loader", 23 | "load", 24 | "res", 25 | "resource", 26 | "init", 27 | "start" 28 | ], 29 | "author": "Fedor Korshunov ", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/fedor/node-no-config/issues" 33 | }, 34 | "homepage": "https://github.com/fedor/node-no-config#readme", 35 | "dependencies": { 36 | "co": "^4.6.0" 37 | }, 38 | "devDependencies": { 39 | "eslint": "^3.6.1", 40 | "intercept-stdout": "^0.1.2", 41 | "istanbul": "^0.4.5", 42 | "lodash.clonedeep": "^4.5.0", 43 | "tape": "^4.6.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const co = require('co') 4 | const test = require('tape') 5 | const cloneDeep = require('lodash.clonedeep') 6 | const intercept = require('intercept-stdout') 7 | 8 | const noConfig = require('./index') 9 | 10 | const config_base = { 11 | resource1: { 12 | default: { 13 | key0: 'resource1-default-value0', 14 | key1: 'resource1-default-value1' 15 | }, 16 | env1: { 17 | key1: 'resource1-env1-value1', 18 | key2: 'resource1-env1-value2' 19 | }, 20 | env2: { 21 | key1: 'resource1-env2-value1', 22 | key2: 'resource1-env2-value2' 23 | } 24 | }, 25 | resource2: { 26 | default: { 27 | key0: 'resource2-default-value0', 28 | key1: 'resource2-default-value1' 29 | }, 30 | env1: { 31 | key1: 'resource2-env1-value1', 32 | key2: 'resource2-env1-value2' 33 | }, 34 | env2: { 35 | key1: 'resource2-env2-value1', 36 | key2: 'resource2-env2-value2' 37 | } 38 | } 39 | } 40 | 41 | test('NODE_ENV not set (removed): default values only', co.wrap(function* (t) { 42 | try { 43 | delete process.env.NODE_ENV 44 | let config = cloneDeep(config_base) 45 | let result = yield noConfig({config}) 46 | let expected = { 47 | env: undefined, 48 | resource1: { 49 | key0: 'resource1-default-value0', 50 | key1: 'resource1-default-value1' 51 | }, 52 | resource2: { 53 | key0: 'resource2-default-value0', 54 | key1: 'resource2-default-value1' 55 | } 56 | } 57 | t.deepEqual(result, expected) 58 | } catch (e) { 59 | t.fail(e) 60 | } 61 | t.end() 62 | })) 63 | 64 | test('NODE_ENV not exists: default values only', co.wrap(function* (t) { 65 | try { 66 | process.env.NODE_ENV = 'env0' 67 | let config = cloneDeep(config_base) 68 | let result = yield noConfig({config}) 69 | let expected = { 70 | env: 'env0', 71 | resource1: { 72 | key0: 'resource1-default-value0', 73 | key1: 'resource1-default-value1' 74 | }, 75 | resource2: { 76 | key0: 'resource2-default-value0', 77 | key1: 'resource2-default-value1' 78 | } 79 | } 80 | t.deepEqual(result, expected) 81 | } catch (e) { 82 | t.fail(e) 83 | } 84 | t.end() 85 | })) 86 | 87 | test('NODE_ENV not exists, no default: empty objects', co.wrap(function* (t) { 88 | try { 89 | delete process.env.NODE_ENV 90 | let config = cloneDeep(config_base) 91 | delete config.resource1.default 92 | delete config.resource2.default 93 | let result = yield noConfig({config}) 94 | let expected = { 95 | env: undefined, 96 | resource1: {}, 97 | resource2: {} 98 | } 99 | t.deepEqual(result, expected) 100 | } catch (e) { 101 | t.fail(e) 102 | } 103 | t.end() 104 | })) 105 | 106 | test('NODE_ENV set: NODE_ENV specific values', co.wrap(function* (t) { 107 | try { 108 | process.env.NODE_ENV = 'env1' 109 | let config = cloneDeep(config_base) 110 | let result = yield noConfig({config}) 111 | let expected = { 112 | env: 'env1', 113 | resource1: { 114 | key0: 'resource1-default-value0', 115 | key1: 'resource1-env1-value1', 116 | key2: 'resource1-env1-value2' 117 | }, 118 | resource2: { 119 | key0: 'resource2-default-value0', 120 | key1: 'resource2-env1-value1', 121 | key2: 'resource2-env1-value2' 122 | } 123 | } 124 | t.deepEqual(result, expected) 125 | } catch (e) { 126 | t.fail(e) 127 | } 128 | t.end() 129 | })) 130 | 131 | test('NODE_ENV set, init() is a function: init() gets expected input, result is captured', co.wrap(function* (t) { 132 | try { 133 | process.env.NODE_ENV = 'env1' 134 | let config = cloneDeep(config_base) 135 | config.resource1.init = (input) => { 136 | let expected_input = { 137 | key0: 'resource1-default-value0', 138 | key1: 'resource1-env1-value1', 139 | key2: 'resource1-env1-value2' 140 | } 141 | t.deepEqual(input, expected_input) 142 | return 'resource1 init called' 143 | } 144 | config.resource2.init = function (input) { 145 | let expected_input = { 146 | key0: 'resource2-default-value0', 147 | key1: 'resource2-env1-value1', 148 | key2: 'resource2-env1-value2' 149 | } 150 | t.deepEqual(input, expected_input) 151 | return 'resource2 init called' 152 | } 153 | let result = yield noConfig({config}) 154 | t.equal(result.resource1.instance, 'resource1 init called') 155 | t.equal(result.resource2.instance, 'resource2 init called') 156 | } catch (e) { 157 | t.fail(e) 158 | } 159 | t.end() 160 | })) 161 | 162 | test('NODE_ENV set, init() returns a Promise, Promise resolves: result is captured', co.wrap(function* (t) { 163 | try { 164 | process.env.NODE_ENV = 'env1' 165 | let config = cloneDeep(config_base) 166 | config.resource1.init = function () { 167 | return Promise.resolve('Promise result') 168 | } 169 | let result = yield noConfig({config}) 170 | t.equal(result.resource1.instance, 'Promise result') 171 | } catch (e) { 172 | t.fail(e) 173 | } 174 | t.end() 175 | })) 176 | 177 | test('NODE_ENV set, init() is a generator function and returns result: result is captured', co.wrap(function* (t) { 178 | try { 179 | process.env.NODE_ENV = 'env1' 180 | let config = cloneDeep(config_base) 181 | config.resource1.init = function* () { 182 | return 'generator function result' 183 | } 184 | let result = yield noConfig({config}) 185 | t.equal(result.resource1.instance, 'generator function result') 186 | } catch (e) { 187 | t.fail(e) 188 | } 189 | t.end() 190 | })) 191 | 192 | test('NODE_ENV set, init() returns Promise, Promise rejects: rejects', co.wrap(function* (t) { 193 | try { 194 | process.env.NODE_ENV = 'env1' 195 | let config = cloneDeep(config_base) 196 | config.resource1.init = function () { 197 | return Promise.reject('Promise error') 198 | } 199 | yield noConfig({config}) 200 | t.fail('Failure was expected') 201 | } catch (e) { 202 | t.equal(e, 'Promise error') 203 | } 204 | t.end() 205 | })) 206 | 207 | test('NODE_ENV set, init() is a function and throws error: rejects', co.wrap(function* (t) { 208 | try { 209 | process.env.NODE_ENV = 'env1' 210 | let config = cloneDeep(config_base) 211 | config.resource1.init = function () { 212 | throw 'function error' 213 | } 214 | yield noConfig({config}) 215 | t.fail('Failure was expected') 216 | } catch (e) { 217 | t.equal(e, 'function error') 218 | } 219 | t.end() 220 | })) 221 | 222 | test('NODE_ENV set, init() is a generator function and throws error: rejects', co.wrap(function* (t) { 223 | try { 224 | process.env.NODE_ENV = 'env1' 225 | let config = cloneDeep(config_base) 226 | config.resource1.init = function* () { 227 | throw 'generator function error' 228 | } 229 | yield noConfig({config}) 230 | t.fail('Failure was expected') 231 | } catch (e) { 232 | t.equal(e, 'generator function error') 233 | } 234 | t.end() 235 | })) 236 | 237 | test('NODE_ENV set, verbose mode: result is printed, "key*" values are masked', co.wrap(function* (t) { 238 | let lines = [] 239 | let unhook_intercept = intercept(function (line) { 240 | line = line.trim() 241 | if (line) { 242 | lines.push(line) 243 | } 244 | return '' 245 | }) 246 | try { 247 | process.env.NODE_ENV = 'env1' 248 | let config = cloneDeep(config_base) 249 | config.resource1.default.param3 = 'resource1-default-value3' 250 | config.resource1.init = function () {} 251 | yield noConfig({config, verbose: true}) 252 | let expected_lines = [ 253 | '[resource1]: starting', 254 | '[resource1]: key0: ********************', 255 | '[resource1]: key1: ********************', 256 | '[resource1]: param3: resource1-default-value3', 257 | '[resource1]: key2: ********************' 258 | ] 259 | t.deepEqual(lines, expected_lines) 260 | } catch (e) { 261 | t.fail(e) 262 | } 263 | unhook_intercept() 264 | t.end() 265 | })) 266 | 267 | test('NODE_ENV set, verbose mode, unmasked: result is printed', co.wrap(function* (t) { 268 | let lines = [] 269 | let unhook_intercept = intercept(function (line) { 270 | line = line.trim() 271 | if (line) { 272 | lines.push(line) 273 | } 274 | return '' 275 | }) 276 | try { 277 | process.env.NODE_ENV = 'env1' 278 | let config = cloneDeep(config_base) 279 | config.resource1.default.param3 = 'resource1-default-value3' 280 | config.resource1.init = function () {} 281 | yield noConfig({config, verbose: true, mask_secrets: false}) 282 | let expected_lines = [ 283 | '[resource1]: starting', 284 | '[resource1]: key0: resource1-default-value0', 285 | '[resource1]: key1: resource1-env1-value1', 286 | '[resource1]: param3: resource1-default-value3', 287 | '[resource1]: key2: resource1-env1-value2' 288 | ] 289 | t.deepEqual(lines, expected_lines) 290 | } catch (e) { 291 | t.fail(e) 292 | } 293 | unhook_intercept() 294 | t.end() 295 | })) 296 | --------------------------------------------------------------------------------