1import JsonFile from '@expo/json-file'; 2import chalk from 'chalk'; 3import fs from 'fs-extra'; 4import path from 'path'; 5 6import { Directories } from '../expotools'; 7import AndroidMacrosGenerator from './AndroidMacrosGenerator'; 8import IosMacrosGenerator from './IosMacrosGenerator'; 9import macros from './macros'; 10 11const EXPO_DIR = Directories.getExpoRepositoryRootDir(); 12 13type TemplateSubstitutions = { 14 [key: string]: string; 15}; 16 17async function getTemplateSubstitutionsFromSecrets(): Promise<TemplateSubstitutions> { 18 try { 19 return await new JsonFile<TemplateSubstitutions>( 20 path.join(EXPO_DIR, 'secrets', 'keys.json') 21 ).readAsync(); 22 } catch { 23 // Don't have access to decrypted secrets, use public keys 24 console.log( 25 "You don't have access to decrypted secrets. Falling back to `template-files/keys.json`." 26 ); 27 return await new JsonFile<TemplateSubstitutions>( 28 path.join(EXPO_DIR, 'template-files', 'keys.json') 29 ).readAsync(); 30 } 31} 32 33async function getTemplateSubstitutionsAsync() { 34 const defaultKeys = await getTemplateSubstitutionsFromSecrets(); 35 36 try { 37 // Keys from secrets/template-files can be overwritten by private-keys.json file. 38 const privateKeys = await new JsonFile(path.join(EXPO_DIR, 'private-keys.json')).readAsync(); 39 return { ...defaultKeys, ...privateKeys }; 40 } catch { 41 return defaultKeys; 42 } 43} 44 45async function generateMacrosAsync(platform, configuration) { 46 const macrosObject = {}; 47 48 console.log('Resolving macros...'); 49 50 for (const [name, func] of Object.entries(macros)) { 51 // @ts-ignore 52 const macroValue = await func.call(macros, platform, configuration); 53 54 macrosObject[name] = macroValue; 55 56 console.log( 57 'Resolved %s macro to %s', 58 chalk.green(name), 59 chalk.yellow(JSON.stringify(macroValue)) 60 ); 61 } 62 console.log(); 63 return macrosObject; 64} 65 66function getMacrosGeneratorForPlatform(platform) { 67 if (platform === 'ios') { 68 return new IosMacrosGenerator(); 69 } 70 if (platform === 'android') { 71 return new AndroidMacrosGenerator(); 72 } 73 throw new Error(`Platform '${platform}' is not supported.`); 74} 75 76function getSkippedTemplates(isBare: boolean): string[] { 77 if (isBare) { 78 return ['AndroidManifest.xml', 'google-services.json']; 79 } 80 81 return []; 82} 83 84async function generateDynamicMacrosAsync(args) { 85 try { 86 const { platform, bareExpo } = args; 87 const templateSubstitutions = await getTemplateSubstitutionsAsync(); 88 89 if (!bareExpo) { 90 const macros = await generateMacrosAsync(platform, args.configuration); 91 const macrosGenerator = getMacrosGeneratorForPlatform(platform); 92 await macrosGenerator.generateAsync({ ...args, macros, templateSubstitutions }); 93 } 94 // Copy template files - it is platform-agnostic. 95 await copyTemplateFilesAsync( 96 platform, 97 { ...args, skipTemplates: getSkippedTemplates(bareExpo) }, 98 templateSubstitutions 99 ); 100 } catch (error) { 101 console.error( 102 `There was an error while generating Expo template files, which could lead to unexpected behavior at runtime:\n${error.stack}` 103 ); 104 process.exit(1); 105 } 106} 107 108async function readExistingSourceAsync(filepath): Promise<string | null> { 109 try { 110 return await fs.readFile(filepath, 'utf8'); 111 } catch { 112 return null; 113 } 114} 115 116async function copyTemplateFileAsync( 117 source: string, 118 dest: string, 119 templateSubstitutions: TemplateSubstitutions, 120 configuration, 121 isOptional: boolean 122): Promise<void> { 123 let [currentSourceFile, currentDestFile] = await Promise.all([ 124 readExistingSourceAsync(source), 125 readExistingSourceAsync(dest), 126 ]); 127 128 if (!currentSourceFile) { 129 console.error(`Couldn't find ${chalk.magenta(source)} file.`); 130 process.exit(1); 131 } 132 133 for (const [textToReplace, value] of Object.entries(templateSubstitutions)) { 134 currentSourceFile = currentSourceFile.replace( 135 new RegExp(`\\$\\{${textToReplace}\\}`, 'g'), 136 value 137 ); 138 } 139 140 if (configuration === 'debug') { 141 // We need these permissions when testing but don't want them 142 // ending up in our release. 143 currentSourceFile = currentSourceFile.replace( 144 `<!-- ADD TEST PERMISSIONS HERE -->`, 145 `<uses-permission android:name="android.permission.WRITE_CONTACTS" />` 146 ); 147 } 148 149 if (currentSourceFile !== currentDestFile) { 150 try { 151 await fs.writeFile(dest, currentSourceFile, 'utf8'); 152 } catch (error) { 153 if (!isOptional) throw error; 154 } 155 } 156} 157 158type TemplatePaths = Record<string, string>; 159type TemplatePathsFile = { 160 paths: TemplatePaths; 161 generateOnly: TemplatePaths; 162}; 163 164async function copyTemplateFilesAsync(platform: string, args: any, templateSubstitutions: any) { 165 const templateFilesPath = args.templateFilesPath || path.join(EXPO_DIR, 'template-files'); 166 const templatePathsFile = (await new JsonFile( 167 path.join(templateFilesPath, `${platform}-paths.json`) 168 ).readAsync()) as TemplatePathsFile; 169 const templatePaths = { ...templatePathsFile.paths, ...templatePathsFile.generateOnly }; 170 const checkIgnoredTemplatePaths = Object.values(templatePathsFile.generateOnly); 171 const promises: Promise<any>[] = []; 172 const skipTemplates: string[] = args.skipTemplates || []; 173 for (const [source, dest] of Object.entries(templatePaths)) { 174 if (skipTemplates.includes(source)) { 175 console.log( 176 'Skipping template %s ...', 177 chalk.cyan(path.join(templateFilesPath, platform, source)) 178 ); 179 continue; 180 } 181 182 const isOptional = checkIgnoredTemplatePaths.includes(dest); 183 console.log( 184 'Rendering %s from template %s %s...', 185 chalk.cyan(path.join(EXPO_DIR, dest)), 186 chalk.cyan(path.join(templateFilesPath, platform, source)), 187 isOptional ? chalk.yellow('(Optional) ') : '' 188 ); 189 190 promises.push( 191 copyTemplateFileAsync( 192 path.join(templateFilesPath, platform, source), 193 path.join(EXPO_DIR, dest), 194 templateSubstitutions, 195 args.configuration, 196 isOptional 197 ) 198 ); 199 } 200 201 await Promise.all(promises); 202} 203 204export { generateDynamicMacrosAsync, getTemplateSubstitutionsAsync }; 205