eas-cli 16.14.1 → 16.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,11 +14,11 @@ const log_1 = tslib_1.__importStar(require("../../log"));
14
14
  const ora_1 = require("../../ora");
15
15
  const projectUtils_1 = require("../../project/projectUtils");
16
16
  const json_1 = require("../../utils/json");
17
- const progress_1 = require("../../utils/progress");
18
17
  const WorkerAssets = tslib_1.__importStar(require("../../worker/assets"));
19
18
  const deployment_1 = require("../../worker/deployment");
20
19
  const upload_1 = require("../../worker/upload");
21
20
  const logs_1 = require("../../worker/utils/logs");
21
+ const MAX_UPLOAD_SIZE = 5e8; // 500MB
22
22
  const isDirectory = (directoryPath) => node_fs_1.default.promises
23
23
  .stat(directoryPath)
24
24
  .then(stat => stat.isDirectory())
@@ -96,14 +96,11 @@ class WorkerDeploy extends EasCommand_1.default {
96
96
  }
97
97
  }
98
98
  async function uploadTarballAsync(tarPath, uploadUrl) {
99
+ const payload = { filePath: tarPath };
99
100
  const { response } = await (0, upload_1.uploadAsync)({
100
- url: uploadUrl,
101
- filePath: tarPath,
102
- compress: false,
103
- headers: {
104
- accept: 'application/json',
105
- },
106
- });
101
+ baseURL: uploadUrl,
102
+ method: 'POST',
103
+ }, payload);
107
104
  if (response.status === 413) {
108
105
  throw new Error('Upload failed! (Payload too large)\n' +
109
106
  `The files in "${path.relative(projectDir, projectDist.path)}" (at: ${projectDir}) exceed the maximum file size (10MB gzip).`);
@@ -116,7 +113,7 @@ class WorkerDeploy extends EasCommand_1.default {
116
113
  if (!json.success || !json.result || typeof json.result !== 'object') {
117
114
  throw new Error(json.message ? `Upload failed: ${json.message}` : 'Upload failed!');
118
115
  }
119
- const { id, fullName, token } = json.result;
116
+ const { id, fullName, token, upload } = json.result;
120
117
  if (typeof token !== 'string') {
121
118
  throw new Error('Upload failed: API failed to return a deployment token');
122
119
  }
@@ -126,56 +123,57 @@ class WorkerDeploy extends EasCommand_1.default {
126
123
  else if (typeof fullName !== 'string') {
127
124
  throw new Error('Upload failed: API failed to return a script name');
128
125
  }
126
+ else if (!Array.isArray(upload) && upload !== undefined) {
127
+ throw new Error('Upload failed: API returned invalid asset upload instructions');
128
+ }
129
129
  const baseURL = new URL('/', uploadUrl).toString();
130
- return { id, fullName, baseURL, token };
130
+ return { id, fullName, baseURL, token, upload };
131
131
  }
132
132
  }
133
- async function uploadAssetsAsync(assetMap, deployParams) {
134
- const uploadParams = [];
135
- const assetPath = projectDist.type === 'server' ? projectDist.clientPath : projectDist.path;
136
- if (!assetPath) {
137
- return;
133
+ async function uploadAssetsAsync(assetFiles, deployParams) {
134
+ const baseURL = new URL('/asset/', deployParams.baseURL);
135
+ const uploadInit = { baseURL, method: 'POST' };
136
+ uploadInit.baseURL.searchParams.set('token', deployParams.token);
137
+ const uploadPayloads = [];
138
+ if (deployParams.upload) {
139
+ const assetsBySHA512 = assetFiles.reduce((map, asset) => {
140
+ map.set(asset.sha512, asset);
141
+ return map;
142
+ }, new Map());
143
+ const payloads = deployParams.upload
144
+ .map(instruction => instruction.sha512.map(sha512 => {
145
+ const asset = assetsBySHA512.get(sha512);
146
+ if (!asset) {
147
+ // NOTE(@kitten): This should never happen
148
+ throw new Error(`Uploading assets failed: API instructed us to upload an asset that does not exist`);
149
+ }
150
+ return asset;
151
+ }))
152
+ .filter(assets => assets && assets.length > 0)
153
+ .map(assets => (assets.length > 1 ? { multipart: assets } : { asset: assets[0] }));
154
+ uploadPayloads.push(...payloads);
138
155
  }
139
- for await (const asset of WorkerAssets.listAssetMapFilesAsync(assetPath, assetMap)) {
140
- const uploadURL = new URL(`/asset/${asset.sha512}`, deployParams.baseURL);
141
- uploadURL.searchParams.set('token', deployParams.token);
142
- uploadParams.push({ url: uploadURL.toString(), filePath: asset.path });
156
+ else {
157
+ // NOTE(@kitten): Legacy format which uploads assets one-by-one
158
+ uploadPayloads.push(...assetFiles.map(asset => ({ asset })));
143
159
  }
144
- const progress = {
145
- total: uploadParams.length,
146
- pending: 0,
147
- percent: 0,
148
- transferred: 0,
149
- };
150
- const updateProgress = (0, progress_1.createProgressTracker)({
151
- total: progress.total,
152
- message(ratio) {
153
- const percent = `${Math.floor(ratio * 100)}`;
154
- const details = chalk_1.default.dim(`(${progress.pending} Pending, ${progress.transferred} Completed, ${progress.total} Total)`);
155
- return `Uploading assets: ${percent.padStart(3)}% ${details}`;
156
- },
157
- completedMessage: 'Uploaded assets',
158
- });
160
+ const progressTotal = uploadPayloads.reduce((acc, payload) => acc + ('multipart' in payload ? payload.multipart.length : 1), 0);
161
+ const progressTracker = (0, upload_1.createProgressBar)(`Uploading ${progressTotal} assets`);
159
162
  try {
160
- for await (const signal of (0, upload_1.batchUploadAsync)(uploadParams)) {
161
- if ('response' in signal) {
162
- progress.pending--;
163
- progress.percent = ++progress.transferred / progress.total;
164
- }
165
- else {
166
- progress.pending++;
167
- }
168
- updateProgress({ progress });
163
+ for await (const signal of (0, upload_1.batchUploadAsync)(uploadInit, uploadPayloads, progressTracker.update)) {
164
+ progressTracker.update(signal.progress);
169
165
  }
170
166
  }
171
167
  catch (error) {
172
- updateProgress({ isComplete: true, error });
168
+ progressTracker.stop();
173
169
  throw error;
174
170
  }
175
- updateProgress({ isComplete: true });
171
+ finally {
172
+ progressTracker.stop();
173
+ }
176
174
  }
177
- let assetMap;
178
175
  let tarPath;
176
+ let assetFiles;
179
177
  let deployResult;
180
178
  let progress = (0, ora_1.ora)('Preparing project').start();
181
179
  try {
@@ -189,9 +187,9 @@ class WorkerDeploy extends EasCommand_1.default {
189
187
  'In case of conflict, the EAS environment variable values will be used: ' +
190
188
  manifestResult.conflictingVariableNames.join(' '));
191
189
  }
192
- assetMap = await WorkerAssets.createAssetMapAsync(projectDist.type === 'server' ? projectDist.clientPath : projectDist.path);
190
+ assetFiles = await WorkerAssets.collectAssetsAsync(projectDist.type === 'server' ? projectDist.clientPath : projectDist.path, { maxFileSize: MAX_UPLOAD_SIZE });
193
191
  tarPath = await WorkerAssets.packFilesIterableAsync(emitWorkerTarballAsync({
194
- assetMap,
192
+ assetMap: WorkerAssets.assetsToAssetsMap(assetFiles),
195
193
  manifest: manifestResult.manifest,
196
194
  }));
197
195
  if (flags.dryRun) {
@@ -232,7 +230,7 @@ class WorkerDeploy extends EasCommand_1.default {
232
230
  }
233
231
  throw error;
234
232
  }
235
- await uploadAssetsAsync(assetMap, deployResult);
233
+ await uploadAssetsAsync(assetFiles, deployResult);
236
234
  await finalizeDeployAsync(deployResult);
237
235
  let deploymentAlias = null;
238
236
  if (flags.aliasName) {
@@ -6,11 +6,24 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGr
6
6
  import { EnvironmentVariableEnvironment } from '../graphql/generated';
7
7
  interface AssetMapOptions {
8
8
  hashOptions?: HashOptions;
9
+ maxFileSize: number;
9
10
  }
11
+ export interface AssetFileEntry {
12
+ normalizedPath: string;
13
+ path: string;
14
+ size: number;
15
+ sha512: string;
16
+ type: string | null;
17
+ }
18
+ /** Collects assets from a given target path */
19
+ export declare function collectAssetsAsync(assetPath: string | undefined, options: AssetMapOptions): Promise<AssetFileEntry[]>;
10
20
  /** Mapping of normalized file paths to a SHA512 hash */
11
- export type AssetMap = Record<string, string>;
12
- /** Creates an asset map of a given target path */
13
- declare function createAssetMapAsync(assetPath?: string, options?: AssetMapOptions): Promise<AssetMap>;
21
+ export type AssetMap = Record<string, string | {
22
+ sha512: string;
23
+ size: number;
24
+ }>;
25
+ /** Converts array of asset entries into AssetMap (as sent to deployment-api) */
26
+ export declare function assetsToAssetsMap(assets: AssetFileEntry[]): AssetMap;
14
27
  export interface Manifest {
15
28
  env: Record<string, string | undefined>;
16
29
  }
@@ -31,16 +44,9 @@ interface WorkerFileEntry {
31
44
  data: Buffer | string;
32
45
  }
33
46
  /** Reads worker files while normalizing sourcemaps and providing normalized paths */
34
- declare function listWorkerFilesAsync(workerPath: string): AsyncGenerator<WorkerFileEntry>;
35
- interface AssetFileEntry {
36
- normalizedPath: string;
37
- sha512: string;
38
- path: string;
39
- }
40
- /** Reads files of an asset maps and enumerates normalized paths and data */
41
- declare function listAssetMapFilesAsync(assetPath: string, assetMap: AssetMap): AsyncGenerator<AssetFileEntry>;
47
+ export declare function listWorkerFilesAsync(workerPath: string): AsyncGenerator<WorkerFileEntry>;
42
48
  /** Entry of a normalized (gzip-safe) path and file data */
43
49
  export type FileEntry = readonly [normalizedPath: string, data: Buffer | string];
44
50
  /** Packs file entries into a tar.gz file (path to tgz returned) */
45
- declare function packFilesIterableAsync(iterable: Iterable<FileEntry> | AsyncIterable<FileEntry>, options?: GzipOptions): Promise<string>;
46
- export { createAssetMapAsync, listWorkerFilesAsync, listAssetMapFilesAsync, packFilesIterableAsync, };
51
+ export declare function packFilesIterableAsync(iterable: Iterable<FileEntry> | AsyncIterable<FileEntry>, options?: GzipOptions): Promise<string>;
52
+ export {};
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.packFilesIterableAsync = exports.listAssetMapFilesAsync = exports.listWorkerFilesAsync = exports.createAssetMapAsync = exports.createManifestAsync = void 0;
3
+ exports.packFilesIterableAsync = exports.listWorkerFilesAsync = exports.createManifestAsync = exports.assetsToAssetsMap = exports.collectAssetsAsync = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const env_1 = require("@expo/env");
6
+ const mime_1 = tslib_1.__importDefault(require("mime"));
6
7
  const minizlib_1 = require("minizlib");
7
8
  const node_crypto_1 = require("node:crypto");
8
9
  const node_fs_1 = tslib_1.__importStar(require("node:fs"));
@@ -50,9 +51,12 @@ function listFilesRecursively(basePath) {
50
51
  continue;
51
52
  }
52
53
  else if (dirent.isFile()) {
54
+ const absolutePath = node_path_1.default.resolve(target, dirent.name);
55
+ const stats = await node_fs_1.default.promises.stat(absolutePath);
53
56
  yield {
54
57
  normalizedPath,
55
- path: node_path_1.default.resolve(target, dirent.name),
58
+ path: absolutePath,
59
+ size: stats.size,
56
60
  };
57
61
  }
58
62
  else if (dirent.isDirectory()) {
@@ -62,17 +66,54 @@ function listFilesRecursively(basePath) {
62
66
  }
63
67
  return recurseAsync();
64
68
  }
65
- /** Creates an asset map of a given target path */
66
- async function createAssetMapAsync(assetPath, options) {
67
- const map = Object.create(null);
69
+ async function determineMimeTypeAsync(filePath) {
70
+ let contentType = mime_1.default.getType(node_path_1.default.basename(filePath));
71
+ if (!contentType) {
72
+ const fileContent = await node_fs_1.default.promises.readFile(filePath, 'utf-8');
73
+ try {
74
+ // check if file is valid JSON without an extension, e.g. for the apple app site association file
75
+ const parsedData = JSON.parse(fileContent);
76
+ if (parsedData) {
77
+ contentType = 'application/json';
78
+ }
79
+ }
80
+ catch { }
81
+ }
82
+ return contentType;
83
+ }
84
+ /** Collects assets from a given target path */
85
+ async function collectAssetsAsync(assetPath, options) {
86
+ const assets = [];
68
87
  if (assetPath) {
69
88
  for await (const file of listFilesRecursively(assetPath)) {
70
- map[file.normalizedPath] = await computeSha512HashAsync(file.path, options?.hashOptions);
89
+ if (file.size > options.maxFileSize) {
90
+ throw new Error(`Upload of "${file.normalizedPath}" aborted: File size is greater than the upload limit (>500MB)`);
91
+ }
92
+ const sha512$ = computeSha512HashAsync(file.path, options?.hashOptions);
93
+ const contentType$ = determineMimeTypeAsync(file.path);
94
+ assets.push({
95
+ normalizedPath: file.normalizedPath,
96
+ path: file.path,
97
+ size: file.size,
98
+ sha512: await sha512$,
99
+ type: await contentType$,
100
+ });
71
101
  }
72
102
  }
73
- return map;
103
+ return assets;
74
104
  }
75
- exports.createAssetMapAsync = createAssetMapAsync;
105
+ exports.collectAssetsAsync = collectAssetsAsync;
106
+ /** Converts array of asset entries into AssetMap (as sent to deployment-api) */
107
+ function assetsToAssetsMap(assets) {
108
+ return assets.reduce((map, entry) => {
109
+ map[entry.normalizedPath] = {
110
+ sha512: entry.sha512,
111
+ size: entry.size,
112
+ };
113
+ return map;
114
+ }, Object.create(null));
115
+ }
116
+ exports.assetsToAssetsMap = assetsToAssetsMap;
76
117
  /** Creates a manifest configuration sent up for deployment */
77
118
  async function createManifestAsync(params, graphqlClient) {
78
119
  // Resolve .env file variables
@@ -110,18 +151,6 @@ async function* listWorkerFilesAsync(workerPath) {
110
151
  }
111
152
  }
112
153
  exports.listWorkerFilesAsync = listWorkerFilesAsync;
113
- /** Reads files of an asset maps and enumerates normalized paths and data */
114
- async function* listAssetMapFilesAsync(assetPath, assetMap) {
115
- for (const normalizedPath in assetMap) {
116
- const filePath = node_path_1.default.resolve(assetPath, normalizedPath.split('/').join(node_path_1.default.sep));
117
- yield {
118
- normalizedPath,
119
- path: filePath,
120
- sha512: assetMap[normalizedPath],
121
- };
122
- }
123
- }
124
- exports.listAssetMapFilesAsync = listAssetMapFilesAsync;
125
154
  /** Packs file entries into a tar.gz file (path to tgz returned) */
126
155
  async function packFilesIterableAsync(iterable, options) {
127
156
  const writePath = `${await createTempWritePathAsync()}.tar.gz`;
@@ -1,23 +1,35 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
3
  import { HeadersInit, RequestInit, Response } from 'node-fetch';
4
- export interface UploadParams extends Omit<RequestInit, 'signal' | 'body'> {
4
+ import { AssetFileEntry } from './assets';
5
+ export type UploadPayload = {
5
6
  filePath: string;
6
- compress?: boolean;
7
- url: string;
7
+ } | {
8
+ asset: AssetFileEntry;
9
+ } | {
10
+ multipart: AssetFileEntry[];
11
+ };
12
+ export interface UploadRequestInit {
13
+ baseURL: string | URL;
8
14
  method?: string;
9
15
  headers?: HeadersInit;
10
- body?: undefined;
11
16
  signal?: AbortSignal;
12
17
  }
13
18
  export interface UploadResult {
14
- params: UploadParams;
19
+ payload: UploadPayload;
15
20
  response: Response;
16
21
  }
17
- export declare function uploadAsync(params: UploadParams): Promise<UploadResult>;
22
+ type OnProgressUpdateCallback = (progress: number) => void;
23
+ export declare function uploadAsync(init: UploadRequestInit, payload: UploadPayload, onProgressUpdate?: OnProgressUpdateCallback): Promise<UploadResult>;
18
24
  export declare function callUploadApiAsync(url: string | URL, init?: RequestInit): Promise<unknown>;
19
25
  export interface UploadPending {
20
- params: UploadParams;
26
+ payload: UploadPayload;
27
+ progress: number;
21
28
  }
22
- export type BatchUploadSignal = UploadResult | UploadPending;
23
- export declare function batchUploadAsync(uploads: readonly UploadParams[]): AsyncGenerator<BatchUploadSignal>;
29
+ export declare function batchUploadAsync(init: UploadRequestInit, payloads: UploadPayload[], onProgressUpdate?: OnProgressUpdateCallback): AsyncGenerator<UploadPending>;
30
+ interface UploadProgressBar {
31
+ update(progress: number): void;
32
+ stop(): void;
33
+ }
34
+ export declare function createProgressBar(label?: string): UploadProgressBar;
35
+ export {};
@@ -1,53 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.batchUploadAsync = exports.callUploadApiAsync = exports.uploadAsync = void 0;
3
+ exports.createProgressBar = exports.batchUploadAsync = exports.callUploadApiAsync = exports.uploadAsync = void 0;
4
4
  const tslib_1 = require("tslib");
5
+ const cli_progress_1 = tslib_1.__importDefault(require("cli-progress"));
5
6
  const https = tslib_1.__importStar(require("https"));
6
7
  const https_proxy_agent_1 = tslib_1.__importDefault(require("https-proxy-agent"));
7
- const mime_1 = tslib_1.__importDefault(require("mime"));
8
- const minizlib_1 = require("minizlib");
9
8
  const node_fetch_1 = tslib_1.__importStar(require("node-fetch"));
10
- const node_fs_1 = tslib_1.__importStar(require("node:fs"));
11
- const promises_1 = require("node:fs/promises");
12
- const node_path_1 = tslib_1.__importDefault(require("node:path"));
9
+ const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
10
+ const node_os_1 = tslib_1.__importDefault(require("node:os"));
11
+ const node_stream_1 = require("node:stream");
13
12
  const promise_retry_1 = tslib_1.__importDefault(require("promise-retry"));
13
+ const multipart_1 = require("./utils/multipart");
14
+ const MAX_CONCURRENCY = Math.min(10, Math.max(node_os_1.default.availableParallelism() * 2, 20));
14
15
  const MAX_RETRIES = 4;
15
- const MAX_CONCURRENCY = 10;
16
- const MIN_RETRY_TIMEOUT = 100;
17
- const MAX_UPLOAD_SIZE = 5e8; // 5MB
18
- const MIN_COMPRESSION_SIZE = 5e4; // 50kB
19
- const isCompressible = (contentType, size) => {
20
- if (size < MIN_COMPRESSION_SIZE) {
21
- // Don't compress small files
22
- return false;
23
- }
24
- else if (contentType && /^(?:audio|video|image)\//i.test(contentType)) {
25
- // Never compress images, audio, or videos as they're presumably precompressed
26
- return false;
27
- }
28
- else if (contentType && /^application\//i.test(contentType)) {
29
- // Only compress `application/` files if they're marked as XML/JSON/JS
30
- return /(?:xml|json5?|javascript)$/i.test(contentType);
31
- }
32
- else {
33
- return true;
34
- }
35
- };
36
- const getContentTypeAsync = async (filePath) => {
37
- let contentType = mime_1.default.getType(node_path_1.default.basename(filePath));
38
- if (!contentType) {
39
- const fileContent = await (0, promises_1.readFile)(filePath, 'utf-8');
40
- try {
41
- // check if file is valid JSON without an extension, e.g. for the apple app site association file
42
- const parsedData = JSON.parse(fileContent);
43
- if (parsedData) {
44
- contentType = 'application/json';
45
- }
46
- }
47
- catch { }
48
- }
49
- return contentType;
50
- };
51
16
  let sharedAgent;
52
17
  const getAgent = () => {
53
18
  if (sharedAgent) {
@@ -66,36 +31,56 @@ const getAgent = () => {
66
31
  }));
67
32
  }
68
33
  };
69
- async function uploadAsync(params) {
70
- const { filePath, signal, compress, method = 'POST', url, headers: headersInit, ...requestInit } = params;
71
- const stat = await node_fs_1.default.promises.stat(filePath);
72
- if (stat.size > MAX_UPLOAD_SIZE) {
73
- throw new Error(`Upload of "${filePath}" aborted: File size is greater than the upload limit (>500MB)`);
74
- }
75
- const contentType = await getContentTypeAsync(filePath);
34
+ async function uploadAsync(init, payload, onProgressUpdate) {
76
35
  return await (0, promise_retry_1.default)(async (retry) => {
77
- const headers = new node_fetch_1.Headers(headersInit);
78
- if (contentType) {
79
- headers.set('content-type', contentType);
80
- }
81
- let bodyStream = (0, node_fs_1.createReadStream)(filePath);
82
- if (compress && isCompressible(contentType, stat.size)) {
83
- const gzip = new minizlib_1.Gzip({ portable: true });
84
- bodyStream.on('error', error => gzip.emit('error', error));
85
- // @ts-expect-error: Gzip implements a Readable-like interface
86
- bodyStream = bodyStream.pipe(gzip);
87
- headers.set('content-encoding', 'gzip');
36
+ if (onProgressUpdate) {
37
+ onProgressUpdate(0);
38
+ }
39
+ const headers = new node_fetch_1.Headers(init.headers);
40
+ const url = new URL(`${init.baseURL}`);
41
+ let errorPrefix;
42
+ let body;
43
+ let method = init.method || 'POST';
44
+ if ('asset' in payload) {
45
+ const { asset } = payload;
46
+ errorPrefix = `Upload of "${asset.normalizedPath}" failed`;
47
+ if (asset.type) {
48
+ headers.set('content-type', asset.type);
49
+ }
50
+ if (asset.size) {
51
+ headers.set('content-length', `${asset.size}`);
52
+ }
53
+ method = 'POST';
54
+ url.pathname = `/asset/${asset.sha512}`;
55
+ body = node_fs_1.default.createReadStream(asset.path);
56
+ }
57
+ else if ('filePath' in payload) {
58
+ const { filePath } = payload;
59
+ errorPrefix = 'Worker deployment failed';
60
+ body = node_fs_1.default.createReadStream(filePath);
61
+ }
62
+ else if ('multipart' in payload) {
63
+ const { multipart } = payload;
64
+ errorPrefix = `Upload of ${multipart.length} assets failed`;
65
+ headers.set('content-type', multipart_1.multipartContentType);
66
+ method = 'PATCH';
67
+ url.pathname = '/asset/batch';
68
+ const iterator = (0, multipart_1.createMultipartBodyFromFilesAsync)(multipart.map(asset => ({
69
+ name: asset.sha512,
70
+ filePath: asset.path,
71
+ contentType: asset.type,
72
+ contentLength: asset.size,
73
+ })), onProgressUpdate);
74
+ body = node_stream_1.Readable.from(iterator);
88
75
  }
89
76
  let response;
90
77
  try {
91
- response = await (0, node_fetch_1.default)(params.url, {
92
- ...requestInit,
78
+ response = await (0, node_fetch_1.default)(url, {
93
79
  method,
94
- body: bodyStream,
80
+ body,
95
81
  headers,
96
82
  agent: getAgent(),
97
- // @ts-expect-error: Internal types don't match
98
- signal,
83
+ signal: init.signal,
99
84
  });
100
85
  }
101
86
  catch (error) {
@@ -113,11 +98,11 @@ async function uploadAsync(params) {
113
98
  if (rayId) {
114
99
  message += `\nReport this error quoting Request ID ${rayId}`;
115
100
  }
116
- return `Upload of "${filePath}" failed: ${message}`;
101
+ return `${errorPrefix}: ${message}`;
117
102
  }
118
103
  else {
119
104
  const json = await response.json().catch(() => null);
120
- return json?.error ?? `Upload of "${filePath}" failed: ${response.statusText}`;
105
+ return json?.error ?? `${errorPrefix}: ${response.statusText}`;
121
106
  }
122
107
  };
123
108
  if (response.status === 408 ||
@@ -127,21 +112,23 @@ async function uploadAsync(params) {
127
112
  return retry(new Error(await getErrorMessageAsync()));
128
113
  }
129
114
  else if (response.status === 413) {
130
- const message = `Upload of "${filePath}" failed: File size exceeded the upload limit`;
115
+ const message = `${errorPrefix}: File size exceeded the upload limit`;
131
116
  throw new Error(message);
132
117
  }
133
118
  else if (!response.ok) {
134
119
  throw new Error(await getErrorMessageAsync());
135
120
  }
121
+ else if (onProgressUpdate) {
122
+ onProgressUpdate(1);
123
+ }
136
124
  return {
137
- params,
125
+ payload,
138
126
  response,
139
127
  };
140
128
  }, {
141
129
  retries: MAX_RETRIES,
142
- minTimeout: MIN_RETRY_TIMEOUT,
143
- randomize: true,
144
- factor: 2,
130
+ minTimeout: 50,
131
+ randomize: false,
145
132
  });
146
133
  }
147
134
  exports.uploadAsync = uploadAsync;
@@ -169,19 +156,41 @@ async function callUploadApiAsync(url, init) {
169
156
  });
170
157
  }
171
158
  exports.callUploadApiAsync = callUploadApiAsync;
172
- async function* batchUploadAsync(uploads) {
159
+ async function* batchUploadAsync(init, payloads, onProgressUpdate) {
160
+ const progressTracker = new Array(payloads.length).fill(0);
173
161
  const controller = new AbortController();
174
162
  const queue = new Set();
163
+ const initWithSignal = { ...init, signal: controller.signal };
164
+ const getProgressValue = () => {
165
+ const progress = progressTracker.reduce((acc, value) => acc + value, 0);
166
+ return progress / payloads.length;
167
+ };
168
+ const sendProgressUpdate = onProgressUpdate &&
169
+ (() => {
170
+ onProgressUpdate(getProgressValue());
171
+ });
175
172
  try {
176
173
  let index = 0;
177
- while (index < uploads.length || queue.size > 0) {
178
- while (queue.size < MAX_CONCURRENCY && index < uploads.length) {
179
- const uploadParams = uploads[index++];
180
- let uploadPromise;
181
- queue.add((uploadPromise = uploadAsync({ ...uploadParams, signal: controller.signal }).finally(() => queue.delete(uploadPromise))));
182
- yield { params: uploadParams };
174
+ while (index < payloads.length || queue.size > 0) {
175
+ while (queue.size < MAX_CONCURRENCY && index < payloads.length) {
176
+ const currentIndex = index++;
177
+ const payload = payloads[currentIndex];
178
+ const onChildProgressUpdate = sendProgressUpdate &&
179
+ ((progress) => {
180
+ progressTracker[currentIndex] = progress;
181
+ sendProgressUpdate();
182
+ });
183
+ const uploadPromise = uploadAsync(initWithSignal, payload, onChildProgressUpdate).finally(() => {
184
+ queue.delete(uploadPromise);
185
+ progressTracker[currentIndex] = 1;
186
+ });
187
+ queue.add(uploadPromise);
188
+ yield { payload, progress: getProgressValue() };
183
189
  }
184
- yield await Promise.race(queue);
190
+ yield {
191
+ ...(await Promise.race(queue)),
192
+ progress: getProgressValue(),
193
+ };
185
194
  }
186
195
  if (queue.size > 0) {
187
196
  controller.abort();
@@ -194,3 +203,16 @@ async function* batchUploadAsync(uploads) {
194
203
  }
195
204
  }
196
205
  exports.batchUploadAsync = batchUploadAsync;
206
+ function createProgressBar(label = 'Uploading assets') {
207
+ const queueProgressBar = new cli_progress_1.default.SingleBar({ format: `|{bar}| {percentage}% ${label}` }, cli_progress_1.default.Presets.rect);
208
+ queueProgressBar.start(1, 0);
209
+ return {
210
+ update(progress) {
211
+ queueProgressBar.update(progress);
212
+ },
213
+ stop() {
214
+ queueProgressBar.stop();
215
+ },
216
+ };
217
+ }
218
+ exports.createProgressBar = createProgressBar;
@@ -0,0 +1,10 @@
1
+ export interface MultipartFileEntry {
2
+ name: string;
3
+ filePath: string;
4
+ contentType: string | null;
5
+ contentLength: number | null;
6
+ }
7
+ export declare const multipartContentType = "multipart/form-data; boundary=----formdata-eas-cli";
8
+ type OnProgressUpdateCallback = (progress: number) => void;
9
+ export declare function createMultipartBodyFromFilesAsync(entries: MultipartFileEntry[], onProgressUpdate?: OnProgressUpdateCallback): AsyncGenerator<Uint8Array>;
10
+ export {};