@percy/client 1.12.0 → 1.14.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.
package/dist/client.js CHANGED
@@ -2,39 +2,38 @@ import fs from 'fs';
2
2
  import PercyEnv from '@percy/env';
3
3
  import { git } from '@percy/env/utils';
4
4
  import logger from '@percy/logger';
5
- import { pool, request, sha256hash, base64encode, getPackageJSON } from './utils.js'; // Default client API URL can be set with an env var for API development
5
+ import { pool, request, sha256hash, base64encode, getPackageJSON } from './utils.js';
6
6
 
7
+ // Default client API URL can be set with an env var for API development
7
8
  const {
8
9
  PERCY_CLIENT_API_URL = 'https://percy.io/api/v1'
9
10
  } = process.env;
10
- const pkg = getPackageJSON(import.meta.url); // Validate ID arguments
11
+ const pkg = getPackageJSON(import.meta.url);
11
12
 
13
+ // Validate ID arguments
12
14
  function validateId(type, id) {
13
15
  if (!id) throw new Error(`Missing ${type} ID`);
14
-
15
16
  if (!(typeof id === 'string' || typeof id === 'number')) {
16
17
  throw new Error(`Invalid ${type} ID`);
17
18
  }
18
- } // Validate project path arguments
19
-
19
+ }
20
20
 
21
+ // Validate project path arguments
21
22
  function validateProjectPath(path) {
22
23
  if (!path) throw new Error('Missing project path');
23
-
24
24
  if (!/^[^/]+?\/.+/.test(path)) {
25
25
  throw new Error(`Invalid project path. Expected "org/project" but received "${path}"`);
26
26
  }
27
- } // PercyClient is used to communicate with the Percy API to create and finalize
27
+ }
28
+
29
+ // PercyClient is used to communicate with the Percy API to create and finalize
28
30
  // builds and snapshot. Uses @percy/env to collect environment information used
29
31
  // during build creation.
30
-
31
-
32
32
  export class PercyClient {
33
33
  log = logger('client');
34
34
  env = new PercyEnv(process.env);
35
35
  clientInfo = new Set();
36
36
  environmentInfo = new Set();
37
-
38
37
  constructor({
39
38
  // read or write token, defaults to PERCY_TOKEN environment variable
40
39
  token,
@@ -50,55 +49,55 @@ export class PercyClient {
50
49
  });
51
50
  this.addClientInfo(clientInfo);
52
51
  this.addEnvironmentInfo(environmentInfo);
53
- } // Adds additional unique client info.
54
-
52
+ }
55
53
 
54
+ // Adds additional unique client info.
56
55
  addClientInfo(info) {
57
56
  for (let i of [].concat(info)) {
58
57
  if (i) this.clientInfo.add(i);
59
58
  }
60
- } // Adds additional unique environment info.
61
-
59
+ }
62
60
 
61
+ // Adds additional unique environment info.
63
62
  addEnvironmentInfo(info) {
64
63
  for (let i of [].concat(info)) {
65
64
  if (i) this.environmentInfo.add(i);
66
65
  }
67
- } // Stringifies client and environment info.
68
-
66
+ }
69
67
 
68
+ // Stringifies client and environment info.
70
69
  userAgent() {
71
70
  let client = new Set([`Percy/${/\w+$/.exec(this.apiUrl)}`].concat(`${pkg.name}/${pkg.version}`, ...this.clientInfo).filter(Boolean));
72
71
  let environment = new Set([...this.environmentInfo].concat(`node/${process.version}`, this.env.info).filter(Boolean));
73
72
  return `${[...client].join(' ')} (${[...environment].join('; ')})`;
74
- } // Checks for a Percy token and returns it.
75
-
73
+ }
76
74
 
75
+ // Checks for a Percy token and returns it.
77
76
  getToken() {
78
77
  let token = this.token || this.env.token;
79
78
  if (!token) throw new Error('Missing Percy token');
80
79
  return token;
81
- } // Returns common headers used for each request with additional
80
+ }
81
+
82
+ // Returns common headers used for each request with additional
82
83
  // headers. Throws an error when the token is missing, which is a required
83
84
  // authorization header.
84
-
85
-
86
85
  headers(headers) {
87
86
  return Object.assign({
88
87
  Authorization: `Token token=${this.getToken()}`,
89
88
  'User-Agent': this.userAgent()
90
89
  }, headers);
91
- } // Performs a GET request for an API endpoint with appropriate headers.
92
-
90
+ }
93
91
 
