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