├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── compose.md ├── config.js ├── docs ├── assets │ ├── css │ │ └── main.css │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ ├── main.js │ │ └── search.js ├── index.html ├── modules.html └── modules │ ├── compose_config.html │ ├── default_compose_opts.html │ ├── ext_handlers.html │ ├── index.html │ ├── load_config.html │ ├── preset_config.html │ ├── process_config.html │ ├── provider_types.html │ ├── store.html │ └── util.html ├── package.json ├── src ├── compose-config.ts ├── default-compose-opts.ts ├── ext-handlers.ts ├── index.ts ├── load-config.ts ├── preset-config.ts ├── process-config.ts ├── provider-types.ts ├── store.ts └── util.ts ├── store.md ├── templates.md ├── test ├── composed-result.js ├── config │ ├── default-0.yaml │ ├── default.js │ ├── default.json │ ├── default.yaml │ ├── development.json │ └── production.js ├── config1 │ ├── default.json │ └── production.json ├── data │ └── foo.txt ├── esm-config │ ├── default.js │ └── test.js └── spec │ ├── compose-config.spec.js │ ├── load-config.spec.ts │ ├── preset-config.spec.js │ ├── process-config.spec.js │ └── store.spec.js ├── tsconfig.json └── xrun-tasks.ts /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/gitbook,osx,webstorm,node 3 | 4 | ### GitBook ### 5 | # Node rules: 6 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 7 | .grunt 8 | 9 | ## Dependency directory 10 | ## Commenting this out is preferred by some people, see 11 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 12 | node_modules 13 | 14 | # Book build output 15 | _book 16 | 17 | # eBook build output 18 | *.epub 19 | *.mobi 20 | *.pdf 21 | 22 | 23 | ### OSX ### 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must end with two \r 29 | Icon 30 | 31 | 32 | # Thumbnails 33 | ._* 34 | 35 | # Files that might appear in the root of a volume 36 | .DocumentRevisions-V100 37 | .fseventsd 38 | .Spotlight-V100 39 | .TemporaryItems 40 | .Trashes 41 | .VolumeIcon.icns 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | 51 | ### WebStorm ### 52 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 53 | 54 | *.iml 55 | 56 | ## Directory-based project format: 57 | .idea/ 58 | # if you remove the above rule, at least ignore the following: 59 | 60 | # User-specific stuff: 61 | # .idea/workspace.xml 62 | # .idea/tasks.xml 63 | # .idea/dictionaries 64 | # .idea/shelf 65 | 66 | # Sensitive or high-churn files: 67 | # .idea/dataSources.ids 68 | # .idea/dataSources.xml 69 | # .idea/sqlDataSources.xml 70 | # .idea/dynamic.xml 71 | # .idea/uiDesigner.xml 72 | 73 | # Gradle: 74 | # .idea/gradle.xml 75 | # .idea/libraries 76 | 77 | # Mongo Explorer plugin: 78 | # .idea/mongoSettings.xml 79 | 80 | ## File-based project format: 81 | *.ipr 82 | *.iws 83 | 84 | ## Plugin-specific files: 85 | 86 | # IntelliJ 87 | /out/ 88 | 89 | # mpeltonen/sbt-idea plugin 90 | .idea_modules/ 91 | 92 | # JIRA plugin 93 | atlassian-ide-plugin.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | fabric.properties 100 | 101 | 102 | ### Node ### 103 | # Logs 104 | logs 105 | *.log 106 | npm-debug.log* 107 | 108 | # Runtime data 109 | pids 110 | *.pid 111 | *.seed 112 | 113 | # Directory for instrumented libs generated by jscoverage/JSCover 114 | lib-cov 115 | 116 | # Coverage directory used by tools like istanbul 117 | coverage 118 | 119 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 120 | .grunt 121 | 122 | # node-waf configuration 123 | .lock-wscript 124 | 125 | # Compiled binary addons (http://nodejs.org/api/addons.html) 126 | build/Release 127 | 128 | # Dependency directory 129 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 130 | node_modules 131 | 132 | # Optional npm cache directory 133 | .npm 134 | 135 | # Optional REPL history 136 | .node_repl_history 137 | 138 | .tmp 139 | .npmrc 140 | lib/ 141 | .nyc_output/ 142 | *-lock* 143 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - v14 5 | - v12 6 | - v10 7 | 8 | script: 9 | - npm run check 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Attach", 9 | "port": 9229, 10 | "request": "attach", 11 | "skipFiles": ["/**"], 12 | "type": "pwa-node" 13 | }, 14 | 15 | { 16 | "type": "node", 17 | "request": "launch", 18 | "name": "Launch Program", 19 | "skipFiles": ["/**"], 20 | "program": "${workspaceFolder}/lib/index.js", 21 | "preLaunchTask": "tsc: build - tsconfig.json", 22 | "outFiles": ["${workspaceFolder}/lib/**/*.js"] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 WalmartLabs 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electrode Confippet [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] 2 | 3 | Confippet is a versatile, flexible utility for managing configurations of 4 | Node.js applications. It's simple to get started, and can be customized and 5 | extended to meet the needs of your app. 6 | 7 |
8 | 9 | ## Contents 10 | 11 | - [Electrode Confippet ![NPM version](#electrode-confippet-npm-versionnpm-url-build-statustravis-imagetravis-url) 12 | - [Contents](#contents) 13 | - [Features](#features) 14 | - [Getting Started](#getting-started) 15 | - [Installation](#installation) 16 | - [Basic Use](#basic-use) 17 | - [Config Composition](#config-composition) 18 | - [Environment Variables](#environment-variables) 19 | - [Using Templates](#using-templates) 20 | - [Usage in Node Modules](#usage-in-node-modules) 21 | - [Customization](#customization) 22 | 23 | ## Features 24 | 25 | - **Simple** - Confippet's `presetConfig` automatically composes a single config 26 | object from multiple files. For most applications, no further customization is 27 | necessary. 28 | - **Flexible** - Supports JSON, YAML, and JavaScript config files. 29 | - **Powerful** - Handles complex multi-instance enterprise deployments. 30 | 31 | ## Getting Started 32 | 33 | In this example, we'll create two config files: a default file that always 34 | loads, and a production file that loads only when the `NODE_ENV` environment 35 | variable is set to `production`. We'll then import those files into a standard 36 | Node.js app. 37 | 38 | ### Installation 39 | 40 | ``` 41 | npm install electrode-confippet --save 42 | ``` 43 | 44 | ### Basic Use 45 | 46 | Make a `config/` directory inside the main app directory, and put the following 47 | into a file named `default.json` in that directory: 48 | 49 | ```json 50 | { 51 | "settings": { 52 | "db": { 53 | "host": "localhost", 54 | "port": 5432, 55 | "database": "clients" 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | Next, add another file called `production.json` to the `config/` directory, with 62 | this content: 63 | 64 | ```json 65 | { 66 | "settings": { 67 | "db": { 68 | "host": "prod-db-server" 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | Finally, in our Node.js app, we can import Confippet and use the configuration 75 | we've created: 76 | 77 | ```javascript 78 | const config = require("electrode-confippet").config; 79 | const db = config.$("settings.db"); 80 | ``` 81 | 82 | In this example, `default.json` will be loaded in all environments, whereas 83 | `production.json` will be loaded only when the `NODE_ENV` environment variable 84 | is set to `production`. In that case, the value of `host` in the `db` object 85 | will be overwritten by the value in `production.json`. 86 | 87 | ## Config Composition 88 | 89 | Confippet's `presetConfig` composes together files in the `config/` directory, 90 | in the following order: 91 | 92 | > This is the same as [node-config files]. 93 | 94 | 1. `default.EXT` 95 | 1. `default-{instance}.EXT` 96 | 1. `{deployment}.EXT` 97 | 1. `{deployment}-{instance}.EXT` 98 | 1. `{short_hostname}.EXT` 99 | 1. `{short_hostname}-{instance}.EXT` 100 | 1. `{short_hostname}-{deployment}.EXT` 101 | 1. `{short_hostname}-{deployment}-{instance}.EXT` 102 | 1. `{full_hostname}.EXT` 103 | 1. `{full_hostname}-{instance}.EXT` 104 | 1. `{full_hostname}-{deployment}.EXT` 105 | 1. `{full_hostname}-{deployment}-{instance}.EXT` 106 | 1. `local.EXT` 107 | 1. `local-{instance}.EXT` 108 | 1. `local-{deployment}.EXT` 109 | 1. `local-{deployment}-{instance}.EXT` 110 | 111 | Where: 112 | 113 | - `EXT` can be any of `["json", "yaml", "js"]`. Confippet will load all of them, 114 | in that order. Each time it finds a config file, the values in that file will 115 | be loaded and merged into the config store. So `js` overrides `yaml`, which 116 | overrides `json`. You can add handlers for other file types and change their 117 | loading order—see [composeConfig] for further details. 118 | - `{instance}` is your app's instance string in multi-instance deployments 119 | (specified by the `NODE_APP_INSTANCE` environment variable). 120 | - `{short_hostname}` is your server name up to the first dot. 121 | - `{full_hostname}` is your whole server name. 122 | - `{deployment}` is your deployment environment (specified by the `NODE_ENV` 123 | environment variable). 124 | 125 | Overridden values are handled as follows: 126 | 127 | - Objects are merged. 128 | - Primitive values (string, boolean, number) are replaced. 129 | - **Arrays are replaced**, unless the key starts with `+` _and_ both the source 130 | and the target are arrays. In that case, the two arrays are joined together 131 | using Lodash's `_.union` method. 132 | 133 | After Confippet loads all available configuration files, it will look for 134 | override JSON strings from the `NODE_CONFIG` and `CONFIPPET*` environment 135 | variables. See the next section for details. 136 | 137 | ## Environment Variables 138 | 139 | Confippet reads the following environment variables when composing a config 140 | store: 141 | 142 | - `AUTO_LOAD_CONFIG_OFF` - If this is set, then Confippet will **not** 143 | automatically load any configuration into the preset `config` store. 144 | `Confippet.config` will be an empty store. This enables you to customize the 145 | config structure before loading. 146 | - `NODE_CONFIG_DIR` - Set the directory to search for config files. By default, 147 | Confippet looks in the `config` directory for config files. 148 | - `NODE_ENV` - By default, Confippet loads `development` config files after 149 | loading `default`. Set this environment variable to change to a different 150 | deployment, such as `production`. 151 | - `NODE_APP_INSTANCE` - If your app is deployed to multiple instances, you can 152 | set this to load instance-specific configurations. 153 | - `AUTO_LOAD_CONFIG_PROCESS_OFF` - By default, after composing the config from 154 | all sources, Confippet will use [processConfig] to process 155 | [templates](#using-templates). You can set this environment variable to 156 | disable template processing. 157 | - `NODE_CONFIG` - You can set this to a valid JSON string and Confippet will 158 | parse it to override the configuration. 159 | - `CONFIPPET*` - Any environment variables that starts with `CONFIPPET` will be 160 | parsed as JSON strings to override the configuration. 161 | 162 | ## Using Templates 163 | 164 | Values in your config files can be templates, which will be resolved with 165 | a preset context. See [processConfig] for more information about how to use 166 | config value templates. 167 | 168 | ## Usage in Node Modules 169 | 170 | If you have a Node.js module that has its own configurations based on 171 | environment variables, like `NODE_ENV`, you can use Confippet to load config 172 | files for your module. 173 | 174 | The example below will use the [default compose options] to compose 175 | configurations from the directory `config` under the script's directory 176 | (`__dirname`). 177 | 178 | ```javascript 179 | const Confippet = require("electrode-confippet"); 180 | 181 | const options = { 182 | dirs: [Path.join(__dirname, "config")], 183 | warnMissing: false, 184 | context: { 185 | deployment: process.env.NODE_ENV 186 | } 187 | }; 188 | 189 | const defaults = { 190 | foo: "bar" 191 | }; 192 | 193 | const config = Confippet.loadConfig(options, defaults /* refresh: true */); 194 | ``` 195 | 196 | ## Customization 197 | 198 | The [composeConfig] feature supports a fully customizable and extendable config 199 | structure. Even Confippet's own preset config structure can be extended, since 200 | it's composed using the same feature. 201 | 202 | If you want to use the preset config, but add an extension handler or insert 203 | a source, you can turn off auto loading, and load it yourself with your own 204 | options. 205 | 206 | > **NOTE:** This has to happen before any other file accesses 207 | > `Confippet.config`. You should do this in your startup `index.js` file. 208 | 209 | For example: 210 | 211 | ```javascript 212 | process.env.AUTO_LOAD_CONFIG_OFF = true; 213 | 214 | const JSON5 = require("json5"); 215 | const fs = require("fs"); 216 | const Confippet = require("electrode-confippet"); 217 | const config = Confippet.config; 218 | 219 | const extHandlers = Confippet.extHandlers; 220 | extHandlers.json5 = (fullF) => JSON5.parse(fs.readFileSync(fullF, "utf8")); 221 | 222 | Confippet.presetConfig.load(config, { 223 | extSearch: ["json", "json5", "yaml", "js"], 224 | extHandlers, 225 | providers: { 226 | customConfig: { 227 | name: "{{env.CUSTOM_CONFIG_SOURCE}}", 228 | order: 300, 229 | type: Confippet.providerTypes.required 230 | } 231 | } 232 | }); 233 | ``` 234 | 235 | The above compose option adds a new provider that looks for a file named by the 236 | environment variable `CUSTOM_CONFIG_SOURCE` and will be loaded after all default 237 | sources are loaded (controlled by `order`). 238 | 239 | It also adds a new extension handler, `json5`, to be loaded after `json`. 240 | 241 | To further understand the `_$` and the `compose` options, please see the 242 | documentation for [store], [composeConfig], and [processConfig]. 243 | 244 | Built with :heart: by [Team Electrode](https://github.com/orgs/electrode-io/people) @WalmartLabs. 245 | 246 | [node-config npm module]: https://github.com/lorenwest/node-config 247 | [node-config files]: https://github.com/lorenwest/node-config/wiki/Configuration-Files 248 | [store]: ./store.md 249 | [composeconfig]: ./compose.md 250 | [processconfig]: ./templates.md 251 | [default compose options]: ./lib/default-compose-opts.js 252 | [npm-image]: https://badge.fury.io/js/electrode-confippet.svg 253 | [npm-url]: https://npmjs.org/package/electrode-confippet 254 | [travis-image]: https://travis-ci.org/electrode-io/electrode-confippet.svg?branch=master 255 | [travis-url]: https://travis-ci.org/electrode-io/electrode-confippet 256 | [daviddm-image]: https://david-dm.org/electrode-io/electrode-confippet.svg?theme=shields.io 257 | [daviddm-url]: https://david-dm.org/electrode-io/electrode-confippet 258 | -------------------------------------------------------------------------------- /compose.md: -------------------------------------------------------------------------------- 1 | # Compose Config 2 | 3 | A composer that can take multiple config files and pick a subset base on environment settings and compose them into a single configuration store. 4 | 5 | ## API 6 | 7 | ### [confippet.composeConfig](#confippetcomposeconfig) 8 | 9 | ```js 10 | const config = composeConfig(options); 11 | ``` 12 | 13 | * `options` 14 | 15 | ```js 16 | { 17 | dir: "config", // Directory containing all the config files 18 | useDefaults: false, // If true or undefined, then include the default compose options 19 | extSearch: [ "json", "yaml", "js" ], // Array of extensions of config files to search, in the order given 20 | extHandlers: { // handlers to load the config file for each extension 21 | json: require, 22 | js: require, 23 | yaml: (filename) => YamlLoader(filename) 24 | }, 25 | failMissing: true, // If true, then fail when an expected config file is missing 26 | warnMissing: true, // If true, then log warning if an expected config file is missing 27 | verbose: false, // If true, then log extra messages to conolse 28 | providers: { // List of config providers 29 | default: { 30 | name: "default", // prefix of the config file name 31 | filter: true, // If not undefined, must be truthy, or an array of all truthy values to enable this provider 32 | type: "required", // provider type, can be required, disabled, optional, or warn 33 | handler: () => { }, // optional handler to load the config 34 | order: 0 35 | } 36 | }, 37 | providerList: undefined, // An array to select providers to use, if undefined then all providers will be used 38 | context: { // context for processing templates in the options itself 39 | deployment: "development" 40 | } 41 | } 42 | ``` 43 | 44 | Where: 45 | 46 | - `dir` - Directory inside which to look for config files 47 | - `useDefaults` - Set to false to not use the default [compose config options](./lib/default-compose-opts.js) 48 | - `extSearch` - Array of extensions of the config files to search, in the order given. 49 | - `extHandlers` - Functions for each extension to handle loading of the config file 50 | - `failMissing` - If true, then fail when an expected config file is missing. 51 | - `warnMissing` - If true, then log warning message when an expected config file is missing. 52 | - `verbose` - If true, then log extra messages to console 53 | - `providers` - A list of objects specifying each config file to load from the config directory 54 | - `providerList` - An array to select providers to use, if undefined then all providers will be used 55 | - `context` - You can use templates in your `options` values and `composeConfig` will process your options with [processConfig]. 56 | 57 | Provider spec: 58 | 59 | - `name` - Prefix of the config file name 60 | - `filter` - If not undefined, must be truthy, or an array of all truthy values to enable this provider 61 | - This allows turn off a provider by context. For example, `["{{instance}}"]` would turn on a provider only if `instance` is defined in context. 62 | - `type` - Type of provider. Can be `required`, `disabled`, `optional`, or `warn`. 63 | - `handler` - An optional handler to retrieve the config 64 | - `order` - A number to control the order the providers to process. Lower values will be loaded first. The config that loads last will override the ones loaded earlier. 65 | 66 | ### Usage in Preset Config 67 | 68 | You can see how preset config use the compose config and store features. 69 | 70 | [preset-config.js](./lib/preset-config.js) 71 | 72 | And the compose config options for the preset-config 73 | 74 | [default-compose-opts.js](./lib/default-compose-opts.js) 75 | 76 | [processConfig]: ./templates.md 77 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const config = require("./lib/store")(); 4 | const presetConfig = require("./lib/preset-config"); 5 | 6 | presetConfig.autoLoad(config); 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electrode-io/electrode-confippet/7cd518be5bcaff0559b2094514581f5668f52eae/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electrode-io/electrode-confippet/7cd518be5bcaff0559b2094514581f5668f52eae/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electrode-io/electrode-confippet/7cd518be5bcaff0559b2094514581f5668f52eae/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electrode-io/electrode-confippet/7cd518be5bcaff0559b2094514581f5668f52eae/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | electrode-confippet 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 |

