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 { getNextSDKVersionAsync } from '../ProjectVersions'; 10import { transformFileAsync } from '../Transforms'; 11 12type ActionOptions = { 13 checkout?: string; 14 sdkVersion?: string; 15}; 16 17const REACT_NATIVE_SUBMODULE_PATH = getReactNativeSubmoduleDir(); 18const REACT_ANDROID_PATH = path.join(ANDROID_DIR, 'ReactAndroid'); 19const REACT_COMMON_PATH = path.join(ANDROID_DIR, 'ReactCommon'); 20const REACT_ANDROID_GRADLE_PATH = path.join(REACT_ANDROID_PATH, 'build.gradle'); 21 22async function checkoutReactNativeSubmoduleAsync(checkoutRef: string): Promise<void> { 23 await spawnAsync('git', ['fetch'], { 24 cwd: REACT_NATIVE_SUBMODULE_PATH, 25 }); 26 await spawnAsync('git', ['checkout', checkoutRef], { 27 cwd: REACT_NATIVE_SUBMODULE_PATH, 28 }); 29} 30 31async function updateReactAndroidAsync(sdkVersion: string): Promise<void> { 32 console.log(`Cleaning ${chalk.magenta(path.relative(EXPO_DIR, REACT_ANDROID_PATH))}...`); 33 await fs.remove(REACT_ANDROID_PATH); 34 35 console.log(`Cleaning ${chalk.magenta(path.relative(EXPO_DIR, REACT_COMMON_PATH))}...`); 36 await fs.remove(REACT_COMMON_PATH); 37 38 console.log( 39 `Running ${chalk.blue('ReactAndroidCodeTransformer')} with ${chalk.yellow( 40 `./gradlew :tools:execute --args ${sdkVersion}` 41 )} command...` 42 ); 43 await spawnAsync('./gradlew', [':tools:execute', '--args', sdkVersion], { 44 cwd: ANDROID_DIR, 45 stdio: 'inherit', 46 }); 47 48 await transformFileAsync(REACT_ANDROID_GRADLE_PATH, [ 49 { 50 find: /^(\s*jsRootDir\s*=\s*)file\(.+\)$/m, 51 replaceWith: '$1file("$projectDir/../../react-native-lab/react-native/Libraries")', 52 }, 53 { 54 find: /^(\s*reactNativeDir\s*=\s*)file\(.+\)$/m, 55 replaceWith: '$1file("$projectDir/../../react-native-lab/react-native")', 56 }, 57 { 58 find: /^(\s*\/\/ We search for the codegen.*\n\s*\/\/ root packages folder.*\n\s*codegenDir = .*)$/m, 59 replaceWith: 60 ' codegenDir = file("$projectDir/../../react-native-lab/react-native/packages/react-native-codegen")', 61 }, 62 { 63 find: /api\("androidx.appcompat:appcompat:\d+\.\d+\.\d+"\)/, 64 replaceWith: 'api("androidx.appcompat:appcompat:1.2.0")', 65 }, 66 { 67 find: /compileSdkVersion\s+\d+/, 68 replaceWith: 'compileSdkVersion 30', 69 }, 70 ]); 71} 72 73async function action(options: ActionOptions) { 74 if (options.checkout) { 75 console.log( 76 `Checking out ${chalk.magenta( 77 path.relative(EXPO_DIR, REACT_NATIVE_SUBMODULE_PATH) 78 )} submodule at ${chalk.blue(options.checkout)} ref...` 79 ); 80 await checkoutReactNativeSubmoduleAsync(options.checkout); 81 } 82 83 // When we're updating React Native, we mostly want it to be for the next SDK that isn't versioned yet. 84 const androidSdkVersion = options.sdkVersion || (await getNextSDKVersionAsync('android')); 85 86 if (!androidSdkVersion) { 87 throw new Error( 88 'Cannot obtain next SDK version. Try to run with --sdkVersion <sdkVersion> flag.' 89 ); 90 } 91 92 console.log( 93 `Updating ${chalk.green('ReactAndroid')} for SDK ${chalk.cyan(androidSdkVersion)} ...` 94 ); 95 await updateReactAndroidAsync(androidSdkVersion); 96} 97 98export default (program: Command) => { 99 program 100 .command('update-react-native') 101 .alias('update-rn', 'urn') 102 .description( 103 'Updates React Native submodule and applies Expo-specific code transformations on ReactAndroid and ReactCommon folders.' 104 ) 105 .option( 106 '-c, --checkout [string]', 107 "Git's ref to the commit, tag or branch on which the React Native submodule should be checkouted." 108 ) 109 .option( 110 '-s, --sdkVersion [string]', 111 'SDK version for which the forked React Native will be used. Defaults to the newest SDK version increased by a major update.' 112 ) 113 .asyncAction(action); 114}; 115