@percy/core 1.6.3 → 1.7.1

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/percy.js CHANGED
@@ -5,7 +5,7 @@ import Queue from './queue.js';
5
5
  import Browser from './browser.js';
6
6
  import { createPercyServer, createStaticServer } from './api.js';
7
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
8
+ import { generatePromise, yieldAll } from './utils.js'; // A Percy instance will create a new build when started, handle snapshot
9
9
  // creation, asset discovery, and resource uploads, and will finalize the build
10
10
  // when stopped. Snapshots are processed concurrently and the build is not
11
11
  // finalized until all snapshots have been handled.
@@ -69,10 +69,6 @@ export class Percy {
69
69
  this.#snapshots.concurrency = concurrency;
70
70
  }
71
71
 
72
- if (this.delayUploads) {
73
- this.#uploads.concurrency = 1;
74
- }
75
-
76
72
  this.client = new PercyClient({
77
73
  token,
78
74
  clientInfo,
@@ -162,17 +158,20 @@ export class Percy {
162
158
  let buildTask = this.#uploads.push('build/create', () => {
163
159
  // pause other queued tasks until after the build is created
164
160
  this.#uploads.stop();
161
+ this.build = {};
165
162
  return this.client.createBuild().then(({
166
163
  data: {
167
164
  id,
168
165
  attributes
169
166
  }
170
167
  }) => {
171
- this.build = {
172
- id
173
- };
174
- this.build.number = attributes['build-number'];
175
- this.build.url = attributes['web-url'];
168
+ let url = attributes['web-url'];
169
+ let number = attributes['build-number'];
170
+ Object.assign(this.build, {
171
+ id,
172
+ url,
173
+ number
174
+ });
176
175
  if (!this.delayUploads) this.#uploads.run();
177
176
  });
178
177
  }, 0); // handle deferred build errors
@@ -246,6 +245,7 @@ export class Percy {
246
245
  if (close) this.#uploads.close(); // prevent creating an empty build when deferred
247
246
 
248
247
  if (!this.deferUploads || !this.#uploads.has('build/create') || this.#uploads.size > 1) {
248
+ if (this.build && !this.build.id) yield* this.#uploads.idle();
249
249
  yield* this.#uploads.flush(s => {
250
250
  // do not log a count when not closing or while creating a build
251
251
  if (!close || this.#uploads.has('build/create')) return;
@@ -371,7 +371,7 @@ export class Percy {
371
371
  } else if ((_this$build3 = this.build) !== null && _this$build3 !== void 0 && _this$build3.error) {
372
372
  throw new Error(this.build.error);
373
373
  } else if (Array.isArray(options)) {
374
- return Promise.all(options.map(o => this.snapshot(o)));
374
+ return yieldAll(options.map(o => this.yield.snapshot(o)));
375
375
  }
376
376
 
377
377
  if (typeof options === 'string') {
@@ -398,13 +398,11 @@ export class Percy {
398
398
  } // gather snapshots from options
399
399
 
400
400
 
401
- let snapshots = yield gatherSnapshots(this, options);
401
+ let snapshots = yield* gatherSnapshots(this, options);
402
402
 
403
403
  try {
404
- // yield each task individually to allow canceling
405
- let tasks = snapshots.map(s => this._takeSnapshot(s));
406
-
407
- for (let task of tasks) yield task;
404
+ // use a try-catch to cancel snapshots that haven't started when the error occurred
405
+ yield* yieldAll(snapshots.map(s => this._takeSnapshot(s)));
408
406
  } catch (error) {
409
407
  // cancel queued snapshots that may not have started
410
408
  snapshots.map(s => this._cancelSnapshot(s));
@@ -458,10 +456,10 @@ export class Percy {
458
456
 
459
457
  if ((_this$build4 = this.build) !== null && _this$build4 !== void 0 && _this$build4.error) throw new Error(this.build.error); // maybe process any existing delayed uploads
460
458
 
461
- if (!this.skipUploads && this.delayUploads) this.#uploads.run();
459
+ if (!this.skipUploads && this.delayUploads && (!this.build || this.build.id)) this.#uploads.run();
462
460
  return this.#uploads.push(`upload/${name}`, async () => {
463
461
  // when delayed, stop the queue before other uploads are processed
464
- if (this.delayUploads) this.#uploads.stop();
462
+ if (this.readyState < 2 && this.delayUploads) this.#uploads.stop();
465
463
 
466
464
  try {
467
465
  /* istanbul ignore if: useful for other internal packages */
package/dist/snapshot.js CHANGED
@@ -2,7 +2,7 @@ import logger from '@percy/logger';
2
2
  import PercyConfig from '@percy/config';
3
3
  import micromatch from 'micromatch';
4
4
  import { configSchema } from './config.js';
5
- import { request, hostnameMatches, createRootResource, createPercyCSSResource, createLogResource } from './utils.js'; // Throw a better error message for missing or invalid urls
5
+ import { request, hostnameMatches, createRootResource, createPercyCSSResource, createLogResource, yieldTo } from './utils.js'; // Throw a better error message for missing or invalid urls
6
6
 
7
7
  export function validURL(url, base) {
8
8
  if (!url) {
@@ -92,21 +92,20 @@ export function mapSnapshotOptions(percy, snapshots, config) {
92
92
  }, []);
93
93
  } // Returns an array of derived snapshot options
94
94
 
95
- export async function gatherSnapshots(percy, options) {
95
+ export async function* gatherSnapshots(percy, options) {
96
96
  let {
97
97
  baseUrl,
98
98
  snapshots
99
99
  } = options;
100
100
  if ('url' in options) snapshots = [options];
101
- if ('sitemap' in options) snapshots = await getSitemapSnapshots(options); // validate evaluated snapshots
101
+ if ('sitemap' in options) snapshots = yield getSitemapSnapshots(options); // validate evaluated snapshots
102
102
 
103
103
  if (typeof snapshots === 'function') {
104
- ({
105
- snapshots
106
- } = validateSnapshotOptions({
104
+ snapshots = yield* yieldTo(snapshots(baseUrl));
105
+ snapshots = validateSnapshotOptions({
107
106
  baseUrl,
108
- snapshots: await snapshots(baseUrl)
109
- }));
107
+ snapshots
108
+ }).snapshots;
110
109
  } // map snapshots with snapshot options
111
110
 
112
111
 
package/dist/utils.js CHANGED
@@ -46,23 +46,25 @@ export function createPercyCSSResource(url, css) {
46
46
 
47
47
  export function createLogResource(logs) {
48
48
  return createResource(`/percy.${Date.now()}.log`, JSON.stringify(logs), 'text/plain');
49
+ } // Returns true or false if the provided object is a generator or not
50
+
51
+ export function isGenerator(subject) {
52
+ return typeof (subject === null || subject === void 0 ? void 0 : subject.next) === 'function' && (typeof subject[Symbol.iterator] === 'function' || typeof subject[Symbol.asyncIterator] === 'function');
49
53
  } // Iterates over the provided generator and resolves to the final value when done. With an
50
54
  // AbortSignal, the generator will throw with the abort reason when aborted. Also accepts an
51
55
  // optional node-style callback, called before the returned promise resolves.
52
56
 
53
57
  export async function generatePromise(gen, signal, cb) {
54
58
  try {
55
- var _gen;
56
-
57
59
  if (typeof signal === 'function') [cb, signal] = [signal];
58
60
  if (typeof gen === 'function') gen = await gen();
59
61
  let {
60
62
  done,
61
63
  value
62
- } = typeof ((_gen = gen) === null || _gen === void 0 ? void 0 : _gen.next) === 'function' && (typeof gen[Symbol.iterator] === 'function' || typeof gen[Symbol.asyncIterator] === 'function') ? await gen.next() : {
64
+ } = !isGenerator(gen) ? {
63
65
  done: true,
64
66
  value: await gen
65
- };
67
+ } : await gen.next();
66
68
 
67
69
  while (!done) {
68
70
  var _signal;
@@ -103,6 +105,33 @@ export class AbortError extends Error {
103
105
  });
104
106
  }
105
107
 
108
+ } // An async generator that yields after every event loop until the promise settles
109
+
110
+ export async function* yieldTo(subject) {
111
+ var _subject;
112
+
113
+ let pending = typeof ((_subject = subject) === null || _subject === void 0 ? void 0 : _subject.finally) === 'function';
114
+ if (pending) subject = subject.finally(() => pending = false);
115
+ /* eslint-disable-next-line no-unmodified-loop-condition */
116
+
117
+ while (pending) yield new Promise(r => setImmediate(r));
118
+
119
+ return isGenerator(subject) ? yield* subject : await subject;
120
+ } // An async generator that runs provided generators concurrently
121
+
122
+ export async function* yieldAll(all) {
123
+ let res = new Array(all.length).fill();
124
+ all = all.map(yieldTo);
125
+
126
+ while (true) {
127
+ res = await Promise.all(all.map((g, i) => {
128
+ var _res$i, _res$i2;
129
+
130
+ return (_res$i = res[i]) !== null && _res$i !== void 0 && _res$i.done ? res[i] : g.next((_res$i2 = res[i]) === null || _res$i2 === void 0 ? void 0 : _res$i2.value);
131
+ }));
132
+ let vals = res.map(r => r === null || r === void 0 ? void 0 : r.value);
133
+ if (res.some(r => !(r !== null && r !== void 0 && r.done))) yield vals;else return vals;
134
+ }
106
135
  } // An async generator that infinitely yields to the predicate function until a truthy value is
107
136
  // returned. When a timeout is provided, an error will be thrown during the next iteration after the
108
137
  // timeout has been exceeded. If an idle option is provided, the predicate will be yielded to a
@@ -182,11 +211,14 @@ async function scrollToBottom(options, onScroll) {
182
211
  });
183
212
  await ((_onScroll = onScroll) === null || _onScroll === void 0 ? void 0 : _onScroll(i, s));
184
213
  }
185
- } // Serializes the provided function with percy helpers for use in evaluating browser scripts
214
+ } // Used to test if a string looks like a function
215
+
186
216
 
217
+ const FUNC_REG = /^(async\s+)?(function\s*)?(\w+\s*)?\(.*?\)\s*(\{|=>)/is; // Serializes the provided function with percy helpers for use in evaluating browser scripts
187
218
 
188
219
  export function serializeFunction(fn) {
189
- let fnbody = typeof fn === 'string' ? `async eval() {\n${fn}\n}` : fn.toString(); // we might have a function shorthand if this fails
220
+ // stringify or convert a function body into a complete function
221
+ let fnbody = typeof fn === 'string' && !FUNC_REG.test(fn) ? `async function eval() {\n${fn}\n}` : fn.toString(); // we might have a function shorthand if this fails
190
222
 
191
223
  /* eslint-disable-next-line no-new, no-new-func */
192
224
 
@@ -204,7 +236,7 @@ export function serializeFunction(fn) {
204
236
  } // wrap the function body with percy helpers
205
237
 
206
238
 
207
- fnbody = 'function withPercyHelpers() {\n' + ['const { config, snapshot } = window.__PERCY__ ?? {};', `return (${fnbody})({`, ' config, snapshot, generatePromise, yieldFor,', ' waitFor, waitForTimeout, waitForSelector, waitForXPath,', ' scrollToBottom', '}, ...arguments);', `${generatePromise}`, `${yieldFor}`, `${waitFor}`, `${waitForTimeout}`, `${waitForSelector}`, `${waitForXPath}`, `${scrollToBottom}`].join('\n') + '\n}';
239
+ fnbody = 'function withPercyHelpers() {\n' + ['const { config, snapshot } = window.__PERCY__ ?? {};', `return (${fnbody})({`, ' config, snapshot, generatePromise, yieldFor,', ' waitFor, waitForTimeout, waitForSelector, waitForXPath,', ' scrollToBottom', '}, ...arguments);', `${isGenerator}`, `${generatePromise}`, `${yieldFor}`, `${waitFor}`, `${waitForTimeout}`, `${waitForSelector}`, `${waitForXPath}`, `${scrollToBottom}`].join('\n') + '\n}';
208
240
  /* istanbul ignore else: ironic. */
209
241
 
210
242
  if (fnbody.includes('cov_')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,10 +39,10 @@
39
39
  "test:types": "tsd"
40
40
  },
41
41
  "dependencies": {
42
- "@percy/client": "1.6.3",
43
- "@percy/config": "1.6.3",
44
- "@percy/dom": "1.6.3",
45
- "@percy/logger": "1.6.3",
42
+ "@percy/client": "1.7.1",
43
+ "@percy/config": "1.7.1",
44
+ "@percy/dom": "1.7.1",
45
+ "@percy/logger": "1.7.1",
46
46
  "content-disposition": "^0.5.4",
47
47
  "cross-spawn": "^7.0.3",
48
48
  "extract-zip": "^2.0.1",
@@ -53,5 +53,5 @@
53
53
  "rimraf": "^3.0.2",
54
54
  "ws": "^8.0.0"
55
55
  },
56
- "gitHead": "ab48a150317a4af36d536a5671701532c669d2d1"
56
+ "gitHead": "012892cdaf7a07aa1b5aa355017639cee0e8a19e"
57
57
  }