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