1import { Command } from '@expo/commander'; 2import JsonFile from '@expo/json-file'; 3import spawnAsync from '@expo/spawn-async'; 4import assert from 'assert'; 5import glob from 'glob-promise'; 6import path from 'path'; 7 8import { EXPO_DIR } from '../Constants'; 9import logger from '../Logger'; 10import { getPackageViewAsync } from '../Npm'; 11import { transformFileAsync } from '../Transforms'; 12import { installAsync as workspaceInstallAsync } from '../Workspace'; 13 14export default (program: Command) => { 15 program 16 .command('setup-react-native-nightly') 17 .description('Setup expo/expo monorepo to install react-native nightly build for testing') 18 .asyncAction(main); 19}; 20 21async function main() { 22 const nightlyVersion = (await getPackageViewAsync('react-native'))?.['dist-tags'].nightly; 23 if (!nightlyVersion) { 24 throw new Error('Unable to get react-native nightly version.'); 25 } 26 27 logger.info('Adding bare-expo optional packages:'); 28 await addBareExpoOptionalPackagesAsync(); 29 30 logger.info('Adding pinned packages:'); 31 const pinnedPackages = { 32 'react-native': nightlyVersion, 33 }; 34 await addPinnedPackagesAsync(pinnedPackages); 35 36 logger.info('Yarning...'); 37 await workspaceInstallAsync(); 38 39 await updateReactNativePackageAsync(); 40 41 await patchReanimatedAsync(nightlyVersion); 42 await patchSkiaAsync(nightlyVersion); 43 44 logger.info('Setting up Expo modules files'); 45 await updateExpoModulesAsync(); 46 47 logger.info('Setting up project files for bare-expo.'); 48 await updateBareExpoAsync(nightlyVersion); 49} 50 51/** 52 * To save the CI build time, some third-party libraries are intentionally not listed as dependencies in bare-expo. 53 * Adding these packages for nightly testing to increase coverage. 54 */ 55async function addBareExpoOptionalPackagesAsync() { 56 const bareExpoRoot = path.join(EXPO_DIR, 'apps', 'bare-expo'); 57 const OPTIONAL_PKGS = ['@shopify/react-native-skia', 'lottie-react-native', 'react-native-maps']; 58 59 const packageJsonNCL = await JsonFile.readAsync( 60 path.join(EXPO_DIR, 'apps', 'native-component-list', 'package.json') 61 ); 62 const versionMap = { 63 ...(packageJsonNCL.devDependencies as object), 64 ...(packageJsonNCL.dependencies as object), 65 }; 66 67 const installPackages = OPTIONAL_PKGS.map((pkg) => { 68 const version = versionMap[pkg]; 69 assert(version); 70 return `${pkg}@${version}`; 71 }); 72 for (const pkg of installPackages) { 73 logger.log(' ', pkg); 74 } 75 76 await spawnAsync('yarn', ['add', ...installPackages], { cwd: bareExpoRoot }); 77} 78 79async function addPinnedPackagesAsync(packages: Record<string, string>) { 80 const workspacePackageJsonPath = path.join(EXPO_DIR, 'package.json'); 81 const json = await JsonFile.readAsync(workspacePackageJsonPath); 82 json.resolutions ||= {}; 83 for (const [name, version] of Object.entries(packages)) { 84 logger.log(' ', `${name}@${version}`); 85 json.resolutions[name] = version; 86 } 87 await JsonFile.writeAsync(workspacePackageJsonPath, json); 88} 89 90async function updateReactNativePackageAsync() { 91 // Workaround for react-native-gradle-plugin 92 const gradlePluginRoot = path.join(EXPO_DIR, 'node_modules', 'react-native-gradle-plugin'); 93 await transformFileAsync( 94 path.join(gradlePluginRoot, 'src/main/kotlin/com/facebook/react/ReactExtension.kt'), 95 [ 96 { 97 find: 'internal val reactNativeDir:', 98 replaceWith: 'val reactNativeDir:', 99 }, 100 ] 101 ); 102 103 const reactNativeRoot = path.join(EXPO_DIR, 'node_modules', 'react-native'); 104 // Workaround duplicated libc++_shared.so from linked fbjni 105 await transformFileAsync(path.join(reactNativeRoot, 'ReactAndroid', 'build.gradle'), [ 106 { 107 find: /^(\s*packagingOptions \{)$/gm, 108 replaceWith: '$1\n pickFirst("**/libc++_shared.so")', 109 }, 110 ]); 111} 112 113async function patchReanimatedAsync(nightlyVersion: string) { 114 const root = path.join(EXPO_DIR, 'node_modules', 'react-native-reanimated'); 115 116 await transformFileAsync(path.join(root, 'scripts', 'reanimated_utils.rb'), [ 117 // Add REACT_NATIVE_OVERRIDE_VERSION support 118 { 119 find: `result[:react_native_version] = react_native_json['version']`, 120 replaceWith: `result[:react_native_version] = ENV["REACT_NATIVE_OVERRIDE_VERSION"] ? ENV["REACT_NATIVE_OVERRIDE_VERSION"] : react_native_json['version']`, 121 }, 122 { 123 find: `result[:react_native_minor_version] = react_native_json['version'].split('.')[1].to_i`, 124 replaceWith: `result[:react_native_minor_version] = result[:react_native_version].split('.')[1].to_i`, 125 }, 126 ]); 127 await transformFileAsync(path.join(root, 'android', 'build.gradle'), [ 128 // Add REACT_NATIVE_OVERRIDE_VERSION support 129 { 130 find: `def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME")`, 131 replaceWith: `def REACT_NATIVE_VERSION = System.getenv("REACT_NATIVE_OVERRIDE_VERSION") ?: reactProperties.getProperty("VERSION_NAME")`, 132 }, 133 // Workaround $minor is undefined 134 { 135 find: /\$minor/g, 136 replaceWith: '$rnMinorVersion', 137 }, 138 // BUILD_FROM_SOURCE 139 { 140 find: /^(boolean BUILD_FROM_SOURCE)\s*=.*/gm, 141 replaceWith: '$1 = true', 142 }, 143 // duplicated class from jni, because ReactAndroid now uses fbjni rather than fbjni-java-only 144 { 145 find: 'implementation "com.facebook.fbjni:fbjni-java-only:', 146 replaceWith: 'compileOnly "com.facebook.fbjni:fbjni:', 147 }, 148 { 149 // no-op tasks 150 find: /\b(task (prepareHermes).*\{)$/gm, 151 replaceWith: `$1\n return`, 152 }, 153 { 154 // download nightly react-native aar 155 find: /^(task unpackReactNativeAAR \{[\s\S]*?^\})/gm, 156 replaceWith: ` 157def reactNativeIsNightly = reactProperties.getProperty("VERSION_NAME").startsWith("0.0.0-") 158 159def downloadReactNativeNightlyAAR = { buildType, version, downloadFile -> 160 def classifier = buildType == 'Debug' ? 'debug' : 'release' 161 download.run { 162 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") 163 onlyIfNewer(true) 164 overwrite(false) 165 dest(downloadFile) 166 } 167} 168 169task unpackReactNativeAAR { 170 def buildType = resolveBuildType() 171 def rnAAR 172 if (reactNativeIsNightly) { 173 def downloadFile = file("\${downloadsDir}/react-native-nightly.aar") 174 downloadReactNativeNightlyAAR(buildType, reactProperties.getProperty("VERSION_NAME"), downloadFile) 175 rnAAR = downloadFile 176 } else { 177 def rnAarMatcher = "**/react-native/**/*\${buildType}.aar" 178 if (REACT_NATIVE_MINOR_VERSION < 69) { 179 rnAarMatcher = "**/**/*.aar" 180 } 181 rnAAR = fileTree("$reactNativeRootDir/android").matching({ it.include rnAarMatcher }).singleFile 182 } 183 def file = rnAAR.absoluteFile 184 def packageName = file.name.tokenize('-')[0] 185 copy { 186 from zipTree(file) 187 into "$reactNativeRootDir/ReactAndroid/src/main/jni/first-party/$packageName/" 188 include "jni/**/*.so" 189 } 190} 191 `, 192 }, 193 { 194 // add prefab support, setup task dependencies and hermes-engine dependencies 195 transform: (text: string) => 196 text + 197 '\n\n' + 198 `android {\n` + 199 ` buildFeatures {\n` + 200 ` prefab true\n` + 201 ` }\n` + 202 `}\n` + 203 `\n` + 204 `dependencies {\n` + 205 ` compileOnly "com.facebook.react:hermes-android:${nightlyVersion}-SNAPSHOT"\n` + 206 `}\n`, 207 }, 208 ]); 209 210 await transformFileAsync(path.join(root, 'android', 'CMakeLists.txt'), [ 211 { 212 // Remove this after reanimated support react-native 0.71 213 find: /(\s*"\$\{REACT_NATIVE_DIR\}\/ReactAndroid\/src\/main\/jni")/g, 214 replaceWith: '$1\n "${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/react/turbomodule"', 215 }, 216 { 217 // find hermes from prefab 218 find: /(string\(APPEND CMAKE_CXX_FLAGS " -DJS_RUNTIME_HERMES=1"\))/g, 219 replaceWith: `find_package(hermes-engine REQUIRED CONFIG)\n $1`, 220 }, 221 { 222 // find hermes from prefab 223 find: /"\$\{BUILD_DIR\}\/.+\/libhermes\.so"/g, 224 replaceWith: `hermes-engine::libhermes`, 225 }, 226 ]); 227 228 // Workaround for UIImplementationProvider breaking change, that would break reanimated layout animation somehow 229 await transformFileAsync( 230 path.join( 231 root, 232 'android/src/main/java/com/swmansion/reanimated/layoutReanimation/ReanimatedUIManager.java' 233 ), 234 [ 235 { 236 find: /^class ReaUiImplementationProvider extends UIImplementationProvider \{[\s\S]*?^\}/gm, 237 replaceWith: '', 238 }, 239 { 240 find: `new ReaUiImplementationProvider(),`, 241 replaceWith: '', 242 }, 243 ] 244 ); 245} 246 247async function patchSkiaAsync(nightlyVersion: string) { 248 const root = path.join(EXPO_DIR, 'node_modules', '@shopify', 'react-native-skia'); 249 250 await transformFileAsync(path.join(root, 'android', 'build.gradle'), [ 251 { 252 // Add REACT_NATIVE_OVERRIDE_VERSION support 253 find: `def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME").split("\\.")[1].toInteger()`, 254 replaceWith: `def REACT_NATIVE_VERSION = (System.getenv("REACT_NATIVE_OVERRIDE_VERSION") ?: reactProperties.getProperty("VERSION_NAME")).split("\\.")[1].toInteger()`, 255 }, 256 { 257 // Remove builtin aar extraction from react-native node_modules 258 find: `defaultDir = file("$nodeModules/react-native/android")`, 259 replaceWith: `defaultDir = file("$nodeModules/react-native")`, 260 }, 261 { 262 // Remove builtin aar extraction from react-native node_modules 263 find: /^\s*def rnAAR.*\n\s*extractJNI.*$/gm, 264 replaceWith: '', 265 }, 266 { 267 // Add prefab support 268 transform: (text: string) => 269 text + 270 '\n\n' + 271 `android {\n` + 272 ` buildFeatures {\n` + 273 ` prefab true\n` + 274 ` }\n` + 275 `}\n`, 276 }, 277 ]); 278 279 await transformFileAsync(path.join(root, 'android', 'CMakeLists.txt'), [ 280 { 281 find: /^(\s*target_link_libraries\(\s*)$/gm, 282 replaceWith: `\ 283find_package(fbjni REQUIRED CONFIG) 284find_package(ReactAndroid REQUIRED CONFIG) 285$1`, 286 }, 287 { 288 find: '${FBJNI_LIBRARY}', 289 replaceWith: 'fbjni::fbjni', 290 }, 291 { 292 find: '${REACT_LIB}', 293 replaceWith: 'ReactAndroid::react_nativemodule_core', 294 }, 295 { 296 find: '${JSI_LIB}', 297 replaceWith: 'ReactAndroid::jsi', 298 }, 299 { 300 find: '${TURBOMODULES_LIB}', 301 replaceWith: 'ReactAndroid::turbomodulejsijni', 302 }, 303 ]); 304} 305 306async function updateExpoModulesAsync() { 307 const gradleFiles = await glob('packages/**/build.gradle', { cwd: EXPO_DIR }); 308 await Promise.all( 309 gradleFiles.map((file) => 310 transformFileAsync(file, [ 311 { 312 find: /\b(com.facebook.fbjni:fbjni):0\.2\.2/g, 313 replaceWith: '$1:0.3.0', 314 }, 315 ]) 316 ) 317 ); 318} 319 320async function updateBareExpoAsync(nightlyVersion: string) { 321 const root = path.join(EXPO_DIR, 'apps', 'bare-expo'); 322 await transformFileAsync(path.join(root, 'android', 'build.gradle'), [ 323 { 324 find: 'resolutionStrategy.force "com.facebook.react:react-native:${reactNativeVersion}"', 325 replaceWith: `resolutionStrategy.dependencySubstitution { 326 substitute module("com.facebook.react:react-native") using module("com.facebook.react:react-android:${nightlyVersion}-SNAPSHOT") 327 substitute module("com.facebook.react:hermes-engine") using module("com.facebook.react:hermes-android:${nightlyVersion}-SNAPSHOT") 328 }`, 329 }, 330 ]); 331 332 await transformFileAsync(path.join(root, 'ios', 'BareExpo', 'AppDelegate.mm'), [ 333 { 334 // Remove this when we upgrade bare-expo to 0.71 335 find: ` RCTAppSetupPrepareApp(application);`, 336 replaceWith: ` 337#if RCT_NEW_ARCH_ENABLED 338 RCTAppSetupPrepareApp(application, YES); 339#else 340 RCTAppSetupPrepareApp(application, NO); 341#endif 342`, 343 }, 344 ]); 345} 346