xref: /expo/packages/@expo/env/src/env.ts (revision adae5ee7)
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 memoEnvironment: NodeJS.ProcessEnv | undefined = undefined;
30
31  function _getForce(
32    projectRoot: string,
33    options: LoadOptions = {}
34  ): Record<string, string | undefined> {
35    if (!isEnabled()) {
36      debug(`Skipping .env files because EXPO_NO_DOTENV is defined`);
37      return {};
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 parsed;
102  }
103
104  /** Get the environment variables without mutating the environment. This returns memoized values unless the `force` property is provided. */
105  function get(projectRoot: string, options: LoadOptions = {}): Record<string, string | undefined> {
106    if (!isEnabled()) {
107      debug(`Skipping .env files because EXPO_NO_DOTENV is defined`);
108      return {};
109    }
110    if (!options.force && memoEnvironment) {
111      return memoEnvironment;
112    }
113    memoEnvironment = _getForce(projectRoot, options);
114    return memoEnvironment;
115  }
116
117  /** Load environment variables from .env files and mutate the current `process.env` with the results. */
118  function load(projectRoot: string, options: LoadOptions = {}) {
119    if (!isEnabled()) {
120      debug(`Skipping .env files because EXPO_NO_DOTENV is defined`);
121      return process.env;
122    }
123
124    const env = get(projectRoot, options);
125    process.env = { ...process.env, ...env };
126    return process.env;
127  }
128
129  return {
130    load,
131    get,
132    _getForce,
133  };
134}
135
136export function getFiles(
137  mode: string | undefined,
138  { silent = false }: Pick<LoadOptions, 'silent'> = {}
139): string[] {
140  if (!isEnabled()) {
141    debug(`Skipping .env files because EXPO_NO_DOTENV is defined`);
142    return [];
143  }
144
145  if (!mode) {
146    if (silent) {
147      debug('NODE_ENV is not defined, proceeding without mode-specific .env');
148    } else {
149      console.error(
150        chalk.red(
151          'The NODE_ENV environment variable is required but was not specified. Ensure the project is bundled with Expo CLI or NODE_ENV is set.'
152        )
153      );
154      console.error(chalk.red('Proceeding without mode-specific .env'));
155    }
156  }
157
158  if (mode && !['development', 'test', 'production'].includes(mode)) {
159    throw new Error(
160      `Environment variable "NODE_ENV=${mode}" is invalid. Valid values are "development", "test", and "production`
161    );
162  }
163
164  if (!mode) {
165    // Support environments that don't respect NODE_ENV
166    return [`.env.local`, '.env'];
167  }
168  // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
169  const dotenvFiles = [
170    `.env.${mode}.local`,
171    // Don't include `.env.local` for `test` environment
172    // since normally you expect tests to produce the same
173    // results for everyone
174    mode !== 'test' && `.env.local`,
175    `.env.${mode}`,
176    '.env',
177  ].filter(Boolean) as string[];
178
179  return dotenvFiles;
180}
181