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