From 749e3ebb23c30c05dc93ecc325660cc73681e7fa Mon Sep 17 00:00:00 2001 From: Nicholas Kahlor Date: Fri, 26 Feb 2021 09:20:40 -0700 Subject: [PATCH 01/18] use dotenv flow to read in environment --- package-lock.json | 19 +++++++++++++++++++ package.json | 5 ++++- src/index.ts | 16 ++++++++++++++++ src/types/Options.ts | 2 ++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 329e104..922c3fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -759,6 +759,12 @@ "@babel/types": "^7.3.0" } }, + "@types/dotenv-flow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/dotenv-flow/-/dotenv-flow-3.1.0.tgz", + "integrity": "sha512-qaWT42KDePdAGZFryYoV7EZnuuYZAO4KPVDWUV9OBOyJx7xCgKKERtVB7jBCM2mtKVI+OMMDK2ef11PWcHJz3g==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.4", "resolved": "https://packages.aa.com/artifactory/api/npm/npm-public/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", @@ -2032,6 +2038,19 @@ "is-obj": "^2.0.0" } }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, + "dotenv-flow": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/dotenv-flow/-/dotenv-flow-3.2.0.tgz", + "integrity": "sha512-GEB6RrR4AbqDJvNSFrYHqZ33IKKbzkvLYiD5eo4+9aFXr4Y4G+QaFrB/fNp0y6McWBmvaPn3ZNjIufnj8irCtg==", + "requires": { + "dotenv": "^8.0.0" + } + }, "duplexer3": { "version": "0.1.4", "resolved": "https://packages.aa.com/artifactory/api/npm/npm-public/duplexer3/-/duplexer3-0.1.4.tgz", diff --git a/package.json b/package.json index 13e5972..a329cdf 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,11 @@ "url": "https://github.com/AmericanAirlines/simple-env/issues" }, "homepage": "https://github.com/AmericanAirlines/simple-env#readme", - "dependencies": {}, + "dependencies": { + "dotenv-flow": "^3.2.0" + }, "devDependencies": { + "@types/dotenv-flow": "^3.1.0", "@typescript-eslint/eslint-plugin": "^4.8.1", "@typescript-eslint/parser": "^4.8.1", "eslint": "^7.13.0", diff --git a/src/index.ts b/src/index.ts index 0c3d237..7edce93 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { config, DotenvConfigOptions, DotenvLoadOutput } from 'dotenv-flow'; import { EnvVarSymbols, UndefinedEnvVars } from './types/EnvVars'; import { SymbolWithDescription } from './types/helpers'; import { InternalOptions, Options } from './types/Options'; @@ -7,6 +8,7 @@ let _symbolizedEnvVars: Record; let _options: InternalOptions = { required: {}, optional: {}, + dotEnvOptions: {}, }; const createSymbol = (description: string): SymbolWithDescription => Symbol(description) as SymbolWithDescription; @@ -33,6 +35,18 @@ function symbolizeVars(input: Record) { ); } +function parseEnv(options: DotenvConfigOptions = {}): any { + const { parsed }: DotenvLoadOutput = config(options); + if (parsed) { + const required: Record = {}; + Object.keys(parsed).forEach((key) => { + required[key] = key; + }); + return required; + } + return undefined; +} + export default function setEnv( options: Options, ): { @@ -43,6 +57,8 @@ export default function setEnv>(_options.required); _requiredEnvVars = Object.values(symbolizedRequiredEnvVars); diff --git a/src/types/Options.ts b/src/types/Options.ts index 54e5b34..9241c1f 100644 --- a/src/types/Options.ts +++ b/src/types/Options.ts @@ -1,9 +1,11 @@ +import { DotenvConfigOptions } from 'dotenv-flow'; import { DefaultEnvVars, UndefinedEnvVars } from './EnvVars'; import { RemoveKeys } from './helpers'; export interface Options { required?: Required; optional?: RemoveKeys; + dotEnvOptions?: DotenvConfigOptions; } export interface InternalOptions extends Required { From f534154a87ee0e00e92bdcaef126b5eae5dd205a Mon Sep 17 00:00:00 2001 From: Nicholas Kahlor Date: Fri, 26 Feb 2021 09:22:36 -0700 Subject: [PATCH 02/18] clean up code --- src/index.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7edce93..6beff53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,16 +35,8 @@ function symbolizeVars(input: Record) { ); } -function parseEnv(options: DotenvConfigOptions = {}): any { - const { parsed }: DotenvLoadOutput = config(options); - if (parsed) { - const required: Record = {}; - Object.keys(parsed).forEach((key) => { - required[key] = key; - }); - return required; - } - return undefined; +function parseEnv(options: DotenvConfigOptions = {}) { + config(options); } export default function setEnv( From 20dcc2fdb0620eba9efb93ece315402805d44e56 Mon Sep 17 00:00:00 2001 From: Nicholas Kahlor Date: Fri, 26 Feb 2021 10:55:38 -0700 Subject: [PATCH 03/18] include a unit test --- .env.unittest | 1 + src/index.ts | 2 +- src/test/index.test.ts | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .env.unittest diff --git a/.env.unittest b/.env.unittest new file mode 100644 index 0000000..6544940 --- /dev/null +++ b/.env.unittest @@ -0,0 +1 @@ +TEST=test \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 6beff53..debb1ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { config, DotenvConfigOptions, DotenvLoadOutput } from 'dotenv-flow'; +import { config, DotenvConfigOptions } from 'dotenv-flow'; import { EnvVarSymbols, UndefinedEnvVars } from './types/EnvVars'; import { SymbolWithDescription } from './types/helpers'; import { InternalOptions, Options } from './types/Options'; diff --git a/src/test/index.test.ts b/src/test/index.test.ts index b7af759..4289be3 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -99,5 +99,11 @@ describe('simple-env', () => { expect(Object.getOwnPropertyDescriptors(env)).not.toHaveProperty('something'); expect(Object.getOwnPropertyDescriptors(env)).toHaveProperty('somethingElse'); }); + + it('will read variable from .env file into process.env', () => { + const env = setEnv({ required: { test: 'TEST' }, dotEnvOptions: { node_env: 'unittest' } }); + + expect(process.env.TEST).toEqual(env.test); + }); }); }); From f4d78dfda5d4ee8af26d83435abddb91417354d2 Mon Sep 17 00:00:00 2001 From: Nicholas Kahlor Date: Fri, 26 Feb 2021 11:04:04 -0700 Subject: [PATCH 04/18] full coverage --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index debb1ad..e0ca5a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,7 +35,7 @@ function symbolizeVars(input: Record) { ); } -function parseEnv(options: DotenvConfigOptions = {}) { +function parseEnv(options: DotenvConfigOptions) { config(options); } From 39e4ad43230947f13b5f212b7cd82b8af7e29f05 Mon Sep 17 00:00:00 2001 From: Nicholas Kahlor Date: Fri, 26 Feb 2021 11:05:34 -0700 Subject: [PATCH 05/18] fix test name --- src/test/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/index.test.ts b/src/test/index.test.ts index 4289be3..ce602db 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -100,7 +100,7 @@ describe('simple-env', () => { expect(Object.getOwnPropertyDescriptors(env)).toHaveProperty('somethingElse'); }); - it('will read variable from .env file into process.env', () => { + it('will read an env variable from a .env file into process.env', () => { const env = setEnv({ required: { test: 'TEST' }, dotEnvOptions: { node_env: 'unittest' } }); expect(process.env.TEST).toEqual(env.test); From a07693d6bcd95e1f25fe494d1200fec8c08f8d3c Mon Sep 17 00:00:00 2001 From: Nicholas Kahlor Date: Fri, 26 Feb 2021 16:19:49 -0700 Subject: [PATCH 06/18] reinvent the wheel a bit, just for kicks --- package.json | 4 +-- src/index.ts | 56 ++++++++++++++++++++++++++++++++++++++---- src/test/index.test.ts | 2 +- src/types/Options.ts | 8 ++++-- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index a329cdf..e17295d 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,7 @@ "url": "https://github.com/AmericanAirlines/simple-env/issues" }, "homepage": "https://github.com/AmericanAirlines/simple-env#readme", - "dependencies": { - "dotenv-flow": "^3.2.0" - }, + "dependencies": {}, "devDependencies": { "@types/dotenv-flow": "^3.1.0", "@typescript-eslint/eslint-plugin": "^4.8.1", diff --git a/src/index.ts b/src/index.ts index e0ca5a4..8a9eeb7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ -import { config, DotenvConfigOptions } from 'dotenv-flow'; +import { readFileSync, existsSync } from 'fs'; +import { EOL } from 'os'; +import { join } from 'path'; import { EnvVarSymbols, UndefinedEnvVars } from './types/EnvVars'; import { SymbolWithDescription } from './types/helpers'; -import { InternalOptions, Options } from './types/Options'; +import { EnvOptions, InternalOptions, Options } from './types/Options'; let _requiredEnvVars: SymbolWithDescription[] = []; let _symbolizedEnvVars: Record; @@ -35,8 +37,52 @@ function symbolizeVars(input: Record) { ); } -function parseEnv(options: DotenvConfigOptions) { - config(options); +function parseLine(line: string): Record { + const delimiter = '='; + const lineRegex = /^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*="?'?.*'?"?$/g; + let [key, val] = line.split(delimiter); + + // Ignore comments, or lines which don't conform to acceptable patterns + if (key.startsWith('#') || key.startsWith('//') || !lineRegex.test(line)) { + return {}; + } + + key = key.trim(); + val = val.trim(); + // Get rid of wrapping double or single quotes + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.substr(1, val.length - 2); + } + + return { [key]: val }; +} + +function parseEnvFile(dotEnvOptions: EnvOptions = {}): Record { + const envPath = dotEnvOptions.pathToEnv || process.cwd(); + const envFilename = dotEnvOptions.envFileName || '.env'; + const fullPath = join(envPath, envFilename); + + if (!existsSync(fullPath)) { + return {}; + } + + const envFileContents = readFileSync(fullPath).toString(); + let envVarPairs: Record = {}; + + const lines = envFileContents.split(EOL); + lines.forEach((line: string) => { + envVarPairs = { ...envVarPairs, ...parseLine(line) }; + }); + + // Toss everything into the environment + Object.entries(envVarPairs).forEach(([key, val]) => { + // Prefer env vars that have been set by the OS + if (!process.env[key]) { + process.env[key] = val; + } + }); + + return envVarPairs; } export default function setEnv( @@ -49,7 +95,7 @@ export default function setEnv>(_options.required); _requiredEnvVars = Object.values(symbolizedRequiredEnvVars); diff --git a/src/test/index.test.ts b/src/test/index.test.ts index ce602db..e47594f 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -101,7 +101,7 @@ describe('simple-env', () => { }); it('will read an env variable from a .env file into process.env', () => { - const env = setEnv({ required: { test: 'TEST' }, dotEnvOptions: { node_env: 'unittest' } }); + const env = setEnv({ required: { test: 'TEST' }, dotEnvOptions: { envFileName: '.env.unittest' } }); expect(process.env.TEST).toEqual(env.test); }); diff --git a/src/types/Options.ts b/src/types/Options.ts index 9241c1f..b76dbf9 100644 --- a/src/types/Options.ts +++ b/src/types/Options.ts @@ -1,11 +1,15 @@ -import { DotenvConfigOptions } from 'dotenv-flow'; import { DefaultEnvVars, UndefinedEnvVars } from './EnvVars'; import { RemoveKeys } from './helpers'; +export interface EnvOptions { + pathToEnv?: string; + envFileName?: string; +} + export interface Options { required?: Required; optional?: RemoveKeys; - dotEnvOptions?: DotenvConfigOptions; + dotEnvOptions?: EnvOptions; } export interface InternalOptions extends Required { From 046ccb27bdab2465f56321da50910681677546c8 Mon Sep 17 00:00:00 2001 From: nkahlor Date: Mon, 8 Mar 2021 18:38:56 -0700 Subject: [PATCH 07/18] remove dependency --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 3e29b47..465def2 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "homepage": "https://github.com/AmericanAirlines/simple-env#readme", "dependencies": {}, "devDependencies": { - "@types/dotenv-flow": "^3.1.0", "@typescript-eslint/eslint-plugin": "^4.8.1", "@typescript-eslint/parser": "^4.8.1", "chalk": "^4.1.0", @@ -52,4 +51,4 @@ "ts-node": "^9.0.0", "typescript": "^4.2.2" } -} +} \ No newline at end of file From dc3747b66c3c1565ac2d9dec0af0347819bf1031 Mon Sep 17 00:00:00 2001 From: nkahlor Date: Mon, 8 Mar 2021 18:41:09 -0700 Subject: [PATCH 08/18] merge filename options --- src/index.ts | 8 +++----- src/types/Options.ts | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8a9eeb7..499cce8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,9 +58,7 @@ function parseLine(line: string): Record { } function parseEnvFile(dotEnvOptions: EnvOptions = {}): Record { - const envPath = dotEnvOptions.pathToEnv || process.cwd(); - const envFilename = dotEnvOptions.envFileName || '.env'; - const fullPath = join(envPath, envFilename); + const fullPath = dotEnvOptions.envFile || join(process.cwd(), '.env') if (!existsSync(fullPath)) { return {}; @@ -88,8 +86,8 @@ function parseEnvFile(dotEnvOptions: EnvOptions = {}): Record { export default function setEnv( options: Options, ): { - readonly [K in keyof (T & Partial)]: (T & Partial)[K]; -} { + readonly [K in keyof (T & Partial)]: (T & Partial)[K]; + } { _options = { ..._options, ...options, diff --git a/src/types/Options.ts b/src/types/Options.ts index 1039e01..b5a49e6 100644 --- a/src/types/Options.ts +++ b/src/types/Options.ts @@ -2,8 +2,7 @@ import { DefaultEnvVars, UndefinedEnvVars } from './EnvVars'; import { RemoveKeys } from './helpers'; export interface EnvOptions { - pathToEnv?: string; - envFileName?: string; + envFile?: string; } export interface Options { From d663bb4eeb7629ba251a6f95fd7c4365f2000091 Mon Sep 17 00:00:00 2001 From: nkahlor Date: Wed, 10 Mar 2021 15:23:47 -0700 Subject: [PATCH 09/18] unit tests, yay --- .env.unittest | 1 - .vscode/launch.json | 21 +++++++++ package-lock.json | 19 --------- package.json | 2 +- src/index.ts | 58 +++---------------------- src/parser.ts | 50 ++++++++++++++++++++++ src/test/index.test.ts | 96 +++++++++++++++++++++++++++++++++++++++++- src/types/Options.ts | 3 +- 8 files changed, 174 insertions(+), 76 deletions(-) delete mode 100644 .env.unittest create mode 100644 .vscode/launch.json create mode 100644 src/parser.ts diff --git a/.env.unittest b/.env.unittest deleted file mode 100644 index 6544940..0000000 --- a/.env.unittest +++ /dev/null @@ -1 +0,0 @@ -TEST=test \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..20535bd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "name": "vscode-jest-tests", + "request": "launch", + "args": [ + "--runInBand" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a007c02..7cce232 100644 --- a/package-lock.json +++ b/package-lock.json @@ -759,12 +759,6 @@ "@babel/types": "^7.3.0" } }, - "@types/dotenv-flow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/dotenv-flow/-/dotenv-flow-3.1.0.tgz", - "integrity": "sha512-qaWT42KDePdAGZFryYoV7EZnuuYZAO4KPVDWUV9OBOyJx7xCgKKERtVB7jBCM2mtKVI+OMMDK2ef11PWcHJz3g==", - "dev": true - }, "@types/graceful-fs": { "version": "4.1.4", "resolved": "https://packages.aa.com/artifactory/api/npm/npm-public/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", @@ -2038,19 +2032,6 @@ "is-obj": "^2.0.0" } }, - "dotenv": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" - }, - "dotenv-flow": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/dotenv-flow/-/dotenv-flow-3.2.0.tgz", - "integrity": "sha512-GEB6RrR4AbqDJvNSFrYHqZ33IKKbzkvLYiD5eo4+9aFXr4Y4G+QaFrB/fNp0y6McWBmvaPn3ZNjIufnj8irCtg==", - "requires": { - "dotenv": "^8.0.0" - } - }, "duplexer3": { "version": "0.1.4", "resolved": "https://packages.aa.com/artifactory/api/npm/npm-public/duplexer3/-/duplexer3-0.1.4.tgz", diff --git a/package.json b/package.json index 465def2..881bfea 100644 --- a/package.json +++ b/package.json @@ -51,4 +51,4 @@ "ts-node": "^9.0.0", "typescript": "^4.2.2" } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 499cce8..3221625 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,14 @@ -import { readFileSync, existsSync } from 'fs'; -import { EOL } from 'os'; -import { join } from 'path'; +import parseEnvFile from './parser'; import { EnvVarSymbols, UndefinedEnvVars } from './types/EnvVars'; import { SymbolWithDescription } from './types/helpers'; -import { EnvOptions, InternalOptions, Options } from './types/Options'; +import { InternalOptions, Options } from './types/Options'; let _requiredEnvVars: SymbolWithDescription[] = []; let _symbolizedEnvVars: Record; let _options: InternalOptions = { required: {}, optional: {}, - dotEnvOptions: {}, + options: {}, }; const createSymbol = (description: string): SymbolWithDescription => Symbol(description) as SymbolWithDescription; @@ -37,52 +35,6 @@ function symbolizeVars(input: Record) { ); } -function parseLine(line: string): Record { - const delimiter = '='; - const lineRegex = /^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*="?'?.*'?"?$/g; - let [key, val] = line.split(delimiter); - - // Ignore comments, or lines which don't conform to acceptable patterns - if (key.startsWith('#') || key.startsWith('//') || !lineRegex.test(line)) { - return {}; - } - - key = key.trim(); - val = val.trim(); - // Get rid of wrapping double or single quotes - if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { - val = val.substr(1, val.length - 2); - } - - return { [key]: val }; -} - -function parseEnvFile(dotEnvOptions: EnvOptions = {}): Record { - const fullPath = dotEnvOptions.envFile || join(process.cwd(), '.env') - - if (!existsSync(fullPath)) { - return {}; - } - - const envFileContents = readFileSync(fullPath).toString(); - let envVarPairs: Record = {}; - - const lines = envFileContents.split(EOL); - lines.forEach((line: string) => { - envVarPairs = { ...envVarPairs, ...parseLine(line) }; - }); - - // Toss everything into the environment - Object.entries(envVarPairs).forEach(([key, val]) => { - // Prefer env vars that have been set by the OS - if (!process.env[key]) { - process.env[key] = val; - } - }); - - return envVarPairs; -} - export default function setEnv( options: Options, ): { @@ -93,7 +45,9 @@ export default function setEnv>(_options.required); _requiredEnvVars = Object.values(symbolizedRequiredEnvVars); diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..2f99cd9 --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,50 @@ +import * as fs from 'fs'; +import { join } from 'path'; +import { EnvOptions } from './types/Options'; + +function parseLine(line: string): Record { + const delimiter = '='; + const lineRegex = /^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*="?'?.*'?"?$/g; + let [key, val] = line.split(delimiter); + + // Ignore comments, or lines which don't conform to acceptable patterns + if (key.startsWith('#') || key.startsWith('//') || !lineRegex.test(line)) { + return {}; + } + + key = key.trim(); + val = val.trim(); + // Get rid of wrapping double or single quotes + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.substr(1, val.length - 2); + } + + return { [key]: val }; +} + +export function parseEnvFile(dotEnvOptions: EnvOptions = {}): void { + const fullPath = dotEnvOptions.envFile || join(process.cwd(), '.env'); + + if (!fs.existsSync(fullPath)) { + return; + } + + const envFileContents = fs.readFileSync(fullPath).toString(); + let envVarPairs: Record = {}; + const eol = /\r?\n/; + + const lines = envFileContents.split(eol); + lines.forEach((line: string) => { + envVarPairs = { ...envVarPairs, ...parseLine(line) }; + }); + + // Toss everything into the environment + Object.entries(envVarPairs).forEach(([key, val]) => { + // Prefer env vars that have been set by the OS + if (key in process.env === false) { + process.env[key] = val; + } + }); +} + +export default parseEnvFile; diff --git a/src/test/index.test.ts b/src/test/index.test.ts index e47594f..d77596f 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -1,9 +1,13 @@ import 'jest'; +import fs from 'fs'; import setEnvDefault from '..'; +import parseEnvFile from '../parser'; describe('simple-env', () => { // Fresh setEnv for each test let setEnv: typeof setEnvDefault; + const readFileSpy = jest.spyOn(fs, 'readFileSync'); + const existsSyncSpy = jest.spyOn(fs, 'existsSync'); beforeEach(async () => { // Reset module cache and dynamically import it again @@ -54,6 +58,7 @@ describe('simple-env', () => { describe('set', () => { afterEach(() => { + process.env = {}; jest.resetModules(); }); @@ -100,10 +105,97 @@ describe('simple-env', () => { expect(Object.getOwnPropertyDescriptors(env)).toHaveProperty('somethingElse'); }); + it('will invoke the parser is loadDotEnv is true', () => { + existsSyncSpy.mockReturnValue(true); + readFileSpy.mockImplementation(() => Buffer.from('TEST=test')); + + setEnv({ optional: { something: 'SOMETHING' }, options: { loadDotEnv: true } }); + + expect(process.env).toHaveProperty('TEST'); + }); + + it('will not invoke the parser by default', () => { + existsSyncSpy.mockReturnValue(true); + readFileSpy.mockImplementation(() => Buffer.from('TEST=test')); + + setEnv({ optional: { something: 'SOMETHING' } }); + + expect(process.env).not.toHaveProperty('TEST'); + }); + }); + + describe('parser', () => { + beforeEach(() => { + process.env = {}; + }); + + afterEach(() => { + jest.resetModules(); + }); + + it('will not overwrite vars that already exist', () => { + const originalValue = 'I already exist'; + const newValue = 'I should not'; + process.env = { TEST: originalValue }; + + readFileSpy.mockImplementation(() => Buffer.from(`TEST=${newValue}`)); + existsSyncSpy.mockReturnValue(true); + + parseEnvFile(); + + expect(process.env.TEST).toEqual(originalValue); + }); + + it('will reject malformed lines', () => { + const fakeFile = + ` + bad + good=this + 4=bad + good2='this' + good3="this" + `; + readFileSpy.mockImplementation(() => Buffer.from(fakeFile)); + existsSyncSpy.mockReturnValue(true); + + parseEnvFile(); + + expect(process.env.good).toEqual('this'); + expect(process.env.good2).toEqual('this'); + expect(process.env.good3).toEqual('this'); + }); + + it('will ignore comments in the file', () => { + const fakeFile = + ` + #comment\n + //comment\n + TEST=test + `; + readFileSpy.mockImplementation(() => Buffer.from(fakeFile)); + existsSyncSpy.mockReturnValue(true); + + parseEnvFile(); + + expect(process.env.TEST).toEqual('test'); + }); + + it('will not do anything if the .env file does not exist', () => { + readFileSpy.mockImplementation(() => Buffer.from('TEST=test')); + existsSyncSpy.mockReturnValue(false); + + parseEnvFile(); + + expect(process.env.TEST).toBeUndefined(); + }); + it('will read an env variable from a .env file into process.env', () => { - const env = setEnv({ required: { test: 'TEST' }, dotEnvOptions: { envFileName: '.env.unittest' } }); + readFileSpy.mockImplementation(() => Buffer.from('TEST=test')); + existsSyncSpy.mockReturnValue(true); + + parseEnvFile(); - expect(process.env.TEST).toEqual(env.test); + expect(process.env.TEST).toEqual('test'); }); }); }); diff --git a/src/types/Options.ts b/src/types/Options.ts index b5a49e6..0f6d1cd 100644 --- a/src/types/Options.ts +++ b/src/types/Options.ts @@ -3,12 +3,13 @@ import { RemoveKeys } from './helpers'; export interface EnvOptions { envFile?: string; + loadDotEnv?: boolean; } export interface Options { required?: Required; optional?: RemoveKeys; - dotEnvOptions?: EnvOptions; + options?: EnvOptions; } export interface InternalOptions extends Omit, 'optional'> { From 71b28bec50d0d1ed48e63633fd8781b5deec8b1c Mon Sep 17 00:00:00 2001 From: nkahlor Date: Wed, 10 Mar 2021 15:27:09 -0700 Subject: [PATCH 10/18] change options name to be more general --- src/parser.ts | 6 +++--- src/types/Options.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 2f99cd9..a07610e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import { join } from 'path'; -import { EnvOptions } from './types/Options'; +import { ConfigOptions } from './types/Options'; function parseLine(line: string): Record { const delimiter = '='; @@ -22,8 +22,8 @@ function parseLine(line: string): Record { return { [key]: val }; } -export function parseEnvFile(dotEnvOptions: EnvOptions = {}): void { - const fullPath = dotEnvOptions.envFile || join(process.cwd(), '.env'); +export function parseEnvFile(options: ConfigOptions = {}): void { + const fullPath = options.envFile || join(process.cwd(), '.env'); if (!fs.existsSync(fullPath)) { return; diff --git a/src/types/Options.ts b/src/types/Options.ts index 0f6d1cd..586776a 100644 --- a/src/types/Options.ts +++ b/src/types/Options.ts @@ -1,7 +1,7 @@ import { DefaultEnvVars, UndefinedEnvVars } from './EnvVars'; import { RemoveKeys } from './helpers'; -export interface EnvOptions { +export interface ConfigOptions { envFile?: string; loadDotEnv?: boolean; } @@ -9,7 +9,7 @@ export interface EnvOptions { export interface Options { required?: Required; optional?: RemoveKeys; - options?: EnvOptions; + options?: ConfigOptions; } export interface InternalOptions extends Omit, 'optional'> { From 1d49d3d6431fcf6ab3135bdecb497893e8860eab Mon Sep 17 00:00:00 2001 From: nkahlor Date: Wed, 10 Mar 2021 16:42:41 -0700 Subject: [PATCH 11/18] fix formatting --- src/index.ts | 4 ++-- src/test/index.test.ts | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3221625..3deb8bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,8 +38,8 @@ function symbolizeVars(input: Record) { export default function setEnv( options: Options, ): { - readonly [K in keyof (T & Partial)]: (T & Partial)[K]; - } { + readonly [K in keyof (T & Partial)]: (T & Partial)[K]; +} { _options = { ..._options, ...options, diff --git a/src/test/index.test.ts b/src/test/index.test.ts index d77596f..5e1d8dc 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -147,8 +147,7 @@ describe('simple-env', () => { }); it('will reject malformed lines', () => { - const fakeFile = - ` + const fakeFile = ` bad good=this 4=bad @@ -166,8 +165,7 @@ describe('simple-env', () => { }); it('will ignore comments in the file', () => { - const fakeFile = - ` + const fakeFile = ` #comment\n //comment\n TEST=test From a1910ea61fc0675703850f34d1bed1f44b2db80b Mon Sep 17 00:00:00 2001 From: nkahlor Date: Wed, 17 Mar 2021 19:44:30 -0700 Subject: [PATCH 12/18] cleaning up --- .gitignore | 2 ++ src/index.ts | 2 +- src/parser.ts | 2 +- src/test/index.test.ts | 78 ++-------------------------------------- src/test/parser.test.ts | 79 +++++++++++++++++++++++++++++++++++++++++ src/types/Options.ts | 3 +- 6 files changed, 86 insertions(+), 80 deletions(-) create mode 100644 src/test/parser.test.ts diff --git a/.gitignore b/.gitignore index 6704566..fd09255 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.vscode + # Logs logs *.log diff --git a/src/index.ts b/src/index.ts index 3deb8bd..dc00504 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,7 +45,7 @@ export default function setEnv { } export function parseEnvFile(options: ConfigOptions = {}): void { - const fullPath = options.envFile || join(process.cwd(), '.env'); + const fullPath = options.envPath || join(process.cwd(), '.env'); if (!fs.existsSync(fullPath)) { return; diff --git a/src/test/index.test.ts b/src/test/index.test.ts index 5e1d8dc..d2d3e00 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -1,7 +1,6 @@ import 'jest'; import fs from 'fs'; import setEnvDefault from '..'; -import parseEnvFile from '../parser'; describe('simple-env', () => { // Fresh setEnv for each test @@ -105,11 +104,11 @@ describe('simple-env', () => { expect(Object.getOwnPropertyDescriptors(env)).toHaveProperty('somethingElse'); }); - it('will invoke the parser is loadDotEnv is true', () => { + it('will invoke the parser is envPath is set', () => { existsSyncSpy.mockReturnValue(true); readFileSpy.mockImplementation(() => Buffer.from('TEST=test')); - setEnv({ optional: { something: 'SOMETHING' }, options: { loadDotEnv: true } }); + setEnv({ optional: { something: 'SOMETHING' }, options: { envPath: './a/.env' } }); expect(process.env).toHaveProperty('TEST'); }); @@ -123,77 +122,4 @@ describe('simple-env', () => { expect(process.env).not.toHaveProperty('TEST'); }); }); - - describe('parser', () => { - beforeEach(() => { - process.env = {}; - }); - - afterEach(() => { - jest.resetModules(); - }); - - it('will not overwrite vars that already exist', () => { - const originalValue = 'I already exist'; - const newValue = 'I should not'; - process.env = { TEST: originalValue }; - - readFileSpy.mockImplementation(() => Buffer.from(`TEST=${newValue}`)); - existsSyncSpy.mockReturnValue(true); - - parseEnvFile(); - - expect(process.env.TEST).toEqual(originalValue); - }); - - it('will reject malformed lines', () => { - const fakeFile = ` - bad - good=this - 4=bad - good2='this' - good3="this" - `; - readFileSpy.mockImplementation(() => Buffer.from(fakeFile)); - existsSyncSpy.mockReturnValue(true); - - parseEnvFile(); - - expect(process.env.good).toEqual('this'); - expect(process.env.good2).toEqual('this'); - expect(process.env.good3).toEqual('this'); - }); - - it('will ignore comments in the file', () => { - const fakeFile = ` - #comment\n - //comment\n - TEST=test - `; - readFileSpy.mockImplementation(() => Buffer.from(fakeFile)); - existsSyncSpy.mockReturnValue(true); - - parseEnvFile(); - - expect(process.env.TEST).toEqual('test'); - }); - - it('will not do anything if the .env file does not exist', () => { - readFileSpy.mockImplementation(() => Buffer.from('TEST=test')); - existsSyncSpy.mockReturnValue(false); - - parseEnvFile(); - - expect(process.env.TEST).toBeUndefined(); - }); - - it('will read an env variable from a .env file into process.env', () => { - readFileSpy.mockImplementation(() => Buffer.from('TEST=test')); - existsSyncSpy.mockReturnValue(true); - - parseEnvFile(); - - expect(process.env.TEST).toEqual('test'); - }); - }); }); diff --git a/src/test/parser.test.ts b/src/test/parser.test.ts new file mode 100644 index 0000000..d11d6dd --- /dev/null +++ b/src/test/parser.test.ts @@ -0,0 +1,79 @@ +import fs from 'fs'; +import parseEnvFile from '../parser'; + +describe('parser', () => { + const readFileSpy = jest.spyOn(fs, 'readFileSync'); + const existsSyncSpy = jest.spyOn(fs, 'existsSync'); + + beforeEach(async () => { + jest.resetModules(); + process.env = {}; + }); + + afterEach(() => { + jest.resetModules(); + }); + + it('will not overwrite vars that already exist', () => { + const originalValue = 'I already exist'; + const newValue = 'I should not'; + process.env = { TEST: originalValue }; + + readFileSpy.mockImplementation(() => Buffer.from(`TEST=${newValue}`)); + existsSyncSpy.mockReturnValue(true); + + parseEnvFile(); + + expect(process.env.TEST).toEqual(originalValue); + }); + + it('will reject malformed lines', () => { + const fakeFile = ` + bad + good=this + 4=bad + good2='this' + good3="this" + `; + readFileSpy.mockImplementation(() => Buffer.from(fakeFile)); + existsSyncSpy.mockReturnValue(true); + + parseEnvFile(); + + expect(process.env.good).toEqual('this'); + expect(process.env.good2).toEqual('this'); + expect(process.env.good3).toEqual('this'); + }); + + it('will ignore comments in the file', () => { + const fakeFile = ` + #comment\n + //comment\n + TEST=test + `; + readFileSpy.mockImplementation(() => Buffer.from(fakeFile)); + existsSyncSpy.mockReturnValue(true); + + parseEnvFile(); + + expect(process.env.TEST).toEqual('test'); + }); + + it('will not do anything if the .env file does not exist', () => { + readFileSpy.mockImplementation(() => Buffer.from('TEST=test')); + existsSyncSpy.mockReturnValue(false); + + parseEnvFile(); + + expect(process.env.TEST).toBeUndefined(); + }); + + it('will read an env variable from a .env file into process.env', () => { + readFileSpy.mockImplementation(() => Buffer.from('TEST=test')); + existsSyncSpy.mockReturnValue(true); + + parseEnvFile(); + + expect(process.env.TEST).toEqual('test'); + }); +}); diff --git a/src/types/Options.ts b/src/types/Options.ts index 586776a..00e7458 100644 --- a/src/types/Options.ts +++ b/src/types/Options.ts @@ -2,8 +2,7 @@ import { DefaultEnvVars, UndefinedEnvVars } from './EnvVars'; import { RemoveKeys } from './helpers'; export interface ConfigOptions { - envFile?: string; - loadDotEnv?: boolean; + envPath?: string; } export interface Options { From aabef0ba2b3bfe5568b90d333b56d9f0eb780658 Mon Sep 17 00:00:00 2001 From: nkahlor Date: Wed, 17 Mar 2021 19:45:37 -0700 Subject: [PATCH 13/18] remove .vscode --- .vscode/launch.json | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 20535bd..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "name": "vscode-jest-tests", - "request": "launch", - "args": [ - "--runInBand" - ], - "cwd": "${workspaceFolder}", - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "program": "${workspaceFolder}/node_modules/jest/bin/jest" - } - ] -} \ No newline at end of file From f10df701f116c7b2e38da0ee10ad5a107053a9e7 Mon Sep 17 00:00:00 2001 From: Nicholas Kahlor Date: Wed, 17 Mar 2021 19:46:45 -0700 Subject: [PATCH 14/18] Update index.test.ts --- src/test/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/index.test.ts b/src/test/index.test.ts index d2d3e00..fac1231 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -104,7 +104,7 @@ describe('simple-env', () => { expect(Object.getOwnPropertyDescriptors(env)).toHaveProperty('somethingElse'); }); - it('will invoke the parser is envPath is set', () => { + it('will invoke the parser if envPath is set', () => { existsSyncSpy.mockReturnValue(true); readFileSpy.mockImplementation(() => Buffer.from('TEST=test')); From 7d7566eaf2e05c950879295ae9fadc77b2f06ac1 Mon Sep 17 00:00:00 2001 From: nkahlor Date: Wed, 17 Mar 2021 20:22:17 -0700 Subject: [PATCH 15/18] DotEnv parsing instructions --- README.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 03a2ffd..c4163ee 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,8 @@ Autocomplete and Strongly Typed Keys are your new best friend! Using `simple-env | Autocomplete | ✅ | ❌ | ❌ | | Strongly Typed Keys | ✅ | ❌ | ❌ | | Single Location Refactor | ✅ | ❌ | ❌ | +| Loads .env | ✅ | ✅ | ❌ | | Return Type Helpers | 🔜 | ❌ | ✅ | -| Loads .env | 🔜 | ✅ | ❌ | Let's see how some of the features above look in code: @@ -161,6 +161,55 @@ export const env = setEnv({ > **NOTE**: if you choose to assign `optional` and `required` env vars individually, `setEnv` should only be done _once_ for each or you will overwrite your previously defined values. +### Loading DotEnv Files + +You can give `simple-env` a path to a `.env` file, and it will parse the file and import the contents into the environment! + +If you don't specify a path, `simple-env` WILL NOT import anything! + +```typescript +// src/env.ts +import setEnv from '@americanairlines/simple-env'; + +export const env = setEnv({ + required: { + nodeEnv: 'NODE_ENV', + someRequiredSecret: 'SOME_REQUIRED_SECRET', + }, + options: { + envPath: "./.env" + } +}); +``` +Each variable needs to be declared on a separate line. + +Comments must be on their own line, inline comments will be read in as part of the value to the variable! + +If `simple-env` finds something that it doesn't know how to parse, it just skips it and moves on to the next thing! + +Here's a sample `.env` file that will make us all happy :^) +``` +# Comments like this will be ignored +// These will be ignored too :^) + +# All of these vars are gonna work just fine! +NODE_ENV=development +SOME_REQUIRED_SECRET='Single quotes are fine!' +ANOTHER_SECRET="Double quotes are fine too, we don't discriminate :^)" +lowercase=no problem + SECRET_2_ELECTRIC_BOOGALOO = "We don't mind whitespace between the equal signs, or before the var definition" +``` + +Here's a sample `.env` file that will make us all sad :^( +``` +# Uh-oh, these ones are invalid, so we'll have to skip them +1BAD_VAR="Variables can't begin with numbers" +ANOTHER BAD VAR="no whitespace allowed in var names" +KEBAB-CASE="can't do it" +VAR_WITHOUT_EQUAL_IS_SKIPPED +loose text like this will also get skipped +``` + # Contributing Interested in contributing to the project? Check out our [Contributing Guidelines](./.github/CONTRIBUTING.md). From b1058ce5bc1d9884d70cc809943f5d042416bf39 Mon Sep 17 00:00:00 2001 From: nkahlor Date: Wed, 17 Mar 2021 20:28:34 -0700 Subject: [PATCH 16/18] bump minor version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7cce232..4b9ab6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@americanairlines/simple-env", - "version": "1.0.4", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 881bfea..3ae5b03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@americanairlines/simple-env", - "version": "1.0.4", + "version": "1.1.0", "description": "An intuitive, strongly typed, and scalable way to retrieve environment variables", "keywords": [ "env", From cf06e658a2263e7a9cec9aca3b63d884bd7162d7 Mon Sep 17 00:00:00 2001 From: Nicholas Kahlor Date: Thu, 18 Mar 2021 08:49:27 -0700 Subject: [PATCH 17/18] Update README.md Co-authored-by: John Kahn <7807353+johnkahn@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4163ee..96b0e90 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ export const env = setEnv({ > **NOTE**: if you choose to assign `optional` and `required` env vars individually, `setEnv` should only be done _once_ for each or you will overwrite your previously defined values. -### Loading DotEnv Files +### Loading .env files You can give `simple-env` a path to a `.env` file, and it will parse the file and import the contents into the environment! From e2d6dd23a44e621a45c02769de71b69d2111c1c1 Mon Sep 17 00:00:00 2001 From: Nicholas Kahlor Date: Thu, 18 Mar 2021 08:49:54 -0700 Subject: [PATCH 18/18] Update README.md Co-authored-by: John Kahn <7807353+johnkahn@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 96b0e90..b8b956c 100644 --- a/README.md +++ b/README.md @@ -163,9 +163,9 @@ export const env = setEnv({ ### Loading .env files -You can give `simple-env` a path to a `.env` file, and it will parse the file and import the contents into the environment! +If you set a path to an `.env` file, `simple-env` will parse the file and import the contents into the environment automatically. These will be available in the `process.env` object. -If you don't specify a path, `simple-env` WILL NOT import anything! +> If you don't specify a path, `simple-env` **will not** automatically load any `.env` files. This is to avoid a breaking change. ```typescript // src/env.ts