@percy/core 1.0.0 → 1.0.3
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/api.js +94 -0
- package/dist/browser.js +292 -0
- package/dist/config.js +551 -0
- package/dist/discovery.js +118 -0
- package/dist/index.js +5 -0
- package/dist/install.js +156 -0
- package/dist/network.js +298 -0
- package/dist/page.js +264 -0
- package/dist/percy.js +484 -0
- package/dist/queue.js +152 -0
- package/dist/server.js +430 -0
- package/dist/session.js +103 -0
- package/dist/snapshot.js +433 -0
- package/dist/utils.js +127 -0
- package/package.json +11 -11
- package/post-install.js +20 -0
- package/test/helpers/dedent.js +27 -0
- package/test/helpers/index.js +34 -0
- package/test/helpers/request.js +15 -0
- package/test/helpers/server.js +33 -0
- package/types/index.d.ts +101 -0
package/dist/percy.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import PercyClient from '@percy/client';
|
|
2
|
+
import PercyConfig from '@percy/config';
|
|
3
|
+
import logger from '@percy/logger';
|
|
4
|
+
import Queue from './queue.js';
|
|
5
|
+
import Browser from './browser.js';
|
|
6
|
+
import { createPercyServer, createStaticServer } from './api.js';
|
|
7
|
+
import { gatherSnapshots, validateSnapshotOptions, discoverSnapshotResources } from './snapshot.js';
|
|
8
|
+
import { generatePromise } from './utils.js'; // A Percy instance will create a new build when started, handle snapshot
|
|
9
|
+
// creation, asset discovery, and resource uploads, and will finalize the build
|
|
10
|
+
// when stopped. Snapshots are processed concurrently and the build is not
|
|
11
|
+
// finalized until all snapshots have been handled.
|
|
12
|
+
|
|
13
|
+
export class Percy {
|
|
14
|
+
log = logger('core');
|
|
15
|
+
readyState = null;
|
|
16
|
+
#uploads = new Queue();
|
|
17
|
+
#snapshots = new Queue(); // Static shortcut to create and start an instance in one call
|
|
18
|
+
|
|
19
|
+
static async start(options) {
|
|
20
|
+
let instance = new this(options);
|
|
21
|
+
await instance.start();
|
|
22
|
+
return instance;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
constructor({
|
|
26
|
+
// initial log level
|
|
27
|
+
loglevel,
|
|
28
|
+
// do not eagerly upload snapshots
|
|
29
|
+
deferUploads,
|
|
30
|
+
// run without uploading anything
|
|
31
|
+
skipUploads,
|
|
32
|
+
// implies `skipUploads` and also skips asset discovery
|
|
33
|
+
dryRun,
|
|
34
|
+
// configuration filepath
|
|
35
|
+
config,
|
|
36
|
+
// provided to @percy/client
|
|
37
|
+
token,
|
|
38
|
+
clientInfo = '',
|
|
39
|
+
environmentInfo = '',
|
|
40
|
+
// snapshot server options
|
|
41
|
+
server = true,
|
|
42
|
+
port = 5338,
|
|
43
|
+
// options such as `snapshot` and `discovery` that are valid Percy config
|
|
44
|
+
// options which will become accessible via the `.config` property
|
|
45
|
+
...options
|
|
46
|
+
} = {}) {
|
|
47
|
+
if (loglevel) this.loglevel(loglevel);
|
|
48
|
+
this.dryRun = !!dryRun;
|
|
49
|
+
this.skipUploads = this.dryRun || !!skipUploads;
|
|
50
|
+
this.deferUploads = this.skipUploads || !!deferUploads;
|
|
51
|
+
if (this.deferUploads) this.#uploads.stop();
|
|
52
|
+
this.config = PercyConfig.load({
|
|
53
|
+
overrides: options,
|
|
54
|
+
path: config
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (this.config.discovery.concurrency) {
|
|
58
|
+
let {
|
|
59
|
+
concurrency
|
|
60
|
+
} = this.config.discovery;
|
|
61
|
+
this.#uploads.concurrency = concurrency;
|
|
62
|
+
this.#snapshots.concurrency = concurrency;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.client = new PercyClient({
|
|
66
|
+
token,
|
|
67
|
+
clientInfo,
|
|
68
|
+
environmentInfo
|
|
69
|
+
});
|
|
70
|
+
this.browser = new Browser({ ...this.config.discovery.launchOptions,
|
|
71
|
+
cookies: this.config.discovery.cookies
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (server) {
|
|
75
|
+
this.server = createPercyServer(this, port);
|
|
76
|
+
} // generator methods are wrapped to autorun and return promises
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
for (let m of ['start', 'stop', 'flush', 'idle', 'snapshot']) {
|
|
80
|
+
// the original generator can be referenced with percy.yield.<method>
|
|
81
|
+
let method = (this.yield || (this.yield = {}))[m] = this[m].bind(this);
|
|
82
|
+
|
|
83
|
+
this[m] = (...args) => generatePromise(method(...args)).then();
|
|
84
|
+
}
|
|
85
|
+
} // Shortcut for controlling the global logger's log level.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
loglevel(level) {
|
|
89
|
+
return logger.loglevel(level);
|
|
90
|
+
} // Snapshot server API address
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
address() {
|
|
94
|
+
var _this$server;
|
|
95
|
+
|
|
96
|
+
return (_this$server = this.server) === null || _this$server === void 0 ? void 0 : _this$server.address();
|
|
97
|
+
} // Set client & environment info, and override loaded config options
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
setConfig({
|
|
101
|
+
clientInfo,
|
|
102
|
+
environmentInfo,
|
|
103
|
+
...config
|
|
104
|
+
}) {
|
|
105
|
+
this.client.addClientInfo(clientInfo);
|
|
106
|
+
this.client.addEnvironmentInfo(environmentInfo); // normalize config and do nothing if empty
|
|
107
|
+
|
|
108
|
+
config = PercyConfig.normalize(config, {
|
|
109
|
+
schema: '/config'
|
|
110
|
+
});
|
|
111
|
+
if (!config) return this.config; // validate provided config options
|
|
112
|
+
|
|
113
|
+
let errors = PercyConfig.validate(config);
|
|
114
|
+
|
|
115
|
+
if (errors) {
|
|
116
|
+
this.log.warn('Invalid config:');
|
|
117
|
+
|
|
118
|
+
for (let e of errors) this.log.warn(`- ${e.path}: ${e.message}`);
|
|
119
|
+
} // merge and override existing config options
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
this.config = PercyConfig.merge([this.config, config], (path, prev, next) => {
|
|
123
|
+
// replace arrays instead of merging
|
|
124
|
+
return Array.isArray(next) && [path, next];
|
|
125
|
+
}); // adjust concurrency if necessary
|
|
126
|
+
|
|
127
|
+
if (this.config.discovery.concurrency) {
|
|
128
|
+
let {
|
|
129
|
+
concurrency
|
|
130
|
+
} = this.config.discovery;
|
|
131
|
+
this.#uploads.concurrency = concurrency;
|
|
132
|
+
this.#snapshots.concurrency = concurrency;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return this.config;
|
|
136
|
+
} // Resolves once snapshot and upload queues are idle
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async *idle() {
|
|
140
|
+
yield* this.#snapshots.idle();
|
|
141
|
+
yield* this.#uploads.idle();
|
|
142
|
+
} // Immediately stops all queues, preventing any more tasks from running
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
close() {
|
|
146
|
+
this.#snapshots.close(true);
|
|
147
|
+
this.#uploads.close(true);
|
|
148
|
+
} // Starts a local API server, a browser process, and queues creating a new Percy build which will run
|
|
149
|
+
// at a later time when uploads are deferred, or run immediately when not deferred.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async *start(options) {
|
|
153
|
+
// already starting or started
|
|
154
|
+
if (this.readyState != null) return;
|
|
155
|
+
this.readyState = 0; // create a percy build as the first immediately queued task
|
|
156
|
+
|
|
157
|
+
let buildTask = this.#uploads.push('build/create', () => {
|
|
158
|
+
// pause other queued tasks until after the build is created
|
|
159
|
+
this.#uploads.stop();
|
|
160
|
+
return this.client.createBuild().then(({
|
|
161
|
+
data: {
|
|
162
|
+
id,
|
|
163
|
+
attributes
|
|
164
|
+
}
|
|
165
|
+
}) => {
|
|
166
|
+
this.build = {
|
|
167
|
+
id
|
|
168
|
+
};
|
|
169
|
+
this.build.number = attributes['build-number'];
|
|
170
|
+
this.build.url = attributes['web-url'];
|
|
171
|
+
this.#uploads.run();
|
|
172
|
+
});
|
|
173
|
+
}, 0); // handle deferred build errors
|
|
174
|
+
|
|
175
|
+
if (this.deferUploads) {
|
|
176
|
+
buildTask.catch(err => {
|
|
177
|
+
this.build = {
|
|
178
|
+
error: 'Failed to create build'
|
|
179
|
+
};
|
|
180
|
+
this.log.error(this.build.error);
|
|
181
|
+
this.log.error(err);
|
|
182
|
+
this.close();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
var _this$server2;
|
|
188
|
+
|
|
189
|
+
// when not deferred, wait until the build is created first
|
|
190
|
+
if (!this.deferUploads) await buildTask; // maybe launch the discovery browser
|
|
191
|
+
|
|
192
|
+
if (!this.dryRun && (options === null || options === void 0 ? void 0 : options.browser) !== false) {
|
|
193
|
+
yield this.browser.launch();
|
|
194
|
+
} // start the server after everything else is ready
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
yield (_this$server2 = this.server) === null || _this$server2 === void 0 ? void 0 : _this$server2.listen(); // mark instance as started
|
|
198
|
+
|
|
199
|
+
this.log.info('Percy has started!');
|
|
200
|
+
this.readyState = 1;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
var _this$server3;
|
|
203
|
+
|
|
204
|
+
// on error, close any running server and browser
|
|
205
|
+
await ((_this$server3 = this.server) === null || _this$server3 === void 0 ? void 0 : _this$server3.close());
|
|
206
|
+
await this.browser.close(); // mark instance as closed
|
|
207
|
+
|
|
208
|
+
this.readyState = 3; // when uploads are deferred, cancel build creation
|
|
209
|
+
|
|
210
|
+
if (error.canceled && this.deferUploads) {
|
|
211
|
+
this.#uploads.cancel('build/create');
|
|
212
|
+
this.readyState = null;
|
|
213
|
+
} // throw an easier-to-understand error when the port is taken
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if (error.code === 'EADDRINUSE') {
|
|
217
|
+
throw new Error('Percy is already running or the port is in use');
|
|
218
|
+
} else {
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} // Wait for currently queued snapshots then run and wait for resulting uploads
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async *flush(close) {
|
|
226
|
+
try {
|
|
227
|
+
// wait until the next event loop for synchronous snapshots
|
|
228
|
+
yield new Promise(r => setImmediate(r)); // close the snapshot queue and wait for it to empty
|
|
229
|
+
|
|
230
|
+
if (this.#snapshots.size) {
|
|
231
|
+
if (close) this.#snapshots.close();
|
|
232
|
+
yield* this.#snapshots.flush(s => {
|
|
233
|
+
// do not log a count when not closing or while dry-running
|
|
234
|
+
if (!close || this.dryRun) return;
|
|
235
|
+
this.log.progress(`Processing ${s} snapshot${s !== 1 ? 's' : ''}...`, !!s);
|
|
236
|
+
});
|
|
237
|
+
} // run, close, and wait for the upload queue to empty
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if (!this.skipUploads && this.#uploads.size) {
|
|
241
|
+
if (close) this.#uploads.close(); // prevent creating an empty build when deferred
|
|
242
|
+
|
|
243
|
+
if (!this.deferUploads || !this.#uploads.has('build/create') || this.#uploads.size > 1) {
|
|
244
|
+
yield* this.#uploads.flush(s => {
|
|
245
|
+
// do not log a count when not closing or while creating a build
|
|
246
|
+
if (!close || this.#uploads.has('build/create')) return;
|
|
247
|
+
this.log.progress(`Uploading ${s} snapshot${s !== 1 ? 's' : ''}...`, !!s);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
// reopen closed queues when canceled
|
|
253
|
+
|
|
254
|
+
/* istanbul ignore else: all errors bubble */
|
|
255
|
+
if (close && error.canceled) {
|
|
256
|
+
this.#snapshots.open();
|
|
257
|
+
this.#uploads.open();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
} // Stops the local API server and browser once snapshots have completed and finalizes the Percy
|
|
263
|
+
// build. Does nothing if not running. When `force` is true, any queued tasks are cleared.
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async *stop(force) {
|
|
267
|
+
var _this$server4, _this$build, _this$build2;
|
|
268
|
+
|
|
269
|
+
// not started, but the browser was launched
|
|
270
|
+
if (!this.readyState && this.browser.isConnected()) {
|
|
271
|
+
await this.browser.close();
|
|
272
|
+
} // not started or already stopped
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
if (!this.readyState || this.readyState > 2) return; // close queues asap
|
|
276
|
+
|
|
277
|
+
if (force) this.close(); // already stopping
|
|
278
|
+
|
|
279
|
+
if (this.readyState === 2) return;
|
|
280
|
+
this.readyState = 2; // log when force stopping
|
|
281
|
+
|
|
282
|
+
if (force) this.log.info('Stopping percy...');
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
// process uploads and close queues
|
|
286
|
+
yield* this.yield.flush(true);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// reset ready state when canceled
|
|
289
|
+
|
|
290
|
+
/* istanbul ignore else: all errors bubble */
|
|
291
|
+
if (error.canceled) this.readyState = 1;
|
|
292
|
+
throw error;
|
|
293
|
+
} // if dry-running, log the total number of snapshots
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
if (this.dryRun && this.#uploads.size) {
|
|
297
|
+
let total = this.#uploads.size - 1; // subtract the build task
|
|
298
|
+
|
|
299
|
+
this.log.info(`Found ${total} snapshot${total !== 1 ? 's' : ''}`);
|
|
300
|
+
} // close any running server and browser
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
await ((_this$server4 = this.server) === null || _this$server4 === void 0 ? void 0 : _this$server4.close());
|
|
304
|
+
await this.browser.close(); // finalize and log build info
|
|
305
|
+
|
|
306
|
+
let meta = {
|
|
307
|
+
build: this.build
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
if ((_this$build = this.build) !== null && _this$build !== void 0 && _this$build.failed) {
|
|
311
|
+
// do not finalize failed builds
|
|
312
|
+
this.log.warn(`Build #${this.build.number} failed: ${this.build.url}`, meta);
|
|
313
|
+
} else if ((_this$build2 = this.build) !== null && _this$build2 !== void 0 && _this$build2.id) {
|
|
314
|
+
// finalize the build
|
|
315
|
+
await this.client.finalizeBuild(this.build.id);
|
|
316
|
+
this.log.info(`Finalized build #${this.build.number}: ${this.build.url}`, meta);
|
|
317
|
+
} else {
|
|
318
|
+
// no build was ever created (likely failed while deferred)
|
|
319
|
+
this.log.warn('Build not created', meta);
|
|
320
|
+
} // mark instance as stopped
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
this.readyState = 3;
|
|
324
|
+
} // Deprecated capture method
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
capture(options) {
|
|
328
|
+
this.log.deprecated('The #capture() method will be ' + 'removed in 1.0.0. Use #snapshot() instead.');
|
|
329
|
+
return this.snapshot(options);
|
|
330
|
+
} // Takes one or more snapshots of a page while discovering resources to upload with the
|
|
331
|
+
// snapshot. Once asset discovery has completed, the queued snapshot will resolve and an upload
|
|
332
|
+
// task will be queued separately. Accepts several different syntaxes for taking snapshots using
|
|
333
|
+
// various methods.
|
|
334
|
+
//
|
|
335
|
+
// snapshot(url|{url}|[...url|{url}])
|
|
336
|
+
// - requires fully qualified resolvable urls
|
|
337
|
+
// - snapshot options may be provided with the object syntax
|
|
338
|
+
//
|
|
339
|
+
// snapshot({snapshots:[...url|{url}]})
|
|
340
|
+
// - optional `baseUrl` prepended to snapshot urls
|
|
341
|
+
// - optional `options` apply to all or specific snapshots
|
|
342
|
+
//
|
|
343
|
+
// snapshot(sitemap|{sitemap})
|
|
344
|
+
// - required to be a fully qualified resolvable url ending in `.xml`
|
|
345
|
+
// - optional `include`/`exclude` to filter snapshots
|
|
346
|
+
// - optional `options` apply to all or specific snapshots
|
|
347
|
+
//
|
|
348
|
+
// snapshot({serve})
|
|
349
|
+
// - server address is prepended to snapshot urls
|
|
350
|
+
// - optional `baseUrl` used when serving pages
|
|
351
|
+
// - optional `rewrites`/`cleanUrls` to control snapshot urls
|
|
352
|
+
// - optional `include`/`exclude` to filter snapshots
|
|
353
|
+
// - optional `snapshots`, with fallback to built-in sitemap.xml
|
|
354
|
+
// - optional `options` apply to all or specific snapshots
|
|
355
|
+
//
|
|
356
|
+
// All available syntaxes will eventually push snapshots to the snapshot queue without the need to
|
|
357
|
+
// await on this method directly. This method resolves after the snapshot upload is queued, but
|
|
358
|
+
// does not await on the upload to complete.
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
snapshot(options) {
|
|
362
|
+
var _this$build3;
|
|
363
|
+
|
|
364
|
+
if (this.readyState !== 1) {
|
|
365
|
+
throw new Error('Not running');
|
|
366
|
+
} else if ((_this$build3 = this.build) !== null && _this$build3 !== void 0 && _this$build3.error) {
|
|
367
|
+
throw new Error(this.build.error);
|
|
368
|
+
} else if (Array.isArray(options)) {
|
|
369
|
+
return Promise.all(options.map(o => this.snapshot(o)));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (typeof options === 'string') {
|
|
373
|
+
options = options.endsWith('.xml') ? {
|
|
374
|
+
sitemap: options
|
|
375
|
+
} : {
|
|
376
|
+
url: options
|
|
377
|
+
};
|
|
378
|
+
} // validate options and add client & environment info
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
options = validateSnapshotOptions(options);
|
|
382
|
+
this.client.addClientInfo(options.clientInfo);
|
|
383
|
+
this.client.addEnvironmentInfo(options.environmentInfo); // return an async generator to allow cancelation
|
|
384
|
+
|
|
385
|
+
return async function* () {
|
|
386
|
+
let server = 'serve' in options ? await createStaticServer(options).listen() : null;
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
if (server) {
|
|
390
|
+
// automatically set specific static server options
|
|
391
|
+
options.baseUrl = new URL(options.baseUrl || '', server.address()).href;
|
|
392
|
+
if (!options.snapshots) options.sitemap = new URL('sitemap.xml', options.baseUrl).href;
|
|
393
|
+
} // gather snapshots from options
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
let snapshots = yield gatherSnapshots(this, options);
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
// yield each task individually to allow canceling
|
|
400
|
+
let tasks = snapshots.map(s => this._takeSnapshot(s));
|
|
401
|
+
|
|
402
|
+
for (let task of tasks) yield task;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
// cancel queued snapshots that may not have started
|
|
405
|
+
snapshots.map(s => this._cancelSnapshot(s));
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
} finally {
|
|
409
|
+
await (server === null || server === void 0 ? void 0 : server.close());
|
|
410
|
+
}
|
|
411
|
+
}.call(this);
|
|
412
|
+
} // Cancel any pending snapshot or snapshot uploads
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
_cancelSnapshot(snapshot) {
|
|
416
|
+
this.#snapshots.cancel(`snapshot/${snapshot.name}`);
|
|
417
|
+
|
|
418
|
+
for (let {
|
|
419
|
+
name
|
|
420
|
+
} of [snapshot, ...(snapshot.additionalSnapshots || [])]) {
|
|
421
|
+
this.#uploads.cancel(`upload/${name}`);
|
|
422
|
+
}
|
|
423
|
+
} // Resolves after asset discovery has finished and uploads have been queued
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
_takeSnapshot(snapshot) {
|
|
427
|
+
// cancel any existing snapshot with the same name
|
|
428
|
+
this._cancelSnapshot(snapshot);
|
|
429
|
+
|
|
430
|
+
return this.#snapshots.push(`snapshot/${snapshot.name}`, async function* () {
|
|
431
|
+
try {
|
|
432
|
+
yield* discoverSnapshotResources(this, snapshot, (snap, resources) => {
|
|
433
|
+
if (!this.dryRun) this.log.info(`Snapshot taken: ${snap.name}`, snap.meta);
|
|
434
|
+
|
|
435
|
+
this._scheduleUpload(snap.name, { ...snap,
|
|
436
|
+
resources
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (error.canceled) {
|
|
441
|
+
this.log.error('Received a duplicate snapshot name, ' + `the previous snapshot was canceled: ${snapshot.name}`);
|
|
442
|
+
} else {
|
|
443
|
+
this.log.error(`Encountered an error taking snapshot: ${snapshot.name}`, snapshot.meta);
|
|
444
|
+
this.log.error(error, snapshot.meta);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}.bind(this));
|
|
448
|
+
} // Queues a snapshot upload with the provided options
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
_scheduleUpload(name, options) {
|
|
452
|
+
var _this$build4;
|
|
453
|
+
|
|
454
|
+
if ((_this$build4 = this.build) !== null && _this$build4 !== void 0 && _this$build4.error) {
|
|
455
|
+
throw new Error(this.build.error);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return this.#uploads.push(`upload/${name}`, async () => {
|
|
459
|
+
try {
|
|
460
|
+
/* istanbul ignore if: useful for other internal packages */
|
|
461
|
+
if (typeof options === 'function') options = await options();
|
|
462
|
+
await this.client.sendSnapshot(this.build.id, options);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
var _error$response;
|
|
465
|
+
|
|
466
|
+
let failed = ((_error$response = error.response) === null || _error$response === void 0 ? void 0 : _error$response.statusCode) === 422 && error.response.body.errors.find(e => {
|
|
467
|
+
var _e$source;
|
|
468
|
+
|
|
469
|
+
return ((_e$source = e.source) === null || _e$source === void 0 ? void 0 : _e$source.pointer) === '/data/attributes/build';
|
|
470
|
+
});
|
|
471
|
+
this.log.error(`Encountered an error uploading snapshot: ${name}`, options.meta);
|
|
472
|
+
this.log.error((failed === null || failed === void 0 ? void 0 : failed.detail) ?? error, options.meta); // build failed at some point, stop accepting snapshots
|
|
473
|
+
|
|
474
|
+
if (failed) {
|
|
475
|
+
this.build.error = failed.detail;
|
|
476
|
+
this.build.failed = true;
|
|
477
|
+
this.close();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
}
|
|
484
|
+
export default Percy;
|
package/dist/queue.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { generatePromise, waitFor } from './utils.js';
|
|
2
|
+
export class Queue {
|
|
3
|
+
running = true;
|
|
4
|
+
closed = false;
|
|
5
|
+
#queued = new Map();
|
|
6
|
+
#pending = new Map();
|
|
7
|
+
|
|
8
|
+
constructor(concurrency = 10) {
|
|
9
|
+
this.concurrency = concurrency;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
push(id, callback, priority) {
|
|
13
|
+
/* istanbul ignore next: race condition paranoia */
|
|
14
|
+
if (this.closed && !id.startsWith('@@/')) return;
|
|
15
|
+
this.cancel(id);
|
|
16
|
+
let task = {
|
|
17
|
+
id,
|
|
18
|
+
callback,
|
|
19
|
+
priority
|
|
20
|
+
};
|
|
21
|
+
task.promise = new Promise((resolve, reject) => {
|
|
22
|
+
Object.assign(task, {
|
|
23
|
+
resolve,
|
|
24
|
+
reject
|
|
25
|
+
});
|
|
26
|
+
this.#queued.set(id, task);
|
|
27
|
+
|
|
28
|
+
this._dequeue();
|
|
29
|
+
});
|
|
30
|
+
return task.promise;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
cancel(id) {
|
|
34
|
+
var _this$pending$get, _this$pending$get$can;
|
|
35
|
+
|
|
36
|
+
(_this$pending$get = this.#pending.get(id)) === null || _this$pending$get === void 0 ? void 0 : (_this$pending$get$can = _this$pending$get.cancel) === null || _this$pending$get$can === void 0 ? void 0 : _this$pending$get$can.call(_this$pending$get);
|
|
37
|
+
this.#pending.delete(id);
|
|
38
|
+
this.#queued.delete(id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
has(id) {
|
|
42
|
+
return this.#queued.has(id) || this.#pending.has(id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clear() {
|
|
46
|
+
this.#queued.clear();
|
|
47
|
+
return this.size;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get size() {
|
|
51
|
+
return this.#queued.size + this.#pending.size;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
run() {
|
|
55
|
+
this.running = true;
|
|
56
|
+
|
|
57
|
+
while (this.running && this.#queued.size && this.#pending.size < this.concurrency) this._dequeue();
|
|
58
|
+
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
stop() {
|
|
63
|
+
this.running = false;
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
open() {
|
|
68
|
+
this.closed = false;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
close(abort) {
|
|
73
|
+
if (abort) this.stop().clear();
|
|
74
|
+
this.closed = true;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
idle(callback) {
|
|
79
|
+
return waitFor(() => {
|
|
80
|
+
callback === null || callback === void 0 ? void 0 : callback(this.#pending.size);
|
|
81
|
+
return !this.#pending.size;
|
|
82
|
+
}, {
|
|
83
|
+
idle: 10
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
empty(callback) {
|
|
88
|
+
return waitFor(() => {
|
|
89
|
+
callback === null || callback === void 0 ? void 0 : callback(this.size);
|
|
90
|
+
return !this.size;
|
|
91
|
+
}, {
|
|
92
|
+
idle: 10
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
flush(callback) {
|
|
97
|
+
let stopped = !this.running;
|
|
98
|
+
this.run().push('@@/flush', () => {
|
|
99
|
+
if (stopped) this.stop();
|
|
100
|
+
});
|
|
101
|
+
return this.idle(pend => {
|
|
102
|
+
let left = [...this.#queued.keys()].indexOf('@@/flush');
|
|
103
|
+
if (!~left && !this.#pending.has('@@/flush')) left = 0;
|
|
104
|
+
callback === null || callback === void 0 ? void 0 : callback(pend + left);
|
|
105
|
+
}).canceled(() => {
|
|
106
|
+
if (stopped) this.stop();
|
|
107
|
+
this.cancel('@@/flush');
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
next() {
|
|
112
|
+
let next;
|
|
113
|
+
|
|
114
|
+
for (let [id, task] of this.#queued) {
|
|
115
|
+
if (!next || task.priority != null && next.priority == null || task.priority < next.priority) next = task;
|
|
116
|
+
if (id === '@@/flush') break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return next;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_dequeue() {
|
|
123
|
+
if (!this.running) return;
|
|
124
|
+
if (this.#pending.size >= this.concurrency) return;
|
|
125
|
+
let task = this.next();
|
|
126
|
+
if (!task) return;
|
|
127
|
+
this.#queued.delete(task.id);
|
|
128
|
+
this.#pending.set(task.id, task);
|
|
129
|
+
|
|
130
|
+
let done = callback => arg => {
|
|
131
|
+
var _task$cancel;
|
|
132
|
+
|
|
133
|
+
if (!((_task$cancel = task.cancel) !== null && _task$cancel !== void 0 && _task$cancel.triggered)) {
|
|
134
|
+
this.#pending.delete(task.id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
callback(arg);
|
|
138
|
+
|
|
139
|
+
this._dequeue();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
let gen = generatePromise(task.callback);
|
|
144
|
+
task.cancel = gen.cancel;
|
|
145
|
+
return gen.then(done(task.resolve), done(task.reject));
|
|
146
|
+
} catch (err) {
|
|
147
|
+
done(task.reject)(err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
export default Queue;
|