eas-cli 16.14.0 → 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.
@@ -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 {};
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMultipartBodyFromFilesAsync = exports.multipartContentType = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
6
+ const CRLF = '\r\n';
7
+ const BOUNDARY_HYPHEN_CHARS = '--';
8
+ const BOUNDARY_ID = '----formdata-eas-cli';
9
+ const FORM_FOOTER = `${BOUNDARY_HYPHEN_CHARS}${BOUNDARY_ID}${BOUNDARY_HYPHEN_CHARS}${CRLF}${CRLF}`;
10
+ const encodeName = (input) => {
11
+ return input.replace(/["\n\\]/g, (c) => {
12
+ switch (c) {
13
+ case '\\':
14
+ return '\\\\';
15
+ case '"':
16
+ return '%22';
17
+ case '\n':
18
+ return '%0A';
19
+ default:
20
+ return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;
21
+ }
22
+ });
23
+ };
24
+ async function* createReadStreamAsync(filePath) {
25
+ for await (const raw of node_fs_1.default.createReadStream(filePath)) {
26
+ const chunk = raw;
27
+ yield new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
28
+ }
29
+ }
30
+ const makeFormHeader = (params) => {
31
+ const name = encodeName(params.name);
32
+ let header = BOUNDARY_HYPHEN_CHARS + BOUNDARY_ID + CRLF;
33
+ header += `Content-Disposition: form-data; name="${name}"; filename="${name}"`;
34
+ if (params.contentType) {
35
+ header += `${CRLF}Content-Type: ${params.contentType}`;
36
+ }
37
+ if (params.contentLength) {
38
+ header += `${CRLF}Content-Length: ${params.contentLength}`;
39
+ }
40
+ header += CRLF;
41
+ header += CRLF;
42
+ return header;
43
+ };
44
+ exports.multipartContentType = `multipart/form-data; boundary=${BOUNDARY_ID}`;
45
+ async function* createMultipartBodyFromFilesAsync(entries, onProgressUpdate) {
46
+ const encoder = new TextEncoder();
47
+ for (let idx = 0; idx < entries.length; idx++) {
48
+ const entry = entries[idx];
49
+ const header = makeFormHeader({
50
+ name: entry.name,
51
+ contentType: entry.contentType,
52
+ contentLength: entry.contentLength,
53
+ });
54
+ yield encoder.encode(header);
55
+ yield* createReadStreamAsync(entry.filePath);
56
+ yield encoder.encode(CRLF);
57
+ if (onProgressUpdate) {
58
+ onProgressUpdate((idx + 1) / entries.length);
59
+ }
60
+ }
61
+ yield encoder.encode(FORM_FOOTER);
62
+ }
63
+ exports.createMultipartBodyFromFilesAsync = createMultipartBodyFromFilesAsync;
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "16.14.0",
2
+ "version": "16.15.0",
3
3
  "commands": {
4
4
  "analytics": {
5
5
  "id": "analytics",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eas-cli",
3
3
  "description": "EAS command line tool",
4
- "version": "16.14.0",
4
+ "version": "16.15.0",
5
5
  "author": "Expo <support@expo.dev>",
6
6
  "bin": {
7
7
  "eas": "./bin/run"
@@ -241,5 +241,5 @@
241
241
  "node": "20.11.0",
242
242
  "yarn": "1.22.21"
243
243
  },
244
- "gitHead": "bf27ec5238e337c0a0bdba524e5815e0e646f8e9"
244
+ "gitHead": "72f3ba817fcf4f210ef9786c2346d76ce32a0378"
245
245
  }