electrode-confippet

54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | 62 |

Electrode Confippet NPM version Build Status

63 | 64 |

Confippet is a versatile, flexible utility for managing configurations of 65 | Node.js applications. It's simple to get started, and can be customized and 66 | extended to meet the needs of your app.

67 |
68 | 69 |

Contents

70 |
71 | 88 | 89 |

Features

90 |
91 |
    92 |
  • Simple - Confippet's presetConfig automatically composes a single config 93 | object from multiple files. For most applications, no further customization is 94 | necessary.
  • 95 |
  • Flexible - Supports JSON, YAML, and JavaScript config files.
  • 96 |
  • Powerful - Handles complex multi-instance enterprise deployments.
  • 97 |
98 | 99 |

Getting Started

100 |
101 |

In this example, we'll create two config files: a default file that always 102 | loads, and a production file that loads only when the NODE_ENV environment 103 | variable is set to production. We'll then import those files into a standard 104 | Node.js app.

105 | 106 |

Installation

107 |
108 |
npm install electrode-confippet --save
109 | 
110 | 111 |

Basic Use

112 |
113 |

Make a config/ directory inside the main app directory, and put the following 114 | into a file named default.json in that directory:

115 |
{
116 |   "settings": {
117 |     "db": {
118 |       "host": "localhost",
119 |       "port": 5432,
120 |       "database": "clients"
121 |     }
122 |   }
123 | }
124 | 
125 |

Next, add another file called production.json to the config/ directory, with 126 | this content:

127 |
{
128 |   "settings": {
129 |     "db": {
130 |       "host": "prod-db-server"
131 |     }
132 |   }
133 | }
134 | 
135 |

Finally, in our Node.js app, we can import Confippet and use the configuration 136 | we've created:

137 |
const config = require("electrode-confippet").config;
138 | const db = config.$("settings.db");
139 | 
140 |

In this example, default.json will be loaded in all environments, whereas 141 | production.json will be loaded only when the NODE_ENV environment variable 142 | is set to production. In that case, the value of host in the db object 143 | will be overwritten by the value in production.json.

144 | 145 |

Config Composition

146 |
147 |

Confippet's presetConfig composes together files in the config/ directory, 148 | in the following order:

149 |
150 |

This is the same as node-config files.

151 |
152 |
    153 |
  1. default.EXT
  2. 154 |
  3. default-{instance}.EXT
  4. 155 |
  5. {deployment}.EXT
  6. 156 |
  7. {deployment}-{instance}.EXT
  8. 157 |
  9. {short_hostname}.EXT
  10. 158 |
  11. {short_hostname}-{instance}.EXT
  12. 159 |
  13. {short_hostname}-{deployment}.EXT
  14. 160 |
  15. {short_hostname}-{deployment}-{instance}.EXT
  16. 161 |
  17. {full_hostname}.EXT
  18. 162 |
  19. {full_hostname}-{instance}.EXT
  20. 163 |
  21. {full_hostname}-{deployment}.EXT
  22. 164 |
  23. {full_hostname}-{deployment}-{instance}.EXT
  24. 165 |
  25. local.EXT
  26. 166 |
  27. local-{instance}.EXT
  28. 167 |
  29. local-{deployment}.EXT
  30. 168 |
  31. local-{deployment}-{instance}.EXT
  32. 169 |
170 |

Where:

171 |
    172 |
  • EXT can be any of ["json", "yaml", "js"]. Confippet will load all of them, 173 | in that order. Each time it finds a config file, the values in that file will 174 | be loaded and merged into the config store. So js overrides yaml, which 175 | overrides json. You can add handlers for other file types and change their 176 | loading order—see composeConfig for further details.
  • 177 |
  • {instance} is your app's instance string in multi-instance deployments 178 | (specified by the NODE_APP_INSTANCE environment variable).
  • 179 |
  • {short_hostname} is your server name up to the first dot.
  • 180 |
  • {full_hostname} is your whole server name.
  • 181 |
  • {deployment} is your deployment environment (specified by the NODE_ENV 182 | environment variable).
  • 183 |
184 |

Overridden values are handled as follows:

185 |
    186 |
  • Objects are merged.
  • 187 |
  • Primitive values (string, boolean, number) are replaced.
  • 188 |
  • Arrays are replaced, unless the key starts with + and both the source 189 | and the target are arrays. In that case, the two arrays are joined together 190 | using Lodash's _.union method.
  • 191 |
192 |

After Confippet loads all available configuration files, it will look for 193 | override JSON strings from the NODE_CONFIG and CONFIPPET* environment 194 | variables. See the next section for details.

195 | 196 |

Environment Variables

197 |
198 |

Confippet reads the following environment variables when composing a config 199 | store:

200 |
    201 |
  • AUTO_LOAD_CONFIG_OFF - If this is set, then Confippet will not 202 | automatically load any configuration into the preset config store. 203 | Confippet.config will be an empty store. This enables you to customize the 204 | config structure before loading.
  • 205 |
  • NODE_CONFIG_DIR - Set the directory to search for config files. By default, 206 | Confippet looks in the config directory for config files.
  • 207 |
  • NODE_ENV - By default, Confippet loads development config files after 208 | loading default. Set this environment variable to change to a different 209 | deployment, such as production.
  • 210 |
  • NODE_APP_INSTANCE - If your app is deployed to multiple instances, you can 211 | set this to load instance-specific configurations.
  • 212 |
  • AUTO_LOAD_CONFIG_PROCESS_OFF - By default, after composing the config from 213 | all sources, Confippet will use processConfig to process 214 | templates. You can set this environment variable to 215 | disable template processing.
  • 216 |
  • NODE_CONFIG - You can set this to a valid JSON string and Confippet will 217 | parse it to override the configuration.
  • 218 |
  • CONFIPPET* - Any environment variables that starts with CONFIPPET will be 219 | parsed as JSON strings to override the configuration.
  • 220 |
221 | 222 |

Using Templates

223 |
224 |

Values in your config files can be templates, which will be resolved with 225 | a preset context. See processConfig for more information about how to use 226 | config value templates.

227 | 228 |

Usage in Node Modules

229 |
230 |

If you have a Node.js module that has its own configurations based on 231 | environment variables, like NODE_ENV, you can use Confippet to load config 232 | files for your module.

233 |

The example below will use the default compose options to compose 234 | configurations from the directory config under the script's directory 235 | (__dirname).

236 |
const Confippet = require("electrode-confippet");
237 | 
238 | const options = {
239 |   dirs: [Path.join(__dirname, "config")],
240 |   warnMissing: false,
241 |   context: {
242 |     deployment: process.env.NODE_ENV
243 |   }
244 | };
245 | 
246 | const defaults = {
247 |   foo: "bar"
248 | };
249 | 
250 | const config = Confippet.loadConfig(options, defaults /* refresh: true */);
251 | 
252 | 253 |

Customization

254 |
255 |

The composeConfig feature supports a fully customizable and extendable config 256 | structure. Even Confippet's own preset config structure can be extended, since 257 | it's composed using the same feature.

258 |

If you want to use the preset config, but add an extension handler or insert 259 | a source, you can turn off auto loading, and load it yourself with your own 260 | options.

261 |
262 |

NOTE: This has to happen before any other file accesses 263 | Confippet.config. You should do this in your startup index.js file.

264 |
265 |

For example:

266 |
process.env.AUTO_LOAD_CONFIG_OFF = true;
267 | 
268 | const JSON5 = require("json5");
269 | const fs = require("fs");
270 | const Confippet = require("electrode-confippet");
271 | const config = Confippet.config;
272 | 
273 | const extHandlers = Confippet.extHandlers;
274 | extHandlers.json5 = (fullF) => JSON5.parse(fs.readFileSync(fullF, "utf8"));
275 | 
276 | Confippet.presetConfig.load(config, {
277 |   extSearch: ["json", "json5", "yaml", "js"],
278 |   extHandlers,
279 |   providers: {
280 |     customConfig: {
281 |       name: "{{env.CUSTOM_CONFIG_SOURCE}}",
282 |       order: 300,
283 |       type: Confippet.providerTypes.required
284 |     }
285 |   }
286 | });
287 | 
288 |

The above compose option adds a new provider that looks for a file named by the 289 | environment variable CUSTOM_CONFIG_SOURCE and will be loaded after all default 290 | sources are loaded (controlled by order).

291 |

It also adds a new extension handler, json5, to be loaded after json.

292 |

To further understand the _$ and the compose options, please see the 293 | documentation for store, composeConfig, and processConfig.

294 |

Built with :heart: by Team Electrode @WalmartLabs.

295 |
296 |
297 | 340 |
341 |
342 |
343 |
344 |

Legend

