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