├── .npmrc ├── test ├── fixtures │ ├── malformed │ │ └── config.json │ ├── config │ │ ├── child.json │ │ ├── error.json │ │ └── config.json │ ├── import │ │ ├── default.json │ │ ├── grandchild.json │ │ ├── config.json │ │ ├── missing.json │ │ ├── child.json │ │ └── override.json │ └── defaults │ │ ├── development.json │ │ ├── supplemental.json │ │ └── config.json ├── harness.js ├── common-test.js ├── provider-test.js └── confit-test.js ├── .travis.yml ├── .gitignore ├── SECURITY.md ├── .npmignore ├── LICENSE.txt ├── package.json ├── .jshintrc ├── index.js ├── CONTRIBUTING.md ├── lib ├── common.js ├── config.js ├── provider.js ├── factory.js └── handlers.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /test/fixtures/malformed/config.json: -------------------------------------------------------------------------------- 1 | { 2 | -------------------------------------------------------------------------------- /test/fixtures/config/child.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child", 3 | "foo": "a value" 4 | } -------------------------------------------------------------------------------- /test/fixtures/import/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "default", 3 | "foo": "bar" 4 | } -------------------------------------------------------------------------------- /test/fixtures/config/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "error", 3 | "foo": "config:not.a.value" 4 | } -------------------------------------------------------------------------------- /test/fixtures/import/grandchild.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grandchild", 3 | "secret": "santa" 4 | } -------------------------------------------------------------------------------- /test/fixtures/import/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parent", 3 | "child": "import:./child.json" 4 | } -------------------------------------------------------------------------------- /test/fixtures/import/missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "missing", 3 | "child": "import:./orphan.json" 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "12" 6 | script: 7 | - "npm run cover" 8 | -------------------------------------------------------------------------------- /test/fixtures/defaults/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "override": "development", 3 | "path": "path:./development.json" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/defaults/supplemental.json: -------------------------------------------------------------------------------- 1 | { 2 | "override": "supplemental", 3 | "path": "path:./supplemental.json" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/defaults/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": "config", 3 | "override": "config", 4 | "misc": "path:./config.json" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/import/child.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child", 3 | "grandchild": "import:./grandchild", 4 | "grandchildJson": "import:./grandchild.json" 5 | } -------------------------------------------------------------------------------- /test/fixtures/import/override.json: -------------------------------------------------------------------------------- 1 | { 2 | "child" : { 3 | "grandchild": { 4 | "name": "surprise", 5 | "another": "claus" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config", 3 | "foo": "config:imported.foo", 4 | "bar": "config:foo", 5 | "baz": "config:path.to.nested.value", 6 | "path": { 7 | "to": { 8 | "nested": { 9 | "value": "config:value" 10 | } 11 | } 12 | }, 13 | "value": false, 14 | "imported": "import:./child.json" 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.seed 2 | *.log 3 | *.csv 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | *.orig 9 | *.swp 10 | 11 | dist 12 | work 13 | build 14 | pids 15 | logs 16 | results 17 | coverage 18 | lib-cov 19 | html-report 20 | xunit.xml 21 | node_modules 22 | npm-debug.log 23 | .nyc_output 24 | .tap 25 | 26 | .project 27 | .idea 28 | .vscode 29 | .settings 30 | .iml 31 | *.sublime-workspace 32 | *.sublime-project 33 | 34 | .DS_Store* 35 | ehthumbs.db 36 | Icon? 37 | Thumbs.db 38 | .AppleDouble 39 | .LSOverride 40 | .Spotlight-V100 41 | .Trashes 42 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We take security very seriously and ask that you follow the following process. 4 | 5 | 6 | ## Contact us 7 | If you think you may have found a security bug we ask that you privately send the details to DL-PP-Kraken-Js@paypal.com. Please make sure to use a descriptive title in the email. 8 | 9 | 10 | ## Expectations 11 | We will generally get back to you within **24 hours**, but a more detailed response may take up to **48 hours**. If you feel we're not responding back in time, please send us a message *without detail* on Twitter [@kraken_js](https://twitter.com/kraken_js). 12 | 13 | 14 | ## History 15 | No reported issues 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Automatically ignored per: 2 | # https://www.npmjs.org/doc/developers.html#Keeping-files-out-of-your-package 3 | # 4 | # .*.swp 5 | # ._* 6 | # .DS_Store 7 | # .git 8 | # .hg 9 | # .lock-wscript 10 | # .svn 11 | # .wafpickle-* 12 | # CVS 13 | # npm-debug.log 14 | # node_modules 15 | 16 | *.seed 17 | *.log 18 | *.csv 19 | *.dat 20 | *.out 21 | *.pid 22 | *.gz 23 | *.orig 24 | 25 | work 26 | build 27 | test 28 | pids 29 | logs 30 | results 31 | coverage 32 | lib-cov 33 | html-report 34 | xunit.xml 35 | .tap 36 | 37 | .project 38 | .idea 39 | .settings 40 | .iml 41 | *.sublime-workspace 42 | *.sublime-project 43 | 44 | ehthumbs.db 45 | Icon? 46 | Thumbs.db 47 | .AppleDouble 48 | .LSOverride 49 | .Spotlight-V100 50 | .Trashes -------------------------------------------------------------------------------- /test/harness.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tape = require('tap').test; 4 | const glob = require('glob'); 5 | const Path = require('path'); 6 | 7 | // Kick things off, but only after the module has completed loading, 8 | // hence the setImmediate. If the load the modules synchronously, 9 | // the exported object isn't yet available (since tests import this 10 | // module) and we get into a weird state. 11 | setImmediate(function () { 12 | // All this mess for npm < 2. With 2.x this can be removed 13 | // and npm script argument globbing can be used. 14 | process.argv.slice(2).forEach(function (arg) { 15 | glob.sync(arg).forEach(function (file) { 16 | require(Path.resolve(process.cwd(), file)); 17 | }); 18 | }); 19 | 20 | // Get a handle on the root test harness so we 21 | // can forcefull kill the process (THANKS TIMERS!) 22 | tape().on('end', function () { setImmediate(process.exit, 0) }); 23 | }); 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*───────────────────────────────────────────────────────────────────────────*\ 2 | │ Copyright (C) 2016 PayPal Software Foundation │ 3 | │ │ 4 | │ Licensed under the Apache License, Version 2.0 (the "License"); │ 5 | │ you may not use this file except in compliance with the License. │ 6 | │ You may obtain a copy of the License at │ 7 | │ │ 8 | │ http://www.apache.org/licenses/LICENSE-2.0 │ 9 | │ │ 10 | │ Unless required by applicable law or agreed to in writing, software │ 11 | │ distributed under the License is distributed on an "AS IS" BASIS, │ 12 | │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ 13 | │ See the License for the specific language governing permissions and │ 14 | │ limitations under the License. │ 15 | \*───────────────────────────────────────────────────────────────────────────*/ 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "confit", 3 | "version": "3.1.0", 4 | "description": "Environment-aware configuration.", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=16" 8 | }, 9 | "files": [ 10 | "index.js", 11 | "lib/" 12 | ], 13 | "directories": { 14 | "test": "test" 15 | }, 16 | "scripts": { 17 | "cover": "nyc tap test/**/*-test.js && nyc report -r html", 18 | "test": "tap test/**/*-test.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/krakenjs/confit.git" 23 | }, 24 | "keywords": [ 25 | "application", 26 | "config", 27 | "configuration" 28 | ], 29 | "author": "Erik Toth ", 30 | "licenses": [ 31 | { 32 | "type": "Apache 2.0", 33 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 34 | } 35 | ], 36 | "readmeFilename": "README.md", 37 | "devDependencies": { 38 | "@tapjs/synonyms": "^1.1.19", 39 | "glob": "^7.1.6", 40 | "jshint": "^2.11.0", 41 | "nyc": "^15.1.0", 42 | "tap": "^18.7.0" 43 | }, 44 | "dependencies": { 45 | "async": "^2.6.3", 46 | "caller": "^1.0.1", 47 | "minimist": "^1.2.5", 48 | "shortstop": "^1.1.0", 49 | "shortstop-handlers": "^1.0.1", 50 | "shush": "^1.0.0" 51 | }, 52 | "tap": { 53 | "plugin": [ 54 | "@tapjs/synonyms" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Enforcing Options 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "es3": false, 7 | "forin": true, 8 | "freeze": true, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "noempty": true, 15 | "nonbsp": true, 16 | "nonew": true, 17 | "plusplus": false, 18 | "quotmark": "single", 19 | "undef": true, 20 | "unused": true, 21 | "strict": false, 22 | "trailing": true, 23 | "maxparams": 3, 24 | "maxdepth": 8, 25 | "maxstatements": 24, 26 | "maxcomplexity": 12, 27 | "maxlen": 120, 28 | 29 | // Relaxing Options 30 | "asi": false, 31 | "boss": false, 32 | "debug": false, 33 | "eqnull": true, 34 | "esnext": true, 35 | "evil": false, 36 | "expr": false, 37 | "funcscope": false, 38 | "gcl": false, 39 | "globalstrict": true, 40 | "iterator": false, 41 | "lastsemic": false, 42 | "laxbreak": false, 43 | "laxcomma": false, 44 | "loopfunc": false, 45 | "maxerr": 50, 46 | "multistr": false, 47 | "notypeof": false, 48 | "proto": false, 49 | "scripturl": false, 50 | "smarttabs": false, 51 | "shadow": false, 52 | "sub": true, 53 | "supernew": false, 54 | "validthis": false, 55 | "noyield": false, 56 | 57 | // Environments 58 | "browser": false, 59 | "couch": false, 60 | "devel": false, 61 | "dojo": false, 62 | "jquery": false, 63 | "mootools": false, 64 | "node": true, 65 | "nonstandard": false, 66 | "phantom": false, 67 | "prototypejs": false, 68 | "rhino": false, 69 | "worker": false, 70 | "wsh": false, 71 | "yui": false 72 | } 73 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*───────────────────────────────────────────────────────────────────────────*\ 2 | │ Copyright (C) 2016 PayPal │ 3 | │ │ 4 | │ Licensed under the Apache License, Version 2.0 (the "License"); │ 5 | │ you may not use this file except in compliance with the License. │ 6 | │ You may obtain a copy of the License at │ 7 | │ │ 8 | │ http://www.apache.org/licenses/LICENSE-2.0 │ 9 | │ │ 10 | │ Unless required by applicable law or agreed to in writing, software │ 11 | │ distributed under the License is distributed on an "AS IS" BASIS, │ 12 | │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ 13 | │ See the License for the specific language governing permissions and │ 14 | │ limitations under the License. │ 15 | \*───────────────────────────────────────────────────────────────────────────*/ 16 | const Path = require('path'); 17 | const caller = require('caller'); 18 | const Factory = require('./lib/factory'); 19 | 20 | 21 | module.exports = function confit(options = {}) { 22 | if (typeof options === 'string') { 23 | options = { basedir: options }; 24 | } 25 | 26 | // ¯\_(ツ)_/¯ ... still normalizing 27 | options.defaults = options.defaults || 'config.json'; 28 | options.basedir = options.basedir || Path.dirname(caller()); 29 | options.protocols = options.protocols || {}; 30 | 31 | return new Factory(options); 32 | }; 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing To confit 2 | 3 | We are always looking for ways to make our modules better. Adding features and fixing bugs allows everyone who depends 4 | on this code to create better, more stable applications. 5 | Feel free to raise a pull request to us. Our team would review your proposed modifications and, if appropriate, merge 6 | your changes into our code. Ideas and other comments are also welcome. 7 | 8 | ## Getting Started 9 | 1. Create your own [fork](https://help.github.com/articles/fork-a-repo) of this [repository](../../fork). 10 | ```bash 11 | # Clone it 12 | $ git clone git@github.com:me/confit.git 13 | 14 | # Change directory 15 | $ cd confit 16 | 17 | # Add the upstream repo 18 | $ git remote add upstream git://github.com/krakenjs/confit.git 19 | 20 | # Get the latest upstream changes 21 | $ git pull upstream 22 | 23 | # Install dependencies 24 | $ npm install 25 | 26 | # Run scripts to verify installation 27 | $ npm test 28 | $ npm run lint 29 | $ npm run cover 30 | ``` 31 | 32 | ## Making Changes 33 | 1. Make sure that your changes adhere to the current coding conventions used throughout the project, indentation, accurate comments, etc. 34 | 2. Lint your code regularly and ensure it passes prior to submitting a PR: 35 | `$ npm run lint`. 36 | 3. Ensure existing tests pass (`$ npm test`) and include test cases which fail without your change and succeed with it. 37 | 38 | ## Submitting Changes 39 | 1. Ensure that no errors are generated by JSLint, CSSLint or any other tools that you use for debugging your code. 40 | 2. Commit your changes in logical chunks, i.e. keep your changes small per single commit. 41 | 3. Locally merge (or rebase) the upstream branch into your topic branch: `$ git pull upstream && git merge`. 42 | 4. Push your topic branch up to your fork: `$ git push origin `. 43 | 5. Open a [Pull Request](https://help.github.com/articles/using-pull-requests) with a clear title and description. 44 | 45 | If you have any questions about contributing, please feel free to contact us by posting your questions on GitHub. 46 | 47 | Copyright 2016 PayPal under [the Apache 2.0 license](LICENSE.txt). 48 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | /*───────────────────────────────────────────────────────────────────────────*\ 2 | │ Copyright (C) 2016 PayPal │ 3 | │ │ 4 | │ Licensed under the Apache License, Version 2.0 (the "License"); │ 5 | │ you may not use this file except in compliance with the License. │ 6 | │ You may obtain a copy of the License at │ 7 | │ │ 8 | │ http://www.apache.org/licenses/LICENSE-2.0 │ 9 | │ │ 10 | │ Unless required by applicable law or agreed to in writing, software │ 11 | │ distributed under the License is distributed on an "AS IS" BASIS, │ 12 | │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ 13 | │ See the License for the specific language governing permissions and │ 14 | │ limitations under the License. │ 15 | \*───────────────────────────────────────────────────────────────────────────*/ 16 | 'use strict'; 17 | 18 | const Path = require('path'); 19 | 20 | /** 21 | * Returns `true` if the given object is strictly an `Object` and not a `Function` (even though functions are objects in JavaScript). Otherwise, returns `false` 22 | * @param {unknown} val 23 | * @returns {boolean} 24 | */ 25 | const isObject = (value) => value !== null && typeof value === "object"; 26 | 27 | 28 | module.exports = { 29 | 30 | env: { 31 | development: /^dev/i, 32 | test : /^test/i, 33 | staging : /^stag/i, 34 | production : /^prod/i 35 | }, 36 | 37 | isAbsolute(path) { 38 | if (typeof path === 'string') { 39 | path = Path.normalize(path); 40 | return path === Path.resolve(path); 41 | } 42 | return false; 43 | }, 44 | 45 | merge(src, dest) { 46 | // NOTE: Do not merge arrays and only merge objects into objects. Do not merge special objects created from custom Classes. 47 | if (!Array.isArray(src) && isObject(src) && isObject(dest) && Object.getPrototypeOf(src) === Object.prototype) { 48 | for (let prop of Object.getOwnPropertyNames(src)) { 49 | let descriptor = Object.getOwnPropertyDescriptor(src, prop); 50 | descriptor.value = this.merge(descriptor.value, dest[prop]); 51 | Object.defineProperty(dest, prop, descriptor); 52 | } 53 | return dest; 54 | } 55 | 56 | return src; 57 | } 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /test/common-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tap').test; 4 | const common = require('../lib/common'); 5 | 6 | 7 | test('isAbsolute', function (t) { 8 | t.ok(common.isAbsolute(__dirname)); 9 | t.ok(common.isAbsolute(__filename)); 10 | 11 | t.notOk(common.isAbsolute('./foo.js')); 12 | t.notOk(common.isAbsolute('foo.js')); 13 | t.notOk(common.isAbsolute()); 14 | t.notOk(common.isAbsolute(0)); 15 | t.notOk(common.isAbsolute(1)); 16 | t.notOk(common.isAbsolute(true)); 17 | t.notOk(common.isAbsolute(false)); 18 | t.notOk(common.isAbsolute({})); 19 | t.end(); 20 | }); 21 | 22 | 23 | test('merge', function (t) { 24 | let src; 25 | let dest; 26 | 27 | src = { a: 'a' }; 28 | dest = {}; 29 | 30 | common.merge(src, dest); 31 | t.notEqual(src, dest); 32 | t.deepEqual(src, dest); 33 | 34 | src = { a: 'a' }; 35 | dest = { a: 'b' }; 36 | 37 | common.merge(src, dest); 38 | t.notEqual(src, dest); 39 | t.deepEqual(src, dest); 40 | 41 | src = { a: 'a' }; 42 | dest = { a: 'b', b: 'b' }; 43 | common.merge(src, dest); 44 | t.notEqual(src, dest); 45 | t.notDeepEqual(src, dest); 46 | t.equal(src.a, dest.a); 47 | t.equal(src.b, undefined); 48 | t.equal(dest.b, 'b'); 49 | 50 | src = { a: { b: 0, c: [ 0, 1, 2 ] } }; 51 | dest = { a: { b: 1, c: [ 'a', 'b', 'c', 'd' ], d: true } }; 52 | common.merge(src, dest); 53 | t.notEqual(src, dest); 54 | t.notDeepEqual(src, dest); 55 | t.notEqual(src.a, dest.a); 56 | t.equal(src.a.b, dest.a.b); 57 | t.equal(src.a.c, dest.a.c); 58 | t.equal(dest.a.d, true); 59 | 60 | t.end(); 61 | }); 62 | 63 | 64 | test('merge with existing props', function (t) { 65 | let src; 66 | let dest; 67 | 68 | src = { 69 | 'a': { 70 | 'foo': false 71 | } 72 | }; 73 | dest = { 'a': '[Object object]' }; 74 | 75 | t.doesNotThrow(function () { 76 | common.merge(src, dest); 77 | t.deepEqual(src.a, dest.a); 78 | }); 79 | 80 | t.end(); 81 | }); 82 | 83 | test('merge special objects', function (t) { 84 | const TestClass = class TestClass {}; 85 | const dest = {}; 86 | const src = { 87 | 'a': { 88 | 'foo': false 89 | }, 90 | 'b': new TestClass(), 91 | }; 92 | src.b['bar'] = true; 93 | 94 | t.doesNotThrow(function () { 95 | common.merge(src, dest); 96 | }); 97 | t.notEqual(src, dest); 98 | t.equal(src.a, dest.a); 99 | t.equal(src.b, dest.b); 100 | t.equal(src.b.bar, dest.b.bar); 101 | 102 | dest.b = new TestClass(); 103 | t.doesNotThrow(function () { 104 | common.merge(src, dest); 105 | }); 106 | t.equal(src.b, dest.b); 107 | t.equal(src.b.bar, dest.b.bar); 108 | 109 | t.end(); 110 | }); 111 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /*───────────────────────────────────────────────────────────────────────────*\ 2 | │ Copyright (C) 2016 PayPal │ 3 | │ │ 4 | │ Licensed under the Apache License, Version 2.0 (the "License"); │ 5 | │ you may not use this file except in compliance with the License. │ 6 | │ You may obtain a copy of the License at │ 7 | │ │ 8 | │ http://www.apache.org/licenses/LICENSE-2.0 │ 9 | │ │ 10 | │ Unless required by applicable law or agreed to in writing, software │ 11 | │ distributed under the License is distributed on an "AS IS" BASIS, │ 12 | │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ 13 | │ See the License for the specific language governing permissions and │ 14 | │ limitations under the License. │ 15 | \*───────────────────────────────────────────────────────────────────────────*/ 16 | 'use strict'; 17 | 18 | const Common = require('./common.js'); 19 | 20 | 21 | class Config { 22 | constructor(data) { 23 | this._store = data; 24 | } 25 | 26 | get(key) { 27 | let obj; 28 | 29 | if (typeof key === 'string' && key.length) { 30 | 31 | key = key.split(':'); 32 | obj = this._store; 33 | 34 | while (obj && key.length) { 35 | if (obj.constructor !== Object) { 36 | // Do not allow traversal into complex types, 37 | // such as Buffer, Date, etc. So, this type 38 | // of key will fail: 'foo:mystring:length' 39 | return undefined; 40 | } 41 | obj = obj[key.shift()]; 42 | } 43 | 44 | return obj; 45 | 46 | } 47 | 48 | return undefined; 49 | } 50 | 51 | set(key, value) { 52 | let obj; 53 | let prop; 54 | 55 | if (typeof key === 'string' && key.length) { 56 | 57 | key = key.split(':'); 58 | obj = this._store; 59 | 60 | while (key.length - 1) { 61 | prop = key.shift(); 62 | 63 | // Create new object for property, if nonexistent 64 | if (!obj.hasOwnProperty(prop)) { 65 | obj[prop] = {}; 66 | } 67 | 68 | obj = obj[prop]; 69 | if (obj && obj.constructor !== Object) { 70 | // Do not allow traversal into complex types, 71 | // such as Buffer, Date, etc. So, this type 72 | // of key will fail: 'foo:mystring:length' 73 | return undefined; 74 | } 75 | } 76 | 77 | return (obj[key.shift()] = value); 78 | } 79 | 80 | return undefined; 81 | } 82 | 83 | use(obj) { 84 | Common.merge(obj, this._store); 85 | } 86 | 87 | merge(config) { 88 | this.use(config._store); 89 | } 90 | } 91 | 92 | module.exports = Config; 93 | -------------------------------------------------------------------------------- /lib/provider.js: -------------------------------------------------------------------------------- 1 | /*───────────────────────────────────────────────────────────────────────────*\ 2 | │ Copyright (C) 2016 PayPal │ 3 | │ │ 4 | │ Licensed under the Apache License, Version 2.0 (the "License"); │ 5 | │ you may not use this file except in compliance with the License. │ 6 | │ You may obtain a copy of the License at │ 7 | │ │ 8 | │ http://www.apache.org/licenses/LICENSE-2.0 │ 9 | │ │ 10 | │ Unless required by applicable law or agreed to in writing, software │ 11 | │ distributed under the License is distributed on an "AS IS" BASIS, │ 12 | │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ 13 | │ See the License for the specific language governing permissions and │ 14 | │ limitations under the License. │ 15 | \*───────────────────────────────────────────────────────────────────────────*/ 16 | 'use strict'; 17 | 18 | const minimist = require('minimist'); 19 | const { debuglog } = require('util'); 20 | const Common = require('./common'); 21 | 22 | 23 | const debug = debuglog('confit'); 24 | 25 | module.exports = { 26 | 27 | argv() { 28 | let result = {}; 29 | let args = minimist(process.argv.slice(2)); 30 | 31 | for (let key of Object.keys(args)) { 32 | if (key === '_') { 33 | // Since the '_' args are standalone, 34 | // just set keys with null values. 35 | for (let prop of args._) { 36 | result[prop] = null; 37 | } 38 | } else { 39 | result[key] = args[key]; 40 | } 41 | } 42 | 43 | return result; 44 | }, 45 | 46 | env(ignore) { 47 | let result = {}; 48 | 49 | // process.env is not a normal object, so we 50 | // need to map values. 51 | for (let env of Object.keys(process.env)) { 52 | //env:env is decided by process.env.NODE_ENV. 53 | //Not allowing process.env.env to override the env:env value. 54 | if (ignore.indexOf(env) < 0) { 55 | result[env] = process.env[env]; 56 | } 57 | } 58 | 59 | return result; 60 | }, 61 | 62 | 63 | convenience() { 64 | let nodeEnv; 65 | let env; 66 | 67 | nodeEnv = process.env.NODE_ENV || 'development'; 68 | env = {}; 69 | 70 | debug(`NODE_ENV set to ${nodeEnv}`); 71 | 72 | // Normalize env and set convenience values. 73 | for (let current of Object.keys(Common.env)) { 74 | let match = Common.env[current].test(nodeEnv); 75 | nodeEnv = match ? current : nodeEnv; 76 | env[current] = match; 77 | } 78 | 79 | debug(`env:env set to ${nodeEnv}`); 80 | 81 | // Set (or re-set) env:{nodeEnv} value in case 82 | // NODE_ENV was not one of our predetermined env 83 | // keys (so `config.get('env:blah')` will be true). 84 | env[nodeEnv] = true; 85 | env.env = nodeEnv; 86 | return { env }; 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /test/provider-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tap').test; 4 | const provider = require('../lib/provider'); 5 | 6 | test('env', function (t) { 7 | const env = process.env; 8 | 9 | t.on('end', function () { 10 | process.env = env; 11 | }); 12 | 13 | t.test('env variables', function (t) { 14 | let val; 15 | 16 | process.env = { 17 | foo: 'bar', 18 | env: 'development' 19 | }; 20 | 21 | val = provider.env(['env']); 22 | t.equal(val.foo, 'bar'); 23 | //env() provider ignores process.env.env 24 | t.equal(val.env, undefined); 25 | t.end(); 26 | }); 27 | 28 | t.end(); 29 | }); 30 | 31 | 32 | test('argv', function (t) { 33 | const argv = process.argv; 34 | 35 | t.on('end', function () { 36 | process.argv = argv; 37 | }); 38 | 39 | t.test('arguments', function (t) { 40 | let val; 41 | 42 | process.argv = [ 'node', __filename, '-a', 'b', '-c', 'd', '--e=f', 'g', 'h' ]; 43 | 44 | val = provider.argv(); 45 | t.equal(typeof val, 'object'); 46 | t.equal(val.a, 'b'); 47 | t.equal(val.c, 'd'); 48 | t.equal(val.e, 'f'); 49 | t.equal(val.g, null); 50 | t.equal(val.h, null); 51 | t.end(); 52 | }); 53 | 54 | t.end(); 55 | }); 56 | 57 | 58 | test('convenience', function (t) { 59 | const env = process.env.NODE_ENV; 60 | 61 | t.on('end', function () { 62 | process.env.NODE_ENV = env; 63 | }); 64 | 65 | t.test('dev', function (t) { 66 | let val; 67 | 68 | process.env.NODE_ENV = 'dev'; 69 | 70 | val = provider.convenience(); 71 | t.equal(val.env.env, 'development'); 72 | t.ok(val.env.development); 73 | t.notOk(val.env.test); 74 | t.notOk(val.env.staging); 75 | t.notOk(val.env.production); 76 | t.end(); 77 | }); 78 | 79 | 80 | t.test('test', function (t) { 81 | let val; 82 | 83 | process.env.NODE_ENV = 'test'; 84 | 85 | val = provider.convenience(); 86 | t.equal(val.env.env, 'test'); 87 | t.notOk(val.env.development); 88 | t.ok(val.env.test); 89 | t.notOk(val.env.staging); 90 | t.notOk(val.env.production); 91 | t.end(); 92 | }); 93 | 94 | 95 | t.test('stage', function (t) { 96 | let val; 97 | 98 | process.env.NODE_ENV = 'stage'; 99 | 100 | val = provider.convenience(); 101 | t.equal(val.env.env, 'staging'); 102 | t.notOk(val.env.development); 103 | t.notOk(val.env.test); 104 | t.ok(val.env.staging); 105 | t.notOk(val.env.production); 106 | t.end(); 107 | }); 108 | 109 | 110 | t.test('prod', function (t) { 111 | let val; 112 | 113 | process.env.NODE_ENV = 'prod'; 114 | 115 | val = provider.convenience(); 116 | t.equal(val.env.env, 'production'); 117 | t.notOk(val.env.development); 118 | t.notOk(val.env.test); 119 | t.notOk(val.env.staging); 120 | t.ok(val.env.production); 121 | t.end(); 122 | }); 123 | 124 | 125 | t.test('none', function (t) { 126 | let val; 127 | 128 | process.env.NODE_ENV = 'none'; 129 | 130 | val = provider.convenience(); 131 | t.equal(val.env.env, 'none'); 132 | t.notOk(val.env.development); 133 | t.notOk(val.env.test); 134 | t.notOk(val.env.staging); 135 | t.notOk(val.env.production); 136 | t.ok(val.env.none); 137 | t.end(); 138 | }); 139 | 140 | 141 | t.test('not set', function (t) { 142 | let val; 143 | 144 | process.env.NODE_ENV = ''; 145 | 146 | val = provider.convenience(); 147 | t.equal(val.env.env, 'development'); 148 | t.ok(val.env.development); 149 | t.notOk(val.env.test); 150 | t.notOk(val.env.staging); 151 | t.notOk(val.env.production); 152 | t.end(); 153 | }); 154 | 155 | t.end(); 156 | }); 157 | -------------------------------------------------------------------------------- /lib/factory.js: -------------------------------------------------------------------------------- 1 | /*───────────────────────────────────────────────────────────────────────────*\ 2 | │ Copyright (C) 2016 PayPal │ 3 | │ │ 4 | │ Licensed under the Apache License, Version 2.0 (the "License"); │ 5 | │ you may not use this file except in compliance with the License. │ 6 | │ You may obtain a copy of the License at │ 7 | │ │ 8 | │ http://www.apache.org/licenses/LICENSE-2.0 │ 9 | │ │ 10 | │ Unless required by applicable law or agreed to in writing, software │ 11 | │ distributed under the License is distributed on an "AS IS" BASIS, │ 12 | │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ 13 | │ See the License for the specific language governing permissions and │ 14 | │ limitations under the License. │ 15 | \*───────────────────────────────────────────────────────────────────────────*/ 16 | 'use strict'; 17 | 18 | const Path = require('path'); 19 | const shush = require('shush'); 20 | const { debuglog } = require('util'); 21 | const Config = require('./config'); 22 | const Common = require('./common'); 23 | const Handlers = require('./handlers'); 24 | const Provider = require('./provider'); 25 | 26 | 27 | const debug = debuglog('confit'); 28 | 29 | class Factory { 30 | 31 | constructor({ basedir, protocols = {}, defaults = 'config.json', envignore = []}) { 32 | this.envignore = envignore.push('env'); 33 | this.basedir = basedir; 34 | this.protocols = protocols; 35 | this.promise = Promise.resolve({}) 36 | .then(store => Common.merge(Provider.convenience(), store)) 37 | .then(Factory.conditional(store => { 38 | let file = Path.join(this.basedir, defaults); 39 | return Handlers.resolveImport(shush(file), this.basedir) 40 | .then(data => Common.merge(data, store)); 41 | })) 42 | .then(Factory.conditional(store => { 43 | let file = Path.join(this.basedir, `${store.env.env}.json`); 44 | return Handlers.resolveImport(shush(file), this.basedir) 45 | .then(data => Common.merge(shush(file), store)); 46 | })) 47 | .then(store => Common.merge(Provider.env(envignore), store)) 48 | .then(store => Common.merge(Provider.argv(), store)); 49 | } 50 | 51 | addDefault(obj) { 52 | this._add(obj, (store, data) => Common.merge(store, data)); 53 | return this; 54 | } 55 | 56 | addOverride(obj) { 57 | this._add(obj, (store, data) => Common.merge(data, store)); 58 | return this; 59 | } 60 | 61 | create(cb) { 62 | this.promise 63 | .then(store => Handlers.resolveImport(store, this.basedir)) 64 | .then(store => Handlers.resolveCustom(store, this.protocols)) 65 | .then(store => Handlers.resolveConfig(store)) 66 | .then(store => cb(null, new Config(store))).catch(cb); 67 | } 68 | 69 | _add(obj, fn) { 70 | let data = this._resolveFile(obj); 71 | let handler = Handlers.resolveImport(data, this.basedir); 72 | this.promise = Promise.all([this.promise, handler]).then(([store, data]) => fn(store, data)); 73 | } 74 | 75 | _resolveFile(path) { 76 | if (typeof path === 'string') { 77 | let file = Common.isAbsolute(path) ? path : Path.join(this.basedir, path); 78 | return shush(file); 79 | } 80 | return path; 81 | } 82 | 83 | static conditional(fn) { 84 | return function (store) { 85 | try { 86 | return fn(store); 87 | } catch (err) { 88 | if (err.code && err.code === 'MODULE_NOT_FOUND') { 89 | debug(`WARNING: ${err.message}`); 90 | return store; 91 | } 92 | throw err; 93 | } 94 | } 95 | } 96 | 97 | } 98 | 99 | module.exports = Factory; 100 | -------------------------------------------------------------------------------- /lib/handlers.js: -------------------------------------------------------------------------------- 1 | /*───────────────────────────────────────────────────────────────────────────*\ 2 | │ Copyright (C) 2016 PayPal │ 3 | │ │ 4 | │ Licensed under the Apache License, Version 2.0 (the "License"); │ 5 | │ you may not use this file except in compliance with the License. │ 6 | │ You may obtain a copy of the License at │ 7 | │ │ 8 | │ http://www.apache.org/licenses/LICENSE-2.0 │ 9 | │ │ 10 | │ Unless required by applicable law or agreed to in writing, software │ 11 | │ distributed under the License is distributed on an "AS IS" BASIS, │ 12 | │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ 13 | │ See the License for the specific language governing permissions and │ 14 | │ limitations under the License. │ 15 | \*───────────────────────────────────────────────────────────────────────────*/ 16 | 'use strict'; 17 | 18 | const shush = require('shush'); 19 | const async = require('async'); 20 | const Shortstop = require('shortstop'); 21 | const { path: createPath } = require('shortstop-handlers'); 22 | 23 | 24 | class Handlers { 25 | 26 | static resolveConfig(data) { 27 | return new Promise((resolve, reject) => { 28 | let usedHandler = false; 29 | 30 | let shorty = Shortstop.create(); 31 | shorty.use('config', function (key) { 32 | usedHandler = true; 33 | 34 | let keys = key.split('.'); 35 | let result = data; 36 | 37 | while (result && keys.length) { 38 | let prop = keys.shift(); 39 | if (!result.hasOwnProperty(prop)) { 40 | return undefined; 41 | } 42 | result = result[prop]; 43 | } 44 | 45 | return keys.length ? null : result; 46 | }); 47 | 48 | async.doWhilst( 49 | function exec(cb) { 50 | usedHandler = false; 51 | shorty.resolve(data, function (err, result) { 52 | if (err) { 53 | cb(err); 54 | return; 55 | } 56 | data = result; 57 | cb(); 58 | }); 59 | }, 60 | 61 | function test() { 62 | return usedHandler; 63 | }, 64 | 65 | function complete(err) { 66 | if (err) { 67 | reject(err); 68 | return; 69 | } 70 | resolve(data); 71 | } 72 | ); 73 | }); 74 | } 75 | 76 | static resolveImport(data, basedir) { 77 | return new Promise((resolve, reject) => { 78 | let path = createPath(basedir); 79 | let shorty = Shortstop.create(); 80 | 81 | shorty.use('import', function (file, cb) { 82 | try { 83 | file = path(file); 84 | return shorty.resolve(shush(file), cb); 85 | } catch (err) { 86 | cb(err); 87 | } 88 | }); 89 | 90 | shorty.resolve(data, function (err, data) { 91 | if (err) { 92 | reject(err); 93 | return; 94 | } 95 | resolve(data); 96 | }); 97 | }); 98 | } 99 | 100 | static resolveCustom(data, protocols) { 101 | return new Promise(function (resolve, reject) { 102 | let shorty = Shortstop.create(); 103 | 104 | for (let protocol of Object.keys(protocols)) { 105 | let impls = protocols[protocol]; 106 | 107 | if (Array.isArray(impls)) { 108 | for (let impl of impls) { 109 | shorty.use(protocol, impl); 110 | } 111 | } else { 112 | shorty.use(protocol, impls); 113 | } 114 | } 115 | 116 | shorty.resolve(data, function (err, data) { 117 | if (err) { 118 | reject(err); 119 | return; 120 | } 121 | resolve(data); 122 | }); 123 | }); 124 | 125 | } 126 | } 127 | 128 | module.exports = Handlers; 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # confit 2 | 3 | [![Build Status](https://travis-ci.org/krakenjs/confit.svg?branch=2.x)](https://travis-ci.org/krakenjs/confit) 4 | 5 | Simple, environment-based configuration. `confit` loads a default JSON 6 | configuration file, additionally loading environment-specific files, if applicable. 7 | It will also process the loaded files using any configured 8 | [shortstop](https://github.com/paypal/shortstop) protocol handlers—see **Options** below. 9 | 10 | `confit` adds support for adding JavaScript-style comments in your json files as each file is processed by [shush](https://github.com/krakenjs/shush) before being merged into your config. 11 | 12 | ## Usage 13 | 14 | ```javascript 15 | const confit = require('confit'); 16 | ``` 17 | 18 | ### confit([options]) 19 | 20 | * `options` (*String* | *Object*) - the base directory in which config files live or a configuration object. If no 21 | arguments is provided, defaults to the directory of the calling file. 22 | * returns - [config factory](#config-factory). 23 | 24 | ```javascript 25 | 'use strict'; 26 | 27 | const path = require('path'); 28 | const confit = require('confit'); 29 | 30 | const basedir = path.join(__dirname, 'config'); 31 | confit(basedir).create(function (err, config) { 32 | config.get; // Function 33 | config.set; // Function 34 | config.use; // Function 35 | 36 | config.get('env:env'); // 'development' 37 | }); 38 | ``` 39 | 40 | ### config factory 41 | 42 | * `addOverride(filepath)` (or) `addOverride(obj)` - Use this to add file (.json or .js), to merge with the config datastore and override the overlapping data if any. Alternatively, you can also pass a json object to override. 43 | * `addDefault(filepath)` (or) `addDefault(obj)` - Use this to add default file (.json or .js), to merge with the config datastore and serve as the default datastore. Alternatively, you can also pass a json object for defaults. 44 | * `create(callback)` - Creates the config object, ready for use. Callback signature: `function (err, config) {}` 45 | 46 | ```javascript 47 | // All methods besides `create` are chainable 48 | confit(options) 49 | .addDefault('./mydefaults.json') //or .addDefault({foo: 'bar'}) 50 | .addOverride('./mysettings.json') //or .addOverride({foo: 'baz'}) 51 | .create(function (err, config) { 52 | // ... 53 | }); 54 | 55 | // - or - 56 | // 57 | // const factory = confit(options); 58 | // factory.addOverride('./mysettings.json'); 59 | // factory.create(function (err, config) { 60 | // // ... 61 | // }); 62 | ``` 63 | 64 | ## Options 65 | 66 | * `basedir` (*String*) - the base directory in which config files can be found. 67 | * `protocols` (*Object*) - An object containing a mapping of 68 | [shortstop](https://github.com/krakenjs/shortstop) protocols to either handler implementations or an array or handler implementations. 69 | These protocols will be used to process the config data prior to registration. 70 | If using an array of handler implementations, each handler is run in series (see [`Multiple handlers` in the shortstop README](https://github.com/krakenjs/shortstop#multiple-handlers)). 71 | * `defaults` (*String*) - the name of the file containing all default values. 72 | Defaults to `config.json`. 73 | * `envignore` (*Array*) - any properties found in `process.env` that should be ignored 74 | 75 | ```javascript 76 | 'use strict'; 77 | 78 | const path = require('path'); 79 | const confit = require('confit'); 80 | const handlers = require('shortstop-handlers'); 81 | 82 | 83 | const options = { 84 | basedir: path.join(__dirname, 'config'), 85 | protocols: { 86 | file: handlers.file(__dirname), 87 | glob: handlers.glob(__dirname) 88 | } 89 | }; 90 | 91 | confit(options).create(function (err, config) { 92 | // ... 93 | }); 94 | ``` 95 | 96 | ## Config API 97 | 98 | * `get(key)` - Retrieve the value for a given key. Colon-delimited keys can be used to traverse the object hierarchy. 99 | * `set(key, value)` - Set a value for the given key. Colon-delimited keys can be used to traverse the object hierarchy. 100 | * `use(obj)` - merge provided object into config. 101 | 102 | ```javascript 103 | config.set('foo', 'bar'); 104 | config.get('foo'); // 'bar' 105 | 106 | config.use({ foo: 'baz' }); 107 | config.get('foo'); // 'baz' 108 | 109 | config.use({ a: { b: { c: 'd' } } } ); 110 | config.get('a:b:c'); // 'd' 111 | ``` 112 | 113 | ## Default Behavior 114 | 115 | By default, `confit` loads `process.env` and `argv` values upon initialization. 116 | Additionally, it creates convenience environment properties prefixed with 117 | `env:` based on the current `NODE_ENV` setting, defaulting to `development`. It 118 | also normalizes `NODE_ENV` settings so values starting with `prod` become 119 | `production`, starting with `stag` become `staging`, starting with `test` 120 | become `test` and starting with `dev` become `development`. 121 | 122 | ```javascript 123 | // NODE_ENV='dev' 124 | config.get('NODE_ENV'); // 'dev' 125 | config.get('env:env'); // 'development' 126 | config.get('env:development'); // true 127 | config.get('env:test'); // false 128 | config.get('env:staging'); // false 129 | config.get('env:production'); // false 130 | ``` 131 | 132 | ```javascript 133 | // NODE_ENV='custom' 134 | config.get('NODE_ENV'); // 'custom' 135 | config.get('env:env'); // 'custom' 136 | config.get('env:development'); // false 137 | config.get('env:test'); // false 138 | config.get('env:staging'); // false 139 | config.get('env:production'); // false 140 | config.get('env:custom'); // true 141 | ``` 142 | 143 | #### Precedence 144 | 145 | Precedence takes the following form (lower numbers overwrite higher numbers): 146 | 147 | 1. command line arguments 148 | 2. env variables 149 | 3. environment-specific config (e.g., `development.json`) 150 | 4. main config (`config.json`) 151 | 5. `env` normalization (`env`, `env:development`, etc) 152 | 153 | #### Shortstop Handlers 154 | 155 | Confit by default comes with 2 shortstop handlers enabled. 156 | 157 | * `import:` 158 | Merges the contents of the specified file into configuration under a given key. 159 | 160 | ```json 161 | { 162 | "foo": "import:./myjsonfile" 163 | } 164 | ``` 165 | 166 | * `config:` 167 | Replaces with the value at a given key. Note that the keys in this case are dot (.) delimited. 168 | 169 | ```json 170 | { 171 | "foo": { 172 | "bar": true 173 | }, 174 | "foobar": "config:foo.bar" 175 | } 176 | ``` 177 | -------------------------------------------------------------------------------- /test/confit-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const test = require('tap').test; 5 | const confit = require('../'); 6 | 7 | const env = process.env.NODE_ENV; 8 | 9 | test('confit', function (t) { 10 | 11 | t.on('end', function () { 12 | process.env.NODE_ENV = env; 13 | }); 14 | 15 | 16 | t.test('api', function (t) { 17 | const factory = confit(); 18 | factory.create(function (err, config) { 19 | t.error(err); 20 | t.equal(typeof config.get, 'function'); 21 | t.equal(typeof config.set, 'function'); 22 | t.equal(typeof config.use, 'function'); 23 | t.end(); 24 | }); 25 | }); 26 | 27 | 28 | t.test('get', function (t) { 29 | //setting process.env.env to development, should not change 'env:env'. 30 | //This should be ignored and 'env:env' should solely depend on process.env.NODE_ENV 31 | process.env.env = 'development'; 32 | 33 | confit().create(function (err, config) { 34 | let val; 35 | 36 | t.error(err); 37 | 38 | val = config.get('env'); 39 | t.equal(typeof val, 'object'); 40 | 41 | val = config.get('env:env'); 42 | t.equal(typeof val, 'string'); 43 | 44 | val = config.get('env:env:length'); 45 | t.equal(typeof val, 'undefined'); 46 | 47 | val = config.get('env:development'); 48 | t.equal(typeof val, 'boolean'); 49 | 50 | val = config.get('env:a'); 51 | t.equal(typeof val, 'undefined'); 52 | 53 | val = config.get('a'); 54 | t.equal(typeof val, 'undefined'); 55 | 56 | val = config.get('a:b'); 57 | t.equal(typeof val, 'undefined'); 58 | 59 | val = config.get('a:b:c'); 60 | t.equal(typeof val, 'undefined'); 61 | 62 | val = config.get(undefined); 63 | t.equal(typeof val, 'undefined'); 64 | 65 | val = config.get(null); 66 | t.equal(typeof val, 'undefined'); 67 | 68 | val = config.get(''); 69 | t.equal(typeof val, 'undefined'); 70 | 71 | val = config.get(false); 72 | t.equal(typeof val, 'undefined'); 73 | 74 | t.end(); 75 | }); 76 | }); 77 | 78 | 79 | t.test('set', function (t) { 80 | confit().create(function (err, config) { 81 | let val; 82 | 83 | t.error(err); 84 | 85 | val = config.set('foo', 'bar'); 86 | t.equal(val, 'bar'); 87 | t.equal(config.get('foo'), 'bar'); 88 | 89 | val = config.set('foo:bar', 'baz'); 90 | t.equal(val, undefined); 91 | t.equal(config.get('foo:bar'), undefined); 92 | 93 | val = config.set('new:thing', 'foo'); 94 | t.equal(val, 'foo'); 95 | t.equal(config.get('new:thing'), 'foo'); 96 | 97 | val = config.set('', 'foo'); 98 | t.equal(val, undefined); 99 | t.equal(config.get(''), undefined); 100 | 101 | val = config.set(undefined, undefined); 102 | t.equal(val, undefined); 103 | 104 | val = config.set('my:prop', 10); 105 | t.equal(val, 10); 106 | t.equal(config.get('my:prop'), 10); 107 | 108 | val = config.set('thing:isEnabled', true); 109 | t.strictEqual(val, true); 110 | t.strictEqual(config.get('thing:isEnabled'), true); 111 | 112 | val = config.set('thing:isEnabled', false); 113 | t.strictEqual(val, false); 114 | t.strictEqual(config.get('thing:isEnabled'), false); 115 | 116 | // Test non-primitives 117 | val = config.set('another:obj', { with: 'prop' }); 118 | t.equal(val.with, 'prop'); 119 | 120 | val = config.get('another:obj'); 121 | t.equal(val.with, 'prop'); 122 | 123 | val = config.get('another:obj:with'); 124 | t.equal(val, 'prop'); 125 | 126 | // Try out arrays 127 | val = config.set('arr', [0,1,2]); 128 | t.ok(Array.isArray(val)); 129 | 130 | val = config.get('arr'); 131 | t.ok(Array.isArray(val)); 132 | t.equal(val[0], 0); 133 | t.equal(val[1], 1); 134 | t.equal(val[2], 2); 135 | 136 | val = config.set('arr:0', 'a'); 137 | t.notOk(val); 138 | 139 | t.end(); 140 | }); 141 | }); 142 | 143 | 144 | t.test('use', function (t) { 145 | confit().create(function (err, config) { 146 | let val; 147 | 148 | t.error(err); 149 | 150 | val = config.use({ foo: { bar: 'baz' } }); 151 | t.notOk(val); 152 | 153 | val = config.get('foo'); 154 | t.equal(typeof val, 'object'); 155 | t.equal(val.bar, 'baz'); 156 | 157 | val = config.get('foo:bar'); 158 | t.equal(val, 'baz'); 159 | 160 | config.use({ arr: [0,1,2] }); 161 | val = config.get('arr'); 162 | t.ok(Array.isArray(val)); 163 | t.equal(val[0], 0); 164 | t.equal(val[1], 1); 165 | t.equal(val[2], 2); 166 | 167 | // Arrays are not merged 168 | config.use({ arr: ['a', 'b', 'c', 'd'] }); 169 | val = config.get('arr'); 170 | t.ok(Array.isArray(val)); 171 | t.equal(val[0], 'a'); 172 | t.equal(val[1], 'b'); 173 | t.equal(val[2], 'c'); 174 | t.equal(val[3], 'd'); 175 | 176 | t.end(); 177 | }); 178 | }); 179 | 180 | 181 | t.test('import protocol', function (t) { 182 | let basedir; 183 | 184 | basedir = path.join(__dirname, 'fixtures', 'import'); 185 | confit(basedir).create(function (err, config) { 186 | t.error(err); 187 | t.equal(config.get('name'), 'parent'); 188 | t.equal(config.get('child:name'), 'child'); 189 | t.equal(config.get('child:grandchild:name'), 'grandchild'); 190 | t.equal(config.get('child:grandchildJson:name'), 'grandchild'); 191 | t.end(); 192 | }); 193 | }); 194 | 195 | 196 | t.test('missing file import', function (t) { 197 | let basedir; 198 | 199 | basedir = path.join(__dirname, 'fixtures', 'import'); 200 | confit(basedir) 201 | .addOverride('./missing.json') 202 | .create(function (err, config) { 203 | t.ok(err); 204 | t.notOk(config); 205 | t.equal(err.message, `Error occured while resolving "import" protocol with value "./orphan.json" at "unknown" handler`); 206 | t.equal(err.cause.code, 'MODULE_NOT_FOUND'); 207 | t.end(); 208 | }); 209 | }); 210 | 211 | 212 | t.test('config protocol', function (t) { 213 | let basedir; 214 | 215 | basedir = path.join(__dirname, 'fixtures', 'config'); 216 | confit(basedir).create(function (err, config) { 217 | t.error(err); 218 | t.ok(config); 219 | t.equal(config.get('name'), 'config'); 220 | t.equal(config.get('foo'), config.get('imported:foo')); 221 | t.equal(config.get('bar'), config.get('foo')); 222 | t.strictEqual(config.get('path:to:nested:value'), config.get('value')); 223 | t.strictEqual(config.get('baz'), config.get('path:to:nested:value')); 224 | t.end(); 225 | }); 226 | }); 227 | 228 | t.test('default file import', function (t) { 229 | let basedir; 230 | 231 | basedir = path.join(__dirname, 'fixtures', 'import'); 232 | confit(basedir) 233 | .addDefault('./default.json') 234 | .create(function (err, config) { 235 | t.error(err); 236 | t.equal(config.get('name'), 'parent'); 237 | t.equal(config.get('foo'), 'bar'); 238 | t.equal(config.get('child:name'), 'child'); 239 | t.equal(config.get('child:grandchild:name'), 'grandchild'); 240 | t.end(); 241 | }); 242 | }); 243 | 244 | 245 | t.test('missing config value', function (t) { 246 | let basedir; 247 | 248 | basedir = path.join(__dirname, 'fixtures', 'config'); 249 | confit(basedir) 250 | .addOverride('./error.json') 251 | .create(function (err, config) { 252 | t.error(err); 253 | t.ok(config); 254 | t.equal(config.get('foo'), undefined); 255 | t.end(); 256 | }); 257 | }); 258 | 259 | 260 | t.test('merge', function (t) { 261 | let basedir; 262 | 263 | basedir = path.join(__dirname, 'fixtures', 'defaults'); 264 | confit(basedir).create(function (err, configA) { 265 | confit().create(function (err, configB) { 266 | t.error(err); 267 | t.doesNotThrow(function () { 268 | configA.merge(configB); 269 | }); 270 | t.end(); 271 | }); 272 | }); 273 | }); 274 | 275 | 276 | t.test('defaults', function (t) { 277 | let basedir; 278 | 279 | // This case should still load the default values 280 | // even though a 'test.json' file does not exist. 281 | process.env.NODE_ENV = 'test'; 282 | basedir = path.join(__dirname, 'fixtures', 'defaults'); 283 | 284 | confit(basedir).create(function (err, config) { 285 | t.error(err); 286 | 287 | // File-based overrides 288 | t.equal(config.get('default'), 'config'); 289 | t.equal(config.get('override'), 'config'); 290 | 291 | // Manual overrides 292 | config.set('override', 'runtime'); 293 | t.equal(config.get('override'), 'runtime'); 294 | 295 | config.use({ override: 'literal' }); 296 | t.equal(config.get('override'), 'literal'); 297 | 298 | t.end(); 299 | }); 300 | }); 301 | 302 | 303 | t.test('overrides', function (t) { 304 | let basedir; 305 | 306 | process.env.NODE_ENV = 'dev'; 307 | basedir = path.join(__dirname, 'fixtures', 'defaults'); 308 | 309 | confit(basedir).create(function (err, config) { 310 | t.error(err); 311 | 312 | // File-based overrides 313 | t.equal(config.get('default'), 'config'); 314 | t.equal(config.get('override'), 'development'); 315 | 316 | // Manual overrides 317 | config.set('override', 'runtime'); 318 | t.equal(config.get('override'), 'runtime'); 319 | 320 | config.use({ override: 'literal' }); 321 | t.equal(config.get('override'), 'literal'); 322 | 323 | t.end(); 324 | }); 325 | 326 | }); 327 | 328 | 329 | t.test('confit addOverride as json object', function (t) { 330 | let basedir; 331 | basedir = path.join(__dirname, 'fixtures', 'config'); 332 | confit(basedir) 333 | .addOverride({ 334 | tic: { 335 | tac: 'toe' 336 | }, 337 | foo: 'bar' 338 | }).create(function (err, config) { 339 | t.error(err); 340 | t.ok(config); 341 | t.equal(config.get('tic:tac'), 'toe'); 342 | t.equal(config.get('foo'),'bar'); 343 | t.equal(config.get('name'), 'config'); 344 | t.end(); 345 | }); 346 | }); 347 | 348 | t.test('confit without files, using just json objects', function(t) { 349 | confit().addDefault({ 350 | foo: 'bar', 351 | tic: { 352 | tac: 'toe' 353 | }, 354 | blue: false 355 | }).addOverride({ 356 | blue: true 357 | }).create(function(err, config) { 358 | t.equal(config.get('foo'), 'bar'); 359 | t.equal(config.get('tic:tac'), 'toe'); 360 | t.equal(config.get('blue'), true); 361 | t.end(); 362 | }); 363 | }); 364 | 365 | 366 | t.test('protocols', function (t) { 367 | let basedir; 368 | let options; 369 | 370 | process.env.NODE_ENV = 'dev'; 371 | basedir = path.join(__dirname, 'fixtures', 'defaults'); 372 | options = { 373 | basedir: basedir, 374 | protocols: { 375 | path: function (value) { 376 | return path.join(basedir, value); 377 | } 378 | } 379 | }; 380 | 381 | confit(options).create(function (err, config) { 382 | t.error(err); 383 | // Ensure handler was run correctly on default file 384 | t.equal(config.get('misc'), path.join(basedir, 'config.json')); 385 | t.equal(config.get('path'), path.join(basedir, 'development.json')); 386 | 387 | config.use({ path: __filename }); 388 | t.equal(config.get('path'), __filename); 389 | t.end(); 390 | }); 391 | }); 392 | 393 | 394 | t.test('protocols (array)', function (t) { 395 | let basedir; 396 | let options; 397 | 398 | process.env.NODE_ENV = 'dev'; 399 | basedir = path.join(__dirname, 'fixtures', 'defaults'); 400 | options = { 401 | basedir: basedir, 402 | protocols: { 403 | path: [ 404 | function (value) { 405 | return path.join(basedir, value); 406 | }, 407 | function (value) { 408 | return value + '!'; 409 | } 410 | ] 411 | } 412 | }; 413 | 414 | confit(options).create(function (err, config) { 415 | t.error(err); 416 | // Ensure handler was run correctly on default file 417 | t.equal(config.get('misc'), path.join(basedir, 'config.json!')); 418 | t.equal(config.get('path'), path.join(basedir, 'development.json!')); 419 | 420 | config.use({ path: __filename }); 421 | t.equal(config.get('path'), __filename); 422 | t.end(); 423 | }); 424 | }); 425 | 426 | 427 | t.test('error', function (t) { 428 | let basedir; 429 | let options; 430 | 431 | process.env.NODE_ENV = 'dev'; 432 | basedir = path.join(__dirname, 'fixtures', 'defaults'); 433 | options = { 434 | basedir: basedir, 435 | protocols: { 436 | path: function (value) { 437 | throw new Error('path'); 438 | } 439 | } 440 | }; 441 | 442 | confit(options).create(function (err, config) { 443 | t.ok(err); 444 | t.equal(err.message, `Error occurred while resolving "path" protocol with value "./config.json" at "path" handler`); 445 | t.notOk(config); 446 | t.end(); 447 | }); 448 | }); 449 | 450 | 451 | t.test('malformed', function (t) { 452 | const basedir = path.join(__dirname, 'fixtures', 'malformed'); 453 | confit(basedir).create(function (err, config) { 454 | t.ok(err); 455 | t.notOk(config); 456 | t.end(); 457 | }); 458 | }); 459 | 460 | 461 | t.test('addOverride', function (t) { 462 | let basedir; 463 | let factory; 464 | 465 | process.env.NODE_ENV = 'test'; 466 | basedir = path.join(__dirname, 'fixtures', 'defaults'); 467 | 468 | factory = confit(basedir); 469 | factory.addOverride('development.json'); 470 | factory.addOverride(path.join(basedir, 'supplemental.json')); 471 | factory.create(function (err, config) { 472 | t.error(err); 473 | t.ok(config); 474 | t.equal(config.get('default'), 'config'); 475 | t.equal(config.get('override'), 'supplemental'); 476 | t.end(); 477 | }); 478 | }); 479 | 480 | 481 | t.test('addOverride error', function (t) { 482 | let basedir; 483 | 484 | 485 | t.throws(function () { 486 | confit(path.join(__dirname, 'fixtures', 'defaults')) 487 | .addOverride('nonexistent.json'); 488 | }); 489 | 490 | t.throws(function () { 491 | confit(path.join(__dirname, 'fixtures', 'defaults')) 492 | .addOverride('malformed.json'); 493 | }); 494 | 495 | t.end(); 496 | }); 497 | 498 | t.test('import: with merging objects in imported files', function(t) { 499 | 500 | const basedir = path.join(__dirname, 'fixtures', 'import'); 501 | const factory = confit(basedir); 502 | factory.addDefault('override.json'); 503 | 504 | factory.create(function(err, config) { 505 | t.error(err); 506 | t.ok(config); 507 | t.equal(config.get('child:grandchild:secret'), 'santa'); 508 | t.equal(config.get('child:grandchild:name'), 'grandchild'); 509 | t.equal(config.get('child:grandchild:another'), 'claus'); 510 | t.end(); 511 | }); 512 | }); 513 | 514 | t.test('precedence', function (t) { 515 | let factory; 516 | const argv = process.argv; 517 | const env = process.env; 518 | 519 | process.argv = [ 'node', __filename, '--override=argv']; 520 | process.env = { 521 | NODE_ENV: 'development', 522 | override: 'env', 523 | misc: 'env' 524 | }; 525 | 526 | factory = confit(path.join(__dirname, 'fixtures', 'defaults')); 527 | factory.create(function (err, config) { 528 | t.error(err); 529 | t.ok(config); 530 | t.equal(config.get('override'), 'argv'); 531 | t.equal(config.get('misc'), 'env'); 532 | process.argv = argv; 533 | process.env = env; 534 | t.end(); 535 | }); 536 | }); 537 | t.test('env ignore', function (t) { 538 | let basedir; 539 | let options; 540 | 541 | const env = process.env = { 542 | NODE_ENV: 'development', 543 | fromlocal: 'config:local', 544 | local: 'motion', 545 | ignoreme: 'file:./path/to/mindyourbusiness' 546 | }; 547 | basedir = path.join(__dirname, 'fixtures', 'defaults'); 548 | options = { 549 | basedir: basedir, 550 | envignore: ['ignoreme'] 551 | }; 552 | 553 | confit(options).create(function (err, config) { 554 | t.error(err); 555 | // Ensure env is read except for the desired ignored property 556 | t.equal(config.get('fromlocal'), env.local); 557 | t.equal(config.get('ignoreme'), undefined); 558 | t.end(); 559 | }); 560 | }); 561 | 562 | t.end(); 563 | }); 564 | --------------------------------------------------------------------------------