@percy/core 1.0.0-beta.9 → 1.0.0

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.
@@ -1,367 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = void 0;
7
-
8
- var _nodeFetch = _interopRequireDefault(require("node-fetch"));
9
-
10
- var _puppeteerCore = _interopRequireDefault(require("puppeteer-core"));
11
-
12
- var _logger = _interopRequireDefault(require("@percy/logger"));
13
-
14
- var _queue2 = _interopRequireDefault(require("./queue"));
15
-
16
- var _assert = _interopRequireDefault(require("./utils/assert"));
17
-
18
- var _idle = _interopRequireDefault(require("./utils/idle"));
19
-
20
- var _installBrowser = _interopRequireDefault(require("./utils/install-browser"));
21
-
22
- var _resources = require("./utils/resources");
23
-
24
- var _url = require("./utils/url");
25
-
26
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
27
-
28
- 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; }
29
-
30
- 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; }
31
-
32
- const REDIRECT_STATUSES = [301, 302, 304, 307, 308];
33
- const ALLOWED_STATUSES = [200, 201, ...REDIRECT_STATUSES]; // A PercyDiscoverer instance connects to a puppeteer browser and concurrently
34
- // discovers resources for snapshots. Resources are only captured from the
35
- // snapshot's root URL by default unless additional allowed hostnames are
36
- // defined. Captured resources are cached so future requests resolve much
37
- // quicker and snapshots can share cached resources.
38
-
39
- var _queue = new WeakMap();
40
-
41
- var _browser = new WeakMap();
42
-
43
- var _cache = new WeakMap();
44
-
45
- class PercyDiscoverer {
46
- constructor({
47
- // asset discovery concurrency
48
- concurrency,
49
- // additional allowed hostnames besides the root URL hostname
50
- allowedHostnames,
51
- // how long to wait before the network is considered to be idle and assets
52
- // are determined to be fully discovered
53
- networkIdleTimeout,
54
- // disable resource caching, the cache is still used but overwritten for each resource
55
- disableAssetCache,
56
- // browser launch options
57
- launchOptions
58
- }) {
59
- _queue.set(this, {
60
- writable: true,
61
- value: null
62
- });
63
-
64
- _browser.set(this, {
65
- writable: true,
66
- value: null
67
- });
68
-
69
- _cache.set(this, {
70
- writable: true,
71
- value: new Map()
72
- });
73
-
74
- _classPrivateFieldSet(this, _queue, new _queue2.default(concurrency));
75
-
76
- Object.assign(this, {
77
- allowedHostnames,
78
- networkIdleTimeout,
79
- disableAssetCache,
80
- launchOptions
81
- });
82
- } // Returns true or false when the browser is connected.
83
-
84
-
85
- isConnected() {
86
- var _classPrivateFieldGet2;
87
-
88
- return !!((_classPrivateFieldGet2 = _classPrivateFieldGet(this, _browser)) === null || _classPrivateFieldGet2 === void 0 ? void 0 : _classPrivateFieldGet2.isConnected());
89
- } // Installs the browser executable if necessary and launches a Puppeteer
90
- // browser instance for use during asset discovery.
91
-
92
-
93
- async launch() {
94
- var _this$launchOptions, _this$launchOptions2;
95
-
96
- if (this.isConnected()) return;
97
- let executablePath = await (0, _installBrowser.default)((_this$launchOptions = this.launchOptions) === null || _this$launchOptions === void 0 ? void 0 : _this$launchOptions.executablePath);
98
-
99
- _classPrivateFieldSet(this, _browser, await _puppeteerCore.default.launch({ ...this.launchOptions,
100
- ignoreHTTPSErrors: true,
101
- handleSIGINT: false,
102
- handleSIGTERM: false,
103
- handleSIGHUP: false,
104
- executablePath,
105
- args: ['--no-sandbox', '--disable-web-security', ...(((_this$launchOptions2 = this.launchOptions) === null || _this$launchOptions2 === void 0 ? void 0 : _this$launchOptions2.args) || [])]
106
- }));
107
- } // Clears any unstarted discovery tasks and closes the browser.
108
-
109
-
110
- async close() {
111
- var _classPrivateFieldGet3;
112
-
113
- _classPrivateFieldGet(this, _queue).clear();
114
-
115
- await ((_classPrivateFieldGet3 = _classPrivateFieldGet(this, _browser)) === null || _classPrivateFieldGet3 === void 0 ? void 0 : _classPrivateFieldGet3.close());
116
- } // Returns a new browser page.
117
-
118
-
119
- async page() {
120
- var _classPrivateFieldGet4;
121
-
122
- return (_classPrivateFieldGet4 = _classPrivateFieldGet(this, _browser)) === null || _classPrivateFieldGet4 === void 0 ? void 0 : _classPrivateFieldGet4.newPage();
123
- } // Gathers resources for a root URL and DOM. The accumulator should be a Map
124
- // and will be populated with resources by URL. Resolves when asset discovery
125
- // finishes, although shouldn't be awaited on as discovery happens concurrently.
126
-
127
-
128
- gatherResources(accumulator, {
129
- rootUrl,
130
- rootDom,
131
- enableJavaScript,
132
- requestHeaders,
133
- width,
134
- meta
135
- }) {
136
- (0, _assert.default)(this.isConnected(), 'Browser not connected'); // discover assets concurrently
137
-
138
- return _classPrivateFieldGet(this, _queue).push(async () => {
139
- _logger.default.debug(`Discovering resources @${width}px for ${rootUrl}`, {
140
- url: rootUrl,
141
- ...meta
142
- }); // get a fresh page
143
-
144
-
145
- let page = await this.page(); // track processing network requests
146
-
147
- let processing = 0; // set page options
148
-
149
- await page.setRequestInterception(true);
150
- await page.setJavaScriptEnabled(enableJavaScript);
151
- await page.setViewport({ ...page.viewport(),
152
- width
153
- });
154
- await page.setExtraHTTPHeaders(requestHeaders); // add and configure request listeners
155
-
156
- page.on('request', this._handleRequest({
157
- onRequest: () => processing++,
158
- rootUrl,
159
- rootDom,
160
- meta
161
- })).on('requestfinished', this._handleRequestFinished({
162
- onFinished: () => processing--,
163
- accumulator,
164
- rootUrl,
165
- meta
166
- })).on('requestfailed', this._handleRequestFailed({
167
- onFailed: () => processing--,
168
- meta
169
- }));
170
-
171
- try {
172
- // navigate to the root URL and wait for the network to idle
173
- await page.goto(rootUrl);
174
- await (0, _idle.default)(() => processing, this.networkIdleTimeout);
175
- } finally {
176
- // cleanup
177
- page.removeAllListeners('request');
178
- page.removeAllListeners('requestfailed');
179
- page.removeAllListeners('requestfinished');
180
- await page.close();
181
- }
182
- });
183
- } // Creates a request handler for the specific root URL and DOM. The handler
184
- // will serve the root DOM for the root URL, respond with possible cached
185
- // responses, skip resources that should not be captured, and abort requests
186
- // that result in an error.
187
-
188
-
189
- _handleRequest({
190
- rootUrl,
191
- rootDom,
192
- onRequest,
193
- meta
194
- }) {
195
- let allowedHostnames = [(0, _url.hostname)(rootUrl)].concat(this.allowedHostnames);
196
- return request => {
197
- let url = request.url();
198
- onRequest(); // skip any logging and handling of data-urls
199
-
200
- if (url.startsWith('data:')) {
201
- return request.continue();
202
- }
203
-
204
- meta = { ...meta,
205
- url
206
- };
207
-
208
- _logger.default.debug(`Handling request for ${url}`, meta);
209
-
210
- try {
211
- if (url === rootUrl) {
212
- // root resource
213
- _logger.default.debug(`Serving root resource for ${url}`, meta);
214
-
215
- request.respond({
216
- status: 200,
217
- body: rootDom,
218
- contentType: 'text/html'
219
- });
220
- } else if (!this.disableAssetCache && _classPrivateFieldGet(this, _cache).has(url)) {
221
- // respond with cached response
222
- _logger.default.debug(`Response cache hit for ${url}`, meta);
223
-
224
- request.respond(_classPrivateFieldGet(this, _cache).get(url).response);
225
- } else {
226
- // do not resolve resources that should not be captured
227
- (0, _assert.default)(allowedHostnames.some(h => (0, _url.domainMatch)(h, url)), 'is remote', meta); // continue the request
228
-
229
- request.continue();
230
- }
231
- } catch (error) {
232
- if (error.name === 'PercyAssertionError') {
233
- _logger.default.debug(`Skipping - ${error.toString()}`, error.meta);
234
- } else {
235
- _logger.default.error(`Encountered an error for ${url}`, meta);
236
-
237
- _logger.default.error(error);
238
- } // request hangs without aborting on error
239
-
240
-
241
- request.abort();
242
- }
243
- };
244
- } // Creates a request finished handler for a specific root URL that will add
245
- // resolved resources to an accumulator. Both the response and resource are
246
- // cached for future snapshots and requests.
247
-
248
-
249
- _handleRequestFinished({
250
- rootUrl,
251
- accumulator,
252
- onFinished,
253
- meta
254
- }) {
255
- return async request => {
256
- let url = (0, _url.normalizeURL)(request.url());
257
- meta = { ...meta,
258
- url
259
- };
260
-
261
- try {
262
- // do nothing for the root URL or URLs that start with `data:`
263
- if (url === rootUrl || url.startsWith('data:')) return; // process and cache the response and resource
264
-
265
- if (this.disableAssetCache || !_classPrivateFieldGet(this, _cache).has(url)) {
266
- _logger.default.debug(`Processing resource - ${url}`, meta);
267
-
268
- let response = await this._parseRequestResponse(url, request, meta);
269
- let mimetype = response.headers['content-type'][0].split(';')[0];
270
- let resource = (0, _resources.createLocalResource)(url, response.body, mimetype, () => {
271
- _logger.default.debug(`Making local copy of response - ${url}`, meta);
272
- });
273
-
274
- _logger.default.debug(`-> url: ${url}`, meta);
275
-
276
- _logger.default.debug(`-> sha: ${resource.sha}`, meta);
277
-
278
- _logger.default.debug(`-> filepath: ${resource.filepath}`, meta);
279
-
280
- _logger.default.debug(`-> mimetype: ${resource.mimetype}`, meta);
281
-
282
- _classPrivateFieldGet(this, _cache).set(url, {
283
- response,
284
- resource
285
- });
286
- } // add the resource to the accumulator
287
-
288
-
289
- accumulator.set(url, _classPrivateFieldGet(this, _cache).get(url).resource);
290
- } catch (error) {
291
- if (error.name === 'PercyAssertionError') {
292
- _logger.default.debug(`Skipping - ${error.toString()}`, error.meta);
293
- } else {
294
- _logger.default.error(`Encountered an error for ${url}`, meta);
295
-
296
- _logger.default.error(error);
297
- }
298
- } finally {
299
- onFinished();
300
- }
301
- };
302
- } // Creates a failed request handler that logs non-generic failure reasons.
303
-
304
-
305
- _handleRequestFailed({
306
- onFailed,
307
- meta
308
- }) {
309
- return req => {
310
- let error = req.failure().errorText; // do not log generic failures since the real error was most likely
311
- // already logged from elsewhere
312
-
313
- if (error !== 'net::ERR_FAILED') {
314
- _logger.default.debug(`Request failed for ${req.url()} - ${error}`, meta);
315
- }
316
-
317
- onFailed();
318
- };
319
- } // Parses a request's response to find the status, headers, and body. Performs
320
- // various response assertions and follows redirect requests using node-fetch.
321
-
322
-
323
- async _parseRequestResponse(url, request, meta) {
324
- let headers, body;
325
- let response = request.response();
326
- (0, _assert.default)(response, 'no response', meta);
327
- let status = response.status();
328
- (0, _assert.default)(ALLOWED_STATUSES.includes(status), 'disallowed status', {
329
- status,
330
- ...meta
331
- });
332
-
333
- if (REDIRECT_STATUSES.includes(status)) {
334
- // fetch's default max redirect length is 20
335
- let length = request.redirectChain().length;
336
- (0, _assert.default)(length <= 20, 'too many redirects', {
337
- length,
338
- ...meta
339
- });
340
- let redirect = await (0, _nodeFetch.default)(response.url(), {
341
- responseType: 'arraybuffer',
342
- headers: request.headers()
343
- });
344
- headers = redirect.headers.raw();
345
- body = await redirect.buffer();
346
- } else {
347
- // CDP returns multiple headers joined by newlines, however
348
- // `request.respond` (used for cached responses) will hang if there are
349
- // newlines in headers. The following reduction normalizes header values
350
- // as arrays split on newlines
351
- headers = Object.entries(response.headers()).reduce((norm, [key, value]) => Object.assign(norm, {
352
- [key]: value.split('\n')
353
- }), {});
354
- body = await response.buffer();
355
- }
356
-
357
- (0, _assert.default)(body.toString(), 'is empty', meta);
358
- return {
359
- status,
360
- headers,
361
- body
362
- };
363
- }
364
-
365
- }
366
-
367
- exports.default = PercyDiscoverer;
package/dist/index.js DELETED
@@ -1,29 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- var _exportNames = {};
7
- Object.defineProperty(exports, "default", {
8
- enumerable: true,
9
- get: function () {
10
- return _percy.default;
11
- }
12
- });
13
-
14
- var _percy = _interopRequireDefault(require("./percy"));
15
-
16
- var _resources = require("./utils/resources");
17
-
18
- Object.keys(_resources).forEach(function (key) {
19
- if (key === "default" || key === "__esModule") return;
20
- if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
21
- Object.defineProperty(exports, key, {
22
- enumerable: true,
23
- get: function () {
24
- return _resources[key];
25
- }
26
- });
27
- });
28
-
29
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
package/dist/percy-css.js DELETED
@@ -1,33 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = injectPercyCSS;
7
-
8
- var _logger = _interopRequireDefault(require("@percy/logger"));
9
-
10
- var _resources = require("./utils/resources");
11
-
12
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
-
14
- // Creates a local Percy CSS resource and injects a Percy CSS link into the
15
- // provided DOM string. Returns both the new DOM string and local resource
16
- // object. If no Percy CSS is provided the return value will be the original DOM
17
- // string and the function will do nothing.
18
- function injectPercyCSS(rootUrl, originalDOM, percyCSS, meta) {
19
- if (!percyCSS) return [originalDOM];
20
- let filename = `percy-specific.${Date.now()}.css`;
21
-
22
- _logger.default.debug('Handling percy-specific css:', meta);
23
-
24
- _logger.default.debug(`-> filename: ${filename}`, meta);
25
-
26
- _logger.default.debug(`-> content: ${percyCSS}`, meta);
27
-
28
- let url = `${rootUrl}/${filename}`;
29
- let resource = (0, _resources.createLocalResource)(url, percyCSS, 'text/css', null, meta);
30
- let link = `<link data-percy-specific-css rel="stylesheet" href="/${filename}"/>`;
31
- let dom = originalDOM.replace(/(<\/body>)(?!.*\1)/is, link + '$&');
32
- return [dom, resource];
33
- }