1import { Command } from '@expo/commander'; 2import spawnAsync from '@expo/spawn-async'; 3import chalk from 'chalk'; 4import fs from 'fs-extra'; 5import path from 'path'; 6 7import { EXPO_DIR, ANDROID_DIR } from '../Constants'; 8import { getReactNativeSubmoduleDir } from '../Directories'; 9import logger from '../Logger'; 10import { getNextSDKVersionAsync } from '../ProjectVersions'; 11import { transformFileAsync } from '../Transforms'; 12 13type ActionOptions = { 14 checkout?: string; 15 sdkVersion?: string; 16}; 17 18const REACT_NATIVE_SUBMODULE_PATH = getReactNativeSubmoduleDir(); 19const REACT_ANDROID_PATH = path.join(ANDROID_DIR, 'ReactAndroid'); 20const REACT_COMMON_PATH = path.join(ANDROID_DIR, 'ReactCommon'); 21const REACT_APPLICATION_MK_PATH = path.join(REACT_ANDROID_PATH, 'src/main/jni/Application.mk'); 22 23async function checkoutReactNativeSubmoduleAsync(checkoutRef: string): Promise<void> { 24 await spawnAsync('git', ['fetch'], { 25 cwd: REACT_NATIVE_SUBMODULE_PATH, 26 }); 27 await spawnAsync('git', ['checkout', checkoutRef], { 28 cwd: REACT_NATIVE_SUBMODULE_PATH, 29 }); 30} 31 32async function updateReactAndroidAsync(sdkVersion: string): Promise<void> { 33 console.log(`Cleaning ${chalk.magenta(path.relative(EXPO_DIR, REACT_ANDROID_PATH))}...`); 34 await fs.remove(REACT_ANDROID_PATH); 35 36 console.log(`Cleaning ${chalk.magenta(path.relative(EXPO_DIR, REACT_COMMON_PATH))}...`); 37 await fs.remove(REACT_COMMON_PATH); 38 39 console.log( 40 `Running ${chalk.blue('ReactAndroidCodeTransformer')} with ${chalk.yellow( 41 `./gradlew :tools:execute --args ${sdkVersion}` 42 )} command...` 43 ); 44 await spawnAsync('./gradlew', [':tools:execute', '--args', sdkVersion], { 45 cwd: ANDROID_DIR, 46 stdio: 'inherit', 47 }); 48 49 logger.info( 50 ' Transforming', 51 chalk.magenta('Application.mk'), 52 'to make use of', 53 chalk.yellow('NDK_ABI_FILTERS') 54 ); 55 await transformFileAsync(REACT_APPLICATION_MK_PATH, [ 56 { 57 find: /^APP_ABI := (.*)$/m, 58 replaceWith: 'APP_ABI := $(if $(NDK_ABI_FILTERS),$(NDK_ABI_FILTERS),$($1))', 59 }, 60 ]); 61} 62 63async function action(options: ActionOptions) { 64 if (options.checkout) { 65 console.log( 66 `Checking out ${chalk.magenta( 67 path.relative(EXPO_DIR, REACT_NATIVE_SUBMODULE_PATH) 68 )} submodule at ${chalk.blue(options.checkout)} ref...` 69 ); 70 await checkoutReactNativeSubmoduleAsync(options.checkout); 71 } 72 73 // When we're updating React Native, we mostly want it to be for the next SDK that isn't versioned yet. 74 const androidSdkVersion = options.sdkVersion || (await getNextSDKVersionAsync('android')); 75 76 if (!androidSdkVersion) { 77 throw new Error( 78 'Cannot obtain next SDK version. Try to run with --sdkVersion <sdkVersion> flag.' 79 ); 80 } 81 82 console.log( 83 `Updating ${chalk.green('ReactAndroid')} for SDK ${chalk.cyan(androidSdkVersion)} ...` 84 ); 85 await updateReactAndroidAsync(androidSdkVersion); 86} 87 88export default (program: Command) => { 89 program 90 .command('update-react-native') 91 .alias('update-rn', 'urn') 92 .description( 93 'Updates React Native submodule and applies Expo-specific code transformations on ReactAndroid and ReactCommon folders.' 94 ) 95 .option( 96 '-c, --checkout [string]', 97 "Git's ref to the commit, tag or branch on which the React Native submodule should be checkouted." 98 ) 99 .option( 100 '-s, --sdkVersion [string]', 101 'SDK version for which the forked React Native will be used. Defaults to the newest SDK version increased by a major update.' 102 ) 103 .asyncAction(action); 104}; 105