92
+ // Performs a GET request for an API endpoint with appropriate headers.
94
93
  get(path) {
95
94
  return request(`${this.apiUrl}/${path}`, {
96
95
  headers: this.headers(),
97
96
  method: 'GET'
98
97
  });
99
- } // Performs a POST request to a JSON API endpoint with appropriate headers.
100
-
98
+ }
101
99
 
100
+ // Performs a POST request to a JSON API endpoint with appropriate headers.
102
101
  post(path, body = {}) {
103
102
  return request(`${this.apiUrl}/${path}`, {
104
103
  headers: this.headers({
@@ -107,11 +106,11 @@ export class PercyClient {
107
106
  method: 'POST',
108
107
  body
109
108
  });
110
- } // Creates a build with optional build resources. Only one build can be
109
+ }
110
+
111
+ // Creates a build with optional build resources. Only one build can be
111
112
  // created at a time per instance so snapshots and build finalization can be
112
113
  // done more seemlessly without manually tracking build ids
113
-
114
-
115
114
  async createBuild({
116
115
  resources = []
117
116
  } = {}) {
@@ -150,10 +149,10 @@ export class PercyClient {
150
149
  }
151
150
  }
152
151
  });
153
- } // Finalizes the active build. When `all` is true, `all-shards=true` is
154
- // added as a query param so the API finalizes all other build shards.
155
-
152
+ }
156
153
 
154
+ // Finalizes the active build. When `all` is true, `all-shards=true` is
155
+ // added as a query param so the API finalizes all other build shards.
157
156
  async finalizeBuild(buildId, {
158
157
  all = false
159
158
  } = {}) {
@@ -161,25 +160,25 @@ export class PercyClient {
161
160
  let qs = all ? 'all-shards=true' : '';
162
161
  this.log.debug(`Finalizing build ${buildId}...`);
163
162
  return this.post(`builds/${buildId}/finalize?${qs}`);
164
- } // Retrieves build data by id. Requires a read access token.
165
-
163
+ }
166
164
 
165
+ // Retrieves build data by id. Requires a read access token.
167
166
  async getBuild(buildId) {
168
167
  validateId('build', buildId);
169
168
  this.log.debug(`Get build ${buildId}`);
170
169
  return this.get(`builds/${buildId}`);
171
- } // Retrieves project builds optionally filtered. Requires a read access token.
172
-
170
+ }
173
171
 
172
+ // Retrieves project builds optionally filtered. Requires a read access token.
174
173
  async getBuilds(project, filters = {}) {
175
174
  validateProjectPath(project);
176
175
  let qs = Object.keys(filters).map(k => Array.isArray(filters[k]) ? filters[k].map(v => `filter[${k}][]=${v}`).join('&') : `filter[${k}]=${filters[k]}`).join('&');
177
176
  this.log.debug(`Fetching builds for ${project}`);
178
177
  return this.get(`projects/${project}/builds?${qs}`);
179
- } // Resolves when the build has finished and is no longer pending or
180
- // processing. By default, will time out if no update after 10 minutes.
181
-
178
+ }
182
179
 
180
+ // Resolves when the build has finished and is no longer pending or
181
+ // processing. By default, will time out if no update after 10 minutes.
183
182
  waitForBuild({
184
183
  build,
185
184
  project,
@@ -194,42 +193,44 @@ export class PercyClient {
194
193
  } else if (project) {
195
194
  validateProjectPath(project);
196
195
  }
197
-
198
196
  commit || (commit = this.env.git.sha);
199
197
  if (!build && !commit) throw new Error('Missing build commit');
200
198
  let sha = commit && (git(`rev-parse ${commit}`) || commit);
201
-
202
199
  let fetchData = async () => {
203
200
  var _await$this$getBuilds;
204
-
205
201
  return build ? (await this.getBuild(build)).data : (_await$this$getBuilds = (await this.getBuilds(project, {
206
202
  sha
207
203
  })).data) === null || _await$this$getBuilds === void 0 ? void 0 : _await$this$getBuilds[0];
208
204
  };
205
+ this.log.debug(`Waiting for build ${build || `${project} (${commit})`}...`);
209
206
 
210
- this.log.debug(`Waiting for build ${build || `${project} (${commit})`}...`); // recursively poll every second until the build finishes
211
-
207
+ // recursively poll every second until the build finishes
212
208
  return new Promise((resolve, reject) => async function poll(last, t) {
213
209
  try {
214
210
  let data = await fetchData();
215
211
  let state = data === null || data === void 0 ? void 0 : data.attributes.state;
216
212
  let pending = !state || state === 'pending' || state === 'processing';
217
- let updated = JSON.stringify(data) !== JSON.stringify(last); // new data received
213
+ let updated = JSON.stringify(data) !== JSON.stringify(last);
218
214
 
215
+ // new data received
219
216
  if (updated) {
220
- t = Date.now(); // no new data within the timeout
217
+ t = Date.now();
218
+
219
+ // no new data within the timeout
221
220
  } else if (Date.now() - t >= timeout) {
222
221
  throw new Error(state == null ? 'Build not found' : 'Timeout exceeded with no updates');
223
- } // call progress every update after the first update
224
-
222
+ }
225
223
 
224
+ // call progress every update after the first update
226
225
  if ((last || pending) && updated) {
227
226
  onProgress === null || onProgress === void 0 ? void 0 : onProgress(data);
228
- } // not finished, poll again
229
-
227
+ }
230
228
 
229
+ // not finished, poll again
231
230
  if (pending) {
232
- return setTimeout(poll, interval, data, t); // build finished
231
+ return setTimeout(poll, interval, data, t);
232
+
233
+ // build finished
233
234
  } else {
234
235
  // ensure progress is called at least once
235
236
  if (!last) onProgress === null || onProgress === void 0 ? void 0 : onProgress(data);
@@ -241,11 +242,11 @@ export class PercyClient {
241
242
  reject(err);
242
243
  }
243
244
  }(null, Date.now()));
244
- } // Uploads a single resource to the active build. If `filepath` is provided,
245
+ }
246
+
247
+ // Uploads a single resource to the active build. If `filepath` is provided,
245
248
  // `content` is read from the filesystem. The sha is optional and will be
