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