├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .nycrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── async-with-builder.js ├── async-with-protocalls.js ├── basic-with-async-hooks.js ├── basic-with-files.js ├── basic-with-hooks.js ├── basic.js ├── complex-configue.js ├── complex.js ├── config-with-protocalls.json ├── config.json ├── config.yaml ├── express-server.js ├── hapi-server.js ├── hapi17-server.js ├── package.json └── secret ├── package-lock.json ├── package.json ├── src ├── core │ ├── builder.js │ ├── configue.js │ ├── getters.js │ ├── utils.js │ └── workflows.js ├── extensions.js └── index.js └── test ├── builder.test.js ├── core.0.resolving.test.js ├── core.1.options.test.js ├── core.2.files.test.js ├── core.3.transform.test.js ├── core.4.tweak-workflow.test.js ├── core.5.shortstop.test.js ├── core.6.misc.test.js ├── data ├── config-bis.json ├── config.json ├── config.json5 ├── config.properties └── config.yaml ├── extensions.express.test.js ├── extensions.hapi.test.js ├── getters.basic-gets.test.js ├── getters.formatkey.test.js ├── getters.getobject.test.js ├── getters.load.test.js └── getters.template.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | coverage 3 | .nyc_output 4 | tmp 5 | private 6 | node_modules 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@-k/base"], 3 | "rules": { 4 | "lodash-fp/use-fp": "warn", 5 | "prefer-rest-params": "warn", 6 | "import/extensions": ["error", {"json": "always"}] 7 | }, 8 | "overrides": [ 9 | { 10 | "files": "examples/*.js", 11 | "rules": { 12 | "import/no-unresolved": "off", 13 | "unicorn/no-process-exit": "off" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Configue CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: 13 | - 8.x 14 | - 10.x 15 | - 12.x 16 | - 14.x 17 | - 16.x 18 | - 18.x 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - run: npm test 28 | - run: npm run publish-coverage 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | test/db 4 | *.old 5 | *.bak 6 | *.tmp 7 | *.log 8 | node_modules* 9 | README.html 10 | *.off 11 | *-off 12 | .DS_Store 13 | npm-debug.log 14 | mem.json 15 | out 16 | test/coverage.html 17 | test/report.html 18 | .idea 19 | .vscode 20 | coverage 21 | /tmp 22 | .nyc_output 23 | private 24 | tmp 25 | examples/package-lock.json 26 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "include": ["src"], 4 | "reporter": ["text", "text-summary", "lcov"] 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to *configue* will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/) 5 | 6 | Up to version [0.7.1] project was know has `hapi-configue`. 7 | 8 | ## [Unreleased][unreleased] 9 | *Nothing So Far* 10 | 11 | ## [1.3.7] - 2023-02-22 12 | Pull request [#41] 13 | ### Fixed 14 | - Address minor vulns running npm audit fix 15 | 16 | ## [1.3.6] - 2021-08-02 17 | Pull request [#31] 18 | ### Fixed 19 | - Address minor vulns running npm audit fix 20 | 21 | ## [1.3.5] - 2020-09-13 22 | ### Fixed 23 | - Address minor vulns running npm audit fix, and patching codecov 24 | 25 | ## [1.3.4] - 2020-05-07 26 | ### Fixed 27 | - do not include tests . in published package, only src and defaults (and LICENSE, package.json, README and CHANGELOG) 28 | 29 | ## [1.3.3] - 2020-05-07 30 | Pull request [#21] 31 | ### Changed 32 | - updated dependencies and fix vuln 33 | - updated README badges 34 | - added node 14 to tested versions 35 | 36 | ## [1.3.2] - 2020-03-09 37 | Pull request [#19] 38 | ### Changed 39 | - updated dependencies and fix vuln 40 | 41 | ## [1.3.1] - 2019-11-18 42 | Pull request [#17] 43 | ### Changed 44 | - refactor with `protocall` v2 with promise support 45 | - use `fp` flavor of `lodash` 46 | - drop `hapi` as dev dependency. (was used in test for plugin) 47 | 48 | ## [1.3.0] - 2019-11-02 49 | Pull request [#16] 50 | ### Changed 51 | - replace shortstop by protocall 52 | - add `protocall` option to replace `shortstop` on long run 53 | 54 | ## [1.2.0] - 2019-11-02 55 | ### Added 56 | - Improved README with return to top links 57 | ### Changed 58 | - upgraded dependencies 59 | - drop @hapi/lab&code for ava 60 | - reorganisation of files 61 | - changed license to MIT 62 | - drop node 8 support 63 | 64 | ## [1.1.2] - 2018-05-15 65 | ### Added 66 | - Improved README with return to top links 67 | ## [1.1.1] - 2018-05-13 68 | ### Changed 69 | - diverses refactors 70 | - split main core file 71 | - introduce helper function 72 | ## [1.1.0] - 2018-05-13 73 | ### Added 74 | - restore async resolve (hence async promise based hooks) 75 | - add support for shortstop protocols in values 76 | - `resolve` getter in fluent builder 77 | - `withOptions` method on the fluent builder 78 | 79 | ## [1.0.1] - 2018-05-12 80 | ### Changed 81 | - Updated dependencies 82 | ## [1.0.0] - 2017-12-18 83 | ### Added 84 | - template function 85 | - getObject factory 86 | - model system 87 | - file type deduction 88 | - specifying a config file through `--configue` option 89 | - getAsync function for asynchronous get 90 | - parse options for argv and env 91 | - tranform options for argv and env 92 | - case convert options for argv and env 93 | - separator joint option 94 | ### Changed 95 | - automatic resolving by default 96 | - postHook now needs to be synchronous 97 | 98 | ## [0.16.0] - 2017-04-03 99 | ### Added 100 | - getFirst accessor 101 | - getAll accessor 102 | 103 | ## [0.15.0] - 2017-04-02 104 | ### Added 105 | - new overrides steps 106 | - new first hook 107 | 108 | ## [0.14.1] - 2017-04-02 109 | ### Added 110 | - can customize the name of the accessor in the hapi server/request 111 | 112 | ## [0.14.0] - 2017-04-01 113 | ### Added 114 | - hapi plugin registration takes care of the resolve 115 | 116 | ### Fixed 117 | - hapi plugin decorated function was not working 118 | 119 | ## [0.13.0] - 2017-04-01 120 | ### Added 121 | - fluent builder 122 | - documentation about configuration 123 | 124 | ## [0.12.0] - 2017-03-31 125 | ### Added 126 | - `argv` and `env` configuration 127 | 128 | ## [0.11.0] - 2017-03-31 129 | ### Added 130 | - promises support 131 | 132 | ## [0.10.1] - 2017-03-31 133 | ### Changed 134 | - upgraded dependencies 135 | 136 | ## [0.10.0] - 2016-02-09 137 | ### Added 138 | - can now specify default value to `get` 139 | 140 | ## [0.9.2] - 2016-02-02 141 | ### Changed 142 | - updated dependencies 143 | 144 | ## [0.9.1] - 2016-02-02 145 | ### Changed 146 | - whole api was refactored: `configue` is now usable without hapi 147 | - `configue` is now registerable to hapi by extracting a plugin from the 148 | instance with `configue.plugin()` 149 | 150 | ## [0.8.2] - 2016-01-08 151 | ### Changed 152 | - Updated Examples 153 | 154 | ## [0.8.1] - 2016-01-08 155 | ### Added 156 | - Setup code quality harness with eslint and Code Climate 157 | 158 | ### Changed 159 | - Improved README 160 | 161 | ## [0.8.0] - 2016-01-08 162 | ### Changed 163 | - Upgrade package name to *Configue* 164 | - Deprecated previous name with 165 | 166 | npm deprecate hapi-configue "<=v0.7.1" "WARNING: This project has been renamed to configue. Install using configue instead." 167 | 168 | ## [0.7.1] - 2016-01-08 169 | ### Added 170 | - Warning for migration to *Configue* 171 | 172 | ## [0.7.0] - 2015-12-07 173 | ### Added 174 | - defaults options 175 | 176 | ## [0.6.0] - 2015-12-05 177 | ### Changed 178 | - hook can now be asynchronous and must be provided with a callback 179 | ### Fixed 180 | - default and overrides hooks in option were not accepted by Joi 181 | 182 | ## [0.5.0] - 2015-12-03 183 | ### Added 184 | - enable full customization with `customWorkflow` 185 | ## [0.4.0] - 2015-12-03 186 | ### Added 187 | - `.npmignore` for lighter package 188 | ## [0.3.3] - 2015-12-03 189 | ### Added 190 | - support to configure files with a single path, or array of path (as string) 191 | 192 | ## [0.3.2] - 2015-12-03 193 | ### Added 194 | - new tests 195 | ### Fixed 196 | - file were overloading each other 197 | - nconf was persisting between creations 198 | 199 | ## [0.3.1] - 2015-12-02 200 | ### Added 201 | - cucumber integration test 202 | ### Fixed 203 | - env variable were ignored 204 | 205 | ## [0.3.0] - 2015-12-02 206 | ### Added 207 | - Joi validation of options 208 | - Hook support 209 | 210 | ## [0.2.0] - 2015-12-02 211 | ### Added 212 | - Minimal "Step" definition 213 | - Usage Example 214 | - More detailed documentation (README+jsdoc) 215 | 216 | ## [0.1.0] - 2015-12-01 217 | - Initial Release 218 | 219 | [#31]: https://github.com/AdrieanKhisbe/configue/pull/31 220 | [#19]: https://github.com/AdrieanKhisbe/configue/pull/19 221 | [#17]: https://github.com/AdrieanKhisbe/configue/pull/17 222 | [#16]: https://github.com/AdrieanKhisbe/configue/pull/16 223 | [unreleased]: https://github.com/AdrieanKhisbe/configue/compare/v1.3.6...HEAD 224 | [1.3.6]: https://github.com/AdrieanKhisbe/configue/compare/v1.3.5...v1.3.6 225 | [1.3.5]: https://github.com/AdrieanKhisbe/configue/compare/v1.3.4...v1.3.5 226 | [1.3.4]: https://github.com/AdrieanKhisbe/configue/compare/v1.3.3...v1.3.4 227 | [1.3.3]: https://github.com/AdrieanKhisbe/configue/compare/v1.3.2...v1.3.3 228 | [1.3.2]: https://github.com/AdrieanKhisbe/configue/compare/v1.3.1...v1.3.2 229 | [1.3.1]: https://github.com/AdrieanKhisbe/configue/compare/v1.3.0...v1.3.1 230 | [1.3.0]: https://github.com/AdrieanKhisbe/configue/compare/v1.2.0...v1.3.0 231 | [1.2.0]: https://github.com/AdrieanKhisbe/configue/compare/v1.1.2...v1.2.0 232 | [1.1.2]: https://github.com/AdrieanKhisbe/configue/compare/v1.1.1...v1.1.2 233 | [1.1.1]: https://github.com/AdrieanKhisbe/configue/compare/v1.1.0...v1.1.1 234 | [1.1.0]: https://github.com/AdrieanKhisbe/configue/compare/v1.0.1...v1.1.0 235 | [1.0.1]: https://github.com/AdrieanKhisbe/configue/compare/v1.0.0...v1.0.1 236 | [1.0.0]: https://github.com/AdrieanKhisbe/configue/compare/v0.16.0...v1.0.0 237 | [0.16.0]: https://github.com/AdrieanKhisbe/configue/compare/v0.15.0...v0.16.0 238 | [0.15.0]: https://github.com/AdrieanKhisbe/configue/compare/v0.14.1...v0.15.0 239 | [0.14.1]: https://github.com/AdrieanKhisbe/configue/compare/v0.14.0...v0.14.1 240 | [0.14.0]: https://github.com/AdrieanKhisbe/configue/compare/v0.13.0...v0.14.0 241 | [0.13.0]: https://github.com/AdrieanKhisbe/configue/compare/v0.12.0...v0.13.0 242 | [0.12.0]: https://github.com/AdrieanKhisbe/configue/compare/v0.11.0...v0.12.0 243 | [0.11.0]: https://github.com/AdrieanKhisbe/configue/compare/v0.10.1...v0.11.0 244 | [0.10.1]: https://github.com/AdrieanKhisbe/configue/compare/v0.10.0...v0.10.1 245 | [0.10.0]: https://github.com/AdrieanKhisbe/configue/compare/v0.9.2...v0.10.0 246 | [0.9.2]: https://github.com/AdrieanKhisbe/configue/compare/v0.9.1...v0.9.2 247 | [0.9.1]: https://github.com/AdrieanKhisbe/configue/compare/v0.8.2...v0.9.1 248 | [0.8.2]: https://github.com/AdrieanKhisbe/configue/compare/v0.8.1...v0.8.2 249 | [0.8.1]: https://github.com/AdrieanKhisbe/configue/compare/v0.8.0...v0.8.1 250 | [0.8.0]: https://github.com/AdrieanKhisbe/configue/compare/v0.7.1...v0.8.0 251 | [0.7.1]: https://github.com/AdrieanKhisbe/hapi-configue/compare/v0.7.0...v0.7.1 252 | [0.7.0]: https://github.com/AdrieanKhisbe/hapi-configue/compare/v0.6.0...v0.7.0 253 | [0.6.0]: https://github.com/AdrieanKhisbe/hapi-configue/compare/v0.5.0...v0.6.0 254 | [0.5.0]: https://github.com/AdrieanKhisbe/hapi-configue/compare/v0.4.0...v0.5.0 255 | [0.4.0]: https://github.com/AdrieanKhisbe/hapi-configue/compare/v0.3.3...v0.4.0 256 | [0.3.3]: https://github.com/AdrieanKhisbe/hapi-configue/compare/v0.3.2...v0.3.3 257 | [0.3.2]: https://github.com/AdrieanKhisbe/hapi-configue/compare/v0.3.1...v0.3.2 258 | [0.3.1]: https://github.com/AdrieanKhisbe/hapi-configue/compare/v0.3.0...v0.3.1 259 | [0.3.0]: https://github.com/AdrieanKhisbe/hapi-configue/compare/v0.2.0...v0.3.0 260 | [0.2.0]: https://github.com/AdrieanKhisbe/hapi-configue/compare/v0.1.0...v0.2.0 261 | [0.1.0]: https://github.com/AdrieanKhisbe/hapi-configue/compare/e482070....v0.1.0 262 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019-2023 Adrien Becchis (AdrieanKhisbe) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | 23 | Originaly BSD-3-Clause form license: 24 | Copyright (c) 2016-8, Adrien Becchis 25 | Copyright (c) 2015, Adrien Becchis, Maxime Lequain and others contributor 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Configue 2 | 3 | > ***CONFIGUE ALL THE THINGS \o/*** 4 | 5 | [![npm version][npm-badge]][npm-url] 6 | [![Build Status][ci-badge]][ci-url] 7 | [![Coverage Status][coverage-badge]][coverage-url] 8 | [![License: MIT][license-badge]][license-url] 9 | 10 | [Configue] is a *node.js* config library to easily customize your app from argv, env, files and more. 11 | 12 | It defines a *conventional workflow* to load a config from environment variables, 13 | command line arguments, files, that you can easily *configure* and *extend*. 14 | 15 | #### Table of Content 16 | - [About Configue](#about-configue) 17 | - [Installation](#installation) 18 | - [Usage](#usage) 19 | * [How To](#how-to) 20 | * [Basic usage without customization](#basic-usage-without-customization) 21 | + [Basic Configue](#basic-configue) 22 | + [Basic Async Configue](#basic-async-configue) 23 | + [Passing By Options](#passing-by-options) 24 | * [Retrieving values](#retrieving-values) 25 | + [Simple `get`](#simple-get) 26 | + [Retrieving Specified Object](#retrieving-specified-object) 27 | - [Load and getObject for punctual retrieval](#load-and-getobject-for-punctual-retrieval) 28 | - [*Models* for frequent usage](#models-for-frequent-usage) 29 | + [Template string](#template-string) 30 | + [Argv / Env direct access](#argv--env-direct-access) 31 | * [Usage with customization of the configuration workflow](#usage-with-customization-of-the-configuration-workflow) 32 | + [Specifying Files](#specifying-files) 33 | + [Using Protocalls(Shortstop protocols)](#using-protocallshortstop-protocols) 34 | + [Passing options to nconf to configure argv and env](#passing-options-to-nconf-to-configure-argv-and-env) 35 | + [Joint options of argv and env to process the values](#joint-options-of-argv-and-env-to-process-the-values) 36 | + [Disabling Steps](#disabling-steps) 37 | + [Step hooks](#step-hooks) 38 | + [Custom Workflow](#custom-workflow) 39 | * [Loading into Hapi](#loading-into-hapi) 40 | * [Loading into express](#loading-into-express) 41 | - [Configuration Recap](#configuration-recap) 42 | * [Configuration Object](#configuration-object) 43 | * [Fluent builder](#fluent-builder) 44 | 45 | ## About Configue[↥] 46 | 47 | [Configue] builds up on [nconf] and its 48 | [Hierarchical configuration](https://github.com/indexzero/nconf#hierarchical-configuration) system. 49 | 50 | It defines a list of _configuration steps_ that are executed in order. 51 | Every property defined on a steps will shadow the same key in the following steps. 52 | 53 | Quoting [nconf]: 54 | > ***"The order in which you attach these configuration sources determines their priority in the hierarchy"*** 55 | 56 | Here are the standard steps [Configue] does define: 57 | - `overrides` : properties that would take precedence on all other steps 58 | - `argv` : command line options 59 | - *configueFile* : file specified by `--configue` in argv if any. 60 | - `env` : environment variables 61 | - `file` : config files 62 | - `defaults` : default objects 63 | 64 | The plugin loads the various configurations in order using _predefined steps_. 65 | 66 | It starts by parsing *argv* then goes through the *env* and the files options 67 | and finishes by loading the default config objects if any. 68 | Hence why every option defined as an argument commandline will override defaults 69 | and environment variables. 70 | 71 | If `--configue` option is specified, the config the specified file holds would 72 | be loaded after the *argv* and before the *env*. This is to enable you to save 73 | many options in many files, and specify at launch with options you want to use. 74 | 75 | ## Installation[↥] 76 | 77 | Just add `configue` has a dependency installing it with `npm`, or with `yarn`. 78 | 79 | npm install --save configue 80 | 81 | yarn add configue 82 | 83 | 84 | ## Usage[↥] 85 | 86 | ### How To[↥] 87 | 88 | To use _Configue_ you need first to create an instance passing the option to the `Configue(opts)` 89 | constructor. Resolving of the config is now done synchronously and automatically unless you specify 90 | the `defer: true` option, or if you opt in for an async resolve. 91 | In case of a problem with the configue options it will throw an Error. 92 | 93 | See the following examples for concrete presentation. 94 | 95 | ### Basic usage without customization[↥] 96 | #### Basic Configue[↥] 97 | ```js 98 | const Configue = require('configue'); 99 | 100 | const configue = new Configue(); 101 | 102 | const who = configue.get('who', 'World'); 103 | console.log(`Hello ${who}`); 104 | ``` 105 | 106 | #### Basic Async Configue[↥] 107 | ```js 108 | const Configue = require('configue'); 109 | 110 | const configue = new Configue({async: true}); 111 | configue.resolve().then(() => { 112 | const who = configue.get('who', 'World'); 113 | console.log(`Hello ${who}`); 114 | }); 115 | ``` 116 | 117 | Async resolve is necessary for some advanced features like async hooks and [shortstop] protocols. 118 | 119 | #### Passing By Options[↥] 120 | You can specify the `who` configue in different manners. 121 | Here are some: 122 | 123 | ```sh 124 | node basic.js --who=Woman 125 | # configue through Env 126 | export who=Man ; node basic.js 127 | who=Human node basic.js 128 | node basic.js --configue=my-who-conf.json 129 | ``` 130 | 131 | The full example is available in the [`examples`](./examples/basic.js) folder. 132 | 133 | ### Retrieving values[↥] 134 | You can retrieve values from the store in different manner, `get` is the most simple one. 135 | 136 | #### Simple `get`[↥] 137 | 138 | To retrieve a *configue* value, use the `get` method on the config holder. 139 | It takes has argument the key of the argument. For nested value you need to 140 | use `:` or `.` to deep access to value, or you can an array of keys. 141 | 142 | It's also possible to specify a default value in case key is `undefined`. 143 | 144 | ```js 145 | configue.get('defined4sure'); 146 | configue.get('some:nested:value'); 147 | configue.get('some.other.nested.value'); 148 | configue.get(['yet', 'another', 'nested', 'value']); 149 | configue.get('mightBeUndefined', 'default'); 150 | ``` 151 | 152 | You can also retrieve a list of value with `getAll`, or the first non undefined value from a list with `getFirst` 153 | 154 | ```js 155 | configue.getAll('defined4sure', 'some:nested:value'); 156 | configue.getAll(['defined4sure', 'some:nested:value']); 157 | configue.getAll(['some.other.nested.value', ['yet', 'another', 'nested', 'value']]); 158 | 159 | configue.getFirst('defined4sure', 'some:nested:value'); 160 | configue.getFirst(['defined4sure', 'some:nested:value'], optionalDefaultValue); 161 | ``` 162 | 163 | #### Retrieving Specified Object[↥] 164 | 165 | When you can to retrieve several values in the same time you can forge object so that they have structure you need. 166 | 167 | ##### Load and getObject for punctual retrieval[↥] 168 | The two main methods are `load` and `getObject` 169 | - `load` by default return the whole merged config as an object. But you can give him a model that would be used 170 | to craft an object. The model is a object whose leaves are configue keys, or array of configue key: 171 | ex: `{serverConfig: {host: 'app:server:host', port: 'PORT'}, ...}` 172 | - `getObject` that takes a list of key, and return an object formed by key / values 173 | 174 | ```js 175 | const {serverConfig} = configue.load({ 176 | serverConfig: {host: 'app:server:host', port: 'PORT'}, 177 | extraOptions: '...' 178 | }); 179 | 180 | const {file, prefix} = configue.getObject('file', 'prefix'); 181 | ``` 182 | 183 | ##### *Models* for frequent usage[↥] 184 | There are case where the forged object are to be used several times, and you dont want to query them over and over. 185 | To do that you can predefined *models* in the configuration. These would be populated once during automatic resolved, 186 | and they would be made accessible under the `_` key. 187 | 188 | ```js 189 | const configue = new Configue({ 190 | models: { 191 | serverConfig: {host: 'app:server:host', port: 'PORT'}, 192 | otherModel: {a: 'a', b: '...'} 193 | } 194 | }); 195 | //... 196 | console.log(configue._.serverConfig); // => host: ..., port: ... 197 | ``` 198 | 199 | #### Template string[↥] 200 | One last way you can get config value is via the `configue.template` (aliased to `configue.t`). 201 | This is a template function you can prefix a template string. The interpolated values will be keys of the 202 | *configue* and then replaced by their value: 203 | 204 | ```js 205 | console.log(configue.t`I will say ${'salute'} to ${'who'}`); 206 | // => I will say Hello to You 207 | // (supposing called with --salute=Hello --who=You) 208 | ``` 209 | You can defined default values by passing a default object to the `template` method: 210 | ```js 211 | console.log( 212 | configue.t({times: 2, who: 'World'})`I will say ${'salute'} to ${'who'} ${'times'} times` 213 | ); 214 | // => I will say Hello to You 2 times 215 | ``` 216 | 217 | #### Argv / Env direct access[↥] 218 | For ease of the the `argv` and `env` can be directly accessible from the *configue* instance: 219 | 220 | ```js 221 | console.log(configue.argv.host); 222 | console.log(configue.env.HOME); 223 | ``` 224 | 225 | Note that values are neither parsed nor transformed. 226 | 227 | ### Usage with customization of the configuration workflow[↥] 228 | 229 | #### Specifying Files[↥] 230 | 231 | The files key can contain a single object or an array of objects containing a `file` key containing the path to the config file. 232 | The object can also reference a *nconf* plugin tasked with the formatting using the key `format`. 233 | 234 | Starting from 1.0 the formatter to use can be automatically deduced for standard files. Supported extensions are 235 | `json`, `yaml`/`yml` but also `properties`/`ini` and `json5` In that case you just need to specify the name of the file. 236 | 237 | ```js 238 | const Configue = require('configue'); 239 | 240 | const configueOptions = { 241 | disable: {argv: true}, 242 | files: [ 243 | {file: './config.json'}, 244 | { 245 | file: './config.yaml', 246 | format: require('nconf-yaml') 247 | }, 248 | 'my-own.properties' 249 | ] 250 | }; 251 | 252 | const configue = new Configue(configueOptions); 253 | ``` 254 | 255 | Note that if only one file is needed, its path can be directly given as options. 256 | 257 | #### Using Protocalls(Shortstop protocols)[↥] 258 | 259 | [Protocall][protocall](originally [Shortstop][shortstop]) is a library that help transform json values by interpreting their content. 260 | Quoting documentation: 261 | > it enables the use of protocols and handlers to enable identification and special handling of json values. 262 | 263 | For instance value with the standard `env` and `file` protocol, 264 | `env:MY_ENV_VARIABLE` will be replaced with the value of `MY_ENV_VARIABLE` while value `file:/some/path` will be 265 | resolved with the content of the given file. 266 | For more details refer to the [protocall] project. 267 | 268 | To enable it, you just need to have `{async: true, protocall: true}` in your *Configue* config object. 269 | 270 | By default *Configue* comes empowered with the protocols from [shortstop-handlers]: 271 | `env`, `file`, `path`, `exec`, `base64`, `require`. 272 | 273 | You can customize behavior of **protocall** by passing a config object as option: 274 | - You can add extra protocols via the `protocols` options, by passing an object `{$protocolName: $handler}` 275 | - You can also precise the `baseDir` for `file`, `path`, `exec`, `require` default handler. (default being current working directory) 276 | - If you don't want the default protocols, use the `noDefaultProtocols` option. 277 | - By default the Buffer are stringified by *Configue*, but you can choose to preserve them with the `preserveBuffer` option. 278 | 279 | For an example of configuration refer to the [following examples](examples/async-with-protocalls.js) using 280 | [this json file](examples/config-with-protocalls.json) as part of the config. 281 | 282 | #### Passing options to nconf to configure argv and env[↥] 283 | You can provide options arguments to `argv` (`yargs`underneath), and `env` in order to customize the behavior 284 | around command line argument and environment variables. 285 | For more in depth readings see nconf options [here][nconf-options-argv-env] 286 | 287 | ```js 288 | const Configue = require('configue'); 289 | 290 | const configueOptions = { 291 | argv: { 292 | f: { 293 | alias: 'file', 294 | demandOption: true, 295 | default: '/etc/passwd', 296 | describe: 'x marks the spot', 297 | type: 'string' 298 | } 299 | }, 300 | env: ['HOME', 'PWD'] // whitelist 301 | }; 302 | 303 | const configue = new Configue(configueOptions); 304 | ``` 305 | 306 | #### Joint options of argv and env to process the values[↥] 307 | It is possible to process the raw values you can get from the `argv` and `env` step, with the **parse**, **separator** 308 | **ignorePrefix**, **normalize** and **transform** options. 309 | 310 | First you can specify to parse values with the **`parse`** option. Argv and Env value will be then parse, 311 | which is convenient to pass simple json from the command line. 312 | 313 | A **`separator`** option is there to indicate the token that will be used to split a key and consider it as a nested value. 314 | This affects both the argv and env step. The value can be either a string, or a regexp such as `'__'` or `/__|--/` 315 | 316 | With **`ignorePrefix`** you can list of prefix for argv and env variable you want to be removed. This is particulary useful 317 | with environment variables that are prefixed with your app name. 318 | 319 | Also a **`normalize`** option enables you to make the variable names uniform with the same case, while using the idiomatic 320 | case for the argv flag name and env variable. (for instance `--my-var` and `MY_VAR`). 321 | This option accept as config the name of case function of lodash, the most useful being `camelCase` which will 322 | transform our both variable into `myVar` as we would like name the javascript variable. 323 | (other options are `kebabCase`, `startCase`, `snakeCase`,`upperCase`, `lowerCase`) 324 | 325 | If you have more complex processing of the env/arg variable name or value, you can use the **`transform`** option, 326 | which accept a function `({key, value}) => ({key:someKey, value:someValue})` that will be passed to nconf. (cf [nconf doc][nconf-transform]) 327 | This will happen after the ignore prefix, and before the case normalisation. 328 | 329 | #### Disabling Steps[↥] 330 | 331 | The argv and env steps can be skipped using the `disable` object in `options`. 332 | 333 | ```js 334 | const configue = new Configue({disable: {argv: true}}); 335 | // ... 336 | ``` 337 | 338 | There is no disabling for `overrides`, `files` and `default`; you just have to don't provide the matching option. 339 | 340 | #### Step hooks[↥] 341 | 342 | Every step (`overrides`, `argv`, `env`, `files`, `defaults`) has a post hook available. 343 | Those can be defined using the `postHooks` key and accept a function that take `nconf`. 344 | In async mode those hooks can be asynchronous by returning a Promise. 345 | 346 | The special hooks `first` enables you to respectively apply a function on nconf at the very beginning. 347 | 348 | ```js 349 | const configue = new Configue({ 350 | postHooks: { 351 | first: function first(nconf) { 352 | // Your code here 353 | }, 354 | overrides: function postOverrides(nconf) { 355 | // Your code here 356 | }, 357 | argv: function postArgv(nconf) { 358 | // Your code here 359 | } 360 | } 361 | }); 362 | // Your code here 363 | ``` 364 | 365 | #### Custom Workflow[↥] 366 | 367 | If needed you can have your full custom configuration workflow, 368 | simply by providing an object with the single key `customWorkflow` 369 | attached to a function taking the `nconf` object, and a `done` callback. 370 | 371 | ```js 372 | const configueOptions = { 373 | customWorkflow(nconf, done) { 374 | // my own config setting 375 | } 376 | }; 377 | 378 | const configue = new Configue(configueOptions); 379 | ``` 380 | 381 | If you use a `yargs` instance, you can assign it to `nconf._yargs` so that `argv` 382 | is directly accessible from `configue.argv 383 | 384 | ### Loading into Hapi[↥] 385 | 386 | Thought _Configue_ is usable without hapi`, (it was originally just a _Hapi_ plugin), 387 | it can be easily loaded in `hapi` to have the _configue_ being easily accessible from 388 | the server, or the request. 389 | 390 | To do this, you need to register the plugin. It takes care to resolve the config if 391 | was not done due to defer. 392 | ```js 393 | const Hapi = require('hapi'); 394 | const Configue = require('configue'); 395 | 396 | const configue = new Configue({some: 'complex config with a model connexion'}); 397 | 398 | const server = new Hapi.Server(); 399 | server.connection(configue._.connexion); // note usage of the model connexion with port in it. 400 | server.register({register: configue.plugin()}, err => { 401 | // starting the server or else 402 | 403 | // access to the config 404 | const config = server.configue('some'); // => 'config' 405 | const configGet = server.configue.get('some'); // => 'config' 406 | // Any other call to server.configue.getAsync/getFirst/getAll/getObject/template/load 407 | // ... 408 | }); 409 | ``` 410 | A more complete example is available in [`examples`](examples/hapi-server.js) folder. 411 | 412 | Note it's possible to provide to `configue.plugin()` a `decorateName` so that you use a custom accessor on `server` or `request`. 413 | 414 | **Warning**: the original plugin is made for the pre 17 version of `hapi`. If you are using `hapi@17 or beyond, 415 | please retrive the plugin with the `plugin17()` method as you can see in the [example server](examples/hapi17-server.js). 416 | 417 | ### Loading into express[↥] 418 | Configue can also be loaded into `express` via it's middleware you can obtain by `configue.middleware()` you just have 419 | to feed to `app.use()` 420 | 421 | A example is available in the [`examples`](examples/express-server.js) folder. 422 | 423 | ## Configuration Recap[↥] 424 | Configue can be configured in two different way. Either using a config object or using a fluent builder. 425 | 426 | ### Configuration Object[↥] 427 | Here is a recap of how the configuration should look like. All options are optional: 428 | 429 | - `customWorkflow`: a function manipulating the `nconf`. This option is exclusive of all others 430 | - `argv`: Config object for `yargv`, a map of config key with an object values (`alias`, `demandOption`, `default`,`describe`, `type`) 431 | - `env`: The options for the `nconf.env` method that can be: 432 | - an array of string, the whitelisted env method 433 | - an object with key: `match`, `whitelist 434 | - `disable`: A object with key `argv` and/or `env` attach to a boolean indicated whether the step should be disable. 435 | - `files`: file or list of files. (object `file`, `format`) 436 | - `defaults`: Object of key mapped to default values. (or array of them) 437 | - `overrides`: Object of key mapped to overrides values. 438 | - `required`: list of key that are required one way or another 439 | - `postHooks`: an object of (`step`: function hook) 440 | step being one of `first`, `overrides`, `argv`, `env`, `files` `defaults` 441 | - `parse`: boolean to request parsing of argv/env value 442 | - `transform`: a function to process argv and env values 443 | - `normalize`: the case name in which you want keys to be converted 444 | - `ignorePrefix`: a prefix or list of them you want to be remove from key name 445 | - `protocall`/`shortstop`: to activate and customize the protocall/shortstop protocols. (prefer `protocall`, `shortstop` is to be deprecated) 446 | - `async`: to activate async mode which will defer resolve 447 | - `defer`: to defer automatic resolve in sync mode 448 | 449 | For more details you can see the `internals.schema` in the `configue-core.js` file around the line 60 450 | 451 | ### Fluent builder[↥] 452 | 453 | Instead to use the configuration object provided to the `Configue` constructor, you can use the fluent builder. 454 | This consist in chaining a list of configuration methods before to retrieve the instance to a `get` method or via a 455 | `resolve` method. 456 | 457 | Here is a simple example: 458 | 459 | ```js 460 | const configue = Configue.defaults({a: 1, b: '2'}) 461 | .parse(true) 462 | .normalize('camelCase') 463 | .get(); 464 | ``` 465 | You can provide a portion of option with the `withOption` method as you can see in this example using `resolve`: 466 | 467 | ```js 468 | Configue.defaults({a: 1, b: '2'}) 469 | .withOptions({parse: true, normalize: 'camelCase'}) 470 | .protocall(true) 471 | .resolve(configue => { 472 | // here goes your code 473 | }); 474 | ``` 475 | 476 | Here is the builder function list, the function name being the name of the key in he object config (except the postHooks function and `withOptions`): 477 | `argv`, `async`, `customWorkflow`, `defaults`, `overrides`, `disable`, `env`, `files`, `required`, `transform`, `parse`, `normalize`, `separator`, `protocall` 478 | and `firstHook`, `overridesHook`, `argvHook`, `envHook`, `filesHook`, `defaultsHook`, `withOptions` 479 | 480 | [↥] 481 | 482 | [↥]: #configue 483 | [Configue]: https://github.com/AdrieanKhisbe/configue 484 | [github-repo]: https://github.com/AdrieanKhisbe/configue 485 | [nconf]: https://github.com/indexzero/nconf 486 | [nconf-options-argv-env]: https://github.com/indexzero/nconf#argv 487 | [nconf-transform]: https://github.com/indexzero/nconf#transform-functionobj 488 | [protocall]: https://github.com/omni-tools/protocall 489 | [shortstop]: https://github.com/krakenjs/shortstop 490 | [shortstop-handlers]: https://github.com/krakenjs/shortstop-handlers 491 | 492 | [npm-badge]: https://img.shields.io/npm/v/configue.svg 493 | [npm-url]: https://npmjs.com/package/configue 494 | [ci-badge]: https://github.com/AdrieanKhisbe/configue/actions/workflows/ci.yml/badge.svg 495 | [ci-url]: https://github.com/AdrieanKhisbe/configue/actions 496 | [coverage-badge]: https://codecov.io/gh/AdrieanKhisbe/configue/branch/master/graph/badge.svg 497 | [coverage-url]: https://codecov.io/gh/AdrieanKhisbe/configue 498 | [license-badge]: https://img.shields.io/badge/License-MIT-blue.svg 499 | [license-url]: https://opensource.org/licenses/MIT 500 | -------------------------------------------------------------------------------- /examples/async-with-builder.js: -------------------------------------------------------------------------------- 1 | const Configue = require('configue'); 2 | 3 | Configue.disable({env: true}) 4 | .files([ 5 | {file: '../examples/config.json'}, 6 | { 7 | file: '../examples/config.yaml', 8 | format: require('nconf-yaml') 9 | } 10 | ]) 11 | .resolve() 12 | .then(configue => { 13 | const salute = configue.get('salute', 'Hello'); 14 | const who = configue.get('who', 'World'); 15 | 16 | console.log(`${salute} ${who}!`); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/async-with-protocalls.js: -------------------------------------------------------------------------------- 1 | const Configue = require('configue'); 2 | 3 | const configueOptions = { 4 | async: true, 5 | disable: {env: true}, 6 | protocall: true, 7 | files: [{file: './config-with-protocalls.json'}] 8 | }; 9 | 10 | Configue.withOptions(configueOptions) 11 | .resolve() 12 | .then(configue => { 13 | console.log(`Secret ${configue.get('salute', 'Hello')} to ${configue.get('who', 'World')}`); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/basic-with-async-hooks.js: -------------------------------------------------------------------------------- 1 | const Configue = require('configue'); 2 | 3 | const configueOptions = { 4 | async: true, 5 | postHooks: { 6 | argv: function postArgv(nconf) { 7 | // Perform some async operation to fetch value on mongo, etcd, api or else 8 | return Promise.resolve('42').then(apiValue => { 9 | nconf.set('hook', `async post-argv hook:${apiValue}`); 10 | }); 11 | } 12 | } 13 | }; 14 | 15 | Configue.withOptions(configueOptions).resolve(configue => { 16 | const who = configue.get('who', 'World'); 17 | const hook = configue.get('hook', 'none'); 18 | 19 | console.log(`Configue: {who: ${who}, hook: ${hook}}`); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/basic-with-files.js: -------------------------------------------------------------------------------- 1 | const Configue = require('configue'); 2 | 3 | const configueOptions = { 4 | defer: true, 5 | disable: {argv: true}, 6 | files: [ 7 | {file: './config.json'}, 8 | { 9 | file: './config.yaml', 10 | format: require('nconf-yaml') 11 | } 12 | ] 13 | }; 14 | 15 | const configue = new Configue(configueOptions); 16 | 17 | try { 18 | console.log(`Need to call resolve defer being true, so resolve is ${configue.resolved}`); 19 | configue.resolve(); 20 | 21 | const salute = configue.get('salute', 'Hello'); 22 | const who = configue.get('who', 'World'); 23 | 24 | console.log(`The Configue tell that "who" is ${who} and "salute" is ${salute}`); 25 | } catch (err) { 26 | console.error('Error loading plugins:\n %s', err); 27 | } 28 | -------------------------------------------------------------------------------- /examples/basic-with-hooks.js: -------------------------------------------------------------------------------- 1 | const Configue = require('configue'); 2 | 3 | const configueOptions = { 4 | postHooks: { 5 | argv: function postArgv(nconf) { 6 | nconf.set('hook', 'post-argv hook'); 7 | } 8 | } 9 | }; 10 | 11 | const configue = new Configue(configueOptions); 12 | const who = configue.get('who', 'World'); 13 | const hook = configue.get('hook', 'none'); 14 | 15 | console.log(`Configue: {who: ${who}, hook: ${hook}}`); 16 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | const Configue = require('configue'); 2 | 3 | const configue = new Configue(); 4 | const who = configue.get('who', 'World'); 5 | console.log(`I know that "who" is ${who}`); 6 | -------------------------------------------------------------------------------- /examples/complex-configue.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | disable: {env: true}, 3 | argv: { 4 | salute: { 5 | alias: 's', 6 | type: 'string' 7 | }, 8 | times: { 9 | alias: 'n', 10 | type: 'number' 11 | } 12 | }, 13 | files: [ 14 | { 15 | file: './config.yaml', 16 | format: require('nconf-yaml') 17 | } 18 | ], 19 | defaults: {salute: 'hey', who: 'you', times: 1}, 20 | models: { 21 | hello: {hello: 'salute', target: 'who'}, 22 | bonjour: c => ({bonjour: c('salute'), cible: 'lemonde', langue: c('lang', 'fr')}) 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /examples/complex.js: -------------------------------------------------------------------------------- 1 | const Configue = require('configue'); 2 | const configue = new Configue(require('./complex-configue')); 3 | 4 | console.log(configue._.hello); 5 | console.log(configue._.bonjour); 6 | console.log(configue.t({who: 'World'})`I will say ${'salute'} to ${'who'} ${'times'} times`); 7 | -------------------------------------------------------------------------------- /examples/config-with-protocalls.json: -------------------------------------------------------------------------------- 1 | { 2 | "who": "file:secret", 3 | "salute": "base64:QmlnIFVw" 4 | } -------------------------------------------------------------------------------- /examples/config.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /examples/config.yaml: -------------------------------------------------------------------------------- 1 | # Basic Yaml Config 2 | salute: Yo 3 | config-file: Yaml -------------------------------------------------------------------------------- /examples/express-server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const Configue = require('configue'); 3 | 4 | const configue = new Configue(); 5 | 6 | const app = express(); 7 | 8 | app.use(configue.middleware()); 9 | 10 | app.get('/', function(req, res) { 11 | res.send(`Hello ${req.configue('who', 'World')}!`); 12 | }); 13 | 14 | const port = configue.get('port', 3000); 15 | app.listen(port, () => { 16 | console.log(`Server running at: ${port}`); 17 | console.log(configue.t({who: 'World'})`With "who" as ${'who'}`); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/hapi-server.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | const Configue = require('configue'); 3 | 4 | const configue = new Configue({ 5 | models: {connexion: {port: 'port'}}, 6 | normalize: 'camelCase', 7 | defaults: {host: 'localhost', port: 3000} 8 | }); 9 | 10 | const server = new Hapi.Server(); 11 | server.connection(configue._.connexion); 12 | 13 | server.register({register: configue.plugin()}, err => { 14 | if (err) return console.log('Error loading plugins'); 15 | 16 | const who = server.configue('who', 'World'); 17 | 18 | server.route({ 19 | method: 'GET', 20 | path: '/', 21 | handler(request, reply) { 22 | const salute = request.configue('salute', 'hello'); 23 | reply(`${salute} ${who}`); 24 | } 25 | }); 26 | 27 | server.start(function() { 28 | console.log(`Server running at: ${server.info.uri}`); 29 | console.log(`With "who" as ${who}`); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/hapi17-server.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | const Configue = require('configue'); 3 | 4 | const configue = new Configue({ 5 | models: {serverOptions: {host: 'host', port: 'port'}}, 6 | normalize: 'camelCase', 7 | defaults: {host: 'localhost', port: 3000} 8 | }); 9 | 10 | const server = new Hapi.Server(configue._.serverOptions); 11 | 12 | server.route({ 13 | method: 'GET', 14 | path: '/', 15 | handler(request) { 16 | const who = server.configue('who', 'World'); 17 | const salute = request.configue('salute', 'hello'); 18 | return `${salute} ${who}`; 19 | } 20 | }); 21 | 22 | const start = async () => { 23 | try { 24 | await server.register(configue.plugin17()); 25 | await server.start(); 26 | } catch (err) { 27 | console.log(err); 28 | process.exit(1); 29 | } 30 | console.log(`Server running at: ${server.info.uri}`); 31 | console.log(configue.t`With "who" as ${'who'}`); 32 | }; 33 | 34 | start(); 35 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "configue-examples", 3 | "version": "1.2.0", 4 | "description": "Examples for configue", 5 | "main": "basic.js", 6 | "private": true, 7 | "scripts": { 8 | "start": "node basic.js" 9 | }, 10 | "keywords": [ 11 | "hapi", 12 | "config" 13 | ], 14 | "author": "Adriean Khisbe (https://github.com/AdrieanKhisbe/)", 15 | "license": "MIT", 16 | "dependencies": { 17 | "configue": "..", 18 | "express": "^4.17.1", 19 | "hapi": "16", 20 | "nconf": "^0.10.0", 21 | "nconf-yaml": "^1.0.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/secret: -------------------------------------------------------------------------------- 1 | Toto -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "configue", 3 | "version": "1.3.7", 4 | "description": "Configue is a config library to easily customize your app from argv, env, files and more.", 5 | "license": "MIT", 6 | "repository": "https://github.com/AdrieanKhisbe/configue", 7 | "homepage": "https://github.com/AdrieanKhisbe/configue#readme", 8 | "keywords": [ 9 | "configuration", 10 | "config", 11 | "settings", 12 | "key value store", 13 | "nconf", 14 | "shortstop" 15 | ], 16 | "engines": { 17 | "node": ">=8.10.0" 18 | }, 19 | "main": "src/index.js", 20 | "author": "Adrien Becchis (https://github.com/AdrieanKhisbe)", 21 | "maintainers": [ 22 | "Adrien Becchis (https://github.com/AdrieanKhisbe)" 23 | ], 24 | "contributors": [ 25 | "Adrien Becchis (https://github.com/AdrieanKhisbe)", 26 | "Maxime Lequain (https://github.com/mlequain)" 27 | ], 28 | "bugs": { 29 | "url": "https://github.com/AdrieanKhisbe/configue/issues" 30 | }, 31 | "scripts": { 32 | "lint": "eslint .", 33 | "test:only": "ava", 34 | "test": "npm run lint && nyc npm run test:only", 35 | "publish-coverage": "codecov" 36 | }, 37 | "files": [ 38 | "src" 39 | ], 40 | "dependencies": { 41 | "bluebird": "^3.7.2", 42 | "joi": "^14.3.1", 43 | "json5": "^2.2.3", 44 | "lodash": "^4.17.21", 45 | "nconf": "^0.11.4", 46 | "nconf-properties": "^0.3.0", 47 | "nconf-yaml": "^1.0.2", 48 | "protocall": "^2.0.0", 49 | "yargs": "^15.4.1" 50 | }, 51 | "devDependencies": { 52 | "@-k/eslint-plugin": "^0.3.1", 53 | "ava": "^2.4.0", 54 | "codecov": "^3.8.3", 55 | "eslint": "^6.8.0", 56 | "nyc": "^15.1.0" 57 | }, 58 | "ava": { 59 | "files": [ 60 | "test/**/*.test.js" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/core/builder.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line lodash-fp/use-fp 2 | const _ = require('lodash'); // keep vanilaa flavor for mutating assign and set 3 | 4 | const optionKeys = [ 5 | 'files', 6 | 'defaults', 7 | 'disable', 8 | 'env', 9 | 'argv', 10 | 'customWorkflow', 11 | 'required', 12 | 'overrides', 13 | 'defer', 14 | 'parse', 15 | 'transform', 16 | 'normalize', 17 | 'separator', 18 | 'async', 19 | 'shortstop' 20 | ]; 21 | 22 | module.exports = function extendWithFluentBuilder(Configue) { 23 | Configue._options = {}; 24 | optionKeys.forEach(option => { 25 | Configue[option] = opt => { 26 | Configue._options[option] = opt; 27 | return Configue; 28 | }; 29 | }); 30 | Configue.protocall = opt => { 31 | Configue._options.protocall = opt; 32 | if (opt) Configue._options.async = true; 33 | return Configue; 34 | }; 35 | Configue.shortstop = Configue.protocall; 36 | ['first', 'overrides', 'argv', 'env', 'files', 'defaults'].forEach(hook => { 37 | Configue[`${hook}Hook`] = opt => { 38 | _.set(Configue._options, `postHooks.${hook}`, opt); 39 | return Configue; 40 | }; 41 | }); 42 | Configue.withOptions = options => { 43 | _.assign(Configue._options, options); 44 | return Configue; 45 | }; 46 | Configue.get = () => { 47 | const options = Configue._options; 48 | Configue._options = {}; 49 | return new Configue(options); 50 | }; 51 | Configue.resolve = configueContinuation => { 52 | Configue._options.async = true; 53 | const configue = Configue.get(); 54 | if (configueContinuation) return configue.resolve().then(configueContinuation); 55 | return configue.resolve(); 56 | }; 57 | return Configue; 58 | }; 59 | -------------------------------------------------------------------------------- /src/core/configue.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const _ = require('lodash/fp'); 3 | const Promise = require('bluebird'); 4 | const protocall = require('protocall'); 5 | const {getPaths} = require('./utils'); 6 | const {applyDefaultWorkflow, applyDefaultWorkflowAsync} = require('./workflows'); 7 | 8 | const Configue = (module.exports = function Configue(options = {}) { 9 | if (!(this instanceof Configue)) { 10 | return new Configue(options); 11 | } 12 | 13 | // load fresh instance of nconf 14 | delete require.cache[require.resolve('nconf')]; 15 | 16 | const nconf = (this.nconf = require('nconf')); 17 | const settings = (this.settings = _.clone(options)); 18 | this.resolved = false; 19 | this.async = options.async; 20 | 21 | if (settings.protocall) this.protocallResolver = createProtocallResolver(settings.protocall); 22 | if (settings.shortstop) this.protocallResolver = createProtocallResolver(settings.shortstop); 23 | 24 | const results = Joi.validate(settings, configueOptionsSchema); 25 | if (results.error) throw results.error; 26 | if ((settings.shortstop || settings.protocall) && !settings.async) 27 | throw new Error('Protocall(Shortstop) usage requires async mode'); 28 | nconf.use('memory'); 29 | nconf.clear(); 30 | 31 | if (!options.defer && !options.async) { 32 | this.resolve(); 33 | } 34 | }); 35 | 36 | /** 37 | * Resolve the configue 38 | */ 39 | Configue.prototype.resolve = function() { 40 | if (this.resolved) return this.async ? Promise.resolve(this) : undefined; 41 | 42 | const completeSetup = () => { 43 | this.resolved = true; 44 | this.argv = _.get('_yargs.argv', this.nconf); 45 | this.env = process.env; 46 | this.populateModels(); 47 | }; 48 | if (this.async) { 49 | return (this.settings.customWorkflow 50 | ? Promise.resolve(this.settings.customWorkflow(this.nconf)) 51 | : Promise.resolve(applyDefaultWorkflowAsync(this.nconf, this.settings)) 52 | ) 53 | .then(() => { 54 | if (this.settings.protocall) { 55 | return resolveProtocalls( 56 | this.nconf, 57 | this.protocallResolver, 58 | this.settings.protocall.preserveBuffer 59 | ); 60 | } 61 | }) 62 | .then(this.populateModels.bind(this)) 63 | .then(completeSetup) 64 | .then(() => this); 65 | } else { 66 | if (this.settings.customWorkflow) this.settings.customWorkflow(this.nconf); 67 | else applyDefaultWorkflow(this.nconf, this.settings); 68 | completeSetup(); 69 | } 70 | }; 71 | 72 | /** 73 | * Populate defined models 74 | */ 75 | Configue.prototype.populateModels = function() { 76 | this._ = _.mapValues(value => this.load(value), this.settings.models); 77 | }; 78 | 79 | /** 80 | * Joi options schema 81 | */ 82 | const configueOptionsSchema = [ 83 | Joi.object({defer: Joi.boolean(), async: Joi.boolean(), customWorkflow: Joi.func()}), 84 | Joi.object().keys({ 85 | async: Joi.boolean(), 86 | defer: Joi.boolean(), 87 | disable: Joi.object({ 88 | argv: Joi.boolean(), 89 | env: Joi.boolean() 90 | }), 91 | normalize: Joi.string().valid([ 92 | 'camelCase', 93 | 'kebabCase', 94 | 'startCase', 95 | 'snakeCase', 96 | 'upperCase', 97 | 'lowerCase' 98 | ]), 99 | transform: [Joi.func(), Joi.array().items(Joi.func())], 100 | parse: Joi.boolean(), 101 | separator: [Joi.string(), Joi.object().type(RegExp)], 102 | ignorePrefix: [Joi.string(), Joi.array().items(Joi.string())], 103 | shortstop: [ 104 | // TODO: mark as deprecated 105 | Joi.boolean(), 106 | Joi.object().keys({ 107 | protocols: Joi.object(), 108 | preserveBuffer: Joi.boolean(), 109 | noDefaultProtocols: Joi.boolean(), 110 | baseDir: Joi.string() 111 | }) 112 | ], 113 | protocall: [ 114 | Joi.boolean(), 115 | Joi.object().keys({ 116 | protocols: Joi.object(), 117 | preserveBuffer: Joi.boolean(), 118 | noDefaultProtocols: Joi.boolean(), 119 | baseDir: Joi.string() 120 | }) 121 | ], 122 | argv: [Joi.object(), Joi.func()], 123 | env: [Joi.object(), Joi.array().items(Joi.string())], 124 | files: [ 125 | Joi.string(), 126 | Joi.array().items( 127 | Joi.object({ 128 | file: Joi.string().required(), 129 | format: Joi.object({stringify: Joi.func(), parse: Joi.func()}) 130 | }) 131 | ), 132 | Joi.array().items(Joi.string()) 133 | ], 134 | defaults: [Joi.object(), Joi.array().items(Joi.object())], 135 | overrides: Joi.object(), 136 | required: Joi.array().items(Joi.string()), 137 | postHooks: Joi.object({ 138 | first: Joi.func(), 139 | overrides: Joi.func(), 140 | argv: Joi.func(), 141 | env: Joi.func(), 142 | files: Joi.func(), 143 | defaults: Joi.func() 144 | }), 145 | models: Joi.object() 146 | }) 147 | ]; 148 | 149 | const createProtocallResolver = options => { 150 | const protocallResolver = _.get('noDefaultProtocols', options) 151 | ? new protocall.Resolver() 152 | : protocall.getDefaultResolver(_.getOr(process.cwd(), 'baseDir', options)); 153 | 154 | if (_.has('protocols', options)) protocallResolver.use(options.protocols); 155 | 156 | return protocallResolver; 157 | }; 158 | 159 | const resolveProtocalls = (nconf, resolver, preseveBuffer) => { 160 | const originalValues = nconf.load(); 161 | return resolver.resolve(originalValues).then(resolvedValues => { 162 | const paths = getPaths(originalValues); 163 | for (const path of paths) { 164 | const resolvedValue = _.get(path, resolvedValues); 165 | if (_.get(path, originalValues) !== resolvedValue) 166 | nconf.set( 167 | path.join(':'), 168 | _.isBuffer(resolvedValue) && !preseveBuffer ? resolvedValue.toString() : resolvedValue 169 | ); 170 | } 171 | }); 172 | }; 173 | -------------------------------------------------------------------------------- /src/core/getters.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash/fp'); 2 | const Promise = require('bluebird'); 3 | const {getPaths} = require('./utils'); 4 | 5 | /** 6 | * Format the key to a nconf compatible format 7 | * @param key the key as array or string 8 | * @returns string formated to be accepted by nconf 9 | */ 10 | function formatKey(key) { 11 | if (_.isArray(key)) return key.join(':'); 12 | else if (key.includes('.') && !key.includes(':')) return key.replace('.', ':'); 13 | return key; 14 | } 15 | 16 | /** 17 | * Get a value from the configue 18 | * @param key queried key 19 | * @param defaultValue default value if key not present 20 | * @returns {*} value or default 21 | */ 22 | function get(key, defaultValue) { 23 | const result = this.nconf.get(formatKey(key)); 24 | return result === undefined ? defaultValue : result; 25 | } 26 | 27 | /** 28 | * Get asynchronously a value from the configue 29 | * 30 | * Support callback or promise api depending if the last arg 31 | * 32 | * @param key queried key 33 | * @param defaultOrCallback callback or optional default value for promise mode 34 | * @returns {*} A promise if no callback given 35 | */ 36 | function getAsync(key, defaultOrCallback) { 37 | if (typeof defaultOrCallback === 'function') { 38 | this.nconf.get(formatKey(key), defaultOrCallback); 39 | } else { 40 | return Promise.fromCallback(cb => 41 | this.nconf.get(formatKey(key), (err, res) => cb(err, res || defaultOrCallback)) 42 | ); 43 | } 44 | } 45 | 46 | /** 47 | * Get first defined value from configue 48 | * 49 | * Keys can be given as spread argument, in that case it's not possible 50 | * to give a default value 51 | * 52 | * @param keys list of keys 53 | * @param defaultValue default value 54 | * @returns {*} first defined value 55 | */ 56 | function getFirst(keys, defaultValue, ...rest) { 57 | const hasDefault = _.isArray(keys) && _.isEmpty(rest); 58 | for (const key of hasDefault ? keys : [keys, defaultValue, ...rest]) { 59 | const result = this.nconf.get(formatKey(key)); 60 | if (result !== undefined) return result; 61 | } 62 | return hasDefault ? defaultValue : undefined; 63 | } 64 | 65 | /** 66 | * Get all keys from config 67 | * 68 | * Keys can be given as spread argument 69 | * 70 | * @param keys all keys that are to be fetched 71 | * @returns {Array} array of fetched values 72 | */ 73 | function getAll(...keys) { 74 | return (Array.isArray(_.head(keys)) ? _.head(keys) : keys).map(key => 75 | this.nconf.get(formatKey(key)) 76 | ); 77 | } 78 | 79 | const configueTemplate = (configue, defaults = {}) => (chains, ...keys) => { 80 | return _.reduce( 81 | (acc, [chain, key]) => { 82 | const base = acc + chain; 83 | if (!key) return base; 84 | return base + configue.get(formatKey(key), _.get(key, defaults)); 85 | }, 86 | '', 87 | _.zip(chains, keys) 88 | ); 89 | }; 90 | 91 | /** 92 | * Template string function, that populate the interpolated keys by they configue value 93 | * 94 | * ex: configue.template`my template string ${mykey}` 95 | * 96 | * The function can be called with a dict of default values, and will return the actual 97 | * template string function 98 | * 99 | * ex:configue.template`my template string ${mykey}` 100 | * 101 | * @param defaultOrChain the default object or the template chains 102 | * @param keys the keys of the template string 103 | * @returns {Function|string} The template string populated or the function with default to serve as template string function 104 | */ 105 | function template(defaultOrChain, ...keys) { 106 | return _.isPlainObject(defaultOrChain) 107 | ? configueTemplate(this, defaultOrChain) 108 | : configueTemplate(this)(defaultOrChain, ...keys); 109 | } 110 | 111 | /** 112 | * Get an object with {key: configueValue(key)} 113 | * @param args the keys to form object with 114 | * @returns {object} the forged object 115 | */ 116 | function getObject(...args) { 117 | const keys = args.length === 1 && Array.isArray(_.first(args)) ? _.first(args) : args; 118 | return _.reduce( 119 | (memo, key) => { 120 | const [fromKey, toKey] = Array.isArray(key) ? key : [key, key]; 121 | return _.set(toKey, this.get(fromKey), memo); 122 | }, 123 | {}, 124 | keys 125 | ); 126 | } 127 | 128 | const populateObj = (configue, obj, paths) => 129 | _.reduce((memo, path) => _.set(path, configue.getFirst(_.get(path, obj)), memo), {}, paths); 130 | 131 | // TODO (maybe) List of key!! (filter) -> its a getALL!! 132 | /** 133 | * Load the configue into one object, eventually based on a model 134 | * 135 | * "model" is either: 136 | * - a (nested) object whose leafes are keys to be replaced by their associated value 137 | * - a function taking a config getter as argument and returning the object 138 | * 139 | * @param model eventual model to load 140 | * @returns {*} all the config or a partial model 141 | */ 142 | function load(model) { 143 | return model 144 | ? _.isFunction(model) 145 | ? model(makeConfigGetter(this)) 146 | : populateObj(this, model, getPaths(model)) 147 | : this.nconf.load(); 148 | } 149 | 150 | function makeConfigGetter(configue) { 151 | const configGetter = configue.get.bind(configue); 152 | configGetter.get = configGetter; 153 | configGetter.getFirst = configue.getFirst.bind(configue); 154 | configGetter.getAll = configue.getAll.bind(configue); 155 | configGetter.getObject = configue.getObject.bind(configue); 156 | configGetter.load = configue.load.bind(configue); 157 | configGetter.template = configue.template.bind(configue); 158 | configGetter.t = configGetter.template; 159 | return configGetter; 160 | } 161 | 162 | module.exports = { 163 | formatKey, 164 | get, 165 | getAsync, 166 | getFirst, 167 | getAll, 168 | template, 169 | getObject, 170 | load, 171 | makeConfigGetter 172 | }; 173 | -------------------------------------------------------------------------------- /src/core/utils.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash/fp'); 2 | 3 | const getPaths = (obj, basePath = []) => 4 | _.reduce( 5 | (memo, path) => 6 | _.isPlainObject(obj[path]) 7 | ? [...memo, ...getPaths(obj[path], [...basePath, path])] 8 | : [...memo, [...basePath, path]], 9 | [], 10 | _.keys(obj) 11 | ); 12 | 13 | module.exports = {getPaths}; 14 | -------------------------------------------------------------------------------- /src/core/workflows.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Yargs = require('yargs'); 3 | const _ = require('lodash/fp'); 4 | const Promise = require('bluebird'); 5 | 6 | /** 7 | * Apply the Default Configuration Workflow 8 | * @param nconf - the nconf object 9 | * @param settings - plugin config 10 | */ 11 | const applyDefaultWorkflow = function applyDefaultWorkflow(nconf, settings) { 12 | // Load eventual overrides values and then iterates over the different steps (in order: argv, env, files, default) 13 | processHook(nconf, settings.postHooks, 'first'); 14 | iterateSteps(nconf, STEPS, settings); 15 | checkRequired(nconf, settings); 16 | }; 17 | 18 | /** 19 | * Apply the Default Configuration Workflow in Async mode 20 | * @param nconf - the nconf object 21 | * @param settings - plugin config 22 | */ 23 | const applyDefaultWorkflowAsync = function applyDefaultWorkflowAsync(nconf, settings) { 24 | return Promise.resolve(processHook(nconf, settings.postHooks, 'first')) 25 | .then(() => iterateSteps(nconf, STEPS, settings)) 26 | .then(() => checkRequired(nconf, settings)); 27 | }; 28 | 29 | /** 30 | * Iterate synchronously over the various steps 31 | * @param nconf - nconf instance 32 | * @param steps - list of configuration steps 33 | * @param settings - project settings 34 | * @returns {Function} 35 | */ 36 | const iterateSteps = (nconf, steps, settings) => { 37 | const hooks = settings.postHooks; 38 | if (settings.async) { 39 | return Promise.each(steps, stepName => { 40 | return STEP_ACTIONS[stepName](nconf, settings).then(() => 41 | processHook(nconf, hooks, stepName) 42 | ); 43 | }); 44 | } 45 | for (const stepName of steps) { 46 | STEP_ACTIONS[stepName](nconf, settings); 47 | processHook(nconf, hooks, stepName); 48 | } 49 | }; 50 | 51 | /** 52 | * Ordered list of configuration steps 53 | * @type {string[]} 54 | */ 55 | const STEPS = ['overrides', 'argv', 'env', 'files', 'defaults']; 56 | // Definition of associated actions below 57 | 58 | /** 59 | * Process the hook for a given step 60 | * @param nconf nconf instance 61 | * @param hooks defined hooks 62 | * @param stepName 63 | * @returns {Function} 64 | */ 65 | const processHook = (nconf, hooks, stepName) => { 66 | const hook = _.get(stepName, hooks); 67 | if (hook) return hook(nconf); 68 | }; 69 | 70 | /** 71 | * Process the hook for a given step 72 | * @param nconf nconf instance 73 | * @param options configue options 74 | */ 75 | const checkRequired = (nconf, options) => { 76 | if (options.required) { 77 | nconf.required(options.required); 78 | } 79 | }; 80 | 81 | const getTransformForNormalize = (normalize, separator) => { 82 | const normalizer = _[normalize]; 83 | if (!separator) 84 | return ({key, value}) => { 85 | if (key === '_') return {key, value}; 86 | return {key: normalizer(key), value}; 87 | }; 88 | return ({key, value}) => { 89 | if (key === '_') return {key, value}; 90 | const separatorMatch = key.match(separator); 91 | if (separatorMatch) { 92 | const actualSeparator = separatorMatch[0]; 93 | const normalizedKey = key 94 | .split(separator) 95 | .map(normalizer) 96 | .join(actualSeparator); 97 | return {key: normalizedKey, value}; 98 | } else return {key: normalizer(key), value}; 99 | }; 100 | }; 101 | 102 | const getTransformForIgnorePrefix = prefix => { 103 | return ({key, value}) => ({key: key.replace(new RegExp(`^${prefix}`), ''), value}); 104 | }; 105 | 106 | const getTransformer = options => { 107 | const transformers = []; 108 | if (options.ignorePrefix) 109 | transformers.push(..._.flatten([options.ignorePrefix]).map(getTransformForIgnorePrefix)); 110 | if (options.transform) transformers.push(options.transform); 111 | if (options.normalize) 112 | transformers.push(getTransformForNormalize(options.normalize, options.separator)); 113 | if (_.isEmpty(transformers)) return; 114 | return kv => _.flatten(transformers).reduce((acc, transformer) => transformer(acc), kv); 115 | }; 116 | 117 | const promiseIfNeeded = options => (options.async ? Promise.resolve() : undefined); 118 | 119 | /** 120 | * Load argv step adding eventualy file passed by command line 121 | * @param nconf - nconf instance 122 | * @param options - plugin options 123 | */ 124 | const loadArgv = (nconf, options) => { 125 | if (_.has('disable.argv', options)) return promiseIfNeeded(options); 126 | 127 | const argOpts = options.argv || {}; 128 | // craft own yargs if needed that is transmit to nconf 129 | const yargs = _.has('argv', argOpts) ? argOpts : Yargs(process.argv.slice(2)).options(argOpts); 130 | yargs.separator = options.separator; 131 | if (options.parse !== undefined) yargs.parseValues = options.parse; 132 | 133 | const transformer = getTransformer(options); 134 | if (transformer) yargs.transform = transformer; 135 | 136 | nconf.argv(yargs); 137 | nconf._yargs = yargs; 138 | const argvFile = nconf.get('configue'); 139 | if (argvFile) { 140 | nconf.file('argv-configue', argvFile); 141 | } 142 | return promiseIfNeeded(options); 143 | }; 144 | 145 | /** 146 | * Load env step 147 | * @param nconf - nconf instance 148 | * @param options - plugin options 149 | */ 150 | const loadEnv = (nconf, options) => { 151 | if (_.has('disable.env', options)) return promiseIfNeeded(options); 152 | 153 | const envOpts = _.isArray(options.env) ? {whitelist: options.env} : options.env || {}; 154 | envOpts.parseValues = options.parse; 155 | envOpts.separator = options.separator; 156 | 157 | const transformer = getTransformer(options); 158 | if (options.normalize) { 159 | envOpts.whitelist = _.map(_[options.normalize], envOpts.whitelist); 160 | } 161 | 162 | if (transformer) envOpts.transform = transformer; 163 | 164 | nconf.env(envOpts); 165 | return promiseIfNeeded(options); 166 | }; 167 | 168 | const nconfYaml = require('nconf-yaml'); 169 | const nconfProperties = require('nconf-properties'); 170 | const nconfJson5 = require('json5'); 171 | 172 | const fileTypeAssociation = { 173 | json: null, 174 | yaml: nconfYaml, 175 | yml: nconfYaml, 176 | properties: nconfProperties, 177 | ini: nconfProperties, 178 | json5: nconfJson5 179 | }; 180 | /** 181 | * Return the format associated to the type of the file 182 | * @param file name of file 183 | * @returns {*} format associated to file 184 | */ 185 | const nconfFormatForFile = file => { 186 | const ext = path.extname(file); 187 | return fileTypeAssociation[ext.slice(1)]; 188 | }; 189 | 190 | /** 191 | * Load the files in options using nconf.file 192 | * @param nconf - nconf instance 193 | * @param options - plugin options 194 | */ 195 | const loadFiles = (nconf, options) => { 196 | const files = options.files; 197 | if (Array.isArray(files) && files.length > 0) { 198 | for (const file of files) { 199 | const path = _.getOr(file, 'file', file); 200 | // file(.file) is used as namespace for nconf 201 | const formater = file.format || nconfFormatForFile(path); 202 | nconf.file(path, formater ? {file: path, format: formater} : file); 203 | } 204 | } else if (typeof files === 'string' && files.length > 0) { 205 | const formater = nconfFormatForFile(files); 206 | if (formater) nconf.file({file: files, format: formater}); 207 | else nconf.file(files); 208 | } 209 | return promiseIfNeeded(options); 210 | }; 211 | 212 | /** 213 | * Load the defaults in options using nconf.defaults 214 | * @param nconf - nconf instance 215 | * @param options - plugin options 216 | */ 217 | const loadDefaults = function loadDefaults(nconf, options) { 218 | const defaults = options.defaults; 219 | nconf.defaults(Array.isArray(defaults) ? _.defaultsAll(defaults) : defaults); 220 | return promiseIfNeeded(options); 221 | }; 222 | 223 | /** 224 | * Load the overrides in options using nconf.overrides 225 | * @param nconf - nconf instance 226 | * @param options - plugin options 227 | */ 228 | const loadOverrides = function loadOverrides(nconf, options) { 229 | const overrides = options.overrides; 230 | nconf.overrides(overrides); 231 | return promiseIfNeeded(options); 232 | }; 233 | 234 | /** 235 | * Steps and their associated action function 236 | * @type {{argv: Function, env: Function, files: loadFiles}} 237 | */ 238 | const STEP_ACTIONS = { 239 | overrides: loadOverrides, 240 | argv: loadArgv, 241 | env: loadEnv, 242 | files: loadFiles, 243 | defaults: loadDefaults 244 | }; 245 | 246 | module.exports = {applyDefaultWorkflow, applyDefaultWorkflowAsync}; 247 | -------------------------------------------------------------------------------- /src/extensions.js: -------------------------------------------------------------------------------- 1 | const {makeConfigGetter} = require('./core/getters'); 2 | 3 | const rawHapiPlugin = function(configue, decorateName = 'configue') { 4 | /** 5 | * Register the Configue plugin and process the various steps and hooks 6 | * @param server - Hapi server to configure 7 | * @param options - options of the Configue Plugin 8 | * @param next - plugin continuation 9 | */ 10 | return function plugin(server, options, next) { 11 | const register = () => { 12 | server.log(['plugin', 'info'], 'Registering the configue as decoration'); 13 | const configGetter = makeConfigGetter(configue); 14 | server.decorate('server', decorateName, configGetter); 15 | server.decorate('request', decorateName, configGetter); 16 | 17 | if (next) next(); 18 | }; 19 | if (configue.resolved) return register(); 20 | 21 | try { 22 | const promiseMaybe = configue.resolve(); 23 | if (promiseMaybe && promiseMaybe.then) { 24 | return promiseMaybe.then(register).catch(next); 25 | } 26 | return register(); 27 | } catch (err) { 28 | if (next) return next(err); 29 | else throw err; 30 | } 31 | }; 32 | }; 33 | 34 | const hapi17Plugin = function(decorateName) { 35 | const configue = this; 36 | return { 37 | register: rawHapiPlugin(configue, decorateName), 38 | pkg: require('../package.json') 39 | }; 40 | }; 41 | 42 | const hapiPlugin = function(decorateName) { 43 | const configue = this; 44 | const plugin = rawHapiPlugin(configue, decorateName); 45 | plugin.attributes = { 46 | pkg: require('../package.json') 47 | }; 48 | return plugin; 49 | }; 50 | 51 | const expressMiddleware = function(decorateName) { 52 | const configue = this; 53 | const configGetter = makeConfigGetter(configue); 54 | const getterName = decorateName || 'configue'; 55 | return (req, res, next) => { 56 | req[getterName] = configGetter; 57 | next(); 58 | }; 59 | }; 60 | 61 | module.exports = {hapiPlugin, hapi17Plugin, expressMiddleware}; 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const Configue = require('./core/configue'); 2 | const addBuilder = require('./core/builder'); 3 | const {get, getAsync, getFirst, getAll, template, getObject, load} = require('./core/getters'); 4 | const {hapiPlugin, hapi17Plugin, expressMiddleware} = require('./extensions'); 5 | 6 | addBuilder(Configue); 7 | 8 | Configue.prototype.get = get; 9 | Configue.prototype.getAsync = getAsync; 10 | Configue.prototype.getFirst = getFirst; 11 | Configue.prototype.getAll = getAll; 12 | Configue.prototype.template = template; 13 | Configue.prototype.t = template; 14 | Configue.prototype.getObject = getObject; 15 | Configue.prototype.load = load; 16 | 17 | Configue.prototype.plugin = hapiPlugin; 18 | Configue.prototype.plugin17 = hapi17Plugin; 19 | Configue.prototype.middleware = expressMiddleware; 20 | 21 | module.exports = Configue; 22 | -------------------------------------------------------------------------------- /test/builder.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Configue = require('../src'); 3 | 4 | test('get is working fine as factory method', t => { 5 | const configue = Configue.get(); 6 | t.assert(configue instanceof Configue); 7 | }); 8 | 9 | test('get is reseting the chain', t => { 10 | Configue.defaults({a: 1}).get(); 11 | const configue2 = Configue.get(); 12 | t.deepEqual(configue2.settings, {}); 13 | }); 14 | 15 | test('options methods sets the good values', t => { 16 | const configue = Configue.defaults({a: 1}) 17 | .env(['HOME']) 18 | .defer(true) 19 | .get(); 20 | t.deepEqual(configue.settings, { 21 | defaults: {a: 1}, 22 | defer: true, 23 | env: ['HOME'] 24 | }); 25 | }); 26 | 27 | test('options methods sets the good values bis', t => { 28 | const hook = () => {}; 29 | const configue = Configue.envHook(hook) 30 | .argvHook(hook) 31 | .get(); 32 | t.deepEqual(configue.settings, { 33 | postHooks: {argv: hook, env: hook} 34 | }); 35 | }); 36 | 37 | test('withOptions methods sets the good values, does not override existing options', t => { 38 | const hook = () => {}; 39 | const configue = Configue.envHook(hook) 40 | .withOptions({async: true, shortstop: true}) 41 | .get(); 42 | t.deepEqual(configue.settings, { 43 | async: true, 44 | shortstop: true, 45 | postHooks: {env: hook} 46 | }); 47 | }); 48 | 49 | test('resolve method builder with chained then', async t => { 50 | const configue = await Configue.shortstop(true).resolve(); 51 | t.deepEqual(configue.settings, { 52 | async: true, 53 | protocall: true 54 | }); 55 | t.assert(configue.resolved); 56 | }); 57 | 58 | test.cb('resolve method builder with passed continuation', t => { 59 | Configue.shortstop(true).resolve(configue => { 60 | t.deepEqual(configue.settings, { 61 | async: true, 62 | protocall: true 63 | }); 64 | t.assert(configue.resolved); 65 | t.end(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/core.0.resolving.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Configue = require('../src'); 3 | 4 | test('resolve is automatic by default', t => { 5 | const configue = Configue(); 6 | t.true(configue.resolved); 7 | }); 8 | 9 | test('resolve is automatic unless defer', t => { 10 | const configue = Configue({defer: true}); 11 | t.false(configue.resolved); 12 | }); 13 | 14 | test('resolve is automatic unless async', t => { 15 | const configue = Configue({async: true}); 16 | t.false(configue.resolved); 17 | }); 18 | 19 | test('resolve can be performed asynchrounously', async t => { 20 | const configue = Configue({async: true}); 21 | const config = await configue.resolve(); 22 | t.is(config, configue); 23 | t.assert(configue.resolved); 24 | }); 25 | 26 | test('resolve is executed once (defer)', t => { 27 | const configue = Configue({defer: true, defaults: {A: 1}}); 28 | t.false(configue.resolved); 29 | configue.resolve(); 30 | configue.resolve(); // coverage ensure that we don't have ran a second times 31 | 32 | t.assert(configue.resolved); 33 | // can't test a resolve with change value since dynamic access to argv and env 34 | }); 35 | 36 | test('resolve is executed once', async t => { 37 | const configue = Configue({async: true, defaults: {A: 1}}); 38 | t.false(configue.resolved); 39 | await configue.resolve(); 40 | const configbis = await configue.resolve(); // coverage ensure that we don't have ran a second times 41 | 42 | t.assert(configbis.resolved); 43 | // can't test a resolve with change value since dynamic access to argv and env 44 | }); 45 | -------------------------------------------------------------------------------- /test/core.1.options.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Yargs = require('yargs'); 3 | const Configue = require('../src'); 4 | 5 | test('detect wrong option item', t => { 6 | t.throws(() => Configue({this: 'is-junk'})); 7 | }); 8 | // TODO maybe add some valid schema 9 | test('presevent usage of shortstop without async mode', t => { 10 | t.throws(() => Configue({shortstop: true}), 'Protocall(Shortstop) usage requires async mode'); 11 | }); 12 | test('presevent usage of protocall without async mode', t => { 13 | t.throws(() => Configue({protocall: true}), 'Protocall(Shortstop) usage requires async mode'); 14 | }); 15 | 16 | test('argv are forwarded to nconf', t => { 17 | const configue = Configue({argv: {key: {default: 'some-value'}}}); 18 | t.is(configue.get('key'), 'some-value'); 19 | }); 20 | 21 | test('argv can be a yargs instance', t => { 22 | const configue = Configue({argv: Yargs(process.argv.slice(2)).defaults('toto', 'titi')}); 23 | t.is(configue.get('toto'), 'titi'); 24 | }); 25 | 26 | test('env config are forwarded to nconf if whitelist', t => { 27 | const configue = Configue({env: ['PWD']}); 28 | t.assert(configue.get('HOME') === undefined); 29 | }); 30 | 31 | test('env config are forwarded to nconf if object', t => { 32 | const configue = Configue({env: {whitelist: ['HOME']}}); 33 | t.assert(configue.get('PWD') === undefined); 34 | }); 35 | 36 | test('required keys are enforced by nconf', t => { 37 | t.throws( 38 | () => 39 | Configue.defaults({A: 1}) 40 | .required(['A', 'B']) 41 | .get(), 42 | 'Missing required keys: B' 43 | ); 44 | }); 45 | 46 | test('required keys are enforced by nconf does not false alarm', t => { 47 | Configue.defaults({A: 1, B: 2, C: 3}) 48 | .required(['A', 'B']) 49 | .get(); 50 | t.pass(); 51 | }); 52 | 53 | test('parse is activated', t => { 54 | process.argv.push('--one=2'); 55 | process.env.universe = '42'; 56 | const configue = Configue.parse(true).get(); 57 | process.argv.pop(); 58 | process.env.universe = undefined; 59 | t.is(configue.get('one'), 2); 60 | t.is(configue.get('universe'), 42); 61 | }); 62 | 63 | test('parse and transform are activated', t => { 64 | process.argv.push('--one=2'); 65 | const configue = Configue.parse(true) 66 | .transform(({key, value}) => ({key, value: `this is ${value + 2}`})) 67 | .get(); 68 | process.argv.pop(); 69 | t.is(configue.get('one'), 'this is 4'); 70 | }); 71 | 72 | test('can load a default object', t => { 73 | const configue = Configue({defaults: {one: 1}}); 74 | t.is(configue.get('one'), 1); 75 | }); 76 | 77 | test('defaults are loaded in order', t => { 78 | const configue = Configue({ 79 | defaults: [{one: 1}, {one: 2, two: 2}] 80 | }); 81 | t.is(configue.get('one'), 1); 82 | t.is(configue.get('two'), 2); 83 | }); 84 | 85 | test('overrides can be defined', t => { 86 | const configue = Configue({overrides: {one: 1}}); 87 | t.is(configue.get('one'), 1); 88 | }); 89 | 90 | test('overrides are not overidden', t => { 91 | process.argv.push('--one=2'); 92 | const configue = Configue({overrides: {one: 1}}); 93 | process.argv.pop(); 94 | t.is(configue.get('one'), 1); 95 | }); 96 | -------------------------------------------------------------------------------- /test/core.2.files.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const test = require('ava'); 3 | const Configue = require('../src'); 4 | 5 | const JSON_CONF_FILE = path.join(__dirname, 'data/config.json'); 6 | const JSON5_CONF_FILE = path.join(__dirname, 'data/config.json5'); 7 | const PROPERTIES_CONF_FILE = path.join(__dirname, 'data/config.properties'); 8 | const JSON_CONF_FILE_BIS = path.join(__dirname, 'data/config-bis.json'); 9 | const YAML_CONF_FILE = path.join(__dirname, 'data/config.yaml'); 10 | 11 | test('can load data from a json file given as string', t => { 12 | const configue = Configue({files: JSON_CONF_FILE}); 13 | t.is(configue.get('key'), 'json-config'); 14 | }); 15 | 16 | test('can load data from a json files given as string array', t => { 17 | const configue = Configue({files: [JSON_CONF_FILE, JSON_CONF_FILE_BIS]}); 18 | t.is(configue.get('key'), 'json-config'); 19 | t.is(configue.get('key-bis'), 'json-config-bis'); 20 | }); 21 | 22 | test('can load data from a json file', t => { 23 | const configue = Configue({files: [{file: JSON_CONF_FILE}]}); 24 | t.is(configue.get('key'), 'json-config'); 25 | t.is(configue.get('nested:key'), 'nested'); 26 | }); 27 | 28 | test('can load data from a json5 file', t => { 29 | const configue = Configue({files: JSON5_CONF_FILE}); 30 | t.is(configue.get('key'), 'json5-config'); 31 | t.is(configue.get('nested:key'), 'nested'); 32 | }); 33 | 34 | test('can load data from a yaml file', t => { 35 | const configueOptions = { 36 | files: [ 37 | { 38 | file: YAML_CONF_FILE, 39 | format: require('nconf-yaml') 40 | } 41 | ] 42 | }; 43 | const configue = Configue(configueOptions); 44 | t.is(configue.get('key'), 'yaml-config'); 45 | t.is(configue.get('nested:key'), 'nested'); 46 | }); 47 | 48 | test('can load data from a yaml file without saying explicitely it is one', t => { 49 | const configueOptions = { 50 | files: YAML_CONF_FILE 51 | }; 52 | const configue = Configue(configueOptions); 53 | t.is(configue.get('key'), 'yaml-config'); 54 | t.is(configue.get('nested:key'), 'nested'); 55 | }); 56 | 57 | test('can load data from a yaml file without saying explicitely it is one in a list', t => { 58 | const configueOptions = { 59 | files: [YAML_CONF_FILE, JSON_CONF_FILE] 60 | }; 61 | const configue = Configue(configueOptions); 62 | t.is(configue.get('key'), 'yaml-config'); 63 | t.is(configue.get('nested:key'), 'nested'); 64 | }); 65 | 66 | test('can load data from a properties file without saying explicitely it is one', t => { 67 | const configueOptions = { 68 | files: PROPERTIES_CONF_FILE 69 | }; 70 | const configue = Configue(configueOptions); 71 | t.is(configue.get('key'), 'properties-config'); 72 | t.is(configue.get('nested:key'), 'nested'); 73 | }); 74 | 75 | test('files are loaded in order', t => { 76 | const configueOptions = { 77 | files: [ 78 | {file: JSON_CONF_FILE}, 79 | { 80 | file: YAML_CONF_FILE, 81 | format: require('nconf-yaml') 82 | } 83 | ] 84 | }; 85 | const configue = Configue(configueOptions); 86 | t.is(configue.get('key'), 'json-config'); 87 | }); 88 | -------------------------------------------------------------------------------- /test/core.3.transform.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Configue = require('../src'); 3 | 4 | const fooTransformer = ({key, value}) => ({key, value: `${value}foo`}); 5 | const barTransformer = ({key, value}) => ({key, value: `${value}bar`}); 6 | 7 | test('transformer can be defined with a single transformer', t => { 8 | process.argv.push('--one=one'); 9 | const configue = new Configue({transform: fooTransformer}); 10 | process.argv.pop(); 11 | 12 | t.is(configue.get('one'), 'onefoo'); 13 | }); 14 | 15 | test('transformer can be defined with ordered transformer', t => { 16 | process.argv.push('--one=one'); 17 | const configue = new Configue({transform: [fooTransformer, barTransformer]}); 18 | process.argv.pop(); 19 | 20 | t.is(configue.get('one'), 'onefoobar'); 21 | }); 22 | 23 | test('transformer can be defined along with normalize', t => { 24 | process.argv.push('--ONE-TWO=douze'); 25 | const configue = new Configue({ 26 | transform: [fooTransformer, barTransformer], 27 | normalize: 'camelCase' 28 | }); 29 | process.argv.pop(); 30 | 31 | t.is(configue.get('oneTwo'), 'douzefoobar'); 32 | }); 33 | 34 | test('ignore-prefix works well for a single prefix', t => { 35 | process.env.MY_SUPER_APP_PORT = '3024'; 36 | const configue = new Configue({ignorePrefix: 'MY_SUPER_APP_'}); 37 | process.env.MY_SUPER_APP_PORT = undefined; 38 | 39 | t.is(configue.get('PORT'), '3024'); 40 | }); 41 | 42 | test('ignore-prefix works well for a multiple prefix', t => { 43 | process.env.MY_SUPER_APP_PORT = '3024'; 44 | process.env.MY_APP_HOST = 'localhost'; 45 | const configue = new Configue({ignorePrefix: ['MY_SUPER_APP_', 'MY_APP_']}); 46 | process.env.MY_SUPER_APP_PORT = undefined; 47 | 48 | t.is(configue.get('PORT'), '3024'); 49 | t.is(configue.get('HOST'), 'localhost'); 50 | }); 51 | 52 | test('ignore-prefix works well along with other transformers', t => { 53 | process.env.MY_APP_PORT = '3024'; 54 | process.env.MY_APP_HOST = 'localhost'; 55 | const configue = new Configue({ignorePrefix: 'MY_APP', normalize: 'camelCase'}); 56 | process.env.MY_SUPER_APP_PORT = undefined; 57 | 58 | t.is(configue.get('port'), '3024'); 59 | t.is(configue.get('host'), 'localhost'); 60 | }); 61 | 62 | test('separator can be defined globally for env and argv with a string', t => { 63 | process.argv.push('--one__two=douze'); 64 | process.env.four__two = '42'; 65 | const configue = new Configue({separator: '__'}); 66 | process.argv.pop(); 67 | process.env.four__two = undefined; 68 | 69 | t.is(configue.get('one:two'), 'douze'); 70 | t.is(configue.get('four:two'), '42'); 71 | }); 72 | 73 | test('separator can be defined globally for env and argv with a regex', t => { 74 | process.argv.push('--one--two=douze'); 75 | process.env.four__two = '42'; 76 | const configue = new Configue({separator: /--|__/}); 77 | process.argv.pop(); 78 | process.env.four__two = undefined; 79 | 80 | t.is(configue.get('one:two'), 'douze'); 81 | t.is(configue.get('four:two'), '42'); 82 | }); 83 | 84 | test('normalize invalid case are rejected', t => { 85 | t.throws(() => Configue.normalize('wtfCase').get(), /"normalize" is not allowed/); // FIXME: check test is correct 86 | }); 87 | 88 | test('normalize works well for argv and env with camelCase', t => { 89 | process.argv.push('--one-two=douze'); 90 | process.env.FOUR_TWO = '42'; 91 | const configue = Configue.normalize('camelCase').get(); 92 | process.argv.pop(); 93 | process.env.FOUR_TWO = undefined; 94 | 95 | t.is(configue.get('oneTwo'), 'douze'); 96 | t.is(configue.get('fourTwo'), '42'); 97 | t.is(configue.get('one-two'), undefined); 98 | t.is(configue.get('FOUR_TWO'), undefined); 99 | }); 100 | 101 | test('normalize works well for argv and env with lowerCase', t => { 102 | process.argv.push('--one-TWO=douze'); 103 | process.env.FOUR_TWO = '42'; 104 | const configue = Configue.normalize('lowerCase').get(); 105 | process.argv.pop(); 106 | process.env.FOUR_TWO = undefined; 107 | 108 | t.is(configue.get('one two'), 'douze'); 109 | t.is(configue.get('four two'), '42'); 110 | t.is(configue.get('one-two'), undefined); 111 | t.is(configue.get('FOUR_TWO'), undefined); 112 | }); 113 | 114 | test('normalize works well along with the separator option', t => { 115 | process.argv.push('--One--Two=douze'); 116 | process.env.FOUR__TWO = '42'; 117 | process.env.NO_SEP = '__'; 118 | const configue = new Configue({separator: /--|__/, normalize: 'camelCase'}); 119 | process.argv.pop(); 120 | process.env.FOUR__TWO = undefined; 121 | process.env.NO_SEP = undefined; 122 | 123 | t.is(configue.get('one:two'), 'douze'); 124 | t.is(configue.get('four:two'), '42'); 125 | t.is(configue.get('noSep'), '__'); 126 | }); 127 | 128 | test('normalize works with the env whitelist option', t => { 129 | process.env.NO_SEP = 'no sep'; 130 | process.env.CAMEL_CASE = 'camelCase'; 131 | const configue = new Configue({ 132 | normalize: 'camelCase', 133 | env: { 134 | whitelist: ['NO_SEP', 'CAMEL_CASE'] 135 | } 136 | }); 137 | process.env.NO_SEP = undefined; 138 | process.env.CAMEL_CASE = undefined; 139 | t.is(configue.get('noSep'), 'no sep'); 140 | t.is(configue.get('camelCase'), 'camelCase'); 141 | }); 142 | -------------------------------------------------------------------------------- /test/core.4.tweak-workflow.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Configue = require('../src'); 3 | 4 | test('enable to disable argv', t => { 5 | process.argv.push('--who=YO'); 6 | process.env.who = 'NO'; 7 | // RISKY!!!! 8 | 9 | const configue = Configue({disable: {argv: true}}); 10 | process.argv.pop(); 11 | process.env.who = undefined; 12 | t.is(configue.get('who'), 'NO'); 13 | }); 14 | 15 | test('enable to disable env', t => { 16 | process.env.who = 'NONO'; 17 | // RISKY!!!! 18 | 19 | const configue = Configue({disable: {env: true}, defaults: {who: 'YES YES'}}); 20 | process.env.who = undefined; 21 | t.is(configue.get('who'), 'YES YES'); 22 | }); 23 | 24 | test('enable to disabLe env in async mode', async t => { 25 | process.argv.push('--who=YO'); 26 | process.env.who = 'NO'; 27 | // RISKY!!!! 28 | 29 | const configue = await Configue.withOptions({disable: {argv: true, env: true}}).resolve(); 30 | process.argv.pop(); 31 | process.env.who = undefined; 32 | 33 | t.is(configue.get('who'), undefined); 34 | }); 35 | 36 | test('accept a custom workflow', t => { 37 | const configueOptions = { 38 | customWorkflow: nconf => nconf.set('workflow', 'custom') 39 | }; 40 | process.argv.push('--workflow=default'); 41 | process.env.key = 'value'; 42 | 43 | const configue = Configue(configueOptions); 44 | process.argv.pop(); 45 | process.env.KEY = undefined; 46 | 47 | t.is(configue.get('workflow'), 'custom'); 48 | t.falsy(configue.get('key')); 49 | }); 50 | 51 | test('accept an async custom workflow', async t => { 52 | const configueOptions = { 53 | customWorkflow: nconf => 54 | Promise.resolve('custom-async').then(workflow => nconf.set('workflow', workflow)) 55 | }; 56 | process.argv.push('--workflow=default'); 57 | process.env.key = 'value'; 58 | 59 | const configue = await Configue.withOptions(configueOptions).resolve(); 60 | process.argv.pop(); 61 | process.env.key = undefined; 62 | t.is(configue.get('workflow'), 'custom-async'); 63 | t.assert(!configue.get('key')); 64 | }); 65 | 66 | test('enable to insert hook', t => { 67 | const configueOptions = { 68 | postHooks: { 69 | overrides: function postOverrides(nconf) { 70 | nconf.set('who', 'ME FIRST!'); 71 | }, 72 | argv: function postArgv(nconf) { 73 | nconf.set('when', 'NOW'); 74 | }, 75 | defaults: function last(nconf) { 76 | nconf.set('when', `RIGHT ${nconf.get('when')}`); 77 | } 78 | } 79 | }; 80 | process.argv.push('--when=later'); 81 | process.env.who = 'me'; 82 | 83 | const configue = Configue(configueOptions); 84 | t.is(configue.get('who'), 'ME FIRST!'); 85 | t.is(configue.get('when'), 'RIGHT NOW'); 86 | }); 87 | 88 | test('first hook is runned to insert hook', t => { 89 | const configueOptions = { 90 | overrides: {who: 'ME second!'}, 91 | postHooks: { 92 | first: function first(nconf) { 93 | nconf.set('who', 'ME FIRST!'); 94 | nconf.set('when', 'RIGHT NOW!'); 95 | } 96 | } 97 | }; 98 | process.env.who = 'me'; 99 | 100 | const configue = Configue(configueOptions); 101 | process.env.who = undefined; 102 | t.is(configue.get('who'), 'ME FIRST!'); 103 | t.is(configue.get('when'), 'RIGHT NOW!'); 104 | }); 105 | 106 | test('handle error in loading process', t => { 107 | t.throws(() => 108 | Configue({ 109 | postHooks: { 110 | argv: function postArgv() { 111 | throw new Error('This is an error'); 112 | } 113 | } 114 | }) 115 | ); 116 | }); 117 | -------------------------------------------------------------------------------- /test/core.5.shortstop.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Configue = require('../src'); 3 | 4 | for (const protocolLib of ['shortstop', 'protocall']) { 5 | // temporary test duplication waiting for shortstop config name to be deprecated 6 | test(`${protocolLib} performs transformation as expected`, async t => { 7 | const configue = await Configue[protocolLib](true) 8 | .defaults({b64: 'base64:YmFzZTY0'}) 9 | .resolve(); 10 | t.is(configue.get('b64'), 'base64'); 11 | }); 12 | 13 | test(`${protocolLib} dont perform transformation when not activated`, async t => { 14 | const configue = await Configue[protocolLib](false) 15 | .defaults({b64: 'base64:YmFzZTY0'}) 16 | .resolve(); 17 | t.is(configue.get('b64'), 'base64:YmFzZTY0'); 18 | }); 19 | test(`${protocolLib} does not include protocols if defaults deactivated`, async t => { 20 | const configue = await Configue[protocolLib]({noDefaultProtocols: true}) 21 | .defaults({b64: 'base64:YmFzZTY0'}) 22 | .resolve(); 23 | t.is(configue.get('b64'), 'base64:YmFzZTY0'); 24 | }); 25 | 26 | test(`${protocolLib} trigger an error if resolve fail`, async t => { 27 | await t.throwsAsync( 28 | () => 29 | Configue[protocolLib](true) 30 | .defaults({file: 'file:/does/not/exist'}) 31 | .resolve(), 32 | "ENOENT: no such file or directory, open '/does/not/exist'" 33 | ); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/core.6.misc.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Configue = require('../src'); 3 | 4 | // argv-env-access-and-prefedined-models 5 | 6 | test('argv can be directly accessed from the configue', t => { 7 | process.argv.push('--one=two'); 8 | const configue = Configue.get(); 9 | process.argv.pop(); 10 | t.is(configue.argv.one, 'two'); 11 | }); 12 | 13 | test('argv cannot be directly accessed if custom workflow is used', t => { 14 | const configue = new Configue({customWorkflow: nconf => nconf.set('workflow', 'custom')}); 15 | t.assert(configue.argv === undefined); 16 | }); 17 | 18 | test('argv can be directly accessed if custom workflow set a _yargs', t => { 19 | const configue = new Configue({customWorkflow: nconf => (nconf._yargs = {argv: 'stub'})}); 20 | t.is(configue.argv, 'stub'); 21 | }); 22 | 23 | test('env can be directly accessed', t => { 24 | process.env.universe = '42'; 25 | const configue = new Configue(); 26 | t.is(configue.env.universe, '42'); 27 | process.env.universe = undefined; 28 | }); 29 | 30 | test('aliases can be simply defined', t => { 31 | const configue = new Configue({ 32 | defaults: {A: {a: 1, b: 2}, B: 42}, 33 | models: { 34 | universe: c => ({a: c('A:a'), b: c.t`the answer is ${'B'}`}), 35 | simple: {a: 'A:a', b: ['B:b', 'A:b']} 36 | } 37 | }); 38 | t.deepEqual(configue._.simple, {a: 1, b: 2}); 39 | t.deepEqual(configue._.universe, {a: 1, b: 'the answer is 42'}); 40 | }); 41 | -------------------------------------------------------------------------------- /test/data/config-bis.json: -------------------------------------------------------------------------------- 1 | { 2 | "key-bis": "json-config-bis" 3 | } -------------------------------------------------------------------------------- /test/data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "json-config", 3 | "nested": { 4 | "key": "nested" 5 | } 6 | } -------------------------------------------------------------------------------- /test/data/config.json5: -------------------------------------------------------------------------------- 1 | { 2 | key: "json5-config", 3 | nested: { 4 | key: "nested" 5 | } 6 | } -------------------------------------------------------------------------------- /test/data/config.properties: -------------------------------------------------------------------------------- 1 | # Basic Properties Config 2 | key = properties-config 3 | 4 | [nested] 5 | key = nested -------------------------------------------------------------------------------- /test/data/config.yaml: -------------------------------------------------------------------------------- 1 | # Basic Yaml Config 2 | key: yaml-config 3 | nested: 4 | key: nested -------------------------------------------------------------------------------- /test/extensions.express.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const _ = require('lodash/fp'); 3 | const Configue = require('../src'); 4 | 5 | test.cb('expose configue handler', t => { 6 | const configue = new Configue({defaults: {r: 2, d: 2}}); 7 | const middleware = configue.middleware(); 8 | 9 | const req = {}, 10 | res = {}; 11 | middleware(req, res, () => { 12 | t.deepEqual(res, {}); 13 | t.assert(_.isFunction(req.configue)); 14 | t.assert(_.isFunction(req.configue.get)); 15 | t.assert(_.isFunction(req.configue.t)); 16 | t.is(req.configue('r'), 2); 17 | t.end(); 18 | }); 19 | }); 20 | 21 | test.cb('expose configue handler with a custom name', t => { 22 | const configue = new Configue({defaults: {r: 2, d: 2}}); 23 | const middleware = configue.middleware('conf'); 24 | 25 | const req = {}, 26 | res = {}; 27 | middleware(req, res, () => { 28 | t.deepEqual(res, {}); 29 | t.assert(_.isFunction(req.conf)); 30 | t.assert(_.isFunction(req.conf.get)); 31 | t.assert(_.isFunction(req.conf.t)); 32 | t.is(req.conf('r'), 2); 33 | t.end(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/extensions.hapi.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const _ = require('lodash/fp'); 3 | const Configue = require('../src'); 4 | 5 | test.cb('hapi should register correctly', t => { 6 | t.plan(6); 7 | const server = { 8 | log() {}, 9 | decorate(type, decorateName, configGetter) { 10 | t.assert(/server|request/.test(type)); 11 | t.is(decorateName, 'configue'); 12 | t.true(_.isFunction(configGetter)); 13 | } 14 | }; 15 | const plugin = new Configue().plugin(); 16 | plugin(server, {}, t.end); 17 | }); 18 | 19 | test.cb('hapi should register with a custom name correctly', t => { 20 | t.plan(6); 21 | const server = { 22 | log() {}, 23 | decorate(type, decorateName, configGetter) { 24 | t.assert(/server|request/.test(type)); 25 | t.is(decorateName, 'conf'); 26 | t.true(_.isFunction(configGetter)); 27 | } 28 | }; 29 | const plugin = new Configue().plugin('conf'); 30 | plugin(server, {}, t.end); 31 | }); 32 | 33 | test.cb('hapi should register after sync resolve', t => { 34 | t.plan(4); 35 | const server = { 36 | log() {}, 37 | decorate(type) { 38 | t.assert(/server|request/.test(type)); 39 | } 40 | }; 41 | const configue = new Configue({defer: true}); 42 | t.is(configue.resolved, false); 43 | const plugin = configue.plugin(); 44 | plugin(server, {}, err => { 45 | try { 46 | if (err) throw err; 47 | t.is(configue.resolved, true); 48 | t.end(); 49 | } catch (error) { 50 | t.end(error); 51 | } 52 | }); 53 | }); 54 | 55 | test.cb('hapi should register after async resolve', t => { 56 | t.plan(4); 57 | const server = { 58 | log() {}, 59 | decorate(type) { 60 | t.assert(/server|request/.test(type)); 61 | } 62 | }; 63 | const configue = new Configue({async: true}); 64 | t.is(configue.resolved, false); 65 | const plugin = configue.plugin(); 66 | plugin(server, {}, err => { 67 | try { 68 | if (err) throw err; 69 | t.is(configue.resolved, true); 70 | t.end(); 71 | } catch (error) { 72 | t.end(error); 73 | } 74 | }); 75 | }); 76 | 77 | test('Hapi17 can be (fakely) registered ', t => { 78 | t.plan(2); 79 | const configue = new Configue(); 80 | const server = { 81 | log() {}, 82 | decorate(type) { 83 | t.assert(/server|request/.test(type)); 84 | } 85 | }; 86 | configue.plugin17().register(server); 87 | }); 88 | 89 | test('Hapi17 handle failure in the resolve', t => { 90 | const configue = Configue({ 91 | defer: true, 92 | customWorkflow: () => { 93 | throw new Error('init failed'); 94 | } 95 | }); 96 | const server = {}; 97 | t.throws(() => configue.plugin17().register(server), 'init failed'); 98 | }); 99 | -------------------------------------------------------------------------------- /test/getters.basic-gets.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Configue = require('../src'); 3 | 4 | test('get value', t => { 5 | const configue = Configue({defaults: {A: '2', B: 42}}); 6 | 7 | // NOTE: code is interpreted from command line: -a !! (-a code, param of lab) 8 | t.is(configue.get('A'), '2'); 9 | t.is(configue.get('B'), 42); 10 | }); 11 | 12 | test('get nested value', t => { 13 | const configue = Configue({defaults: {root: {a: '2', b: 42}}}); 14 | t.deepEqual(configue.get('root'), {a: '2', b: 42}); 15 | t.is(configue.get('root:a'), '2'); 16 | t.is(configue.get('root:b'), 42); 17 | }); 18 | 19 | test('get nested value with array', t => { 20 | const configue = Configue({defaults: {root: {a: '2', b: 42}}}); 21 | t.is(configue.get(['root', 'a']), '2'); 22 | t.is(configue.get(['root', 'b']), 42); 23 | }); 24 | 25 | test('get nested value with dot', t => { 26 | const configue = Configue({defaults: {root: {a: '2', b: 42}}}); 27 | t.is(configue.get('root.a'), '2'); 28 | t.is(configue.get('root.b'), 42); 29 | }); 30 | 31 | test('get defaultValue', t => { 32 | const configue = Configue({defaults: {A: '2', B: 42}}); 33 | t.is(configue.get('C', 'pasdefini'), 'pasdefini'); 34 | }); 35 | 36 | test('get defaultValue if result is set undefined', t => { 37 | const configue = Configue({defaults: {idonotexist: undefined, zero: 0}}); 38 | t.is(configue.get('idonotexist', 'unlessItellsSo'), 'unlessItellsSo'); 39 | t.is(configue.get('zero', 12), 0); 40 | }); 41 | 42 | test('get first value', t => { 43 | const configue = Configue({overrides: {A: '2', B: 42, C: false}}); 44 | 45 | t.is(configue.getFirst('A', 'B', 'C'), '2'); 46 | t.is(configue.getFirst('b', 'B'), 42); 47 | t.is(configue.getFirst('acd', 'C'), false); 48 | t.is(configue.getFirst('aa', 'bb', 'cc'), undefined); 49 | }); 50 | 51 | test('get first value from array', t => { 52 | const configue = Configue({overrides: {A: '2', B: 42, C: false}}); 53 | 54 | t.is(configue.getFirst(['A', 'B', 'C']), '2'); 55 | t.is(configue.getFirst(['b', 'B']), 42); 56 | t.is(configue.getFirst(['acd', 'C']), false); 57 | t.is(configue.getFirst(['aa', 'bb', 'cc']), undefined); 58 | }); 59 | 60 | test('get all value', t => { 61 | const configue = Configue({overrides: {A: '2', B: 42, C: false}}); 62 | 63 | t.deepEqual(configue.getAll('A', 'B', 'C'), ['2', 42, false]); 64 | t.deepEqual(configue.getAll('b', 'B'), [undefined, 42]); 65 | }); 66 | 67 | test('get all value from array', t => { 68 | const configue = Configue({overrides: {A: '2', B: 42, C: false}}); 69 | 70 | t.deepEqual(configue.getAll(['A', 'B', 'C']), ['2', 42, false]); 71 | t.deepEqual(configue.getAll(['b', 'B']), [undefined, 42]); 72 | }); 73 | 74 | test.cb('get value async with callback', t => { 75 | const configue = Configue({overrides: {A: '2', B: 42, C: false}}); 76 | configue.getAsync('A', (err, res) => { 77 | t.is(err, null); 78 | t.is(res, '2'); 79 | t.end(); 80 | }); 81 | }); 82 | 83 | test('get value async without callback (hence returns promise)', async t => { 84 | const configue = Configue({overrides: {A: '2', B: 42, C: false}}); 85 | const res = await configue.getAsync('A'); 86 | t.is(res, '2'); 87 | }); 88 | 89 | test('get value async with default value (hence returns promise)', async t => { 90 | const configue = Configue({overrides: {A: '2', B: 42, C: false}}); 91 | const res = await configue.getAsync('D', 2); 92 | t.is(res, 2); 93 | }); 94 | -------------------------------------------------------------------------------- /test/getters.formatkey.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const {formatKey} = require('../src/core/getters'); 3 | 4 | test('does nothing for normal keys', t => { 5 | t.is(formatKey('toto'), 'toto'); 6 | t.is(formatKey('titi-toto'), 'titi-toto'); 7 | }); 8 | 9 | test('does nothing for nconf keys with ":"', t => { 10 | t.is(formatKey('toto:titi'), 'toto:titi'); 11 | t.is(formatKey('titi:toto'), 'titi:toto'); 12 | }); 13 | 14 | test('transform keys with dot in them', t => { 15 | t.is(formatKey('toto.titi'), 'toto:titi'); 16 | t.is(formatKey('titi.toto'), 'titi:toto'); 17 | }); 18 | 19 | test('transform keys passed in an array', t => { 20 | t.is(formatKey(['toto', 'titi']), 'toto:titi'); 21 | t.is(formatKey(['titi', 'toto']), 'titi:toto'); 22 | }); 23 | -------------------------------------------------------------------------------- /test/getters.getobject.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Configue = require('../src'); 3 | 4 | test('basic getObject', t => { 5 | const configue = Configue({defaults: {A: '2', B: 42}}); 6 | t.deepEqual(configue.getObject('A', 'B'), {A: '2', B: 42}); 7 | }); 8 | 9 | test('basic getObject from array', t => { 10 | const configue = Configue({defaults: {A: '2', B: 42}}); 11 | t.deepEqual(configue.getObject(['A', 'B']), {A: '2', B: 42}); 12 | }); 13 | 14 | test('basic getObject with rename', t => { 15 | const configue = Configue({defaults: {A: '2', B: 42}}); 16 | t.deepEqual(configue.getObject(['A', 'a'], 'B'), {a: '2', B: 42}); 17 | }); 18 | 19 | test('complex getObject with rename', t => { 20 | const configue = Configue({defaults: {A: {alpha: 2, beta: 4}, B: 42}}); 21 | t.deepEqual(configue.getObject(['A:alpha', 'alpha'], ['B', 'beta']), {alpha: 2, beta: 42}); 22 | }); 23 | -------------------------------------------------------------------------------- /test/getters.load.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Configue = require('../src'); 3 | 4 | test('simple load', t => { 5 | const configue = Configue({defaults: {A: '2', B: 42}}); 6 | 7 | const config = configue.load(); 8 | t.is(config.A, '2'); 9 | t.is(config.B, 42); 10 | }); 11 | 12 | test('load with simple model', t => { 13 | const configue = Configue({defaults: {A: '2', B: 42}}); 14 | 15 | const config = configue.load({a: 'A', b: 'B'}); 16 | t.deepEqual(config, {a: '2', b: 42}); 17 | }); 18 | 19 | test('load with complex model', t => { 20 | const configue = Configue({defaults: {A: {a: 1, b: 2}, B: 42}}); 21 | 22 | const config = configue.load({a: 'A:a', b: {b: 'B'}}); 23 | t.deepEqual(config, {a: 1, b: {b: 42}}); 24 | }); 25 | 26 | test('load with complex model and multiple values', t => { 27 | const configue = Configue({defaults: {A: {a: 1, b: 2}, B: 42}}); 28 | 29 | const config = configue.load({a: ['a:a', 'A:a'], b: {b: ['B', 'A']}}); 30 | t.deepEqual(config, {a: 1, b: {b: 42}}); 31 | }); 32 | 33 | test('load with complex model and multiple values (using template)', t => { 34 | const configue = Configue({defaults: {A: {a: 1, b: 2}, B: 42}}); 35 | 36 | const config = configue.load(c => ({a: c('A:a'), b: c.t`answer is ${'B'}`})); 37 | t.deepEqual(config, {a: 1, b: 'answer is 42'}); 38 | }); 39 | -------------------------------------------------------------------------------- /test/getters.template.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Configue = require('../src'); 3 | 4 | test('basic template', t => { 5 | const configue = Configue({defaults: {A: '2', B: 42}}); 6 | t.is(configue.template`answer to life is ${'B'}`, 'answer to life is 42'); 7 | }); 8 | 9 | test('basic template via alias', t => { 10 | const configue = Configue({defaults: {A: '2', B: 42}}); 11 | t.is(configue.t`1+1=${'A'}`, '1+1=2'); 12 | }); 13 | 14 | test('basic template with default', t => { 15 | const configue = Configue({defaults: {A: '2', B: 42}}); 16 | t.is(configue.t({A: 4, C: 1})`1+${'C'}=${'A'}`, '1+1=2'); 17 | }); 18 | 19 | test('complex template, object not handled for now', t => { 20 | const configue = Configue({defaults: {A: '2', B: {b: 'b', c: 2}}}); 21 | t.is(configue.t`1+${'B'}=Nan`, '1+[object Object]=Nan'); 22 | }); 23 | --------------------------------------------------------------------------------