UNPKG

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