xref: /expo/packages/@expo/env/src/env.ts (revision e30a2315)
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