@percy/core 1.0.0-beta.8 → 1.0.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/README.md +226 -67
- package/dist/api.js +94 -0
- package/dist/browser.js +292 -0
- package/dist/config.js +512 -30
- package/dist/discovery.js +118 -0
- package/dist/index.js +5 -29
- package/dist/install.js +156 -0
- package/dist/network.js +298 -0
- package/dist/page.js +264 -0
- package/dist/percy.js +373 -306
- package/dist/queue.js +122 -73
- package/dist/server.js +424 -76
- package/dist/session.js +103 -0
- package/dist/snapshot.js +433 -0
- package/dist/utils.js +127 -0
- package/package.json +42 -28
- package/post-install.js +20 -0
- package/test/helpers/server.js +33 -0
- package/types/index.d.ts +69 -39
- package/dist/discoverer.js +0 -367
- package/dist/percy-css.js +0 -33
- package/dist/utils/assert.js +0 -50
- package/dist/utils/bytes.js +0 -24
- package/dist/utils/idle.js +0 -15
- package/dist/utils/install-browser.js +0 -76
- package/dist/utils/resources.js +0 -75
- package/dist/utils/url.js +0 -64
package/dist/percy.js
CHANGED
|
@@ -1,55 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
var _config = _interopRequireDefault(require("@percy/config"));
|
|
11
|
-
|
|
12
|
-
var _logger = _interopRequireDefault(require("@percy/logger"));
|
|
13
|
-
|
|
14
|
-
var _config2 = require("./config");
|
|
15
|
-
|
|
16
|
-
var _discoverer = _interopRequireDefault(require("./discoverer"));
|
|
17
|
-
|
|
18
|
-
var _percyCss = _interopRequireDefault(require("./percy-css"));
|
|
19
|
-
|
|
20
|
-
var _server = require("./server");
|
|
21
|
-
|
|
22
|
-
var _queue = _interopRequireDefault(require("./queue"));
|
|
23
|
-
|
|
24
|
-
var _assert = _interopRequireDefault(require("./utils/assert"));
|
|
25
|
-
|
|
26
|
-
var _resources = require("./utils/resources");
|
|
27
|
-
|
|
28
|
-
var _url = require("./utils/url");
|
|
29
|
-
|
|
30
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
31
|
-
|
|
32
|
-
function _classPrivateFieldGet(receiver, privateMap) { var descriptor = privateMap.get(receiver); if (!descriptor) { throw new TypeError("attempted to get private field on non-instance"); } if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
|
|
33
|
-
|
|
34
|
-
function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = privateMap.get(receiver); if (!descriptor) { throw new TypeError("attempted to set private field on non-instance"); } if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } return value; }
|
|
35
|
-
|
|
36
|
-
// Register core config options
|
|
37
|
-
_config.default.addSchema(_config2.schema); // A Percy instance will create a new build when started, handle snapshot
|
|
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
|
|
38
9
|
// creation, asset discovery, and resource uploads, and will finalize the build
|
|
39
10
|
// when stopped. Snapshots are processed concurrently and the build is not
|
|
40
11
|
// finalized until all snapshots have been handled.
|
|
41
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
|
|
42
18
|
|
|
43
|
-
var _captures = new WeakMap();
|
|
44
|
-
|
|
45
|
-
var _snapshots = new WeakMap();
|
|
46
|
-
|
|
47
|
-
var _stopping = new WeakMap();
|
|
48
|
-
|
|
49
|
-
var _running = new WeakMap();
|
|
50
|
-
|
|
51
|
-
class Percy {
|
|
52
|
-
// Static shortcut to create and start an instance in one call
|
|
53
19
|
static async start(options) {
|
|
54
20
|
let instance = new this(options);
|
|
55
21
|
await instance.start();
|
|
@@ -57,6 +23,16 @@ class Percy {
|
|
|
57
23
|
}
|
|
58
24
|
|
|
59
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,
|
|
60
36
|
// provided to @percy/client
|
|
61
37
|
token,
|
|
62
38
|
clientInfo = '',
|
|
@@ -64,106 +40,178 @@ class Percy {
|
|
|
64
40
|
// snapshot server options
|
|
65
41
|
server = true,
|
|
66
42
|
port = 5338,
|
|
67
|
-
// capture concurrency
|
|
68
|
-
concurrency = 5,
|
|
69
|
-
// initial log level
|
|
70
|
-
loglevel,
|
|
71
|
-
// configuration filepath
|
|
72
|
-
config,
|
|
73
43
|
// options such as `snapshot` and `discovery` that are valid Percy config
|
|
74
|
-
// options which will become accessible via the
|
|
44
|
+
// options which will become accessible via the `.config` property
|
|
75
45
|
...options
|
|
76
46
|
} = {}) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
80
55
|
});
|
|
81
56
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
}
|
|
86
64
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
65
|
+
this.client = new PercyClient({
|
|
66
|
+
token,
|
|
67
|
+
clientInfo,
|
|
68
|
+
environmentInfo
|
|
90
69
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
writable: true,
|
|
94
|
-
value: false
|
|
70
|
+
this.browser = new Browser({ ...this.config.discovery.launchOptions,
|
|
71
|
+
cookies: this.config.discovery.cookies
|
|
95
72
|
});
|
|
96
73
|
|
|
97
|
-
if (loglevel) {
|
|
98
|
-
this.loglevel(loglevel);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
74
|
if (server) {
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
}
|
|
75
|
+
this.server = createPercyServer(this, port);
|
|
76
|
+
} // generator methods are wrapped to autorun and return promises
|
|
105
77
|
|
|
106
|
-
_classPrivateFieldSet(this, _snapshots, new _queue.default());
|
|
107
78
|
|
|
108
|
-
|
|
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);
|
|
109
82
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
overrides: options
|
|
113
|
-
});
|
|
114
|
-
this.discoverer = new _discoverer.default(this.config.discovery);
|
|
115
|
-
this.client = new _client.default({
|
|
116
|
-
token,
|
|
117
|
-
clientInfo,
|
|
118
|
-
environmentInfo
|
|
119
|
-
});
|
|
83
|
+
this[m] = (...args) => generatePromise(method(...args)).then();
|
|
84
|
+
}
|
|
120
85
|
} // Shortcut for controlling the global logger's log level.
|
|
121
86
|
|
|
122
87
|
|
|
123
88
|
loglevel(level) {
|
|
124
|
-
return
|
|
125
|
-
} //
|
|
89
|
+
return logger.loglevel(level);
|
|
90
|
+
} // Snapshot server API address
|
|
126
91
|
|
|
127
92
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
} // Starts the local API server, the asset discovery process, and creates a new
|
|
131
|
-
// Percy build. When an error is encountered, the discoverer and server are closed.
|
|
93
|
+
address() {
|
|
94
|
+
var _this$server;
|
|
132
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
|
|
133
98
|
|
|
134
|
-
async start() {
|
|
135
|
-
// throws when the token is missing
|
|
136
|
-
this.client.getToken();
|
|
137
99
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
}
|
|
143
134
|
|
|
135
|
+
return this.config;
|
|
136
|
+
} // Resolves once snapshot and upload queues are idle
|
|
144
137
|
|
|
145
|
-
await this.discoverer.launch();
|
|
146
|
-
await this.client.createBuild(); // log build details
|
|
147
138
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
152
164
|
}
|
|
153
|
-
}
|
|
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
|
+
}
|
|
154
185
|
|
|
155
|
-
|
|
186
|
+
try {
|
|
187
|
+
var _this$server2;
|
|
156
188
|
|
|
157
|
-
|
|
189
|
+
// when not deferred, wait until the build is created first
|
|
190
|
+
if (!this.deferUploads) await buildTask; // maybe launch the discovery browser
|
|
158
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
|
|
159
195
|
|
|
160
|
-
|
|
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;
|
|
161
201
|
} catch (error) {
|
|
162
|
-
var _this$
|
|
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
|
|
163
214
|
|
|
164
|
-
// on error, close any running browser or server
|
|
165
|
-
await this.discoverer.close();
|
|
166
|
-
(_this$server = this.server) === null || _this$server === void 0 ? void 0 : _this$server.close(); // throw an easier-to understand error when the port is taken
|
|
167
215
|
|
|
168
216
|
if (error.code === 'EADDRINUSE') {
|
|
169
217
|
throw new Error('Percy is already running or the port is in use');
|
|
@@ -171,247 +219,266 @@ class Percy {
|
|
|
171
219
|
throw error;
|
|
172
220
|
}
|
|
173
221
|
}
|
|
174
|
-
} //
|
|
175
|
-
// finalizes the Percy build. Does nothing if not running.
|
|
222
|
+
} // Wait for currently queued snapshots then run and wait for resulting uploads
|
|
176
223
|
|
|
177
224
|
|
|
178
|
-
async
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
});
|
|
189
249
|
}
|
|
190
|
-
}
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
// reopen closed queues when canceled
|
|
191
253
|
|
|
192
|
-
|
|
254
|
+
/* istanbul ignore else: all errors bubble */
|
|
255
|
+
if (close && error.canceled) {
|
|
256
|
+
this.#snapshots.open();
|
|
257
|
+
this.#uploads.open();
|
|
258
|
+
}
|
|
193
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.
|
|
194
264
|
|
|
195
|
-
if (_classPrivateFieldGet(this, _captures).length) {
|
|
196
|
-
_logger.default.info(`Waiting for ${_classPrivateFieldGet(this, _captures).length} page(s) to finish snapshotting`, meta);
|
|
197
|
-
} else if (_classPrivateFieldGet(this, _snapshots).length) {
|
|
198
|
-
_logger.default.info(`Waiting for ${_classPrivateFieldGet(this, _snapshots).length} snapshot(s) to finish uploading`, meta);
|
|
199
|
-
} // wait for any queued captures or snapshots
|
|
200
265
|
|
|
266
|
+
async *stop(force) {
|
|
267
|
+
var _this$server4, _this$build, _this$build2;
|
|
201
268
|
|
|
202
|
-
|
|
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
|
|
203
273
|
|
|
204
|
-
(_this$server2 = this.server) === null || _this$server2 === void 0 ? void 0 : _this$server2.close();
|
|
205
|
-
await this.discoverer.close();
|
|
206
274
|
|
|
207
|
-
|
|
275
|
+
if (!this.readyState || this.readyState > 2) return; // close queues asap
|
|
208
276
|
|
|
277
|
+
if (force) this.close(); // already stopping
|
|
209
278
|
|
|
210
|
-
|
|
279
|
+
if (this.readyState === 2) return;
|
|
280
|
+
this.readyState = 2; // log when force stopping
|
|
211
281
|
|
|
212
|
-
|
|
282
|
+
if (force) this.log.info('Stopping percy...');
|
|
213
283
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
async idle() {
|
|
220
|
-
await Promise.all([_classPrivateFieldGet(this, _captures).idle(), _classPrivateFieldGet(this, _snapshots).idle()]);
|
|
221
|
-
} // Handles asset discovery for the URL and DOM snapshot at each requested
|
|
222
|
-
// width with the provided options. Resolves when the snapshot has been taken
|
|
223
|
-
// and asset discovery is finished, but does not gaurantee that the snapshot
|
|
224
|
-
// will be succesfully uploaded.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
snapshot({
|
|
228
|
-
url,
|
|
229
|
-
name,
|
|
230
|
-
domSnapshot,
|
|
231
|
-
widths,
|
|
232
|
-
minHeight,
|
|
233
|
-
percyCSS,
|
|
234
|
-
requestHeaders,
|
|
235
|
-
enableJavaScript,
|
|
236
|
-
clientInfo,
|
|
237
|
-
environmentInfo
|
|
238
|
-
}) {
|
|
239
|
-
var _widths, _widths2, _minHeight, _ref, _enableJavaScript;
|
|
240
|
-
|
|
241
|
-
// required assertions
|
|
242
|
-
(0, _assert.default)(this.isRunning(), 'Not running');
|
|
243
|
-
(0, _assert.default)(url, 'Missing required argument: url');
|
|
244
|
-
(0, _assert.default)(name, 'Missing required argument: name');
|
|
245
|
-
(0, _assert.default)(domSnapshot, 'Missing required argument: domSnapshot'); // fallback to instance snapshot widths
|
|
284
|
+
try {
|
|
285
|
+
// process uploads and close queues
|
|
286
|
+
yield* this.yield.flush(true);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// reset ready state when canceled
|
|
246
289
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
250
294
|
|
|
251
|
-
url = (0, _url.normalizeURL)(url); // fallback to instance minimum height
|
|
252
295
|
|
|
253
|
-
|
|
296
|
+
if (this.dryRun && this.#uploads.size) {
|
|
297
|
+
let total = this.#uploads.size - 1; // subtract the build task
|
|
254
298
|
|
|
255
|
-
|
|
299
|
+
this.log.info(`Found ${total} snapshot${total !== 1 ? 's' : ''}`);
|
|
300
|
+
} // close any running server and browser
|
|
256
301
|
|
|
257
|
-
requestHeaders = { ...this.config.snapshot.requestHeaders,
|
|
258
|
-
...requestHeaders
|
|
259
|
-
}; // fallback to instance enable JS flag
|
|
260
302
|
|
|
261
|
-
|
|
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
|
|
262
305
|
|
|
263
306
|
let meta = {
|
|
264
|
-
|
|
265
|
-
name
|
|
266
|
-
},
|
|
267
|
-
build: {
|
|
268
|
-
id: this.client.build.id
|
|
269
|
-
}
|
|
307
|
+
build: this.build
|
|
270
308
|
};
|
|
271
309
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
+
}
|
|
275
371
|
|
|
276
|
-
|
|
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
|
|
277
379
|
|
|
278
|
-
_logger.default.debug(`-> url: ${url}`, meta);
|
|
279
380
|
|
|
280
|
-
|
|
381
|
+
options = validateSnapshotOptions(options);
|
|
382
|
+
this.client.addClientInfo(options.clientInfo);
|
|
383
|
+
this.client.addEnvironmentInfo(options.environmentInfo); // return an async generator to allow cancelation
|
|
281
384
|
|
|
282
|
-
|
|
385
|
+
return async function* () {
|
|
386
|
+
let server = 'serve' in options ? await createStaticServer(options).listen() : null;
|
|
283
387
|
|
|
284
|
-
|
|
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
|
|
285
394
|
|
|
286
|
-
_logger.default.debug(`-> requestHeaders: ${JSON.stringify(requestHeaders)}`, meta);
|
|
287
395
|
|
|
288
|
-
|
|
289
|
-
// asynchronously, but perform the above synchronously
|
|
396
|
+
let snapshots = yield gatherSnapshots(this, options);
|
|
290
397
|
|
|
398
|
+
try {
|
|
399
|
+
// yield each task individually to allow canceling
|
|
400
|
+
let tasks = snapshots.map(s => this._takeSnapshot(s));
|
|
291
401
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
295
413
|
|
|
296
|
-
let resources = new Map([[url, (0, _resources.createRootResource)(url, percyDOM)]]); // include the Percy CSS resource if there was one
|
|
297
414
|
|
|
298
|
-
|
|
415
|
+
_cancelSnapshot(snapshot) {
|
|
416
|
+
this.#snapshots.cancel(`snapshot/${snapshot.name}`);
|
|
299
417
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
meta
|
|
307
|
-
}))); // include a log resource for debugging
|
|
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
|
|
308
424
|
|
|
309
|
-
let logs = await _logger.default.query({
|
|
310
|
-
filter: ({
|
|
311
|
-
snapshot: s
|
|
312
|
-
}) => (s === null || s === void 0 ? void 0 : s.name) === name
|
|
313
|
-
});
|
|
314
|
-
resources.set('percy-logs', (0, _resources.createLogResource)(logs)); // log that the snapshot has been taken before uploading it
|
|
315
425
|
|
|
316
|
-
|
|
426
|
+
_takeSnapshot(snapshot) {
|
|
427
|
+
// cancel any existing snapshot with the same name
|
|
428
|
+
this._cancelSnapshot(snapshot);
|
|
317
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);
|
|
318
434
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
329
449
|
|
|
330
|
-
_logger.default.error(error);
|
|
331
|
-
}));
|
|
332
|
-
}).catch(error => {
|
|
333
|
-
_logger.default.error(`Encountered an error taking snapshot: ${name}`, meta);
|
|
334
450
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
451
|
+
_scheduleUpload(name, options) {
|
|
452
|
+
var _this$build4;
|
|
338
453
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
waitFor,
|
|
343
|
-
execute,
|
|
344
|
-
snapshots = [],
|
|
345
|
-
...options
|
|
346
|
-
}) {
|
|
347
|
-
(0, _assert.default)(this.isRunning(), 'Not running');
|
|
348
|
-
(0, _assert.default)(url, `Missing URL for${name ? ` ${name}` : ' snapshots'}`);
|
|
349
|
-
snapshots = name ? [{
|
|
350
|
-
name,
|
|
351
|
-
execute
|
|
352
|
-
}].concat(snapshots) : snapshots;
|
|
353
|
-
(0, _assert.default)(snapshots.length && snapshots.every(s => s.name), `Missing name for ${url}`); // the entire capture process happens within the async capture queue
|
|
354
|
-
|
|
355
|
-
return _classPrivateFieldGet(this, _captures).push(async () => {
|
|
356
|
-
let results = [];
|
|
357
|
-
let page;
|
|
454
|
+
if ((_this$build4 = this.build) !== null && _this$build4 !== void 0 && _this$build4.error) {
|
|
455
|
+
throw new Error(this.build.error);
|
|
456
|
+
}
|
|
358
457
|
|
|
458
|
+
return this.#uploads.push(`upload/${name}`, async () => {
|
|
359
459
|
try {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
await page.setBypassCSP(true); // set any request headers
|
|
364
|
-
|
|
365
|
-
await page.setExtraHTTPHeaders(options.requestHeaders || {}); // @todo - resize viewport
|
|
366
|
-
// go to and wait for network idle
|
|
367
|
-
|
|
368
|
-
await page.goto(url, {
|
|
369
|
-
waitUntil: 'networkidle2'
|
|
370
|
-
}); // inject @percy/dom for serialization
|
|
371
|
-
|
|
372
|
-
await page.addScriptTag({
|
|
373
|
-
path: require.resolve('@percy/dom')
|
|
374
|
-
}); // wait for any other elements or timeout before snapshotting
|
|
375
|
-
|
|
376
|
-
if (waitFor) await page.waitFor(waitFor); // multiple snapshots can be captured on a single page
|
|
377
|
-
|
|
378
|
-
for (let {
|
|
379
|
-
name,
|
|
380
|
-
execute
|
|
381
|
-
} of snapshots) {
|
|
382
|
-
// optionally execute a script to interact with the page
|
|
383
|
-
if (execute) await execute(page); // serialize and capture a DOM snapshot
|
|
384
|
-
|
|
385
|
-
/* istanbul ignore next: no instrumenting injected code */
|
|
386
|
-
|
|
387
|
-
let domSnapshot = await page.evaluate(({
|
|
388
|
-
enableJavaScript
|
|
389
|
-
}) =>
|
|
390
|
-
/* eslint-disable-next-line no-undef */
|
|
391
|
-
PercyDOM.serialize({
|
|
392
|
-
enableJavaScript
|
|
393
|
-
}), options); // snapshots are awaited on concurrently after sequentially capturing their DOM
|
|
394
|
-
|
|
395
|
-
results.push(this.snapshot({ ...options,
|
|
396
|
-
url,
|
|
397
|
-
name,
|
|
398
|
-
domSnapshot
|
|
399
|
-
}));
|
|
400
|
-
}
|
|
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);
|
|
401
463
|
} catch (error) {
|
|
402
|
-
|
|
464
|
+
var _error$response;
|
|
403
465
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
var _page;
|
|
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;
|
|
407
468
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
+
}
|
|
411
479
|
}
|
|
412
480
|
});
|
|
413
481
|
}
|
|
414
482
|
|
|
415
483
|
}
|
|
416
|
-
|
|
417
|
-
exports.default = Percy;
|
|
484
|
+
export default Percy;
|