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