diff --git a/packages/expo-cli/package.json b/packages/expo-cli/package.json index 38a3c13535..adc8f81efa 100644 --- a/packages/expo-cli/package.json +++ b/packages/expo-cli/package.json @@ -87,7 +87,6 @@ "cli-table3": "^0.6.0", "command-exists": "^1.2.8", "commander": "2.17.1", - "concat-stream": "1.6.2", "dateformat": "3.0.3", "env-editor": "^0.4.1", "envinfo": "7.5.0", diff --git a/packages/expo-cli/src/exp.ts b/packages/expo-cli/src/exp.ts index 5f68c53aa5..72e9230c4c 100755 --- a/packages/expo-cli/src/exp.ts +++ b/packages/expo-cli/src/exp.ts @@ -16,7 +16,6 @@ import url from 'url'; import wrapAnsi from 'wrap-ansi'; import { Analytics, - Api, ApiV2, Binaries, Config, @@ -46,7 +45,6 @@ import { ora } from './utils/ora'; // directory const packageJSON = require('../package.json'); -Api.setClientName(packageJSON.version); ApiV2.setClientName(packageJSON.version); // The following prototyped functions are not used here, but within in each file found in `./commands` diff --git a/packages/xdl/src/Api.ts b/packages/xdl/src/Api.ts deleted file mode 100644 index d445fe616e..0000000000 --- a/packages/xdl/src/Api.ts +++ /dev/null @@ -1,263 +0,0 @@ -import axios, { AxiosRequestConfig, Canceler } from 'axios'; -import concat from 'concat-stream'; -import FormData from 'form-data'; -import fs from 'fs-extra'; -import path from 'path'; - -import { - API_V2_MAX_BODY_LENGTH, - API_V2_MAX_CONTENT_LENGTH, - Config, - ConnectionStatus, - Extract, - Session, - UserManager, - UserSettings, - XDLError, -} from './internal'; - -const TIMER_DURATION = 30000; -const TIMEOUT = 3600000; - -let exponentClient = 'xdl'; - -type HttpMethod = 'get' | 'post' | 'put' | 'delete'; -type RequestOptions = AxiosRequestConfig & { formData?: FormData }; - -class ApiError extends Error { - readonly name = 'ApiError'; - code: string; - readonly _isApiError = true; - serverError: any; - - constructor(code: string, message: string) { - super(message); - this.code = code; - } -} - -// These aren't constants because some commands switch between staging and prod -function _rootBaseUrl() { - return `${Config.api.scheme}://${Config.api.host}`; -} - -function _apiBaseUrl() { - let rootBaseUrl = _rootBaseUrl(); - if (Config.api.port) { - rootBaseUrl += ':' + Config.api.port; - } - return rootBaseUrl + '/--/api'; -} - -async function _callMethodAsync( - url: string, - method: HttpMethod = 'get', - requestBody: any, - requestOptions: RequestOptions, - returnEntireResponse = false -) { - const clientId = await Session.clientIdAsync(); - const session = await UserManager.getSessionAsync(); - const skipValidationToken = process.env['EXPO_SKIP_MANIFEST_VALIDATION_TOKEN']; - - const headers: any = { - 'Exp-ClientId': clientId, - 'Exponent-Client': exponentClient, - }; - - if (skipValidationToken) { - headers['Exp-Skip-Manifest-Validation-Token'] = skipValidationToken; - } - - // Handle auth method, prioritizing authorization tokens before session secrets - if (session?.accessToken) { - headers['Authorization'] = `Bearer ${session.accessToken}`; - } else if (session?.sessionSecret) { - headers['Expo-Session'] = session.sessionSecret; - } - - let options: AxiosRequestConfig = { - url, - method, - headers, - maxContentLength: API_V2_MAX_CONTENT_LENGTH, - maxBodyLength: API_V2_MAX_BODY_LENGTH, - }; - - if (requestBody) { - options = { - ...options, - data: requestBody, - }; - } - - if (requestOptions.formData) { - const { formData, ...rest } = requestOptions; - const convertedFormData = await _convertFormDataToBuffer(formData); - const { data } = convertedFormData; - options.headers = { - ...options.headers, - ...formData.getHeaders(), - }; - options = { ...options, data, ...rest }; - } else { - options = { ...options, ...requestOptions }; - } - - if (!requestOptions.hasOwnProperty('timeout') && ConnectionStatus.isOffline()) { - options.timeout = 1; - } - - const response = await axios.request(options); - if (!response) { - throw new Error('Unexpected error: Request failed.'); - } - const responseBody = response.data; - let responseObj; - if (typeof responseBody === 'string') { - try { - responseObj = JSON.parse(responseBody); - } catch (e) { - throw new XDLError( - 'INVALID_JSON', - 'Invalid JSON returned from API: ' + e + '. Response body: ' + responseBody - ); - } - } else { - responseObj = responseBody; - } - if (responseObj.err) { - const err = new ApiError( - responseObj.code || 'API_ERROR', - 'API Response Error: ' + responseObj.err - ); - err.serverError = responseObj.err; - throw err; - } else { - return returnEntireResponse ? response : responseObj; - } -} - -async function _convertFormDataToBuffer(formData: FormData): Promise<{ data: Buffer }> { - return new Promise(resolve => { - formData.pipe(concat({ encoding: 'buffer' }, data => resolve({ data }))); - }); -} - -type ProgressCallback = (progressPercentage: number) => void; -type RetryCallback = (cancel: Canceler) => void; - -async function _downloadAsync( - url: string, - outputPath: string, - progressFunction?: ProgressCallback, - retryFunction?: RetryCallback -) { - let promptShown = false; - let currentProgress = 0; - - const { cancel, token } = axios.CancelToken.source(); - - let warningTimer = setTimeout(() => { - if (retryFunction) { - retryFunction(cancel); - } - promptShown = true; - }, TIMER_DURATION); - - const tmpPath = `${outputPath}.download`; - const config: AxiosRequestConfig = { - timeout: TIMEOUT, - responseType: 'stream', - cancelToken: token, - }; - const response = await axios(url, config); - await new Promise(resolve => { - const totalDownloadSize = response.data.headers['content-length']; - let downloadProgress = 0; - response.data - .on('data', (chunk: Buffer) => { - downloadProgress += chunk.length; - const roundedProgress = Math.floor((downloadProgress / totalDownloadSize) * 100); - if (currentProgress !== roundedProgress) { - currentProgress = roundedProgress; - clearTimeout(warningTimer); - if (!promptShown) { - warningTimer = setTimeout(() => { - if (retryFunction) { - retryFunction(cancel); - } - promptShown = true; - }, TIMER_DURATION); - } - if (progressFunction) { - progressFunction(roundedProgress); - } - } - }) - .on('end', () => { - clearTimeout(warningTimer); - if (progressFunction && currentProgress !== 100) { - progressFunction(100); - } - resolve(); - }) - .pipe(fs.createWriteStream(tmpPath)); - }); - await fs.rename(tmpPath, outputPath); -} - -/** @deprecated use ApiV2, got or GraphQL depending on use case. */ -export default class ApiClient { - static host: string = Config.api.host; - static port: number = Config.api.port || 80; - - static setClientName(name: string) { - exponentClient = name; - } - - static async callMethodAsync( - methodName: string, - args: any, - method?: HttpMethod, - requestBody?: any, - requestOptions: RequestOptions = {}, - returnEntireResponse: boolean = false - ) { - const url = - _apiBaseUrl() + - '/' + - encodeURIComponent(methodName) + - '/' + - encodeURIComponent(JSON.stringify(args)); - return _callMethodAsync(url, method, requestBody, requestOptions, returnEntireResponse); - } - - static async callPathAsync( - path: string, - method?: HttpMethod, - requestBody?: any, - requestOptions: RequestOptions = {} - ) { - const url = _rootBaseUrl() + path; - return _callMethodAsync(url, method, requestBody, requestOptions); - } - - static async downloadAsync( - url: string, - outputPath: string, - { extract = false } = {}, - progressFunction?: ProgressCallback, - retryFunction?: RetryCallback - ): Promise { - if (extract) { - const dotExpoHomeDirectory = UserSettings.dotExpoHomeDirectory(); - const tmpPath = path.join(dotExpoHomeDirectory, 'tmp-download-file'); - await _downloadAsync(url, tmpPath, progressFunction); - await Extract.extractAsync(tmpPath, outputPath); - fs.removeSync(tmpPath); - } else { - await _downloadAsync(url, outputPath, progressFunction, retryFunction); - } - } -} diff --git a/packages/xdl/src/ApiV2.ts b/packages/xdl/src/ApiV2.ts index 75c59b86bd..6850b5f99a 100644 --- a/packages/xdl/src/ApiV2.ts +++ b/packages/xdl/src/ApiV2.ts @@ -7,8 +7,8 @@ import QueryString from 'querystring'; import { Config, ConnectionStatus } from './internal'; -export const MAX_CONTENT_LENGTH = 100 /* MB */ * 1024 * 1024; -export const MAX_BODY_LENGTH = 100 /* MB */ * 1024 * 1024; +const MAX_CONTENT_LENGTH = 100 /* MB */ * 1024 * 1024; +const MAX_BODY_LENGTH = 100 /* MB */ * 1024 * 1024; // These aren't constants because some commands switch between staging and prod function _rootBaseUrl() { diff --git a/packages/xdl/src/Simulator.ts b/packages/xdl/src/Simulator.ts index 34fd915379..f253b89623 100644 --- a/packages/xdl/src/Simulator.ts +++ b/packages/xdl/src/Simulator.ts @@ -10,9 +10,9 @@ import semver from 'semver'; import { Analytics, - Api, BundleIdentifier, delayAsync, + downloadAppAsync, learnMore, Logger, NotificationCode, @@ -424,7 +424,7 @@ export async function _downloadSimulatorAppAsync( fs.mkdirpSync(dir); try { - await Api.downloadAsync(url, dir, { extract: true }, downloadProgressCallback); + await downloadAppAsync(url, dir, { extract: true }, downloadProgressCallback); } catch (e) { fs.removeSync(dir); throw e; diff --git a/packages/xdl/src/index.ts b/packages/xdl/src/index.ts index da68efc08e..abf256bb31 100644 --- a/packages/xdl/src/index.ts +++ b/packages/xdl/src/index.ts @@ -6,7 +6,6 @@ if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { export { Analytics, Android, - Api, ApiV2, Binaries, Config, diff --git a/packages/xdl/src/internal.ts b/packages/xdl/src/internal.ts index 88e0d78b61..ae6ae23ba4 100644 --- a/packages/xdl/src/internal.ts +++ b/packages/xdl/src/internal.ts @@ -19,12 +19,7 @@ export { default as NotificationCode } from './NotificationCode'; export { learnMore } from './logs/TerminalLink'; export { default as Analytics } from './Analytics'; export * as Android from './Android'; -export { default as Api } from './Api'; -export { - default as ApiV2, - MAX_BODY_LENGTH as API_V2_MAX_BODY_LENGTH, - MAX_CONTENT_LENGTH as API_V2_MAX_CONTENT_LENGTH, -} from './ApiV2'; +export { default as ApiV2 } from './ApiV2'; export * as Binaries from './Binaries'; export * as EmbeddedAssets from './EmbeddedAssets'; export { ErrorCode } from './ErrorCode'; @@ -83,6 +78,7 @@ export * as ExpSchema from './project/ExpSchema'; export { delayAsync } from './utils/delayAsync'; export { choosePortAsync } from './utils/choosePortAsync'; export { downloadApkAsync } from './utils/downloadApkAsync'; +export { downloadAppAsync } from './utils/downloadAppAsync'; export * as BundleIdentifier from './BundleIdentifier'; export * as FsCache from './tools/FsCache'; export * as WebpackEnvironment from './webpack-utils/WebpackEnvironment'; diff --git a/packages/xdl/src/utils/downloadApkAsync.ts b/packages/xdl/src/utils/downloadApkAsync.ts index 25f3b978df..b161622a43 100644 --- a/packages/xdl/src/utils/downloadApkAsync.ts +++ b/packages/xdl/src/utils/downloadApkAsync.ts @@ -1,7 +1,7 @@ import fs from 'fs-extra'; import path from 'path'; -import { Api, UserSettings, Versions } from '../internal'; +import { downloadAppAsync, UserSettings, Versions } from '../internal'; function _apkCacheDirectory() { const dotExpoHomeDirectory = UserSettings.dotExpoHomeDirectory(); @@ -26,6 +26,6 @@ export async function downloadApkAsync( return apkPath; } - await Api.downloadAsync(url, apkPath, undefined, downloadProgressCallback); + await downloadAppAsync(url, apkPath, undefined, downloadProgressCallback); return apkPath; } diff --git a/packages/xdl/src/utils/downloadAppAsync.ts b/packages/xdl/src/utils/downloadAppAsync.ts new file mode 100644 index 0000000000..5847e73335 --- /dev/null +++ b/packages/xdl/src/utils/downloadAppAsync.ts @@ -0,0 +1,89 @@ +import axios, { AxiosRequestConfig, Canceler } from 'axios'; +import fs from 'fs-extra'; +import path from 'path'; + +import { Extract, UserSettings } from '../internal'; + +const TIMER_DURATION = 30000; +const TIMEOUT = 3600000; + +type ProgressCallback = (progressPercentage: number) => void; +type RetryCallback = (cancel: Canceler) => void; + +async function _downloadAsync( + url: string, + outputPath: string, + progressFunction?: ProgressCallback, + retryFunction?: RetryCallback +) { + let promptShown = false; + let currentProgress = 0; + + const { cancel, token } = axios.CancelToken.source(); + + let warningTimer = setTimeout(() => { + if (retryFunction) { + retryFunction(cancel); + } + promptShown = true; + }, TIMER_DURATION); + + const tmpPath = `${outputPath}.download`; + const config: AxiosRequestConfig = { + timeout: TIMEOUT, + responseType: 'stream', + cancelToken: token, + }; + const response = await axios(url, config); + await new Promise(resolve => { + const totalDownloadSize = response.data.headers['content-length']; + let downloadProgress = 0; + response.data + .on('data', (chunk: Buffer) => { + downloadProgress += chunk.length; + const roundedProgress = Math.floor((downloadProgress / totalDownloadSize) * 100); + if (currentProgress !== roundedProgress) { + currentProgress = roundedProgress; + clearTimeout(warningTimer); + if (!promptShown) { + warningTimer = setTimeout(() => { + if (retryFunction) { + retryFunction(cancel); + } + promptShown = true; + }, TIMER_DURATION); + } + if (progressFunction) { + progressFunction(roundedProgress); + } + } + }) + .on('end', () => { + clearTimeout(warningTimer); + if (progressFunction && currentProgress !== 100) { + progressFunction(100); + } + resolve(); + }) + .pipe(fs.createWriteStream(tmpPath)); + }); + await fs.rename(tmpPath, outputPath); +} + +export async function downloadAppAsync( + url: string, + outputPath: string, + { extract = false } = {}, + progressFunction?: ProgressCallback, + retryFunction?: RetryCallback +): Promise { + if (extract) { + const dotExpoHomeDirectory = UserSettings.dotExpoHomeDirectory(); + const tmpPath = path.join(dotExpoHomeDirectory, 'tmp-download-file'); + await _downloadAsync(url, tmpPath, progressFunction); + await Extract.extractAsync(tmpPath, outputPath); + fs.removeSync(tmpPath); + } else { + await _downloadAsync(url, outputPath, progressFunction, retryFunction); + } +}