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