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