246
249
  // created from `content` if one is not provided.
247
-
248
-
249
250
  async uploadResource(buildId, {
250
251
  url,
251
252
  sha,
@@ -264,9 +265,9 @@ export class PercyClient {
264
265
  }
265
266
  }
266
267
  });
267
- } // Uploads resources to the active build concurrently, two at a time.
268
-
268
+ }
269
269
 
270
+ // Uploads resources to the active build concurrently, two at a time.
270
271
  async uploadResources(buildId, resources) {
271
272
  validateId('build', buildId);
272
273
  this.log.debug(`Uploading resources for ${buildId}...`);
@@ -275,9 +276,9 @@ export class PercyClient {
275
276
  yield this.uploadResource(buildId, resource);
276
277
  }
277
278
  }, this, 2);
278
- } // Creates a snapshot for the active build using the provided attributes.
279
-
279
+ }
280
280
 
281
+ // Creates a snapshot for the active build using the provided attributes.
281
282
  async createSnapshot(buildId, {
282
283
  name,
283
284
  widths,
@@ -291,18 +292,14 @@ export class PercyClient {
291
292
  validateId('build', buildId);
292
293
  this.addClientInfo(clientInfo);
293
294
  this.addEnvironmentInfo(environmentInfo);
294
-
295
295
  if (!this.clientInfo.size || !this.environmentInfo.size) {
296
296
  this.log.warn('Warning: Missing `clientInfo` and/or `environmentInfo` properties');
297
297
  }
298
-
299
298
  this.log.debug(`Creating snapshot: ${name}...`);
300
-
301
299
  for (let resource of resources) {
302
300
  if (resource.sha || resource.content || !resource.filepath) continue;
303
301
  resource.content = await fs.promises.readFile(resource.filepath);
304
302
  }
305
-
306
303
  return this.post(`builds/${buildId}/snapshots`, {
307
304
  data: {
308
305
  type: 'snapshots',
@@ -329,23 +326,21 @@ export class PercyClient {
329
326
  }
330
327
  }
331
328
  });
332
- } // Finalizes a snapshot.
333
-
329
+ }
334
330
 
331
+ // Finalizes a snapshot.
335
332
  async finalizeSnapshot(snapshotId) {
336
333
  validateId('snapshot', snapshotId);
337
334
  this.log.debug(`Finalizing snapshot ${snapshotId}...`);
338
335
  return this.post(`snapshots/${snapshotId}/finalize`);
339
- } // Convenience method for creating a snapshot for the active build, uploading
340
- // missing resources for the snapshot, and finalizing the snapshot.
341
-
336
+ }
342
337
 
