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 await patchReactNavigationAsync(); 48 await patchGestureHandlerAsync(); 49 50 logger.info('Setting up Expo modules files'); 51 await updateExpoModulesAsync(); 52 53 logger.info('Setting up project files for bare-expo.'); 54 await updateBareExpoAsync(nightlyVersion); 55} 56 57/** 58 * To save the CI build time, some third-party libraries are intentionally not listed as dependencies in bare-expo. 59 * Adding these packages for nightly testing to increase coverage. 60 */ 61async function addBareExpoOptionalPackagesAsync() { 62 const bareExpoRoot = path.join(EXPO_DIR, 'apps', 'bare-expo'); 63 const OPTIONAL_PKGS = ['@shopify/react-native-skia', 'lottie-react-native', 'react-native-maps']; 64 65 const packageJsonNCL = await JsonFile.readAsync( 66 path.join(EXPO_DIR, 'apps', 'native-component-list', 'package.json') 67 ); 68 const versionMap = { 69 ...(packageJsonNCL.devDependencies as object), 70 ...(packageJsonNCL.dependencies as object), 71 // override @shopify/react-native-skia version to fix xcode 14.3 build error 72 '@shopify/react-native-skia': '0.1.184', 73 }; 74 75 const installPackages = OPTIONAL_PKGS.map((pkg) => { 76 const version = versionMap[pkg]; 77 assert(version); 78 return `${pkg}@${version}`; 79 }); 80 for (const pkg of installPackages) { 81 logger.log(' ', pkg); 82 } 83 84 await spawnAsync('yarn', ['add', ...installPackages], { cwd: bareExpoRoot }); 85} 86 87async function addPinnedPackagesAsync(packages: Record<string, string>) { 88 const workspacePackageJsonPath = path.join(EXPO_DIR, 'package.json'); 89 const json = await JsonFile.readAsync(workspacePackageJsonPath); 90 json.resolutions ||= {}; 91 for (const [name, version] of Object.entries(packages)) { 92 logger.log(' ', `${name}@${version}`); 93 json.resolutions[name] = version; 94 } 95 await JsonFile.writeAsync(workspacePackageJsonPath, json); 96} 97 98async function updateReactNativePackageAsync() { 99 const reactNativeRoot = path.join(EXPO_DIR, 'node_modules', 'react-native'); 100 101 // Update native ReactNativeVersion 102 const versions = (process.env.REACT_NATIVE_OVERRIDE_VERSION ?? '9999.9999.9999').split('.'); 103 await transformFileAsync( 104 path.join( 105 reactNativeRoot, 106 'ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java' 107 ), 108 [ 109 { 110 find: /("major", )\d+,/g, 111 replaceWith: `$1${versions[0]},`, 112 }, 113 { 114 find: /("minor", )\d+,/g, 115 replaceWith: `$1${versions[1]},`, 116 }, 117 { 118 find: /("patch", )\d+,/g, 119 replaceWith: `$1${versions[2]},`, 120 }, 121 ] 122 ); 123 124 // Workaround build error for React-bridging depending on butter 125 const bridgingFiles = await glob('ReactCommon/react/bridging/*.{h,cpp}', { 126 cwd: reactNativeRoot, 127 absolute: true, 128 }); 129 await Promise.all( 130 bridgingFiles.map((file) => 131 transformFileAsync(file, [ 132 { 133 find: /<butter\/map\.h>/g, 134 replaceWith: '<map>', 135 }, 136 { 137 find: /<butter\/function\.h>/g, 138 replaceWith: '<functional>', 139 }, 140 { 141 find: /butter::(map|function)/g, 142 replaceWith: 'std::$1', 143 }, 144 ]) 145 ) 146 ); 147} 148 149async function patchReanimatedAsync(nightlyVersion: string) { 150 const root = path.join(EXPO_DIR, 'node_modules', 'react-native-reanimated'); 151 152 await transformFileAsync(path.join(root, 'android', 'build.gradle'), [ 153 { 154 find: /\$minor/g, 155 replaceWith: '$rnMinorVersion', 156 }, 157 ]); 158 159 await transformFileAsync(path.join(root, 'RNReanimated.podspec'), [ 160 { 161 find: /^(\s*['"]USE_HEADERMAP['"]\s+=>\s+['"]YES['"],\s*)$/gm, 162 replaceWith: `$1\n "CLANG_CXX_LANGUAGE_STANDARD" => "c++17",`, 163 }, 164 ]); 165} 166 167async function patchDetoxAsync() { 168 const patchFile = path.join(PATCHES_ROOT, 'detox.patch'); 169 const patchContent = await fs.readFile(patchFile, 'utf8'); 170 await applyPatchAsync({ patchContent, cwd: EXPO_DIR, stripPrefixNum: 1 }); 171} 172 173async function patchReactNavigationAsync() { 174 await transformFileAsync( 175 path.join(EXPO_DIR, 'node_modules', '@react-navigation/elements', 'src/Header/Header.tsx'), 176 [ 177 { 178 // Weird that the nightlies will break if pass `undefined` to the `transform` prop 179 find: 'style={[{ height, minHeight, maxHeight, opacity, transform }]}', 180 replaceWith: 181 'style={[{ height, minHeight, maxHeight, opacity, transform: transform ?? [] }]}', 182 }, 183 ] 184 ); 185} 186 187async function patchGestureHandlerAsync() { 188 await transformFileAsync( 189 path.join( 190 EXPO_DIR, 191 'node_modules', 192 'react-native-gesture-handler', 193 'android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt' 194 ), 195 [ 196 { 197 find: 'decorateRuntime(jsContext.get())', 198 replaceWith: 'decorateRuntime(jsContext!!.get())', 199 }, 200 ] 201 ); 202} 203 204async function updateExpoModulesAsync() { 205 // no-op currently 206} 207 208async function updateBareExpoAsync(nightlyVersion: string) { 209 const root = path.join(EXPO_DIR, 'apps', 'bare-expo'); 210 await transformFileAsync(path.join(root, 'android', 'settings.gradle'), [ 211 { 212 find: /react-native-gradle-plugin/g, 213 replaceWith: '@react-native/gradle-plugin', 214 }, 215 ]); 216 217 await transformFileAsync(path.join(root, 'ios', 'Podfile'), [ 218 { 219 find: /(platform :ios, )['"]13\.0['"]/g, 220 replaceWith: "$1'13.4'", 221 }, 222 ]); 223} 224