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 53export function environmentVariableSerializerPlugin( 54 entryPoint: string, 55 preModules: readonly Module<MixedOutput>[], 56 graph: ReadOnlyGraph, 57 options: SerializerOptions 58): SerializerParameters { 59 // Skip replacement in Node.js environments. 60 if (options.sourceUrl && getTransformEnvironment(options.sourceUrl) === 'node') { 61 debug('Skipping environment variable inlining in Node.js environment.'); 62 return [entryPoint, preModules, graph, options]; 63 } 64 65 // Adds about 5ms on a blank Expo Router app. 66 // TODO: We can probably cache the results. 67 68 // In development, we need to add the process.env object to ensure it 69 // persists between Fast Refresh updates. 70 if (options.dev) { 71 // Set the process.env object to the current environment variables object 72 // ensuring they aren't iterable, settable, or enumerable. 73 const str = `process.env=Object.defineProperties(process.env, {${Object.keys( 74 getAllExpoPublicEnvVars() 75 ) 76 .map((key) => `${JSON.stringify(key)}: { value: ${JSON.stringify(process.env[key])} }`) 77 .join(',')}});`; 78 79 const [firstModule, ...restModules] = preModules; 80 // const envCode = `var process=this.process||{};${str}`; 81 // process.env 82 return [ 83 entryPoint, 84 [ 85 // First module defines the process.env object. 86 firstModule, 87 // Second module modifies the process.env object. 88 getEnvPrelude(str), 89 // Now we add the rest 90 ...restModules, 91 ], 92 graph, 93 options, 94 ]; 95 } 96 97 // In production, inline all process.env variables to ensure they cannot be iterated and read arbitrarily. 98 for (const value of graph.dependencies.values()) { 99 // Skip node_modules, the feature is a bit too sensitive to allow in arbitrary code. 100 if (/node_modules/.test(value.path)) { 101 continue; 102 } 103 104 for (const index in value.output) { 105 // TODO: This probably breaks source maps. 106 const code = replaceEnvironmentVariables(value.output[index].data.code, process.env); 107 value.output[index].data.code = code; 108 } 109 } 110 return [entryPoint, preModules, graph, options]; 111} 112 113function getEnvPrelude(contents: string): Module<MixedOutput> { 114 const code = '// HMR env vars from Expo CLI (dev-only)\n' + contents; 115 const name = '__env__'; 116 const lineCount = countLines(code); 117 118 return { 119 dependencies: new Map(), 120 getSource: (): Buffer => Buffer.from(code), 121 inverseDependencies: new CountingSet(), 122 path: name, 123 output: [ 124 { 125 type: 'js/script/virtual', 126 data: { 127 code, 128 // @ts-expect-error: typed incorrectly upstream 129 lineCount, 130 map: [], 131 }, 132 }, 133 ], 134 }; 135} 136