diff --git a/README.md b/README.md index 704bc42..251a7c4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Example root files: ``` # with npm -npm install @ringofstorms/dotenv-multi +npm install @ringofstorms/dotenv-multi dotenv dotenv-expand yn ``` # Usage diff --git a/package-lock.json b/package-lock.json index ead1f38..e2c9364 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@ringofstorms/dotenv-multi", - "version": "0.0.1", + "version": "0.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -35,12 +35,14 @@ "dotenv": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true }, "dotenv-expand": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true }, "fs.realpath": { "version": "1.0.0", @@ -126,7 +128,8 @@ "yn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yn/-/yn-4.0.0.tgz", - "integrity": "sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg==" + "integrity": "sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg==", + "dev": true } } } diff --git a/package.json b/package.json index 9a0b6c6..47b7f9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ringofstorms/dotenv-multi", - "version": "0.1.1", + "version": "0.2.0", "description": "Dot environment file loader that acts like Create React App's supporting many files with per NODE_ENV/local settings.", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -14,7 +14,7 @@ }, "author": "Josh (RingOfStorms)", "license": "ISC", - "dependencies": { + "peerDependencies": { "dotenv": "^8.2.0", "dotenv-expand": "^5.1.0", "yn": "^4.0.0" @@ -22,7 +22,10 @@ "devDependencies": { "@types/node": "^14.14.14", "rimraf": "^3.0.2", - "typescript": "^4.1.3" + "typescript": "^4.1.3", + "dotenv": "^8.2.0", + "dotenv-expand": "^5.1.0", + "yn": "^4.0.0" }, "repository": { "type": "git", diff --git a/src/booleanConverter.ts b/src/booleanConverter.ts new file mode 100644 index 0000000..65ee38d --- /dev/null +++ b/src/booleanConverter.ts @@ -0,0 +1,28 @@ +import * as yn from 'yn'; + +import { BooleanConverter, ConfigOptions } from "./types" + +export const booleanConverter: BooleanConverter = (options: ConfigOptions, log: (...args: any[]) => void) => { + const convertPredicate = (key: string): boolean => { + if (key) { + if (Array.isArray(options.convertToBoolean)) { + if (options.convertToBoolean.includes(key)) { + return true; + } + } else if (options.convertToBoolean === key) { + return true; + } + if (typeof options.convertToBooleanPredicate === 'function' && options.convertToBooleanPredicate(key)) { + return true; + } + } + return false; + } + Object.entries(process.env).forEach(([key, value]) => { + if (convertPredicate(key)) { + const boolValue = yn(value) ? 'true' : ''; + process.env[key] = boolValue; + log('Environment variable converted into string boolean value:', key, "original:", value, 'newValue:', boolValue); + } + }); +} diff --git a/src/dotenvMulti.ts b/src/dotenvMulti.ts new file mode 100644 index 0000000..f4734c9 --- /dev/null +++ b/src/dotenvMulti.ts @@ -0,0 +1,46 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import * as dotenv from 'dotenv'; +import * as dotenvExpand from 'dotenv-expand'; + +import { BooleanConverter, ConfigOptions } from "./types" +import { Lazy } from "./lazy" + +const lazyBoolConverter = new Lazy(() => require('./booleanConverter').booleanConverter); + +export function config(options: ConfigOptions = {}): void { + const log = options.debug ? console.log : (() => {}); + + // Same logic as create-react-app for resolving env files + // https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/config/env.js + const NODE_ENV = (process.env.NODE_ENV || 'unknown').toLowerCase(); + const appDirectory = fs.realpathSync(process.cwd()); + const dotenvFilePath: string = path.resolve(appDirectory, '.env'); + + const dotenvFiles = [ + `${ dotenvFilePath }.${ NODE_ENV }.local`, + `${ dotenvFilePath }.${ NODE_ENV }`, + NODE_ENV !== 'test' && `${ dotenvFilePath }.local`, + dotenvFilePath, + ].filter(i => i) as string[]; // this cast is safe due to the identity filter + + log('Environment files to check:', dotenvFiles.join()); + + const propsConfigured = {}; + dotenvFiles.forEach((dotenvFile) => { + if (fs.existsSync(dotenvFile)) { + const {parsed} = dotenvExpand(dotenv.config({path: dotenvFile, debug: options.debug || undefined })); + Object.assign(propsConfigured, parsed); + } + }); + + log('Environment variables loaded from dotenvs:', Object.keys(propsConfigured).length); + log('\t', propsConfigured); + + const runBoolConverter: boolean = !!(options.convertToBooleanPredicate || options.convertToBoolean); + log('Environment variable boolean converter will run:', runBoolConverter); + if (runBoolConverter) { + lazyBoolConverter.get()(options, log); + } +} diff --git a/src/index.ts b/src/index.ts index cd4b63d..a940205 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,69 +1,2 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -import * as dotenv from 'dotenv'; -import * as dotenvExpand from 'dotenv-expand'; -import * as yn from 'yn'; - -export interface ConfigOptions { - convertToBooleanPredicate?: (variable: string) => boolean; - convertToBoolean?: string | string[]; - debug?: boolean; -} - -export function config(options: ConfigOptions = {}): void { - const log = options.debug ? console.log : (() => {}); - - // Same logic as create-react-app for resolving env files - // https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/config/env.js - const NODE_ENV = (process.env.NODE_ENV || 'unknown').toLowerCase(); - const appDirectory = fs.realpathSync(process.cwd()); - const dotenvFilePath: string = path.resolve(appDirectory, '.env'); - - const dotenvFiles = [ - `${ dotenvFilePath }.${ NODE_ENV }.local`, - `${ dotenvFilePath }.${ NODE_ENV }`, - NODE_ENV !== 'test' && `${ dotenvFilePath }.local`, - dotenvFilePath, - ].filter(i => i) as string[]; // this cast is safe due to the identity filter - - log('Environment files to check:', dotenvFiles.join()); - - const propsConfigured = {}; - dotenvFiles.forEach((dotenvFile) => { - if (fs.existsSync(dotenvFile)) { - const {parsed} = dotenvExpand(dotenv.config({path: dotenvFile, debug: options.debug || undefined })); - Object.assign(propsConfigured, parsed); - } - }); - - log('Environment variables loaded from dotenvs:', Object.keys(propsConfigured).length); - log('\t', propsConfigured); - - const runBoolConverter: boolean = !!(options.convertToBooleanPredicate || options.convertToBoolean); - log('Environment variable boolean converter will run:', runBoolConverter); - if (runBoolConverter) { - const convertPredicate = (key: string): boolean => { - if (key) { - if (Array.isArray(options.convertToBoolean)) { - if (options.convertToBoolean.includes(key)) { - return true; - } - } else if (options.convertToBoolean === key) { - return true; - } - if (typeof options.convertToBooleanPredicate === 'function' && options.convertToBooleanPredicate(key)) { - return true; - } - } - return false; - } - Object.entries(process.env).forEach(([key, value]) => { - if (convertPredicate(key)) { - const boolValue = yn(value) ? 'true' : ''; - process.env[key] = boolValue; - log('Environment variable converted into string boolean value:', key, "original:", value, 'newValue:', boolValue); - } - }); - } -} +export * from './dotenvMulti' +export * from './types'; diff --git a/src/lazy.ts b/src/lazy.ts new file mode 100644 index 0000000..ed5cb24 --- /dev/null +++ b/src/lazy.ts @@ -0,0 +1,12 @@ +export class Lazy { + private instance: T | undefined = undefined; + + constructor(private readonly factory: () => any) {} + + get (): T { + if (!this.instance) { + this.instance = this.factory(); + } + return this.instance as T; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9104026 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,9 @@ +export interface ConfigOptions { + convertToBooleanPredicate?: (variable: string) => boolean; + convertToBoolean?: string | string[]; + debug?: boolean; +} + +export interface BooleanConverter { + (options: ConfigOptions, log: (...args: any[]) => void): void; +} \ No newline at end of file