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