UNPKG

12.8 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 return 'filter[' + key + ']=' + filter[key];
210 }).join('&');
211
212 if (queryString.length > 0) {
213 queryString = '?' + queryString;
214 }
215
216 return this._httpGet(this.apiUrl + '/projects/' + project + '/builds' + queryString);
217 }
218 }, {
219 key: 'makeResource',
220 value: function makeResource(options) {
221 return new Resource(options);
222 }
223
224 // Synchronously walks a directory of compiled assets and returns an array of Resource objects.
225
226 }, {
227 key: 'gatherBuildResources',
228 value: function gatherBuildResources(rootDir, options) {
229 return utils.gatherBuildResources(this, rootDir, options);
230 }
231 }, {
232 key: 'uploadResource',
233 value: function uploadResource(buildId, content) {
234 var sha = utils.sha256hash(content);
235 var data = {
236 data: {
237 type: 'resources',
238 id: sha,
239 attributes: {
240 'base64-content': utils.base64encode(content)
241 }
242 }
243 };
244
245 return this._httpPost(this.apiUrl + '/builds/' + buildId + '/resources/', data);
246 }
247 }, {
248 key: 'uploadResources',
249 value: function uploadResources(buildId, resources) {
250 var _marked = /*#__PURE__*/regeneratorRuntime.mark(generatePromises);
251
252 var _this = this;
253 function generatePromises() {
254 var _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, resource, content;
255
256 return regeneratorRuntime.wrap(function generatePromises$(_context) {
257 while (1) {
258 switch (_context.prev = _context.next) {
259 case 0:
260 _iteratorNormalCompletion = true;
261 _didIteratorError = false;
262 _iteratorError = undefined;
263 _context.prev = 3;
264 _iterator = resources[Symbol.iterator]();
265
266 case 5:
267 if (_iteratorNormalCompletion = (_step = _iterator.next()).done) {
268 _context.next = 13;
269 break;
270 }
271
272 resource = _step.value;
273 content = resource.localPath ? fs.readFileSync(resource.localPath) : resource.content;
274 _context.next = 10;
275 return _this.uploadResource(buildId, content);
276
277 case 10:
278 _iteratorNormalCompletion = true;
279 _context.next = 5;
280 break;
281
282 case 13:
283 _context.next = 19;
284 break;
285
286 case 15:
287 _context.prev = 15;
288 _context.t0 = _context['catch'](3);
289 _didIteratorError = true;
290 _iteratorError = _context.t0;
291
292 case 19:
293 _context.prev = 19;
294 _context.prev = 20;
295
296 if (!_iteratorNormalCompletion && _iterator.return) {
297 _iterator.return();
298 }
299
300 case 22:
301 _context.prev = 22;
302
303 if (!_didIteratorError) {
304 _context.next = 25;
305 break;
306 }
307
308 throw _iteratorError;
309
310 case 25:
311 return _context.finish(22);
312
313 case 26:
314 return _context.finish(19);
315
316 case 27:
317 case 'end':
318 return _context.stop();
319 }
320 }
321 }, _marked, this, [[3, 15, 19, 27], [20,, 22, 26]]);
322 }
323
324 var pool = new PromisePool(generatePromises(), CONCURRENCY);
325 return pool.start();
326 }
327 }, {
328 key: 'uploadMissingResources',
329 value: function uploadMissingResources(buildId, response, resources) {
330 var missingResourceShas = utils.getMissingResources(response);
331 if (!missingResourceShas.length) {
332 return Promise.resolve();
333 }
334
335 var resourcesBySha = resources.reduce(function (map, resource) {
336 map[resource.sha] = resource;
337 return map;
338 }, {});
339 var missingResources = missingResourceShas.map(function (resource) {
340 return resourcesBySha[resource.id];
341 });
342
343 return this.uploadResources(buildId, missingResources);
344 }
345 }, {
346 key: 'createSnapshot',
347 value: function createSnapshot(buildId, resources, options) {
348 options = options || {};
349 resources = resources || [];
350
351 var data = {
352 data: {
353 type: 'snapshots',
354 attributes: {
355 name: options.name || null,
356 'enable-javascript': options.enableJavaScript || null,
357 widths: options.widths || null,
358 'minimum-height': options.minimumHeight || null
359 },
360 relationships: {
361 resources: {
362 data: resources.map(function (resource) {
363 return resource.serialize();
364 })
365 }
366 }
367 }
368 };
369
370 this._sdkClientInfo = options.clientInfo;
371 this._sdkEnvironmentInfo = options.environmentInfo;
372 return this._httpPost(this.apiUrl + '/builds/' + buildId + '/snapshots/', data);
373 }
374 }, {
375 key: 'finalizeSnapshot',
376 value: function finalizeSnapshot(snapshotId) {
377 return this._httpPost(this.apiUrl + '/snapshots/' + snapshotId + '/finalize', {});
378 }
379 }, {
380 key: 'finalizeBuild',
381 value: function finalizeBuild(buildId, options) {
382 options = options || {};
383 var allShards = options.allShards || false;
384 var query = allShards ? '?all-shards=true' : '';
385 return this._httpPost(this.apiUrl + '/builds/' + buildId + '/finalize' + query, {});
386 }
387 }]);
388
389 return PercyClient;
390}();
391
392module.exports = PercyClient;
\No newline at end of file