338
+ // Convenience method for creating a snapshot for the active build, uploading
339
+ // missing resources for the snapshot, and finalizing the snapshot.
343
340
  async sendSnapshot(buildId, options) {
344
341
  var _snapshot$data$relati, _snapshot$data$relati2;
345
-
346
342
  let snapshot = await this.createSnapshot(buildId, options);
347
343
  let missing = (_snapshot$data$relati = snapshot.data.relationships) === null || _snapshot$data$relati === void 0 ? void 0 : (_snapshot$data$relati2 = _snapshot$data$relati['missing-resources']) === null || _snapshot$data$relati2 === void 0 ? void 0 : _snapshot$data$relati2.data;
348
-
349
344
  if (missing !== null && missing !== void 0 && missing.length) {
350
345
  let resources = options.resources.reduce((acc, r) => Object.assign(acc, {
351
346
  [r.sha]: r
@@ -354,11 +349,9 @@ export class PercyClient {
354
349
  id
355
350
  }) => resources[id]));
356
351
  }
357
-
358
352
  await this.finalizeSnapshot(snapshot.data.id);
359
353
  return snapshot;
360
354
  }
361
-
362
355
  async createComparison(snapshotId, {
363
356
  tag,
364
357
  tiles = [],
@@ -366,12 +359,10 @@ export class PercyClient {
366
359
  } = {}) {
367
360
  validateId('snapshot', snapshotId);
368
361
  this.log.debug(`Creating comparision: ${tag.name}...`);
369
-
370
362
  for (let tile of tiles) {
371
363
  if (tile.sha || tile.content || !tile.filepath) continue;
372
364
  tile.content = await fs.promises.readFile(tile.filepath);
373
365
  }
374
-
375
366
  return this.post(`snapshots/${snapshotId}/comparisons`, {
376
367
  data: {
377
368
  type: 'comparisons',
@@ -409,7 +400,6 @@ export class PercyClient {
409
400
  }
410
401
  });
411
402
  }
412
-
413
403
  async uploadComparisonTile(comparisonId, {
414
404
  index = 0,
415
405
  total = 1,
@@ -429,7 +419,6 @@ export class PercyClient {
429
419
  }
430
420
  });
431
421
  }
432
-
433
422
  async uploadComparisonTiles(comparisonId, tiles) {
434
423
  validateId('comparison', comparisonId);
435
424
  this.log.debug(`Uploading comparison tiles for ${comparisonId}...`);
@@ -443,13 +432,11 @@ export class PercyClient {
443
432
  }
444
433
  }, this, 2);
445
434
  }
446
-
447
435
  async finalizeComparison(comparisonId) {
448
436
  validateId('comparison', comparisonId);
449
437
  this.log.debug(`Finalizing comparison ${comparisonId}...`);
450
438
  return this.post(`comparisons/${comparisonId}/finalize`);
451
439
  }
452
-
453
440
  async sendComparison(buildId, options) {
454
441
  let snapshot = await this.createSnapshot(buildId, options);
455
442
  let comparison = await this.createComparison(snapshot.data.id, options);
@@ -457,6 +444,5 @@ export class PercyClient {
457
444
  await this.finalizeComparison(comparison.data.id);
458
445
  return comparison;
459
446
  }
460
-
461
447
  }
462
448
  export default PercyClient;
package/dist/proxy.js CHANGED
@@ -4,44 +4,48 @@ import http from 'http';
4
4
  import https from 'https';
5
5
  import logger from '@percy/logger';
6
6
  const CRLF = '\r\n';
7
- const STATUS_REG = /^HTTP\/1.[01] (\d*)/; // Returns true if the URL hostname matches any patterns
7
+ const STATUS_REG = /^HTTP\/1.[01] (\d*)/;
8
8
 
9
+ // Returns true if the URL hostname matches any patterns
9
10
  export function hostnameMatches(patterns, url) {
10
11
  let subject = new URL(url);
12
+
11
13
  /* istanbul ignore next: only strings are provided internally by the client proxy; core (which
12
14
  * borrows this util) sometimes provides an array of patterns or undefined */
13
-
14
15
  patterns = typeof patterns === 'string' ? patterns.split(/[\s,]+/) : [].concat(patterns);
15
-
16
16
  for (let pattern of patterns) {
17
17
  if (pattern === '*') return true;
18
- if (!pattern) continue; // parse pattern
18
+ if (!pattern) continue;
19
19
 
20
+ // parse pattern
20
21
  let {
21
22
  groups: rule
22
- } = pattern.match(/^(?<hostname>.+?)(?::(?<port>\d+))?$/); // missing a hostname or ports do not match
23
+ } = pattern.match(/^(?<hostname>.+?)(?::(?<port>\d+))?$/);
23
24
 
25
+ // missing a hostname or ports do not match
24
26
  if (!rule.hostname || rule.port && rule.port !== subject.port) {
25
27
  continue;
26
- } // wildcards are treated the same as leading dots
27
-
28
+ }
28
29
 
