1import { Command } from '@expo/commander'; 2import chalk from 'chalk'; 3import fs from 'fs-extra'; 4import inquirer from 'inquirer'; 5import os from 'os'; 6import path from 'path'; 7 8import { runReactNativeCodegenAsync } from '../Codegen'; 9import { EXPO_DIR } from '../Constants'; 10import { GitDirectory } from '../Git'; 11import logger from '../Logger'; 12import { PackageJson } from '../Packages'; 13import { updateBundledVersionsAsync } from '../ProjectVersions'; 14import * as Workspace from '../Workspace'; 15import { 16 getVendoringAvailablePlatforms, 17 listAvailableVendoredModulesAsync, 18 vendorPlatformAsync, 19} from '../vendoring'; 20import vendoredModulesConfig from '../vendoring/config'; 21import { legacyVendorModuleAsync } from '../vendoring/legacy'; 22import { VendoringModuleConfig, VendoringTargetConfig } from '../vendoring/types'; 23 24type ActionOptions = { 25 list: boolean; 26 listOutdated: boolean; 27 target: string; 28 module: string; 29 platform: string; 30 commit: string; 31 semverPrefix: string; 32 updateDependencies?: boolean; 33}; 34 35const EXPO_GO_TARGET = 'expo-go'; 36 37export default (program: Command) => { 38 program 39 .command('update-vendored-module') 40 .alias('update-module', 'uvm') 41 .description('Updates 3rd party modules.') 42 .option('-l, --list', 'Shows a list of available 3rd party modules.', false) 43 .option('-o, --list-outdated', 'Shows a list of outdated 3rd party modules.', false) 44 .option( 45 '-t, --target <string>', 46 'The target to update, e.g. Expo Go or development client.', 47 EXPO_GO_TARGET 48 ) 49 .option('-m, --module <string>', 'Name of the module to update.') 50 .option( 51 '-p, --platform <string>', 52 'A platform on which the vendored module will be updated.', 53 'all' 54 ) 55 .option( 56 '-c, --commit <string>', 57 'Git reference on which to checkout when copying 3rd party module.', 58 'master' 59 ) 60 .option( 61 '-s, --semver-prefix <string>', 62 'Setting this flag forces to use given semver prefix. Some modules may specify them by the config, but in case we want to update to alpha/beta versions we should use an empty prefix to be more strict.', 63 null 64 ) 65 .option( 66 '-u, --update-dependencies', 67 'Whether to update workspace dependencies and bundled native modules.', 68 true 69 ) 70 .asyncAction(action); 71}; 72 73async function action(options: ActionOptions) { 74 const target = await resolveTargetNameAsync(options.target); 75 const targetConfig = vendoredModulesConfig[target]; 76 77 if (options.list || options.listOutdated) { 78 if (target !== EXPO_GO_TARGET) { 79 throw new Error(`Listing vendored modules for target "${target}" is not supported.`); 80 } 81 await listAvailableVendoredModulesAsync(targetConfig.modules, options.listOutdated); 82 return; 83 } 84 85 const moduleName = await resolveModuleNameAsync(options.module, targetConfig); 86 const sourceDirectory = path.join(os.tmpdir(), 'ExpoVendoredModules', moduleName); 87 const moduleConfig = targetConfig.modules[moduleName]; 88 89 logger.log( 90 ' Cloning %s#%s from %s', 91 chalk.green(moduleName), 92 chalk.cyan(options.commit), 93 chalk.magenta(moduleConfig.source) 94 ); 95 96 try { 97 // Clone repository from the source 98 await GitDirectory.shallowCloneAsync( 99 sourceDirectory, 100 moduleConfig.source, 101 options.commit ?? 'master' 102 ); 103 104 const platforms = resolvePlatforms(options.platform); 105 106 for (const platform of platforms) { 107 if (!targetConfig.platforms[platform]) { 108 continue; 109 } 110 await runCodegenIfNeeded(sourceDirectory, moduleConfig, platform); 111 112 // TODO(@tsapeta): Remove this once all vendored modules are migrated to the new system. 113 if (!targetConfig.modules[moduleName][platform]) { 114 // If the target doesn't support this platform, maybe legacy vendoring does. 115 logger.info('‼️ Using legacy vendoring for platform %s', chalk.yellow(platform)); 116 await legacyVendorModuleAsync(moduleName, platform, sourceDirectory); 117 continue; 118 } 119 120 const relativeTargetDirectory = path.join( 121 targetConfig.platforms[platform].targetDirectory, 122 moduleName 123 ); 124 const targetDirectory = path.join(EXPO_DIR, relativeTargetDirectory); 125 126 logger.log( 127 ' Vendoring for %s to %s', 128 chalk.yellow(platform), 129 chalk.magenta(relativeTargetDirectory) 130 ); 131 132 // Clean up previous version 133 await fs.remove(targetDirectory); 134 135 // Delegate further steps to platform's provider 136 await vendorPlatformAsync(platform, sourceDirectory, targetDirectory, moduleConfig[platform]); 137 } 138 139 // Update dependency versions only for Expo Go target. 140 if (options.updateDependencies !== false && target === EXPO_GO_TARGET) { 141 const packageJsonPath = path.join( 142 sourceDirectory, 143 moduleConfig.packageJsonPath ?? 'package.json' 144 ); 145 const packageJson = require(packageJsonPath) as PackageJson; 146 const semverPrefix = 147 (options.semverPrefix != null ? options.semverPrefix : moduleConfig.semverPrefix) || ''; 148 const newVersionRange = `${semverPrefix}${packageJson.version}`; 149 150 await updateDependenciesAsync(moduleName, newVersionRange); 151 } 152 } finally { 153 // Clean cloned repo 154 await fs.remove(sourceDirectory); 155 } 156 logger.success(' Successfully updated %s\n', chalk.bold(moduleName)); 157} 158 159/** 160 * Updates versions in bundled native modules and workspace projects. 161 */ 162async function updateDependenciesAsync(moduleName: string, versionRange: string) { 163 logger.log('✍️ Updating bundled native modules'); 164 165 await updateBundledVersionsAsync({ 166 [moduleName]: versionRange, 167 }); 168 169 logger.log('✍️ Updating workspace dependencies'); 170 171 await Workspace.updateDependencyAsync(moduleName, versionRange); 172} 173 174/** 175 * Validates provided target name or prompts for the valid one. 176 */ 177async function resolveTargetNameAsync(providedTargetName: string): Promise<string> { 178 const targets = Object.keys(vendoredModulesConfig); 179 180 if (providedTargetName) { 181 if (targets.includes(providedTargetName)) { 182 return providedTargetName; 183 } 184 throw new Error(`Couldn't find config for ${providedTargetName} target.`); 185 } 186 const { targetName } = await inquirer.prompt([ 187 { 188 type: 'list', 189 name: 'targetName', 190 prefix: '❔', 191 message: 'In which target do you want to update vendored module?', 192 choices: targets.map((target) => ({ 193 name: vendoredModulesConfig[target].name, 194 value: target, 195 })), 196 }, 197 ]); 198 return targetName; 199} 200 201/** 202 * Validates provided module name or prompts for the valid one. 203 */ 204async function resolveModuleNameAsync( 205 providedModuleName: string, 206 targetConfig: VendoringTargetConfig 207): Promise<string> { 208 const moduleNames = Object.keys(targetConfig.modules); 209 210 if (providedModuleName) { 211 if (moduleNames.includes(providedModuleName)) { 212 return providedModuleName; 213 } 214 throw new Error(`Couldn't find config for ${providedModuleName} module.`); 215 } 216 const { moduleName } = await inquirer.prompt([ 217 { 218 type: 'list', 219 name: 'moduleName', 220 prefix: '❔', 221 message: 'Which vendored module do you want to update?', 222 choices: moduleNames, 223 }, 224 ]); 225 return moduleName; 226} 227 228function resolvePlatforms(platform: string): string[] { 229 const all = getVendoringAvailablePlatforms(); 230 return all.includes(platform) ? [platform] : all; 231} 232 233async function runCodegenIfNeeded( 234 sourceDirectory: string, 235 moduleConfig: VendoringModuleConfig, 236 platform: string 237) { 238 const packageJsonPath = path.join( 239 sourceDirectory, 240 moduleConfig.packageJsonPath ?? 'package.json' 241 ); 242 const packageJson = require(packageJsonPath) as PackageJson; 243 const libs = packageJson?.codegenConfig?.libraries ?? []; 244 const fabricDisabledLibs = libs.filter((lib) => lib.type !== 'components'); 245 if (!fabricDisabledLibs.length) { 246 return; 247 } 248 if (platform !== 'android' && platform !== 'ios') { 249 throw new Error(`Unsupported platform - ${platform}`); 250 } 251 252 const reactNativeRoot = path.join(EXPO_DIR, 'react-native-lab', 'react-native'); 253 const codegenPkgRoot = path.join(reactNativeRoot, 'packages', 'react-native-codegen'); 254 255 await Promise.all( 256 fabricDisabledLibs.map((lib) => 257 runReactNativeCodegenAsync({ 258 reactNativeRoot, 259 codegenPkgRoot, 260 outputDir: path.join(sourceDirectory, platform), 261 name: lib.name, 262 type: lib.type, 263 platform, 264 jsSrcsDir: path.join(sourceDirectory, lib.jsSrcsDir), 265 }) 266 ) 267 ); 268} 269