@percy/client 1.0.0-beta.76 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/client",
3
- "version": "1.0.0-beta.76",
3
+ "version": "1.0.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,13 +10,19 @@
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },
13
- "main": "dist/index.js",
13
+ "engines": {
14
+ "node": ">=14"
15
+ },
14
16
  "files": [
15
- "dist",
16
- "test/helpers.js"
17
+ "./dist",
18
+ "./test/helpers.js"
17
19
  ],
18
- "engines": {
19
- "node": ">=12"
20
+ "main": "./dist/index.js",
21
+ "type": "module",
22
+ "exports": {
23
+ ".": "./dist/index.js",
24
+ "./utils": "./dist/utils.js",
25
+ "./test/helpers": "./test/helpers.js"
20
26
  },
21
27
  "scripts": {
22
28
  "build": "node ../../scripts/build",
@@ -25,11 +31,8 @@
25
31
  "test:coverage": "yarn test --coverage"
26
32
  },
27
33
  "dependencies": {
28
- "@percy/env": "1.0.0-beta.76",
29
- "@percy/logger": "1.0.0-beta.76"
30
- },
31
- "devDependencies": {
32
- "mock-require": "^3.0.3"
34
+ "@percy/env": "1.0.0",
35
+ "@percy/logger": "1.0.0"
33
36
  },
34
- "gitHead": "445af68d8e270e2a35fc74e26422ed5d3c91d2ae"
37
+ "gitHead": "6df509421a60144e4f9f5d59dc57a5675372a0b2"
35
38
  }