29
- rule.hostname = rule.hostname.replace(/^\*/, ''); // hostnames are equal or end with a wildcard rule
30
+ // wildcards are treated the same as leading dots
31
+ rule.hostname = rule.hostname.replace(/^\*/, '');
30
32
 
33
+ // hostnames are equal or end with a wildcard rule
31
34
  if (rule.hostname === subject.hostname || rule.hostname.startsWith('.') && subject.hostname.endsWith(rule.hostname)) {
32
35
  return true;
33
36
  }
34
37
  }
35
-
36
38
  return false;
37
- } // Returns the port number of a URL object. Defaults to port 443 for https
38
- // protocols or port 80 otherwise.
39
+ }
39
40
 
41
+ // Returns the port number of a URL object. Defaults to port 443 for https
42
+ // protocols or port 80 otherwise.
40
43
  export function port(options) {
41
44
  if (options.port) return options.port;
42
45
  return options.protocol === 'https:' ? 443 : 80;
43
- } // Returns a string representation of a URL-like object
46
+ }
44
47
 
48
+ // Returns a string representation of a URL-like object
45
49
  export function href(options) {
46
50
  let {
47
51
  protocol,
@@ -53,72 +57,64 @@ export function href(options) {
53
57
  } = options;
54
58
  return `${protocol}//${hostname}:${port(options)}` + (path || `${pathname || ''}${search || ''}${hash || ''}`);
55
59
  }
56
- ; // Returns the proxy URL for a set of request options
60
+ ;
57
61
 
62
+ // Returns the proxy URL for a set of request options
58
63
  export function getProxy(options) {
59
64
  let proxyUrl = options.protocol === 'https:' && (process.env.https_proxy || process.env.HTTPS_PROXY) || process.env.http_proxy || process.env.HTTP_PROXY;
60
65
  let shouldProxy = !!proxyUrl && !hostnameMatches(process.env.no_proxy || process.env.NO_PROXY, href(options));
61
-
62
66
  if (shouldProxy) {
63
67
  proxyUrl = new URL(proxyUrl);
64
68
  let isHttps = proxyUrl.protocol === 'https:';
65
-
66
69
  if (!isHttps && proxyUrl.protocol !== 'http:') {
67
70
  throw new Error(`Unsupported proxy protocol: ${proxyUrl.protocol}`);
68
71
  }
69
-
70
72
  let proxy = {
71
73
  isHttps
72
74
  };
73
75
  proxy.auth = !!proxyUrl.username && 'Basic ' + (proxyUrl.password ? Buffer.from(`${proxyUrl.username}:${proxyUrl.password}`) : Buffer.from(proxyUrl.username)).toString('base64');
74
76
  proxy.host = proxyUrl.hostname;
75
77
  proxy.port = port(proxyUrl);
76
-
77
78
  proxy.connect = () => (isHttps ? tls : net).connect({
78
79
  rejectUnauthorized: options.rejectUnauthorized,
79
80
  host: proxy.host,
80
81
  port: proxy.port
81
82
  });
82
-
83
83
  return proxy;
84
84
  }
85
- } // Proxified http agent
85
+ }
86
86
 
87
+ // Proxified http agent
87
88
  export class ProxyHttpAgent extends http.Agent {
88
89
  // needed for https proxies
89
90
  httpsAgent = new https.Agent({
90
91
  keepAlive: true
91
92
  });
92
-
93
93
  addRequest(request, options) {
94
94
  var _request$outputData;
95
-
96
95
  let proxy = getProxy(options);
97
96
  if (!proxy) return super.addRequest(request, options);
98
- logger('client:proxy').debug(`Proxying request: ${options.href}`); // modify the request for proxying
97
+ logger('client:proxy').debug(`Proxying request: ${options.href}`);
99
98
 
99
+ // modify the request for proxying
100
100
  request.path = href(options);
101
-
102
101
  if (proxy.auth) {
103
102
  request.setHeader('Proxy-Authorization', proxy.auth);
104
- } // regenerate headers since we just changed things
105
-
103
+ }
106
104
 
105
+ // regenerate headers since we just changed things
107
106
  delete request._header;
108
-
109
107
  request._implicitHeader();
110
-
111
108
  if (((_request$outputData = request.outputData) === null || _request$outputData === void 0 ? void 0 : _request$outputData.length) > 0) {
112
109
  let first = request.outputData[0].data;
113
110
  let endOfHeaders = first.indexOf(CRLF.repeat(2)) + 4;
114
111
  request.outputData[0].data = request._header + first.substring(endOfHeaders);
115
- } // coerce the connection to the proxy
116
-
112
+ }
117
113
 
