1import { getExpoHomeDirectory } from '@expo/config/build/getUserState'; 2import path from 'path'; 3import ProgressBar from 'progress'; 4import { gt } from 'semver'; 5 6import { downloadAppAsync } from './downloadAppAsync'; 7import { CommandError } from './errors'; 8import { ora } from './ora'; 9import { profile } from './profile'; 10import { createProgressBar } from './progress'; 11import { getVersionsAsync, SDKVersion } from '../api/getVersions'; 12import { Log } from '../log'; 13 14const debug = require('debug')('expo:utils:downloadExpoGo') as typeof console.log; 15 16const platformSettings: Record< 17 string, 18 { 19 shouldExtractResults: boolean; 20 versionsKey: keyof SDKVersion; 21 getFilePath: (filename: string) => string; 22 } 23> = { 24 ios: { 25 versionsKey: 'iosClientUrl', 26 getFilePath: (filename) => 27 path.join(getExpoHomeDirectory(), 'ios-simulator-app-cache', `${filename}.app`), 28 shouldExtractResults: true, 29 }, 30 android: { 31 versionsKey: 'androidClientUrl', 32 getFilePath: (filename) => 33 path.join(getExpoHomeDirectory(), 'android-apk-cache', `${filename}.apk`), 34 shouldExtractResults: false, 35 }, 36}; 37 38/** 39 * @internal exposed for testing. 40 * @returns the matching `SDKVersion` object from the Expo API. 41 */ 42export async function getExpoGoVersionEntryAsync(sdkVersion: string): Promise<SDKVersion> { 43 const { sdkVersions: versions } = await getVersionsAsync(); 44 let version: SDKVersion; 45 46 if (sdkVersion.toUpperCase() === 'UNVERSIONED') { 47 // find the latest version 48 const latestVersionKey = Object.keys(versions).reduce((a, b) => { 49 if (gt(b, a)) { 50 return b; 51 } 52 return a; 53 }, '0.0.0'); 54 55 Log.warn( 56 `Downloading the latest Expo Go client (${latestVersionKey}). This will not fully conform to UNVERSIONED.` 57 ); 58 version = versions[latestVersionKey]; 59 } else { 60 version = versions[sdkVersion]; 61 } 62 63 if (!version) { 64 throw new CommandError(`Unable to find a version of Expo Go for SDK ${sdkVersion}`); 65 } 66 return version; 67} 68 69/** Download the Expo Go app from the Expo servers (if only it was this easy for every app). */ 70export async function downloadExpoGoAsync( 71 platform: keyof typeof platformSettings, 72 { 73 url, 74 sdkVersion, 75 }: { 76 url?: string; 77 sdkVersion?: string; 78 } 79): Promise<string> { 80 const { getFilePath, versionsKey, shouldExtractResults } = platformSettings[platform]; 81 82 const spinner = ora({ text: 'Fetching Expo Go', color: 'white' }).start(); 83 84 let bar: ProgressBar | null = null; 85 86 try { 87 if (!url) { 88 if (!sdkVersion) { 89 throw new CommandError( 90 `Unable to determine which Expo Go version to install (platform: ${platform})` 91 ); 92 } 93 94 const version = await getExpoGoVersionEntryAsync(sdkVersion); 95 96 debug(`Installing Expo Go version for SDK ${sdkVersion} at URL: ${version[versionsKey]}`); 97 url = version[versionsKey] as string; 98 } 99 } catch (error) { 100 spinner.fail(); 101 throw error; 102 } 103 104 const filename = path.parse(url).name; 105 106 try { 107 const outputPath = getFilePath(filename); 108 debug(`Downloading Expo Go from "${url}" to "${outputPath}".`); 109 debug( 110 `The requested copy of Expo Go might already be cached in: "${getExpoHomeDirectory()}". You can disable the cache with EXPO_NO_CACHE=1` 111 ); 112 await profile(downloadAppAsync)({ 113 url, 114 // Save all encrypted cache data to `~/.expo/expo-go` 115 cacheDirectory: 'expo-go', 116 outputPath, 117 extract: shouldExtractResults, 118 onProgress({ progress, total }) { 119 if (progress && total) { 120 if (!bar) { 121 if (spinner.isSpinning) { 122 spinner.stop(); 123 } 124 bar = createProgressBar('Downloading the Expo Go app [:bar] :percent :etas', { 125 width: 64, 126 total: 100, 127 // clear: true, 128 complete: '=', 129 incomplete: ' ', 130 }); 131 } else { 132 bar!.update(progress, total); 133 } 134 } 135 }, 136 }); 137 return outputPath; 138 } finally { 139 spinner.stop(); 140 // @ts-expect-error 141 bar?.terminate(); 142 } 143} 144