package/dist/client.js DELETED
@@ -1,371 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = exports.PercyClient = void 0;
7
-
8
- var _env = _interopRequireDefault(require("@percy/env"));
9
-
10
- var _utils = require("@percy/env/dist/utils");
11
-
12
- var _logger = _interopRequireDefault(require("@percy/logger"));
13
-
14
- var _package = _interopRequireDefault(require("../package.json"));
15
-
16
- var _utils2 = require("./utils");
17
-
18
- var _request = _interopRequireDefault(require("./request"));
19
-
20
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
21
-
22
- function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
23
-
24
- // Default client API URL can be set with an env var for API development
25
- const {
26
- PERCY_CLIENT_API_URL = 'https://percy.io/api/v1'
27
- } = process.env; // Validate build ID arguments
28
-
29
- function validateBuildId(id) {
30
- if (!id) throw new Error('Missing build ID');
31
-
32
- if (!(typeof id === 'string' || typeof id === 'number')) {
33
- throw new Error('Invalid build ID');
34
- }
35
- } // Validate project path arguments
36
-
37
-
38
- function validateProjectPath(path) {
39
- if (!path) throw new Error('Missing project path');
40
-
41
- if (!/^[^/]+?\/.+/.test(path)) {
42
- throw new Error(`Invalid project path. Expected "org/project" but received "${path}"`);
43
- }
44
- } // PercyClient is used to communicate with the Percy API to create and finalize
45
- // builds and snapshot. Uses @percy/env to collect environment information used
46
- // during build creation.
47
-
48
-
49
- class PercyClient {
50
- constructor({
51
- // read or write token, defaults to PERCY_TOKEN environment variable
52
- token,
53
- // initial user agent info
54
- clientInfo,
55
- environmentInfo,
56
- // versioned api url
57
- apiUrl = PERCY_CLIENT_API_URL
58
- } = {}) {
59
- _defineProperty(this, "log", (0, _logger.default)('client'));
60
-
61
- _defineProperty(this, "env", new _env.default(process.env));
62
-
63
- _defineProperty(this, "clientInfo", new Set());
64
-
65
- _defineProperty(this, "environmentInfo", new Set());
66
-
67
- Object.assign(this, {
68
- token,
69
- apiUrl
70
- });
71
- this.addClientInfo(clientInfo);
72
- this.addEnvironmentInfo(environmentInfo);
73
- } // Adds additional unique client info.
74
-
75
-
76
- addClientInfo(info) {
77
- for (let i of [].concat(info)) {
78
- if (i) this.clientInfo.add(i);
79
- }
80
- } // Adds additional unique environment info.
81
-
82
-
83
- addEnvironmentInfo(info) {
84
- for (let i of [].concat(info)) {
85
- if (i) this.environmentInfo.add(i);
86
- }
87
- } // Stringifies client and environment info.
88
-
89
-
90
- userAgent() {
91
- let client = new Set([`Percy/${/\w+$/.exec(this.apiUrl)}`].concat(`${_package.default.name}/${_package.default.version}`, ...this.clientInfo).filter(Boolean));
92
- let environment = new Set([...this.environmentInfo].concat(`node/${process.version}`, this.env.info).filter(Boolean));
93
- return `${[...client].join(' ')} (${[...environment].join('; ')})`;
94
- } // Checks for a Percy token and returns it.
95
-
96
-
97
- getToken() {
98
- let token = this.token || this.env.token;
99
- if (!token) throw new Error('Missing Percy token');
100
- return token;
101
- } // Returns common headers used for each request with additional
102
- // headers. Throws an error when the token is missing, which is a required
103
- // authorization header.
104
-
105
-
106
- headers(headers) {
107
- return Object.assign({
108
- Authorization: `Token token=${this.getToken()}`,
109
- 'User-Agent': this.userAgent()
110
- }, headers);
111
- } // Performs a GET request for an API endpoint with appropriate headers.
112
-
113
-
114
- get(path) {
115
- return (0, _request.default)(`${this.apiUrl}/${path}`, {
116
- headers: this.headers(),
117
- method: 'GET'
118
- });
119
- } // Performs a POST request to a JSON API endpoint with appropriate headers.
120
-
121
-
122
- post(path, body = {}) {
123
- return (0, _request.default)(`${this.apiUrl}/${path}`, {
124
- headers: this.headers({
125
- 'Content-Type': 'application/vnd.api+json'
126
- }),
127
- method: 'POST',
128
- body
129
- });
130
- } // Creates a build with optional build resources. Only one build can be
131
- // created at a time per instance so snapshots and build finalization can be
132
- // done more seemlessly without manually tracking build ids
133
-
134
-
135
- async createBuild({
136
- resources = []
137
- } = {}) {
138
- this.log.debug('Creating a new build...');
139
- return this.post('builds', {
140
- data: {
141
- type: 'builds',
142
- attributes: {
143
- branch: this.env.git.branch,
144
- 'target-branch': this.env.target.branch,
145
- 'target-commit-sha': this.env.target.commit,
146
- 'commit-sha': this.env.git.sha,
147
- 'commit-committed-at': this.env.git.committedAt,
148
- 'commit-author-name': this.env.git.authorName,
149
- 'commit-author-email': this.env.git.authorEmail,
150
- 'commit-committer-name': this.env.git.committerName,
151
- 'commit-committer-email': this.env.git.committerEmail,
152
- 'commit-message': this.env.git.message,
153
- 'pull-request-number': this.env.pullRequest,
154
- 'parallel-nonce': this.env.parallel.nonce,
155
- 'parallel-total-shards': this.env.parallel.total,
156
- partial: this.env.partial
157
- },
158
- relationships: {
159
- resources: {
160
- data: resources.map(r => ({
161
- type: 'resources',
162
- id: r.sha || (0, _utils2.sha256hash)(r.content),
163
- attributes: {
164
- 'resource-url': r.url,
165
- 'is-root': r.root || null,
166
- mimetype: r.mimetype || null
167
- }
168
- }))
169
- }
170
- }
171
- }
172
- });
173
- } // Finalizes the active build. When `all` is true, `all-shards=true` is
174
- // added as a query param so the API finalizes all other build shards.
175
-
176
-
177
- async finalizeBuild(buildId, {
178
- all = false
179
- } = {}) {
180
- validateBuildId(buildId);
181
- let qs = all ? 'all-shards=true' : '';
182
- this.log.debug(`Finalizing build ${buildId}...`);
183
- return this.post(`builds/${buildId}/finalize?${qs}`);
184
- } // Retrieves build data by id. Requires a read access token.
185
-
186
-
187
- async getBuild(buildId) {
188
- validateBuildId(buildId);
189
- this.log.debug(`Get build ${buildId}`);
190
- return this.get(`builds/${buildId}`);
191
- } // Retrieves project builds optionally filtered. Requires a read access token.
192
-
193
-
194
- async getBuilds(project, filters = {}) {
195
- validateProjectPath(project);
196
- let qs = Object.keys(filters).map(k => Array.isArray(filters[k]) ? filters[k].map(v => `filter[${k}][]=${v}`).join('&') : `filter[${k}]=${filters[k]}`).join('&');
197
- this.log.debug(`Fetching builds for ${project}`);
198
- return this.get(`projects/${project}/builds?${qs}`);
199
- } // Resolves when the build has finished and is no longer pending or
200
- // processing. By default, will time out if no update after 10 minutes.
201
-
202
-
203
- waitForBuild({
204
- build,
205
- project,
206
- commit,
207
- timeout = 10 * 60 * 1000,
208
- interval = 1000
209
- }, onProgress) {
210
- if (commit && !project) {
211
- throw new Error('Missing project path for commit');
212
- } else if (!commit && !build) {
213
- throw new Error('Missing build ID or commit SHA');
214
- } else if (project) {
215
- validateProjectPath(project);
216
- }
217
-
218
- let sha = commit && ((0, _utils.git)(`rev-parse ${commit}`) || commit);
219
-
220
- let fetchData = async () => build ? (await this.getBuild(build)).data : (await this.getBuilds(project, {
221
- sha
222
- })).data[0];
223
-
224
- this.log.debug(`Waiting for build ${build || `${project} (${commit})`}...`); // recursively poll every second until the build finishes
225
-
226
- return new Promise((resolve, reject) => async function poll(last, t) {
227
- try {
228
- let data = await fetchData();
229
- let state = data === null || data === void 0 ? void 0 : data.attributes.state;
230
- let pending = !state || state === 'pending' || state === 'processing';
231
- let updated = JSON.stringify(data) !== JSON.stringify(last); // new data received
232
-
233
- if (updated) {
234
- t = Date.now(); // no new data within the timeout
235
- } else if (Date.now() - t >= timeout) {
236
- throw new Error('Timeout exceeded without an update');
237
- } // call progress every update after the first update
238
-
239
-
240
- if ((last || pending) && updated) {
241
- onProgress === null || onProgress === void 0 ? void 0 : onProgress(data);
242
- } // not finished, poll again
243
-
244
-
245
- if (pending) {
246
- return setTimeout(poll, interval, data, t); // build finished
247
- } else {
248
- // ensure progress is called at least once
249
- if (!last) onProgress === null || onProgress === void 0 ? void 0 : onProgress(data);
250
- resolve({
251
- data
252
- });
253
- }
254
- } catch (err) {
255
- reject(err);
256
- }
257
- }(null, Date.now()));
258
- } // Uploads a single resource to the active build. If `filepath` is provided,
259
- // `content` is read from the filesystem. The sha is optional and will be
260
- // created from `content` if one is not provided.
261
-
262
-
263
- async uploadResource(buildId, {
264
- url,
265
- sha,
266
- filepath,
267
- content
268
- } = {}) {
269
- validateBuildId(buildId);
270
- this.log.debug(`Uploading resource: ${url}...`);
271
- content = filepath ? require('fs').readFileSync(filepath) : content;
272
- return this.post(`builds/${buildId}/resources`, {
273
- data: {
274
- type: 'resources',
275
- id: sha || (0, _utils2.sha256hash)(content),
276
- attributes: {
277
- 'base64-content': (0, _utils2.base64encode)(content)
278
- }
279
- }
280
- });
281
- } // Uploads resources to the active build concurrently, two at a time.
282
-
283
-
284
- async uploadResources(buildId, resources) {
285
- validateBuildId(buildId);
286
- this.log.debug(`Uploading resources for ${buildId}...`);
287
- return (0, _utils2.pool)(function* () {
288
- for (let resource of resources) {
289
- yield this.uploadResource(buildId, resource);
290
- }
291
- }, this, 2);
292
- } // Creates a snapshot for the active build using the provided attributes.
293
-
294
-
295
- async createSnapshot(buildId, {
296
- name,
297
- widths,
298
- minHeight,
299
- enableJavaScript,
300
- clientInfo,
301
- environmentInfo,
302
- resources = []
303
- } = {}) {
304
- validateBuildId(buildId);
305
- this.addClientInfo(clientInfo);
306
- this.addEnvironmentInfo(environmentInfo);
307
-
308
- if (!this.clientInfo.size || !this.environmentInfo.size) {
309
- this.log.warn('Warning: Missing `clientInfo` and/or `environmentInfo` properties');
310
- }
311
-
312
- this.log.debug(`Creating snapshot: ${name}...`);
313
- return this.post(`builds/${buildId}/snapshots`, {
314
- data: {
315
- type: 'snapshots',
316
- attributes: {
317
- name: name || null,
318
- widths: widths || null,
319
- 'minimum-height': minHeight || null,
320
- 'enable-javascript': enableJavaScript || null
321
- },
322
- relationships: {
323
- resources: {
324
- data: resources.map(r => ({
325
- type: 'resources',
326
- id: r.sha || (0, _utils2.sha256hash)(r.content),
327
- attributes: {
328
- 'resource-url': r.url || null,
329
- 'is-root': r.root || null,
330
- mimetype: r.mimetype || null
331
- }
332
- }))
333
- }
334
- }
335
- }
336
- });
337
- } // Finalizes a snapshot.
338
-
339
-
340
- async finalizeSnapshot(snapshotId) {
341
- if (!snapshotId) throw new Error('Missing snapshot ID');
342
- this.log.debug(`Finalizing snapshot ${snapshotId}...`);
343
- return this.post(`snapshots/${snapshotId}/finalize`);
344
- } // Convenience method for creating a snapshot for the active build, uploading
345
- // missing resources for the snapshot, and finalizing the snapshot.
346
-
347
-
348
- async sendSnapshot(buildId, options) {
349
- var _snapshot$data$relati, _snapshot$data$relati2;
350
-
351
- let snapshot = await this.createSnapshot(buildId, options);
352
- 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;
353
-
354
- if (missing !== null && missing !== void 0 && missing.length) {
355
- let resources = options.resources.reduce((acc, r) => Object.assign(acc, {
356
- [r.sha]: r
357
- }), {});
358
- await this.uploadResources(buildId, missing.map(({
359
- id
360
- }) => resources[id]));
361
- }
362
-
363
- await this.finalizeSnapshot(snapshot.data.id);
364
- return snapshot;
365
- }
366
-
367
- }
368
-
369
- exports.PercyClient = PercyClient;
370
- var _default = PercyClient;
371
- exports.default = _default;
package/dist/index.js DELETED
@@ -1,23 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- Object.defineProperty(exports, "PercyClient", {
7
- enumerable: true,
8
- get: function () {
9
- return _client.PercyClient;
10
- }
11
- });
12
- Object.defineProperty(exports, "default", {
13
- enumerable: true,
14
- get: function () {
15
- return _client.default;
16
- }
17
- });
18
-
19
- var _client = _interopRequireWildcard(require("./client"));
20
-
21
- function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
22
-
23
- function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
package/dist/request.js DELETED
@@ -1,316 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = exports.ProxyHttpsAgent = exports.ProxyHttpAgent = void 0;
7
- exports.getProxy = getProxy;
8
- exports.href = href;
9
- exports.port = port;
10
- exports.proxyAgentFor = proxyAgentFor;
11
- exports.request = request;
12
-
13
- var _net = _interopRequireDefault(require("net"));
14
-
15
- var _tls = _interopRequireDefault(require("tls"));
16
-
17
- var _http = _interopRequireDefault(require("http"));
18
-
19
- var _https = _interopRequireDefault(require("https"));
20
-
21
- var _logger = _interopRequireDefault(require("@percy/logger"));
22
-
23
- var _utils = require("./utils");
24
-
25
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
26
-
27
- function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
28
-
29
- const CRLF = '\r\n';
30
- const STATUS_REG = /^HTTP\/1.[01] (\d*)/;
31
- const RETRY_ERROR_CODES = ['ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'EHOSTUNREACH', 'EAI_AGAIN']; // Returns the port number of a URL object. Defaults to port 443 for https
32
- // protocols or port 80 otherwise.
33
-
34
- function port(options) {
35
- if (options.port) return options.port;
36
- return options.protocol === 'https:' ? 443 : 80;
37
- }
38
-
39
- function href(options) {
40
- let {
41
- protocol,
42
- hostname,
43
- path,
44
- pathname,
45
- search,
46
- hash
47
- } = options;
48
- return `${protocol}//${hostname}:${port(options)}` + (path || `${pathname || ''}${search || ''}${hash || ''}`);
49
- }
50
-
51
- ;
52
-
53
- function getProxy(options) {
54
- let proxyUrl = options.protocol === 'https:' && (process.env.https_proxy || process.env.HTTPS_PROXY) || process.env.http_proxy || process.env.HTTP_PROXY;
55
- let shouldProxy = !!proxyUrl && !(0, _utils.hostnameMatches)(process.env.no_proxy || process.env.NO_PROXY, href(options));
56
-
57
- if (shouldProxy) {
58
- proxyUrl = new URL(proxyUrl);
59
- let isHttps = proxyUrl.protocol === 'https:';
60
-
61
- if (!isHttps && proxyUrl.protocol !== 'http:') {
62
- throw new Error(`Unsupported proxy protocol: ${proxyUrl.protocol}`);
63
- }
64
-
65
- let proxy = {
66
- isHttps
67
- };
68
- proxy.auth = !!proxyUrl.username && 'Basic ' + (proxyUrl.password ? Buffer.from(`${proxyUrl.username}:${proxyUrl.password}`) : Buffer.from(proxyUrl.username)).toString('base64');
69
- proxy.host = proxyUrl.hostname;
70
- proxy.port = port(proxyUrl);
71
-
72
- proxy.connect = () => (isHttps ? _tls.default : _net.default).connect({
73
- rejectUnauthorized: options.rejectUnauthorized,
74
- host: proxy.host,
75
- port: proxy.port
76
- });
77
-
78
- return proxy;
79
- }
80
- } // Proxified http agent
81
-
82
-
83
- class ProxyHttpAgent extends _http.default.Agent {
84
- constructor(...args) {
85
- super(...args);
86
-
87
- _defineProperty(this, "httpsAgent", new _https.default.Agent({
88
- keepAlive: true
89
- }));
90
- }
91
-
92
- addRequest(request, options) {
93
- var _request$outputData;
94
-
95
- let proxy = getProxy(options);
96
- if (!proxy) return super.addRequest(request, options);
97
- (0, _logger.default)('client:proxy').debug(`Proxying request: ${options.href}`); // modify the request for proxying
98
-
99
- request.path = href(options);
100
-
101
- if (proxy.auth) {
102
- request.setHeader('Proxy-Authorization', proxy.auth);
103
- } // regenerate headers since we just changed things
104
-
105
-
106
- delete request._header;
107
-
108
- request._implicitHeader();
109
-
110
- if (((_request$outputData = request.outputData) === null || _request$outputData === void 0 ? void 0 : _request$outputData.length) > 0) {
111
- let first = request.outputData[0].data;
112
- let endOfHeaders = first.indexOf(CRLF.repeat(2)) + 4;
113
- request.outputData[0].data = request._header + first.substring(endOfHeaders);
114
- } // coerce the connection to the proxy
115
-
116
-
117
- options.port = proxy.port;
118
- options.host = proxy.host;
119
- delete options.path;
120
-
121
- if (proxy.isHttps) {
122
- // use the underlying https agent to complete the connection
123
- request.agent = this.httpsAgent;
124
- return this.httpsAgent.addRequest(request, options);
125
- } else {
126
- return super.addRequest(request, options);
127
- }
128
- }
129
-
130
- } // Proxified https agent
131
-
132
-
133
- exports.ProxyHttpAgent = ProxyHttpAgent;
134
-
135
- class ProxyHttpsAgent extends _https.default.Agent {
136
- constructor(options) {
137
- // default keep-alive
138
- super({
139
- keepAlive: true,
140
- ...options
141
- });
142
- }
143
-
144
- createConnection(options, callback) {
145
- let proxy = getProxy(options);
146
- if (!proxy) return super.createConnection(options, callback);
147
- (0, _logger.default)('client:proxy').debug(`Proxying request: ${href(options)}`); // generate proxy connect message
148
-
149
- let host = `${options.hostname}:${port(options)}`;
150
- let connectMessage = [`CONNECT ${host} HTTP/1.1`, `Host: ${host}`];
151
-
152
- if (proxy.auth) {
153
- connectMessage.push(`Proxy-Authorization: ${proxy.auth}`);
154
- }
155
-
156
- connectMessage = connectMessage.join(CRLF);
157
- connectMessage += CRLF.repeat(2); // start the proxy connection and setup listeners
158
-
159
- let socket = proxy.connect();
160
-
161
- let handleError = err => {
162
- socket.destroy(err);
163
- callback(err);
164
- };
165
-
166
- let handleClose = () => handleError(new Error('Connection closed while sending request to upstream proxy'));
167
-
168
- let buffer = '';
169
-
170
- let handleData = data => {
171
- var _buffer$match;
172
-
173
- buffer += data.toString(); // haven't received end of headers yet, keep buffering
174
-
175
- if (!buffer.includes(CRLF.repeat(2))) return; // stop listening after end of headers
176
-
177
- socket.off('data', handleData);
178
-
179
- if (((_buffer$match = buffer.match(STATUS_REG)) === null || _buffer$match === void 0 ? void 0 : _buffer$match[1]) !== '200') {
180
- return handleError(new Error('Error establishing proxy connection. ' + `Response from server was: ${buffer}`));
181
- }
182
-
183
- options.socket = socket;
184
- options.servername = options.hostname; // callback not passed in so not to be added as a listener
185
-
186
- callback(null, super.createConnection(options));
187
- }; // send and handle the connect message
188
-
189
-
190
- socket.on('error', handleError).on('close', handleClose).on('data', handleData).write(connectMessage);
191
- }
192
-
193
- }
194
-
195
- exports.ProxyHttpsAgent = ProxyHttpsAgent;
196
-
197
- function proxyAgentFor(url, options) {
198
- let cache = proxyAgentFor.cache || (proxyAgentFor.cache = new Map());
199
- let {
200
- protocol,
201
- hostname
202
- } = new URL(url);
203
- let cachekey = `${protocol}//${hostname}`;
204
-
205
- if (!cache.has(cachekey)) {
206
- cache.set(cachekey, protocol === 'https:' ? new ProxyHttpsAgent(options) : new ProxyHttpAgent(options));
207
- }
208
-
209
- return cache.get(cachekey);
210
- } // Proxified request function that resolves with the response body when the request is successful
211
- // and rejects when a non-successful response is received. The rejected error contains response data
212
- // and any received error details. Server 500 errors are retried up to 5 times at 50ms intervals by
213
- // default, and 404 errors may also be optionally retried. If a callback is provided, it is called
214
- // with the parsed response body and response details. If the callback returns a value, that value
215
- // will be returned in the final resolved promise instead of the response body.
216
-
217
-
218
- function request(url, options = {}, callback) {
219
- // accept `request(url, callback)`
220
- if (typeof options === 'function') [options, callback] = [{}, options]; // gather request options
221
-
222
- let {
223
- body,
224
- headers,
225
- retries,
226
- retryNotFound,
227
- interval,
228
- noProxy,
229
- ...requestOptions
230
- } = options;
231
- let {
232
- protocol,
233
- hostname,
234
- port,
235
- pathname,
236
- search,
237
- hash
238
- } = new URL(url); // automatically stringify body content
239
-
240
- if (body && typeof body !== 'string') {
241
- headers = {
242
- 'Content-Type': 'application/json',
243
- ...headers
244
- };
245
- body = JSON.stringify(body);
246
- } // combine request options
247
-
248
-
249
- Object.assign(requestOptions, {
250
- agent: requestOptions.agent || !noProxy && proxyAgentFor(url) || null,
251
- path: pathname + search + hash,
252
- protocol,
253
- hostname,
254
- headers,
255
- port
256
- });
257
- return (0, _utils.retry)((resolve, reject, retry) => {
258
- let handleError = error => {
259
- if (handleError.handled) return;
260
- handleError.handled = true;
261
- let shouldRetry = error.response // maybe retry 404s and always retry 500s
262
- ? retryNotFound && error.response.statusCode === 404 || error.response.statusCode >= 500 && error.response.statusCode < 600 // retry specific error codes
263
- : !!error.code && RETRY_ERROR_CODES.includes(error.code);
264
- return shouldRetry ? retry(error) : reject(error);
265
- };
266
-
267
- let handleFinished = async (body, res) => {
268
- let raw = body; // attempt to parse the body as json
269
-
270
- try {
271
- body = JSON.parse(body);
272
- } catch (e) {}
273
-
274
- try {
275
- if (res.statusCode >= 200 && res.statusCode < 300) {
276
- var _await$callback, _callback;
277
-
278
- // resolve successful statuses after the callback
279
- resolve((_await$callback = await ((_callback = callback) === null || _callback === void 0 ? void 0 : _callback(body, res))) !== null && _await$callback !== void 0 ? _await$callback : body);
280
- } else {
281
- var _body, _body$errors, _body$errors$find;
282
-
283
- // use the first error detail or the status message
284
- throw new Error(((_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) || `${res.statusCode} ${res.statusMessage || raw}`);
285
- }
286
- } catch (error) {
287
- handleError(Object.assign(error, {
288
- response: {
289
- statusCode: res.statusCode,
290
- headers: res.headers,
291
- body
292
- }
293
- }));
294
- }
295
- };
296
-
297
- let handleResponse = res => {
298
- let body = '';
299
- res.setEncoding('utf8');
300
- res.on('data', chunk => body += chunk);
301
- res.on('end', () => handleFinished(body, res));
302
- res.on('error', handleError);
303
- };
304
-
305
- let req = (protocol === 'https:' ? _https.default : _http.default).request(requestOptions);
306
- req.on('response', handleResponse);
307
- req.on('error', handleError);
308
- req.end(body);
309
- }, {
310
- retries,
311
- interval
312
- });
313
- }
314
-
315
- var _default = request;
316
- exports.default = _default;
package/dist/utils.js DELETED
@@ -1,122 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.base64encode = base64encode;
7
- exports.hostnameMatches = hostnameMatches;
8
- exports.pool = pool;
9
- exports.retry = retry;
10
- exports.sha256hash = sha256hash;
11
-
12
- var _crypto = _interopRequireDefault(require("crypto"));
13
-
14
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
-
16
- // Returns a sha256 hash of a string.
17
- function sha256hash(content) {
18
- return _crypto.default.createHash('sha256').update(content, 'utf-8').digest('hex');
19
- } // Returns a base64 encoding of a string or buffer.
20
-
21
-
22
- function base64encode(content) {
23
- return Buffer.from(content).toString('base64');
24
- } // Creates a concurrent pool of promises created by the given generator.
25
- // Resolves when the generator's final promise resolves and rejects when any
26
- // generated promise rejects.
27
-
28
-
29
- function pool(generator, context, concurrency) {
30
- return new Promise((resolve, reject) => {
31
- let iterator = generator.call(context);
32
- let queue = 0;
33
- let ret = [];
34
- let err; // generates concurrent promises
35
-
36
- let proceed = () => {
37
- while (queue < concurrency) {
38
- let {
39
- done,
40
- value: promise
41
- } = iterator.next();
42
-
43
- if (done || err) {
44
- if (!queue && err) reject(err);
45
- if (!queue) resolve(ret);
46
- return;
47
- }
48
-
49
- queue++;
50
- promise.then(value => {
51
- queue--;
52
- ret.push(value);
53
- proceed();
54
- }).catch(error => {
55
- queue--;
56
- err = error;
57
- proceed();
58
- });
59
- }
60
- }; // start generating promises
61
-
62
-
63
- proceed();
64
- });
65
- } // Returns a promise that resolves or rejects when the provided function calls
66
- // `resolve` or `reject` respectively. The third function argument, `retry`,
67
- // will recursively call the function at the specified interval until retries
68
- // are exhausted, at which point the promise will reject with the last error
69
- // passed to `retry`.
70
-
71
-
72
- function retry(fn, {
73
- retries = 5,
74
- interval = 50
75
- }) {
76
- return new Promise((resolve, reject) => {
77
- let run = () => fn(resolve, reject, retry); // wait an interval to try again or reject with the error
78
-
79
-
80
- let retry = err => {
81
- if (retries-- > 0) {
82
- setTimeout(run, interval);
83
- } else {
84
- reject(err);
85
- }
86
- }; // start trying
87
-
88
-
89
- run();
90
- });
91
- } // Returns true if the URL hostname matches any patterns
92
-
93
-
94
- function hostnameMatches(patterns, url) {
95
- let subject = new URL(url);
96
- /* istanbul ignore next: only strings are provided internally by the client proxy; core (which
97
- * borrows this util) sometimes provides an array of patterns or undefined */
98
-
99
- patterns = typeof patterns === 'string' ? patterns.split(/[\s,]+/) : [].concat(patterns);
100
-
101
- for (let pattern of patterns) {
102
- if (pattern === '*') return true;
103
- if (!pattern) continue; // parse pattern
104
-
105
- let {
106
- groups: rule
107
- } = pattern.match(/^(?<hostname>.+?)(?::(?<port>\d+))?$/); // missing a hostname or ports do not match
108
-
109
- if (!rule.hostname || rule.port && rule.port !== subject.port) {
110
- continue;
111
- } // wildcards are treated the same as leading dots
112
-
113
-
114
- rule.hostname = rule.hostname.replace(/^\*/, ''); // hostnames are equal or end with a wildcard rule
115
-
116
- if (rule.hostname === subject.hostname || rule.hostname.startsWith('.') && subject.hostname.endsWith(rule.hostname)) {
117
- return true;
118
- }
119
- }
120
-
121
- return false;
122
- }
package/test/helpers.js DELETED
@@ -1,81 +0,0 @@
1
- const nock = require('nock');
2
-
3
- const DEFAULT_REPLIES = {
4
- '/builds': () => [201, {
5
- data: {
6
- id: '123',
7
- attributes: {
8
- 'build-number': 1,
9
- 'web-url': 'https://percy.io/test/test/123'
10
- }
11
- }
12
- }],
13
-
14
- '/builds/123/snapshots': ({ body }) => [201, {
15
- data: {
16
- id: '4567',
17
- attributes: body.attributes,
18
- relationships: {
19
- 'missing-resources': {
20
- data: body.data.relationships.resources
21
- .data.map(({ id }) => ({ id }))
22
- }
23
- }
24
- }
25
- }]
26
- };
27
-
28
- const mockAPI = {
29
- nock: null,
30
- requests: null,
31
- replies: null,
32
-
33
- start(delay = 0) {
34
- nock.cleanAll();
35
- nock.disableNetConnect();
36
- nock.enableNetConnect('storage.googleapis.com|localhost|127.0.0.1');
37
-
38
- let n = this.nock = nock('https://percy.io/api/v1').persist();
39
- let requests = this.requests = {};
40
- let replies = this.replies = {};
41
-
42
- function intercept(_, body) {
43
- let { path, headers, method } = this.req;
44
-
45
- try { body = JSON.parse(body); } catch {}
46
- path = path.replace('/api/v1', '');
47
-
48
- let req = { body, headers, method };
49
- let reply = replies[path] && (
50
- replies[path].length > 1
51
- ? replies[path].shift()
52
- : replies[path][0]
53
- );
54
-
55
- requests[path] = requests[path] || [];
56
- requests[path].push(req);
57
-
58
- return reply ? reply(req) : (
59
- DEFAULT_REPLIES[path]
60
- ? DEFAULT_REPLIES[path](req)
61
- : [200]
62
- );
63
- }
64
-
65
- n.get(/.*/).delay(delay).reply(intercept);
66
- n.post(/.*/).delay(delay).reply(intercept);
67
- },
68
-
69
- reply(path, handler) {
70
- this.replies[path] = this.replies[path] || [];
71
- this.replies[path].push(handler);
72
- return this;
73
- },
74
-
75
- cleanAll() {
76
- nock.cleanAll();
77
- return this;
78
- }
79
- };
80
-
81
- module.exports = mockAPI;