114
+ // coerce the connection to the proxy
118
115
  options.port = proxy.port;
119
116
  options.host = proxy.host;
120
117
  delete options.path;
121
-
122
118
  if (proxy.isHttps) {
123
119
  // use the underlying https agent to complete the connection
124
120
  request.agent = this.httpsAgent;
@@ -127,9 +123,9 @@ export class ProxyHttpAgent extends http.Agent {
127
123
  return super.addRequest(request, options);
128
124
  }
129
125
  }
126
+ }
130
127
 
131
- } // Proxified https agent
132
-
128
+ // Proxified https agent
133
129
  export class ProxyHttpsAgent extends https.Agent {
134
130
  constructor(options) {
135
131
  // default keep-alive
@@ -138,56 +134,47 @@ export class ProxyHttpsAgent extends https.Agent {
138
134
  ...options
139
135
  });
140
136
  }
141
-
142
137
  createConnection(options, callback) {
143
138
  let proxy = getProxy(options);
144
139
  if (!proxy) return super.createConnection(options, callback);
145
- logger('client:proxy').debug(`Proxying request: ${href(options)}`); // generate proxy connect message
140
+ logger('client:proxy').debug(`Proxying request: ${href(options)}`);
146
141
 
142
+ // generate proxy connect message
147
143
  let host = `${options.hostname}:${port(options)}`;
148
144
  let connectMessage = [`CONNECT ${host} HTTP/1.1`, `Host: ${host}`];
149
-
150
145
  if (proxy.auth) {
151
146
  connectMessage.push(`Proxy-Authorization: ${proxy.auth}`);
152
147
  }
153
-
154
148
  connectMessage = connectMessage.join(CRLF);
155
- connectMessage += CRLF.repeat(2); // start the proxy connection and setup listeners
149
+ connectMessage += CRLF.repeat(2);
156
150
 
151
+ // start the proxy connection and setup listeners
157
152
  let socket = proxy.connect();
158
-
159
153
  let handleError = err => {
160
154
  socket.destroy(err);
161
155
  callback(err);
162
156
  };
163
-
164
157
  let handleClose = () => handleError(new Error('Connection closed while sending request to upstream proxy'));
165
-
166
158
  let buffer = '';
167
-
168
159
  let handleData = data => {
169
160
  var _buffer$match;
170
-
171
- buffer += data.toString(); // haven't received end of headers yet, keep buffering
172
-
173
- if (!buffer.includes(CRLF.repeat(2))) return; // stop listening after end of headers
174
-
161
+ buffer += data.toString();
162
+ // haven't received end of headers yet, keep buffering
163
+ if (!buffer.includes(CRLF.repeat(2))) return;
164
+ // stop listening after end of headers
175
165
  socket.off('data', handleData);
176
-
177
166
  if (((_buffer$match = buffer.match(STATUS_REG)) === null || _buffer$match === void 0 ? void 0 : _buffer$match[1]) !== '200') {
178
167
  return handleError(new Error('Error establishing proxy connection. ' + `Response from server was: ${buffer}`));
179
168
  }
180
-
181
169
  options.socket = socket;
182
- options.servername = options.hostname; // callback not passed in so not to be added as a listener
183
-
170
+ options.servername = options.hostname;
171
+ // callback not passed in so not to be added as a listener
184
172
  callback(null, super.createConnection(options));
185
- }; // send and handle the connect message
186
-
173
+ };
187
174
 
175
+ // send and handle the connect message
188
176
  socket.on('error', handleError).on('close', handleClose).on('data', handleData).write(connectMessage);
189
177
  }
190
-
191
178
  }
192
179
  export function proxyAgentFor(url, options) {
193
180
  let cache = proxyAgentFor.cache || (proxyAgentFor.cache = new Map());
@@ -196,10 +183,8 @@ export function proxyAgentFor(url, options) {
196
183
  hostname
197
184
  } = new URL(url);
198
185
  let cachekey = `${protocol}//${hostname}`;
199
-
200
186
  if (!cache.has(cachekey)) {
201
187
  cache.set(cachekey, protocol === 'https:' ? new ProxyHttpsAgent(options) : new ProxyHttpAgent(options));
202
188
  }
203
-
204
189
  return cache.get(cachekey);
205
190
  }
package/dist/utils.js CHANGED
@@ -2,16 +2,19 @@ import os from 'os';
2
2
  import fs from 'fs';
3
3
  import url from 'url';
4
4
  import path from 'path';
5
- import crypto from 'crypto'; // Returns a sha256 hash of a string.
5
+ import crypto from 'crypto';
6
6
 