345 |
346 |
    347 |
  • Variable
  • 348 |
  • Function
  • 349 |
  • Type alias
  • 350 |
351 |
352 |
353 |
354 |
355 |

Generated using TypeDoc

356 |
357 |
358 | 359 | 360 | -------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | electrode-confippet 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 |

electrode-confippet

54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |

Index

62 |
63 |
64 |
65 |

Modules

66 | 78 |
79 |
80 |
81 |
82 |
83 | 126 |
127 |
128 |
129 |
130 |

Legend

131 |
132 |
    133 |
  • Variable
  • 134 |
  • Function
  • 135 |
  • Type alias
  • 136 |
137 |
138 |
139 |
140 |
141 |

Generated using TypeDoc

142 |
143 |
144 | 145 | 146 | -------------------------------------------------------------------------------- /docs/modules/compose_config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | compose-config | electrode-confippet 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Module compose-config

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Index

70 |
71 |
72 |
73 |

Functions

74 | 77 |
78 |
79 |
80 |
81 |
82 |

Functions

83 |
84 | 85 |

export=

86 |
    87 |
  • export=(options: any): {}
  • 88 |
89 |
    90 |
  • 91 | 96 |

    Parameters

    97 |
      98 |
    • 99 |
      options: any
      100 |
    • 101 |
    102 |

    Returns {}

    103 |
      104 |
    105 |
  • 106 |
107 |
108 |
109 |
110 | 156 |
157 |
158 |
159 |
160 |

Legend

161 |
162 |
    163 |
  • Variable
  • 164 |
  • Function
  • 165 |
  • Type alias
  • 166 |
167 |
168 |
169 |
170 |
171 |

Generated using TypeDoc

172 |
173 |
174 | 175 | 176 | -------------------------------------------------------------------------------- /docs/modules/ext_handlers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ext-handlers | electrode-confippet 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Module ext-handlers

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Index

70 |
71 |
72 |
73 |

Properties

74 | 77 |
78 |
79 |
80 |
81 |
82 |

Properties

83 |
84 | 85 |

export=

86 |
export=: { js: (fullFilePath: string) => unknown; json: (fullFilePath: string) => unknown; ts: (fullFilePath: string) => unknown; yaml: (fullDirPath: string) => any }
87 | 89 |
90 |

Type declaration

91 |
    92 |
  • 93 |
    js: (fullFilePath: string) => unknown
    94 |
      95 |
    • 96 |
        97 |
      • (fullFilePath: string): unknown
      • 98 |
      99 |
        100 |
      • 101 |
        102 |
        103 |

        Load a config partial from a JS module

        104 |
        105 |
        106 |

        Parameters

        107 |
          108 |
        • 109 |
          fullFilePath: string
          110 |
          111 |

          full path to the the file

          112 |
          113 |
        • 114 |
        115 |

        Returns unknown

        116 |

        If it's an ES6 module and has default, then default is returned, 117 | else returns the module as config partial.

        118 |
      • 119 |
      120 |
    • 121 |
    122 |
  • 123 |
  • 124 |
    json: (fullFilePath: string) => unknown
    125 |
      126 |
    • 127 |
        128 |
      • (fullFilePath: string): unknown
      • 129 |
      130 |
        131 |
      • 132 |
        133 |
        134 |

        Load a config partial from a JS module

        135 |
        136 |
        137 |

        Parameters

        138 |
          139 |
        • 140 |
          fullFilePath: string
          141 |
          142 |

          full path to the the file

          143 |
          144 |
        • 145 |
        146 |

        Returns unknown

        147 |

        If it's an ES6 module and has default, then default is returned, 148 | else returns the module as config partial.

        149 |
      • 150 |
      151 |
    • 152 |
    153 |
  • 154 |
  • 155 |
    ts: (fullFilePath: string) => unknown
    156 |
      157 |
    • 158 |
        159 |
      • (fullFilePath: string): unknown
      • 160 |
      161 |
        162 |
      • 163 |
        164 |
        165 |

        Load a config partial from a JS module

        166 |
        167 |
        168 |

        Parameters

        169 |
          170 |
        • 171 |
          fullFilePath: string
          172 |
          173 |

          full path to the the file

          174 |
          175 |
        • 176 |
        177 |

        Returns unknown

        178 |

        If it's an ES6 module and has default, then default is returned, 179 | else returns the module as config partial.

        180 |
      • 181 |
      182 |
    • 183 |
    184 |
  • 185 |
  • 186 |
    yaml: (fullDirPath: string) => any
    187 |
      188 |
    • 189 |
        190 |
      • (fullDirPath: string): any
      • 191 |
      192 |
        193 |
      • 194 |

        Parameters

        195 |
          196 |
        • 197 |
          fullDirPath: string
          198 |
        • 199 |
        200 |

        Returns any

        201 |
      • 202 |
      203 |
    • 204 |
    205 |
  • 206 |
207 |
208 |
209 |
210 |
211 | 257 |
258 |
259 |
260 |
261 |

Legend

262 |
263 |
    264 |
  • Variable
  • 265 |
  • Function
  • 266 |
  • Type alias
  • 267 |
268 |
269 |
270 |
271 |
272 |

Generated using TypeDoc

273 |
274 |
275 | 276 | 277 | -------------------------------------------------------------------------------- /docs/modules/load_config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | load-config | electrode-confippet 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Module load-config

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Index

70 |
71 |
72 |
73 |

Type aliases

74 | 80 |
81 |
82 |

Functions

83 | 86 |
87 |
88 |
89 |
90 |
91 |

Type aliases

92 |
93 | 94 |

ExtHandler

95 |
ExtHandler: (_fullFilePath: string) => unknown
96 | 101 |
102 |
103 |

handler for a config file of a specific extension

104 |
105 |
106 |
param
107 |

full path to the config file

108 |
109 |
returns
110 |

the config object loaded from the file

111 |
112 |
113 |
114 |
115 |

Type declaration

116 |
    117 |
  • 118 |
      119 |
    • (_fullFilePath: string): unknown
    • 120 |
    121 |
      122 |
    • 123 |

      Parameters

      124 |
        125 |
      • 126 |
        _fullFilePath: string
        127 |
      • 128 |
      129 |

      Returns unknown

      130 |
    • 131 |
    132 |
  • 133 |
134 |
135 |
136 |
137 | 138 |

LoadContext

139 |
LoadContext: { defaultFilter?: "enabled" | ""; defaultType?: ProviderTypes; deployment?: string; fullHostname?: string; hostname?: string; instance?: string }
140 | 145 |
146 |
147 |

Context of environment values to determine which config partial to load from a config directory

148 |
149 |
150 |
151 |

Type declaration

152 |
    153 |
  • 154 |
    Optional defaultFilter?: "enabled" | ""
    155 |
    156 |
    157 |

    default filter for providers - default filtering rule for internal providers.

    158 |
    159 |
      160 |
    • Default "enabled" - all internal providers are enabled
    • 161 |
    • Set this to "" and all default providers are disabled and you must set your own providers 162 | to load config partials
    • 163 |
    164 |
    165 |
  • 166 |
  • 167 |
    Optional defaultType?: ProviderTypes
    168 |
    169 |
    170 |

    Provider type for the internal default and deployment config provider. 171 | Default "required" - which means user must have a default.js and a file for the NODE_ENV 172 | deployment, such as development.js or production.js

    173 |
    174 |
    175 |
  • 176 |
  • 177 |
    Optional deployment?: string
    178 |
    179 |
    180 |

    deployment env. Take from env NODE_ENV if not specified. Default: "development"

    181 |
    182 |
    183 |
  • 184 |
  • 185 |
    Optional fullHostname?: string
    186 |
    187 |
    188 |

    full FQDN hostname

    189 |
    190 |
    191 |
  • 192 |
  • 193 |
    Optional hostname?: string
    194 |
    195 |
    196 |

    short hostname

    197 |
    198 |
    199 |
  • 200 |
  • 201 |
    Optional instance?: string
    202 |
    203 |
    204 |

    process instance. Take from env NODE_APP_INSTANCE if not specified. Default: ""

    205 |
    206 |
    207 |
  • 208 |
209 |
210 |
211 |
212 | 213 |

LoadOptions

214 |
LoadOptions: { cache?: boolean; context?: LoadContext; dir: string; extHandlers?: Record<string, ExtHandler>; extSearch?: string[]; failMissing?: boolean; verbose?: boolean; warnMissing?: boolean }
215 | 220 |
221 |
222 |

Options for loading and composing config partial files from a directory

223 |
224 |

Sample:

225 |
import { loadConfig } from "electrode-confippet";
226 | 
227 | const config = loadConfig({
228 |   dir: Path.join(__dirname, "config")
229 | })
230 | 
231 |
232 |
233 |

Type declaration

