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'; 12 13import { withDevLauncherAppDelegate } from './withDevLauncherAppDelegate'; 14 15const pkg = require('expo-dev-launcher/package.json'); 16 17const DEV_LAUNCHER_ANDROID_IMPORT = 'expo.modules.devlauncher.DevLauncherController'; 18const DEV_LAUNCHER_ON_NEW_INTENT = ` 19 @Override 20 public void onNewIntent(Intent intent) { 21 if (DevLauncherController.tryToHandleIntent(this, intent)) { 22 return; 23 } 24 super.onNewIntent(intent); 25 } 26`; 27const DEV_LAUNCHER_WRAPPED_ACTIVITY_DELEGATE = `DevLauncherController.wrapReactActivityDelegate(this, () -> $1);`; 28const DEV_LAUNCHER_ANDROID_INIT = 'DevLauncherController.initialize(this, getReactNativeHost());'; 29 30const DEV_LAUNCHER_POD_IMPORT = 31 "pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', :configurations => :debug"; 32 33async function readFileAsync(path: string): Promise<string> { 34 return fs.promises.readFile(path, 'utf8'); 35} 36 37async function saveFileAsync(path: string, content: string): Promise<void> { 38 return fs.promises.writeFile(path, content, 'utf8'); 39} 40 41function addLines(content: string, find: string | RegExp, offset: number, toAdd: string[]) { 42 const lines = content.split('\n'); 43 44 let lineIndex = lines.findIndex(line => line.match(find)); 45 46 for (const newLine of toAdd) { 47 if (!content.includes(newLine)) { 48 lines.splice(lineIndex + offset, 0, newLine); 49 lineIndex++; 50 } 51 } 52 53 return lines.join('\n'); 54} 55 56function addJavaImports(javaSource: string, javaImports: string[]): string { 57 const lines = javaSource.split('\n'); 58 const lineIndexWithPackageDeclaration = lines.findIndex(line => line.match(/^package .*;$/)); 59 for (const javaImport of javaImports) { 60 if (!javaSource.includes(javaImport)) { 61 const importStatement = `import ${javaImport};`; 62 lines.splice(lineIndexWithPackageDeclaration + 1, 0, importStatement); 63 } 64 } 65 return lines.join('\n'); 66} 67 68async function editMainApplication( 69 config: ExportedConfigWithProps, 70 action: (mainApplication: string) => string 71): Promise<void> { 72 const mainApplicationPath = path.join( 73 config.modRequest.platformProjectRoot, 74 'app', 75 'src', 76 'main', 77 'java', 78 ...config.android!.package!.split('.'), 79 'MainApplication.java' 80 ); 81 82 try { 83 const mainApplication = action(await readFileAsync(mainApplicationPath)); 84 return await saveFileAsync(mainApplicationPath, mainApplication); 85 } catch (e) { 86 WarningAggregator.addWarningIOS( 87 'expo-dev-launcher', 88 `Couldn't modified MainApplication.java - ${e}.` 89 ); 90 } 91} 92 93async function editPodfile(config: ExportedConfigWithProps, action: (podfile: string) => string) { 94 const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); 95 try { 96 const podfile = action(await readFileAsync(podfilePath)); 97 98 return await saveFileAsync(podfilePath, podfile); 99 } catch (e) { 100 WarningAggregator.addWarningIOS('expo-dev-launcher', `Couldn't modified AppDelegate.m - ${e}.`); 101 } 102} 103 104const withDevLauncherApplication: ConfigPlugin = config => { 105 return withDangerousMod(config, [ 106 'android', 107 async config => { 108 await editMainApplication(config, mainApplication => { 109 mainApplication = addJavaImports(mainApplication, [DEV_LAUNCHER_ANDROID_IMPORT]); 110 111 mainApplication = addLines(mainApplication, 'super.onCreate()', 1, [ 112 ` ${DEV_LAUNCHER_ANDROID_INIT}`, 113 ]); 114 115 return mainApplication; 116 }); 117 return config; 118 }, 119 ]); 120}; 121 122const withDevLauncherActivity: ConfigPlugin = config => { 123 return withMainActivity(config, config => { 124 if (config.modResults.language === 'java') { 125 let content = addJavaImports(config.modResults.contents, [ 126 DEV_LAUNCHER_ANDROID_IMPORT, 127 'android.content.Intent', 128 ]); 129 130 if (!content.includes(DEV_LAUNCHER_ON_NEW_INTENT)) { 131 const lines = content.split('\n'); 132 const onCreateIndex = lines.findIndex(line => line.includes('public class MainActivity')); 133 134 lines.splice(onCreateIndex + 1, 0, DEV_LAUNCHER_ON_NEW_INTENT); 135 136 content = lines.join('\n'); 137 } 138 139 if (!content.includes('DevLauncherController.wrapReactActivityDelegate')) { 140 content = content.replace( 141 /(new ReactActivityDelegate(.*|\s)*});$/m, 142 DEV_LAUNCHER_WRAPPED_ACTIVITY_DELEGATE 143 ); 144 } 145 146 config.modResults.contents = content; 147 } else { 148 WarningAggregator.addWarningAndroid( 149 'expo-dev-launcher', 150 `Cannot automatically configure MainActivity if it's not java` 151 ); 152 } 153 154 return config; 155 }); 156}; 157 158const withDevLauncherPodfile: ConfigPlugin = config => { 159 return withDangerousMod(config, [ 160 'ios', 161 async config => { 162 await editPodfile(config, podfile => { 163 podfile = podfile.replace("platform :ios, '10.0'", "platform :ios, '11.0'"); 164 // Match both variations of Ruby config: 165 // unknown: pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', :configurations => :debug 166 // Rubocop: pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', configurations: :debug 167 if ( 168 !podfile.match( 169 /pod ['"]expo-dev-launcher['"],\s?path: ['"]\.\.\/node_modules\/expo-dev-launcher['"],\s?:?configurations:?\s(?:=>\s)?:debug/ 170 ) 171 ) { 172 podfile = addLines(podfile, 'use_react_native', 0, [` ${DEV_LAUNCHER_POD_IMPORT}`]); 173 } 174 return podfile; 175 }); 176 return config; 177 }, 178 ]); 179}; 180 181const withDevLauncher = (config: ExpoConfig) => { 182 config = withDevLauncherActivity(config); 183 config = withDevLauncherApplication(config); 184 config = withDevLauncherPodfile(config); 185 config = withDevLauncherAppDelegate(config); 186 return config; 187}; 188 189export default createRunOncePlugin(withDevLauncher, pkg.name, pkg.version); 190