├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── dist └── index.js ├── package.json ├── src └── index.js └── test ├── alternate-filename ├── environment.json └── index.js ├── loader.js ├── mixed ├── env.json └── index.js ├── no-file └── index.js ├── not-valid-json ├── env.json └── index.js ├── optional ├── env.json └── index.js ├── required ├── env.json └── index.js ├── spy.js └── validators ├── env.json └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | src/ 3 | test/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "5.1" 5 | - "5.0" 6 | - "4.2" 7 | - "4.1" 8 | - "4.0" 9 | - "iojs" 10 | - "0.12" 11 | - "0.11" 12 | - "0.10" 13 | env: 14 | - NODE_DEBUG=checkenv 15 | script: 16 | - npm run test 17 | after_script: 18 | - npm run coveralls -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `checkenv` - Check Your Environment 2 | 3 | [![npm version](https://badge.fury.io/js/checkenv.svg)](https://www.npmjs.com/package/checkenv) [![Build Status](https://travis-ci.org/inxilpro/node-checkenv.svg)](https://travis-ci.org/inxilpro/node-checkenv) [![Coverage Status](https://coveralls.io/repos/inxilpro/node-checkenv/badge.svg?branch=master&service=github)](https://coveralls.io/github/inxilpro/node-checkenv?branch=master) [![Dependency Status](https://david-dm.org/inxilpro/node-checkenv.svg)](https://david-dm.org/inxilpro/node-checkenv) 4 | 5 | A modern best-practice is to [store your application's configuration in environmental variables](http://12factor.net/config). This allows you to keep all config data outside of your repository, and store it in a standard, system-agnostic location. Modern build/deploy/development tools make it easier to manage these variables per-host, but they're still often undocumented, and can lead to bugs when missing. 6 | 7 | This module lets you define all the environmental variables your application relies on in an `env.json` file. It then provides a method to check for these variables at application launch, and print a help screen if any are missing. 8 | 9 | ## Installation 10 | 11 | ``` bash 12 | $ npm i -S checkenv 13 | ``` 14 | 15 | ## Usage 16 | 17 | First, define a JSON file called `env.json` in your project root (see below). Then, add the following line to the top of your project's entry file: 18 | 19 | ``` js 20 | require('checkenv').check(); 21 | ``` 22 | 23 | By default, `checkenv` will print a pretty error message and call `process.exit(1)` if any required variables are missing. It will also print an error message if optional variables are missing, but will not exit the process. 24 | 25 | ![Screen Shot](https://cloud.githubusercontent.com/assets/21592/11595855/8f5cb9d6-9a7f-11e5-9128-376f91fd6d1a.jpg) 26 | 27 | If you would like to handle errors yourself, `check` takes an optional `pretty` argument which causes it to throw errors instead of printing an error message. This will only result in an error being thrown on missing required variables. 28 | 29 | ``` js 30 | try { 31 | require('checkenv').check(false); 32 | } catch (e) { 33 | // Do something with this error 34 | } 35 | ``` 36 | 37 | ## Configuration 38 | 39 | Your JSON file should define the environmental variables as keys, and either a boolean (required) as the value, or a configuration object with any of the options below. 40 | 41 | ### JSON 42 | ``` json 43 | { 44 | "NODE_ENV": { 45 | "description": "This defines the current environment", 46 | "validators": [{ 47 | "name": "in", 48 | "options": ["development", "testing", "staging", "production"] 49 | }] 50 | }, 51 | "PORT": { 52 | "description": "This is the port the API server will run on", 53 | "default": 3000 54 | }, 55 | "NODE_PATH": true, 56 | "DEBUG": { 57 | "required": false, 58 | "description": "If set, enables additional debug messages" 59 | } 60 | } 61 | ``` 62 | 63 | ### Options 64 | 65 | #### `required` 66 | 67 | Defines whether or not this variable is required. By default, all variables are required, so you must explicitly set them to optional by setting this to `false` 68 | 69 | #### `description` 70 | 71 | Describes the variable and how it should be used. Useful for new developers setting up the project, and is printed in the error output if present. 72 | 73 | #### `default` 74 | 75 | Defines the default value to use if variable is unset. Implicitly sets `required` to `false`. 76 | 77 | #### `validators` 78 | 79 | An array of validators that the variable must pass. See [validator.js](https://github.com/chriso/validator.js) for details about all validators. Format for each validator is: 80 | 81 | ``` javascript 82 | { 83 | /* ... */ 84 | "validators": [ 85 | "validator name", // Option-less validators can be passed as strings 86 | { // Validators w/ options must be passed as objects 87 | "name": "validator name", 88 | "options": options // Option format varies, see below 89 | } 90 | ] 91 | /* ... */ 92 | } 93 | ``` 94 | 95 | Possible validators (see [validator.js](https://github.com/chriso/validator.js) for details): 96 | 97 | - `contains` — `options` should be a string with what the value should contain 98 | - `equals` — `options` should be a string of the exact value 99 | - `before` — `options` should be a date 100 | - `after` — `options` should be a date 101 | - `alpha` 102 | - `alphanumeric` 103 | - `ascii` 104 | - `base64` 105 | - `boolean` 106 | - `date` 107 | - `decimal` 108 | - `fqdn` 109 | - `float` — `options` MAY be an object with `min` or `max` properties 110 | - `hex-color` 111 | - `hexadecimal` 112 | - `ip4` — same as `ip` with `"options": 4` 113 | - `ip6` — same as `ip` with `"options": 6` 114 | - `ip` — `options` MAY be number (`4` or `6`) 115 | - `iso8601` 116 | - `enum` — alias for `in` 117 | - `in` — `options` MUST be an array of possible values 118 | - `int` — `options` MAY be an object with `min` or `max` properties 119 | - `json` 120 | - `length` — `options` MUST be an object with `min`, `max` or both 121 | - `lowercase` 122 | - `mac-address` 123 | - `numeric` 124 | - `url` 125 | - `uuid3` — same as `uuid` with `"options": 3` 126 | - `uuid4` — same as `uuid` with `"options": 4` 127 | - `uuid5` — same as `uuid` with `"options": 5` 128 | - `uuid` — `options` MAY be a number (`3`, `4` or `5`) 129 | - `uppercase` 130 | - `regex` — alias for `matches` 131 | - `regexp` — alias for `matches` 132 | - `matches` — `options` MUST be either a string representing a regex, or an array in the format `["regex", "modifiers"]` 133 | 134 | ### See Also 135 | 136 | If you like this module, you may also want to check out: 137 | 138 | - [`dotenv`](https://github.com/motdotla/dotenv) Load missing environmental variables from `.env` 139 | - [`app-root-path`](https://github.com/inxilpro/node-app-root-path) Automatically determine 140 | the root path for the current application 141 | - [`enforce-node-path`](https://github.com/inxilpro/enforce-node-path) Enforce the usage of 142 | the `NODE_PATH` environmental variable 143 | 144 | ## Change Log 145 | 146 | ### 1.2.2 147 | - Better handling of syntax errors in `env.json` (thanks yalcindo!) 148 | 149 | ### 1.2.0 150 | - Validation (via [validator.js](https://github.com/chriso/validator.js)) 151 | 152 | ### 1.1.1 153 | - Prints default value in help 154 | 155 | ### 1.1.0 156 | - Added support for default values 157 | - Added support to change filename via `setFilename()` 158 | 159 | ### 1.0.6 160 | - Bugfix — please do not use versions before 1.0.6 161 | 162 | ### 1.0.5 163 | - Passes tests for node 0.10 through 5.1 164 | 165 | ### 1.0.0 166 | - Initial release 167 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load dependencies 4 | 5 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 8 | 9 | Object.defineProperty(exports, "__esModule", { 10 | value: true 11 | }); 12 | exports.options = options; 13 | exports.setConfig = setConfig; 14 | exports.setFilename = setFilename; 15 | exports.scan = scan; 16 | exports.load = load; 17 | exports.validate = validate; 18 | exports.check = check; 19 | exports.help = help; 20 | 21 | var _path = require('path'); 22 | 23 | var _fs = require('fs'); 24 | 25 | var _fs2 = _interopRequireDefault(_fs); 26 | 27 | var _windowSize = require('window-size'); 28 | 29 | var _wrapAnsi = require('wrap-ansi'); 30 | 31 | var _wrapAnsi2 = _interopRequireDefault(_wrapAnsi); 32 | 33 | var _chalk = require('chalk'); 34 | 35 | var _validator = require('validator'); 36 | 37 | var _validator2 = _interopRequireDefault(_validator); 38 | 39 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 40 | 41 | // Cached config object 42 | var config; 43 | 44 | // Filename is configurable 45 | var filename = 'env.json'; 46 | 47 | // Default config 48 | var defaultOpts = { 49 | 'required': true, 50 | 'description': null, 51 | 'default': null, 52 | 'validators': [] 53 | }; 54 | 55 | // Debugger 56 | var debug = function debug() {}; 57 | if ('NODE_DEBUG' in process.env && /\bcheckenv\b/i.test(process.env.NODE_DEBUG)) { 58 | debug = function (message) { 59 | return console.log((0, _chalk.yellow)('DEBUG: ' + message)); 60 | }; 61 | } 62 | 63 | // Backwards-compat file exists checker 64 | function access(path) { 65 | try { 66 | debug('Looking for ' + path); 67 | if ('accessSync' in _fs2.default) { 68 | _fs2.default.accessSync(path, _fs2.default.R_OK); 69 | } else { 70 | _fs2.default.closeSync(_fs2.default.openSync(path, 'r')); 71 | } 72 | debug('Found ' + path); 73 | return true; 74 | } catch (e) { 75 | debug(e.message); 76 | return false; 77 | } 78 | } 79 | 80 | // Load options, including defaults, for a variable 81 | function options(name) { 82 | load(name); 83 | 84 | // Build opts 85 | var userOpts = 'object' === _typeof(config[name]) ? config[name] : { 'required': false !== config[name] }; 86 | return _extends({}, defaultOpts, userOpts); 87 | } 88 | 89 | function setConfig(newConfig) { 90 | config = newConfig; 91 | } 92 | 93 | function setFilename(newFilename) { 94 | filename = newFilename; 95 | } 96 | 97 | // Scans directory tree for env.json 98 | function scan() { 99 | var current; 100 | var next = (0, _path.dirname)((0, _path.resolve)(module.parent.filename)); 101 | while (next !== current) { 102 | current = next; 103 | var path = (0, _path.resolve)(current, filename); 104 | if (access(path)) { 105 | return path; 106 | } 107 | next = (0, _path.resolve)(current, '..'); 108 | } 109 | 110 | throw new Error(filename + ' not found anywhere in the current directory tree'); 111 | } 112 | 113 | // Loads config from found env.json 114 | function load(name) { 115 | if (!config) { 116 | var path = scan(); 117 | config = require(path); 118 | } 119 | 120 | if (name && !(name in config)) { 121 | throw new Error('No configuration for "' + name + '"'); 122 | } 123 | 124 | return config; 125 | } 126 | 127 | function validateOptions(name, options) { 128 | var expectedType = arguments.length <= 2 || arguments[2] === undefined ? 'object' : arguments[2]; 129 | 130 | var actualType = Array.isArray(options) ? 'array' : typeof options === 'undefined' ? 'undefined' : _typeof(options); 131 | if (expectedType !== actualType) { 132 | throw new Error('The "' + name + '" validator expects options to be passed as an ' + expectedType + ', ' + actualType + ' received instead'); 133 | } 134 | } 135 | 136 | function minMaxMessage(options) { 137 | if (!options || 'object' !== (typeof options === 'undefined' ? 'undefined' : _typeof(options))) { 138 | return ''; 139 | } 140 | 141 | if (options.min && options.max) { 142 | return ' between ' + options.min + ' and ' + options.max; 143 | } else if (options.min) { 144 | return ' greater than or equal to ' + options.min; 145 | } else if (options.max) { 146 | return ' less than or equal to ' + options.max; 147 | } 148 | return ''; 149 | } 150 | 151 | /** 152 | * @see https://github.com/chriso/validator.js 153 | */ 154 | function validate(name, value) { 155 | load(name); 156 | var opts = options(name); 157 | 158 | // Check if we have any validators 159 | if (!opts.validators) { 160 | return true; 161 | } 162 | 163 | // Force all validators into objects 164 | var validators = opts.validators.map(function (validatorConfig) { 165 | var t = typeof validatorConfig === 'undefined' ? 'undefined' : _typeof(validatorConfig); 166 | 167 | if ('string' === t) { 168 | validatorConfig = { 169 | name: validatorConfig, 170 | options: null 171 | }; 172 | } else if ('object' !== t || !('name' in validatorConfig)) { 173 | throw new Error('Invalid validatorConfig configuration: ' + JSON.stringify(validatorConfig)); 174 | } 175 | 176 | return validatorConfig; 177 | }); 178 | 179 | // Run validators a build array of errors 180 | var errors = validators.reduce(function (errors, _ref) { 181 | var name = _ref.name; 182 | var options = _ref.options; 183 | 184 | switch (name) { 185 | case 'contains': 186 | validateOptions(name, options, 'string'); 187 | if (!_validator2.default.contains(value, options)) { 188 | errors.push('Must contain the string "' + options + '"'); 189 | } 190 | break; 191 | 192 | case 'equals': 193 | validateOptions(name, options, 'string'); 194 | if (!_validator2.default.equals(value, options)) { 195 | errors.push('Must be set to "' + options + '"'); 196 | } 197 | break; 198 | 199 | case 'before': 200 | case 'after': 201 | validateOptions(name, options, 'string'); 202 | if (!_validator2.default.isDate(options)) { 203 | throw new Error('The "' + name + '" validator expects its options to be a valid date, but "' + value + '" supplied'); 204 | } 205 | 206 | if ('after' === name) { 207 | if (!_validator2.default.isAfter(value, new Date(options))) { 208 | errors.push('Must be set to a date after ' + options); 209 | } 210 | } else if ('before' === name) { 211 | if (!_validator2.default.isBefore(value, new Date(options))) { 212 | errors.push('Must be set to a date before ' + options); 213 | } 214 | } 215 | break; 216 | 217 | case 'alpha': 218 | if (!_validator2.default.isAlpha(value)) { 219 | errors.push('Must be alpha characters only (a-z)'); 220 | } 221 | break; 222 | 223 | case 'alphanumeric': 224 | if (!_validator2.default.isAlphanumeric(value)) { 225 | errors.push('Must be alphanumeric characters only'); 226 | } 227 | break; 228 | 229 | case 'ascii': 230 | if (!_validator2.default.isAscii(value)) { 231 | errors.push('Must be ASCII characters only'); 232 | } 233 | break; 234 | 235 | case 'base64': 236 | if (!_validator2.default.isBase64(value)) { 237 | errors.push('Must be a base64-encoded string'); 238 | } 239 | break; 240 | 241 | case 'boolean': 242 | if (!_validator2.default.isBoolean(value)) { 243 | errors.push('Must be a boolean (true, false, 1, or 0)'); 244 | } 245 | break; 246 | 247 | case 'date': 248 | if (!_validator2.default.isDate(value)) { 249 | errors.push('Must be a date'); 250 | } 251 | break; 252 | 253 | case 'decimal': 254 | if (!_validator2.default.isDecimal(value)) { 255 | errors.push('Must be a decimal number'); 256 | } 257 | break; 258 | 259 | case 'fqdn': 260 | if (!_validator2.default.isFQDN(value, options)) { 261 | errors.push('Must be a fully qualified domain name'); 262 | } 263 | break; 264 | 265 | case 'float': 266 | if (!_validator2.default.isFloat(value, options)) { 267 | errors.push('Must be a floating point number' + minMaxMessage(options)); 268 | } 269 | break; 270 | 271 | case 'hex-color': 272 | if (!_validator2.default.isHexColor(value)) { 273 | errors.push('Must be a HEX color'); 274 | } 275 | break; 276 | 277 | case 'hexadecimal': 278 | if (!_validator2.default.isHexadecimal(value)) { 279 | errors.push('Must be a hexadecimal number'); 280 | } 281 | break; 282 | 283 | case 'ip4': 284 | case 'ip6': 285 | case 'ip': 286 | if (!options) { 287 | var versionMatch = name.match(/(\d)$/); 288 | if (versionMatch) { 289 | options = parseInt(versionMatch[1], 10); 290 | } 291 | } 292 | if (!_validator2.default.isIP(value, options)) { 293 | errors.push('Must be an IP address' + (options ? ' (version ' + options + ')' : '')); 294 | } 295 | break; 296 | 297 | case 'iso8601': 298 | if (!_validator2.default.isISO8601(value)) { 299 | errors.push('Must be an ISO8601-formatted date'); 300 | } 301 | break; 302 | 303 | case 'enum': 304 | case 'in': 305 | validateOptions(name, options, 'array'); 306 | if (!_validator2.default.isIn(value, options)) { 307 | errors.push('Must be on of: "' + options.join('", "') + '"'); 308 | } 309 | break; 310 | 311 | case 'int': 312 | if (!_validator2.default.isInt(value, options)) { 313 | errors.push('Must be an integer' + minMaxMessage(options)); 314 | } 315 | break; 316 | 317 | case 'json': 318 | if (!_validator2.default.isJSON(value)) { 319 | errors.push('Must be JSON'); 320 | } 321 | break; 322 | 323 | case 'length': 324 | validateOptions(name, options, 'object'); 325 | if (!options.min && !options.max) { 326 | throw new Error('The "' + name + '" validator requires a "min" or a "max" option'); 327 | } 328 | 329 | var min = options.min || 0; 330 | var max = options.max || undefined; 331 | 332 | if (!_validator2.default.isLength(value, min, max)) { 333 | errors.push('Must have a character length' + minMaxMessage(options)); 334 | } 335 | break; 336 | 337 | case 'lowercase': 338 | if (!_validator2.default.isLowercase(value)) { 339 | errors.push('Must be lower case'); 340 | } 341 | break; 342 | 343 | case 'mac-address': 344 | if (!_validator2.default.isMACAddress(value)) { 345 | errors.push('Must be a MAC address'); 346 | } 347 | break; 348 | 349 | case 'numeric': 350 | if (!_validator2.default.isNumeric(value)) { 351 | errors.push('Must be numeric'); 352 | } 353 | break; 354 | 355 | case 'url': 356 | if (!_validator2.default.isURL(value, options)) { 357 | errors.push('Must be a URL'); 358 | } 359 | break; 360 | 361 | case 'uuid3': 362 | case 'uuid4': 363 | case 'uuid5': 364 | case 'uuid': 365 | if (!options) { 366 | var versionMatch = name.match(/(\d)$/); 367 | if (versionMatch) { 368 | options = parseInt(versionMatch[1], 10); 369 | } 370 | } 371 | if (!_validator2.default.isUUID(value, options)) { 372 | errors.push('Must be a UUID' + (options ? ' (version ' + options + ')' : '')); 373 | } 374 | break; 375 | 376 | case 'uppercase': 377 | if (!_validator2.default.isUppercase(value)) { 378 | errors.push('Must be upper case'); 379 | } 380 | break; 381 | 382 | case 'regex': 383 | case 'regexp': 384 | case 'matches': 385 | if ('string' === typeof options) { 386 | options = [options]; 387 | } 388 | validateOptions(name, options, 'array'); 389 | 390 | var res; 391 | if (1 === options.length) { 392 | res = _validator2.default.matches(value, options[0]); 393 | } else if (2 === options.length) { 394 | res = _validator2.default.matches(value, options[0], options[1]); 395 | } 396 | if (!res) { 397 | errors.push('Must match the regular expression /' + options[0] + '/' + options[1]); 398 | } 399 | break; 400 | } 401 | 402 | return errors; 403 | }, []); 404 | 405 | return errors; 406 | } 407 | 408 | // Run checks 409 | function check() { 410 | var pretty = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; 411 | 412 | try { 413 | load(); 414 | } catch (e) { 415 | 416 | if (false === pretty || e.toString().indexOf('SyntaxError') !== -1) { 417 | throw e; 418 | } 419 | 420 | var pkg = require('../package.json'); 421 | console.error("\n" + (0, _wrapAnsi2.default)(_chalk.bgRed.white('ERROR:') + ' Unable to load ' + (0, _chalk.blue)(filename) + '; see ' + (0, _chalk.underline)(pkg.homepage), _windowSize.width) + "\n"); 422 | process.exit(1); 423 | } 424 | 425 | var required = []; 426 | var optional = []; 427 | var validationErrors = []; 428 | 429 | for (var name in config) { 430 | debug('Checking for variable ' + name); 431 | 432 | // Load opts 433 | var opts = options(name); 434 | 435 | // Check if variable is set 436 | if (name in process.env) { 437 | debug('Found variable ' + name); 438 | var errors = validate(name, process.env[name]); 439 | if (errors.length) { 440 | if (false === pretty) { 441 | var err = new Error('Environmental variable "' + name + '" did not pass validation'); 442 | err.validationMessages = errors; 443 | throw err; 444 | } 445 | validationErrors.push({ name: name, errors: errors }); 446 | } 447 | continue; 448 | } 449 | 450 | // Check if default is set 451 | if (opts.default) { 452 | debug('Setting ' + name + ' to ' + JSON.stringify(opts.default)); 453 | process.env[name] = opts.default; 454 | optional.push(name); 455 | continue; 456 | } 457 | 458 | // Check if variable is set as optional 459 | if (false === opts.required) { 460 | debug(name + ' is optional'); 461 | optional.push(name); 462 | continue; 463 | } 464 | 465 | debug(name + ' is required and missing'); 466 | required.push(name); 467 | if (false === pretty) { 468 | throw new Error('Environmental variable "' + name + '" must be set'); // FIXME 469 | } 470 | } 471 | 472 | if (true === pretty && (required.length || validationErrors.length || optional.length)) { 473 | console.error(''); 474 | if (required.length) { 475 | header(required.length, 'required'); 476 | required.forEach(function (name) { 477 | console.error(help(name)); 478 | }); 479 | } 480 | if (validationErrors.length) { 481 | header(validationErrors.length, 'invalid'); 482 | validationErrors.forEach(function (_ref2) { 483 | var name = _ref2.name; 484 | var errors = _ref2.errors; 485 | 486 | console.error(help(name, errors)); 487 | }); 488 | } 489 | if (optional.length) { 490 | if (required.length) { 491 | console.error(''); 492 | } 493 | header(optional.length, 'missing (but optional)'); 494 | optional.forEach(function (name) { 495 | console.error(help(name)); 496 | }); 497 | } 498 | console.error(''); 499 | } 500 | 501 | debug('Required missing: ' + required.length); 502 | if (required.length || validationErrors.length) { 503 | process.exit(1); 504 | } 505 | } 506 | 507 | // Print header 508 | function header(count, adv) { 509 | var s = 1 === count ? '' : 's'; 510 | var is = 1 === count ? 'is' : 'are'; 511 | var message = ' The following ' + count + ' environmental variable' + s + ' ' + is + ' ' + adv + ': '; 512 | console.error((0, _wrapAnsi2.default)(/optional/.test(adv) ? _chalk.bgYellow.white(message) : _chalk.bgRed.black(message), _windowSize.width)); 513 | } 514 | 515 | // Get formatted help for variable 516 | function help(name, errors) { 517 | load(name); 518 | 519 | var opts = options(name); 520 | var help = (0, _chalk.blue)(name); 521 | 522 | if (opts.default) { 523 | help += (0, _chalk.yellow)(' (default=' + opts.default + ')'); 524 | } 525 | 526 | if (opts.description) { 527 | help += ' ' + opts.description; 528 | } 529 | 530 | if (errors && errors.length) { 531 | errors.forEach(function (error) { 532 | help += '\n - ' + error; 533 | }); 534 | } 535 | 536 | return (0, _wrapAnsi2.default)(help, _windowSize.width); 537 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "checkenv", 3 | "version": "1.2.2", 4 | "description": "Require certain environmental variables", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "tape test/**/*.js | tap-spec", 8 | "test-src": "TEST_SRC=1 tape test/**/*.js | tap-spec", 9 | "test-debug": "NODE_DEBUG=checkenv npm run test-src", 10 | "test-validators": "TEST_SRC=1 tape test/validators/*.js | tap-spec", 11 | "coverage": "istanbul cover tape test/**/*.js | tap-spec", 12 | "coveralls": "npm run coverage && cat ./coverage/lcov.info | coveralls", 13 | "build": "babel -d dist/ src/", 14 | "watch": "babel -w -d dist/ src/", 15 | "prepublish": "npm run build" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/inxilpro/node-checkenv.git" 20 | }, 21 | "homepage": "https://www.npmjs.com/package/checkenv", 22 | "author": "Chris Morrell", 23 | "license": "MIT", 24 | "keywords": [ 25 | "environment", 26 | "env", 27 | "variables", 28 | "NODE_ENV" 29 | ], 30 | "devDependencies": { 31 | "babel-cli": "^6.2.0", 32 | "babel-core": "^6.2.1", 33 | "babel-preset-es2015": "^6.1.18", 34 | "babel-preset-stage-2": "^6.3.13", 35 | "coveralls": "^2.11.4", 36 | "glob": "^6.0.1", 37 | "istanbul": "^0.4.1", 38 | "tap-spec": "^4.1.1", 39 | "tape": "^4.2.2" 40 | }, 41 | "dependencies": { 42 | "chalk": "^1.1.1", 43 | "validator": "^4.4.0", 44 | "window-size": "^0.1.4", 45 | "wrap-ansi": "^2.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load dependencies 4 | import { resolve, dirname, sep } from 'path'; 5 | import fs from 'fs'; 6 | import { width } from 'window-size'; 7 | import wrap from 'wrap-ansi'; 8 | import { underline, blue, yellow, bgRed, bgYellow } from 'chalk'; 9 | import validator from 'validator'; 10 | 11 | // Cached config object 12 | var config; 13 | 14 | // Filename is configurable 15 | var filename = 'env.json'; 16 | 17 | // Default config 18 | const defaultOpts = { 19 | 'required': true, 20 | 'description': null, 21 | 'default': null, 22 | 'validators': [] 23 | }; 24 | 25 | // Debugger 26 | var debug = () => {}; 27 | if ('NODE_DEBUG' in process.env && /\bcheckenv\b/i.test(process.env.NODE_DEBUG)) { 28 | debug = message => console.log(yellow(`DEBUG: ${message}`)); 29 | } 30 | 31 | // Backwards-compat file exists checker 32 | function access(path) { 33 | try { 34 | debug(`Looking for ${path}`); 35 | if ('accessSync' in fs) { 36 | fs.accessSync(path, fs.R_OK); 37 | } else { 38 | fs.closeSync(fs.openSync(path, 'r')); 39 | } 40 | debug(`Found ${path}`); 41 | return true; 42 | } catch (e) { 43 | debug(e.message); 44 | return false; 45 | } 46 | } 47 | 48 | // Load options, including defaults, for a variable 49 | export function options(name) { 50 | load(name); 51 | 52 | // Build opts 53 | const userOpts = ('object' === typeof config[name] ? config[name] : { 'required': (false !== config[name]) }); 54 | return { 55 | ...defaultOpts, 56 | ...userOpts 57 | }; 58 | } 59 | 60 | export function setConfig(newConfig) { 61 | config = newConfig; 62 | } 63 | 64 | export function setFilename(newFilename) { 65 | filename = newFilename; 66 | } 67 | 68 | // Scans directory tree for env.json 69 | export function scan() { 70 | var current; 71 | var next = dirname(resolve(module.parent.filename)); 72 | while (next !== current) { 73 | current = next; 74 | const path = resolve(current, filename); 75 | if (access(path)) { 76 | return path; 77 | } 78 | next = resolve(current, '..'); 79 | } 80 | 81 | throw new Error(`${filename} not found anywhere in the current directory tree`); 82 | } 83 | 84 | // Loads config from found env.json 85 | export function load(name) { 86 | if (!config) { 87 | const path = scan(); 88 | config = require(path); 89 | } 90 | 91 | if (name && !(name in config)) { 92 | throw new Error(`No configuration for "${name}"`); 93 | } 94 | 95 | return config; 96 | } 97 | 98 | function validateOptions(name, options, expectedType = 'object') { 99 | const actualType = (Array.isArray(options) ? 'array' : typeof options); 100 | if (expectedType !== actualType) { 101 | throw new Error(`The "${name}" validator expects options to be passed as an ${expectedType}, ${actualType} received instead`); 102 | } 103 | } 104 | 105 | function minMaxMessage(options) { 106 | if (!options || 'object' !== typeof options) { 107 | return ''; 108 | } 109 | 110 | if (options.min && options.max) { 111 | return ` between ${options.min} and ${options.max}`; 112 | } else if (options.min) { 113 | return ` greater than or equal to ${options.min}`; 114 | } else if (options.max) { 115 | return ` less than or equal to ${options.max}`; 116 | } 117 | return ''; 118 | } 119 | 120 | /** 121 | * @see https://github.com/chriso/validator.js 122 | */ 123 | export function validate(name, value) { 124 | load(name); 125 | const opts = options(name); 126 | 127 | // Check if we have any validators 128 | if (!opts.validators) { 129 | return true; 130 | } 131 | 132 | // Force all validators into objects 133 | const validators = opts.validators.map(validatorConfig => { 134 | const t = typeof validatorConfig; 135 | 136 | if ('string' === t) { 137 | validatorConfig = { 138 | name: validatorConfig, 139 | options: null 140 | }; 141 | } else if ('object' !== t || !('name' in validatorConfig)) { 142 | throw new Error(`Invalid validatorConfig configuration: ${JSON.stringify(validatorConfig)}`); 143 | } 144 | 145 | return validatorConfig; 146 | }); 147 | 148 | // Run validators a build array of errors 149 | const errors = validators.reduce((errors, {name, options}) => { 150 | switch (name) { 151 | case 'contains': 152 | validateOptions(name, options, 'string'); 153 | if (!validator.contains(value, options)) { 154 | errors.push(`Must contain the string "${options}"`); 155 | } 156 | break; 157 | 158 | case 'equals': 159 | validateOptions(name, options, 'string'); 160 | if (!validator.equals(value, options)) { 161 | errors.push(`Must be set to "${options}"`); 162 | } 163 | break; 164 | 165 | case 'before': 166 | case 'after': 167 | validateOptions(name, options, 'string'); 168 | if (!validator.isDate(options)) { 169 | throw new Error(`The "${name}" validator expects its options to be a valid date, but "${value}" supplied`); 170 | } 171 | 172 | if ('after' === name) { 173 | if (!validator.isAfter(value, new Date(options))) { 174 | errors.push(`Must be set to a date after ${options}`); 175 | } 176 | } else if ('before' === name) { 177 | if (!validator.isBefore(value, new Date(options))) { 178 | errors.push(`Must be set to a date before ${options}`); 179 | } 180 | } 181 | break; 182 | 183 | case 'alpha': 184 | if (!validator.isAlpha(value)) { 185 | errors.push('Must be alpha characters only (a-z)'); 186 | } 187 | break; 188 | 189 | case 'alphanumeric': 190 | if (!validator.isAlphanumeric(value)) { 191 | errors.push('Must be alphanumeric characters only'); 192 | } 193 | break; 194 | 195 | case 'ascii': 196 | if (!validator.isAscii(value)) { 197 | errors.push('Must be ASCII characters only'); 198 | } 199 | break; 200 | 201 | case 'base64': 202 | if (!validator.isBase64(value)) { 203 | errors.push('Must be a base64-encoded string'); 204 | } 205 | break; 206 | 207 | case 'boolean': 208 | if (!validator.isBoolean(value)) { 209 | errors.push('Must be a boolean (true, false, 1, or 0)'); 210 | } 211 | break; 212 | 213 | case 'date': 214 | if (!validator.isDate(value)) { 215 | errors.push('Must be a date'); 216 | } 217 | break; 218 | 219 | case 'decimal': 220 | if (!validator.isDecimal(value)) { 221 | errors.push('Must be a decimal number'); 222 | } 223 | break; 224 | 225 | case 'fqdn': 226 | if (!validator.isFQDN(value, options)) { 227 | errors.push('Must be a fully qualified domain name'); 228 | } 229 | break; 230 | 231 | case 'float': 232 | if (!validator.isFloat(value, options)) { 233 | errors.push('Must be a floating point number' + minMaxMessage(options)); 234 | } 235 | break; 236 | 237 | case 'hex-color': 238 | if (!validator.isHexColor(value)) { 239 | errors.push('Must be a HEX color'); 240 | } 241 | break; 242 | 243 | case 'hexadecimal': 244 | if (!validator.isHexadecimal(value)) { 245 | errors.push('Must be a hexadecimal number'); 246 | } 247 | break; 248 | 249 | case 'ip4': 250 | case 'ip6': 251 | case 'ip': 252 | if (!options) { 253 | const versionMatch = name.match(/(\d)$/); 254 | if (versionMatch) { 255 | options = parseInt(versionMatch[1], 10); 256 | } 257 | } 258 | if (!validator.isIP(value, options)) { 259 | errors.push('Must be an IP address' + (options ? ` (version ${options})` : '')); 260 | } 261 | break; 262 | 263 | case 'iso8601': 264 | if (!validator.isISO8601(value)) { 265 | errors.push('Must be an ISO8601-formatted date'); 266 | } 267 | break; 268 | 269 | case 'enum': 270 | case 'in': 271 | validateOptions(name, options, 'array'); 272 | if (!validator.isIn(value, options)) { 273 | errors.push('Must be on of: "' + options.join('", "') + '"'); 274 | } 275 | break; 276 | 277 | case 'int': 278 | if (!validator.isInt(value, options)) { 279 | errors.push('Must be an integer' + minMaxMessage(options)); 280 | } 281 | break; 282 | 283 | case 'json': 284 | if (!validator.isJSON(value)) { 285 | errors.push('Must be JSON'); 286 | } 287 | break; 288 | 289 | case 'length': 290 | validateOptions(name, options, 'object'); 291 | if (!options.min && !options.max) { 292 | throw new Error(`The "${name}" validator requires a "min" or a "max" option`); 293 | } 294 | 295 | var min = options.min || 0; 296 | var max = options.max || undefined; 297 | 298 | if (!validator.isLength(value, min, max)) { 299 | errors.push('Must have a character length' + minMaxMessage(options)); 300 | } 301 | break; 302 | 303 | case 'lowercase': 304 | if (!validator.isLowercase(value)) { 305 | errors.push('Must be lower case'); 306 | } 307 | break; 308 | 309 | case 'mac-address': 310 | if (!validator.isMACAddress(value)) { 311 | errors.push('Must be a MAC address'); 312 | } 313 | break; 314 | 315 | case 'numeric': 316 | if (!validator.isNumeric(value)) { 317 | errors.push('Must be numeric'); 318 | } 319 | break; 320 | 321 | case 'url': 322 | if (!validator.isURL(value, options)) { 323 | errors.push('Must be a URL'); 324 | } 325 | break; 326 | 327 | case 'uuid3': 328 | case 'uuid4': 329 | case 'uuid5': 330 | case 'uuid': 331 | if (!options) { 332 | const versionMatch = name.match(/(\d)$/); 333 | if (versionMatch) { 334 | options = parseInt(versionMatch[1], 10); 335 | } 336 | } 337 | if (!validator.isUUID(value, options)) { 338 | errors.push('Must be a UUID' + (options ? ` (version ${options})` : '')); 339 | } 340 | break; 341 | 342 | case 'uppercase': 343 | if (!validator.isUppercase(value)) { 344 | errors.push('Must be upper case'); 345 | } 346 | break; 347 | 348 | case 'regex': 349 | case 'regexp': 350 | case 'matches': 351 | if ('string' === typeof options) { 352 | options = [options]; 353 | } 354 | validateOptions(name, options, 'array'); 355 | 356 | var res; 357 | if (1 === options.length) { 358 | res = validator.matches(value, options[0]); 359 | } else if (2 === options.length) { 360 | res = validator.matches(value, options[0], options[1]); 361 | } 362 | if (!res) { 363 | errors.push(`Must match the regular expression /${options[0]}/${options[1]}`); 364 | } 365 | break; 366 | } 367 | 368 | return errors; 369 | }, []); 370 | 371 | return errors; 372 | } 373 | 374 | // Run checks 375 | export function check(pretty = true) { 376 | try { 377 | load(); 378 | } catch (e) { 379 | 380 | if (false === pretty || e.toString().indexOf('SyntaxError') !== -1) { 381 | throw e; 382 | } 383 | 384 | const pkg = require('../package.json'); 385 | console.error("\n" + wrap(bgRed.white('ERROR:') + ' Unable to load ' + blue(filename) + '; see ' + underline(pkg.homepage), width) + "\n"); 386 | process.exit(1); 387 | } 388 | 389 | let required = []; 390 | let optional = []; 391 | let validationErrors = []; 392 | 393 | for (var name in config) { 394 | debug(`Checking for variable ${name}`); 395 | 396 | // Load opts 397 | const opts = options(name); 398 | 399 | // Check if variable is set 400 | if (name in process.env) { 401 | debug(`Found variable ${name}`); 402 | const errors = validate(name, process.env[name]); 403 | if (errors.length) { 404 | if (false === pretty) { 405 | var err = new Error(`Environmental variable "${name}" did not pass validation`); 406 | err.validationMessages = errors; 407 | throw err; 408 | } 409 | validationErrors.push({ name, errors }); 410 | } 411 | continue; 412 | } 413 | 414 | // Check if default is set 415 | if (opts.default) { 416 | debug(`Setting ${name} to ${JSON.stringify(opts.default)}`); 417 | process.env[name] = opts.default; 418 | optional.push(name); 419 | continue; 420 | } 421 | 422 | // Check if variable is set as optional 423 | if (false === opts.required) { 424 | debug(`${name} is optional`); 425 | optional.push(name); 426 | continue; 427 | } 428 | 429 | debug(`${name} is required and missing`); 430 | required.push(name); 431 | if (false === pretty) { 432 | throw new Error(`Environmental variable "${name}" must be set`); // FIXME 433 | } 434 | } 435 | 436 | if (true === pretty && (required.length || validationErrors.length || optional.length)) { 437 | console.error(''); 438 | if (required.length) { 439 | header(required.length, 'required'); 440 | required.forEach(name => { 441 | console.error(help(name)); 442 | }); 443 | } 444 | if (validationErrors.length) { 445 | header(validationErrors.length, 'invalid'); 446 | validationErrors.forEach(({ name, errors}) => { 447 | console.error(help(name, errors)); 448 | }); 449 | } 450 | if (optional.length) { 451 | if (required.length) { 452 | console.error(''); 453 | } 454 | header(optional.length, 'missing (but optional)'); 455 | optional.forEach(name => { 456 | console.error(help(name)); 457 | }); 458 | } 459 | console.error(''); 460 | } 461 | 462 | debug('Required missing: ' + required.length); 463 | if (required.length || validationErrors.length) { 464 | process.exit(1); 465 | } 466 | } 467 | 468 | // Print header 469 | function header(count, adv) { 470 | const s = (1 === count ? '' : 's'); 471 | const is = (1 === count ? 'is' : 'are'); 472 | let message = ` The following ${count} environmental variable${s} ${is} ${adv}: `; 473 | console.error(wrap((/optional/.test(adv) ? bgYellow.white(message) : bgRed.black(message)), width)); 474 | } 475 | 476 | // Get formatted help for variable 477 | export function help(name, errors) 478 | { 479 | load(name); 480 | 481 | const opts = options(name); 482 | let help = blue(name); 483 | 484 | if (opts.default) { 485 | help += yellow(` (default=${opts.default})`); 486 | } 487 | 488 | if (opts.description) { 489 | help += ` ${opts.description}`; 490 | } 491 | 492 | if (errors && errors.length) { 493 | errors.forEach(error => { 494 | help += `\n - ${error}`; 495 | }); 496 | } 497 | 498 | return wrap(help, width); 499 | } 500 | 501 | -------------------------------------------------------------------------------- /test/alternate-filename/environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": true 3 | } -------------------------------------------------------------------------------- /test/alternate-filename/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tape = require('tape'); 4 | var checkenv = require(require('../loader')()); 5 | var spy = require('../spy.js'); 6 | 7 | tape('WHEN AN ALTERNATE FILENAME IS PROVIDED:', function(s) { 8 | s.test('load()', function(t) { 9 | t.plan(1); 10 | spy.setup(); 11 | 12 | checkenv.setFilename('environment.json'); 13 | 14 | spy.reset(); 15 | t.doesNotThrow(function() { 16 | checkenv.load(); 17 | }, 'should find the renamed file'); 18 | 19 | spy.restore(); 20 | }); 21 | }); -------------------------------------------------------------------------------- /test/loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function() { 4 | // Figure out what to load 5 | var dir = (process.env.TEST_SRC ? 'src' : 'dist'); 6 | var path = __dirname + '/../' + dir + '/index'; 7 | 8 | if (process.env.TEST_SRC) { 9 | require("babel-register"); 10 | } 11 | 12 | // Purge cache if necessary 13 | var key = require.resolve(path); 14 | if (key in require.cache) { 15 | delete require.cache[key]; 16 | } 17 | 18 | return key; 19 | } -------------------------------------------------------------------------------- /test/mixed/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": { 3 | "required": true, 4 | "description": "Description of environmental variable A" 5 | }, 6 | "B": { 7 | "required": false, 8 | "description": "Description of environmental variable B" 9 | }, 10 | "C": { 11 | "default": "Hello world", 12 | "description": "Description of environmental variable C" 13 | } 14 | } -------------------------------------------------------------------------------- /test/mixed/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tape = require('tape'); 4 | var checkenv = require(require('../loader')()); 5 | var spy = require('../spy.js'); 6 | 7 | tape('WHEN SOME VARIABLES ARE REQUIRED AND SOME ARE OPTIONAL:', function(s) { 8 | s.test('check()', function(t) { 9 | t.plan(15); 10 | spy.setup(); 11 | 12 | spy.reset(); 13 | checkenv.check(); 14 | t.equal(spy.exitCount(), 1, 'should call process.exit() if no variables are set'); 15 | t.ok(spy.lastExitCode() > 0, 'should exit with a non-zero exit code if process.exit() is called'); 16 | t.ok(spy.errorCount() > 0, 'should call console.error() if no variables are set'); 17 | t.equal(process.env.C, 'Hello world', 'should set default when unset'); 18 | 19 | spy.reset(['B']); 20 | checkenv.check(); 21 | t.equal(spy.exitCount(), 1, 'should call process.exit() if only optional variables are set'); 22 | t.ok(spy.lastExitCode() > 0, 'should exit with a non-zero exit code if process.exit() is called'); 23 | t.ok(spy.errorCount() > 0, 'should call console.error() if only optional variables are set'); 24 | t.equal(process.env.C, 'Hello world', 'should set default when unset'); 25 | 26 | spy.reset(['A']); 27 | checkenv.check(); 28 | t.equal(spy.exitCount(), 0, 'should not call process.exit() if only required variables are set'); 29 | t.ok(spy.errorCount() > 0, 'should call console.error() if optional variables are missing'); 30 | t.equal(process.env.C, 'Hello world', 'should set default when unset'); 31 | 32 | spy.reset(['A', 'B']); 33 | checkenv.check(); 34 | t.equal(spy.exitCount(), 0, 'should not call process.exit() if all variables are set'); 35 | t.equal(spy.errorCount(), 0, 'should call console.error() if all variables are set'); 36 | t.equal(process.env.C, 'Hello world', 'should set default when unset'); 37 | 38 | spy.reset(['C']); 39 | checkenv.check(); 40 | t.equal(process.env.C, 'set by spy', 'should not overwrite value with default if value is set'); 41 | 42 | spy.restore(); 43 | }); 44 | 45 | s.test('check(false)', function(t) { 46 | t.plan(8); 47 | spy.setup(); 48 | 49 | spy.reset(); 50 | t.throws(function() { 51 | checkenv.check(false); 52 | }, 'should throw an error if required variables are missing'); 53 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 54 | 55 | spy.reset(['B']); 56 | t.throws(function() { 57 | checkenv.check(false); 58 | }, 'should throw an error if only optional variables are set'); 59 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 60 | 61 | spy.reset(['A']); 62 | t.doesNotThrow(function() { 63 | checkenv.check(false); 64 | }, 'should not throw an error if required variables are set'); 65 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 66 | 67 | spy.reset(['A', 'B']); 68 | t.doesNotThrow(function() { 69 | checkenv.check(false); 70 | }, 'should not throw an error if all variables are set'); 71 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 72 | 73 | spy.restore(); 74 | }); 75 | 76 | s.test('help()', function(t) { 77 | t.plan(1); 78 | 79 | t.throws(function() { 80 | var key = 'MISSING_' + (Number.MAX_VALUE * Math.random()); 81 | checkenv.help(key); 82 | }, /no configuration/i, 'should throw an error when loading help for a non-configured variable'); 83 | }); 84 | }); -------------------------------------------------------------------------------- /test/no-file/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tape = require('tape'); 4 | var checkenv = require(require('../loader')()); 5 | var spy = require('../spy.js'); 6 | 7 | tape('WHEN env.json IS MISSING:', function(s) { 8 | s.test('check()', function(t) { 9 | t.plan(3); 10 | spy.setup(); 11 | 12 | spy.reset(); 13 | checkenv.check(); 14 | t.equal(spy.exitCount(), 1, 'should call process.exit() if env.json is missing'); 15 | t.ok(spy.lastExitCode() > 0, 'should exit with a non-zero exit code if env.json is missing'); 16 | t.ok(spy.errorCount() > 0, 'should call console.error() if env.json is missing'); 17 | 18 | spy.restore(); 19 | }); 20 | 21 | s.test('check(false)', function(t) { 22 | t.plan(1); 23 | 24 | t.throws(function() { 25 | checkenv.check(false); 26 | }, /not found/i, 'should throw a "not found" error'); 27 | }); 28 | }); -------------------------------------------------------------------------------- /test/not-valid-json/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": { 3 | "required": true, 4 | "description": "Description of environmental variable A" 5 | }, 6 | "B": { 7 | "required": true, 8 | "description": "Description of environmental variable B" 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /test/not-valid-json/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tape = require('tape'); 4 | var checkenv = require(require('../loader')()); 5 | var spy = require('../spy.js'); 6 | 7 | tape('WHEN env.json IS NOT VALID JSON:', function(s) { 8 | s.test('check()', function(t) { 9 | t.plan(4); 10 | spy.setup(); 11 | 12 | spy.reset(); 13 | 14 | t.throws(function() { 15 | checkenv.check(); 16 | }, /SyntaxError/i, 'should throw a "SyntaxError" error'); 17 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 18 | 19 | spy.reset(['A']); 20 | 21 | t.throws(function() { 22 | checkenv.check(false); 23 | }, /SyntaxError/i, 'should throw a "SyntaxError" error'); 24 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 25 | 26 | spy.restore(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/optional/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": { 3 | "required": false, 4 | "description": "Description of environmental variable A" 5 | }, 6 | "B": { 7 | "required": false, 8 | "description": "Description of environmental variable B" 9 | } 10 | } -------------------------------------------------------------------------------- /test/optional/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tape = require('tape'); 4 | var checkenv = require(require('../loader')()); 5 | var spy = require('../spy.js'); 6 | 7 | function reset(pass) { 8 | var vars; 9 | if (pass) { 10 | vars = ['A', 'B'] 11 | } 12 | spy.reset(vars); 13 | } 14 | 15 | tape('WHEN VARIABLES ARE OPTIONAL:', function(s) { 16 | s.test('check()', function(t) { 17 | t.plan(2); 18 | spy.setup(); 19 | 20 | reset(); 21 | checkenv.check(); 22 | t.ok(spy.errorCount() > 0, 'should call console.error() if optional variables are missing'); 23 | 24 | reset(true); 25 | checkenv.check(); 26 | t.equal(spy.errorCount(), 0, 'should not call console.error() if all variables are set'); 27 | 28 | spy.restore(); 29 | }); 30 | 31 | s.test('check(false)', function(t) { 32 | t.plan(4); 33 | spy.setup(); 34 | 35 | reset(); 36 | t.doesNotThrow(function() { 37 | checkenv.check(false); 38 | }, 'should not throw an error if optional variables are missing'); 39 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 40 | 41 | reset(true); 42 | t.doesNotThrow(function() { 43 | checkenv.check(false); 44 | }, 'should not throw an error if optional variables are set'); 45 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 46 | 47 | spy.restore(); 48 | }); 49 | }); -------------------------------------------------------------------------------- /test/required/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": { 3 | "required": true, 4 | "description": "Description of environmental variable A" 5 | }, 6 | "B": { 7 | "required": true, 8 | "description": "Description of environmental variable B" 9 | } 10 | } -------------------------------------------------------------------------------- /test/required/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tape = require('tape'); 4 | var checkenv = require(require('../loader')()); 5 | var spy = require('../spy.js'); 6 | 7 | tape('WHEN VARIABLES ARE REQUIRED:', function(s) { 8 | s.test('check()', function(t) { 9 | t.plan(8); 10 | spy.setup(); 11 | 12 | spy.reset(); 13 | checkenv.check(); 14 | t.equal(spy.exitCount(), 1, 'should call process.exit() if required variables are missing'); 15 | t.ok(spy.lastExitCode() > 0, 'should exit with a non-zero exit code if process.exit() is called'); 16 | t.ok(spy.errorCount() > 0, 'should call console.error() if required variables are missing'); 17 | 18 | spy.reset(['A']); 19 | checkenv.check(); 20 | t.equal(spy.exitCount(), 1, 'should call process.exit() if some required variables are missing'); 21 | t.ok(spy.lastExitCode() > 0, 'should exit with a non-zero exit code if process.exit() is called'); 22 | t.ok(spy.errorCount() > 0, 'should call console.error() if some required variables are missing'); 23 | 24 | spy.reset(['A', 'B']); 25 | checkenv.check(); 26 | t.equal(spy.exitCount(), 0, 'should not call process.exit() if required variables are set'); 27 | t.equal(spy.errorCount(), 0, 'should not call console.error() if required variables are set'); 28 | 29 | spy.restore(); 30 | }); 31 | 32 | s.test('check(false)', function(t) { 33 | t.plan(6); 34 | spy.setup(); 35 | 36 | spy.reset(); 37 | t.throws(function() { 38 | checkenv.check(false); 39 | }, 'should throw an error if required variables are missing'); 40 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 41 | 42 | spy.reset(['A']); 43 | t.throws(function() { 44 | checkenv.check(false); 45 | }, 'should throw an error if some required variables are missing'); 46 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 47 | 48 | spy.reset(['A', 'B']); 49 | t.doesNotThrow(function() { 50 | checkenv.check(false); 51 | }, 'should not throw an error if required variables are set'); 52 | t.equal(spy.errorCount(), 0, 'and should not call console.error()'); 53 | 54 | spy.restore(); 55 | }); 56 | }); -------------------------------------------------------------------------------- /test/spy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var originalExit = process.exit; 4 | var originalError = console.error; 5 | 6 | var exitCount = 0; 7 | var lastExitCode = null; 8 | var errorCount = 0; 9 | var errorMessages = []; 10 | var managedVars = []; 11 | 12 | exports.setup = function() { 13 | process.exit = function(code) { 14 | exitCount++; 15 | lastExitCode = code; 16 | }; 17 | 18 | console.error = function(message) { 19 | errorCount++; 20 | errorMessages.push(message); 21 | } 22 | }; 23 | 24 | exports.reset = function(vars) { 25 | exitCount = 0; 26 | lastExitCode = null; 27 | errorCount = 0; 28 | errorMessages = []; 29 | 30 | // Handle setting and unsetting variables 31 | var key; 32 | 33 | // Delete any existing managed keys 34 | managedVars.forEach(function(key, idx) { 35 | if (key in process.env) { 36 | delete process.env[key]; 37 | } 38 | }); 39 | managedVars = []; 40 | 41 | if (Array.isArray(vars)) { 42 | vars.forEach(function(key) { 43 | managedVars.push(key); 44 | process.env[key] = 'set by spy'; 45 | }); 46 | } 47 | }; 48 | 49 | exports.restore = function() { 50 | process.exit = originalExit; 51 | exports.reset(); 52 | }; 53 | 54 | exports.exitCount = function() { 55 | return exitCount; 56 | }; 57 | 58 | exports.lastExitCode = function() { 59 | return lastExitCode; 60 | }; 61 | 62 | exports.errorCount = function() { 63 | return errorCount; 64 | }; 65 | 66 | exports.errorMessages = function() { 67 | return errorMessages; 68 | }; 69 | -------------------------------------------------------------------------------- /test/validators/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": { 3 | "validators": [ 4 | { 5 | "name": "contains", 6 | "options": "word" 7 | }, { 8 | "name": "in", 9 | "options": ["one", "two", "three"] 10 | }, 11 | "alpha" 12 | ] 13 | } 14 | } -------------------------------------------------------------------------------- /test/validators/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tape = require('tape'); 4 | var checkenv = require(require('../loader')()); 5 | var spy = require('../spy.js'); 6 | 7 | function e(name, value) { 8 | if (!value && name in process.env) { 9 | delete process.env[name] 10 | } else if (value) { 11 | process.env[name] = value; 12 | } 13 | } 14 | 15 | function config(name, value, options) { 16 | spy.reset(); 17 | e('VALIDATOR_TEST', value); 18 | checkenv.setConfig({ 19 | VALIDATOR_TEST: { 20 | validators: [{ 21 | name: name, 22 | options: options 23 | }] 24 | } 25 | }); 26 | } 27 | 28 | var validationTests = [{ 29 | name: 'contains', 30 | fail: [{ 31 | value: 'abc', 32 | options: 'xyz', 33 | regex: /contain the string \"xyz\"/i 34 | }], 35 | pass: [{ 36 | value: 'abcdefghi', 37 | options: 'def' 38 | }] 39 | }, { 40 | name: 'equals', 41 | fail: [{ 42 | value: 'abcxyz', 43 | options: 'xyz', 44 | regex: /be set to \"xyz\"/i 45 | }], 46 | pass: [{ 47 | value: 'abc', 48 | options: 'abc' 49 | }] 50 | }, { 51 | name: 'before', 52 | fail: [{ 53 | value: '2015-01-01', 54 | options: '2012-07-14', 55 | regex: /date before 2012-07-14/i 56 | }], 57 | pass: [{ 58 | value: '2014-12-31', 59 | options: '2015-01-01' 60 | }] 61 | }, { 62 | name: 'after', 63 | fail: [{ 64 | value: '2000-01-01', 65 | options: '2015-01-01', 66 | regex: /date after 2015-01-01/i 67 | }], 68 | pass: [{ 69 | value: '2015-01-01', 70 | options: '2014-08-24' 71 | }] 72 | } , { 73 | name: 'alpha', 74 | fail: [{ 75 | value: 'abc123', 76 | regex: /alpha/i 77 | }], 78 | pass: [{ 79 | value: 'abcabc' 80 | }] 81 | }, { 82 | name: 'alphanumeric', 83 | fail: [{ 84 | value: 'abc123!', 85 | regex: /alphanumeric/i 86 | }], 87 | pass: [{ 88 | value: 'abc123' 89 | }] 90 | }, { 91 | name: 'ascii', 92 | fail: [{ 93 | value: '👾', 94 | regex: /ascii/i 95 | }], 96 | pass: [{ 97 | value: '`1234567890-=~!@#$%^&*()_+qwertyuiop[]\\QWERTYUIOP{}|;\'\:",./<>?' 98 | }] 99 | }, { 100 | name: 'base64', 101 | fail: [{ 102 | value: 'this is not base64-encoded', 103 | regex: /base64/i 104 | }], 105 | pass: [{ 106 | value: 'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=' 107 | }] 108 | }, { 109 | name: 'boolean', 110 | fail: [{ 111 | value: 'abc', 112 | regex: /boolean/i 113 | }], 114 | pass: [ 115 | 'true', 116 | 'false', 117 | '0', 118 | '1' 119 | ] 120 | }, { 121 | name: 'date', 122 | fail: [{ 123 | value: 'this is not a date!', 124 | regex: /must be a date/i 125 | }], 126 | pass: [{ 127 | value: '2015-05-18' 128 | }] 129 | }, { 130 | name: 'decimal', 131 | fail: [{ 132 | value: 'aaa', 133 | regex: /decimal/i 134 | }], 135 | pass: [ 136 | '1', 137 | '1.2', 138 | '0.1' 139 | ] 140 | }, { 141 | name: 'fqdn', 142 | fail: [{ 143 | value: 'abc', 144 | regex: /fully qualified/i 145 | }], 146 | pass: [ 147 | 'cmorrell.com', 148 | 'cmorrell.ninja', 149 | 'www.cmorrell.com', 150 | 'john--snow.com', 151 | 'xn--froschgrn-x9a.com', 152 | 'abc.xyz' 153 | ] 154 | }, { 155 | name: 'float', 156 | fail: [{ 157 | value: '--1', 158 | regex: /floating point/i 159 | }, { 160 | value: 'abc', 161 | regex: /floating point/i 162 | }, { 163 | value: '3.14', 164 | options: { min: 4 }, 165 | regex: /greater than/i 166 | }, { 167 | value: '3.14', 168 | options: { max: 3 }, 169 | regex: /less than/i 170 | }, { 171 | value: '99.9', 172 | options: { min: 50.5, max: 75.5 }, 173 | regex: /between/i 174 | }], 175 | pass: [ 176 | '123', 177 | '123.', 178 | '123.456', 179 | '-987.654', 180 | '0.1234' 181 | ] 182 | }, { 183 | name: 'hex-color', 184 | fail: [{ 185 | value: '99999G', 186 | regex: /hex color/i 187 | }], 188 | pass: [ 189 | '663399', 190 | '#663399', 191 | 'dada99', 192 | 'DADA99', 193 | 'DBda99' 194 | ] 195 | }, 196 | { 197 | name: 'hexadecimal', 198 | fail: [{ 199 | value: 'aaa39347937272gk', 200 | regex: /hexadecimal/i 201 | }], 202 | pass: [ 203 | 'a', 204 | 'b', 205 | 'c', 206 | 'd', 207 | 'e', 208 | 'f', 209 | '0', 210 | '1', 211 | '9' 212 | ] 213 | }, 214 | { 215 | name: 'ip4', 216 | fail: [{ 217 | value: '2001:db8:0000:1:1:1:1:1', 218 | regex: /version 4/i 219 | }, { 220 | value: '127.0.0.256', 221 | regex: /version 4/i 222 | }], 223 | pass: [ 224 | '127.0.0.1' 225 | ] 226 | }, { 227 | name: 'ip6', 228 | fail: [{ 229 | value: '9999:zzz:9999:9:9:9:9:9', 230 | regex: /version 6/i 231 | }, { 232 | value: '127.0.0.1', 233 | regex: /version 6/i 234 | }], 235 | pass: [ 236 | '2001:db8:0000:1:1:1:1:1', 237 | ] 238 | }, { 239 | name: 'ip', 240 | fail: [{ 241 | value: '2001:db8:0000:1:1:1:1:1', 242 | options: 4, 243 | regex: /version 4/i 244 | }, { 245 | value: '127.0.0.256', 246 | options: 4, 247 | regex: /version 4/i 248 | }, { 249 | value: '9999:zzz:9999:9:9:9:9:9', 250 | options: 6, 251 | regex: /version 6/i 252 | }, { 253 | value: '127.0.0.1', 254 | options: 6, 255 | regex: /version 6/i 256 | }, { 257 | value: '127.0.0.299', 258 | regex: /ip address/i 259 | }, { 260 | value: 'abc', 261 | regex: /ip address/i 262 | }], 263 | pass: [ 264 | '127.0.0.1', 265 | '2001:db8:0000:1:1:1:1:1' 266 | ] 267 | }, { 268 | name: 'iso8601', 269 | fail: [{ 270 | value: '200905', 271 | regex: /iso8601/i 272 | }], 273 | pass: [ 274 | '2009-05-19', 275 | '20090519' 276 | ] 277 | }, { 278 | name: 'enum', 279 | fail: [{ 280 | value: 'z', 281 | options: ['a', 'b', 'c'], 282 | regex: /must be on of/i 283 | }], 284 | pass: [{ 285 | value: 'b', 286 | options: ['a', 'b', 'c'] 287 | }] 288 | }, 289 | { 290 | name: 'in', 291 | fail: [{ 292 | value: 'z', 293 | options: ['a', 'b', 'c'], 294 | regex: /must be on of/i 295 | }], 296 | pass: [{ 297 | value: 'b', 298 | options: ['a', 'b', 'c'] 299 | }] 300 | }, 301 | { 302 | name: 'int', 303 | fail: [{ 304 | value: '1.1', 305 | regex: /integer/i 306 | }, { 307 | value: 'abc', 308 | regex: /integer/i 309 | }, { 310 | value: '2', 311 | options: { min: 4 }, 312 | regex: /greater than/i 313 | }, { 314 | value: '7', 315 | options: { max: 3 }, 316 | regex: /less than/i 317 | }, { 318 | value: '99', 319 | options: { min: 50, max: 75 }, 320 | regex: /between/i 321 | }], 322 | pass: [{ 323 | value: '123' 324 | }, { 325 | value: '5', 326 | options: { min: 4 } 327 | }, { 328 | value: '3', 329 | options: { max: 3 } 330 | }, { 331 | value: '63', 332 | options: { min: 50, max: 75 } 333 | }] 334 | }, { 335 | name: 'json', 336 | fail: [{ 337 | value: 'this: is not json', 338 | regex: /json/i 339 | }], 340 | pass: [ 341 | '{ "hello": "world" }' 342 | ] 343 | }, { 344 | name: 'length', 345 | fail: [{ 346 | value: 'abc', 347 | options: { min: 4 }, 348 | regex: /greater than/i 349 | }, { 350 | value: 'abcdefg', 351 | options: { max: 4 }, 352 | regex: /less than/i 353 | }, { 354 | value: 'abc', 355 | options: { min: 4, max: 10 }, 356 | regex: /between/i 357 | }], 358 | pass: [{ 359 | value: 'abcd', 360 | options: { min: 4 } 361 | }, { 362 | value: 'abcd', 363 | options: { max: 4 } 364 | }, { 365 | value: 'abcdef', 366 | options: { min: 4, max: 10 } 367 | }] 368 | }, { 369 | name: 'lowercase', 370 | fail: [{ 371 | value: 'ABC', 372 | regex: /lower/i 373 | }], 374 | pass: [{ 375 | value: 'abc' 376 | }] 377 | }, { 378 | name: 'mac-address', 379 | fail: [{ 380 | value: '01:02:03:04:05', 381 | regex: /mac address/i 382 | }], 383 | pass: [ 384 | '01:02:03:04:05:ab' 385 | ] 386 | }, { 387 | name: 'numeric', 388 | fail: [{ 389 | value: '123.123', 390 | regex: /numeric/i 391 | }], 392 | pass: [ 393 | '123', 394 | '+123', 395 | '-123' 396 | ] 397 | }, { 398 | name: 'url', 399 | fail: [{ 400 | value: 'not a URL', 401 | regex: /url/i 402 | }], 403 | pass: [ 404 | 'http://www.cmorrell.com' 405 | ] 406 | }, 407 | { 408 | name: 'uuid3', 409 | fail: [{ 410 | value: '713ae7e3-cb32-45f9-adcb-7c4fa86b90c1', 411 | regex: /version 3/i 412 | }, { 413 | value: '987FBC97-4BED-5078-AF07-9141BA07C9F3', 414 | regex: /version 3/i 415 | }], 416 | pass: [{ 417 | value: 'A987FBC9-4BED-3078-CF07-9141BA07C9F3' 418 | }] 419 | }, 420 | { 421 | name: 'uuid4', 422 | fail: [{ 423 | value: 'A987FBC9-4BED-3078-CF07-9141BA07C9F3', 424 | regex: /version 4/i 425 | }, { 426 | value: '987FBC97-4BED-5078-AF07-9141BA07C9F3', 427 | regex: /version 4/i 428 | }], 429 | pass: [{ 430 | value: '713ae7e3-cb32-45f9-adcb-7c4fa86b90c1' 431 | }] 432 | }, 433 | { 434 | name: 'uuid5', 435 | fail: [{ 436 | value: '713ae7e3-cb32-45f9-adcb-7c4fa86b90c1', 437 | regex: /version 5/i 438 | }, { 439 | value: 'A987FBC9-4BED-3078-CF07-9141BA07C9F3', 440 | regex: /version 5/i 441 | }], 442 | pass: [{ 443 | value: '987FBC97-4BED-5078-AF07-9141BA07C9F3' 444 | }] 445 | }, 446 | { 447 | name: 'uuid', 448 | fail: [{ 449 | value: '713ae7e3-cb32-45f9-adcb-7c4fa86b90c1', 450 | options: 3, 451 | regex: /version 3/i 452 | }, { 453 | value: '987FBC97-4BED-5078-AF07-9141BA07C9F3', 454 | options: 3, 455 | regex: /version 3/i 456 | }, { 457 | value: 'A987FBC9-4BED-3078-CF07-9141BA07C9F3', 458 | options: 4, 459 | regex: /version 4/i 460 | }, { 461 | value: '987FBC97-4BED-5078-AF07-9141BA07C9F3', 462 | options: 4, 463 | regex: /version 4/i 464 | }, { 465 | value: '713ae7e3-cb32-45f9-adcb-7c4fa86b90c1', 466 | options: 5, 467 | regex: /version 5/i 468 | }, { 469 | value: 'A987FBC9-4BED-3078-CF07-9141BA07C9F3', 470 | options: 5, 471 | regex: /version 5/i 472 | }, { 473 | value: 'Not a UUID', 474 | regex: /uuid/i 475 | }], 476 | pass: [ 477 | 'A987FBC9-4BED-3078-CF07-9141BA07C9F3', 478 | '713ae7e3-cb32-45f9-adcb-7c4fa86b90c1', 479 | '987FBC97-4BED-5078-AF07-9141BA07C9F3' 480 | ] 481 | }, { 482 | name: 'uppercase', 483 | fail: [{ 484 | value: 'abc', 485 | regex: /upper/i 486 | }], 487 | pass: [{ 488 | value: 'ABC' 489 | }] 490 | }, { 491 | name: 'matches', 492 | fail: [{ 493 | value: 'abc', 494 | options: 'ABC', 495 | regex: /regular expression/i 496 | }, { 497 | value: 'def', 498 | options: ['abc', 'i'], 499 | regex: /regular expression/i 500 | }], 501 | pass: [{ 502 | value: 'ABC', 503 | options: 'ABC' 504 | }, { 505 | value: 'ABC', 506 | options: ['abc', 'i'] 507 | }] 508 | }]; 509 | 510 | tape('VALIDATORS:', function(s) { 511 | validationTests.forEach(function(v) { 512 | s.test('"' + v.name + '" validator', function(t) { 513 | var expectedFailures = 7; 514 | var expectedPasses = 3; 515 | 516 | t.plan((v.fail.length * expectedFailures) + (v.pass.length * expectedPasses)); 517 | spy.setup(); 518 | 519 | // Failures 520 | v.fail.forEach(function(f) { 521 | // Pretty 522 | config(v.name, f.value, f.options); 523 | checkenv.check(); 524 | t.equal(spy.exitCount(), 1, 'should call process.exit() if validation fails'); 525 | t.ok(spy.lastExitCode() > 0, 'should exit with a non-zero exit code if validation fails'); 526 | t.ok(spy.errorCount() > 0, 'should call console.error() if validation fails'); 527 | t.ok(f.regex.test(spy.errorMessages().join(' ')), 'stderr should match ' + f.regex); 528 | 529 | // Throws 530 | config(v.name, f.value, f.options); 531 | t.throws(function() { 532 | checkenv.check(false); 533 | }, /did not pass/i, 'should throw an error if validation fails and pretty === false'); 534 | 535 | try { 536 | checkenv.check(false); 537 | } catch (e) { 538 | t.ok(e.validationMessages, 'error should have a validationMessages property'); 539 | var messages = (e.validationMessages ? e.validationMessages.join(' ') : ''); 540 | t.ok(f.regex.test(messages), 'validationMessages should match ' + f.regex); 541 | } 542 | }); 543 | 544 | // Passes 545 | v.pass.forEach(function(p) { 546 | if ('string' === typeof p) { 547 | p = { 548 | value: p, 549 | options: null 550 | }; 551 | } 552 | config(v.name, p.value, p.options); 553 | 554 | t.equal(spy.exitCount(), 0, 'should not call process.exit() if validation passes'); 555 | t.equal(spy.errorCount(), 0, 'should not call console.error() if validation passes'); 556 | 557 | config(v.name, p.value, p.options); 558 | t.doesNotThrow(function() { 559 | checkenv.check(false); 560 | }, 'should not throw an error if validation passes and pretty === false'); 561 | }); 562 | 563 | spy.restore(); 564 | }); 565 | }); 566 | }); --------------------------------------------------------------------------------