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 patchSkiaAsync(nightlyVersion); 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 // Workaround duplicated libc++_shared.so from linked fbjni 97 await transformFileAsync(path.join(reactNativeRoot, 'ReactAndroid', 'build.gradle'), [ 98 { 99 find: /^(\s*packagingOptions \{)$/gm, 100 replaceWith: '$1\n pickFirst("**/libc++_shared.so")', 101 }, 102 ]); 103 104 // Update native ReactNativeVersion 105 const versions = (process.env.REACT_NATIVE_OVERRIDE_VERSION ?? '9999.9999.9999').split('.'); 106 await transformFileAsync( 107 path.join( 108 reactNativeRoot, 109 'ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java' 110 ), 111 [ 112 { 113 find: /("major", )\d+,/g, 114 replaceWith: `$1${versions[0]},`, 115 }, 116 { 117 find: /("minor", )\d+,/g, 118 replaceWith: `$1${versions[1]},`, 119 }, 120 { 121 find: /("patch", )\d+,/g, 122 replaceWith: `$1${versions[2]},`, 123 }, 124 ] 125 ); 126 127 // Workaround build error for React-bridging depending on butter 128 const bridgingFiles = await glob('ReactCommon/react/bridging/*.{h,cpp}', { 129 cwd: reactNativeRoot, 130 absolute: true, 131 }); 132 await Promise.all( 133 bridgingFiles.map((file) => 134 transformFileAsync(file, [ 135 { 136 find: /<butter\/map\.h>/g, 137 replaceWith: '<map>', 138 }, 139 { 140 find: /<butter\/function\.h>/g, 141 replaceWith: '<functional>', 142 }, 143 { 144 find: /butter::(map|function)/g, 145 replaceWith: 'std::$1', 146 }, 147 ]) 148 ) 149 ); 150} 151 152async function patchReanimatedAsync(nightlyVersion: string) { 153 const root = path.join(EXPO_DIR, 'node_modules', 'react-native-reanimated'); 154 155 await transformFileAsync(path.join(root, 'android', 'build.gradle'), [ 156 { 157 // add prefab support, setup task dependencies and hermes-engine dependencies 158 transform: (text: string) => 159 text + 160 '\n\n' + 161 `android {\n` + 162 ` buildFeatures {\n` + 163 ` prefab true\n` + 164 ` }\n` + 165 `}\n` + 166 `\n` + 167 `dependencies {\n` + 168 ` compileOnly "com.facebook.react:hermes-android:${nightlyVersion}-SNAPSHOT"\n` + 169 `}\n`, 170 }, 171 ]); 172 173 const patchFile = path.join(PATCHES_ROOT, 'react-native-reanimated+2.12.0.patch'); 174 const patchContent = await fs.readFile(patchFile, 'utf8'); 175 await applyPatchAsync({ patchContent, cwd: EXPO_DIR, stripPrefixNum: 1 }); 176} 177 178async function patchSkiaAsync(nightlyVersion: string) { 179 const root = path.join(EXPO_DIR, 'node_modules', '@shopify', 'react-native-skia'); 180 181 await transformFileAsync(path.join(root, 'android', 'build.gradle'), [ 182 { 183 // Add REACT_NATIVE_OVERRIDE_VERSION support 184 find: `def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME").split("\\.")[1].toInteger()`, 185 replaceWith: `def REACT_NATIVE_VERSION = (System.getenv("REACT_NATIVE_OVERRIDE_VERSION") ?: reactProperties.getProperty("VERSION_NAME")).split("\\.")[1].toInteger()`, 186 }, 187 { 188 // Remove builtin aar extraction from react-native node_modules 189 find: `defaultDir = file("$nodeModules/react-native/android")`, 190 replaceWith: `defaultDir = file("$nodeModules/react-native")`, 191 }, 192 { 193 // Remove builtin aar extraction from react-native node_modules 194 find: /^\s*def rnAAR.*\n\s*extractJNI.*$/gm, 195 replaceWith: '', 196 }, 197 { 198 // Add prefab support 199 transform: (text: string) => 200 text + 201 '\n\n' + 202 `android {\n` + 203 ` buildFeatures {\n` + 204 ` prefab true\n` + 205 ` }\n` + 206 `}\n`, 207 }, 208 ]); 209 210 await transformFileAsync(path.join(root, 'android', 'CMakeLists.txt'), [ 211 { 212 find: /^(\s*target_link_libraries\(\s*)$/gm, 213 replaceWith: `\ 214find_package(fbjni REQUIRED CONFIG) 215find_package(ReactAndroid REQUIRED CONFIG) 216$1`, 217 }, 218 { 219 find: '${FBJNI_LIBRARY}', 220 replaceWith: 'fbjni::fbjni', 221 }, 222 { 223 find: '${REACT_LIB}', 224 replaceWith: 'ReactAndroid::react_nativemodule_core', 225 }, 226 { 227 find: '${JSI_LIB}', 228 replaceWith: 'ReactAndroid::jsi', 229 }, 230 { 231 find: '${TURBOMODULES_LIB}', 232 replaceWith: 'ReactAndroid::turbomodulejsijni', 233 }, 234 ]); 235} 236 237async function updateExpoModulesAsync() { 238 const gradleFiles = await glob('packages/**/build.gradle', { cwd: EXPO_DIR }); 239 await Promise.all( 240 gradleFiles.map((file) => 241 transformFileAsync(file, [ 242 { 243 find: /\b(com.facebook.fbjni:fbjni):0\.2\.2/g, 244 replaceWith: '$1:0.3.0', 245 }, 246 { 247 find: /ndkVersion = ['"]21\.4\.7075529['"]/g, 248 replaceWith: '', 249 }, 250 ]) 251 ) 252 ); 253 254 await transformFileAsync( 255 path.join(EXPO_DIR, 'packages/expo-modules-core/android/src/main/cpp/MethodMetadata.cpp'), 256 [ 257 { 258 // Workaround build error for CallbackWrapper interface change: 259 // https://github.com/facebook/react-native/commit/229a1ded15772497fd632c299b336566d001e37d 260 find: 'auto weakWrapper = react::CallbackWrapper::createWeak(strongLongLiveObjectCollection,', 261 replaceWith: 'auto weakWrapper = react::CallbackWrapper::createWeak(', 262 }, 263 ] 264 ); 265} 266 267async function updateBareExpoAsync(nightlyVersion: string) { 268 const root = path.join(EXPO_DIR, 'apps', 'bare-expo'); 269 const patchFile = path.join(PATCHES_ROOT, 'bare-expo.patch'); 270 const patchContent = await fs.readFile(patchFile, 'utf8'); 271 await applyPatchAsync({ patchContent, cwd: EXPO_DIR, stripPrefixNum: 1 }); 272 273 await transformFileAsync(path.join(root, 'ios', 'BareExpo', 'AppDelegate.mm'), [ 274 { 275 // Remove this when we upgrade bare-expo to 0.71 276 find: ` RCTAppSetupPrepareApp(application);`, 277 replaceWith: ` 278#if RCT_NEW_ARCH_ENABLED 279 RCTAppSetupPrepareApp(application, YES); 280#else 281 RCTAppSetupPrepareApp(application, NO); 282#endif 283`, 284 }, 285 ]); 286 287 // Try to workaround detox hanging on CI 288 await transformFileAsync(path.join(root, 'ios', 'Podfile.properties.json'), [ 289 { 290 find: `"expo.jsEngine": "hermes"`, 291 replaceWith: `"expo.jsEngine": "jsc"`, 292 }, 293 ]); 294} 295