16a750d06SEvan Bacon/** 26a750d06SEvan Bacon * Copyright © 2023 650 Industries. 36a750d06SEvan Bacon * 46a750d06SEvan Bacon * This source code is licensed under the MIT license found in the 56a750d06SEvan Bacon * LICENSE file in the root directory of this source tree. 66a750d06SEvan Bacon */ 7f5917927SEvan Baconimport chalk from 'chalk'; 86a750d06SEvan Baconimport * as dotenv from 'dotenv'; 96a750d06SEvan Baconimport { expand } from 'dotenv-expand'; 106a750d06SEvan Baconimport * as fs from 'fs'; 1178a6243aSEvan Baconimport { boolish } from 'getenv'; 126a750d06SEvan Baconimport * as path from 'path'; 136a750d06SEvan Bacon 1478a6243aSEvan Bacontype LoadOptions = { 1578a6243aSEvan Bacon silent?: boolean; 1678a6243aSEvan Bacon force?: boolean; 1778a6243aSEvan Bacon}; 1878a6243aSEvan Bacon 196a750d06SEvan Baconconst debug = require('debug')('expo:env') as typeof console.log; 206a750d06SEvan Bacon 2178a6243aSEvan Baconexport function isEnabled(): boolean { 2278a6243aSEvan Bacon return !boolish('EXPO_NO_DOTENV', false); 2378a6243aSEvan Bacon} 2478a6243aSEvan Bacon 256a750d06SEvan Baconexport function createControlledEnvironment() { 266a750d06SEvan Bacon const IS_DEBUG = require('debug').enabled('expo:env'); 276a750d06SEvan Bacon 286a750d06SEvan Bacon let userDefinedEnvironment: NodeJS.ProcessEnv | undefined = undefined; 29*e30a2315SEvan Bacon let memo: { env: NodeJS.ProcessEnv; files: string[] } | undefined = undefined; 306a750d06SEvan Bacon 3178a6243aSEvan Bacon function _getForce( 3278a6243aSEvan Bacon projectRoot: string, 3378a6243aSEvan Bacon options: LoadOptions = {} 34*e30a2315SEvan Bacon ): { env: Record<string, string | undefined>; files: string[] } { 3578a6243aSEvan Bacon if (!isEnabled()) { 3678a6243aSEvan Bacon debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 37*e30a2315SEvan Bacon return { env: {}, files: [] }; 3878a6243aSEvan Bacon } 3978a6243aSEvan Bacon 406a750d06SEvan Bacon if (!userDefinedEnvironment) { 416a750d06SEvan Bacon userDefinedEnvironment = { ...process.env }; 426a750d06SEvan Bacon } 436a750d06SEvan Bacon 446a750d06SEvan Bacon // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 4578a6243aSEvan Bacon const dotenvFiles = getFiles(process.env.NODE_ENV, options); 466a750d06SEvan Bacon 476a750d06SEvan Bacon const loadedEnvFiles: string[] = []; 486a750d06SEvan Bacon const parsed: dotenv.DotenvParseOutput = {}; 496a750d06SEvan Bacon 506a750d06SEvan Bacon // Load environment variables from .env* files. Suppress warnings using silent 516a750d06SEvan Bacon // if this file is missing. dotenv will never modify any environment variables 526a750d06SEvan Bacon // that have already been set. Variable expansion is supported in .env files. 536a750d06SEvan Bacon // https://github.com/motdotla/dotenv 546a750d06SEvan Bacon // https://github.com/motdotla/dotenv-expand 556a750d06SEvan Bacon dotenvFiles.forEach((dotenvFile) => { 566a750d06SEvan Bacon const absoluteDotenvFile = path.resolve(projectRoot, dotenvFile); 576a750d06SEvan Bacon if (!fs.existsSync(absoluteDotenvFile)) { 586a750d06SEvan Bacon return; 596a750d06SEvan Bacon } 606a750d06SEvan Bacon try { 616a750d06SEvan Bacon const results = expand( 626a750d06SEvan Bacon dotenv.config({ 636a750d06SEvan Bacon debug: IS_DEBUG, 646a750d06SEvan Bacon path: absoluteDotenvFile, 656a750d06SEvan Bacon // We will handle overriding ourselves to allow for HMR. 666a750d06SEvan Bacon override: true, 676a750d06SEvan Bacon }) 686a750d06SEvan Bacon ); 696a750d06SEvan Bacon if (results.parsed) { 706a750d06SEvan Bacon loadedEnvFiles.push(absoluteDotenvFile); 716a750d06SEvan Bacon debug(`Loaded environment variables from: ${absoluteDotenvFile}`); 726a750d06SEvan Bacon 736a750d06SEvan Bacon for (const key of Object.keys(results.parsed || {})) { 746a750d06SEvan Bacon if ( 756a750d06SEvan Bacon typeof parsed[key] === 'undefined' && 766a750d06SEvan Bacon // Custom override logic to prevent overriding variables that 776a750d06SEvan Bacon // were set before the CLI process began. 786a750d06SEvan Bacon typeof userDefinedEnvironment?.[key] === 'undefined' 796a750d06SEvan Bacon ) { 806a750d06SEvan Bacon parsed[key] = results.parsed[key]; 816a750d06SEvan Bacon } 826a750d06SEvan Bacon } 836a750d06SEvan Bacon } else { 846a750d06SEvan Bacon debug(`Failed to load environment variables from: ${absoluteDotenvFile}`); 856a750d06SEvan Bacon } 866a750d06SEvan Bacon } catch (error: unknown) { 876a750d06SEvan Bacon if (error instanceof Error) { 886a750d06SEvan Bacon console.error( 896a750d06SEvan Bacon `Failed to load environment variables from ${absoluteDotenvFile}: ${error.message}` 906a750d06SEvan Bacon ); 916a750d06SEvan Bacon } else { 926a750d06SEvan Bacon throw error; 936a750d06SEvan Bacon } 946a750d06SEvan Bacon } 956a750d06SEvan Bacon }); 966a750d06SEvan Bacon 976a750d06SEvan Bacon if (!loadedEnvFiles.length) { 986a750d06SEvan Bacon debug(`No environment variables loaded from .env files.`); 996a750d06SEvan Bacon } 1006a750d06SEvan Bacon 101*e30a2315SEvan Bacon return { env: parsed, files: loadedEnvFiles }; 1026a750d06SEvan Bacon } 1036a750d06SEvan Bacon 1046a750d06SEvan Bacon /** Get the environment variables without mutating the environment. This returns memoized values unless the `force` property is provided. */ 105*e30a2315SEvan Bacon function get( 106*e30a2315SEvan Bacon projectRoot: string, 107*e30a2315SEvan Bacon options: LoadOptions = {} 108*e30a2315SEvan Bacon ): { env: Record<string, string | undefined>; files: string[] } { 10978a6243aSEvan Bacon if (!isEnabled()) { 11078a6243aSEvan Bacon debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 111*e30a2315SEvan Bacon return { env: {}, files: [] }; 11278a6243aSEvan Bacon } 113*e30a2315SEvan Bacon if (!options.force && memo) { 114*e30a2315SEvan Bacon return memo; 1156a750d06SEvan Bacon } 116*e30a2315SEvan Bacon memo = _getForce(projectRoot, options); 117*e30a2315SEvan Bacon return memo; 1186a750d06SEvan Bacon } 1196a750d06SEvan Bacon 1206a750d06SEvan Bacon /** Load environment variables from .env files and mutate the current `process.env` with the results. */ 12178a6243aSEvan Bacon function load(projectRoot: string, options: LoadOptions = {}) { 12278a6243aSEvan Bacon if (!isEnabled()) { 12378a6243aSEvan Bacon debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 12478a6243aSEvan Bacon return process.env; 12578a6243aSEvan Bacon } 12678a6243aSEvan Bacon 127*e30a2315SEvan Bacon const envInfo = get(projectRoot, options); 128*e30a2315SEvan Bacon 129*e30a2315SEvan Bacon if (!options.force) { 130*e30a2315SEvan Bacon const keys = Object.keys(envInfo.env); 131*e30a2315SEvan Bacon if (keys.length) { 132*e30a2315SEvan Bacon console.log( 133*e30a2315SEvan Bacon chalk.gray('env: load', envInfo.files.map((file) => path.basename(file)).join(' ')) 134*e30a2315SEvan Bacon ); 135*e30a2315SEvan Bacon console.log(chalk.gray('env: export', keys.join(' '))); 136*e30a2315SEvan Bacon } 137*e30a2315SEvan Bacon } 138*e30a2315SEvan Bacon 139*e30a2315SEvan Bacon process.env = { ...process.env, ...envInfo.env }; 1406a750d06SEvan Bacon return process.env; 1416a750d06SEvan Bacon } 1426a750d06SEvan Bacon 1436a750d06SEvan Bacon return { 1446a750d06SEvan Bacon load, 1456a750d06SEvan Bacon get, 1466a750d06SEvan Bacon _getForce, 1476a750d06SEvan Bacon }; 1486a750d06SEvan Bacon} 1496a750d06SEvan Bacon 15078a6243aSEvan Baconexport function getFiles( 15178a6243aSEvan Bacon mode: string | undefined, 15278a6243aSEvan Bacon { silent = false }: Pick<LoadOptions, 'silent'> = {} 15378a6243aSEvan Bacon): string[] { 15478a6243aSEvan Bacon if (!isEnabled()) { 15578a6243aSEvan Bacon debug(`Skipping .env files because EXPO_NO_DOTENV is defined`); 15678a6243aSEvan Bacon return []; 15778a6243aSEvan Bacon } 15878a6243aSEvan Bacon 1596a750d06SEvan Bacon if (!mode) { 16078a6243aSEvan Bacon if (silent) { 16178a6243aSEvan Bacon debug('NODE_ENV is not defined, proceeding without mode-specific .env'); 16278a6243aSEvan Bacon } else { 163f5917927SEvan Bacon console.error( 164f5917927SEvan Bacon chalk.red( 165f5917927SEvan Bacon 'The NODE_ENV environment variable is required but was not specified. Ensure the project is bundled with Expo CLI or NODE_ENV is set.' 166f5917927SEvan Bacon ) 1676a750d06SEvan Bacon ); 168f5917927SEvan Bacon console.error(chalk.red('Proceeding without mode-specific .env')); 1696a750d06SEvan Bacon } 17078a6243aSEvan Bacon } 1716a750d06SEvan Bacon 172f5917927SEvan Bacon if (mode && !['development', 'test', 'production'].includes(mode)) { 1736a750d06SEvan Bacon throw new Error( 1746a750d06SEvan Bacon `Environment variable "NODE_ENV=${mode}" is invalid. Valid values are "development", "test", and "production` 1756a750d06SEvan Bacon ); 1766a750d06SEvan Bacon } 1776a750d06SEvan Bacon 178f5917927SEvan Bacon if (!mode) { 179f5917927SEvan Bacon // Support environments that don't respect NODE_ENV 180f5917927SEvan Bacon return [`.env.local`, '.env']; 181f5917927SEvan Bacon } 1826a750d06SEvan Bacon // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 1836a750d06SEvan Bacon const dotenvFiles = [ 1846a750d06SEvan Bacon `.env.${mode}.local`, 1856a750d06SEvan Bacon // Don't include `.env.local` for `test` environment 1866a750d06SEvan Bacon // since normally you expect tests to produce the same 1876a750d06SEvan Bacon // results for everyone 1886a750d06SEvan Bacon mode !== 'test' && `.env.local`, 1896a750d06SEvan Bacon `.env.${mode}`, 1906a750d06SEvan Bacon '.env', 1916a750d06SEvan Bacon ].filter(Boolean) as string[]; 1926a750d06SEvan Bacon 1936a750d06SEvan Bacon return dotenvFiles; 1946a750d06SEvan Bacon} 195