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