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