@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// aliased to src for coverage during tests without needing to compile this file
|
|
2
|
+
import Server from '../../dist/server.js';
|
|
3
|
+
|
|
4
|
+
export function createTestServer({ default: defaultReply, ...replies }, port = 8000) {
|
|
5
|
+
let server = new Server();
|
|
6
|
+
|
|
7
|
+
// alternate route handling
|
|
8
|
+
let handleReply = reply => async (req, res) => {
|
|
9
|
+
let [status, headers, body] = typeof reply === 'function' ? await reply(req) : reply;
|
|
10
|
+
if (!Buffer.isBuffer(body) && typeof body !== 'string') body = JSON.stringify(body);
|
|
11
|
+
return res.send(status, headers, body);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// map replies to alternate route handlers
|
|
15
|
+
server.reply = (p, reply) => (replies[p] = handleReply(reply), null);
|
|
16
|
+
for (let [p, reply] of Object.entries(replies)) server.reply(p, reply);
|
|
17
|
+
if (defaultReply) defaultReply = handleReply(defaultReply);
|
|
18
|
+
|
|
19
|
+
// track requests and route replies
|
|
20
|
+
server.requests = [];
|
|
21
|
+
server.route(async (req, res, next) => {
|
|
22
|
+
let pathname = req.url.pathname;
|
|
23
|
+
if (req.url.search) pathname += req.url.search;
|
|
24
|
+
server.requests.push(req.body ? [pathname, req.body] : [pathname]);
|
|
25
|
+
let reply = replies[req.url.pathname] || defaultReply;
|
|
26
|
+
return reply ? await reply(req, res) : next();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// automatically listen
|
|
30
|
+
return server.listen(port);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default createTestServer;
|
package/types/index.d.ts
CHANGED
|
@@ -1,71 +1,101 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// utility types
|
|
2
|
+
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
|
3
|
+
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
|
3
4
|
|
|
4
5
|
type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'silent';
|
|
5
|
-
interface Pojo { [x: string]: any; }
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
interface Pojo {
|
|
8
|
+
[x: string]: any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface AuthCredentials {
|
|
12
|
+
username: string;
|
|
13
|
+
password: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
interface DiscoveryOptions {
|
|
17
|
+
requestHeaders?: Pojo;
|
|
18
|
+
authorization?: AuthCredentials;
|
|
16
19
|
allowedHostnames?: string[];
|
|
20
|
+
disableCache?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DiscoveryLaunchOptions {
|
|
24
|
+
executable?: string;
|
|
25
|
+
args?: string[];
|
|
26
|
+
timeout?: number;
|
|
27
|
+
headless?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface AllDiscoveryOptions extends DiscoveryOptions {
|
|
17
31
|
networkIdleTimeout?: number;
|
|
18
|
-
disableAssetCache?: boolean;
|
|
19
32
|
concurrency?: number;
|
|
20
|
-
launchOptions?:
|
|
33
|
+
launchOptions?: DiscoveryLaunchOptions;
|
|
21
34
|
}
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
interface CommonSnapshotOptions {
|
|
37
|
+
widths?: number[];
|
|
38
|
+
minHeight?: number;
|
|
39
|
+
percyCSS?: string;
|
|
40
|
+
enableJavaScript?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SnapshotOptions extends CommonSnapshotOptions {
|
|
44
|
+
discovery?: DiscoveryOptions;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ClientEnvInfo = {
|
|
25
48
|
clientInfo?: string,
|
|
26
|
-
environmentInfo?: string
|
|
49
|
+
environmentInfo?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type PercyConfigOptions<C = Pojo> = C & {
|
|
53
|
+
snapshot?: CommonSnapshotOptions,
|
|
54
|
+
discovery?: AllDiscoveryOptions
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type PercyOptions<C = Pojo> = {
|
|
58
|
+
token?: string,
|
|
27
59
|
server?: boolean,
|
|
28
60
|
port?: number,
|
|
29
61
|
concurrency?: number,
|
|
30
62
|
loglevel?: LogLevel,
|
|
31
|
-
config?: undefined | string | false
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
63
|
+
config?: undefined | string | false
|
|
64
|
+
} & ClientEnvInfo & PercyConfigOptions<C>;
|
|
65
|
+
|
|
66
|
+
type SnapshotExec = () => void | Promise<void>;
|
|
35
67
|
|
|
36
|
-
type
|
|
37
|
-
|
|
68
|
+
type AdditionalSnapshot = (XOR<XOR<
|
|
69
|
+
{ name: string },
|
|
70
|
+
{ prefix: string, suffix?: string }>,
|
|
71
|
+
{ suffix: string, prefix?: string }>
|
|
72
|
+
) & { execute: SnapshotExec };
|
|
38
73
|
|
|
39
74
|
declare class Percy {
|
|
40
75
|
static start(options?: PercyOptions): Promise<Percy>;
|
|
41
76
|
constructor(options?: PercyOptions);
|
|
42
77
|
loglevel(): LogLevel;
|
|
43
78
|
loglevel(level: LogLevel): void;
|
|
44
|
-
|
|
79
|
+
config: PercyConfigOptions;
|
|
80
|
+
setConfig(config: ClientEnvInfo & PercyConfigOptions): PercyConfigOptions;
|
|
45
81
|
start(): Promise<void>;
|
|
46
|
-
stop(): Promise<void>;
|
|
82
|
+
stop(force?: boolean): Promise<void>;
|
|
47
83
|
idle(): Promise<void>;
|
|
84
|
+
close(): void;
|
|
48
85
|
|
|
49
|
-
snapshot(options:
|
|
86
|
+
snapshot(options: {
|
|
50
87
|
url: string,
|
|
51
|
-
name
|
|
52
|
-
domSnapshot: string,
|
|
88
|
+
name?: string,
|
|
53
89
|
clientInfo?: string,
|
|
54
90
|
environmentInfo?: string
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
} | {
|
|
64
|
-
url: string,
|
|
65
|
-
snapshots: CaptureSnapshots,
|
|
66
|
-
waitFor?: string | number,
|
|
67
|
-
execute?: CaptureExec
|
|
68
|
-
})): Promise<void>;
|
|
91
|
+
} & XOR<{
|
|
92
|
+
domSnapshot: string
|
|
93
|
+
}, {
|
|
94
|
+
waitForTimeout?: number,
|
|
95
|
+
waitForSelector?: string,
|
|
96
|
+
execute?: SnapshotExec,
|
|
97
|
+
additionalSnapshots?: AdditionalSnapshot[]
|
|
98
|
+
}> & SnapshotOptions): Promise<void>;
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
export default Percy;
|
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/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
|
-
}
|
package/dist/utils/assert.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.default = percyAssert;
|
|
7
|
-
|
|
8
|
-
var _assert = require("assert");
|
|
9
|
-
|
|
10
|
-
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
11
|
-
|
|
12
|
-
// Percy assertions errors contain extra meta data and have specific lookup keys
|
|
13
|
-
// for different computed assertion error messages.
|
|
14
|
-
class PercyAssertionError extends Error {
|
|
15
|
-
constructor(lookup, meta = {}) {
|
|
16
|
-
var _PercyAssertionError$, _PercyAssertionError$2, _PercyAssertionError$3;
|
|
17
|
-
|
|
18
|
-
super((_PercyAssertionError$ = (_PercyAssertionError$2 = (_PercyAssertionError$3 = PercyAssertionError.dict)[lookup]) === null || _PercyAssertionError$2 === void 0 ? void 0 : _PercyAssertionError$2.call(_PercyAssertionError$3, meta)) !== null && _PercyAssertionError$ !== void 0 ? _PercyAssertionError$ : lookup);
|
|
19
|
-
this.name = this.constructor.name;
|
|
20
|
-
this.meta = meta;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
toString() {
|
|
24
|
-
return this.message;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
} // Wraps native assert to throw a Percy assertion error with optional meta.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
_defineProperty(PercyAssertionError, "dict", {
|
|
31
|
-
'disallowed status': ({
|
|
32
|
-
status
|
|
33
|
-
}) => `Disallowed response status [${status}]`,
|
|
34
|
-
'is empty': () => 'Empty response',
|
|
35
|
-
'is remote': () => 'Remote resource',
|
|
36
|
-
'no response': () => 'No response',
|
|
37
|
-
'too many bytes': ({
|
|
38
|
-
size
|
|
39
|
-
}) => `Max file size exceeded [${size}]`,
|
|
40
|
-
'too many redirects': ({
|
|
41
|
-
length
|
|
42
|
-
}) => `Too many redirects [${length}]`,
|
|
43
|
-
'too many widths': ({
|
|
44
|
-
widths
|
|
45
|
-
}) => `Too many widths requested: maximum is 10, requested ${widths}`
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
function percyAssert(condition, lookup, meta) {
|
|
49
|
-
(0, _assert.strict)(condition, new PercyAssertionError(lookup, meta));
|
|
50
|
-
}
|
package/dist/utils/bytes.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.default = readableBytes;
|
|
7
|
-
|
|
8
|
-
// Converts a raw byte integer into a human readable string.
|
|
9
|
-
function readableBytes(bytes) {
|
|
10
|
-
let units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
11
|
-
let thresh = 1024;
|
|
12
|
-
let u = -1;
|
|
13
|
-
|
|
14
|
-
if (Math.abs(bytes) < thresh) {
|
|
15
|
-
return `${bytes}B`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
while (Math.abs(bytes) >= thresh && u < units.length - 1) {
|
|
19
|
-
bytes /= thresh;
|
|
20
|
-
++u;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return `${bytes.toFixed(1)}${units[u]}`;
|
|
24
|
-
}
|
package/dist/utils/idle.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.default = idle;
|
|
7
|
-
|
|
8
|
-
// Returns a promise that resolves when the `count` function returns `0`. If
|
|
9
|
-
// `count` returns non-zero, will check again after `timeout`. The `count`
|
|
10
|
-
// function must eventually return `0` or the promise will never resolve.
|
|
11
|
-
function idle(count, timeout = 10) {
|
|
12
|
-
return new Promise(resolve => function check() {
|
|
13
|
-
if (count() === 0) resolve();else setTimeout(check, timeout);
|
|
14
|
-
}());
|
|
15
|
-
}
|