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 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 97 // Update native ReactNativeVersion 98 const versions = (process.env.REACT_NATIVE_OVERRIDE_VERSION ?? '9999.9999.9999').split('.'); 99 await transformFileAsync( 100 path.join( 101 reactNativeRoot, 102 'ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java' 103 ), 104 [ 105 { 106 find: /("major", )\d+,/g, 107 replaceWith: `$1${versions[0]},`, 108 }, 109 { 110 find: /("minor", )\d+,/g, 111 replaceWith: `$1${versions[1]},`, 112 }, 113 { 114 find: /("patch", )\d+,/g, 115 replaceWith: `$1${versions[2]},`, 116 }, 117 ] 118 ); 119 120 // Workaround build error for React-bridging depending on butter 121 const bridgingFiles = await glob('ReactCommon/react/bridging/*.{h,cpp}', { 122 cwd: reactNativeRoot, 123 absolute: true, 124 }); 125 await Promise.all( 126 bridgingFiles.map((file) => 127 transformFileAsync(file, [ 128 { 129 find: /<butter\/map\.h>/g, 130 replaceWith: '<map>', 131 }, 132 { 133 find: /<butter\/function\.h>/g, 134 replaceWith: '<functional>', 135 }, 136 { 137 find: /butter::(map|function)/g, 138 replaceWith: 'std::$1', 139 }, 140 ]) 141 ) 142 ); 143} 144 145async function patchReanimatedAsync(nightlyVersion: string) { 146 const root = path.join(EXPO_DIR, 'node_modules', 'react-native-reanimated'); 147 148 await transformFileAsync(path.join(root, 'android', 'build.gradle'), [ 149 { 150 find: /\$minor/g, 151 replaceWith: '$rnMinorVersion', 152 }, 153 ]); 154 155 await transformFileAsync(path.join(root, 'RNReanimated.podspec'), [ 156 { 157 find: /^(\s*['"]USE_HEADERMAP['"]\s+=>\s+['"]YES['"],\s*)$/gm, 158 replaceWith: `$1\n "CLANG_CXX_LANGUAGE_STANDARD" => "c++17",`, 159 }, 160 ]); 161} 162 163async function patchDetoxAsync() { 164 const patchFile = path.join(PATCHES_ROOT, 'detox.patch'); 165 const patchContent = await fs.readFile(patchFile, 'utf8'); 166 await applyPatchAsync({ patchContent, cwd: EXPO_DIR, stripPrefixNum: 1 }); 167} 168 169async function updateExpoModulesAsync() { 170 await transformFileAsync( 171 path.join(EXPO_DIR, 'packages/expo-modules-core/android/src/main/cpp/MethodMetadata.cpp'), 172 [ 173 { 174 // Workaround build error for CallbackWrapper interface change: 175 // https://github.com/facebook/react-native/commit/229a1ded15772497fd632c299b336566d001e37d 176 find: 'auto weakWrapper = react::CallbackWrapper::createWeak(strongLongLiveObjectCollection,', 177 replaceWith: 'auto weakWrapper = react::CallbackWrapper::createWeak(', 178 }, 179 ] 180 ); 181} 182 183async function updateBareExpoAsync(nightlyVersion: string) { 184 const root = path.join(EXPO_DIR, 'apps', 'bare-expo'); 185 await transformFileAsync(path.join(root, 'android', 'settings.gradle'), [ 186 { 187 find: /react-native-gradle-plugin/g, 188 replaceWith: '@react-native/gradle-plugin', 189 }, 190 ]); 191} 192