1import { ExpoConfig } from 'expo/config'; 2import { 3 ConfigPlugin, 4 createRunOncePlugin, 5 ExportedConfigWithProps, 6 WarningAggregator, 7 withDangerousMod, 8 withMainActivity, 9} from 'expo/config-plugins'; 10import fs from 'fs'; 11import path from 'path'; 12import semver from 'semver'; 13 14import { InstallationPage } from './constants'; 15import { withDevMenuAppDelegate } from './withDevMenuAppDelegate'; 16 17const pkg = require('expo-dev-menu/package.json'); 18 19const DEV_MENU_ANDROID_IMPORT = 'expo.modules.devmenu.react.DevMenuAwareReactActivity'; 20const DEV_MENU_ACTIVITY_CLASS = 'public class MainActivity extends DevMenuAwareReactActivity {'; 21 22async function readFileAsync(path: string): Promise<string> { 23 return fs.promises.readFile(path, 'utf8'); 24} 25 26async function saveFileAsync(path: string, content: string): Promise<void> { 27 return fs.promises.writeFile(path, content, 'utf8'); 28} 29 30function addJavaImports(javaSource: string, javaImports: string[]): string { 31 const lines = javaSource.split('\n'); 32 const lineIndexWithPackageDeclaration = lines.findIndex((line) => line.match(/^package .*;$/)); 33 for (const javaImport of javaImports) { 34 if (!javaSource.includes(javaImport)) { 35 const importStatement = `import ${javaImport};`; 36 lines.splice(lineIndexWithPackageDeclaration + 1, 0, importStatement); 37 } 38 } 39 return lines.join('\n'); 40} 41 42function addLines(content: string, find: string | RegExp, offset: number, toAdd: string[]) { 43 const lines = content.split('\n'); 44 45 let lineIndex = lines.findIndex((line) => line.match(find)); 46 47 for (const newLine of toAdd) { 48 if (!content.includes(newLine)) { 49 lines.splice(lineIndex + offset, 0, newLine); 50 lineIndex++; 51 } 52 } 53 54 return lines.join('\n'); 55} 56 57async function editPodfile(config: ExportedConfigWithProps, action: (podfile: string) => string) { 58 const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); 59 try { 60 const podfile = action(await readFileAsync(podfilePath)); 61 62 return await saveFileAsync(podfilePath, podfile); 63 } catch (e) { 64 WarningAggregator.addWarningIOS( 65 'expo-dev-menu', 66 `Couldn't modified AppDelegate.m - ${e}. 67See the expo-dev-client installation instructions to modify your AppDelegate manually: ${InstallationPage}` 68 ); 69 } 70} 71 72const withDevMenuActivity: ConfigPlugin = (config) => { 73 return withMainActivity(config, (config) => { 74 if (config.modResults.language === 'java') { 75 let content = config.modResults.contents; 76 content = addJavaImports(content, [DEV_MENU_ANDROID_IMPORT]); 77 content = content.replace( 78 'public class MainActivity extends ReactActivity {', 79 DEV_MENU_ACTIVITY_CLASS 80 ); 81 config.modResults.contents = content; 82 } else { 83 WarningAggregator.addWarningAndroid( 84 'expo-dev-menu', 85 `Cannot automatically configure MainActivity if it's not java. 86See the expo-dev-client installation instructions to modify your MainActivity manually: ${InstallationPage}` 87 ); 88 } 89 90 return config; 91 }); 92}; 93 94const withDevMenuPodfile: ConfigPlugin = (config) => { 95 return withDangerousMod(config, [ 96 'ios', 97 async (config) => { 98 await editPodfile(config, (podfile) => { 99 podfile = podfile.replace("platform :ios, '10.0'", "platform :ios, '11.0'"); 100 // Match both variations of Ruby config: 101 // unknown: pod 'expo-dev-menu', path: '../node_modules/expo-dev-menu', :configurations => :debug 102 // Rubocop: pod 'expo-dev-menu', path: '../node_modules/expo-dev-menu', configurations: :debug 103 if ( 104 !podfile.match( 105 /pod ['"]expo-dev-menu['"],\s?path: ['"][^'"]*node_modules\/expo-dev-menu['"],\s?:?configurations:?\s(?:=>\s)?:debug/ 106 ) 107 ) { 108 const packagePath = path.dirname(require.resolve('expo-dev-menu/package.json')); 109 const relativePath = path.relative(config.modRequest.platformProjectRoot, packagePath); 110 podfile = addLines(podfile, 'use_react_native', 0, [ 111 ` pod 'expo-dev-menu', path: '${relativePath}', :configurations => :debug`, 112 ]); 113 } 114 return podfile; 115 }); 116 return config; 117 }, 118 ]); 119}; 120 121const withDevMenu = (config: ExpoConfig) => { 122 // projects using SDKs before 45 need the old regex-based integration 123 // TODO: remove this config plugin once we drop support for SDK 44 124 if (config.sdkVersion && semver.lt(config.sdkVersion, '45.0.0')) { 125 config = withDevMenuActivity(config); 126 config = withDevMenuPodfile(config); 127 config = withDevMenuAppDelegate(config); 128 } 129 return config; 130}; 131 132export default createRunOncePlugin(withDevMenu, pkg.name, pkg.version); 133