1import { Command } from '@expo/commander'; 2import aws from 'aws-sdk'; 3import chalk from 'chalk'; 4import fs from 'fs-extra'; 5import inquirer from 'inquirer'; 6import path from 'path'; 7 8import { EXPO_DIR } from '../Constants'; 9import askForPlatformAsync from '../utils/askForPlatformAsync'; 10import askForSdkVersionAsync from '../utils/askForSDKVersionAsync'; 11import { link } from '../Formatter'; 12import Git from '../Git'; 13import logger from '../Logger'; 14import { modifySdkVersionsAsync, getSdkVersionsAsync } from '../Versions'; 15import { ClientBuilder, Platform } from '../client-build/types'; 16import IosClientBuilder from '../client-build/IosClientBuilder'; 17import AndroidClientBuilder from '../client-build/AndroidClientBuilder'; 18import { getNewestSDKVersionAsync } from '../ProjectVersions'; 19 20const s3Client = new aws.S3({ region: 'us-east-1' }); 21const { yellow, blue, magenta } = chalk; 22 23type ActionOptions = { 24 platform?: Platform; 25 release: boolean; 26}; 27 28export default (program: Command) => { 29 program 30 .command('client-build') 31 .alias('cb') 32 .description( 33 'Builds Expo client for iOS simulator or APK for Android, uploads the archive to S3 and saves its url to versions endpoint.' 34 ) 35 .option('-p, --platform [string]', 'Platform for which the client will be built.') 36 .option( 37 '-r, --release', 38 'Whether to upload and release the client build to staging versions endpoint.', 39 false 40 ) 41 .asyncAction(main); 42}; 43 44async function main(options: ActionOptions) { 45 const platform = options.platform || (await askForPlatformAsync()); 46 const sdkBranchVersion = await Git.getSDKVersionFromBranchNameAsync(); 47 48 if (options.release && !sdkBranchVersion) { 49 throw new Error(`Client builds can be released only from the release branch!`); 50 } 51 52 const builder = getBuilderForPlatform(platform); 53 const sdkVersion = 54 sdkBranchVersion || 55 (await askForSdkVersionAsync(platform, await getNewestSDKVersionAsync(platform))); 56 const appVersion = await builder.getAppVersionAsync(); 57 58 await buildOrUseCacheAsync(builder); 59 60 if (sdkVersion && options.release) { 61 await uploadAsync(builder, sdkVersion, appVersion); 62 await releaseAsync(builder, sdkVersion, appVersion); 63 } 64} 65 66function getBuilderForPlatform(platform: Platform): ClientBuilder { 67 switch (platform) { 68 case 'ios': 69 return new IosClientBuilder(); 70 case 'android': 71 return new AndroidClientBuilder(); 72 default: { 73 throw new Error(`Platform "${platform}" is not supported yet!`); 74 } 75 } 76} 77 78async function askToRecreateSimulatorBuildAsync(): Promise<boolean> { 79 if (process.env.CI) { 80 return false; 81 } 82 const { createNew } = await inquirer.prompt<{ createNew: boolean }>([ 83 { 84 type: 'confirm', 85 name: 'createNew', 86 message: 'Do you want to create a fresh one?', 87 default: true, 88 }, 89 ]); 90 return createNew; 91} 92 93async function askToOverrideBuildAsync(): Promise<boolean> { 94 if (process.env.CI) { 95 // we should never override anything in CI, too easy to accidentally mess something up in prod 96 return false; 97 } 98 const { override } = await inquirer.prompt<{ override: boolean }>([ 99 { 100 type: 'confirm', 101 name: 'override', 102 message: 'Do you want to override it?', 103 default: true, 104 }, 105 ]); 106 return override; 107} 108 109async function buildOrUseCacheAsync(builder: ClientBuilder): Promise<void> { 110 const appPath = builder.getAppPath(); 111 112 // Build directory already exists, we could reuse that one — especially useful on the CI. 113 if (await fs.pathExists(appPath)) { 114 const relativeAppPath = path.relative(EXPO_DIR, appPath); 115 logger.info(`Client build already exists at ${magenta.bold(relativeAppPath)}`); 116 117 if (!(await askToRecreateSimulatorBuildAsync())) { 118 logger.info('Skipped building the app, using cached build instead...'); 119 return; 120 } 121 } 122 await builder.buildAsync(); 123} 124 125async function uploadAsync( 126 builder: ClientBuilder, 127 sdkVersion: string, 128 appVersion: string 129): Promise<void> { 130 const sdkVersions = await getSdkVersionsAsync(sdkVersion); 131 132 // Target app url already defined in versions endpoint. 133 // We make this check to reduce the risk of unintentional overrides. 134 if (sdkVersions?.[`${builder.platform}ClientUrl`] === builder.getClientUrl(appVersion)) { 135 logger.info(`Build ${yellow.bold(appVersion)} is already defined in versions endpoint.`); 136 logger.info('The new build would be uploaded onto the same URL.'); 137 138 if (!(await askToOverrideBuildAsync())) { 139 logger.warn('Refused overriding the build, exiting the proces...'); 140 process.exit(0); 141 return; 142 } 143 } 144 logger.info(`Uploading ${yellow.bold(appVersion)} build`); 145 146 await builder.uploadBuildAsync(s3Client, appVersion); 147} 148 149async function releaseAsync( 150 builder: ClientBuilder, 151 sdkVersion: string, 152 appVersion: string 153): Promise<void> { 154 const clientUrl = builder.getClientUrl(appVersion); 155 156 logger.info( 157 `Updating versions endpoint with client url ${blue.bold(link(clientUrl, clientUrl))}` 158 ); 159 160 await updateClientUrlAndVersionAsync(builder, sdkVersion, appVersion); 161} 162 163async function updateClientUrlAndVersionAsync( 164 builder: ClientBuilder, 165 sdkVersion: string, 166 appVersion: string 167) { 168 await modifySdkVersionsAsync(sdkVersion, (sdkVersions) => { 169 sdkVersions[`${builder.platform}ClientUrl`] = builder.getClientUrl(appVersion); 170 sdkVersions[`${builder.platform}ClientVersion`] = appVersion; 171 return sdkVersions; 172 }); 173} 174