UNPKG

13.1 kBJavaScriptView Raw
1'use strict';
2
3var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
4
5var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
6
7function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
8
9var http = require('http');
10var https = require('https');
11var utils = require('./utils');
12var Environment = require('./environment');
13var UserAgent = require('./user-agent');
14var retry = require('bluebird-retry');
15var requestPromise = require('request-promise');
16var PromisePool = require('es6-promise-pool');
17var regeneratorRuntime = require('regenerator-runtime'); // eslint-disable-line no-unused-vars
18var fs = require('fs');
19
20require('dotenv').config();
21
22var JSON_API_CONTENT_TYPE = 'application/vnd.api+json';
23var CONCURRENCY = 2;
24
25var Resource = function () {
26 function Resource(options) {
27 _classCallCheck(this, Resource);
28
29 if (!options.resourceUrl) {
30 throw new Error('"resourceUrl" arg is required to create a Resource.');
31 }
32 if (!options.sha && !options.content) {
33 throw new Error('Either "sha" or "content" is required to create a Resource.');
34 }
35 if (/\s/.test(options.resourceUrl)) {
36 throw new Error('"resourceUrl" arg includes whitespace. It needs to be encoded.');
37 }
38 this.resourceUrl = options.resourceUrl;
39 this.content = options.content;
40 this.sha = options.sha || utils.sha256hash(options.content);
41 this.mimetype = options.mimetype;
42 this.isRoot = options.isRoot;
43
44 // Temporary convenience attributes, will not be serialized. These are used, for example,
45 // to hold the local path so reading file contents can be deferred.
46 this.localPath = options.localPath;
47 }
48
49 _createClass(Resource, [{
50 key: 'serialize',
51 value: function serialize() {
52 return {
53 type: 'resources',
54 id: this.sha,
55 attributes: {
56 'resource-url': this.resourceUrl,
57 mimetype: this.mimetype || null,
58 'is-root': this.isRoot || null
59 }
60 };
61 }
62 }]);
63
64 return Resource;
65}();
66
67var PercyClient = function () {
68 function PercyClient(options) {
69 _classCallCheck(this, PercyClient);
70
71 options = options || {};
72 this.token = options.token;
73 this.apiUrl = options.apiUrl || 'https://percy.io/api/v1';
74 this.environment = options.environment || new Environment(process.env);
75 this._httpClient = requestPromise;
76 this._httpModule = this.apiUrl.indexOf('http://') === 0 ? http : https;
77 // A custom HttpAgent with pooling and keepalive.
78 this._httpAgent = new this._httpModule.Agent({
79 maxSockets: 5,
80 keepAlive: true
81 });
82 this._clientInfo = options.clientInfo;
83 this._environmentInfo = options.environmentInfo;
84 this._sdkClientInfo = null;
85 this._sdkEnvironmentInfo = null;
86 }
87
88 _createClass(PercyClient, [{
89 key: '_headers',
90 value: function _headers(headers) {
91 return _extends({
92 Authorization: 'Token token=' + this.token,
93 'User-Agent': new UserAgent(this).toString()
94 }, headers);
95 }
96 }, {
97 key: '_httpGet',
98 value: function _httpGet(uri) {
99 var requestOptions = {
100 method: 'GET',
101 uri: uri,
102 headers: this._headers(),
103 json: true,
104 resolveWithFullResponse: true,
105 agent: this._httpAgent
106 };
107
108 return retry(this._httpClient, {
109 context: this,
110 args: [uri, requestOptions],
111 interval: 50,
112 max_tries: 5,
113 throw_original: true,
114 predicate: function predicate(err) {
115 return err.statusCode >= 500 && err.statusCode < 600;
116 }
117 });
118 }
119 }, {
120 key: '_httpPost',
121 value: function _httpPost(uri, data) {
122 var requestOptions = {
123 method: 'POST',
124 uri: uri,
125 body: data,
126 headers: this._headers({ 'Content-Type': JSON_API_CONTENT_TYPE }),
127 json: true,
128 resolveWithFullResponse: true,
129 agent: this._httpAgent
130 };
131
132 return retry(this._httpClient, {
133 context: this,
134 args: [uri, requestOptions],
135 interval: 50,
136 max_tries: 5,
137 throw_original: true,
138 predicate: function predicate(err) {
139 return err.statusCode >= 500 && err.statusCode < 600;
140 }
141 });
142 }
143 }, {
144 key: 'createBuild',
145 value: function createBuild(options) {
146 var parallelNonce = this.environment.parallelNonce;
147 var parallelTotalShards = this.environment.parallelTotalShards;
148
149 // Only pass parallelism data if it all exists.
150 if (!parallelNonce || !parallelTotalShards) {
151 parallelNonce = null;
152 parallelTotalShards = null;
153 }
154
155 options = options || {};
156
157 var commitData = options['commitData'] || this.environment.commitData;
158
159 var data = {
160 data: {
161 type: 'builds',
162 attributes: {
163 branch: commitData.branch,
164 'target-branch': this.environment.targetBranch,
165 'target-commit-sha': this.environment.targetCommitSha,
166 'commit-sha': commitData.sha,
167 'commit-committed-at': commitData.committedAt,
168 'commit-author-name': commitData.authorName,
169 'commit-author-email': commitData.authorEmail,
170 'commit-committer-name': commitData.committerName,
171 'commit-committer-email': commitData.committerEmail,
172 'commit-message': commitData.message,
173 'pull-request-number': this.environment.pullRequestNumber,
174 'parallel-nonce': parallelNonce,
175 'parallel-total-shards': parallelTotalShards,
176 partial: this.environment.partialBuild
177 }
178 }
179 };
180
181 if (options.resources) {
182 data['data']['relationships'] = {
183 resources: {
184 data: options.resources.map(function (resource) {
185 return resource.serialize();
186 })
187 }
188 };
189 }
190
191 return this._httpPost(this.apiUrl + '/builds/', data);
192 }
193
194 // This method is unavailable to normal write-only project tokens.
195
196 }, {
197 key: 'getBuild',
198 value: function getBuild(buildId) {
199 return this._httpGet(this.apiUrl + '/builds/' + buildId);
200 }
201
202 // This method is unavailable to normal write-only project tokens.
203
204 }, {
205 key: 'getBuilds',
206 value: function getBuilds(project, filter) {
207 filter = filter || {};
208 var queryString = Object.keys(filter).map(function (key) {
209 if (Array.isArray(filter[key])) {
210 // If filter value is an array, match Percy API's format expectations of:
211 // filter[key][]=value1&filter[key][]=value2
212 return filter[key].map(function (array_value) {
213 return 'filter[' + key + '][]=' + array_value;
214 }).join('&');
215 } else {
216 return 'filter[' + key + ']=' + filter[key];
217 }
218 }).join('&');
219
220 if (queryString.length > 0) {
221 queryString = '?' + queryString;
222 }
223
224 return this._httpGet(this.apiUrl + '/projects/' + project + '/builds' + queryString);
225 }
226 }, {
227 key: 'makeResource',
228 value: function makeResource(options) {
229 return new Resource(options);
230 }
231
232 // Synchronously walks a directory of compiled assets and returns an array of Resource objects.
233
234 }, {
235 key: 'gatherBuildResources',
236 value: function gatherBuildResources(rootDir, options) {
237 return utils.gatherBuildResources(this, rootDir, options);
238 }
239 }, {
240 key: 'uploadResource',
241 value: function uploadResource(buildId, content) {
242 var sha = utils.sha256hash(content);
243 var data = {
244 data: {
245 type: 'resources',
246 id: sha,
247 attributes: {
248 'base64-content': utils.base64encode(content)
249 }
250 }
251 };
252
253 return this._httpPost(this.apiUrl + '/builds/' + buildId + '/resources/', data);
254 }
255 }, {
256 key: 'uploadResources',
257 value: function uploadResources(buildId, resources) {
258 var _marked = /*#__PURE__*/regeneratorRuntime.mark(generatePromises);
259
260 var _this = this;
261 function generatePromises() {
262 var _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, resource, content;
263
264 return regeneratorRuntime.wrap(function generatePromises$(_context) {
265 while (1) {
266 switch (_context.prev = _context.next) {
267 case 0:
268 _iteratorNormalCompletion = true;
269 _didIteratorError = false;
270 _iteratorError = undefined;
271 _context.prev = 3;
272 _iterator = resources[Symbol.iterator]();
273
274 case 5:
275 if (_iteratorNormalCompletion = (_step = _iterator.next()).done) {
276 _context.next = 13;
277 break;
278 }
279
280 resource = _step.value;
281 content = resource.localPath ? fs.readFileSync(resource.localPath) : resource.content;
282 _context.next = 10;
283 return _this.uploadResource(buildId, content);
284
285 case 10:
286 _iteratorNormalCompletion = true;
287 _context.next = 5;
288 break;
289
290 case 13:
291 _context.next = 19;
292 break;
293
294 case 15:
295 _context.prev = 15;
296 _context.t0 = _context['catch'](3);
297 _didIteratorError = true;
298 _iteratorError = _context.t0;
299
300 case 19:
301 _context.prev = 19;
302 _context.prev = 20;
303
304 if (!_iteratorNormalCompletion && _iterator.return) {
305 _iterator.return();
306 }
307
308 case 22:
309 _context.prev = 22;
310
311 if (!_didIteratorError) {
312 _context.next = 25;
313 break;
314 }
315
316 throw _iteratorError;
317
318 case 25:
319 return _context.finish(22);
320
321 case 26:
322 return _context.finish(19);
323
324 case 27:
325 case 'end':
326 return _context.stop();
327 }
328 }
329 }, _marked, this, [[3, 15, 19, 27], [20,, 22, 26]]);
330 }
331
332 var pool = new PromisePool(generatePromises(), CONCURRENCY);
333 return pool.start();
334 }
335 }, {
336 key: 'uploadMissingResources',
337 value: function uploadMissingResources(buildId, response, resources) {
338 var missingResourceShas = utils.getMissingResources(response);
339 if (!missingResourceShas.length) {
340 return Promise.resolve();
341 }
342
343 var resourcesBySha = resources.reduce(function (map, resource) {
344 map[resource.sha] = resource;
345 return map;
346 }, {});
347 var missingResources = missingResourceShas.map(function (resource) {
348 return resourcesBySha[resource.id];
349 });
350
351 return this.uploadResources(buildId, missingResources);
352 }
353 }, {
354 key: 'createSnapshot',
355 value: function createSnapshot(buildId, resources, options) {
356 options = options || {};
357 resources = resources || [];
358
359 var data = {
360 data: {
361 type: 'snapshots',
362 attributes: {
363 name: options.name || null,
364 'enable-javascript': options.enableJavaScript || null,
365 widths: options.widths || null,
366 'minimum-height': options.minimumHeight || null
367 },
368 relationships: {
369 resources: {
370 data: resources.map(function (resource) {
371 return resource.serialize();
372 })
373 }
374 }
375 }
376 };
377
378 this._sdkClientInfo = options.clientInfo;
379 this._sdkEnvironmentInfo = options.environmentInfo;
380 return this._httpPost(this.apiUrl + '/builds/' + buildId + '/snapshots/', data);
381 }
382 }, {
383 key: 'finalizeSnapshot',
384 value: function finalizeSnapshot(snapshotId) {
385 return this._httpPost(this.apiUrl + '/snapshots/' + snapshotId + '/finalize', {});
386 }
387 }, {
388 key: 'finalizeBuild',
389 value: function finalizeBuild(buildId, options) {
390 options = options || {};
391 var allShards = options.allShards || false;
392 var query = allShards ? '?all-shards=true' : '';
393 return this._httpPost(this.apiUrl + '/builds/' + buildId + '/finalize' + query, {});
394 }
395 }]);
396
397 return PercyClient;
398}();
399
400module.exports = PercyClient;
\No newline at end of file