1import { Command } from '@expo/commander';
2import JsonFile from '@expo/json-file';
3import spawnAsync from '@expo/spawn-async';
4import assert from 'assert';
5import fs from 'fs-extra';
6import glob from 'glob-promise';
7import path from 'path';
8
9import { EXPO_DIR, EXPOTOOLS_DIR } from '../Constants';
10import logger from '../Logger';
11import { getPackageViewAsync } from '../Npm';
12import { transformFileAsync } from '../Transforms';
13import { applyPatchAsync } from '../Utils';
14import { installAsync as workspaceInstallAsync } from '../Workspace';
15
16const PATCHES_ROOT = path.join(EXPOTOOLS_DIR, 'src', 'react-native-nightlies', 'patches');
17
18export default (program: Command) => {
19  program
20    .command('setup-react-native-nightly')
21    .description('Setup expo/expo monorepo to install react-native nightly build for testing')
22    .asyncAction(main);
23};
24
25async function main() {
26  const nightlyVersion = (await getPackageViewAsync('react-native'))?.['dist-tags'].nightly;
27  if (!nightlyVersion) {
28    throw new Error('Unable to get react-native nightly version.');
29  }
30
31  logger.info('Adding bare-expo optional packages:');
32  await addBareExpoOptionalPackagesAsync();
33
34  logger.info('Adding pinned packages:');
35  const pinnedPackages = {
36    'react-native': nightlyVersion,
37  };
38  await addPinnedPackagesAsync(pinnedPackages);
39
40  logger.info('Yarning...');
41  await workspaceInstallAsync();
42
43  await updateReactNativePackageAsync();
44
45  await patchReanimatedAsync(nightlyVersion);
46  await patchDetoxAsync();
47
48  logger.info('Setting up Expo modules files');
49  await updateExpoModulesAsync();
50
51  logger.info('Setting up project files for bare-expo.');
52  await updateBareExpoAsync(nightlyVersion);
53}
54
55/**
56 * To save the CI build time, some third-party libraries are intentionally not listed as dependencies in bare-expo.
57 * Adding these packages for nightly testing to increase coverage.
58 */
59async function addBareExpoOptionalPackagesAsync() {
60  const bareExpoRoot = path.join(EXPO_DIR, 'apps', 'bare-expo');
61  const OPTIONAL_PKGS = ['@shopify/react-native-skia', 'lottie-react-native', 'react-native-maps'];
62
63  const packageJsonNCL = await JsonFile.readAsync(
64    path.join(EXPO_DIR, 'apps', 'native-component-list', 'package.json')
65  );
66  const versionMap = {
67    ...(packageJsonNCL.devDependencies as object),
68    ...(packageJsonNCL.dependencies as object),
69  };
70
71  const installPackages = OPTIONAL_PKGS.map((pkg) => {
72    const version = versionMap[pkg];
73    assert(version);
74    return `${pkg}@${version}`;
75  });
76  for (const pkg of installPackages) {
77    logger.log('  ', pkg);
78  }
79
80  await spawnAsync('yarn', ['add', ...installPackages], { cwd: bareExpoRoot });
81}
82
83async function addPinnedPackagesAsync(packages: Record<string, string>) {
84  const workspacePackageJsonPath = path.join(EXPO_DIR, 'package.json');
85  const json = await JsonFile.readAsync(workspacePackageJsonPath);
86  json.resolutions ||= {};
87  for (const [name, version] of Object.entries(packages)) {
88    logger.log('  ', `${name}@${version}`);
89    json.resolutions[name] = version;
90  }
91  await JsonFile.writeAsync(workspacePackageJsonPath, json);
92}
93
94async function updateReactNativePackageAsync() {
95  const reactNativeRoot = path.join(EXPO_DIR, 'node_modules', 'react-native');
96
97  // Update native ReactNativeVersion
98  const versions = (process.env.REACT_NATIVE_OVERRIDE_VERSION ?? '9999.9999.9999').split('.');
99  await transformFileAsync(
100    path.join(
101      reactNativeRoot,
102      'ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java'
103    ),
104    [
105      {
106        find: /("major", )\d+,/g,
107        replaceWith: `$1${versions[0]},`,
108      },
109      {
110        find: /("minor", )\d+,/g,
111        replaceWith: `$1${versions[1]},`,
112      },
113      {
114        find: /("patch", )\d+,/g,
115        replaceWith: `$1${versions[2]},`,
116      },
117    ]
118  );
119
120  // Workaround build error for React-bridging depending on butter
121  const bridgingFiles = await glob('ReactCommon/react/bridging/*.{h,cpp}', {
122    cwd: reactNativeRoot,
123    absolute: true,
124  });
125  await Promise.all(
126    bridgingFiles.map((file) =>
127      transformFileAsync(file, [
128        {
129          find: /<butter\/map\.h>/g,
130          replaceWith: '<map>',
131        },
132        {
133          find: /<butter\/function\.h>/g,
134          replaceWith: '<functional>',
135        },
136        {
137          find: /butter::(map|function)/g,
138          replaceWith: 'std::$1',
139        },
140      ])
141    )
142  );
143}
144
145async function patchReanimatedAsync(nightlyVersion: string) {
146  const root = path.join(EXPO_DIR, 'node_modules', 'react-native-reanimated');
147
148  await transformFileAsync(path.join(root, 'android', 'build.gradle'), [
149    {
150      find: /\$minor/g,
151      replaceWith: '$rnMinorVersion',
152    },
153  ]);
154
155  await transformFileAsync(path.join(root, 'RNReanimated.podspec'), [
156    {
157      find: /^(\s*['"]USE_HEADERMAP['"]\s+=>\s+['"]YES['"],\s*)$/gm,
158      replaceWith: `$1\n    "CLANG_CXX_LANGUAGE_STANDARD" => "c++17",`,
159    },
160  ]);
161}
162
163async function patchDetoxAsync() {
164  const patchFile = path.join(PATCHES_ROOT, 'detox.patch');
165  const patchContent = await fs.readFile(patchFile, 'utf8');
166  await applyPatchAsync({ patchContent, cwd: EXPO_DIR, stripPrefixNum: 1 });
167}
168
169async function updateExpoModulesAsync() {
170  await transformFileAsync(
171    path.join(EXPO_DIR, 'packages/expo-modules-core/android/src/main/cpp/MethodMetadata.cpp'),
172    [
173      {
174        // Workaround build error for CallbackWrapper interface change:
175        // https://github.com/facebook/react-native/commit/229a1ded15772497fd632c299b336566d001e37d
176        find: 'auto weakWrapper = react::CallbackWrapper::createWeak(strongLongLiveObjectCollection,',
177        replaceWith: 'auto weakWrapper = react::CallbackWrapper::createWeak(',
178      },
179    ]
180  );
181}
182
183async function updateBareExpoAsync(nightlyVersion: string) {
184  const root = path.join(EXPO_DIR, 'apps', 'bare-expo');
185  await transformFileAsync(path.join(root, 'android', 'settings.gradle'), [
186    {
187      find: /react-native-gradle-plugin/g,
188      replaceWith: '@react-native/gradle-plugin',
189    },
190  ]);
191}
192