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 // Match both variations of Ruby config: 120 // unknown: pod 'expo-dev-menu', path: '../node_modules/expo-dev-menu', :configurations => :debug 121 // Rubocop: pod 'expo-dev-menu', path: '../node_modules/expo-dev-menu', configurations: :debug 122 if ( 123 !podfile.match( 124 /pod ['"]expo-dev-menu['"],\s?path: ['"]\.\.\/node_modules\/expo-dev-menu['"],\s?:?configurations:?\s(?:=>\s)?:debug/ 125 ) 126 ) { 127 podfile = addLines(podfile, 'use_react_native', 0, [` ${DEV_MENU_POD_IMPORT}`]); 128 } 129 return podfile; 130 }); 131 return config; 132 }, 133 ]); 134}; 135 136const withDevMenuAppDelegate: ConfigPlugin = config => { 137 return withDangerousMod(config, [ 138 'ios', 139 async config => { 140 await editAppDelegate(config, appDelegate => { 141 if (!appDelegate.includes(DEV_MENU_IOS_IMPORT)) { 142 const lines = appDelegate.split('\n'); 143 lines.splice(1, 0, DEV_MENU_IOS_IMPORT); 144 145 appDelegate = lines.join('\n'); 146 } 147 148 if (!appDelegate.includes(DEV_MENU_IOS_INIT)) { 149 const lines = appDelegate.split('\n'); 150 151 const initializeReactNativeAppIndex = lines.findIndex(line => 152 line.includes('- (RCTBridge *)initializeReactNativeApp') 153 ); 154 155 const rootViewControllerIndex = lines.findIndex( 156 (line, index) => 157 initializeReactNativeAppIndex < index && line.includes('rootViewController') 158 ); 159 160 lines.splice(rootViewControllerIndex - 1, 0, DEV_MENU_IOS_INIT); 161 162 appDelegate = lines.join('\n'); 163 } 164 165 return appDelegate; 166 }); 167 168 return config; 169 }, 170 ]); 171}; 172 173const withDevMenu = (config: ExpoConfig) => { 174 config = withDevMenuActivity(config); 175 config = withDevMenuPodfile(config); 176 config = withDevMenuAppDelegate(config); 177 return config; 178}; 179 180export default withDevMenu; 181