1/**
2 * Copyright © 2022 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 { Graph, MixedOutput, Module, SerializerOptions } from 'metro';
8import countLines from 'metro/src/lib/countLines';
9
10import { SerializerParameters } from './withExpoSerializers';
11
12const debug = require('debug')('expo:metro-config:serializer:env-var') as typeof console.log;
13
14export function replaceEnvironmentVariables(
15  code: string,
16  env: Record<string, string | undefined>
17): string {
18  // match and replace env variables that aren't NODE_ENV or JEST_WORKER_ID
19  // return code.match(/process\.env\.(EXPO_PUBLIC_[A-Z_]+)/g);
20  return code.replace(/process\.env\.([a-zA-Z0-9_]+)/gm, (match) => {
21    const name = match.replace('process.env.', '');
22    if (
23      // Must start with EXPO_PUBLIC_ to be replaced
24      !/^EXPO_PUBLIC_/.test(name)
25    ) {
26      return match;
27    }
28
29    const value = JSON.stringify(env[name] ?? '');
30    debug(`Inlining environment variable "${match}" with ${value}`);
31    return value;
32  });
33}
34
35export function getTransformEnvironment(url: string): string | null {
36  const match = url.match(/[&?]transform\.environment=([^&]+)/);
37  return match ? match[1] : null;
38}
39
40function getAllExpoPublicEnvVars() {
41  // Create an object containing all environment variables that start with EXPO_PUBLIC_
42  const env = {};
43  for (const key in process.env) {
44    if (key.startsWith('EXPO_PUBLIC_')) {
45      // @ts-ignore
46      env[key] = process.env[key];
47    }
48  }
49  return env;
50}
51
52export function environmentVariableSerializerPlugin(
53  entryPoint: string,
54  preModules: readonly Module[],
55  graph: Graph,
56  options: SerializerOptions
57): SerializerParameters {
58  // Skip replacement in Node.js environments.
59  if (options.sourceUrl && getTransformEnvironment(options.sourceUrl) === 'node') {
60    debug('Skipping environment variable inlining in Node.js environment.');
61    return [entryPoint, preModules, graph, options];
62  }
63
64  // Adds about 5ms on a blank Expo Router app.
65  // TODO: We can probably cache the results.
66
67  // In development, we need to add the process.env object to ensure it
68  // persists between Fast Refresh updates.
69  if (options.dev) {
70    // Set the process.env object to the current environment variables object
71    // ensuring they aren't iterable, settable, or enumerable.
72    const str = `process.env=Object.defineProperties(process.env, {${Object.keys(
73      getAllExpoPublicEnvVars()
74    )
75      .map((key) => `${JSON.stringify(key)}: { value: ${JSON.stringify(process.env[key])} }`)
76      .join(',')}});`;
77
78    const [firstModule, ...restModules] = preModules;
79    // const envCode = `var process=this.process||{};${str}`;
80    // process.env
81    return [
82      entryPoint,
83      [
84        // First module defines the process.env object.
85        firstModule,
86        // Second module modifies the process.env object.
87        getEnvPrelude(str),
88        // Now we add the rest
89        ...restModules,
90      ],
91      graph,
92      options,
93    ];
94  }
95
96  // In production, inline all process.env variables to ensure they cannot be iterated and read arbitrarily.
97  for (const value of graph.dependencies.values()) {
98    // Skip node_modules, the feature is a bit too sensitive to allow in arbitrary code.
99    if (/node_modules/.test(value.path)) {
100      continue;
101    }
102
103    for (const index in value.output) {
104      // TODO: This probably breaks source maps.
105      const code = replaceEnvironmentVariables(value.output[index].data.code, process.env);
106      value.output[index].data.code = code;
107    }
108  }
109  return [entryPoint, preModules, graph, options];
110}
111
112function getEnvPrelude(contents: string): Module<MixedOutput> {
113  const code = '// HMR env vars from Expo CLI (dev-only)\n' + contents;
114  const name = '__env__';
115
116  return {
117    dependencies: new Map(),
118    getSource: (): Buffer => Buffer.from(code),
119    inverseDependencies: new Set(),
120    path: name,
121    output: [
122      {
123        type: 'js/script/virtual',
124        data: {
125          code,
126          lineCount: countLines(code),
127          map: [],
128        },
129      },
130    ],
131  };
132}
133