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