@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/server.js
CHANGED
|
@@ -1,82 +1,430 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
function _classPrivateMethodInitSpec(obj, privateSet) { _checkPrivateRedeclaration(obj, privateSet); privateSet.add(obj); }
|
|
2
|
+
|
|
3
|
+
function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
|
|
4
|
+
|
|
5
|
+
function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }
|
|
6
|
+
|
|
7
|
+
function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
|
|
8
|
+
|
|
9
|
+
function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
|
|
10
|
+
|
|
11
|
+
function _classPrivateMethodGet(receiver, privateSet, fn) { if (!privateSet.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return fn; }
|
|
12
|
+
|
|
13
|
+
function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; }
|
|
14
|
+
|
|
15
|
+
function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); }
|
|
16
|
+
|
|
17
|
+
function _classApplyDescriptorSet(receiver, descriptor, value) { 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; } }
|
|
18
|
+
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import http from 'http';
|
|
22
|
+
import { WebSocketServer } from 'ws';
|
|
23
|
+
import mime from 'mime-types';
|
|
24
|
+
import disposition from 'content-disposition';
|
|
25
|
+
import { pathToRegexp, match as pathToMatch, compile as makeToPath } from 'path-to-regexp'; // custom incoming message adds a `url` and `body` properties containing the parsed URL and message
|
|
26
|
+
// buffer respectively; both available after the 'end' event is emitted
|
|
27
|
+
|
|
28
|
+
export class IncomingMessage extends http.IncomingMessage {
|
|
29
|
+
constructor(socket) {
|
|
30
|
+
let buffer = [];
|
|
31
|
+
super(socket).on('data', d => buffer.push(d)).on('end', () => {
|
|
32
|
+
var _this$headers$content;
|
|
33
|
+
|
|
34
|
+
this.url = new URL(this.url, `http://${this.headers.host}`);
|
|
35
|
+
if (buffer.length) this.body = Buffer.concat(buffer);
|
|
36
|
+
|
|
37
|
+
if (this.body && (_this$headers$content = this.headers['content-type']) !== null && _this$headers$content !== void 0 && _this$headers$content.includes('json')) {
|
|
38
|
+
try {
|
|
39
|
+
this.body = JSON.parse(this.body);
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
} // custom server response adds additional convenience methods
|
|
46
|
+
|
|
47
|
+
export class ServerResponse extends http.ServerResponse {
|
|
48
|
+
// responds with a status, headers, and body; the second argument can be an content-type string,
|
|
49
|
+
// or a headers object, with content-length being automatically set when a `body` is provided
|
|
50
|
+
send(status, headers, body) {
|
|
51
|
+
if (typeof headers === 'string') {
|
|
52
|
+
this.setHeader('Content-Type', headers);
|
|
53
|
+
headers = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (body != null && !this.hasHeader('Content-Length')) {
|
|
57
|
+
this.setHeader('Content-Length', Buffer.byteLength(body));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return this.writeHead(status, headers).end(body);
|
|
61
|
+
} // responds with a status and content with a plain/text content-type
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
text(status, content) {
|
|
65
|
+
if (arguments.length < 2) [status, content] = [200, status];
|
|
66
|
+
return this.send(status, 'text/plain', content.toString());
|
|
67
|
+
} // responds with a status and stringified `data` with a json content-type
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
json(status, data) {
|
|
71
|
+
if (arguments.length < 2) [status, data] = [200, status];
|
|
72
|
+
return this.send(status, 'application/json', JSON.stringify(data));
|
|
73
|
+
} // responds with a status and streams a file with appropriate headers
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
file(status, filepath) {
|
|
77
|
+
if (arguments.length < 2) [status, filepath] = [200, status];
|
|
78
|
+
filepath = path.resolve(filepath);
|
|
79
|
+
let {
|
|
80
|
+
size
|
|
81
|
+
} = fs.lstatSync(filepath);
|
|
82
|
+
let range = parseByteRange(this.req.headers.range, size); // support simple range requests
|
|
83
|
+
|
|
84
|
+
if (this.req.headers.range) {
|
|
85
|
+
let byteRange = range ? `${range.start}-${range.end}` : '*';
|
|
86
|
+
this.setHeader('Content-Range', `bytes ${byteRange}/${size}`);
|
|
87
|
+
if (!range) return this.send(416);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.writeHead(range ? 206 : status, {
|
|
91
|
+
'Accept-Ranges': 'bytes',
|
|
92
|
+
'Content-Type': mime.contentType(path.extname(filepath)),
|
|
93
|
+
'Content-Length': range ? range.end - range.start + 1 : size,
|
|
94
|
+
'Content-Disposition': disposition(filepath, {
|
|
95
|
+
type: 'inline'
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
fs.createReadStream(filepath, range).pipe(this);
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
} // custom server error with a status and default reason
|
|
103
|
+
|
|
104
|
+
export class ServerError extends Error {
|
|
105
|
+
static throw(status, reason) {
|
|
106
|
+
throw new this(status, reason);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
constructor(status = 500, reason) {
|
|
110
|
+
super(reason || http.STATUS_CODES[status]);
|
|
111
|
+
this.status = status;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
} // custom server class handles routing requests and provides alternate methods and properties
|
|
115
|
+
|
|
116
|
+
var _sockets = /*#__PURE__*/new WeakMap();
|
|
117
|
+
|
|
118
|
+
var _defaultPort = /*#__PURE__*/new WeakMap();
|
|
119
|
+
|
|
120
|
+
var _up = /*#__PURE__*/new WeakMap();
|
|
121
|
+
|
|
122
|
+
var _handleUpgrade = /*#__PURE__*/new WeakSet();
|
|
123
|
+
|
|
124
|
+
var _routes = /*#__PURE__*/new WeakMap();
|
|
125
|
+
|
|
126
|
+
var _route = /*#__PURE__*/new WeakSet();
|
|
127
|
+
|
|
128
|
+
var _handleRequest = /*#__PURE__*/new WeakSet();
|
|
129
|
+
|
|
130
|
+
export class Server extends http.Server {
|
|
131
|
+
constructor({
|
|
132
|
+
port
|
|
133
|
+
} = {}) {
|
|
134
|
+
super({
|
|
135
|
+
IncomingMessage,
|
|
136
|
+
ServerResponse
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
_classPrivateMethodInitSpec(this, _handleRequest);
|
|
140
|
+
|
|
141
|
+
_classPrivateMethodInitSpec(this, _route);
|
|
142
|
+
|
|
143
|
+
_classPrivateMethodInitSpec(this, _handleUpgrade);
|
|
144
|
+
|
|
145
|
+
_classPrivateFieldInitSpec(this, _sockets, {
|
|
146
|
+
writable: true,
|
|
147
|
+
value: new Set()
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
_classPrivateFieldInitSpec(this, _defaultPort, {
|
|
151
|
+
writable: true,
|
|
152
|
+
value: void 0
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
_classPrivateFieldInitSpec(this, _up, {
|
|
156
|
+
writable: true,
|
|
157
|
+
value: []
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
_classPrivateFieldInitSpec(this, _routes, {
|
|
161
|
+
writable: true,
|
|
162
|
+
value: [{
|
|
163
|
+
priority: -1,
|
|
164
|
+
handle: (req, res, next) => {
|
|
165
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
166
|
+
|
|
167
|
+
if (req.method === 'OPTIONS') {
|
|
168
|
+
let allowHeaders = req.headers['access-control-request-headers'] || '*';
|
|
169
|
+
let allowMethods = [...new Set(_classPrivateFieldGet(this, _routes).flatMap(route => (!route.match || route.match(req.url.pathname)) && route.methods || []))].join(', ');
|
|
170
|
+
res.setHeader('Access-Control-Allow-Headers', allowHeaders);
|
|
171
|
+
res.setHeader('Access-Control-Allow-Methods', allowMethods);
|
|
172
|
+
res.writeHead(204).end();
|
|
173
|
+
} else {
|
|
174
|
+
res.setHeader('Access-Control-Expose-Headers', '*');
|
|
175
|
+
return next();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}, {
|
|
179
|
+
priority: 3,
|
|
180
|
+
handle: req => ServerError.throw(404)
|
|
181
|
+
}]
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
_classPrivateFieldSet(this, _defaultPort, port); // handle requests on end
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
this.on('request', (req, res) => {
|
|
188
|
+
req.on('end', () => _classPrivateMethodGet(this, _handleRequest, _handleRequest2).call(this, req, res));
|
|
189
|
+
}); // handle websocket upgrades
|
|
190
|
+
|
|
191
|
+
this.on('upgrade', (req, sock, head) => {
|
|
192
|
+
_classPrivateMethodGet(this, _handleUpgrade, _handleUpgrade2).call(this, req, sock, head);
|
|
193
|
+
}); // track open connections to terminate when the server closes
|
|
194
|
+
|
|
195
|
+
this.on('connection', socket => {
|
|
196
|
+
let handleClose = () => _classPrivateFieldGet(this, _sockets).delete(socket);
|
|
197
|
+
|
|
198
|
+
_classPrivateFieldGet(this, _sockets).add(socket.on('close', handleClose));
|
|
35
199
|
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
200
|
+
} // return the listening port or any default port
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
get port() {
|
|
204
|
+
var _super$address;
|
|
205
|
+
|
|
206
|
+
return ((_super$address = super.address()) === null || _super$address === void 0 ? void 0 : _super$address.port) ?? _classPrivateFieldGet(this, _defaultPort);
|
|
207
|
+
} // return a string representation of the server address
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
address() {
|
|
211
|
+
let port = this.port;
|
|
212
|
+
let host = 'http://localhost';
|
|
213
|
+
return port ? `${host}:${port}` : host;
|
|
214
|
+
} // return a promise that resolves when the server is listening
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
listen(port = _classPrivateFieldGet(this, _defaultPort)) {
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
let handle = err => off() && err ? reject(err) : resolve(this);
|
|
220
|
+
|
|
221
|
+
let off = () => this.off('error', handle).off('listening', handle);
|
|
222
|
+
|
|
223
|
+
super.listen(port, handle).once('error', handle);
|
|
41
224
|
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
225
|
+
} // return a promise that resolves when the server closes
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
close() {
|
|
229
|
+
return new Promise(resolve => {
|
|
230
|
+
_classPrivateFieldGet(this, _sockets).forEach(socket => socket.destroy());
|
|
231
|
+
|
|
232
|
+
super.close(resolve);
|
|
50
233
|
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
234
|
+
} // handle websocket upgrades
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
websocket(pathname, handle) {
|
|
238
|
+
if (!handle) [pathname, handle] = [null, pathname];
|
|
239
|
+
|
|
240
|
+
_classPrivateFieldGet(this, _up).push({
|
|
241
|
+
match: pathname && pathToMatch(pathname),
|
|
242
|
+
handle: (req, sock, head) => new Promise(resolve => {
|
|
243
|
+
let wss = new WebSocketServer({
|
|
244
|
+
noServer: true,
|
|
245
|
+
clientTracking: false
|
|
246
|
+
});
|
|
247
|
+
wss.handleUpgrade(req, sock, head, resolve);
|
|
248
|
+
}).then(ws => handle(ws, req))
|
|
56
249
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
250
|
+
|
|
251
|
+
if (pathname) {
|
|
252
|
+
_classPrivateFieldGet(this, _up).sort((a, b) => (a.match ? -1 : 1) - (b.match ? -1 : 1));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// set request routing and handling for pathnames and methods
|
|
259
|
+
route(method, pathname, handle) {
|
|
260
|
+
if (arguments.length === 1) [handle, method] = [method];
|
|
261
|
+
if (arguments.length === 2) [handle, pathname] = [pathname];
|
|
262
|
+
if (arguments.length === 2 && !Array.isArray(method) && method[0] === '/') [pathname, method] = [method];
|
|
263
|
+
return _classPrivateMethodGet(this, _route, _route2).call(this, {
|
|
264
|
+
priority: !pathname ? 0 : !method ? 1 : 2,
|
|
265
|
+
methods: method && [].concat(method).map(m => m.toUpperCase()),
|
|
266
|
+
match: pathname && pathToMatch(pathname),
|
|
267
|
+
handle
|
|
268
|
+
});
|
|
269
|
+
} // install a route that serves requested files from the provided directory
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
serve(pathname, directory, options) {
|
|
273
|
+
var _options;
|
|
274
|
+
|
|
275
|
+
if (typeof directory !== 'string') [options, directory] = [directory];
|
|
276
|
+
if (!directory) [pathname, directory] = ['/', pathname];
|
|
277
|
+
let root = path.resolve(directory);
|
|
278
|
+
if (!fs.existsSync(root)) throw new Error(`Not found: ${directory}`);
|
|
279
|
+
let mountPattern = pathToRegexp(pathname, null, {
|
|
280
|
+
end: false
|
|
62
281
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
282
|
+
let rewritePath = createRewriter((_options = options) === null || _options === void 0 ? void 0 : _options.rewrites, (pathname, rewrite) => {
|
|
283
|
+
try {
|
|
284
|
+
let filepath = decodeURIComponent(pathname.replace(mountPattern, ''));
|
|
285
|
+
if (!isPathInside(root, filepath)) ServerError.throw();
|
|
286
|
+
return rewrite(filepath);
|
|
287
|
+
} catch {
|
|
288
|
+
throw new ServerError(400);
|
|
289
|
+
}
|
|
70
290
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
291
|
+
return _classPrivateMethodGet(this, _route, _route2).call(this, {
|
|
292
|
+
priority: 2,
|
|
293
|
+
methods: ['GET'],
|
|
294
|
+
match: pathname => mountPattern.test(pathname),
|
|
295
|
+
handle: async (req, res, next) => {
|
|
296
|
+
try {
|
|
297
|
+
var _options2;
|
|
298
|
+
|
|
299
|
+
let pathname = rewritePath(req.url.pathname);
|
|
300
|
+
let file = await getFile(root, pathname, (_options2 = options) === null || _options2 === void 0 ? void 0 : _options2.cleanUrls);
|
|
301
|
+
if (!(file !== null && file !== void 0 && file.stats.isFile())) return await next();
|
|
302
|
+
return res.file(file.path);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
let statusPage = path.join(root, `${err.status}.html`);
|
|
305
|
+
if (!fs.existsSync(statusPage)) throw err;
|
|
306
|
+
return res.file(err.status, statusPage);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
} // route and respond to requests; handling errors if necessary
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
} // create a url rewriter from provided rewrite rules
|
|
314
|
+
|
|
315
|
+
function _handleUpgrade2(req, sock, head) {
|
|
316
|
+
let up = _classPrivateFieldGet(this, _up).find(u => !u.match || u.match(req.url));
|
|
317
|
+
|
|
318
|
+
if (up) return up.handle(req, sock, head);
|
|
319
|
+
sock.write(`HTTP/1.1 400 ${http.STATUS_CODES[400]}\r\n` + 'Connection: close\r\n\r\n');
|
|
320
|
+
sock.destroy();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function _route2(route) {
|
|
324
|
+
let i = _classPrivateFieldGet(this, _routes).findIndex(r => r.priority >= route.priority);
|
|
325
|
+
|
|
326
|
+
_classPrivateFieldGet(this, _routes).splice(i, 0, route);
|
|
327
|
+
|
|
328
|
+
return this;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function _handleRequest2(req, res) {
|
|
332
|
+
// support node < 15.7.0
|
|
333
|
+
res.req ?? (res.req = req);
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// invoke routes like middleware
|
|
337
|
+
await async function cont(routes, i = 0) {
|
|
338
|
+
let next = () => cont(routes, i + 1);
|
|
339
|
+
|
|
340
|
+
let {
|
|
341
|
+
methods,
|
|
342
|
+
match,
|
|
343
|
+
handle
|
|
344
|
+
} = routes[i];
|
|
345
|
+
let result = !methods || methods.includes(req.method);
|
|
346
|
+
result && (result = !match || match(req.url.pathname));
|
|
347
|
+
if (result) req.params = result.params;
|
|
348
|
+
return result ? handle(req, res, next) : next();
|
|
349
|
+
}(_classPrivateFieldGet(this, _routes));
|
|
350
|
+
} catch (error) {
|
|
351
|
+
var _req$headers$accept, _req$headers$content;
|
|
352
|
+
|
|
353
|
+
let {
|
|
354
|
+
status = 500,
|
|
355
|
+
message
|
|
356
|
+
} = error; // fallback error handling
|
|
357
|
+
|
|
358
|
+
if ((_req$headers$accept = req.headers.accept) !== null && _req$headers$accept !== void 0 && _req$headers$accept.includes('json') || (_req$headers$content = req.headers['content-type']) !== null && _req$headers$content !== void 0 && _req$headers$content.includes('json')) {
|
|
359
|
+
res.json(status, {
|
|
360
|
+
error: message
|
|
361
|
+
});
|
|
362
|
+
} else {
|
|
363
|
+
res.text(status, message);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function createRewriter(rewrites = [], cb) {
|
|
369
|
+
let normalize = p => path.posix.normalize(path.posix.join('/', p));
|
|
370
|
+
|
|
371
|
+
if (!Array.isArray(rewrites)) rewrites = Object.entries(rewrites);
|
|
372
|
+
let rewrite = [{
|
|
373
|
+
// resolve and normalize the path before rewriting
|
|
374
|
+
apply: p => path.posix.resolve(normalize(p))
|
|
375
|
+
}].concat(rewrites.map(([src, dest]) => {
|
|
376
|
+
// compile rewrite rules into functions
|
|
377
|
+
let match = pathToMatch(normalize(src));
|
|
378
|
+
let toPath = makeToPath(normalize(dest));
|
|
379
|
+
return {
|
|
380
|
+
match,
|
|
381
|
+
apply: r => toPath(r.params)
|
|
382
|
+
};
|
|
383
|
+
})).reduceRight((next, rule) => pathname => {
|
|
384
|
+
var _rule$match;
|
|
385
|
+
|
|
386
|
+
// compose all rewrites into a single function
|
|
387
|
+
let result = ((_rule$match = rule.match) === null || _rule$match === void 0 ? void 0 : _rule$match.call(rule, pathname)) ?? pathname;
|
|
388
|
+
if (result) pathname = rule.apply(result);
|
|
389
|
+
return next(pathname);
|
|
390
|
+
}, p => p); // allow additional pathname processing around the rewriter
|
|
391
|
+
|
|
392
|
+
return p => cb(p, rewrite);
|
|
393
|
+
} // returns true if the pathname is inside the root pathname
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
function isPathInside(root, pathname) {
|
|
397
|
+
let abs = path.resolve(path.join(root, pathname));
|
|
398
|
+
return !abs.lastIndexOf(root, 0) && (abs[root.length] === path.sep || !abs[root.length]);
|
|
399
|
+
} // get the absolute path and stats of a possible file
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
async function getFile(root, pathname, cleanUrls) {
|
|
403
|
+
for (let filename of [pathname].concat(cleanUrls ? path.join(pathname, 'index.html') : [], cleanUrls && pathname.length > 2 ? pathname.replace(/\/?$/, '.html') : [])) {
|
|
404
|
+
let filepath = path.resolve(path.join(root, filename));
|
|
405
|
+
let stats = await fs.promises.lstat(filepath).catch(() => {});
|
|
406
|
+
if (stats !== null && stats !== void 0 && stats.isFile()) return {
|
|
407
|
+
path: filepath,
|
|
408
|
+
stats
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
} // returns the start and end of a byte range or undefined if unable to parse
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
const RANGE_REGEXP = /^bytes=(\d*)?-(\d*)?(?:\b|$)/;
|
|
415
|
+
|
|
416
|
+
function parseByteRange(range, size) {
|
|
417
|
+
let [, start, end = size] = (range === null || range === void 0 ? void 0 : range.match(RANGE_REGEXP)) ?? [0, 0, 0];
|
|
418
|
+
start = Math.max(parseInt(start, 10), 0);
|
|
419
|
+
end = Math.min(parseInt(end, 10), size - 1);
|
|
420
|
+
if (isNaN(start)) [start, end] = [size - end, size - 1];
|
|
421
|
+
if (start >= 0 && start < end) return {
|
|
422
|
+
start,
|
|
423
|
+
end
|
|
424
|
+
};
|
|
425
|
+
} // include ServerError and createRewriter as static properties
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
Server.Error = ServerError;
|
|
429
|
+
Server.createRewriter = createRewriter;
|
|
430
|
+
export default Server;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import logger from '@percy/logger';
|
|
3
|
+
export class Session extends EventEmitter {
|
|
4
|
+
#callbacks = new Map();
|
|
5
|
+
log = logger('core:session');
|
|
6
|
+
children = new Map();
|
|
7
|
+
|
|
8
|
+
constructor(browser, {
|
|
9
|
+
params,
|
|
10
|
+
sessionId: parentId
|
|
11
|
+
}) {
|
|
12
|
+
var _this$parent;
|
|
13
|
+
|
|
14
|
+
super();
|
|
15
|
+
this.browser = browser;
|
|
16
|
+
this.sessionId = params.sessionId;
|
|
17
|
+
this.targetId = params.targetInfo.targetId;
|
|
18
|
+
this.type = params.targetInfo.type;
|
|
19
|
+
this.isDocument = this.type === 'page' || this.type === 'iframe';
|
|
20
|
+
this.parent = browser.sessions.get(parentId);
|
|
21
|
+
(_this$parent = this.parent) === null || _this$parent === void 0 ? void 0 : _this$parent.children.set(this.sessionId, this);
|
|
22
|
+
this.on('Inspector.targetCrashed', this._handleTargetCrashed);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async close() {
|
|
26
|
+
if (!this.browser) return;
|
|
27
|
+
await this.browser.send('Target.closeTarget', {
|
|
28
|
+
targetId: this.targetId
|
|
29
|
+
}).catch(this._handleClosedError);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async send(method, params) {
|
|
33
|
+
/* istanbul ignore next: race condition paranoia */
|
|
34
|
+
if (this.closedReason) {
|
|
35
|
+
throw new Error(`Protocol error (${method}): ${this.closedReason}`);
|
|
36
|
+
} // send a raw message to the browser so we can provide a sessionId
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
let id = await this.browser.send({
|
|
40
|
+
sessionId: this.sessionId,
|
|
41
|
+
method,
|
|
42
|
+
params
|
|
43
|
+
}); // will resolve or reject when a matching response is received
|
|
44
|
+
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
this.#callbacks.set(id, {
|
|
47
|
+
error: new Error(),
|
|
48
|
+
resolve,
|
|
49
|
+
reject,
|
|
50
|
+
method
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_handleMessage(data) {
|
|
56
|
+
if (data.id && this.#callbacks.has(data.id)) {
|
|
57
|
+
// resolve or reject a pending promise created with #send()
|
|
58
|
+
let callback = this.#callbacks.get(data.id);
|
|
59
|
+
this.#callbacks.delete(data.id);
|
|
60
|
+
/* istanbul ignore next: races with browser._handleMessage() */
|
|
61
|
+
|
|
62
|
+
if (data.error) {
|
|
63
|
+
callback.reject(Object.assign(callback.error, {
|
|
64
|
+
message: `Protocol error (${callback.method}): ${data.error.message}` + ('data' in data.error ? `: ${data.error.data}` : '')
|
|
65
|
+
}));
|
|
66
|
+
} else {
|
|
67
|
+
callback.resolve(data.result);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// emit the message as an event
|
|
71
|
+
this.emit(data.method, data.params);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_handleClose() {
|
|
76
|
+
var _this$parent2;
|
|
77
|
+
|
|
78
|
+
this.closedReason || (this.closedReason = 'Session closed.'); // reject any pending callbacks
|
|
79
|
+
|
|
80
|
+
for (let callback of this.#callbacks.values()) {
|
|
81
|
+
callback.reject(Object.assign(callback.error, {
|
|
82
|
+
message: `Protocol error (${callback.method}): ${this.closedReason}`
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.#callbacks.clear();
|
|
87
|
+
(_this$parent2 = this.parent) === null || _this$parent2 === void 0 ? void 0 : _this$parent2.children.delete(this.sessionId);
|
|
88
|
+
this.browser = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_handleTargetCrashed = () => {
|
|
92
|
+
this.closedReason = 'Session crashed!';
|
|
93
|
+
this.close();
|
|
94
|
+
};
|
|
95
|
+
/* istanbul ignore next: encountered during closing races */
|
|
96
|
+
|
|
97
|
+
_handleClosedError = error => {
|
|
98
|
+
if (!error.message.endsWith(this.closedReason)) {
|
|
99
|
+
this.log.debug(error, this.meta);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export default Session;
|