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 memo: { env: NodeJS.ProcessEnv; files: string[] } | undefined = undefined; 30 31 function _getForce( 32 projectRoot: string, 33 options: LoadOptions = {} 34 ): { env: Record<string, string | undefined>; files: string[] } { 35 if (!isEnabled()) { 36 debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 37 return { env: {}, files: [] }; 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 { env: parsed, files: loadedEnvFiles }; 102 } 103 104 /** Get the environment variables without mutating the environment. This returns memoized values unless the `force` property is provided. */ 105 function get( 106 projectRoot: string, 107 options: LoadOptions = {} 108 ): { env: Record<string, string | undefined>; files: string[] } { 109 if (!isEnabled()) { 110 debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 111 return { env: {}, files: [] }; 112 } 113 if (!options.force && memo) { 114 return memo; 115 } 116 memo = _getForce(projectRoot, options); 117 return memo; 118 } 119 120 /** Load environment variables from .env files and mutate the current `process.env` with the results. */ 121 function load(projectRoot: string, options: LoadOptions = {}) { 122 if (!isEnabled()) { 123 debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 124 return process.env; 125 } 126 127 const envInfo = get(projectRoot, options); 128 129 if (!options.force) { 130 const keys = Object.keys(envInfo.env); 131 if (keys.length) { 132 console.log( 133 chalk.gray('env: load', envInfo.files.map((file) => path.basename(file)).join(' ')) 134 ); 135 console.log(chalk.gray('env: export', keys.join(' '))); 136 } 137 } 138 139 process.env = { ...process.env, ...envInfo.env }; 140 return process.env; 141 } 142 143 return { 144 load, 145 get, 146 _getForce, 147 }; 148} 149 150export function getFiles( 151 mode: string | undefined, 152 { silent = false }: Pick<LoadOptions, 'silent'> = {} 153): string[] { 154 if (!isEnabled()) { 155 debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 156 return []; 157 } 158 159 if (!mode) { 160 if (silent) { 161 debug('NODE_ENV is not defined, proceeding without mode-specific .env'); 162 } else { 163 console.error( 164 chalk.red( 165 'The NODE_ENV environment variable is required but was not specified. Ensure the project is bundled with Expo CLI or NODE_ENV is set.' 166 ) 167 ); 168 console.error(chalk.red('Proceeding without mode-specific .env')); 169 } 170 } 171 172 if (mode && !['development', 'test', 'production'].includes(mode)) { 173 throw new Error( 174 `Environment variable "NODE_ENV=${mode}" is invalid. Valid values are "development", "test", and "production` 175 ); 176 } 177 178 if (!mode) { 179 // Support environments that don't respect NODE_ENV 180 return [`.env.local`, '.env']; 181 } 182 // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 183 const dotenvFiles = [ 184 `.env.${mode}.local`, 185 // Don't include `.env.local` for `test` environment 186 // since normally you expect tests to produce the same 187 // results for everyone 188 mode !== 'test' && `.env.local`, 189 `.env.${mode}`, 190 '.env', 191 ].filter(Boolean) as string[]; 192 193 return dotenvFiles; 194} 195