1import { Command } from '@expo/commander';
2import JsonFile from '@expo/json-file';
3import glob from 'glob-promise';
4import path from 'path';
5
6import { EXPO_DIR } from '../Constants';
7import logger from '../Logger';
8import { getPackageViewAsync } from '../Npm';
9import { transformFileAsync } from '../Transforms';
10import { installAsync as workspaceInstallAsync } from '../Workspace';
11
12export default (program: Command) => {
13  program
14    .command('setup-react-native-nightly')
15    .description('Setup expo/expo monorepo to install react-native nightly build for testing')
16    .asyncAction(main);
17};
18
19async function main() {
20  const nightlyVersion = (await getPackageViewAsync('react-native'))?.['dist-tags'].nightly;
21  if (!nightlyVersion) {
22    throw new Error('Unable to get react-native nightly version.');
23  }
24
25  logger.info('Adding pinned packages:');
26  const pinnedPackages = {
27    'react-native': nightlyVersion,
28  };
29  await addPinnedPackagesAsync(pinnedPackages);
30
31  logger.info('Yarning...');
32  await workspaceInstallAsync();
33
34  await updateReactNativePackageAsync();
35
36  await patchReanimatedAsync(nightlyVersion);
37
38  await removeKotlinAndroidExtensionAsync();
39
40  logger.info('Setting up Expo modules files');
41  await updateExpoModulesAsync();
42
43  logger.info('Setting up project files for bare-expo.');
44  await updateBareExpoAsync(nightlyVersion);
45}
46
47async function addPinnedPackagesAsync(packages: Record<string, string>) {
48  const workspacePackageJsonPath = path.join(EXPO_DIR, 'package.json');
49  const json = await JsonFile.readAsync(workspacePackageJsonPath);
50  json.resolutions ||= {};
51  for (const [name, version] of Object.entries(packages)) {
52    logger.log('  ', `${name}@${version}`);
53    json.resolutions[name] = version;
54  }
55  await JsonFile.writeAsync(workspacePackageJsonPath, json);
56}
57
58async function updateReactNativePackageAsync() {
59  // Workaround for react-native-gradle-plugin
60  const gradlePluginRoot = path.join(EXPO_DIR, 'node_modules', 'react-native-gradle-plugin');
61  await transformFileAsync(
62    path.join(gradlePluginRoot, 'src/main/kotlin/com/facebook/react/ReactExtension.kt'),
63    [
64      {
65        find: 'internal val reactNativeDir:',
66        replaceWith: 'val reactNativeDir:',
67      },
68    ]
69  );
70
71  const reactNativeRoot = path.join(EXPO_DIR, 'node_modules', 'react-native');
72  // Workaround duplicated libc++_shared.so from linked fbjni
73  await transformFileAsync(path.join(reactNativeRoot, 'ReactAndroid', 'build.gradle'), [
74    {
75      find: /^(\s*packagingOptions \{)$/gm,
76      replaceWith: '$1\n        pickFirst("**/libc++_shared.so")',
77    },
78  ]);
79}
80
81async function patchReanimatedAsync(nightlyVersion: string) {
82  const root = path.join(EXPO_DIR, 'node_modules', 'react-native-reanimated');
83
84  await transformFileAsync(path.join(root, 'scripts', 'reanimated_utils.rb'), [
85    // Add REACT_NATIVE_OVERRIDE_VERSION support
86    {
87      find: `result[:react_native_version] = react_native_json['version']`,
88      replaceWith: `result[:react_native_version] = ENV["REACT_NATIVE_OVERRIDE_VERSION"] ? ENV["REACT_NATIVE_OVERRIDE_VERSION"] : react_native_json['version']`,
89    },
90    {
91      find: `result[:react_native_minor_version] = react_native_json['version'].split('.')[1].to_i`,
92      replaceWith: `result[:react_native_minor_version] = result[:react_native_version].split('.')[1].to_i`,
93    },
94  ]);
95  await transformFileAsync(path.join(root, 'android', 'build.gradle'), [
96    // Add REACT_NATIVE_OVERRIDE_VERSION support
97    {
98      find: `def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME")`,
99      replaceWith: `def REACT_NATIVE_VERSION = System.getenv("REACT_NATIVE_OVERRIDE_VERSION") ?: reactProperties.getProperty("VERSION_NAME")`,
100    },
101    // Workaround $minor is undefined
102    {
103      find: /\$minor/g,
104      replaceWith: '$rnMinorVersion',
105    },
106    // BUILD_FROM_SOURCE
107    {
108      find: /^(boolean BUILD_FROM_SOURCE)\s*=.*/gm,
109      replaceWith: '$1 = true',
110    },
111    // duplicated class from jni, because ReactAndroid now uses fbjni rather than fbjni-java-only
112    {
113      find: 'implementation "com.facebook.fbjni:fbjni-java-only:',
114      replaceWith: 'compileOnly "com.facebook.fbjni:fbjni:',
115    },
116    {
117      // no-op tasks
118      find: /\b(task (prepareHermes).*\{)$/gm,
119      replaceWith: `$1\n    return`,
120    },
121    {
122      // download nightly react-native aar
123      find: /^(task unpackReactNativeAAR \{[\s\S]*?^\})/gm,
124      replaceWith: `
125def reactNativeIsNightly = reactProperties.getProperty("VERSION_NAME").startsWith("0.0.0-")
126
127def downloadReactNativeNightlyAAR = { buildType, version, downloadFile ->
128  def classifier = buildType == 'Debug' ? 'debug' : 'release'
129  download.run {
130    src("https://oss.sonatype.org/service/local/artifact/maven/redirect?r=snapshots&g=com.facebook.react&a=react-native&c=\${classifier}&e=aar&v=\${version}-SNAPSHOT")
131    onlyIfNewer(true)
132    overwrite(false)
133    dest(downloadFile)
134  }
135}
136
137task unpackReactNativeAAR {
138  def buildType = resolveBuildType()
139  def rnAAR
140  if (reactNativeIsNightly) {
141    def downloadFile = file("\${downloadsDir}/react-native-nightly.aar")
142    downloadReactNativeNightlyAAR(buildType, reactProperties.getProperty("VERSION_NAME"), downloadFile)
143    rnAAR = downloadFile
144  } else {
145    def rnAarMatcher = "**/react-native/**/*\${buildType}.aar"
146    if (REACT_NATIVE_MINOR_VERSION < 69) {
147      rnAarMatcher = "**/**/*.aar"
148    }
149    rnAAR = fileTree("$reactNativeRootDir/android").matching({ it.include rnAarMatcher }).singleFile
150  }
151  def file = rnAAR.absoluteFile
152  def packageName = file.name.tokenize('-')[0]
153  copy {
154    from zipTree(file)
155    into "$reactNativeRootDir/ReactAndroid/src/main/jni/first-party/$packageName/"
156    include "jni/**/*.so"
157  }
158}
159      `,
160    },
161    {
162      // add prefab support, setup task dependencies and hermes-engine dependencies
163      transform: (text: string) =>
164        text +
165        '\n\n' +
166        `android {\n` +
167        `  buildFeatures {\n` +
168        `    prefab true\n` +
169        `  }\n` +
170        `}\n` +
171        `\n` +
172        `dependencies {\n` +
173        `  compileOnly "com.facebook.react:hermes-engine:${nightlyVersion}-SNAPSHOT"` +
174        `}\n`,
175    },
176  ]);
177
178  await transformFileAsync(path.join(root, 'android', 'CMakeLists.txt'), [
179    {
180      // Remove this after reanimated support react-native 0.71
181      find: /(\s*"\$\{REACT_NATIVE_DIR\}\/ReactAndroid\/src\/main\/jni")/g,
182      replaceWith: '$1\n        "${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/react/turbomodule"',
183    },
184    {
185      // find hermes from prefab
186      find: /(string\(APPEND CMAKE_CXX_FLAGS " -DJS_RUNTIME_HERMES=1"\))/g,
187      replaceWith: `find_package(hermes-engine REQUIRED CONFIG)\n    $1`,
188    },
189    {
190      // find hermes from prefab
191      find: /"\$\{BUILD_DIR\}\/.+\/libhermes\.so"/g,
192      replaceWith: `hermes-engine::libhermes`,
193    },
194  ]);
195
196  // Workaround for UIImplementationProvider breaking change, that would break reanimated layout animation somehow
197  await transformFileAsync(
198    path.join(
199      root,
200      'android/src/main/java/com/swmansion/reanimated/layoutReanimation/ReanimatedUIManager.java'
201    ),
202    [
203      {
204        find: /^class ReaUiImplementationProvider extends UIImplementationProvider \{[\s\S]*?^\}/gm,
205        replaceWith: '',
206      },
207      {
208        find: `new ReaUiImplementationProvider(),`,
209        replaceWith: '',
210      },
211    ]
212  );
213}
214
215/**
216 * Remove deprecated kotlin-android-extensions
217 * TODO: remove this after detox updated
218 */
219async function removeKotlinAndroidExtensionAsync() {
220  const gradleFiles = ['node_modules/detox/android/detox/build.gradle'];
221
222  await Promise.all(
223    gradleFiles.map((file) =>
224      transformFileAsync(file, [
225        {
226          find: /apply plugin: ['"]kotlin-android-extensions['"]/g,
227          replaceWith: '',
228        },
229      ])
230    )
231  );
232}
233
234async function updateExpoModulesAsync() {
235  const gradleFiles = await glob('packages/**/build.gradle', { cwd: EXPO_DIR });
236  await Promise.all(
237    gradleFiles.map((file) =>
238      transformFileAsync(file, [
239        {
240          find: /\b(com.facebook.fbjni:fbjni):0\.2\.2/g,
241          replaceWith: '$1:0.3.0',
242        },
243      ])
244    )
245  );
246}
247
248async function updateBareExpoAsync(nightlyVersion: string) {
249  const root = path.join(EXPO_DIR, 'apps', 'bare-expo');
250  await transformFileAsync(path.join(root, 'android', 'build.gradle'), [
251    {
252      find: 'resolutionStrategy.force "com.facebook.react:react-native:${reactNativeVersion}"',
253      replaceWith: `resolutionStrategy.force "com.facebook.react:react-native:${nightlyVersion}-SNAPSHOT"`,
254    },
255  ]);
256
257  await transformFileAsync(path.join(root, 'ios', 'BareExpo', 'AppDelegate.mm'), [
258    {
259      // Remove this when we upgrade bare-expo to 0.71
260      find: `  RCTAppSetupPrepareApp(application);`,
261      replaceWith: `
262#if RCT_NEW_ARCH_ENABLED
263  RCTAppSetupPrepareApp(application, YES);
264#else
265  RCTAppSetupPrepareApp(application, NO);
266#endif
267`,
268    },
269  ]);
270}
271