7
+ // Returns a sha256 hash of a string.
7
8
  export function sha256hash(content) {
8
9
  return crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
9
- } // Returns a base64 encoding of a string or buffer.
10
+ }
10
11
 
12
+ // Returns a base64 encoding of a string or buffer.
11
13
  export function base64encode(content) {
12
14
  return Buffer.from(content).toString('base64');
13
- } // Returns the package.json content at the package path.
15
+ }
14
16
 
17
+ // Returns the package.json content at the package path.
15
18
  export function getPackageJSON(rel) {
16
19
  /* istanbul ignore else: sanity check */
17
20
  if (rel.startsWith('file:')) rel = url.fileURLToPath(rel);
@@ -19,32 +22,31 @@ export function getPackageJSON(rel) {
19
22
  if (fs.existsSync(pkg)) return JSON.parse(fs.readFileSync(pkg));
20
23
  let dir = path.dirname(rel);
21
24
  /* istanbul ignore else: sanity check */
22
-
23
25
  if (dir !== rel && dir !== os.homedir()) return getPackageJSON(dir);
24
- } // Creates a concurrent pool of promises created by the given generator.
26
+ }
27
+
28
+ // Creates a concurrent pool of promises created by the given generator.
25
29
  // Resolves when the generator's final promise resolves and rejects when any
26
30
  // generated promise rejects.
27
-
28
31
  export function pool(generator, context, concurrency) {
29
32
  return new Promise((resolve, reject) => {
30
33
  let iterator = generator.call(context);
31
34
  let queue = 0;
32
35
  let ret = [];
33
- let err; // generates concurrent promises
36
+ let err;
34
37
 
38
+ // generates concurrent promises
35
39
  let proceed = () => {
36
40
  while (queue < concurrency) {
37
41
  let {
38
42
  done,
39
43
  value: promise
40
44
  } = iterator.next();
41
-
42
45
  if (done || err) {
43
46
  if (!queue && err) reject(err);
44
47
  if (!queue) resolve(ret);
45
48
  return;
46
49
  }
47
-
48
50
  queue++;
49
51
  promise.then(value => {
50
52
  queue--;
@@ -56,49 +58,53 @@ export function pool(generator, context, concurrency) {
56
58
  proceed();
57
59
  });
58
60
  }
59
- }; // start generating promises
60
-
61
+ };
61
62
 
63
+ // start generating promises
62
64
  proceed();
63
65
  });
64
- } // Returns a promise that resolves or rejects when the provided function calls
66
+ }
67
+
68
+ // Returns a promise that resolves or rejects when the provided function calls
65
69
  // `resolve` or `reject` respectively. The third function argument, `retry`,
66
70
  // will recursively call the function at the specified interval until retries
67
71
  // are exhausted, at which point the promise will reject with the last error
68
72
  // passed to `retry`.
69
-
70
73
  export function retry(fn, {
71
74
  retries = 5,
72
75
  interval = 50
73
76
  }) {
74
77
  return new Promise((resolve, reject) => {
75
- let run = () => fn(resolve, reject, retry); // wait an interval to try again or reject with the error
76
-
78
+ let run = () => fn(resolve, reject, retry);
77
79
 
80
+ // wait an interval to try again or reject with the error
78
81
  let retry = err => {
79
82
  if (retries-- > 0) {
80
83
  setTimeout(run, interval);
81
84
  } else {
82
85
  reject(err);
83
86
  }
84
- }; // start trying
85
-
87
+ };
86
88
 
89
+ // start trying
87
90
  run();
88
91
  });
89
- } // Used by the request util when retrying specific errors
92
+ }
90
93
 
