1import { Command } from '@expo/commander'; 2import JsonFile from '@expo/json-file'; 3import fs from 'fs-extra'; 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 // Remove this after we upgrade reanimated 30 'react-native-reanimated': '2.10.0', 31 }; 32 await addPinnedPackagesAsync(pinnedPackages); 33 34 logger.info('Yarning...'); 35 await workspaceInstallAsync(); 36 37 await updateReactNativePackageAsync(); 38 39 await patchReanimatedAsync(); 40 41 // Workaround for deprecated `Linking.removeEventListener` 42 // Remove this after we migrate to @react-navigation/native@^6.0.12 43 // https://linear.app/expo/issue/ENG-4148/upgrade-react-navigation-across-expoexpo-monorepo 44 // https://github.com/react-navigation/react-navigation/commit/bd5cd55e130cba2c6c35bbf360e3727a9fcf00e4 45 await patchReactNavigationAsync(); 46 47 logger.info('Setting up project files for bare-expo.'); 48 await updateBareExpoAsync(); 49} 50 51async function addPinnedPackagesAsync(packages: Record<string, string>) { 52 const workspacePackageJsonPath = path.join(EXPO_DIR, 'package.json'); 53 const json = await JsonFile.readAsync(workspacePackageJsonPath); 54 json.resolutions ||= {}; 55 for (const [name, version] of Object.entries(packages)) { 56 logger.log(' ', `${name}@${version}`); 57 json.resolutions[name] = version; 58 } 59 await JsonFile.writeAsync(workspacePackageJsonPath, json); 60} 61 62async function updateReactNativePackageAsync() { 63 const root = path.join(EXPO_DIR, 'node_modules', 'react-native'); 64 65 // Third party libraries used to use react-native minor version, update the version 1000.999.0 as the latest version 66 await transformFileAsync(path.join(root, 'package.json'), [ 67 { 68 find: '"version": "0.0.0-', 69 replaceWith: '"version": "1000.999.0-', 70 }, 71 ]); 72 await transformFileAsync(path.join(root, 'ReactAndroid', 'gradle.properties'), [ 73 { 74 find: 'VERSION_NAME=1000.0.0-', 75 replaceWith: 'VERSION_NAME=1000.999.0-', 76 }, 77 ]); 78 79 // Build hermes source from the main branch 80 await fs.writeFile(path.join(root, 'sdks', '.hermesversion'), 'main'); 81 await transformFileAsync(path.join(root, 'sdks', 'hermes-engine', 'hermes-engine.podspec'), [ 82 { 83 // Use the fake version to force building hermes from source 84 find: "version = package['version']", 85 replaceWith: "version = '1000.0.0'", 86 }, 87 ]); 88 89 // Remove unused hermes build artifacts to reduce build time 90 await transformFileAsync(path.join(root, 'sdks', 'hermes-engine', 'hermes-engine.podspec'), [ 91 { 92 find: './utils/build-mac-framework.sh', 93 replaceWith: '', 94 }, 95 ]); 96 await transformFileAsync( 97 path.join(root, 'sdks', 'hermes-engine', 'utils', 'build-ios-framework.sh'), 98 [ 99 { 100 find: 'build_apple_framework "iphoneos" "arm64" "$ios_deployment_target"', 101 replaceWith: '', 102 }, 103 { 104 find: 'build_apple_framework "catalyst" "x86_64;arm64" "$ios_deployment_target"', 105 replaceWith: '', 106 }, 107 { 108 find: 'create_universal_framework "iphoneos" "iphonesimulator" "catalyst"', 109 replaceWith: 'create_universal_framework "iphonesimulator"', 110 }, 111 ] 112 ); 113} 114 115async function patchReanimatedAsync() { 116 // Workaround for reanimated doesn't support the hermes where building from source 117 const root = path.join(EXPO_DIR, 'node_modules', 'react-native-reanimated'); 118 await transformFileAsync(path.join(root, 'android', 'build.gradle'), [ 119 { 120 find: /\bdef hermesAAR = file\(.+\)/g, 121 replaceWith: 122 'def hermesAAR = file("$reactNative/ReactAndroid/hermes-engine/build/outputs/aar/hermes-engine-debug.aar")', 123 }, 124 ]); 125 126 // Remove this after reanimated support react-native 0.71 127 await transformFileAsync(path.join(root, 'android', 'CMakeLists.txt'), [ 128 { 129 find: /(\s*"\$\{NODE_MODULES_DIR\}\/react-native\/ReactAndroid\/src\/main\/jni")/g, 130 replaceWith: 131 '$1\n "${NODE_MODULES_DIR}/react-native/ReactAndroid/src/main/jni/react/turbomodule"', 132 }, 133 ]); 134} 135 136async function patchReactNavigationAsync() { 137 const root = path.join(EXPO_DIR, 'node_modules', '@react-navigation'); 138 await transformFileAsync(path.join(root, 'native', 'src', 'useLinking.native.tsx'), [ 139 { 140 find: `\ 141 Linking.addEventListener('url', callback); 142 143 return () => Linking.removeEventListener('url', callback);`, 144 replaceWith: `\ 145 const subscription = Linking.addEventListener('url', callback); 146 147 return () => subscription.remove();`, 148 }, 149 ]); 150} 151 152async function updateBareExpoAsync() { 153 const gradlePropsFile = path.join(EXPO_DIR, 'apps', 'bare-expo', 'android', 'gradle.properties'); 154 let content = await fs.readFile(gradlePropsFile, 'utf8'); 155 if (!content.match('reactNativeNightly=true')) { 156 content += `\nreactNativeNightly=true\n`; 157 await fs.writeFile(gradlePropsFile, content); 158 } 159} 160