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 packageJson = require(path.join(sourceDirectory, 'package.json')) as PackageJson; 140 const semverPrefix = 141 (options.semverPrefix != null ? options.semverPrefix : moduleConfig.semverPrefix) || ''; 142 const newVersionRange = `${semverPrefix}${packageJson.version}`; 143 144 await updateDependenciesAsync(moduleName, newVersionRange); 145 } 146 } finally { 147 // Clean cloned repo 148 await fs.remove(sourceDirectory); 149 } 150 logger.success(' Successfully updated %s\n', chalk.bold(moduleName)); 151} 152 153/** 154 * Updates versions in bundled native modules and workspace projects. 155 */ 156async function updateDependenciesAsync(moduleName: string, versionRange: string) { 157 logger.log('✍️ Updating bundled native modules'); 158 159 await updateBundledVersionsAsync({ 160 [moduleName]: versionRange, 161 }); 162 163 logger.log('✍️ Updating workspace dependencies'); 164 165 await Workspace.updateDependencyAsync(moduleName, versionRange); 166} 167 168/** 169 * Validates provided target name or prompts for the valid one. 170 */ 171async function resolveTargetNameAsync(providedTargetName: string): Promise<string> { 172 const targets = Object.keys(vendoredModulesConfig); 173 174 if (providedTargetName) { 175 if (targets.includes(providedTargetName)) { 176 return providedTargetName; 177 } 178 throw new Error(`Couldn't find config for ${providedTargetName} target.`); 179 } 180 const { targetName } = await inquirer.prompt([ 181 { 182 type: 'list', 183 name: 'targetName', 184 prefix: '❔', 185 message: 'In which target do you want to update vendored module?', 186 choices: targets.map((target) => ({ 187 name: vendoredModulesConfig[target].name, 188 value: target, 189 })), 190 }, 191 ]); 192 return targetName; 193} 194 195/** 196 * Validates provided module name or prompts for the valid one. 197 */ 198async function resolveModuleNameAsync( 199 providedModuleName: string, 200 targetConfig: VendoringTargetConfig 201): Promise<string> { 202 const moduleNames = Object.keys(targetConfig.modules); 203 204 if (providedModuleName) { 205 if (moduleNames.includes(providedModuleName)) { 206 return providedModuleName; 207 } 208 throw new Error(`Couldn't find config for ${providedModuleName} module.`); 209 } 210 const { moduleName } = await inquirer.prompt([ 211 { 212 type: 'list', 213 name: 'moduleName', 214 prefix: '❔', 215 message: 'Which vendored module do you want to update?', 216 choices: moduleNames, 217 }, 218 ]); 219 return moduleName; 220} 221 222function resolvePlatforms(platform: string): string[] { 223 const all = getVendoringAvailablePlatforms(); 224 return all.includes(platform) ? [platform] : all; 225} 226