1/** 2 * Copyright © 2023 650 Industries. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7import chalk from 'chalk'; 8import * as dotenv from 'dotenv'; 9import { expand } from 'dotenv-expand'; 10import * as fs from 'fs'; 11import { boolish } from 'getenv'; 12import * as path from 'path'; 13 14type LoadOptions = { 15 silent?: boolean; 16 force?: boolean; 17}; 18 19const debug = require('debug')('expo:env') as typeof console.log; 20 21export function isEnabled(): boolean { 22 return !boolish('EXPO_NO_DOTENV', false); 23} 24 25export function createControlledEnvironment() { 26 const IS_DEBUG = require('debug').enabled('expo:env'); 27 28 let userDefinedEnvironment: NodeJS.ProcessEnv | undefined = undefined; 29 let memoEnvironment: NodeJS.ProcessEnv | undefined = undefined; 30 31 function _getForce( 32 projectRoot: string, 33 options: LoadOptions = {} 34 ): Record<string, string | undefined> { 35 if (!isEnabled()) { 36 debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 37 return {}; 38 } 39 40 if (!userDefinedEnvironment) { 41 userDefinedEnvironment = { ...process.env }; 42 } 43 44 // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 45 const dotenvFiles = getFiles(process.env.NODE_ENV, options); 46 47 const loadedEnvFiles: string[] = []; 48 const parsed: dotenv.DotenvParseOutput = {}; 49 50 // Load environment variables from .env* files. Suppress warnings using silent 51 // if this file is missing. dotenv will never modify any environment variables 52 // that have already been set. Variable expansion is supported in .env files. 53 // https://github.com/motdotla/dotenv 54 // https://github.com/motdotla/dotenv-expand 55 dotenvFiles.forEach((dotenvFile) => { 56 const absoluteDotenvFile = path.resolve(projectRoot, dotenvFile); 57 if (!fs.existsSync(absoluteDotenvFile)) { 58 return; 59 } 60 try { 61 const results = expand( 62 dotenv.config({ 63 debug: IS_DEBUG, 64 path: absoluteDotenvFile, 65 // We will handle overriding ourselves to allow for HMR. 66 override: true, 67 }) 68 ); 69 if (results.parsed) { 70 loadedEnvFiles.push(absoluteDotenvFile); 71 debug(`Loaded environment variables from: ${absoluteDotenvFile}`); 72 73 for (const key of Object.keys(results.parsed || {})) { 74 if ( 75 typeof parsed[key] === 'undefined' && 76 // Custom override logic to prevent overriding variables that 77 // were set before the CLI process began. 78 typeof userDefinedEnvironment?.[key] === 'undefined' 79 ) { 80 parsed[key] = results.parsed[key]; 81 } 82 } 83 } else { 84 debug(`Failed to load environment variables from: ${absoluteDotenvFile}`); 85 } 86 } catch (error: unknown) { 87 if (error instanceof Error) { 88 console.error( 89 `Failed to load environment variables from ${absoluteDotenvFile}: ${error.message}` 90 ); 91 } else { 92 throw error; 93 } 94 } 95 }); 96 97 if (!loadedEnvFiles.length) { 98 debug(`No environment variables loaded from .env files.`); 99 } 100 101 return parsed; 102 } 103 104 /** Get the environment variables without mutating the environment. This returns memoized values unless the `force` property is provided. */ 105 function get(projectRoot: string, options: LoadOptions = {}): Record<string, string | undefined> { 106 if (!isEnabled()) { 107 debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 108 return {}; 109 } 110 if (!options.force && memoEnvironment) { 111 return memoEnvironment; 112 } 113 memoEnvironment = _getForce(projectRoot, options); 114 return memoEnvironment; 115 } 116 117 /** Load environment variables from .env files and mutate the current `process.env` with the results. */ 118 function load(projectRoot: string, options: LoadOptions = {}) { 119 if (!isEnabled()) { 120 debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 121 return process.env; 122 } 123 124 const env = get(projectRoot, options); 125 process.env = { ...process.env, ...env }; 126 return process.env; 127 } 128 129 return { 130 load, 131 get, 132 _getForce, 133 }; 134} 135 136export function getFiles( 137 mode: string | undefined, 138 { silent = false }: Pick<LoadOptions, 'silent'> = {} 139): string[] { 140 if (!isEnabled()) { 141 debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 142 return []; 143 } 144 145 if (!mode) { 146 if (silent) { 147 debug('NODE_ENV is not defined, proceeding without mode-specific .env'); 148 } else { 149 console.error( 150 chalk.red( 151 'The NODE_ENV environment variable is required but was not specified. Ensure the project is bundled with Expo CLI or NODE_ENV is set.' 152 ) 153 ); 154 console.error(chalk.red('Proceeding without mode-specific .env')); 155 } 156 } 157 158 if (mode && !['development', 'test', 'production'].includes(mode)) { 159 throw new Error( 160 `Environment variable "NODE_ENV=${mode}" is invalid. Valid values are "development", "test", and "production` 161 ); 162 } 163 164 if (!mode) { 165 // Support environments that don't respect NODE_ENV 166 return [`.env.local`, '.env']; 167 } 168 // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 169 const dotenvFiles = [ 170 `.env.${mode}.local`, 171 // Don't include `.env.local` for `test` environment 172 // since normally you expect tests to produce the same 173 // results for everyone 174 mode !== 'test' && `.env.local`, 175 `.env.${mode}`, 176 '.env', 177 ].filter(Boolean) as string[]; 178 179 return dotenvFiles; 180} 181