19657025fSTomasz Sapetaimport { Command } from '@expo/commander'; 2a1a2c328SKudo Chienimport spawnAsync from '@expo/spawn-async'; 3eeffdb10STomasz Sapetaimport chalk from 'chalk'; 4eeffdb10STomasz Sapetaimport fs from 'fs-extra'; 5eeffdb10STomasz Sapetaimport inquirer from 'inquirer'; 69657025fSTomasz Sapetaimport os from 'os'; 79657025fSTomasz Sapetaimport path from 'path'; 8eeffdb10STomasz Sapeta 99bf0723bSKudo Chienimport { runReactNativeCodegenAsync } from '../Codegen'; 109657025fSTomasz Sapetaimport { EXPO_DIR } from '../Constants'; 119657025fSTomasz Sapetaimport { GitDirectory } from '../Git'; 129657025fSTomasz Sapetaimport logger from '../Logger'; 13a1a2c328SKudo Chienimport { downloadPackageTarballAsync } from '../Npm'; 149657025fSTomasz Sapetaimport { PackageJson } from '../Packages'; 159657025fSTomasz Sapetaimport { updateBundledVersionsAsync } from '../ProjectVersions'; 169657025fSTomasz Sapetaimport * as Workspace from '../Workspace'; 179657025fSTomasz Sapetaimport { 189657025fSTomasz Sapeta getVendoringAvailablePlatforms, 199657025fSTomasz Sapeta listAvailableVendoredModulesAsync, 209657025fSTomasz Sapeta vendorPlatformAsync, 219657025fSTomasz Sapeta} from '../vendoring'; 229657025fSTomasz Sapetaimport vendoredModulesConfig from '../vendoring/config'; 23a1a0cab6STomasz Sapetaimport { legacyVendorModuleAsync } from '../vendoring/legacy'; 241967a7daSKudo Chienimport { VendoringModuleConfig, VendoringTargetConfig } from '../vendoring/types'; 25eeffdb10STomasz Sapeta 269657025fSTomasz Sapetatype ActionOptions = { 27eeffdb10STomasz Sapeta list: boolean; 28eeffdb10STomasz Sapeta listOutdated: boolean; 299657025fSTomasz Sapeta target: string; 30eeffdb10STomasz Sapeta module: string; 319657025fSTomasz Sapeta platform: string; 32eeffdb10STomasz Sapeta commit: string; 33eeffdb10STomasz Sapeta semverPrefix: string; 349657025fSTomasz Sapeta updateDependencies?: boolean; 35eeffdb10STomasz Sapeta}; 36eeffdb10STomasz Sapeta 379657025fSTomasz Sapetaconst EXPO_GO_TARGET = 'expo-go'; 38eeffdb10STomasz Sapeta 39eeffdb10STomasz Sapetaexport default (program: Command) => { 40eeffdb10STomasz Sapeta program 41a1a0cab6STomasz Sapeta .command('update-vendored-module') 42a1a0cab6STomasz Sapeta .alias('update-module', 'uvm') 43eeffdb10STomasz Sapeta .description('Updates 3rd party modules.') 44eeffdb10STomasz Sapeta .option('-l, --list', 'Shows a list of available 3rd party modules.', false) 45eeffdb10STomasz Sapeta .option('-o, --list-outdated', 'Shows a list of outdated 3rd party modules.', false) 469657025fSTomasz Sapeta .option( 479657025fSTomasz Sapeta '-t, --target <string>', 489657025fSTomasz Sapeta 'The target to update, e.g. Expo Go or development client.', 499657025fSTomasz Sapeta EXPO_GO_TARGET 509657025fSTomasz Sapeta ) 51eeffdb10STomasz Sapeta .option('-m, --module <string>', 'Name of the module to update.') 52eeffdb10STomasz Sapeta .option( 53eeffdb10STomasz Sapeta '-p, --platform <string>', 54eeffdb10STomasz Sapeta 'A platform on which the vendored module will be updated.', 55eeffdb10STomasz Sapeta 'all' 56eeffdb10STomasz Sapeta ) 57eeffdb10STomasz Sapeta .option( 58eeffdb10STomasz Sapeta '-c, --commit <string>', 59eeffdb10STomasz Sapeta 'Git reference on which to checkout when copying 3rd party module.', 60eeffdb10STomasz Sapeta 'master' 61eeffdb10STomasz Sapeta ) 62eeffdb10STomasz Sapeta .option( 63eeffdb10STomasz Sapeta '-s, --semver-prefix <string>', 64eeffdb10STomasz Sapeta '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.', 65eeffdb10STomasz Sapeta null 66eeffdb10STomasz Sapeta ) 679657025fSTomasz Sapeta .option( 689657025fSTomasz Sapeta '-u, --update-dependencies', 699657025fSTomasz Sapeta 'Whether to update workspace dependencies and bundled native modules.', 709657025fSTomasz Sapeta true 719657025fSTomasz Sapeta ) 72eeffdb10STomasz Sapeta .asyncAction(action); 73eeffdb10STomasz Sapeta}; 749657025fSTomasz Sapeta 759657025fSTomasz Sapetaasync function action(options: ActionOptions) { 769657025fSTomasz Sapeta const target = await resolveTargetNameAsync(options.target); 779657025fSTomasz Sapeta const targetConfig = vendoredModulesConfig[target]; 789657025fSTomasz Sapeta 799657025fSTomasz Sapeta if (options.list || options.listOutdated) { 809657025fSTomasz Sapeta if (target !== EXPO_GO_TARGET) { 819657025fSTomasz Sapeta throw new Error(`Listing vendored modules for target "${target}" is not supported.`); 829657025fSTomasz Sapeta } 839657025fSTomasz Sapeta await listAvailableVendoredModulesAsync(targetConfig.modules, options.listOutdated); 849657025fSTomasz Sapeta return; 859657025fSTomasz Sapeta } 869657025fSTomasz Sapeta 879657025fSTomasz Sapeta const moduleName = await resolveModuleNameAsync(options.module, targetConfig); 88*d58f61aeSKudo Chien const downloadSourceDir = path.join(os.tmpdir(), 'ExpoVendoredModules', moduleName); 899657025fSTomasz Sapeta const moduleConfig = targetConfig.modules[moduleName]; 909657025fSTomasz Sapeta 919657025fSTomasz Sapeta try { 92*d58f61aeSKudo Chien await downloadSourceAsync(downloadSourceDir, moduleName, moduleConfig, options); 93*d58f61aeSKudo Chien const sourceDirectory = moduleConfig.rootDir 94*d58f61aeSKudo Chien ? path.join(downloadSourceDir, moduleConfig.rootDir) 95*d58f61aeSKudo Chien : downloadSourceDir; 969657025fSTomasz Sapeta 979657025fSTomasz Sapeta const platforms = resolvePlatforms(options.platform); 989657025fSTomasz Sapeta 999657025fSTomasz Sapeta for (const platform of platforms) { 1009657025fSTomasz Sapeta if (!targetConfig.platforms[platform]) { 1019657025fSTomasz Sapeta continue; 1029657025fSTomasz Sapeta } 1031967a7daSKudo Chien await runCodegenIfNeeded(sourceDirectory, moduleConfig, platform); 104a1a0cab6STomasz Sapeta 105a1a0cab6STomasz Sapeta // TODO(@tsapeta): Remove this once all vendored modules are migrated to the new system. 106a1a0cab6STomasz Sapeta if (!targetConfig.modules[moduleName][platform]) { 107a1a0cab6STomasz Sapeta // If the target doesn't support this platform, maybe legacy vendoring does. 108a1a0cab6STomasz Sapeta logger.info('‼️ Using legacy vendoring for platform %s', chalk.yellow(platform)); 109a1a0cab6STomasz Sapeta await legacyVendorModuleAsync(moduleName, platform, sourceDirectory); 110a1a0cab6STomasz Sapeta continue; 111a1a0cab6STomasz Sapeta } 112a1a0cab6STomasz Sapeta 1139657025fSTomasz Sapeta const relativeTargetDirectory = path.join( 1149657025fSTomasz Sapeta targetConfig.platforms[platform].targetDirectory, 1159657025fSTomasz Sapeta moduleName 1169657025fSTomasz Sapeta ); 1179657025fSTomasz Sapeta const targetDirectory = path.join(EXPO_DIR, relativeTargetDirectory); 1189657025fSTomasz Sapeta 1199657025fSTomasz Sapeta logger.log( 1209657025fSTomasz Sapeta ' Vendoring for %s to %s', 1219657025fSTomasz Sapeta chalk.yellow(platform), 1229657025fSTomasz Sapeta chalk.magenta(relativeTargetDirectory) 1239657025fSTomasz Sapeta ); 1249657025fSTomasz Sapeta 1259657025fSTomasz Sapeta // Clean up previous version 1269657025fSTomasz Sapeta await fs.remove(targetDirectory); 1279657025fSTomasz Sapeta 1289657025fSTomasz Sapeta // Delegate further steps to platform's provider 1299657025fSTomasz Sapeta await vendorPlatformAsync(platform, sourceDirectory, targetDirectory, moduleConfig[platform]); 1309657025fSTomasz Sapeta } 1319657025fSTomasz Sapeta 1329657025fSTomasz Sapeta // Update dependency versions only for Expo Go target. 1339657025fSTomasz Sapeta if (options.updateDependencies !== false && target === EXPO_GO_TARGET) { 1341967a7daSKudo Chien const packageJsonPath = path.join( 1351967a7daSKudo Chien sourceDirectory, 1361967a7daSKudo Chien moduleConfig.packageJsonPath ?? 'package.json' 1371967a7daSKudo Chien ); 1381032c4b9STomasz Sapeta const packageJson = require(packageJsonPath) as PackageJson; 1399657025fSTomasz Sapeta const semverPrefix = 1409657025fSTomasz Sapeta (options.semverPrefix != null ? options.semverPrefix : moduleConfig.semverPrefix) || ''; 1419657025fSTomasz Sapeta const newVersionRange = `${semverPrefix}${packageJson.version}`; 1429657025fSTomasz Sapeta 1439657025fSTomasz Sapeta await updateDependenciesAsync(moduleName, newVersionRange); 1449657025fSTomasz Sapeta } 1459657025fSTomasz Sapeta } finally { 1469657025fSTomasz Sapeta // Clean cloned repo 147*d58f61aeSKudo Chien await fs.remove(downloadSourceDir); 1489657025fSTomasz Sapeta } 1499657025fSTomasz Sapeta logger.success(' Successfully updated %s\n', chalk.bold(moduleName)); 1509657025fSTomasz Sapeta} 1519657025fSTomasz Sapeta 1529657025fSTomasz Sapeta/** 153a1a2c328SKudo Chien * Downloads vendoring module source code either from git repository or npm 154a1a2c328SKudo Chien */ 155a1a2c328SKudo Chienasync function downloadSourceAsync( 156a1a2c328SKudo Chien sourceDirectory: string, 157a1a2c328SKudo Chien moduleName: string, 158a1a2c328SKudo Chien moduleConfig: VendoringModuleConfig, 159a1a2c328SKudo Chien options: ActionOptions 160a1a2c328SKudo Chien) { 161a1a2c328SKudo Chien if (moduleConfig.sourceType === 'npm') { 162a1a2c328SKudo Chien const version = options.commit ?? 'latest'; 163a1a2c328SKudo Chien logger.log(' Downloading %s@%s from npm', chalk.green(moduleName), chalk.cyan(version)); 164a1a2c328SKudo Chien 165a1a2c328SKudo Chien const tarball = await downloadPackageTarballAsync( 166a1a2c328SKudo Chien sourceDirectory, 167a1a2c328SKudo Chien moduleConfig.source, 168a1a2c328SKudo Chien version 169a1a2c328SKudo Chien ); 170a1a2c328SKudo Chien // `--strip-component 1` to extract files from package/ folder 171a1a2c328SKudo Chien await spawnAsync('tar', ['--strip-component', '1', '-xf', tarball], { cwd: sourceDirectory }); 172a1a2c328SKudo Chien return; 173a1a2c328SKudo Chien } 174a1a2c328SKudo Chien 175a1a2c328SKudo Chien // Clone repository from the source 176a1a2c328SKudo Chien logger.log( 177a1a2c328SKudo Chien ' Cloning %s#%s from %s', 178a1a2c328SKudo Chien chalk.green(moduleName), 179a1a2c328SKudo Chien chalk.cyan(options.commit), 180a1a2c328SKudo Chien chalk.magenta(moduleConfig.source) 181a1a2c328SKudo Chien ); 182a1a2c328SKudo Chien 183a1a2c328SKudo Chien await GitDirectory.shallowCloneAsync( 184a1a2c328SKudo Chien sourceDirectory, 185a1a2c328SKudo Chien moduleConfig.source, 186a1a2c328SKudo Chien options.commit ?? 'master' 187a1a2c328SKudo Chien ); 188a1a2c328SKudo Chien} 189a1a2c328SKudo Chien 190a1a2c328SKudo Chien/** 1919657025fSTomasz Sapeta * Updates versions in bundled native modules and workspace projects. 1929657025fSTomasz Sapeta */ 1939657025fSTomasz Sapetaasync function updateDependenciesAsync(moduleName: string, versionRange: string) { 1949657025fSTomasz Sapeta logger.log('✍️ Updating bundled native modules'); 1959657025fSTomasz Sapeta 1969657025fSTomasz Sapeta await updateBundledVersionsAsync({ 1979657025fSTomasz Sapeta [moduleName]: versionRange, 1989657025fSTomasz Sapeta }); 1999657025fSTomasz Sapeta 2009657025fSTomasz Sapeta logger.log('✍️ Updating workspace dependencies'); 2019657025fSTomasz Sapeta 2029657025fSTomasz Sapeta await Workspace.updateDependencyAsync(moduleName, versionRange); 2039657025fSTomasz Sapeta} 2049657025fSTomasz Sapeta 2059657025fSTomasz Sapeta/** 2069657025fSTomasz Sapeta * Validates provided target name or prompts for the valid one. 2079657025fSTomasz Sapeta */ 2089657025fSTomasz Sapetaasync function resolveTargetNameAsync(providedTargetName: string): Promise<string> { 2099657025fSTomasz Sapeta const targets = Object.keys(vendoredModulesConfig); 2109657025fSTomasz Sapeta 2119657025fSTomasz Sapeta if (providedTargetName) { 2129657025fSTomasz Sapeta if (targets.includes(providedTargetName)) { 2139657025fSTomasz Sapeta return providedTargetName; 2149657025fSTomasz Sapeta } 2159657025fSTomasz Sapeta throw new Error(`Couldn't find config for ${providedTargetName} target.`); 2169657025fSTomasz Sapeta } 2179657025fSTomasz Sapeta const { targetName } = await inquirer.prompt([ 2189657025fSTomasz Sapeta { 2199657025fSTomasz Sapeta type: 'list', 2209657025fSTomasz Sapeta name: 'targetName', 2219657025fSTomasz Sapeta prefix: '❔', 2229657025fSTomasz Sapeta message: 'In which target do you want to update vendored module?', 2239657025fSTomasz Sapeta choices: targets.map((target) => ({ 2249657025fSTomasz Sapeta name: vendoredModulesConfig[target].name, 2259657025fSTomasz Sapeta value: target, 2269657025fSTomasz Sapeta })), 2279657025fSTomasz Sapeta }, 2289657025fSTomasz Sapeta ]); 2299657025fSTomasz Sapeta return targetName; 2309657025fSTomasz Sapeta} 2319657025fSTomasz Sapeta 2329657025fSTomasz Sapeta/** 2339657025fSTomasz Sapeta * Validates provided module name or prompts for the valid one. 2349657025fSTomasz Sapeta */ 2359657025fSTomasz Sapetaasync function resolveModuleNameAsync( 2369657025fSTomasz Sapeta providedModuleName: string, 2379657025fSTomasz Sapeta targetConfig: VendoringTargetConfig 2389657025fSTomasz Sapeta): Promise<string> { 2399657025fSTomasz Sapeta const moduleNames = Object.keys(targetConfig.modules); 2409657025fSTomasz Sapeta 2419657025fSTomasz Sapeta if (providedModuleName) { 2429657025fSTomasz Sapeta if (moduleNames.includes(providedModuleName)) { 2439657025fSTomasz Sapeta return providedModuleName; 2449657025fSTomasz Sapeta } 2459657025fSTomasz Sapeta throw new Error(`Couldn't find config for ${providedModuleName} module.`); 2469657025fSTomasz Sapeta } 2479657025fSTomasz Sapeta const { moduleName } = await inquirer.prompt([ 2489657025fSTomasz Sapeta { 2499657025fSTomasz Sapeta type: 'list', 2509657025fSTomasz Sapeta name: 'moduleName', 2519657025fSTomasz Sapeta prefix: '❔', 2529657025fSTomasz Sapeta message: 'Which vendored module do you want to update?', 2539657025fSTomasz Sapeta choices: moduleNames, 2549657025fSTomasz Sapeta }, 2559657025fSTomasz Sapeta ]); 2569657025fSTomasz Sapeta return moduleName; 2579657025fSTomasz Sapeta} 2589657025fSTomasz Sapeta 2599657025fSTomasz Sapetafunction resolvePlatforms(platform: string): string[] { 2609657025fSTomasz Sapeta const all = getVendoringAvailablePlatforms(); 2619657025fSTomasz Sapeta return all.includes(platform) ? [platform] : all; 2629657025fSTomasz Sapeta} 2631967a7daSKudo Chien 2641967a7daSKudo Chienasync function runCodegenIfNeeded( 2651967a7daSKudo Chien sourceDirectory: string, 2661967a7daSKudo Chien moduleConfig: VendoringModuleConfig, 2671967a7daSKudo Chien platform: string 2681967a7daSKudo Chien) { 2691967a7daSKudo Chien const packageJsonPath = path.join( 2701967a7daSKudo Chien sourceDirectory, 2711967a7daSKudo Chien moduleConfig.packageJsonPath ?? 'package.json' 2721967a7daSKudo Chien ); 2731967a7daSKudo Chien const packageJson = require(packageJsonPath) as PackageJson; 2741967a7daSKudo Chien const libs = packageJson?.codegenConfig?.libraries ?? []; 2751967a7daSKudo Chien const fabricDisabledLibs = libs.filter((lib) => lib.type !== 'components'); 2761967a7daSKudo Chien if (!fabricDisabledLibs.length) { 2771967a7daSKudo Chien return; 2781967a7daSKudo Chien } 2791967a7daSKudo Chien if (platform !== 'android' && platform !== 'ios') { 2801967a7daSKudo Chien throw new Error(`Unsupported platform - ${platform}`); 2811967a7daSKudo Chien } 2821967a7daSKudo Chien 2831967a7daSKudo Chien const reactNativeRoot = path.join(EXPO_DIR, 'react-native-lab', 'react-native'); 2841967a7daSKudo Chien const codegenPkgRoot = path.join(reactNativeRoot, 'packages', 'react-native-codegen'); 2851967a7daSKudo Chien 2861967a7daSKudo Chien await Promise.all( 2871967a7daSKudo Chien fabricDisabledLibs.map((lib) => 2889bf0723bSKudo Chien runReactNativeCodegenAsync({ 2891967a7daSKudo Chien reactNativeRoot, 2901967a7daSKudo Chien codegenPkgRoot, 2911967a7daSKudo Chien outputDir: path.join(sourceDirectory, platform), 2921967a7daSKudo Chien name: lib.name, 2931967a7daSKudo Chien type: lib.type, 2941967a7daSKudo Chien platform, 2951967a7daSKudo Chien jsSrcsDir: path.join(sourceDirectory, lib.jsSrcsDir), 2961967a7daSKudo Chien }) 2971967a7daSKudo Chien ) 2981967a7daSKudo Chien ); 2991967a7daSKudo Chien} 300