91
- const RETRY_ERROR_CODES = ['ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'EHOSTUNREACH', 'EAI_AGAIN']; // Proxified request function that resolves with the response body when the request is successful
94
+ // Used by the request util when retrying specific errors
95
+ const RETRY_ERROR_CODES = ['ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'EHOSTUNREACH', 'EAI_AGAIN'];
96
+
97
+ // Proxified request function that resolves with the response body when the request is successful
92
98
  // and rejects when a non-successful response is received. The rejected error contains response data
93
99
  // and any received error details. Server 500 errors are retried up to 5 times at 50ms intervals by
94
100
  // default, and 404 errors may also be optionally retried. If a callback is provided, it is called
95
101
  // with the parsed response body and response details. If the callback returns a value, that value
96
102
  // will be returned in the final resolved promise instead of the response body.
97
-
98
103
  export async function request(url, options = {}, callback) {
99
104
  // accept `request(url, callback)`
100
- if (typeof options === 'function') [options, callback] = [{}, options]; // gather request options
105
+ if (typeof options === 'function') [options, callback] = [{}, options];
101
106
 
107
+ // gather request options
102
108
  let {
103
109
  body,
104
110
  headers,
@@ -116,24 +122,26 @@ export async function request(url, options = {}, callback) {
116
122
  pathname,
117
123
  search,
118
124
  hash
119
- } = new URL(url); // reference the default export so tests can mock it
125
+ } = new URL(url);
120
126
 
127
+ // reference the default export so tests can mock it
121
128
  let {
122
129
  default: http
123
130
  } = await import(protocol === 'https:' ? 'https' : 'http');
124
131
  let {
125
132
  proxyAgentFor
126
- } = await import('./proxy.js'); // automatically stringify body content
133
+ } = await import('./proxy.js');
127
134
 
135
+ // automatically stringify body content
128
136
  if (body !== undefined && typeof body !== 'string') {
129
137
  headers = {
130
138
  'Content-Type': 'application/json',
131
139
  ...headers
132
140
  };
133
141
  body = JSON.stringify(body);
134
- } // combine request options
135
-
142
+ }
136
143
 
144
+ // combine request options
137
145
  Object.assign(requestOptions, {
138
146
  agent: requestOptions.agent || !noProxy && proxyAgentFor(url) || null,
139
147
  path: pathname + search + hash,
@@ -145,33 +153,32 @@ export async function request(url, options = {}, callback) {
145
153
  return retry((resolve, reject, retry) => {
146
154
  let handleError = error => {
147
155
  if (handleError.handled) return;
148
- handleError.handled = true; // maybe retry 404s, always retry 500s, or retry specific errors
156
+ handleError.handled = true;
149
157
 
158
+ // maybe retry 404s, always retry 500s, or retry specific errors
150
159
  let shouldRetry = error.response ? retryNotFound && error.response.statusCode === 404 || error.response.statusCode >= 500 && error.response.statusCode < 600 : !!error.code && RETRY_ERROR_CODES.includes(error.code);
151
160
  return shouldRetry ? retry(error) : reject(error);
152
161
  };
153
-
154
162
  let handleFinished = async (body, res) => {
155
163
  let {
156
164
  statusCode,
157
165
  headers
158
166
  } = res;
159
- let raw = body.toString('utf-8'); // only return a buffer when requested
167
+ let raw = body.toString('utf-8');
160
168
 
161
- if (buffer !== true) body = raw; // attempt to parse the body as json
169
+ // only return a buffer when requested
170
+ if (buffer !== true) body = raw;
162
171
 
172
+ // attempt to parse the body as json
163
173
  try {
164
174
  body = JSON.parse(raw);
165
175
  } catch {}
166
-
167
176
  try {
168
177
  if (statusCode >= 200 && statusCode < 300) {
169
178
  var _callback;
170
-
171
179
  resolve((await ((_callback = callback) === null || _callback === void 0 ? void 0 : _callback(body, res))) ?? body);
172
180
  } else {
173
181
  var _body, _body$errors, _body$errors$find;
174
-
175
182
  let err = (_body = body) === null || _body === void 0 ? void 0 : (_body$errors = _body.errors) === null || _body$errors === void 0 ? void 0 : (_body$errors$find = _body$errors.find(e => e.detail)) === null || _body$errors$find === void 0 ? void 0 : _body$errors$find.detail;
176
183
  throw new Error(err || `${statusCode} ${res.statusMessage || raw}`);
177
184
  }
@@ -186,14 +193,12 @@ export async function request(url, options = {}, callback) {
186
193
  }));
187
194
  }
188
195
  };
189
-
190
196
  let handleResponse = res => {
191
197
  let chunks = [];
192
198
  res.on('data', chunk => chunks.push(chunk));
193
199
  res.on('end', () => handleFinished(Buffer.concat(chunks), res));
194
200
  res.on('error', handleError);
195
201
  };
196
-
197
202
  let req = http.request(requestOptions);
198
203
  req.on('response', handleResponse);
199
204
  req.on('error', handleError);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/client",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,8 +31,8 @@
31
31
  "test:coverage": "yarn test --coverage"
32
32
  },
33
33
  "dependencies": {
34
- "@percy/env": "1.12.0",
35
- "@percy/logger": "1.12.0"
34
+ "@percy/env": "1.14.0",
35
+ "@percy/logger": "1.14.0"
36
36
  },
37
- "gitHead": "4303b74df91f60e36065141289d2ef2277d1d6fc"
37
+ "gitHead": "fd72688e449d6dd3eafd346fc07879cb3bb01a4e"
38
38
  }