1import { Command } from '@expo/commander'; 2import { Config, Versions } from '@expo/xdl'; 3import chalk from 'chalk'; 4import inquirer from 'inquirer'; 5import * as jsondiffpatch from 'jsondiffpatch'; 6import cloneDeep from 'lodash/cloneDeep'; 7import set from 'lodash/set'; 8import unset from 'lodash/unset'; 9import semver from 'semver'; 10 11import { STAGING_API_HOST, PRODUCTION_API_HOST } from '../Constants'; 12import { sleepAsync } from '../Utils'; 13 14type ActionOptions = { 15 sdkVersion: string; 16 root: boolean; 17 deprecated?: boolean; 18 releaseNoteUrl?: string; 19 key?: string; 20 value?: any; 21 delete: boolean; 22 deleteSdk: boolean; 23 reset: boolean; 24}; 25 26async function chooseSdkVersionAsync(sdkVersions: string[]): Promise<string> { 27 const { sdkVersion } = await inquirer.prompt<{ sdkVersion: string }>([ 28 { 29 type: 'list', 30 name: 'sdkVersion', 31 default: sdkVersions[0], 32 choices: sdkVersions, 33 }, 34 ]); 35 return sdkVersion; 36} 37 38async function askForCorrectnessAsync(): Promise<boolean> { 39 const { isCorrect } = await inquirer.prompt<{ isCorrect: boolean }>([ 40 { 41 type: 'confirm', 42 name: 'isCorrect', 43 message: `Does this look correct? Type \`y\` or press enter to update ${chalk.green( 44 'staging' 45 )} config.`, 46 default: true, 47 }, 48 ]); 49 return isCorrect; 50} 51 52function setConfigValueForKey(config: object, key: string, value: any): void { 53 if (value === undefined) { 54 console.log(`Deleting ${chalk.yellow(key)} config key ...`); 55 unset(config, key); 56 } else { 57 console.log(`Changing ${chalk.yellow(key)} config key ...`); 58 set(config, key, value); 59 } 60} 61 62async function applyChangesToStagingAsync(delta: any, previousVersions: any, newVersions: any) { 63 if (!delta) { 64 console.log(chalk.yellow('There are no changes to apply in the configuration.')); 65 return; 66 } 67 68 console.log( 69 `\nHere is the diff of changes to apply on ${chalk.green('staging')} version config:` 70 ); 71 console.log(jsondiffpatch.formatters.console.format(delta!, previousVersions)); 72 73 const isCorrect = await askForCorrectnessAsync(); 74 75 if (isCorrect) { 76 // Save new configuration. 77 try { 78 await Versions.setVersionsAsync(newVersions); 79 } catch (error) { 80 console.error(error); 81 } 82 83 console.log( 84 chalk.green('\nSuccessfully updated staging config. You can check it out on'), 85 chalk.blue(`https://${STAGING_API_HOST}/--/api/v2/versions`) 86 ); 87 } else { 88 console.log(chalk.yellow('Canceled')); 89 } 90} 91 92async function resetStagingConfigurationAsync() { 93 // Get current production config. 94 Config.api.host = PRODUCTION_API_HOST; 95 const productionVersions = await Versions.versionsAsync(); 96 97 // Wait for the cache to invalidate. 98 await sleepAsync(10); 99 100 // Get current staging config. 101 Config.api.host = STAGING_API_HOST; 102 const stagingVersions = await Versions.versionsAsync(); 103 104 // Calculate the diff between them. 105 const delta = jsondiffpatch.diff(stagingVersions, productionVersions); 106 107 // Reset changes (if any) on staging. 108 await applyChangesToStagingAsync(delta, stagingVersions, productionVersions); 109} 110 111async function applyChangesToRootAsync(options: ActionOptions, versions: any) { 112 const newVersions = cloneDeep(versions); 113 if (options.key) { 114 if (!('value' in options) && !options.delete) { 115 console.log(chalk.red('`--key` flag requires `--value` or `--delete` flag.')); 116 return; 117 } 118 setConfigValueForKey(newVersions, options.key, options.delete ? undefined : options.value); 119 } 120 121 const delta = jsondiffpatch.diff(versions, newVersions); 122 123 await applyChangesToStagingAsync(delta, versions, newVersions); 124} 125 126async function applyChangesToSDKVersionAsync(options: ActionOptions, versions: any) { 127 const sdkVersions = Object.keys(versions.sdkVersions).sort(semver.rcompare); 128 const sdkVersion = options.sdkVersion || (await chooseSdkVersionAsync(sdkVersions)); 129 const containsSdk = sdkVersions.includes(sdkVersion); 130 131 if (!semver.valid(sdkVersion)) { 132 console.error(chalk.red(`Provided SDK version ${chalk.cyan(sdkVersion)} is invalid.`)); 133 return; 134 } 135 if (!containsSdk) { 136 const { addNewSdk } = await inquirer.prompt<{ addNewSdk: boolean }>([ 137 { 138 type: 'confirm', 139 name: 'addNewSdk', 140 message: `Configuration for SDK ${chalk.cyan( 141 sdkVersion 142 )} doesn't exist. Do you want to initialize it?`, 143 default: true, 144 }, 145 ]); 146 if (!addNewSdk) { 147 console.log(chalk.yellow('Canceled')); 148 return; 149 } 150 } 151 152 // If SDK is already there, make a deep clone of the sdkVersion config so we can calculate a diff later. 153 const sdkVersionConfig = containsSdk ? cloneDeep(versions.sdkVersions[sdkVersion]) : {}; 154 155 console.log(`\nUsing ${chalk.blue(STAGING_API_HOST)} host ...`); 156 console.log(`Using SDK ${chalk.cyan(sdkVersion)} ...`); 157 158 if ('deprecated' in options) { 159 setConfigValueForKey(sdkVersionConfig, 'isDeprecated', !!options.deprecated); 160 } 161 if ('releaseNoteUrl' in options && typeof options.releaseNoteUrl === 'string') { 162 setConfigValueForKey(sdkVersionConfig, 'releaseNoteUrl', options.releaseNoteUrl); 163 } 164 if (options.key) { 165 if (!('value' in options) && !options.delete) { 166 console.log(chalk.red('`--key` flag requires `--value` or `--delete` flag.')); 167 return; 168 } 169 setConfigValueForKey(sdkVersionConfig, options.key, options.delete ? undefined : options.value); 170 } 171 172 const newVersions = { 173 ...versions, 174 sdkVersions: { 175 ...versions.sdkVersions, 176 [sdkVersion]: sdkVersionConfig, 177 }, 178 }; 179 180 if (options.deleteSdk) { 181 delete newVersions.sdkVersions[sdkVersion]; 182 } 183 184 const delta = jsondiffpatch.diff( 185 versions.sdkVersions[sdkVersion], 186 newVersions.sdkVersions[sdkVersion] 187 ); 188 189 await applyChangesToStagingAsync(delta, versions.sdkVersions[sdkVersion], newVersions); 190} 191 192async function action(options: ActionOptions) { 193 if (options.reset) { 194 await resetStagingConfigurationAsync(); 195 return; 196 } 197 198 Config.api.host = STAGING_API_HOST; 199 const versions = await Versions.versionsAsync(); 200 201 if (options.root) { 202 await applyChangesToRootAsync(options, versions); 203 } else { 204 await applyChangesToSDKVersionAsync(options, versions); 205 } 206} 207 208export default (program: Command) => { 209 program 210 .command('update-versions-endpoint') 211 .alias('update-versions') 212 .description( 213 `Updates SDK configuration under ${chalk.blue('https://staging.exp.host/--/api/v2/versions')}` 214 ) 215 .option( 216 '-s, --sdkVersion [string]', 217 'SDK version to update. Can be chosen from the list if not provided.' 218 ) 219 .option( 220 '--root', 221 'Modify a key at the root of the versions config rather than a specific SDK version.', 222 false 223 ) 224 .option('-d, --deprecated [boolean]', 'Sets chosen SDK version as deprecated.') 225 .option('-r, --release-note-url [string]', 'URL pointing to the release blog post.') 226 .option('-k, --key [string]', 'A custom, dotted key that you want to set in the configuration.') 227 .option('-v, --value [any]', 'Value for the custom key to be set in the configuration.') 228 .option('--delete', 'Deletes config entry under key specified by `--key` flag.', false) 229 .option( 230 '--delete-sdk', 231 'Deletes configuration for SDK specified by `--sdkVersion` flag.', 232 false 233 ) 234 .option('--reset', 'Resets changes on staging to the state from production.', false) 235 .asyncAction(action); 236}; 237