1import { 2 ConfigPlugin, 3 createRunOncePlugin, 4 ExportedConfigWithProps, 5 WarningAggregator, 6 withDangerousMod, 7 withMainActivity, 8} from '@expo/config-plugins'; 9import { ExpoConfig } from '@expo/config-types'; 10import fs from 'fs'; 11import path from 'path'; 12import semver from 'semver'; 13 14import { resolveExpoUpdatesVersion } from './resolveExpoUpdatesVersion'; 15import { withDevLauncherAppDelegate } from './withDevLauncherAppDelegate'; 16 17const pkg = require('expo-dev-launcher/package.json'); 18 19const DEV_LAUNCHER_ANDROID_IMPORT = 'expo.modules.devlauncher.DevLauncherController'; 20const DEV_LAUNCHER_UPDATES_ANDROID_IMPORT = 'expo.modules.updates.UpdatesDevLauncherController'; 21const DEV_LAUNCHER_ON_NEW_INTENT = ` 22 @Override 23 public void onNewIntent(Intent intent) { 24 if (DevLauncherController.tryToHandleIntent(this, intent)) { 25 return; 26 } 27 super.onNewIntent(intent); 28 } 29`; 30const DEV_LAUNCHER_WRAPPED_ACTIVITY_DELEGATE = `DevLauncherController.wrapReactActivityDelegate(this, () -> $1);`; 31const DEV_LAUNCHER_ANDROID_INIT = 'DevLauncherController.initialize(this, getReactNativeHost());'; 32const DEV_LAUNCHER_UPDATES_ANDROID_INIT = 33 'DevLauncherController.getInstance().setUpdatesInterface(UpdatesDevLauncherController.initialize(this));'; 34const DEV_LAUNCHER_UPDATES_DEVELOPER_SUPPORT = 35 'return DevLauncherController.getInstance().getUseDeveloperSupport();'; 36 37const DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS = `import 'expo-dev-client';`; 38 39async function readFileAsync(path: string): Promise<string> { 40 return fs.promises.readFile(path, 'utf8'); 41} 42 43async function saveFileAsync(path: string, content: string): Promise<void> { 44 return fs.promises.writeFile(path, content, 'utf8'); 45} 46 47function addLines(content: string, find: string | RegExp, offset: number, toAdd: string[]) { 48 const lines = content.split('\n'); 49 50 let lineIndex = lines.findIndex(line => line.match(find)); 51 52 for (const newLine of toAdd) { 53 if (!content.includes(newLine)) { 54 lines.splice(lineIndex + offset, 0, newLine); 55 lineIndex++; 56 } 57 } 58 59 return lines.join('\n'); 60} 61 62function replaceLine(content: string, find: string | RegExp, replace: string) { 63 const lines = content.split('\n'); 64 65 if (!content.includes(replace)) { 66 const lineIndex = lines.findIndex(line => line.match(find)); 67 lines.splice(lineIndex, 1, replace); 68 } 69 70 return lines.join('\n'); 71} 72 73function addJavaImports(javaSource: string, javaImports: string[]): string { 74 const lines = javaSource.split('\n'); 75 const lineIndexWithPackageDeclaration = lines.findIndex(line => line.match(/^package .*;$/)); 76 for (const javaImport of javaImports) { 77 if (!javaSource.includes(javaImport)) { 78 const importStatement = `import ${javaImport};`; 79 lines.splice(lineIndexWithPackageDeclaration + 1, 0, importStatement); 80 } 81 } 82 return lines.join('\n'); 83} 84 85async function editMainApplication( 86 config: ExportedConfigWithProps, 87 action: (mainApplication: string) => string 88): Promise<void> { 89 const mainApplicationPath = path.join( 90 config.modRequest.platformProjectRoot, 91 'app', 92 'src', 93 'main', 94 'java', 95 ...config.android!.package!.split('.'), 96 'MainApplication.java' 97 ); 98 99 try { 100 const mainApplication = action(await readFileAsync(mainApplicationPath)); 101 return await saveFileAsync(mainApplicationPath, mainApplication); 102 } catch (e) { 103 WarningAggregator.addWarningIOS( 104 'expo-dev-launcher', 105 `Couldn't modify MainApplication.java - ${e}.` 106 ); 107 } 108} 109 110async function editPodfile(config: ExportedConfigWithProps, action: (podfile: string) => string) { 111 const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); 112 try { 113 const podfile = action(await readFileAsync(podfilePath)); 114 return await saveFileAsync(podfilePath, podfile); 115 } catch (e) { 116 WarningAggregator.addWarningIOS('expo-dev-launcher', `Couldn't modify AppDelegate.m - ${e}.`); 117 } 118} 119 120async function editIndex(config: ExportedConfigWithProps, action: (index: string) => string) { 121 const indexPath = path.join(config.modRequest.projectRoot, 'index.js'); 122 try { 123 const index = action(await readFileAsync(indexPath)); 124 return await saveFileAsync(indexPath, index); 125 } catch (e) { 126 WarningAggregator.addWarningIOS('expo-dev-launcher', `Couldn't modify index.js - ${e}.`); 127 } 128} 129 130const withDevLauncherApplication: ConfigPlugin = config => { 131 return withDangerousMod(config, [ 132 'android', 133 async config => { 134 await editMainApplication(config, mainApplication => { 135 mainApplication = addJavaImports(mainApplication, [DEV_LAUNCHER_ANDROID_IMPORT]); 136 137 mainApplication = addLines(mainApplication, 'initializeFlipper\\(this', 0, [ 138 ` ${DEV_LAUNCHER_ANDROID_INIT}`, 139 ]); 140 141 let expoUpdatesVersion; 142 try { 143 expoUpdatesVersion = resolveExpoUpdatesVersion(config.modRequest.projectRoot); 144 } catch (e) { 145 WarningAggregator.addWarningAndroid( 146 'expo-dev-launcher', 147 `Failed to check compatibility with expo-updates - ${e}` 148 ); 149 } 150 if (expoUpdatesVersion && semver.gt(expoUpdatesVersion, '0.6.0')) { 151 mainApplication = addJavaImports(mainApplication, [DEV_LAUNCHER_UPDATES_ANDROID_IMPORT]); 152 mainApplication = addLines(mainApplication, 'initializeFlipper\\(this', 0, [ 153 ` ${DEV_LAUNCHER_UPDATES_ANDROID_INIT}`, 154 ]); 155 mainApplication = replaceLine( 156 mainApplication, 157 'return BuildConfig.DEBUG;', 158 ` ${DEV_LAUNCHER_UPDATES_DEVELOPER_SUPPORT}` 159 ); 160 } 161 162 return mainApplication; 163 }); 164 return config; 165 }, 166 ]); 167}; 168 169const withDevLauncherActivity: ConfigPlugin = config => { 170 return withMainActivity(config, config => { 171 if (config.modResults.language === 'java') { 172 let content = addJavaImports(config.modResults.contents, [ 173 DEV_LAUNCHER_ANDROID_IMPORT, 174 'android.content.Intent', 175 ]); 176 177 if (!content.includes(DEV_LAUNCHER_ON_NEW_INTENT)) { 178 const lines = content.split('\n'); 179 const onCreateIndex = lines.findIndex(line => line.includes('public class MainActivity')); 180 181 lines.splice(onCreateIndex + 1, 0, DEV_LAUNCHER_ON_NEW_INTENT); 182 183 content = lines.join('\n'); 184 } 185 186 if (!content.includes('DevLauncherController.wrapReactActivityDelegate')) { 187 content = content.replace( 188 /(new ReactActivityDelegate(.*|\s)*});$/m, 189 DEV_LAUNCHER_WRAPPED_ACTIVITY_DELEGATE 190 ); 191 } 192 193 config.modResults.contents = content; 194 } else { 195 WarningAggregator.addWarningAndroid( 196 'expo-dev-launcher', 197 `Cannot automatically configure MainActivity if it's not java` 198 ); 199 } 200 201 return config; 202 }); 203}; 204 205const withDevLauncherPodfile: ConfigPlugin = config => { 206 return withDangerousMod(config, [ 207 'ios', 208 async config => { 209 await editPodfile(config, podfile => { 210 podfile = podfile.replace("platform :ios, '10.0'", "platform :ios, '11.0'"); 211 // Match both variations of Ruby config: 212 // unknown: pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', :configurations => :debug 213 // Rubocop: pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', configurations: :debug 214 if ( 215 !podfile.match( 216 /pod ['"]expo-dev-launcher['"],\s?path: ['"][^'"]*node_modules\/expo-dev-launcher['"],\s?:?configurations:?\s(?:=>\s)?:debug/ 217 ) 218 ) { 219 const packagePath = path.dirname(require.resolve('expo-dev-launcher/package.json')); 220 const relativePath = path.relative(config.modRequest.platformProjectRoot, packagePath); 221 podfile = addLines(podfile, 'use_react_native', 0, [ 222 ` pod 'expo-dev-launcher', path: '${relativePath}', :configurations => :debug`, 223 ]); 224 } 225 return podfile; 226 }); 227 return config; 228 }, 229 ]); 230}; 231 232const withErrorHandling: ConfigPlugin = config => { 233 return withDangerousMod(config, [ 234 // We want to edit js file, but for the `DangerousMod` we need to select a platform. 235 'android', 236 async config => { 237 await editIndex(config, index => { 238 if (!index.includes(DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS)) { 239 index = DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS + '\n\n' + index; 240 } 241 return index; 242 }); 243 return config; 244 }, 245 ]); 246}; 247 248const withDevLauncher = (config: ExpoConfig) => { 249 config = withDevLauncherActivity(config); 250 config = withDevLauncherApplication(config); 251 config = withDevLauncherPodfile(config); 252 config = withDevLauncherAppDelegate(config); 253 config = withErrorHandling(config); 254 return config; 255}; 256 257export default createRunOncePlugin(withDevLauncher, pkg.name, pkg.version); 258