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