@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.
- package/README.md +226 -67
- package/package.json +43 -29
- package/dist/config.js +0 -69
- package/dist/discoverer.js +0 -367
- package/dist/index.js +0 -29
- package/dist/percy-css.js +0 -33
- package/dist/percy.js +0 -428
- package/dist/queue.js +0 -103
- package/dist/server.js +0 -82
- package/dist/utils/assert.js +0 -50
- package/dist/utils/bytes.js +0 -24
- package/dist/utils/idle.js +0 -20
- package/dist/utils/install-browser.js +0 -76
- package/dist/utils/resources.js +0 -75
- package/dist/utils/url.js +0 -64
- package/types/index.d.ts +0 -71
package/dist/discoverer.js
DELETED
|
@@ -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
|
-
}
|