1import { 2 ConfigPlugin, 3 withDangerousMod, 4 withMainActivity, 5 WarningAggregator, 6 ExportedConfigWithProps, 7} from '@expo/config-plugins'; 8import { ExpoConfig } from '@expo/config-types'; 9import fs from 'fs'; 10import path from 'path'; 11 12const DEV_MENU_ANDROID_IMPORT = 'expo.modules.devmenu.react.DevMenuAwareReactActivity'; 13const DEV_MENU_ACTIVITY_CLASS = 'public class MainActivity extends DevMenuAwareReactActivity {'; 14 15const DEV_MENU_POD_IMPORT = 16 "pod 'expo-dev-menu', path: '../node_modules/expo-dev-menu', :configurations => :debug"; 17 18const DEV_MENU_IOS_IMPORT = ` 19#if defined(EX_DEV_MENU_ENABLED) 20@import EXDevMenu; 21#endif`; 22 23const DEV_MENU_IOS_INIT = ` 24#if defined(EX_DEV_MENU_ENABLED) 25 [DevMenuManager configureWithBridge:bridge]; 26#endif`; 27 28async function readFileAsync(path: string): Promise<string> { 29 return fs.promises.readFile(path, 'utf8'); 30} 31 32async function saveFileAsync(path: string, content: string): Promise<void> { 33 return fs.promises.writeFile(path, content, 'utf8'); 34} 35 36function addJavaImports(javaSource: string, javaImports: string[]): string { 37 const lines = javaSource.split('\n'); 38 const lineIndexWithPackageDeclaration = lines.findIndex(line => line.match(/^package .*;$/)); 39 for (const javaImport of javaImports) { 40 if (!javaSource.includes(javaImport)) { 41 const importStatement = `import ${javaImport};`; 42 lines.splice(lineIndexWithPackageDeclaration + 1, 0, importStatement); 43 } 44 } 45 return lines.join('\n'); 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 63async function editPodfile(config: ExportedConfigWithProps, action: (podfile: string) => string) { 64 const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); 65 try { 66 const podfile = action(await readFileAsync(podfilePath)); 67 68 return await saveFileAsync(podfilePath, podfile); 69 } catch (e) { 70 WarningAggregator.addWarningIOS('ios-devMenu', `Couldn't modified AppDelegate.m - ${e}.`); 71 } 72} 73 74async function editAppDelegate( 75 config: ExportedConfigWithProps, 76 action: (appDelegate: string) => string 77) { 78 const appDelegatePath = path.join( 79 config.modRequest.platformProjectRoot, 80 config.modRequest.projectName!, 81 'AppDelegate.m' 82 ); 83 84 try { 85 const appDelegate = action(await readFileAsync(appDelegatePath)); 86 return await saveFileAsync(appDelegatePath, appDelegate); 87 } catch (e) { 88 WarningAggregator.addWarningIOS('ios-devMenu', `Couldn't modified AppDelegate.m - ${e}.`); 89 } 90} 91 92const withDevMenuActivity: ConfigPlugin = config => { 93 return withMainActivity(config, config => { 94 if (config.modResults.language === 'java') { 95 let content = config.modResults.contents; 96 content = addJavaImports(content, [DEV_MENU_ANDROID_IMPORT]); 97 content = content.replace( 98 'public class MainActivity extends ReactActivity {', 99 DEV_MENU_ACTIVITY_CLASS 100 ); 101 config.modResults.contents = content; 102 } else { 103 WarningAggregator.addWarningAndroid( 104 'android-devMenu', 105 `Cannot automatically configure MainActivity if it's not java` 106 ); 107 } 108 109 return config; 110 }); 111}; 112 113const withDevMenuPodfile: ConfigPlugin = config => { 114 return withDangerousMod(config, [ 115 'ios', 116 async config => { 117 await editPodfile(config, podfile => { 118 podfile = podfile.replace("platform :ios, '10.0'", "platform :ios, '11.0'"); 119 podfile = addLines(podfile, 'use_react_native', 0, [` ${DEV_MENU_POD_IMPORT}`]); 120 return podfile; 121 }); 122 return config; 123 }, 124 ]); 125}; 126 127const withDevMenuAppDelegate: ConfigPlugin = config => { 128 return withDangerousMod(config, [ 129 'ios', 130 async config => { 131 await editAppDelegate(config, appDelegate => { 132 if (!appDelegate.includes(DEV_MENU_IOS_IMPORT)) { 133 const lines = appDelegate.split('\n'); 134 lines.splice(1, 0, DEV_MENU_IOS_IMPORT); 135 136 appDelegate = lines.join('\n'); 137 } 138 139 if (!appDelegate.includes(DEV_MENU_IOS_INIT)) { 140 const lines = appDelegate.split('\n'); 141 142 const initializeReactNativeAppIndex = lines.findIndex(line => 143 line.includes('- (RCTBridge *)initializeReactNativeApp') 144 ); 145 146 const rootViewControllerIndex = lines.findIndex( 147 (line, index) => 148 initializeReactNativeAppIndex < index && line.includes('rootViewController') 149 ); 150 151 lines.splice(rootViewControllerIndex - 1, 0, DEV_MENU_IOS_INIT); 152 153 appDelegate = lines.join('\n'); 154 } 155 156 return appDelegate; 157 }); 158 159 return config; 160 }, 161 ]); 162}; 163 164const withDevMenu = (config: ExpoConfig) => { 165 config = withDevMenuActivity(config); 166 config = withDevMenuPodfile(config); 167 config = withDevMenuAppDelegate(config); 168 return config; 169}; 170 171export default withDevMenu; 172