xref: /expo/packages/@expo/env/src/env.ts (revision 4c11f2d9)
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 * as dotenv from 'dotenv';
9import { expand } from 'dotenv-expand';
10import * as fs from 'fs';
11import * as path from 'path';
12
13const debug = require('debug')('expo:env') as typeof console.log;
14
15export function createControlledEnvironment() {
16  const IS_DEBUG = require('debug').enabled('expo:env');
17
18  let userDefinedEnvironment: NodeJS.ProcessEnv | undefined = undefined;
19  let memoEnvironment: NodeJS.ProcessEnv | undefined = undefined;
20
21  function _getForce(projectRoot: string): Record<string, string | undefined> {
22    if (!userDefinedEnvironment) {
23      userDefinedEnvironment = { ...process.env };
24    }
25
26    // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
27    const dotenvFiles = getFiles(process.env.NODE_ENV);
28
29    const loadedEnvFiles: string[] = [];
30    const parsed: dotenv.DotenvParseOutput = {};
31
32    // Load environment variables from .env* files. Suppress warnings using silent
33    // if this file is missing. dotenv will never modify any environment variables
34    // that have already been set. Variable expansion is supported in .env files.
35    // https://github.com/motdotla/dotenv
36    // https://github.com/motdotla/dotenv-expand
37    dotenvFiles.forEach((dotenvFile) => {
38      const absoluteDotenvFile = path.resolve(projectRoot, dotenvFile);
39      if (!fs.existsSync(absoluteDotenvFile)) {
40        return;
41      }
42      try {
43        const results = expand(
44          dotenv.config({
45            debug: IS_DEBUG,
46            path: absoluteDotenvFile,
47            // We will handle overriding ourselves to allow for HMR.
48            override: true,
49          })
50        );
51        if (results.parsed) {
52          loadedEnvFiles.push(absoluteDotenvFile);
53          debug(`Loaded environment variables from: ${absoluteDotenvFile}`);
54
55          for (const key of Object.keys(results.parsed || {})) {
56            if (
57              typeof parsed[key] === 'undefined' &&
58              // Custom override logic to prevent overriding variables that
59              // were set before the CLI process began.
60              typeof userDefinedEnvironment?.[key] === 'undefined'
61            ) {
62              parsed[key] = results.parsed[key];
63            }
64          }
65        } else {
66          debug(`Failed to load environment variables from: ${absoluteDotenvFile}`);
67        }
68      } catch (error: unknown) {
69        if (error instanceof Error) {
70          console.error(
71            `Failed to load environment variables from ${absoluteDotenvFile}: ${error.message}`
72          );
73        } else {
74          throw error;
75        }
76      }
77    });
78
79    if (!loadedEnvFiles.length) {
80      debug(`No environment variables loaded from .env files.`);
81    }
82
83    return parsed;
84  }
85
86  /** Get the environment variables without mutating the environment. This returns memoized values unless the `force` property is provided. */
87  function get(
88    projectRoot: string,
89    { force }: { force?: boolean } = {}
90  ): Record<string, string | undefined> {
91    if (!force && memoEnvironment) {
92      return memoEnvironment;
93    }
94    memoEnvironment = _getForce(projectRoot);
95    return memoEnvironment;
96  }
97
98  /** Load environment variables from .env files and mutate the current `process.env` with the results. */
99  function load(projectRoot: string, { force }: { force?: boolean } = {}) {
100    const env = get(projectRoot, { force });
101    process.env = { ...process.env, ...env };
102    return process.env;
103  }
104
105  return {
106    load,
107    get,
108    _getForce,
109  };
110}
111
112export function getFiles(mode: string | undefined): string[] {
113  if (!mode) {
114    throw new Error(
115      'The NODE_ENV environment variable is required but was not specified. Ensure the project is bundled with Expo CLI.'
116    );
117  }
118
119  if (!mode || !['development', 'test', 'production'].includes(mode)) {
120    throw new Error(
121      `Environment variable "NODE_ENV=${mode}" is invalid. Valid values are "development", "test", and "production`
122    );
123  }
124
125  // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
126  const dotenvFiles = [
127    `.env.${mode}.local`,
128    // Don't include `.env.local` for `test` environment
129    // since normally you expect tests to produce the same
130    // results for everyone
131    mode !== 'test' && `.env.local`,
132    `.env.${mode}`,
133    '.env',
134  ].filter(Boolean) as string[];
135
136  return dotenvFiles;
137}
138