// Based on https://github.com/grimen/node-env-file with various improvements to // remove global state and conversion to TypeScript. import { existsSync, readFileSync } from 'fs-extra'; import { resolve } from 'path'; export interface Options { verbose?: boolean; overwrite?: boolean; raise?: boolean; logger?: typeof console; allowDuplicateKeys?: boolean; } export const parseEnvFile = (env_file: string, options: Options = {}) => { options = { logger: console, overwrite: false, raise: true, verbose: false, allowDuplicateKeys: false, ...options, }; if (typeof env_file !== 'string') { if (options.raise) { throw new TypeError(`Environment file argument is not a valid \`String\`: ${env_file}`); } else { if (options.verbose && options.logger) { options.logger.error('[ENV]: ERROR Environment file argument is not a valid `String`:', env_file); } return {}; } } try { env_file = resolve(env_file); } catch (error) { if (options.raise) { throw new TypeError(`Environment file path could not be resolved: ${error}`); } else { if (options.verbose && options.logger) { options.logger.error('[ENV]: ERROR Environment file path could not be resolved:', env_file); } return {}; } } const data: Record> = {}; data[env_file] = {}; if (options.verbose && options.logger) { options.logger.info('[ENV]: Loading environment:', env_file); } if (existsSync(env_file)) { let lines: string[] = []; try { lines = (readFileSync(env_file, 'utf8') || '') .split(/\r?\n|\r/) .filter((line) => { return /\s*=\s*/i.test(line); }) .map((line) => { return line.replace('exports ', ''); }); } catch (error) { if (options.raise) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied (error as any).message = `Environment file could not be read: ${env_file}: ${(error as any).message}`; throw error; } else { if (options.verbose && options.logger) { options.logger.error('[ENV]: ERROR Environment file could not be read:', env_file); } return {}; } } const lineComments: string[] = []; const lineVariables: string[] = []; let is_comment = false; for (const line of lines) { is_comment = /^\s*#/i.test(line); // ignore comment lines (starting with #). if (is_comment) { lineComments.push(line); if (options.verbose && options.logger) { options.logger.info('[ENV]: Ignored line:', line); } } else { lineVariables.push(line); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const key_value = line.match(/^([^=]+)\s*=\s*(.*)$/) as any; const env_key = key_value[1]; // remove ' and " characters if right side of = is quoted const env_value = key_value[2].match(/^(['"]?)([^\n]*)\1$/m)[2]; if ((env_key in data[env_file]) && !options.allowDuplicateKeys) throw new Error(`Found duplicate key: ${env_key}`); if (options.overwrite) { data[env_file][env_key] = env_value; if (options.verbose && options.logger && data[env_file][env_key] !== env_value) { options.logger.info('[ENV]: Overwritten ', data[env_file][env_key], ' => ', env_value); } } else { data[env_file][env_key] = process.env[env_key] || env_value; } if (options.verbose && options.logger) { options.logger.info('[ENV]:', data[env_file]); } } } } else { if (options.raise) { throw new TypeError(`Environment file doesn't exist: ${env_file}`); } else { if (options.verbose && options.logger) { options.logger.error('[ENV]: ERROR Environment file path could not be resolved:', env_file); } return {}; } } return data[env_file]; };