1import { Command } from '@expo/commander'; 2import JsonFile from '@expo/json-file'; 3import spawnAsync from '@expo/spawn-async'; 4import chalk from 'chalk'; 5import fs from 'fs-extra'; 6import path from 'path'; 7 8import { PACKAGES_DIR } from '../Constants'; 9import { Template, getAvailableProjectTemplatesAsync } from '../ProjectTemplates'; 10import { getNewestSDKVersionAsync, sdkVersionAsync } from '../ProjectVersions'; 11 12type ActionOptions = { 13 sdkVersion?: string; 14}; 15 16const DEPENDENCIES_KEYS = ['dependencies', 'devDependencies', 'peerDependencies']; 17const BUNDLED_NATIVE_MODULES_PATH = path.join(PACKAGES_DIR, 'expo', 'bundledNativeModules.json'); 18 19/** 20 * Finds target version range, that is usually `bundledModuleVersion` param, 21 * but in some specific cases we want to use different version range. 22 * 23 * @param targetVersionRange Version range that exists in `bundledNativeModules.json` file. 24 * @param currentVersion Version range that is currenty used in the template. 25 * @param sdkVersion SDK version string to which we're upgrading. 26 */ 27function resolveTargetVersionRange( 28 targetVersionRange: string, 29 currentVersion: string, 30 sdkVersion: string 31) { 32 if (currentVersion === '*') { 33 return currentVersion; 34 } 35 if (/^https?:\/\/.*\/react-native\//.test(currentVersion)) { 36 return `https://github.com/expo/react-native/archive/sdk-${sdkVersion}.tar.gz`; 37 } 38 return targetVersionRange; 39} 40 41/** 42 * Updates single project template. 43 * 44 * @param template Template object containing name and path. 45 * @param modulesToUpdate An object with module names to update and their version ranges. 46 * @param sdkVersion SDK version string to which we're upgrading. 47 */ 48async function updateTemplateAsync( 49 template: Template, 50 modulesToUpdate: object, 51 sdkVersion: string 52): Promise<void> { 53 console.log(`Updating ${chalk.bold.green(template.name)}...`); 54 55 const packageJsonPath = path.join(template.path, 'package.json'); 56 const packageJson = require(packageJsonPath); 57 58 for (const dependencyKey of DEPENDENCIES_KEYS) { 59 const dependencies = packageJson[dependencyKey]; 60 61 if (!dependencies) { 62 continue; 63 } 64 for (const dependencyName in dependencies) { 65 const currentVersion = dependencies[dependencyName]; 66 const targetVersion = resolveTargetVersionRange( 67 modulesToUpdate[dependencyName], 68 currentVersion, 69 sdkVersion 70 ); 71 72 if (targetVersion) { 73 if (targetVersion === currentVersion) { 74 console.log( 75 chalk.yellow('>'), 76 `Current version ${chalk.cyan(targetVersion)} of ${chalk.blue( 77 dependencyName 78 )} is up-to-date.` 79 ); 80 } else { 81 console.log( 82 chalk.yellow('>'), 83 `Updating ${chalk.blue(dependencyName)} from ${chalk.cyan( 84 currentVersion 85 )} to ${chalk.cyan(targetVersion)}...` 86 ); 87 packageJson[dependencyKey][dependencyName] = targetVersion; 88 } 89 } 90 } 91 } 92 await JsonFile.writeAsync(packageJsonPath, packageJson); 93} 94 95/** 96 * Removes template's `yarn.lock` and runs `yarn`. 97 * 98 * @param templatePath Root path of the template. 99 */ 100async function yarnTemplateAsync(templatePath: string): Promise<void> { 101 console.log(chalk.yellow('>'), 'Yarning...'); 102 103 const yarnLockPath = path.join(templatePath, 'yarn.lock'); 104 105 if (await fs.pathExists(yarnLockPath)) { 106 // We do want to always install the newest possible versions that match bundledNativeModules versions, 107 // so let's remove yarn.lock before updating re-yarning dependencies. 108 await fs.remove(yarnLockPath); 109 } 110 await spawnAsync('yarn', [], { 111 stdio: 'ignore', 112 cwd: templatePath, 113 env: process.env, 114 }); 115} 116 117async function action(options: ActionOptions) { 118 // At this point of the release process all platform should have the same newest SDK version. 119 const sdkVersion = options.sdkVersion ?? (await getNewestSDKVersionAsync('ios')); 120 121 if (!sdkVersion) { 122 throw new Error( 123 `Cannot infer current SDK version - please use ${chalk.gray('--sdkVersion')} flag.` 124 ); 125 } 126 127 const bundledNativeModules = require(BUNDLED_NATIVE_MODULES_PATH); 128 const templates = await getAvailableProjectTemplatesAsync(); 129 const expoVersion = await sdkVersionAsync(); 130 131 const modulesToUpdate = { 132 ...bundledNativeModules, 133 expo: `~${expoVersion}`, 134 }; 135 136 for (const template of templates) { 137 await updateTemplateAsync(template, modulesToUpdate, sdkVersion); 138 await yarnTemplateAsync(template.path); 139 console.log(chalk.yellow('>'), chalk.green('Success!'), '\n'); 140 } 141} 142 143export default (program: Command) => { 144 program 145 .command('update-project-templates') 146 .alias('update-templates', 'upt') 147 .description( 148 'Updates dependencies of project templates to the versions that are defined in bundledNativeModules.json file.' 149 ) 150 .option( 151 '-s, --sdkVersion [string]', 152 'SDK version for which the project templates should be updated. Defaults to the newest SDK version.' 153 ) 154 .asyncAction(action); 155}; 156