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