import { ConfigPlugin, createRunOncePlugin, ExportedConfigWithProps, WarningAggregator, withDangerousMod, withMainActivity, } from '@expo/config-plugins'; import { ExpoConfig } from '@expo/config-types'; import fs from 'fs'; import path from 'path'; import semver from 'semver'; import { resolveExpoUpdatesVersion } from './resolveExpoUpdatesVersion'; import { withDevLauncherAppDelegate } from './withDevLauncherAppDelegate'; const pkg = require('expo-dev-launcher/package.json'); const DEV_LAUNCHER_ANDROID_IMPORT = 'expo.modules.devlauncher.DevLauncherController'; const DEV_LAUNCHER_UPDATES_ANDROID_IMPORT = 'expo.modules.updates.UpdatesDevLauncherController'; const DEV_LAUNCHER_ON_NEW_INTENT = ` @Override public void onNewIntent(Intent intent) { if (DevLauncherController.tryToHandleIntent(this, intent)) { return; } super.onNewIntent(intent); } `; const DEV_LAUNCHER_WRAPPED_ACTIVITY_DELEGATE = `DevLauncherController.wrapReactActivityDelegate(this, () -> $1);`; const DEV_LAUNCHER_ANDROID_INIT = 'DevLauncherController.initialize(this, getReactNativeHost());'; const DEV_LAUNCHER_UPDATES_ANDROID_INIT = `if (BuildConfig.DEBUG) { DevLauncherController.getInstance().setUpdatesInterface(UpdatesDevLauncherController.initialize(this)); }`; const DEV_LAUNCHER_UPDATES_DEVELOPER_SUPPORT = 'return DevLauncherController.getInstance().getUseDeveloperSupport();'; const DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS = `import 'expo-dev-client'`; const DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS_VIA_LAUNCHER = `import 'expo-dev-launcher'`; async function readFileAsync(path: string): Promise { return fs.promises.readFile(path, 'utf8'); } async function saveFileAsync(path: string, content: string): Promise { return fs.promises.writeFile(path, content, 'utf8'); } function addLines(content: string, find: string | RegExp, offset: number, toAdd: string[]) { const lines = content.split('\n'); let lineIndex = lines.findIndex((line) => line.match(find)); for (const newLine of toAdd) { if (!content.includes(newLine)) { lines.splice(lineIndex + offset, 0, newLine); lineIndex++; } } return lines.join('\n'); } function replaceLine(content: string, find: string | RegExp, replace: string) { const lines = content.split('\n'); if (!content.includes(replace)) { const lineIndex = lines.findIndex((line) => line.match(find)); lines.splice(lineIndex, 1, replace); } return lines.join('\n'); } function addJavaImports(javaSource: string, javaImports: string[]): string { const lines = javaSource.split('\n'); const lineIndexWithPackageDeclaration = lines.findIndex((line) => line.match(/^package .*;$/)); for (const javaImport of javaImports) { if (!javaSource.includes(javaImport)) { const importStatement = `import ${javaImport};`; lines.splice(lineIndexWithPackageDeclaration + 1, 0, importStatement); } } return lines.join('\n'); } async function editMainApplication( config: ExportedConfigWithProps, action: (mainApplication: string) => string ): Promise { const mainApplicationPath = path.join( config.modRequest.platformProjectRoot, 'app', 'src', 'main', 'java', ...config.android!.package!.split('.'), 'MainApplication.java' ); try { const mainApplication = action(await readFileAsync(mainApplicationPath)); return await saveFileAsync(mainApplicationPath, mainApplication); } catch (e) { WarningAggregator.addWarningIOS( 'expo-dev-launcher', `Couldn't modify MainApplication.java - ${e}.` ); } } async function editPodfile(config: ExportedConfigWithProps, action: (podfile: string) => string) { const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); try { const podfile = action(await readFileAsync(podfilePath)); return await saveFileAsync(podfilePath, podfile); } catch (e) { WarningAggregator.addWarningIOS('expo-dev-launcher', `Couldn't modify AppDelegate.m - ${e}.`); } } async function editIndex(config: ExportedConfigWithProps, action: (index: string) => string) { const indexPath = path.join(config.modRequest.projectRoot, 'index.js'); try { const index = action(await readFileAsync(indexPath)); return await saveFileAsync(indexPath, index); } catch (e) { WarningAggregator.addWarningIOS('expo-dev-launcher', `Couldn't modify index.js - ${e}.`); } } const withDevLauncherApplication: ConfigPlugin = (config) => { return withDangerousMod(config, [ 'android', async (config) => { await editMainApplication(config, (mainApplication) => { mainApplication = addJavaImports(mainApplication, [DEV_LAUNCHER_ANDROID_IMPORT]); mainApplication = addLines(mainApplication, 'initializeFlipper\\(this', 0, [ ` ${DEV_LAUNCHER_ANDROID_INIT}`, ]); let expoUpdatesVersion; try { expoUpdatesVersion = resolveExpoUpdatesVersion(config.modRequest.projectRoot); } catch (e) { WarningAggregator.addWarningAndroid( 'expo-dev-launcher', `Failed to check compatibility with expo-updates - ${e}` ); } if (expoUpdatesVersion && semver.gt(expoUpdatesVersion, '0.6.0')) { mainApplication = addJavaImports(mainApplication, [DEV_LAUNCHER_UPDATES_ANDROID_IMPORT]); mainApplication = addLines(mainApplication, 'initializeFlipper\\(this', 0, [ ` ${DEV_LAUNCHER_UPDATES_ANDROID_INIT}`, ]); mainApplication = replaceLine( mainApplication, 'return BuildConfig.DEBUG;', ` ${DEV_LAUNCHER_UPDATES_DEVELOPER_SUPPORT}` ); } return mainApplication; }); return config; }, ]); }; const withDevLauncherActivity: ConfigPlugin = (config) => { return withMainActivity(config, (config) => { if (config.modResults.language === 'java') { let content = addJavaImports(config.modResults.contents, [ DEV_LAUNCHER_ANDROID_IMPORT, 'android.content.Intent', ]); if (!content.includes(DEV_LAUNCHER_ON_NEW_INTENT)) { const lines = content.split('\n'); const onCreateIndex = lines.findIndex((line) => line.includes('public class MainActivity')); lines.splice(onCreateIndex + 1, 0, DEV_LAUNCHER_ON_NEW_INTENT); content = lines.join('\n'); } if (!content.includes('DevLauncherController.wrapReactActivityDelegate')) { content = content.replace( /(new ReactActivityDelegate(.*|\s)*});$/m, DEV_LAUNCHER_WRAPPED_ACTIVITY_DELEGATE ); } config.modResults.contents = content; } else { WarningAggregator.addWarningAndroid( 'expo-dev-launcher', `Cannot automatically configure MainActivity if it's not java` ); } return config; }); }; const withDevLauncherPodfile: ConfigPlugin = (config) => { return withDangerousMod(config, [ 'ios', async (config) => { await editPodfile(config, (podfile) => { podfile = podfile.replace("platform :ios, '10.0'", "platform :ios, '11.0'"); // Match both variations of Ruby config: // unknown: pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', :configurations => :debug // Rubocop: pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', configurations: :debug if ( !podfile.match( /pod ['"]expo-dev-launcher['"],\s?path: ['"][^'"]*node_modules\/expo-dev-launcher['"],\s?:?configurations:?\s(?:=>\s)?:debug/ ) ) { const packagePath = path.dirname(require.resolve('expo-dev-launcher/package.json')); const relativePath = path.relative(config.modRequest.platformProjectRoot, packagePath); podfile = addLines(podfile, 'use_react_native', 0, [ ` pod 'expo-dev-launcher', path: '${relativePath}', :configurations => :debug`, ]); } return podfile; }); return config; }, ]); }; const withErrorHandling: ConfigPlugin = (config) => { const injectErrorHandlers = async (config: ExportedConfigWithProps) => { await editIndex(config, (index) => { if ( !index.includes(DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS) && !index.includes(DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS_VIA_LAUNCHER) ) { index = DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS + ';\n\n' + index; } return index; }); return config; }; // We need to run the same task twice to ensure it will work on both platforms, // because if someone runs `expo run:ios`, it will trigger only dangerous mode for that specific platform. // Note: after the first execution, the second one won't change anything. config = withDangerousMod(config, ['android', injectErrorHandlers]); config = withDangerousMod(config, ['ios', injectErrorHandlers]); return config; }; const withDevLauncher = (config: ExpoConfig) => { config = withDevLauncherActivity(config); config = withDevLauncherApplication(config); config = withDevLauncherPodfile(config); config = withDevLauncherAppDelegate(config); config = withErrorHandling(config); return config; }; export default createRunOncePlugin(withDevLauncher, pkg.name, pkg.version);