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 (e) { 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 (error) { 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 76async function generateDynamicMacrosAsync(args) { 77 try { 78 const { platform } = args; 79 const templateSubstitutions = await getTemplateSubstitutionsAsync(); 80 81 const macros = await generateMacrosAsync(platform, args.configuration); 82 const macrosGenerator = getMacrosGeneratorForPlatform(platform); 83 84 await macrosGenerator.generateAsync({ ...args, macros, templateSubstitutions }); 85 86 // Copy template files - it is platform-agnostic. 87 await copyTemplateFilesAsync(platform, args, templateSubstitutions); 88 } catch (error) { 89 console.error( 90 `There was an error while generating Expo template files, which could lead to unexpected behavior at runtime:\n${error.stack}` 91 ); 92 process.exit(1); 93 } 94} 95 96async function readExistingSourceAsync(filepath): Promise<string | null> { 97 try { 98 return await fs.readFile(filepath, 'utf8'); 99 } catch (e) { 100 return null; 101 } 102} 103 104async function copyTemplateFileAsync( 105 source: string, 106 dest: string, 107 templateSubstitutions: TemplateSubstitutions, 108 configuration, 109 isOptional: boolean 110): Promise<void> { 111 let [currentSourceFile, currentDestFile] = await Promise.all([ 112 readExistingSourceAsync(source), 113 readExistingSourceAsync(dest), 114 ]); 115 116 if (!currentSourceFile) { 117 console.error(`Couldn't find ${chalk.magenta(source)} file.`); 118 process.exit(1); 119 } 120 121 for (const [textToReplace, value] of Object.entries(templateSubstitutions)) { 122 currentSourceFile = currentSourceFile.replace( 123 new RegExp(`\\$\\{${textToReplace}\\}`, 'g'), 124 value 125 ); 126 } 127 128 if (configuration === 'debug') { 129 // We need these permissions when testing but don't want them 130 // ending up in our release. 131 currentSourceFile = currentSourceFile.replace( 132 `<!-- ADD TEST PERMISSIONS HERE -->`, 133 `<uses-permission android:name="android.permission.WRITE_CONTACTS" />` 134 ); 135 } 136 137 if (currentSourceFile !== currentDestFile) { 138 try { 139 await fs.writeFile(dest, currentSourceFile, 'utf8'); 140 } catch (error) { 141 if (!isOptional) throw error; 142 } 143 } 144} 145 146type TemplatePaths = Record<string, string>; 147type TemplatePathsFile = { 148 paths: TemplatePaths; 149 generateOnly: TemplatePaths; 150}; 151 152async function copyTemplateFilesAsync(platform: string, args: any, templateSubstitutions: any) { 153 const templateFilesPath = args.templateFilesPath || path.join(EXPO_DIR, 'template-files'); 154 const templatePathsFile = (await new JsonFile( 155 path.join(templateFilesPath, `${platform}-paths.json`) 156 ).readAsync()) as TemplatePathsFile; 157 const templatePaths = { ...templatePathsFile.paths, ...templatePathsFile.generateOnly }; 158 const checkIgnoredTemplatePaths = Object.values(templatePathsFile.generateOnly); 159 const promises: Promise<any>[] = []; 160 const skipTemplates: string[] = args.skipTemplates || []; 161 for (const [source, dest] of Object.entries(templatePaths)) { 162 if (skipTemplates.includes(source)) { 163 console.log( 164 'Skipping template %s ...', 165 chalk.cyan(path.join(templateFilesPath, platform, source)) 166 ); 167 continue; 168 } 169 170 const isOptional = checkIgnoredTemplatePaths.includes(dest); 171 console.log( 172 'Rendering %s from template %s %s...', 173 chalk.cyan(path.join(EXPO_DIR, dest)), 174 chalk.cyan(path.join(templateFilesPath, platform, source)), 175 isOptional ? chalk.yellow('(Optional) ') : '' 176 ); 177 178 promises.push( 179 copyTemplateFileAsync( 180 path.join(templateFilesPath, platform, source), 181 path.join(EXPO_DIR, dest), 182 templateSubstitutions, 183 args.configuration, 184 isOptional 185 ) 186 ); 187 } 188 189 await Promise.all(promises); 190} 191 192export { generateDynamicMacrosAsync, getTemplateSubstitutionsAsync }; 193