234 |
    235 |
  • 236 |
    Optional cache?: boolean
    237 |
    238 |
    239 |

    set to false to skip cache, load and return a new copy, Default: true

    240 |
    241 |
    242 |
  • 243 |
  • 244 |
    Optional context?: LoadContext
    245 |
    246 |
    247 |

    Environment setting context

    248 |
    249 |

    Not for typical use cases. The context generally come from 250 | env variables: NODE_ENV and NODE_APP_INSTANCE

    251 |
    252 |
  • 253 |
  • 254 |
    dir: string
    255 |
    256 |
    257 |

    The directory to load config files from. Will use Path.resolve to generate 258 | absolute full path for each one.

    259 |
    260 |
    261 |
  • 262 |
  • 263 |
    Optional extHandlers?: Record<string, ExtHandler>
    264 |
    265 |
    266 |

    Custom handlers for a file extension

    267 |
    268 |

    For Example:

    269 |
    {
    270 |   yaml: (fullF) => jsYaml.load(fs.readFileSync(fullF, "utf8"))
    271 | }
    272 | 
    273 |
    274 |
  • 275 |
  • 276 |
    Optional extSearch?: string[]
    277 |
    278 |
    279 |

    Customize config file extensions search.

    280 |
    281 |

    Default: `["json", "yaml", "js", "ts"]

    282 |
    283 |
  • 284 |
  • 285 |
    Optional failMissing?: boolean
    286 |
    287 |
    288 |

    Throw error if a partial provider requires config file but it's missing

    289 |
    290 |
    291 |
  • 292 |
  • 293 |
    Optional verbose?: boolean
    294 |
    295 |
    296 |

    Enable console.log noises about what's happening

    297 |
    298 |
    299 |
  • 300 |
  • 301 |
    Optional warnMissing?: boolean
    302 |
    303 |
    304 |

    console.error a warning if a partial provider requires config file but it's missing

    305 |
    306 |
    307 |
  • 308 |
309 |
310 |
311 |
312 | 313 |

ProviderTypes

314 |
ProviderTypes: "required" | "disabled" | "optional" | "warn"
315 | 320 |
321 |
322 |

Type of a config partial provider:

323 |
324 |
    325 |
  • required - config partial file must exist for the provider
  • 326 |
  • disabled - do not run the provider
  • 327 |
  • optional - config partial file doesn't need to exist for the provider
  • 328 |
  • warn - log a warning if partial file doesn't exist for the provider
  • 329 |
330 |
331 |
332 |
333 |
334 |

Functions

335 |
336 | 337 |

Const loadConfig

338 |
    339 |
  • loadConfig(options: LoadOptions, defaults?: unknown, refresh?: boolean): any
  • 340 |
341 |
    342 |
  • 343 | 348 |
    349 |
    350 |

    Load config files from a directory.

    351 |
    352 |
      353 |
    • the directory can contain multiple files, each has a partial config. this function 354 | will load the files according to various environment settings and compose them 355 | into a single one.

      356 |
    • 357 |
    • Each config file in the directory can be JSON, JS, Yaml, or TS. For JS or TS, if it's 358 | ES6 Module and has default export, then it's used as config partial, else the whole 359 | module will be used.

      360 |
    • 361 |
    • The result is cached using the full directory path

      362 |
    • 363 |
    364 |
    365 |

    Parameters

    366 |
      367 |
    • 368 |
      options: LoadOptions
      369 |
      370 |

      LoadOptions for loading config files

      371 |
      372 |
    • 373 |
    • 374 |
      Optional defaults: unknown
      375 |
      376 |

      default config values

      377 |
      378 |
    • 379 |
    • 380 |
      Optional refresh: boolean
      381 |
      382 |

      refresh the cached copy - NOTE this will clear any values user set in the config

      383 |
      384 |
    • 385 |
    386 |

    Returns any

    387 |

    returns config

    388 |
  • 389 |
390 |
391 |
392 |
393 | 451 |
452 |
453 |
454 |
455 |

Legend

456 |
457 |
    458 |
  • Variable
  • 459 |
  • Function
  • 460 |
  • Type alias
  • 461 |
462 |
463 |
464 |
465 |
466 |

Generated using TypeDoc

467 |
468 |
469 | 470 | 471 | -------------------------------------------------------------------------------- /docs/modules/preset_config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | preset-config | electrode-confippet 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Module preset-config

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Index

70 |
71 |
72 |
73 |

Properties

74 | 77 |
78 |
79 |
80 |
81 |
82 |

Properties

83 |
84 | 85 |

export=

86 |
export=: { autoLoad: (config: any, options: any) => void; load: (config: any, options: any) => void }
87 | 89 |
90 |

Type declaration

91 |
    92 |
  • 93 |
    autoLoad: (config: any, options: any) => void
    94 |
      95 |
    • 96 |
        97 |
      • (config: any, options: any): void
      • 98 |
      99 |
        100 |
      • 101 |

        Parameters

        102 |
          103 |
        • 104 |
          config: any
          105 |
        • 106 |
        • 107 |
          options: any
          108 |
        • 109 |
        110 |

        Returns void

        111 |
      • 112 |
      113 |
    • 114 |
    115 |
  • 116 |
  • 117 |
    load: (config: any, options: any) => void
    118 |
      119 |
    • 120 |
        121 |
      • (config: any, options: any): void
      • 122 |
      123 |
        124 |
      • 125 |

        Parameters

        126 |
          127 |
        • 128 |
          config: any
          129 |
        • 130 |
        • 131 |
          options: any
          132 |
        • 133 |
        134 |

        Returns void

        135 |
      • 136 |
      137 |
    • 138 |
    139 |
  • 140 |
141 |
142 |
143 |
144 |
145 | 191 |
192 |
193 |
194 |
195 |

Legend

196 |
197 |
    198 |
  • Variable
  • 199 |
  • Function
  • 200 |
  • Type alias
  • 201 |
202 |
203 |
204 |
205 |
206 |

Generated using TypeDoc

207 |
208 |
209 | 210 | 211 | -------------------------------------------------------------------------------- /docs/modules/process_config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | process-config | electrode-confippet 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Module process-config

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Index

70 |
71 |
72 |
73 |

References

74 | 77 |
78 |
79 |
80 |
81 |
82 |

References

83 |
84 | 85 |

export=

86 | Renames and exports __type 87 |
88 |
89 |
90 | 136 |
137 |
138 |
139 |
140 |

Legend

141 |
142 |
    143 |
  • Variable
  • 144 |
  • Function
  • 145 |
  • Type alias
  • 146 |
147 |
148 |
149 |
150 |
151 |

Generated using TypeDoc

152 |
153 |
154 | 155 | 156 | -------------------------------------------------------------------------------- /docs/modules/provider_types.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | provider-types | electrode-confippet 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Module provider-types

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Index

70 |
71 |
72 |
73 |

Properties

74 | 77 |
78 |
79 |
80 |
81 |
82 |

Properties

83 |
84 | 85 |

export=

86 |
export=: { disabled: string; optional: string; required: string; warn: string }
87 | 89 |
90 |

Type declaration

91 |
    92 |
  • 93 |
    disabled: string
    94 |
  • 95 |
  • 96 |
    optional: string
    97 |
  • 98 |
  • 99 |
    required: string
    100 |
  • 101 |
  • 102 |
    warn: string
    103 |
  • 104 |
105 |
106 |
107 |
108 |
109 | 155 |
156 |
157 |
158 |
159 |

Legend

160 |
161 |
    162 |
  • Variable
  • 163 |
  • Function
  • 164 |
  • Type alias
  • 165 |
166 |
167 |
168 |
169 |
170 |

Generated using TypeDoc

171 |
172 |
173 | 174 | 175 | -------------------------------------------------------------------------------- /docs/modules/store.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | store | electrode-confippet 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Module store

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Index

70 |
71 |
72 |
73 |

References

74 | 77 |
78 |
79 |
80 |
81 |
82 |

References

83 |
84 | 85 |

export=

86 | Renames and exports __type 87 |
88 |
89 |
90 | 136 |
137 |
138 |
139 |
140 |

Legend

141 |
142 |
    143 |
  • Variable
  • 144 |
  • Function
  • 145 |
  • Type alias
  • 146 |
147 |
148 |
149 |
150 |
151 |

Generated using TypeDoc

152 |
153 |
154 | 155 | 156 | -------------------------------------------------------------------------------- /docs/modules/util.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | util | electrode-confippet 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Module util

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Index

70 |
71 |
72 |
73 |

Variables

74 | 77 |
78 |
79 |
80 |
81 |
82 |

Variables

83 |
84 | 85 |

Const export=

86 |
export=: { merge: (...args: any[]) => any; replaceArray: (a: any, b: any) => any[]; uMerge: (...args: any[]) => any; unionArray: (a: any, b: any, k: any) => any[] } = ...
87 | 92 |
93 |

Type declaration

94 |
    95 |
  • 96 |
    merge: (...args: any[]) => any
    97 |
      98 |
    • 99 |
        100 |
      • (...args: any[]): any
      • 101 |
      102 |
        103 |
      • 104 |

        Parameters

        105 |
          106 |
        • 107 |
          Rest ...args: any[]
          108 |
        • 109 |
        110 |

        Returns any

        111 |
      • 112 |
      113 |
    • 114 |
    115 |
  • 116 |
  • 117 |
    replaceArray: (a: any, b: any) => any[]
    118 |
      119 |
    • 120 |
        121 |
      • (a: any, b: any): any[]
      • 122 |
      123 |
        124 |
      • 125 |

        Parameters

        126 |
          127 |
        • 128 |
          a: any
          129 |
        • 130 |
        • 131 |
          b: any
          132 |
        • 133 |
        134 |

        Returns any[]

        135 |
      • 136 |
      137 |
    • 138 |
    139 |
  • 140 |
  • 141 |
    uMerge: (...args: any[]) => any
    142 |
      143 |
    • 144 |
        145 |
      • (...args: any[]): any
      • 146 |
      147 |
        148 |
      • 149 |

        Parameters

        150 |
          151 |
        • 152 |
          Rest ...args: any[]
          153 |
        • 154 |
        155 |

        Returns any

        156 |
      • 157 |
      158 |
    • 159 |
    160 |
  • 161 |
  • 162 |
    unionArray: (a: any, b: any, k: any) => any[]
    163 |
      164 |
    • 165 |
        166 |
      • (a: any, b: any, k: any): any[]
      • 167 |
      168 |
        169 |
      • 170 |

        Parameters

        171 |
          172 |
        • 173 |
          a: any
          174 |
        • 175 |
        • 176 |
          b: any
          177 |
        • 178 |
        • 179 |
          k: any
          180 |
        • 181 |
        182 |

        Returns any[]

        183 |
      • 184 |
      185 |
    • 186 |
    187 |
  • 188 |
189 |
190 |
191 |
192 |
193 | 239 |
240 |
241 |
242 |
243 |

Legend

244 |
245 |
    246 |
  • Variable
  • 247 |
  • Function
  • 248 |
  • Type alias
  • 249 |
250 |
251 |
252 |
253 |
254 |

Generated using TypeDoc

255 |
256 |
257 | 258 | 259 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electrode-confippet", 3 | "version": "1.7.1", 4 | "description": "Managing NodeJS application configuration", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "rm -rf lib && tsc", 8 | "test": "xrun xarc/test-only", 9 | "check": "xrun --serial build xarc/check", 10 | "prepublishOnly": "xrun [[build, docs], xarc/check]", 11 | "coverage": "xrun --serial build xarc/test-cov", 12 | "docs": "xrun xarc/docs", 13 | "prepare": "npm run build" 14 | }, 15 | "keywords": [], 16 | "author": "Joel Chen ", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/electrode-io/electrode-confippet.git" 20 | }, 21 | "engines": { 22 | "node": ">= 4.2.2" 23 | }, 24 | "license": "Apache-2.0", 25 | "dependencies": { 26 | "js-yaml": "^3.5.3", 27 | "lodash": "^4.13.1", 28 | "tslib": "^2.1.0" 29 | }, 30 | "devDependencies": { 31 | "@istanbuljs/nyc-config-typescript": "^1.0.1", 32 | "@types/chai": "^4.2.14", 33 | "@types/lodash": "^4.14.168", 34 | "@types/mocha": "^8.2.0", 35 | "@types/node": "^14.14.16", 36 | "@types/sinon": "^9.0.10", 37 | "@types/sinon-chai": "^3.2.5", 38 | "@typescript-eslint/eslint-plugin": "^4.11.0", 39 | "@typescript-eslint/parser": "^4.11.0", 40 | "@xarc/module-dev": "^3.2.1", 41 | "@xarc/run": "^1.0.4", 42 | "babel-eslint": "^10.1.0", 43 | "chai": "^4.2.0", 44 | "eslint": "^7.16.0", 45 | "eslint-config-walmart": "^2.2.1", 46 | "eslint-plugin-filenames": "^1.1.0", 47 | "eslint-plugin-jsdoc": "^30.7.9", 48 | "intercept-stdout": "^0.1.2", 49 | "mocha": "^8.2.1", 50 | "nyc": "^15.1.0", 51 | "prettier": "^2.2.1", 52 | "sinon": "^9.2.2", 53 | "sinon-chai": "^3.5.0", 54 | "source-map-support": "^0.5.19", 55 | "ts-node": "^9.1.1", 56 | "typedoc": "^0.20.13", 57 | "typescript": "^4.1.3" 58 | }, 59 | "@xarc/module-dev": { 60 | "features": [ 61 | "eslint", 62 | "eslintTS", 63 | "mocha", 64 | "prettier", 65 | "typedoc", 66 | "typescript" 67 | ] 68 | }, 69 | "nyc": { 70 | "extends": [ 71 | "@istanbuljs/nyc-config-typescript" 72 | ], 73 | "all": true, 74 | "reporter": [ 75 | "lcov", 76 | "text", 77 | "text-summary" 78 | ], 79 | "exclude": [ 80 | "*clap.js", 81 | "*clap.ts", 82 | "coverage", 83 | "dist", 84 | "docs", 85 | "gulpfile.js", 86 | "lib/*.ts", 87 | "src", 88 | "test", 89 | "xrun*.js", 90 | "xrun*.ts" 91 | ], 92 | "check-coverage": true, 93 | "statements": 100, 94 | "branches": 100, 95 | "functions": 100, 96 | "lines": 100, 97 | "cache": false, 98 | "exclude-after-remap": false 99 | }, 100 | "eslintConfig": { 101 | "extends": [ 102 | "./node_modules/@xarc/module-dev/config/eslint/.eslintrc-node" 103 | ], 104 | "parser": "@typescript-eslint/parser", 105 | "parserOptions": { 106 | "ecmaVersion": 2020, 107 | "sourceType": "module" 108 | } 109 | }, 110 | "files": [ 111 | "lib", 112 | "src", 113 | "config.js" 114 | ], 115 | "mocha": { 116 | "require": [ 117 | "ts-node/register", 118 | "source-map-support/register", 119 | "@xarc/module-dev/config/test/setup.js" 120 | ], 121 | "recursive": true 122 | }, 123 | "prettier": { 124 | "printWidth": 100 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/compose-config.ts: -------------------------------------------------------------------------------- 1 | import * as Path from "path"; 2 | import * as fs from "fs"; 3 | import _ from "lodash"; 4 | import assert from "assert"; 5 | import defaultOpts from "./default-compose-opts"; 6 | import providerTypes from "./provider-types"; 7 | import processConfig from "./process-config"; 8 | import util from "./util"; 9 | 10 | /* eslint-disable max-statements */ 11 | function composeConfigDir(dir, data, options) { 12 | const exts = options.extSearch; 13 | const handlers = options.extHandlers; 14 | 15 | dir = Path.resolve(dir); 16 | 17 | const load = (key, provider) => { 18 | const found = exts 19 | .map((ext) => { 20 | const fullF = Path.join(dir, `${provider.name}.${ext}`); 21 | 22 | if (fs.existsSync(fullF)) { 23 | assert(handlers[ext], `Config handler for extension ${ext} missing`); 24 | util.uMerge(data, handlers[ext](fullF)); 25 | return ext; 26 | } 27 | 28 | return undefined; 29 | }) 30 | .filter((x) => x); 31 | 32 | if (_.isEmpty(found)) { 33 | const msg = 34 | `Config provider ${key}: no file ${provider.name} of ` + 35 | `extensions ${exts} found in ${dir}`; 36 | 37 | if (provider.type === providerTypes.required) { 38 | if (options.failMissing !== false) { 39 | throw new Error(msg); 40 | } 41 | } else if (provider.type === providerTypes.warn) { 42 | if (options.warnMissing !== false) { 43 | console.error("WARNING:", msg); // eslint-disable-line 44 | } 45 | } 46 | } 47 | }; 48 | 49 | const filterOff = (filter) => { 50 | // if it's an array any element of the array is falsy => filtered off 51 | // otherwise if itself is falsy => filtered off 52 | return _.isArray(filter) ? _.find(filter, (x) => !x) !== undefined : !filter; 53 | }; 54 | 55 | const isEnable = (p) => { 56 | const x = p.filter; 57 | assert(p.type, "config provider type must be specified"); 58 | return ( 59 | p.type !== providerTypes.disabled && 60 | (p.name || p.handler) && 61 | (x === undefined ? true : !filterOff(x)) 62 | ); 63 | }; 64 | 65 | const num = (x) => { 66 | return _.isString(x) ? parseInt(x, 10) : x; 67 | }; 68 | const checkNaN = (x) => { 69 | return isNaN(x) ? -1 : x; // eslint-disable-line 70 | }; 71 | const order = (p) => checkNaN(num(p.order)); 72 | 73 | const providers = options.providers; 74 | 75 | const getList = () => options.providerList || Object.keys(providers); 76 | 77 | const list = getList().filter((k) => isEnable(providers[k])); 78 | 79 | assert(list.length > 0, "config providers empty"); 80 | 81 | list 82 | .sort((a, b) => order(providers[a]) - order(providers[b])) 83 | .forEach((k) => { 84 | const p = providers[k]; 85 | if (p.handler) { 86 | if (options.verbose) { 87 | console.log("Confippet.compose: calling config provider:", k, JSON.stringify(p)); // eslint-disable-line 88 | } 89 | util.uMerge(data, p.handler()); 90 | } else { 91 | if (options.verbose) { 92 | console.log("Confippet.compose: loading config provider:", k, JSON.stringify(p)); // eslint-disable-line 93 | } 94 | load(k, p); 95 | } 96 | }); 97 | 98 | return data; 99 | } 100 | 101 | function composeConfig(options) { 102 | options = options || {}; 103 | 104 | if (options.useDefaults !== false) { 105 | options = util.merge(defaultOpts(), options); 106 | } 107 | 108 | processConfig(options, options); 109 | 110 | const dirs = options.dirs || [options.dir]; 111 | const data = {}; 112 | 113 | dirs.forEach((dir) => { 114 | composeConfigDir(dir, data, options); 115 | }); 116 | 117 | return data; 118 | } 119 | 120 | export = composeConfig; 121 | -------------------------------------------------------------------------------- /src/default-compose-opts.ts: -------------------------------------------------------------------------------- 1 | import providerTypes from "./provider-types"; 2 | import util from "./util"; 3 | 4 | const envOrder = 1000; 5 | const confippetEnvOrder = 2000; 6 | function defaultOpts() { 7 | return { 8 | dir: "config", 9 | extSearch: ["json", "yaml", "js", "ts"], 10 | extHandlers: require("./ext-handlers"), // eslint-disable-line 11 | failMissing: true, // whether fails if a required provider missing 12 | warnMissing: true, // whether warns if a warn provider missing 13 | verbose: false, 14 | providerList: undefined, 15 | // 16 | // {{deployment}} - resolves to "development" by default 17 | // {{instance}} - resolves to "" by default 18 | // {{hostname}} - resolves to "" by default 19 | // {{fullHostname}} - resolves to "" by default 20 | // 21 | // types: required, optional, warn, disabled 22 | // 23 | providers: { 24 | default: { 25 | name: "default", 26 | filter: ["{{defaultFilter}}"], 27 | type: "{{defaultType}}", 28 | order: 100 29 | }, 30 | defaultInstance: { 31 | name: "default-{{instance}}", 32 | filter: ["{{defaultFilter}}", "{{instance}}"], 33 | type: providerTypes.warn, 34 | order: 120 35 | }, 36 | deployment: { 37 | name: "{{deployment}}", 38 | filter: ["{{defaultFilter}}"], 39 | type: "{{defaultType}}", 40 | order: 140 41 | }, 42 | deploymentInstance: { 43 | name: "{{deployment}}-{{instance}}", 44 | filter: ["{{defaultFilter}}", "{{instance}}"], 45 | type: providerTypes.warn, 46 | order: 160 47 | }, 48 | hostname: { 49 | name: "{{hostname}}", 50 | filter: ["{{defaultFilter}}"], 51 | type: providerTypes.optional, 52 | order: 180 53 | }, 54 | hostnameInstance: { 55 | name: "{{hostname}}-{{instance}}", 56 | filter: ["{{defaultFilter}}", "{{hostname}}", "{{instance}}"], 57 | type: providerTypes.optional, 58 | order: 200 59 | }, 60 | hostnameDeployment: { 61 | name: "{{hostname}}-{{deployment}}", 62 | filter: ["{{defaultFilter}}", "{{hostname}}"], 63 | type: providerTypes.optional, 64 | order: 220 65 | }, 66 | hostnameDeploymentInstance: { 67 | name: "{{hostname}}-{{deployment}}-{{instance}}", 68 | filter: ["{{defaultFilter}}", "{{hostname}}", "{{instance}}"], 69 | type: providerTypes.optional, 70 | order: 240 71 | }, 72 | fullHostname: { 73 | name: "{{fullHostname}}", 74 | filter: ["{{defaultFilter}}", "{{fullHostname}}"], 75 | type: providerTypes.optional, 76 | order: 260 77 | }, 78 | fullHostnameInstance: { 79 | name: "{{fullHostname}}-{{instance}}", 80 | filter: ["{{defaultFilter}}", "{{fullHostname}}", "{{instance}}"], 81 | type: providerTypes.warn, 82 | order: 280 83 | }, 84 | fullHostnameDeployment: { 85 | name: "{{fullHostname}}-{{deployment}}", 86 | filter: ["{{defaultFilter}}", "{{fullHostname}}"], 87 | type: providerTypes.optional, 88 | order: 300 89 | }, 90 | fullHostnameDeploymentInstance: { 91 | name: "{{fullHostname}}-{{deployment}}-{{instance}}", 92 | filter: ["{{defaultFilter}}", "{{fullHostname}}", "{{instance}}"], 93 | type: providerTypes.warn, 94 | order: 320 95 | }, 96 | local: { 97 | name: "local", 98 | filter: ["{{defaultFilter}}"], 99 | type: providerTypes.optional, 100 | order: 340 101 | }, 102 | localInstance: { 103 | name: "local-{{instance}}", 104 | filter: ["{{defaultFilter}}", "{{instance}}"], 105 | type: providerTypes.warn, 106 | order: 360 107 | }, 108 | localDeployment: { 109 | name: "local-{{deployment}}", 110 | filter: ["{{defaultFilter}}"], 111 | type: providerTypes.optional, 112 | order: 380 113 | }, 114 | localDeploymentInstance: { 115 | name: "local-{{deployment}}-{{instance}}", 116 | filter: ["{{defaultFilter}}", "{{instance}}"], 117 | type: providerTypes.warn, 118 | order: 400 119 | }, 120 | env: { 121 | filter: ["{{defaultFilter}}"], 122 | type: providerTypes.disabled, 123 | handler: function () { 124 | const env = {}; 125 | Object.keys(process.env).forEach((k) => { 126 | env[k] = process.env[k]; 127 | }); 128 | 129 | return { env }; 130 | }, 131 | order: envOrder 132 | }, 133 | confippetEnv: { 134 | filter: ["{{defaultFilter}}"], 135 | type: providerTypes.required, 136 | handler: function () { 137 | const data = {}; 138 | Object.keys(process.env).forEach((k) => { 139 | if (k === "NODE_CONFIG" || k.startsWith("CONFIPPET")) { 140 | util.uMerge(data, JSON.parse(process.env[k])); 141 | } 142 | }); 143 | 144 | return data; 145 | }, 146 | order: confippetEnvOrder 147 | } 148 | }, 149 | context: { 150 | deployment: "development", 151 | instance: "", 152 | hostname: "", 153 | fullHostname: "", 154 | defaultType: providerTypes.required, 155 | defaultFilter: "enabled" // NOTE: set this to "" or undefined to disable all default providers 156 | } 157 | }; 158 | } 159 | 160 | export = defaultOpts; 161 | -------------------------------------------------------------------------------- /src/ext-handlers.ts: -------------------------------------------------------------------------------- 1 | import * as jsYaml from "js-yaml"; 2 | import * as fs from "fs"; 3 | 4 | /** 5 | * Load a config partial from a JS module 6 | * 7 | * @param fullFilePath full path to the the file 8 | * 9 | * @returns If it's an ES6 module and has `default`, then `default` is returned, 10 | * else returns the module as config partial. 11 | */ 12 | function loadJs(fullFilePath: string): unknown { 13 | const configMod = require(fullFilePath); 14 | 15 | if (configMod.__esModule) { 16 | if (configMod.hasOwnProperty("default")) { 17 | return configMod.default; 18 | } else { 19 | // will be using the whole module as config partial, so hide the ES module flag 20 | try { 21 | Object.defineProperty(configMod, "__esModule", { enumerable: false }); 22 | } catch { 23 | // oh well, can't hide it 24 | } 25 | } 26 | } 27 | 28 | return configMod; 29 | } 30 | 31 | export = { 32 | json: loadJs, 33 | js: loadJs, 34 | yaml: (fullDirPath: string) => jsYaml.load(fs.readFileSync(fullDirPath, "utf8")), 35 | ts: loadJs, 36 | }; 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | import processConfig from "./process-config"; 4 | import composeConfig from "./compose-config"; 5 | import presetConfig from "./preset-config"; 6 | import store from "./store"; 7 | import providerTypes from "./provider-types"; 8 | import util from "./util"; 9 | import extHandlers from "./ext-handlers"; 10 | import { loadConfig } from "./load-config"; 11 | 12 | const confippet = { 13 | processConfig: processConfig, 14 | composeConfig: composeConfig, 15 | presetConfig: presetConfig, 16 | store: store, 17 | providerTypes: providerTypes, 18 | extHandlers: extHandlers, 19 | util: util, 20 | loadConfig: loadConfig, 21 | }; 22 | 23 | Object.defineProperty(confippet, "config", { 24 | get: () => require("../config"), 25 | }); 26 | 27 | export = confippet; 28 | -------------------------------------------------------------------------------- /src/load-config.ts: -------------------------------------------------------------------------------- 1 | import store from "./store"; 2 | import * as Path from "path"; 3 | import presetConfig from "./preset-config"; 4 | 5 | const configs = {}; 6 | 7 | /** 8 | * handler for a config file of a specific extension 9 | * 10 | * @param _fullFilePath full path to the config file 11 | * @returns the config object loaded from the file 12 | */ 13 | export type ExtHandler = (_fullFilePath: string) => unknown; // eslint-disable-line 14 | 15 | /** 16 | * Type of a config partial provider: 17 | * 18 | * - `required` - config partial file must exist for the provider 19 | * - `disabled` - do not run the provider 20 | * - `optional` - config partial file doesn't need to exist for the provider 21 | * - `warn` - log a warning if partial file doesn't exist for the provider 22 | */ 23 | export type ProviderTypes = "required" | "disabled" | "optional" | "warn"; 24 | 25 | /** 26 | * Context of environment values to determine which config partial to load from a config directory 27 | */ 28 | export type LoadContext = { 29 | /** deployment env. Take from env `NODE_ENV` if not specified. **Default**: "development" */ 30 | deployment?: string; 31 | /** process instance. Take from env `NODE_APP_INSTANCE` if not specified. **Default**: `""` */ 32 | instance?: string; 33 | 34 | /** 35 | * Provider type for the internal `default` and `deployment` config provider. 36 | * **Default** `"required"` - which means user must have a `default.js` and a file for the NODE_ENV 37 | * deployment, such as `development.js` or `production.js` 38 | */ 39 | defaultType?: ProviderTypes; 40 | 41 | /** 42 | * default filter for providers - default filtering rule for internal providers. 43 | * 44 | * - **Default** `"enabled"` - all internal providers are enabled 45 | * - Set this to `""` and all default providers are disabled and you must set your own providers 46 | * to load config partials 47 | * 48 | */ 49 | defaultFilter?: "enabled" | ""; 50 | /** short hostname */ 51 | hostname?: string; 52 | /** full FQDN hostname */ 53 | fullHostname?: string; 54 | }; 55 | 56 | /** 57 | * Options for loading and composing config partial files from a directory 58 | * 59 | * Sample: 60 | * 61 | * ```ts 62 | * import { loadConfig } from "electrode-confippet"; 63 | * 64 | * const config = loadConfig({ 65 | * dir: Path.join(__dirname, "config") 66 | * }) 67 | * ``` 68 | */ 69 | export type LoadOptions = { 70 | /** 71 | * The directory to load config files from. Will use `Path.resolve` to generate 72 | * absolute full path for each one. 73 | */ 74 | dir: string; 75 | 76 | /** 77 | * Customize config file extensions search. 78 | * 79 | * **Default**: `["json", "yaml", "js", "ts"] 80 | */ 81 | extSearch?: string[]; 82 | 83 | /** 84 | * Custom handlers for a file extension 85 | * 86 | * **For Example**: 87 | * 88 | * ```js 89 | * { 90 | * yaml: (fullF) => jsYaml.load(fs.readFileSync(fullF, "utf8")) 91 | * } 92 | * ``` 93 | */ 94 | extHandlers?: Record; 95 | 96 | /** Throw error if a partial provider requires config file but it's missing */ 97 | failMissing?: boolean; 98 | 99 | /** `console.error` a warning if a partial provider requires config file but it's missing */ 100 | warnMissing?: boolean; 101 | 102 | /** Enable `console.log` noises about what's happening */ 103 | verbose?: boolean; 104 | 105 | /** set to `false` to skip cache, load and return a new copy, **Default**: `true` */ 106 | cache?: boolean; 107 | 108 | /** 109 | * Environment setting context 110 | * 111 | * Not for typical use cases. The context generally come from 112 | * env variables: `NODE_ENV` and `NODE_APP_INSTANCE` 113 | */ 114 | context?: LoadContext; 115 | }; 116 | 117 | const CACHED = Symbol("confippet loadConfig cached config"); 118 | 119 | /** 120 | * Load config files from a directory. 121 | * 122 | * - the directory can contain multiple files, each has a partial config. this function 123 | * will load the files according to various environment settings and compose them 124 | * into a single one. 125 | * 126 | * - Each config file in the directory can be JSON, JS, Yaml, or TS. For JS or TS, if it's 127 | * ES6 Module and has `default` export, then it's used as config partial, else the whole 128 | * module will be used. 129 | * 130 | * - The result is cached using the full directory path 131 | * 132 | * @param options LoadOptions for loading config files 133 | * @param defaults default config values 134 | * @param refresh refresh the cached copy - **NOTE** this will clear any values user set in the config 135 | * 136 | * @returns returns config 137 | */ 138 | export const loadConfig = (options: LoadOptions, defaults?: unknown, refresh?: boolean) => { 139 | const cache = options.cache !== false; 140 | 141 | const cacheKey = Path.resolve(options.dir); 142 | 143 | const config = (cache && configs[cacheKey]) || store(); 144 | 145 | if (!cache || !config[CACHED] || refresh) { 146 | config._$.reset(); 147 | config._$.defaults(defaults); 148 | presetConfig.load(config, { ...options }); 149 | if (cache) { 150 | config[CACHED] = true; 151 | configs[cacheKey] = config; 152 | } 153 | } 154 | 155 | return config; 156 | }; 157 | -------------------------------------------------------------------------------- /src/preset-config.ts: -------------------------------------------------------------------------------- 1 | import util from "./util"; 2 | 3 | function load(config, options) { 4 | options = options || {}; 5 | 6 | const dirs = []; 7 | 8 | for (let i = 0, dir = ""; (dir = process.env[`NODE_CONFIG_DIR_${i}`]); ++i) { 9 | dirs.push(dir); 10 | } 11 | 12 | if (dirs.length > 0 && process.env.NODE_CONFIG_DIR) { 13 | dirs.push(process.env.NODE_CONFIG_DIR); 14 | } 15 | 16 | if (dirs.length > 0) { 17 | console.log("config dirs", dirs); // eslint-disable-line 18 | } 19 | 20 | util.merge(options, { 21 | dirs: dirs.length > 0 && dirs, 22 | dir: process.env.NODE_CONFIG_DIR, 23 | context: { 24 | deployment: process.env.NODE_ENV, 25 | instance: process.env.NODE_APP_INSTANCE 26 | } 27 | }); 28 | 29 | config._$.compose(options); 30 | 31 | if (!process.env.AUTO_LOAD_CONFIG_PROCESS_OFF) { 32 | config._$.process(); 33 | } 34 | } 35 | 36 | function autoLoad(config, options) { 37 | if (!process.env.AUTO_LOAD_CONFIG_OFF) { 38 | load(config, options); 39 | } 40 | } 41 | 42 | export = { 43 | load, 44 | autoLoad 45 | }; 46 | -------------------------------------------------------------------------------- /src/process-config.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import * as fs from "fs"; 3 | 4 | function processObj(obj, data) { 5 | const depthPath = data.depth.join("."); 6 | 7 | _.each(obj, (v, k) => { 8 | if (_.isObjectLike(v)) { 9 | data.depth.push(k); 10 | processObj(v, data); 11 | data.depth.pop(); 12 | return; 13 | } 14 | 15 | const resolve = (tmpl) => { 16 | const refs = tmpl.split(":"); 17 | const path = refs[0]; 18 | 19 | if (path.startsWith("-")) { 20 | return path.substr(1); 21 | } 22 | 23 | const x = _.get(data.context, path); 24 | 25 | if (_.isFunction(x)) { 26 | return x({ 27 | context: data.context, 28 | config: data.config, 29 | obj, 30 | key: k, 31 | value: v, 32 | tmpl, 33 | params: _.drop(refs), 34 | depthPath 35 | }); 36 | } else if (_.isUndefined(x)) { 37 | data.missing.push({ path: `${depthPath}.${k}`, value: v, tmpl }); 38 | return ""; 39 | } else { 40 | const extras = _(refs).drop().map(resolve).value().join(""); 41 | return `${x}${extras}`; 42 | } 43 | }; 44 | 45 | if (_.isString(v) && _.includes(v, "{{")) { 46 | obj[k] = v.replace(/\{\{([^}]+)}}/g, (match, tmpl) => { 47 | const newV = resolve(tmpl); 48 | data.more += _.includes(newV, "{{") ? 1 : 0; 49 | return newV; 50 | }); 51 | } 52 | }); 53 | } 54 | 55 | function processConfig(config, options) { 56 | if (_.isEmpty(config)) { 57 | return []; 58 | } 59 | 60 | options = options || {}; 61 | 62 | const context = { 63 | config, 64 | process, 65 | argv: process.argv, 66 | cwd: process.cwd(), 67 | env: process.env, 68 | now: Date.now, 69 | readFile: (data) => { 70 | if (data.params[0]) { 71 | const enc = data.params[1] || "utf8"; 72 | return fs.readFileSync(data.params[0].trim()).toString(enc.trim()); 73 | } 74 | throw new Error("config file readFile template missing filename"); 75 | }, 76 | 77 | getEnv: (data) => { 78 | if (data.params[0]) { 79 | let value = process.env[data.params[0]]; 80 | if (value) { 81 | const cc = data.params[1]; 82 | if (cc === "lowerCase" || cc === "LC") { 83 | value = value.toLowerCase(); 84 | } else if (cc === "upperCase" || cc === "UC") { 85 | value = value.toUpperCase(); 86 | } 87 | } 88 | return value; 89 | } 90 | return undefined; 91 | } 92 | }; 93 | 94 | _.defaults(context, options.context); 95 | 96 | const data = { config, context, options, more: 1, missing: [], depth: ["config"] }; 97 | const maxRun = 20; 98 | 99 | for (let i = 0; data.more > 0; i++) { 100 | if (i >= maxRun) { 101 | throw new Error(`Unable to process config after ${maxRun} passes.`); 102 | } 103 | data.more = 0; 104 | processObj(config, data); 105 | } 106 | 107 | return data.missing; 108 | } 109 | 110 | export = processConfig; 111 | -------------------------------------------------------------------------------- /src/provider-types.ts: -------------------------------------------------------------------------------- 1 | export = { 2 | required: "required", 3 | disabled: "disabled", 4 | optional: "optional", 5 | warn: "warn" 6 | }; 7 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import processConfig from "./process-config"; 3 | import composeConfig from "./compose-config"; 4 | import util from "./util"; 5 | 6 | function hideProperties(obj, props) { 7 | props.forEach((prop) => { 8 | Object.defineProperty(obj, prop, { 9 | enumerable: false, 10 | writable: false, 11 | configurable: false 12 | }); 13 | }); 14 | } 15 | 16 | class Config { 17 | store: any; 18 | constructor(store) { 19 | this.store = store; 20 | } 21 | 22 | use(data) { 23 | util.merge(this.store, data); 24 | } 25 | 26 | defaults(data) { 27 | _.defaultsDeep(this.store, _.clone(data)); 28 | } 29 | 30 | compose(info) { 31 | util.merge(this.store, composeConfig(info)); 32 | } 33 | 34 | process(options) { 35 | processConfig(this.store, options); 36 | } 37 | 38 | reset() { 39 | const keys = Object.keys(this.store); 40 | keys.forEach((k) => { 41 | delete this.store[k]; 42 | }); 43 | } 44 | } 45 | 46 | function $get(p) { 47 | return _.get(this, p); // eslint-disable-line 48 | } 49 | 50 | function factory() { 51 | const store: any = {}; 52 | 53 | store.$ = $get; 54 | 55 | store._$ = new Config(store); 56 | 57 | hideProperties(store, ["$", "_$"]); 58 | 59 | return store; 60 | } 61 | 62 | export = factory; 63 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | 3 | const util = { 4 | replaceArray: (a, b) => (_.isArray(b) && b) || undefined, 5 | merge: function (...args) { 6 | Array.prototype.push.call(args, util.replaceArray); 7 | return _.mergeWith.apply(_, args); // eslint-disable-line 8 | }, 9 | // 10 | // if both are arrays and key starts with "+" then union the arrays 11 | // 12 | unionArray: (a, b, k) => { 13 | if (_.isArray(b)) { 14 | if (k.startsWith("+") && _.isArray(a)) { 15 | return _.union(a, b); 16 | } 17 | return b; 18 | } 19 | return undefined; 20 | }, 21 | 22 | uMerge: function (...args) { 23 | Array.prototype.push.call(args, util.unionArray); 24 | return _.mergeWith.apply(_, args); // eslint-disable-line 25 | } 26 | }; 27 | 28 | export = util; 29 | -------------------------------------------------------------------------------- /store.md: -------------------------------------------------------------------------------- 1 | # Config Store 2 | 3 | Confippet's config is store in an object that contains two special hidden properties `$` and `_$`. 4 | 5 | Example to create a new store: 6 | 7 | ```js 8 | const Confippet = require("electrode-confippet"); 9 | 10 | const store = Confippet.store(); // creates a new store 11 | ``` 12 | 13 | The preset config is a store object: 14 | 15 | ```js 16 | const config = require("electrode-confippet").config; 17 | 18 | const url = config.$("service.url"); 19 | ``` 20 | 21 | ## `$` 22 | 23 | `$` is a function that allows you to retrieve config with a string or array path. 24 | 25 | For example, instead of doing: 26 | 27 | ```js 28 | const url = config.service && config.service.url; 29 | ``` 30 | 31 | You can do: 32 | 33 | ```js 34 | const url = config.$("service.url"); 35 | ``` 36 | 37 | ## `_$` 38 | 39 | `_$` is an object with these methods. 40 | 41 | ### `_$.use(data)` 42 | 43 | Directly override existing config with `data`. 44 | 45 | Example: 46 | 47 | ```js 48 | config._$.use({ url: "http://localhost"}); 49 | ``` 50 | 51 | Will change `config.url` to `http://localhost`. 52 | 53 | ### `_$.defaults(data)` 54 | 55 | Directly use `data` as defaults for the config. 56 | 57 | Example: 58 | 59 | ```js 60 | config._$.defaults({ url: "http://localhost" }); 61 | ``` 62 | 63 | Will set `config.url` to `http://localhost` if it's `undefined`. 64 | 65 | ### `_$.compose(info)` 66 | 67 | Load and add config using [composeConfig]. 68 | 69 | Example: 70 | 71 | ```js 72 | config._$.compose(composeOptions); 73 | ``` 74 | 75 | ### `_$.reset()` 76 | 77 | Set config to `{}` 78 | 79 | [composeConfig]: ./compose.md 80 | -------------------------------------------------------------------------------- /templates.md: -------------------------------------------------------------------------------- 1 | # Template Processing 2 | 3 | Confippet has a simple processor to allow you to use templates in your config values. 4 | 5 | If you are using the preset config only, then it automatically does this. You can see the [template context](#context) for what's available. 6 | 7 | ### Usage 8 | 9 | ```js 10 | const Confippet = require("electrode-confippet"); 11 | const data = Confippet.compose({ dir: "config" }); 12 | require("electrode-server")(Confippet.processConfig(data)); 13 | ``` 14 | 15 | ### Example Config 16 | 17 | ```js 18 | { 19 | x: "{{config.y}}", 20 | n0: "{{argv.0}}", 21 | n1: "{{argv.1}}", 22 | y: "{{config.testFile}}", 23 | p: "{{process.cwd}}", 24 | testFile: "{{process.cwd}}/test/data-{{env.NODE_APP_INSTANCE}}.txt", 25 | acwd: "{{cwd}}", 26 | now: "{{now}}", 27 | bad: "{{bad}}", 28 | badConf: "{{config.bad1.bad2}}", 29 | badN1: { 30 | badN2: { 31 | badN3: "{{config.badx.bady}}" 32 | } 33 | }, 34 | ui: { 35 | env: "{{env.NODE_ENV}}" 36 | } 37 | } 38 | ``` 39 | 40 | After template processing, this could become something similar to the following one: 41 | 42 | ```js 43 | { 44 | "x": "/Users/xchen11/dev/electrode-confippet/test/data-5.txt", 45 | "n0": "/usr/local/nvm/versions/node/v4.2.4/bin/node", 46 | "n1": "/Users/xchen11/dev/electrode-confippet/node_modules/mocha/bin/_mocha", 47 | "y": "/Users/xchen11/dev/electrode-confippet/test/data-5.txt", 48 | "p": "/Users/xchen11/dev/electrode-confippet", 49 | "testFile": "/Users/xchen11/dev/electrode-confippet/test/data-5.txt", 50 | "acwd": "/Users/xchen11/dev/electrode-confippet", 51 | "now": "1454105798037", 52 | "bad": "", 53 | "badConf": "", 54 | "badN1": { 55 | "badN2": { 56 | "badN3": "" 57 | } 58 | }, 59 | "ui": { 60 | "env": "development" 61 | } 62 | } 63 | ``` 64 | 65 | And you get back an array of missing values from the processor: 66 | 67 | ```js 68 | [ 69 | { 70 | "path": "config.bad", 71 | "value": "{{bad}}", 72 | "tmpl": "bad" 73 | }, 74 | { 75 | "path": "config.badConf", 76 | "value": "{{config.bad1.bad2}}", 77 | "tmpl": "config.bad1.bad2" 78 | }, 79 | { 80 | "path": "config.badN1.badN2.badN3", 81 | "value": "{{config.badx.bady}}", 82 | "tmpl": "config.badx.bady" 83 | } 84 | ] 85 | ``` 86 | 87 | ### The Template 88 | 89 | The template is in the form of `{{ref1:-some text:ref2:refN}}`. 90 | 91 | * Each `ref` is used as a path to retrieve a value from the context. 92 | 93 | * If `ref` starts with `-` then it's used as a literal string. 94 | 95 | * If the first `ref` (`ref1`) resolves to a function, then the remaining `ref`s are treated as literal strings and passed as params in an array to the function. See [Function Ref](#function-ref). 96 | 97 | > While you can have multiple `ref`s in a single `{{}}`, the recommended form for multiple `ref`s however is `{{ref1}}some text{{ref2}}{{refN}}` 98 | 99 |   100 | > Please note that the template processing is just using the straight string replace. It's very simple and dumb. It will run multiple passes to do the replacement, but nothing fancy. 101 | 102 | ### Context 103 | 104 | The context contains these object you can use: 105 | 106 | - `config` - refer back to your own config. ie: `{{config.someConfig.config1}}` 107 | - `process` - refer to Node global `process` 108 | - `argv` - refer to Node `process.argv`. ie: `{{argv.0}}` 109 | - `cwd` - refer to Node `process.cwd`. ie: `{{cwd}}` 110 | - `env` - refer to Node `process.env`. ie: `{{env.NODE_ENV}}` 111 | - `now` - refer to `Date.now`. ie: `{{now}}` 112 | - `readFile` - function to read a file. Usage is `{{readFile:filename:encoding}}`. ie: `{{readFile:data/foo.txt:utf8}}` 113 | - `getEnv` - function to retrieve ENV variable, and allow you to convert the value to lowercase or uppercase. 114 | - Usage is `{{getEnv:ENV_VAR_NAME}}` or `{{getEnv:ENV_VAR_NAME:lowerCase}}` or `{{getEnv:ENV_VAR_NAME:upperCase}}` 115 | 116 | ### Function Ref 117 | 118 | If the first `ref` in a template resolves to a function, then it will be called with an object containing the following parameters, with the remaining `ref`s stored in `params`. 119 | 120 | `{ context, config, obj, key, value, tmpl, params, depthPath }` 121 | 122 | Where: 123 | 124 | - `context` - the context 125 | - `config` - the config 126 | - `obj` - current sub object in config being processed, could be `config` itself 127 | - `key` - current key in `obj` being processed 128 | - `value` - value for `key` 129 | - `tmpl` - template extracted from the config value 130 | - `params` - the remaining `:` separated `ref`s as an array. 131 | - `depthPath` - current depth path in config. ie: `config.sub1.sub2` 132 | 133 | For example: 134 | 135 | This template: 136 | 137 | ``` 138 | {{readFile:data/foo.txt:utf8}} 139 | ``` 140 | 141 | Will trigger this function call: 142 | 143 | ```js 144 | context.readFile({ 145 | context, config, obj, key, value, tmpl, 146 | params: [ "data/foo.txt", "utf8" ], depthPath 147 | }); 148 | ``` 149 | 150 | ## APIs 151 | 152 | ### [processConfig](#processconfig) 153 | 154 | `processConfig(config, options)` 155 | 156 | Process your config. 157 | 158 | #### Parameters 159 | 160 | - `config` - your config to be processed 161 | - `options` - options 162 | - `options.context` - additional context you want to add. 163 | 164 | For example, you can pass in the following options. 165 | 166 | ```js 167 | { 168 | context: { 169 | test: (data) => "test", 170 | custom: { 171 | url: "http://test" 172 | } 173 | } 174 | } 175 | ``` 176 | 177 | With that, you can refer to them in your config like this: 178 | 179 | ```js 180 | { 181 | test: "{{test}}", 182 | url: "{{custom.url}}" 183 | } 184 | ``` 185 | 186 | #### Returns 187 | 188 | An array of config items for which its template resolved to `undefined`. An empty array `[]` is returned if everything resolved. 189 | 190 | ie: 191 | 192 | ```js 193 | [ 194 | { 195 | path: "config.badConf.badKey", 196 | value: "{{config.nonExistingConfig}}", 197 | tmpl: "config.nonExistingConfig" 198 | } 199 | ] 200 | ``` 201 | -------------------------------------------------------------------------------- /test/composed-result.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = () => { 4 | return { 5 | json: "json", 6 | yaml: "yaml", 7 | js: "js", 8 | instance0: "yaml", 9 | deployment: "dev", 10 | arr: ["js", 1, { b: 50 }], 11 | "+uArray": ["a", "b", "c", "x", "1"], 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /test/config/default-0.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | instance0: "yaml" 3 | -------------------------------------------------------------------------------- /test/config/default.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | "use strict"; 4 | 5 | module.exports = { 6 | js: "js", 7 | arr: ["js", 1, { b: 50 }], 8 | "+uArray": ["a", "b", "c", "1"], 9 | }; 10 | -------------------------------------------------------------------------------- /test/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "json": "json", 3 | "yaml": "json", 4 | "js": "json", 5 | "+uArray": ["a", "b", "c"] 6 | } 7 | -------------------------------------------------------------------------------- /test/config/default.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | yaml: "yaml" 3 | js: "yaml" 4 | +uArray: [ "a", "b", "c", "x" ] -------------------------------------------------------------------------------- /test/config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "deployment": "dev" 3 | } 4 | -------------------------------------------------------------------------------- /test/config/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | deployment: "prod", 3 | arr: ["prod", 1, "2"] 4 | }; 5 | -------------------------------------------------------------------------------- /test/config1/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "js": "1", 3 | "default1": "json" 4 | } 5 | -------------------------------------------------------------------------------- /test/config1/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "deployment": "1", 3 | "deployment1": "production" 4 | } 5 | -------------------------------------------------------------------------------- /test/data/foo.txt: -------------------------------------------------------------------------------- 1 | hello, world 2 | -------------------------------------------------------------------------------- /test/esm-config/default.js: -------------------------------------------------------------------------------- 1 | exports.__esModule = true; 2 | 3 | exports.default = { 4 | esm: true, 5 | }; 6 | -------------------------------------------------------------------------------- /test/esm-config/test.js: -------------------------------------------------------------------------------- 1 | exports.__esModule = true; 2 | 3 | exports.test = "test"; 4 | -------------------------------------------------------------------------------- /test/spec/compose-config.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const Path = require("path"); 4 | const providerTypes = require("../../lib/provider-types"); 5 | const intercept = require("intercept-stdout"); 6 | const Confippet = require("../.."); 7 | const composedResult = require("../composed-result"); 8 | const util = require("../../lib/util"); 9 | 10 | describe("confippet composeConfig", function () { 11 | it("should compose config from directory", function () { 12 | let logs = 0; 13 | const unhookIntercept = intercept(function (txt) { 14 | if (txt.startsWith("Confippet.compose:")) { 15 | logs++; 16 | } 17 | }); 18 | 19 | const result = util.merge( 20 | { 21 | foo: { 22 | bar: "env", 23 | }, 24 | node: { 25 | config: "xyz", 26 | }, 27 | }, 28 | composedResult() 29 | ); 30 | 31 | process.env.CONFIPPET_0 = JSON.stringify({ 32 | foo: { bar: "env" }, 33 | }); 34 | process.env.NODE_CONFIG = JSON.stringify({ 35 | node: { config: "xyz" }, 36 | }); 37 | const data = Confippet.composeConfig({ 38 | dir: Path.join(__dirname, "../config"), 39 | verbose: true, 40 | providers: { 41 | env: { 42 | type: providerTypes.required, 43 | }, 44 | }, 45 | context: { 46 | instance: "0", 47 | }, 48 | }); 49 | unhookIntercept(); 50 | const env = data.env; 51 | delete data.env; 52 | expect(data).to.deep.equal(result); 53 | expect(env).to.deep.equal(process.env); 54 | expect(logs).to.be.above(0); 55 | delete process.env.CONFIPPET_0; 56 | delete process.env.NODE_CONFIG; 57 | }); 58 | 59 | it("should compose extensions according to extSearch", function () { 60 | const data = Confippet.composeConfig({ 61 | dir: Path.join(__dirname, "../config"), 62 | extSearch: ["js", "json", "yaml"], 63 | context: { 64 | instance: "0", 65 | }, 66 | }); 67 | expect(data.js).to.equal("yaml"); 68 | }); 69 | 70 | it("should not fail required when requested not to", function () { 71 | process.env.CONFIPPET_0 = JSON.stringify({ 72 | foo: { bar: "env" }, 73 | }); 74 | const data = Confippet.composeConfig({ 75 | dir: Path.join(__dirname, "../data"), 76 | failMissing: false, 77 | }); 78 | expect(data.foo.bar).to.equal("env"); 79 | delete process.env.CONFIPPET_0; 80 | }); 81 | 82 | const testWarn = (warnMissing) => { 83 | let warn = 0; 84 | const unhookIntercept = intercept(function (txt) { 85 | if (txt.startsWith("WARNING:")) { 86 | warn++; 87 | } 88 | }); 89 | Confippet.composeConfig({ 90 | dir: Path.join(__dirname, "../config"), 91 | warnMissing, 92 | context: { 93 | instance: "0", 94 | }, 95 | }); 96 | if (warnMissing) { 97 | expect(warn).to.be.above(0); 98 | } else { 99 | expect(warn).to.equal(0); 100 | } 101 | unhookIntercept(); 102 | }; 103 | 104 | it("should warn if warn provider missing", function () { 105 | testWarn(true); 106 | }); 107 | 108 | it("should warn if warn provider missing but requested not to", function () { 109 | testWarn(false); 110 | }); 111 | 112 | it("should throw when no config can be found", function () { 113 | expect(Confippet.composeConfig).to.throw(Error); 114 | }); 115 | 116 | it("should throw when no config provided", function () { 117 | expect(() => Confippet.composeConfig({ useDefaults: false })).to.throw(Error); 118 | }); 119 | 120 | it("should throw when provider type missing", function () { 121 | expect(() => { 122 | Confippet.composeConfig({ 123 | failMissing: false, 124 | warnMissing: false, 125 | providers: { 126 | a: { 127 | handler: () => {}, 128 | }, 129 | }, 130 | }); 131 | }).to.throw(Error); 132 | }); 133 | 134 | it("should throw when providers resolve to empty", function () { 135 | expect(() => { 136 | Confippet.composeConfig({ 137 | failMissing: false, 138 | warnMissing: false, 139 | providerList: [], 140 | }); 141 | }).to.throw(Error); 142 | }); 143 | 144 | it("should turn off all default providers by flag", function () { 145 | expect(() => { 146 | Confippet.composeConfig({ 147 | failMissing: false, 148 | warnMissing: false, 149 | context: { 150 | defaultFilter: "", 151 | }, 152 | }); 153 | }).to.throw(Error); 154 | }); 155 | 156 | it("should turn all default required to optional by flag", function () { 157 | Confippet.composeConfig({ 158 | warnMissing: false, 159 | context: { 160 | defaultType: providerTypes.optional, 161 | }, 162 | }); 163 | }); 164 | 165 | it("should call provider w/o order as -1", function () { 166 | let bCalled = false; 167 | let cCalled = false; 168 | Confippet.composeConfig({ 169 | failMissing: false, 170 | warnMissing: false, 171 | providerList: ["a", "b", "c", "e"], 172 | providers: { 173 | c: { 174 | type: providerTypes.required, 175 | handler: () => { 176 | cCalled = true; 177 | }, 178 | order: "-2", 179 | }, 180 | b: { 181 | type: providerTypes.required, 182 | handler: () => { 183 | expect(cCalled).to.equal(true); 184 | bCalled = true; 185 | }, 186 | filter: "enabled", 187 | order: 0, 188 | }, 189 | a: { 190 | type: providerTypes.required, 191 | handler: () => { 192 | expect(bCalled).to.equal(false); 193 | }, 194 | }, 195 | d: { 196 | type: providerTypes.required, 197 | handler: () => { 198 | throw new Error("not expect provider d to be called"); 199 | }, 200 | }, 201 | e: { 202 | type: providerTypes.required, 203 | handler: () => { 204 | throw new Error("not expect provider e to be called"); 205 | }, 206 | filter: false, 207 | }, 208 | }, 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /test/spec/load-config.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { loadConfig } from "../../lib/load-config"; 4 | import * as Path from "path"; 5 | import { expect } from "chai"; 6 | 7 | describe("confippet loadConfig", function () { 8 | it("should compose config from directory", () => { 9 | const config: any = loadConfig( 10 | { 11 | dir: Path.join(__dirname, "../config1"), 12 | context: { 13 | deployment: "default", 14 | }, 15 | }, 16 | { foo: "bar" } 17 | ); 18 | 19 | expect(config).to.have.property("foo"); 20 | expect(config.js).to.equal("1"); 21 | }); 22 | 23 | it("should hand back cached config", () => { 24 | const config: any = loadConfig( 25 | { 26 | dir: Path.join(__dirname, "../config1"), 27 | context: { 28 | deployment: "default", 29 | }, 30 | }, 31 | { foo: "bar2" } 32 | ); 33 | 34 | const symbols = Object.getOwnPropertySymbols(config); 35 | expect(symbols.map((x) => x.toString())).includes(`Symbol(confippet loadConfig cached config)`); 36 | expect(config).to.have.property("foo"); 37 | expect(config.foo).to.equal("bar"); 38 | expect(config.js).to.equal("1"); 39 | }); 40 | 41 | it("should reload cached config", () => { 42 | const config: any = loadConfig( 43 | { 44 | dir: Path.join(__dirname, "../config1"), 45 | context: { 46 | deployment: "default", 47 | }, 48 | }, 49 | { foo: "bar2" }, 50 | true 51 | ); 52 | expect(config).to.have.property("foo"); 53 | expect(config.foo).to.equal("bar2"); 54 | }); 55 | 56 | it("should allow loading new copy w/o cache", () => { 57 | const config: any = loadConfig( 58 | { 59 | dir: Path.join(__dirname, "../config1"), 60 | context: { 61 | deployment: "default", 62 | }, 63 | cache: false, 64 | }, 65 | { foo: "bar2" } 66 | ); 67 | const symbols = Object.getOwnPropertySymbols(config); 68 | expect(symbols.map((x) => x.toString())).not.includes( 69 | `Symbol(confippet loadConfig cached config)` 70 | ); 71 | expect(config).to.have.property("foo"); 72 | expect(config.foo).to.equal("bar2"); 73 | }); 74 | 75 | it("should load config from ES6 modules", () => { 76 | const config: any = loadConfig( 77 | { 78 | dir: Path.join(__dirname, "../esm-config"), 79 | context: { 80 | deployment: "test", 81 | }, 82 | }, 83 | { foo: "bar2" } 84 | ); 85 | expect(config.esm).equal(true); 86 | expect(config.test).equal("test"); 87 | // __esModule flag should've been hidden 88 | expect(Object.keys(config)).to.not.include("__esModule"); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/spec/preset-config.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | "use strict"; 4 | 5 | const presetConfig = require("../../lib/preset-config"); 6 | const composedResult = require("../composed-result"); 7 | const _ = require("lodash"); 8 | const Confippet = require("../../"); 9 | 10 | describe("preset-config", function () { 11 | const result = composedResult(); 12 | let config; 13 | 14 | const resetEnv = () => { 15 | delete process.env.AUTO_LOAD_CONFIG_OFF; 16 | delete process.env.AUTO_LOAD_CONFIG_PROCESS_OFF; 17 | delete process.env.NODE_CONFIG_DIR; 18 | delete process.env.NODE_ENV; 19 | delete process.env.NODE_APP_INSTANCE; 20 | delete process.env.NODE_CONFIG; 21 | }; 22 | 23 | before(() => { 24 | resetEnv(); 25 | process.env.NODE_CONFIG_DIR = "test/config"; 26 | process.env.NODE_APP_INSTANCE = "0"; 27 | config = Confippet.config; 28 | expect(config).to.deep.equal(result); 29 | }); 30 | 31 | beforeEach(() => { 32 | resetEnv(); 33 | config._$.reset(); 34 | expect(config).to.deep.equal({}); 35 | }); 36 | 37 | it("should skip instance file if it's not defined", () => { 38 | process.env.NODE_CONFIG_DIR = "test/config"; 39 | presetConfig.autoLoad(config); 40 | expect(config.instance0).to.equal(undefined); 41 | }); 42 | 43 | it("should use default location if NODE_CONFIG_DIR is not defined", () => { 44 | process.env.NODE_APP_INSTANCE = "0"; 45 | process.env.NODE_CONFIG = JSON.stringify({ 46 | tx: "{{config.json}}", 47 | }); 48 | presetConfig.autoLoad(config, { dir: "test/config" }); 49 | expect(config.$("tx")).to.equal("json"); 50 | delete config.tx; 51 | expect(config).to.deep.equal(result); 52 | }); 53 | 54 | it("should not auto load if AUTO_LOAD_CONFIG_OFF is set", () => { 55 | expect(config).to.deep.equal({}); 56 | process.env.AUTO_LOAD_CONFIG_OFF = "true"; 57 | presetConfig.autoLoad(config, { dir: "test/config" }); 58 | expect(config).to.deep.equal({}); 59 | }); 60 | 61 | it("should not process if AUTO_LOAD_CONFIG_PROCESS_OFF is set", () => { 62 | expect(config).to.deep.equal({}); 63 | process.env.AUTO_LOAD_CONFIG_PROCESS_OFF = "true"; 64 | process.env.NODE_APP_INSTANCE = "0"; 65 | process.env.NODE_CONFIG = JSON.stringify({ 66 | tx: "{{config.json}}", 67 | }); 68 | presetConfig.autoLoad(config, { dir: "test/config" }); 69 | expect(config.$("tx")).to.equal("{{config.json}}"); 70 | delete config.tx; 71 | expect(config).to.deep.equal(result); 72 | }); 73 | 74 | it("should load production if set", () => { 75 | process.env.NODE_CONFIG_DIR = "test/config"; 76 | process.env.NODE_APP_INSTANCE = "0"; 77 | process.env.NODE_ENV = "production"; 78 | presetConfig.autoLoad(config); 79 | const prodResult = _.cloneDeep(result); 80 | prodResult.deployment = "prod"; 81 | prodResult.arr = ["prod", 1, "2"]; 82 | expect(config).to.deep.equal(prodResult); 83 | }); 84 | 85 | it("should load configs from multiple directories", () => { 86 | process.env.NODE_CONFIG_DIR = "test/config"; 87 | process.env.NODE_CONFIG_DIR_0 = "test/config1"; 88 | process.env.NODE_APP_INSTANCE = "0"; 89 | process.env.NODE_ENV = "production"; 90 | presetConfig.autoLoad(config); 91 | const prodResult = _.cloneDeep(result); 92 | prodResult.default1 = "json"; 93 | prodResult.deployment = "prod"; 94 | prodResult.deployment1 = "production"; 95 | prodResult.arr = ["prod", 1, "2"]; 96 | expect(config).to.deep.equal(prodResult); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/spec/process-config.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | "use strict"; 4 | 5 | const _ = require("lodash"); 6 | const Confippet = require("../.."); 7 | const fs = require("fs"); 8 | 9 | const expect = require("chai").expect; 10 | 11 | describe("processConfig", function () { 12 | it("should do nothing for empty config", () => { 13 | expect(Confippet.processConfig().length).to.equal(0); 14 | expect(Confippet.processConfig(false).length).to.equal(0); 15 | expect(Confippet.processConfig("").length).to.equal(0); 16 | expect(Confippet.processConfig({}).length).to.equal(0); 17 | expect(Confippet.processConfig([]).length).to.equal(0); 18 | expect(Confippet.processConfig(undefined).length).to.equal(0); 19 | expect(Confippet.processConfig(null).length).to.equal(0); 20 | }); 21 | 22 | it("should not process config w/o templates", () => { 23 | const config = { 24 | a: "a", 25 | d1234: true, 26 | etewe: 50, 27 | "f-qeoir": { 28 | "g~1234{{}}": { 29 | h: 100, 30 | }, 31 | }, 32 | }; 33 | 34 | const save = _.cloneDeep(config); 35 | Confippet.processConfig(config); 36 | delete config.env; 37 | delete config.argv; 38 | expect(config).to.deep.equal(save); 39 | }); 40 | 41 | it("should process all templates in config", () => { 42 | const config = { 43 | x: "{{config.y}}", 44 | n0: "{{argv.0}}", 45 | n1: "{{argv.1}}", 46 | y: "{{config.testFile}}", 47 | p: "{{process.cwd}}", 48 | testFile: "{{process.cwd}}/test/data-{{env.NODE_APP_INSTANCE}}.txt", 49 | acwd: "{{cwd}}", 50 | now: "{{now}}", 51 | bad: "{{bad}}", 52 | badConf: "{{config.bad1.bad2}}", 53 | badN1: { 54 | badN2: { 55 | badN3: "{{config.badx.bady}}", 56 | }, 57 | }, 58 | mm: { 59 | nn: { 60 | aa: [ 61 | { 62 | m: { 63 | n: { 64 | mx: "{{config.m.n.x}}", 65 | }, 66 | }, 67 | }, 68 | { 69 | m: { 70 | n: { 71 | my: "{{config.m.n.y}}", 72 | }, 73 | }, 74 | }, 75 | ], 76 | }, 77 | }, 78 | m: { 79 | n: { 80 | x: "50", 81 | y: "60", 82 | }, 83 | }, 84 | key: "{{readFile: test/data/foo.txt : ascii}}", 85 | key2: "{{readFile:test/data/foo.txt}}", 86 | crazy: "{{cwd:- now :process.cwd}}", 87 | pointless: "{{- pointless }}", 88 | lowerEnv1: "{{getEnv:foo1:lowerCase}}", 89 | lowerEnv2: "{{getEnv:foo1:LC}}", 90 | upperEnv1: "{{getEnv:foo1:upperCase}}", 91 | upperEnv2: "{{getEnv:foo1:UC}}", 92 | unchangeEnv1: "{{getEnv:foo1}}", 93 | unchangeEnv2: "{{getEnv:foo1:???}}", 94 | badEnv: "{{getEnv:bad_env}}", 95 | badEnv2: "{{getEnv}}", 96 | ui: { 97 | env: "{{env.NODE_ENV}}", 98 | }, 99 | }; 100 | 101 | process.env.NODE_APP_INSTANCE = 5; 102 | process.env.NODE_ENV = "development"; 103 | process.env.foo1 = "FooBar"; 104 | const missing = Confippet.processConfig(config); 105 | expect(config.mm.nn.aa[0].m.n.mx).to.equal("50"); 106 | expect(config.mm.nn.aa[1].m.n.my).to.equal("60"); 107 | expect(config.x).to.equal(`${process.cwd()}/test/data-5.txt`); 108 | expect(config.n0).to.equal(process.argv[0]); 109 | expect(config.n1).to.equal(process.argv[1]); 110 | expect(config.y).to.equal(config.x); 111 | expect(config.p).to.equal(process.cwd()); 112 | expect(parseInt(config.now, 10)).to.be.above(0); 113 | expect(config.bad).to.equal(""); 114 | expect(config.badConf).to.equal(""); 115 | expect(config.key).to.equal(fs.readFileSync("test/data/foo.txt").toString("ascii")); 116 | expect(config.key2).to.equal(fs.readFileSync("test/data/foo.txt").toString("utf8")); 117 | expect(config.crazy).to.equal(`${process.cwd()} now ${process.cwd()}`); 118 | expect(config.pointless).to.equal(` pointless `); 119 | expect(config.ui.env).to.equal("development"); 120 | expect(config.badEnv).to.equal("undefined"); 121 | expect(config.badEnv2).to.equal("undefined"); 122 | expect(config.lowerEnv1).to.equal("foobar"); 123 | expect(config.lowerEnv2).to.equal("foobar"); 124 | expect(config.upperEnv1).to.equal("FOOBAR"); 125 | expect(config.upperEnv2).to.equal("FOOBAR"); 126 | expect(config.unchangeEnv1).to.equal("FooBar"); 127 | expect(config.unchangeEnv2).to.equal("FooBar"); 128 | expect(missing.length).to.equal(3); 129 | expect(missing[0].path).to.equal("config.bad"); 130 | expect(missing[0].value).to.equal("{{bad}}"); 131 | expect(missing[1].path).to.equal("config.badConf"); 132 | expect(missing[1].value).to.equal("{{config.bad1.bad2}}"); 133 | expect(missing[2].path).to.equal("config.badN1.badN2.badN3"); 134 | expect(missing[2].value).to.equal("{{config.badx.bady}}"); 135 | }); 136 | 137 | it("should throw error if readFile missing filename", () => { 138 | const config = { 139 | key: "{{readFile}}", 140 | }; 141 | expect(() => Confippet.processConfig(config)).to.throw(); 142 | }); 143 | 144 | it("should throw error if readFile missing file", () => { 145 | const config = { 146 | key: "{{readFile:missing_file.txt}}", 147 | }; 148 | expect(() => Confippet.processConfig(config)).to.throw(); 149 | }); 150 | 151 | it("should throw error for circular templates", () => { 152 | const config = { 153 | x: "{{config.x}}", 154 | }; 155 | 156 | expect(() => Confippet.processConfig(config)).to.throw(); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /test/spec/store.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | "use strict"; 4 | 5 | const Confippet = require("../.."); 6 | 7 | describe("store", function () { 8 | it("should create a config store", () => { 9 | const store = Confippet.store(); 10 | store._$.use({ 11 | foo: { 12 | x: { 13 | val: ["bar", "blah", "hello"], 14 | }, 15 | }, 16 | }); 17 | 18 | store._$.defaults({ 19 | d: { 20 | val: [ 21 | { 22 | foo: { 23 | x: 10, 24 | }, 25 | }, 26 | { 27 | hello: "world", 28 | }, 29 | ], 30 | }, 31 | foo: { 32 | x: "oops", 33 | bar: "hello", 34 | }, 35 | }); 36 | 37 | expect(store.$("foo.x.val.0")).to.equal("bar"); 38 | expect(store.$("foo.x.val.1")).to.equal("blah"); 39 | expect(store.$("foo.x.val.2")).to.equal("hello"); 40 | expect(store.$([])).to.deep.equal(undefined); 41 | expect(store.$("")).to.equal(undefined); 42 | expect(store.$(true)).to.equal(undefined); 43 | expect(store.$(5)).to.equal(undefined); 44 | 45 | expect(store.d.val[0].foo.x).to.equal(10); 46 | expect(store.$("d.val.0.foo.x")).to.equal(10); 47 | expect(store.$("foo.bar")).to.equal("hello"); 48 | 49 | store._$.use({ 50 | foo: { 51 | x: { 52 | val: "500", 53 | }, 54 | }, 55 | p: { 56 | y: [ 57 | "{{config.foo.x.val}}", 58 | { 59 | k: "{{config.foo.bar}}", 60 | }, 61 | { 62 | n: "{{env.TEST_N}}", 63 | }, 64 | ], 65 | }, 66 | }); 67 | 68 | expect(store.$("foo.x.val")).to.equal("500"); 69 | process.env.TEST_N = "nn1"; 70 | store._$.process(); 71 | delete process.env.TEST_N; 72 | expect(store.$("p.y.0")).to.equal("500"); 73 | expect(store.$("p.y.1.k")).to.equal("hello"); 74 | expect(store.$("p.y.2.n")).to.equal("nn1"); 75 | 76 | store._$.reset(); 77 | expect(store).to.deep.equal({}); 78 | 79 | store._$.compose({ 80 | dir: "test/config", 81 | }); 82 | 83 | const result = require("../composed-result")(); 84 | delete result.instance0; 85 | 86 | expect(store).to.deep.equal(result); 87 | expect(store.$("arr[0]")).to.equal("js"); 88 | expect(store.$("arr[1]")).to.equal(1); 89 | expect(store.$("arr[2].b")).to.equal(50); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "lib": [ 5 | "es6" 6 | ], 7 | "module": "CommonJS", 8 | "esModuleInterop": true, 9 | "importHelpers": true, 10 | "target": "ES2018", 11 | "preserveConstEnums": true, 12 | "sourceMap": true, 13 | "declaration": true, 14 | "types": [ 15 | "node", 16 | "mocha", 17 | "chai", 18 | "sinon", 19 | "sinon-chai" 20 | ], 21 | "forceConsistentCasingInFileNames": true, 22 | "noImplicitReturns": true, 23 | "alwaysStrict": true, 24 | "strictFunctionTypes": true 25 | }, 26 | "include": [ 27 | "src" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /xrun-tasks.ts: -------------------------------------------------------------------------------- 1 | import { loadTasks } from "@xarc/module-dev"; 2 | loadTasks(); 3 | --------------------------------------------------------------------------------