├── .editorconfig ├── .env ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── Readme.md ├── SECURITY.md ├── package-lock.json ├── package.json ├── src ├── index.spec.ts └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_size = 2 7 | indent_style = space 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | LOCALENV_TEST=true 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | extends: [ 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier/@typescript-eslint", 6 | "plugin:prettier/recommended" 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, 10 | sourceType: "module" 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | npm-debug.log 4 | dist/ 5 | coverage/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | node_js: 9 | - "8" 10 | - "stable" 11 | 12 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 13 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Envobj 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![NPM downloads][downloads-image]][downloads-url] 5 | [![Build status][travis-image]][travis-url] 6 | [![Test coverage][coveralls-image]][coveralls-url] 7 | 8 | > Tiny environment variable helper. 9 | 10 | Ensures that all the required variables are present. Throws on invalid and missing values. 11 | 12 | ## Install 13 | 14 | ``` 15 | npm install envobj 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```js 21 | const { envobj, string, number, boolean } = require("envobj"); 22 | 23 | const env = envobj( 24 | { 25 | DATABASE_URL: string, 26 | PORT: number, 27 | USE_PAPERTRAIL: boolean 28 | }, 29 | process.env, 30 | { 31 | PORT: "8000" // Requires a number, set `8000` if `PORT` is missing. 32 | } 33 | ); 34 | ``` 35 | 36 | **Built-in validators:** `string`, `number`, `boolean` and `integer`. 37 | 38 | ### `.env` 39 | 40 | Use with [`localenv`](https://github.com/defunctzombie/localenv) to populate `process.env` automatically in development from `.env` and `.env.local`. 41 | 42 | ```js 43 | import { envobj, number } from "envobj"; 44 | 45 | import "localenv"; 46 | 47 | export const env = envobj( 48 | { 49 | PORT: number 50 | }, 51 | process.env 52 | ); 53 | ``` 54 | 55 | **Tip:** Check in `.env` and exclude `.env.local` so teammates can get started quickly. 56 | 57 | ## License 58 | 59 | MIT 60 | 61 | [npm-image]: https://img.shields.io/npm/v/envobj.svg?style=flat 62 | [npm-url]: https://npmjs.org/package/envobj 63 | [downloads-image]: https://img.shields.io/npm/dm/envobj.svg?style=flat 64 | [downloads-url]: https://npmjs.org/package/envobj 65 | [travis-image]: https://img.shields.io/travis/matthewmueller/envobj.svg?style=flat 66 | [travis-url]: https://travis-ci.org/matthewmueller/envobj 67 | [coveralls-image]: https://img.shields.io/coveralls/matthewmueller/envobj.svg?style=flat 68 | [coveralls-url]: https://coveralls.io/r/matthewmueller/envobj?branch=master 69 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security contact information 4 | 5 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "envobj", 3 | "version": "2.0.2", 4 | "description": "Tiny environment variable helper", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "files": [ 8 | "dist/" 9 | ], 10 | "scripts": { 11 | "prettier": "prettier --write", 12 | "lint": "eslint \"./src/**/*.{js,jsx,ts,tsx}\" --quiet --fix", 13 | "format": "npm run prettier -- \"{.,src/**}/*.{js,jsx,ts,tsx,json,md,yml,yaml}\"", 14 | "build": "rimraf dist/ && tsc", 15 | "specs": "jest --coverage", 16 | "test": "npm run build && npm run lint && npm run specs", 17 | "prepublish": "npm run build" 18 | }, 19 | "keywords": [ 20 | "env", 21 | "var", 22 | "variable", 23 | "obj", 24 | "schema" 25 | ], 26 | "repository": "matthewmueller/envobj", 27 | "author": "Matthew Mueller", 28 | "license": "MIT", 29 | "jest": { 30 | "roots": [ 31 | "/src/" 32 | ], 33 | "transform": { 34 | "\\.tsx?$": "ts-jest" 35 | }, 36 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(tsx?|jsx?)$", 37 | "moduleFileExtensions": [ 38 | "ts", 39 | "tsx", 40 | "js", 41 | "jsx", 42 | "json", 43 | "node" 44 | ] 45 | }, 46 | "husky": { 47 | "hooks": { 48 | "pre-commit": "lint-staged" 49 | } 50 | }, 51 | "lint-staged": { 52 | "*.{js,jsx,ts,tsx,json,md,yml,yaml}": [ 53 | "npm run prettier", 54 | "git add" 55 | ] 56 | }, 57 | "publishConfig": { 58 | "access": "public" 59 | }, 60 | "devDependencies": { 61 | "@types/jest": "^24.9.0", 62 | "@types/node": "^13.1.8", 63 | "@typescript-eslint/eslint-plugin": "^2.17.0", 64 | "@typescript-eslint/parser": "^2.17.0", 65 | "eslint": "^6.8.0", 66 | "eslint-config-prettier": "^6.9.0", 67 | "eslint-plugin-prettier": "^3.1.2", 68 | "husky": "^4.2.0", 69 | "jest": "^25.1.0", 70 | "lint-staged": "^10.0.1", 71 | "localenv": "^0.2.2", 72 | "prettier": "^1.19.1", 73 | "rimraf": "^3.0.0", 74 | "ts-jest": "^24.3.0", 75 | "typescript": "^3.7.5" 76 | }, 77 | "dependencies": { 78 | "make-error": "^1.3.5" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | envobj, 3 | string, 4 | boolean, 5 | number, 6 | integer, 7 | InvalidEnvError 8 | } from "./index"; 9 | 10 | describe("envobj", () => { 11 | it("should parse env", () => { 12 | const env = envobj( 13 | { STRING: string, BOOLEAN: boolean, NUMBER: number }, 14 | { STRING: "foobar" }, 15 | { BOOLEAN: "true" }, 16 | { NUMBER: "123" } 17 | ); 18 | 19 | expect(env).toEqual({ STRING: "foobar", BOOLEAN: true, NUMBER: 123 }); 20 | }); 21 | 22 | it("should error on missing keys", () => { 23 | expect(() => envobj({ MISSING: string }, {})).toThrowError(InvalidEnvError); 24 | }); 25 | 26 | it("should error on invalid keys", () => { 27 | expect(() => envobj({ INVALID: number }, { INVALID: "test" })).toThrowError( 28 | InvalidEnvError 29 | ); 30 | }); 31 | 32 | describe("string", () => { 33 | it("should return strings", () => { 34 | expect(string("123")).toEqual("123"); 35 | }); 36 | }); 37 | 38 | describe("boolean", () => { 39 | it("should return booleans", () => { 40 | expect(boolean("true")).toEqual(true); 41 | expect(boolean("TRUE")).toEqual(true); 42 | expect(boolean("false")).toEqual(false); 43 | expect(boolean("FALSE")).toEqual(false); 44 | expect(boolean("invalid")).toEqual(undefined); 45 | }); 46 | }); 47 | 48 | describe("number", () => { 49 | it("should return numbers", () => { 50 | expect(number("123")).toEqual(123); 51 | expect(number("123.45")).toEqual(123.45); 52 | expect(number("invalid")).toEqual(undefined); 53 | }); 54 | }); 55 | 56 | describe("integer", () => { 57 | it("should return integers", () => { 58 | expect(integer("123")).toEqual(123); 59 | expect(integer("123.45")).toEqual(undefined); 60 | expect(integer("invalid")).toEqual(undefined); 61 | }); 62 | }); 63 | 64 | describe("localenv integration", () => { 65 | beforeAll(() => { 66 | require("localenv"); 67 | }); 68 | 69 | it("should load from `.env`", () => { 70 | const env = envobj({ LOCALENV_TEST: boolean }, process.env); 71 | 72 | expect(env).toEqual({ LOCALENV_TEST: true }); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from "make-error"; 2 | 3 | /** 4 | * Create error message from missing and invalid parameters. 5 | */ 6 | function makeMessage( 7 | missing: Array, 8 | invalid: Array<[PropertyKey, any]> 9 | ) { 10 | const message: string[] = []; 11 | 12 | if (missing.length) { 13 | const details = missing.join(", "); 14 | 15 | message.push(`Missing values: ${details}`); 16 | } 17 | 18 | if (invalid.length) { 19 | const details = invalid 20 | .map(([key, value]) => `${String(key)}: ${value}`) 21 | .join(", "); 22 | 23 | message.push(`Invalid values: ${details}`); 24 | } 25 | 26 | return message.join("; "); 27 | } 28 | 29 | /** 30 | * Invalid environment error. 31 | */ 32 | export class InvalidEnvError extends BaseError { 33 | constructor(public missing: T[], public invalid: Array<[T, any]>) { 34 | super(makeMessage(missing, invalid)); 35 | } 36 | } 37 | 38 | /** 39 | * Valid schema object. 40 | */ 41 | export interface Schema { 42 | [key: string]: (value: string) => any; 43 | } 44 | 45 | /** 46 | * Valid source object. 47 | */ 48 | export type Source = Partial>; 49 | 50 | /** 51 | * Derive the environment from a simple schema. 52 | */ 53 | export type Env = { 54 | [K in keyof T]: Exclude, undefined>; 55 | }; 56 | 57 | /** 58 | * Tiny environment variable helper. 59 | */ 60 | export function envobj( 61 | schema: T, 62 | source: Source, 63 | ...sources: Source[] 64 | ): Env { 65 | const env: Env = Object.create(null); 66 | const missing: Array = []; 67 | const invalid: Array<[keyof T, any]> = []; 68 | 69 | for (const key of Object.keys(schema) as (keyof T)[]) { 70 | let value: string | undefined = source[key]; 71 | 72 | for (const source of sources) { 73 | if (value !== undefined) break; 74 | value = source[key]; 75 | } 76 | 77 | if (value === undefined) { 78 | missing.push(key); 79 | } else { 80 | const result = schema[key](value); 81 | if (result === undefined) invalid.push([key, value]); 82 | env[key] = result; 83 | } 84 | } 85 | 86 | // Throw on invalid environment. 87 | if (invalid.length || missing.length) { 88 | throw new InvalidEnvError(missing, invalid); 89 | } 90 | 91 | return env; 92 | } 93 | 94 | /** 95 | * Parse string variable. 96 | */ 97 | export const string = (input: string) => String(input); 98 | 99 | /** 100 | * Parse boolean variable. 101 | */ 102 | export const boolean = (input: string) => { 103 | const value = input.toLowerCase(); 104 | if (value === "true") return true; 105 | if (value === "false") return false; 106 | return undefined; 107 | }; 108 | 109 | /** 110 | * Parse numeric variable. 111 | */ 112 | export const number = (input: string) => { 113 | const value = Number(input); 114 | if (isNaN(value)) return undefined; 115 | return value; 116 | }; 117 | 118 | /** 119 | * Parse integer variable. 120 | */ 121 | export const integer = (input: string) => { 122 | const value = Number(input); 123 | if (isNaN(value) || value % 1 !== 0) return undefined; 124 | return value; 125 | }; 126 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "lib": ["ES2018"], 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "module": "CommonJS", 8 | "moduleResolution": "Node", 9 | "strict": true, 10 | "declaration": true, 11 | "sourceMap": true, 12 | "inlineSources": true, 13 | "esModuleInterop": true 14 | }, 15 | "include": ["src/**/*"] 16 | } 17 | --------------------------------------------------------------------------------