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