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