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