@percy/core 1.0.0-beta.73 → 1.0.0-beta.76

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/dist/api.js ADDED
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createPercyServer = createPercyServer;
7
+
8
+ var _fs = _interopRequireDefault(require("fs"));
9
+
10
+ var _logger = _interopRequireDefault(require("@percy/logger"));
11
+
12
+ var _server = _interopRequireDefault(require("./server"));
13
+
14
+ var _package = _interopRequireDefault(require("../package.json"));
15
+
16
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
+
18
+ function createPercyServer(percy, port) {
19
+ return new _server.default({
20
+ port
21
+ }) // facilitate logger websocket connections
22
+ .websocket(ws => _logger.default.connect(ws)) // general middleware
23
+ .route((req, res, next) => {
24
+ // treat all request bodies as json
25
+ if (req.body) try {
26
+ req.body = JSON.parse(req.body);
27
+ } catch {} // add version header
28
+
29
+ res.setHeader('Access-Control-Expose-Headers', '*, X-Percy-Core-Version');
30
+ res.setHeader('X-Percy-Core-Version', _package.default.version); // return json errors
31
+
32
+ return next().catch(e => {
33
+ var _e$status;
34
+
35
+ return res.json((_e$status = e.status) !== null && _e$status !== void 0 ? _e$status : 500, {
36
+ error: e.message,
37
+ success: false
38
+ });
39
+ });
40
+ }) // healthcheck returns basic information
41
+ .route('get', '/percy/healthcheck', (req, res) => res.json(200, {
42
+ loglevel: percy.loglevel(),
43
+ config: percy.config,
44
+ build: percy.build,
45
+ success: true
46
+ })) // get or set config options
47
+ .route(['get', 'post'], '/percy/config', async (req, res) => res.json(200, {
48
+ config: req.body ? await percy.setConfig(req.body) : percy.config,
49
+ success: true
50
+ })) // responds once idle (may take a long time)
51
+ .route('get', '/percy/idle', async (req, res) => res.json(200, {
52
+ success: await percy.idle().then(() => true)
53
+ })) // convenient @percy/dom bundle
54
+ .route('get', '/percy/dom.js', (req, res) => {
55
+ return res.file(200, require.resolve('@percy/dom'));
56
+ }) // legacy agent wrapper for @percy/dom
57
+ .route('get', '/percy-agent.js', async (req, res) => {
58
+ (0, _logger.default)('core:server').deprecated(['It looks like you’re using @percy/cli with an older SDK.', 'Please upgrade to the latest version to fix this warning.', 'See these docs for more info: https:docs.percy.io/docs/migrating-to-percy-cli'].join(' '));
59
+ let content = await _fs.default.promises.readFile(require.resolve('@percy/dom'), 'utf-8');
60
+ let wrapper = '(window.PercyAgent = class { snapshot(n, o) { return PercyDOM.serialize(o); } });';
61
+ return res.send(200, 'applicaton/javascript', content.concat(wrapper));
62
+ }) // post one or more snapshots
63
+ .route('post', '/percy/snapshot', async (req, res) => {
64
+ let snapshot = percy.snapshot(req.body);
65
+ if (!req.url.searchParams.has('async')) await snapshot;
66
+ return res.json(200, {
67
+ success: true
68
+ });
69
+ }) // stops percy at the end of the current event loop
70
+ .route('/percy/stop', (req, res) => {
71
+ setImmediate(() => percy.stop());
72
+ return res.json(200, {
73
+ success: true
74
+ });
75
+ });
76
+ }
package/dist/percy.js CHANGED
@@ -17,7 +17,7 @@ var _queue = _interopRequireDefault(require("./queue"));
17
17
 
18
18
  var _browser = _interopRequireDefault(require("./browser"));
19
19
 
20
- var _server = _interopRequireDefault(require("./server"));
20
+ var _api = require("./api");
21
21
 
22
22
  var _snapshot = require("./snapshot");
23
23
 
@@ -139,7 +139,7 @@ class Percy {
139
139
  } // start the server after everything else is ready
140
140
 
141
141
 
142
- yield (_this$server = this.server) === null || _this$server === void 0 ? void 0 : _this$server.listen(this.port); // mark instance as started
142
+ yield (_this$server = this.server) === null || _this$server === void 0 ? void 0 : _this$server.listen(); // mark instance as started
143
143
 
144
144
  this.log.info('Percy has started!');
145
145
  this.readyState = 1;
@@ -275,8 +275,7 @@ class Percy {
275
275
  });
276
276
 
277
277
  if (server) {
278
- this.server = (0, _server.default)(this);
279
- this.port = port;
278
+ this.server = (0, _api.createPercyServer)(this, port);
280
279
  }
281
280
  } // Shortcut for controlling the global logger's log level.
282
281
 
@@ -287,7 +286,9 @@ class Percy {
287
286
 
288
287
 
289
288
  address() {
290
- return `http://localhost:${this.port}`;
289
+ var _this$server4;
290
+
291
+ return (_this$server4 = this.server) === null || _this$server4 === void 0 ? void 0 : _this$server4.address();
291
292
  } // Set client & environment info, and override loaded config options
292
293
 
293
294
 
@@ -403,7 +404,7 @@ class Percy {
403
404
  } catch (error) {
404
405
  var _error$response, _failed$detail;
405
406
 
406
- let failed = ((_error$response = error.response) === null || _error$response === void 0 ? void 0 : _error$response.status) === 422 && error.response.body.errors.find(e => {
407
+ let failed = ((_error$response = error.response) === null || _error$response === void 0 ? void 0 : _error$response.statusCode) === 422 && error.response.body.errors.find(e => {
407
408
  var _e$source;
408
409
 
409
410
  return ((_e$source = e.source) === null || _e$source === void 0 ? void 0 : _e$source.pointer) === '/data/attributes/build';
package/dist/server.js CHANGED
@@ -3,193 +3,469 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.createPercyServer = createPercyServer;
7
- exports.createServer = createServer;
8
- exports.default = void 0;
6
+ exports.default = exports.ServerResponse = exports.ServerError = exports.Server = exports.IncomingMessage = void 0;
9
7
 
10
8
  var _fs = _interopRequireDefault(require("fs"));
11
9
 
10
+ var _path = _interopRequireDefault(require("path"));
11
+
12
12
  var _http = _interopRequireDefault(require("http"));
13
13
 
14
- var _ws = require("ws");
14
+ var _ws = _interopRequireDefault(require("ws"));
15
+
16
+ var _mimeTypes = _interopRequireDefault(require("mime-types"));
15
17
 
16
- var _logger = _interopRequireDefault(require("@percy/logger"));
18
+ var _contentDisposition = _interopRequireDefault(require("content-disposition"));
17
19
 
18
- var _package = _interopRequireDefault(require("../package.json"));
20
+ var _pathToRegexp = require("path-to-regexp");
19
21
 
20
22
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
21
23
 
22
- async function getReply({
23
- version,
24
- routes
25
- }, request, response) {
26
- var _headers$ContentType, _body$length, _body;
24
+ function _classPrivateMethodInitSpec(obj, privateSet) { _checkPrivateRedeclaration(obj, privateSet); privateSet.add(obj); }
27
25
 
28
- let [url] = request.url.split('?');
29
- let route = routes[url] || routes.default;
30
- let reply; // cors preflight
26
+ function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
31
27
 
32
- if (request.method === 'OPTIONS') {
33
- reply = [204, {}];
34
- reply[1]['Access-Control-Allow-Methods'] = 'GET,POST,OPTIONS';
35
- reply[1]['Access-Control-Request-Headers'] = 'Vary';
36
- let allowed = request.headers['access-control-request-headers'];
37
- if (allowed !== null && allowed !== void 0 && allowed.length) reply[1]['Access-Control-Allow-Headers'] = allowed;
38
- } else {
39
- reply = await Promise.resolve().then(() => {
40
- var _routes$middleware;
28
+ function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }
41
29
 
42
- return (_routes$middleware = routes.middleware) === null || _routes$middleware === void 0 ? void 0 : _routes$middleware.call(routes, request, response);
43
- }).then(() => route === null || route === void 0 ? void 0 : route(request, response)).catch(routes.catch);
44
- } // response was handled
30
+ function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
45
31
 
32
+ function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
46
33
 
47
- if (response.headersSent) return []; // default 404 when reply is not an array
34
+ function _classPrivateMethodGet(receiver, privateSet, fn) { if (!privateSet.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return fn; }
48
35
 
49
- let [status, headers, body] = Array.isArray(reply) ? reply : [404, {}]; // support content-type header shortcut
36
+ function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; }
50
37
 
51
- if (typeof headers === 'string') headers = {
52
- 'Content-Type': headers
53
- }; // auto stringify json
38
+ function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); }
54
39
 
55
- if ((_headers$ContentType = headers['Content-Type']) !== null && _headers$ContentType !== void 0 && _headers$ContentType.includes('json')) body = JSON.stringify(body); // add additional headers
40
+ 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; } }
56
41
 
57
- headers['Content-Length'] = (_body$length = (_body = body) === null || _body === void 0 ? void 0 : _body.length) !== null && _body$length !== void 0 ? _body$length : 0; // cors headers
42
+ // custom incoming message adds a `url` and `body` properties containing the parsed URL and message
43
+ // buffer respectively; both available after the 'end' event is emitted
44
+ class IncomingMessage extends _http.default.IncomingMessage {
45
+ constructor(socket) {
46
+ let buffer = [];
47
+ super(socket).on('data', d => buffer.push(d)).on('end', () => {
48
+ var _this$headers$content;
58
49
 
59
- headers['Access-Control-Expose-Headers'] = 'X-Percy-Core-Version';
60
- headers['Access-Control-Allow-Origin'] = '*'; // version header
50
+ this.url = new URL(this.url, `http://${this.headers.host}`);
51
+ if (buffer.length) this.body = Buffer.concat(buffer);
52
+
53
+ if (this.body && (_this$headers$content = this.headers['content-type']) !== null && _this$headers$content !== void 0 && _this$headers$content.includes('json')) {
54
+ try {
55
+ this.body = JSON.parse(this.body);
56
+ } catch {}
57
+ }
58
+ });
59
+ }
60
+
61
+ } // custom server response adds additional convenience methods
61
62
 
62
- headers['X-Percy-Core-Version'] = version;
63
- return [status, headers, body];
64
- }
65
63
 
66
- function createServer(routes) {
67
- let context = {
68
- version: _package.default.version,
64
+ exports.IncomingMessage = IncomingMessage;
69
65
 
70
- get listening() {
71
- return context.server.listening;
66
+ class ServerResponse extends _http.default.ServerResponse {
67
+ // responds with a status, headers, and body; the second argument can be an content-type string,
68
+ // or a headers object, with content-length being automatically set when a `body` is provided
69
+ send(status, headers, body) {
70
+ if (typeof headers === 'string') {
71
+ this.setHeader('Content-Type', headers);
72
+ headers = null;
72
73
  }
73
74
 
74
- }; // create a simple server to route request responses
75
+ if (body != null && !this.hasHeader('Content-Length')) {
76
+ this.setHeader('Content-Length', Buffer.byteLength(body));
77
+ }
78
+
79
+ return this.writeHead(status, headers).end(body);
80
+ } // responds with a status and content with a plain/text content-type
81
+
82
+
83
+ text(status, content) {
84
+ if (arguments.length < 2) [status, content] = [200, status];
85
+ return this.send(status, 'text/plain', content.toString());
86
+ } // responds with a status and stringified `data` with a json content-type
87
+
88
+
89
+ json(status, data) {
90
+ if (arguments.length < 2) [status, data] = [200, status];
91
+ return this.send(status, 'application/json', JSON.stringify(data));
92
+ } // responds with a status and streams a file with appropriate headers
75
93
 
76
- context.routes = routes;
77
- context.server = _http.default.createServer((request, response) => {
78
- request.params = new URLSearchParams(request.url.split('?')[1]);
79
- request.on('data', chunk => {
80
- request.body = (request.body || '') + chunk;
94
+
95
+ file(status, filepath) {
96
+ if (arguments.length < 2) [status, filepath] = [200, status];
97
+ filepath = _path.default.resolve(filepath);
98
+
99
+ let {
100
+ size
101
+ } = _fs.default.lstatSync(filepath);
102
+
103
+ let range = parseByteRange(this.req.headers.range, size); // support simple range requests
104
+
105
+ if (this.req.headers.range) {
106
+ let byteRange = range ? `${range.start}-${range.end}` : '*';
107
+ this.setHeader('Content-Range', `bytes ${byteRange}/${size}`);
108
+ if (!range) return this.send(416);
109
+ }
110
+
111
+ this.writeHead(range ? 206 : status, {
112
+ 'Accept-Ranges': 'bytes',
113
+ 'Content-Type': _mimeTypes.default.contentType(_path.default.extname(filepath)),
114
+ 'Content-Length': range ? range.end - range.start + 1 : size,
115
+ 'Content-Disposition': (0, _contentDisposition.default)(filepath, {
116
+ type: 'inline'
117
+ })
81
118
  });
82
- request.on('end', async () => {
83
- try {
84
- request.body = JSON.parse(request.body);
85
- } catch (e) {}
86
119
 
87
- let [status, headers, body] = await getReply(context, request, response);
88
- if (!response.headersSent) response.writeHead(status, headers).end(body);
120
+ _fs.default.createReadStream(filepath, range).pipe(this);
121
+
122
+ return this;
123
+ }
124
+
125
+ } // custom server error with a status and default reason
126
+
127
+
128
+ exports.ServerResponse = ServerResponse;
129
+
130
+ class ServerError extends Error {
131
+ static throw(status, reason) {
132
+ throw new this(status, reason);
133
+ }
134
+
135
+ constructor(status = 500, reason) {
136
+ super(reason || _http.default.STATUS_CODES[status]);
137
+ this.status = status;
138
+ }
139
+
140
+ } // custom server class handles routing requests and provides alternate methods and properties
141
+
142
+
143
+ exports.ServerError = ServerError;
144
+
145
+ var _sockets = /*#__PURE__*/new WeakMap();
146
+
147
+ var _defaultPort = /*#__PURE__*/new WeakMap();
148
+
149
+ var _up = /*#__PURE__*/new WeakMap();
150
+
151
+ var _handleUpgrade = /*#__PURE__*/new WeakSet();
152
+
153
+ var _routes = /*#__PURE__*/new WeakMap();
154
+
155
+ var _route = /*#__PURE__*/new WeakSet();
156
+
157
+ var _handleRequest = /*#__PURE__*/new WeakSet();
158
+
159
+ class Server extends _http.default.Server {
160
+ constructor({
161
+ port
162
+ } = {}) {
163
+ super({
164
+ IncomingMessage,
165
+ ServerResponse
89
166
  });
90
- }); // track connections
91
167
 
92
- context.sockets = new Set();
93
- context.server.on('connection', s => {
94
- context.sockets.add(s.on('close', () => context.sockets.delete(s)));
95
- }); // immediately kill connections on close
168
+ _classPrivateMethodInitSpec(this, _handleRequest);
96
169
 
97
- context.close = () => new Promise(resolve => {
98
- context.sockets.forEach(s => s.destroy());
99
- context.server.close(resolve);
100
- }); // starts the server
170
+ _classPrivateMethodInitSpec(this, _route);
101
171
 
172
+ _classPrivateMethodInitSpec(this, _handleUpgrade);
102
173
 
103
- context.listen = port => new Promise((resolve, reject) => {
104
- context.server.on('listening', () => resolve(context));
105
- context.server.on('error', reject);
106
- context.server.listen(port);
107
- }); // add routes programatically
174
+ _classPrivateFieldInitSpec(this, _sockets, {
175
+ writable: true,
176
+ value: new Set()
177
+ });
108
178
 
179
+ _classPrivateFieldInitSpec(this, _defaultPort, {
180
+ writable: true,
181
+ value: void 0
182
+ });
109
183
 
110
- context.reply = (url, handler) => {
111
- routes[url] = handler;
112
- return context;
113
- };
184
+ _classPrivateFieldInitSpec(this, _up, {
185
+ writable: true,
186
+ value: []
187
+ });
188
+
189
+ _classPrivateFieldInitSpec(this, _routes, {
190
+ writable: true,
191
+ value: [{
192
+ priority: -1,
193
+ handle: (req, res, next) => {
194
+ res.setHeader('Access-Control-Allow-Origin', '*');
195
+
196
+ if (req.method === 'OPTIONS') {
197
+ let allowHeaders = req.headers['access-control-request-headers'] || '*';
198
+ let allowMethods = [...new Set(_classPrivateFieldGet(this, _routes).flatMap(route => (!route.match || route.match(req.url.pathname)) && route.methods || []))].join(', ');
199
+ res.setHeader('Access-Control-Allow-Headers', allowHeaders);
200
+ res.setHeader('Access-Control-Allow-Methods', allowMethods);
201
+ res.writeHead(204).end();
202
+ } else {
203
+ res.setHeader('Access-Control-Expose-Headers', '*');
204
+ return next();
205
+ }
206
+ }
207
+ }, {
208
+ priority: 3,
209
+ handle: req => ServerError.throw(404)
210
+ }]
211
+ });
212
+
213
+ _classPrivateFieldSet(this, _defaultPort, port); // handle requests on end
214
+
215
+
216
+ this.on('request', (req, res) => {
217
+ req.on('end', () => _classPrivateMethodGet(this, _handleRequest, _handleRequest2).call(this, req, res));
218
+ }); // handle websocket upgrades
219
+
220
+ this.on('upgrade', (req, sock, head) => {
221
+ _classPrivateMethodGet(this, _handleUpgrade, _handleUpgrade2).call(this, req, sock, head);
222
+ }); // track open connections to terminate when the server closes
223
+
224
+ this.on('connection', socket => {
225
+ let handleClose = () => _classPrivateFieldGet(this, _sockets).delete(socket);
114
226
 
115
- return context;
227
+ _classPrivateFieldGet(this, _sockets).add(socket.on('close', handleClose));
228
+ });
229
+ } // return the listening port or any default port
230
+
231
+
232
+ get port() {
233
+ var _super$address$port, _super$address;
234
+
235
+ return (_super$address$port = (_super$address = super.address()) === null || _super$address === void 0 ? void 0 : _super$address.port) !== null && _super$address$port !== void 0 ? _super$address$port : _classPrivateFieldGet(this, _defaultPort);
236
+ } // return a string representation of the server address
237
+
238
+
239
+ address() {
240
+ let port = this.port;
241
+ let host = 'http://localhost';
242
+ return port ? `${host}:${port}` : host;
243
+ } // return a promise that resolves when the server is listening
244
+
245
+
246
+ listen(port = _classPrivateFieldGet(this, _defaultPort)) {
247
+ return new Promise((resolve, reject) => {
248
+ let handle = err => off() && err ? reject(err) : resolve(this);
249
+
250
+ let off = () => this.off('error', handle).off('listening', handle);
251
+
252
+ super.listen(port, handle).once('error', handle);
253
+ });
254
+ } // return a promise that resolves when the server closes
255
+
256
+
257
+ close() {
258
+ return new Promise(resolve => {
259
+ _classPrivateFieldGet(this, _sockets).forEach(socket => socket.destroy());
260
+
261
+ super.close(resolve);
262
+ });
263
+ } // handle websocket upgrades
264
+
265
+
266
+ websocket(pathname, handle) {
267
+ if (!handle) [pathname, handle] = [null, pathname];
268
+
269
+ _classPrivateFieldGet(this, _up).push({
270
+ match: pathname && (0, _pathToRegexp.match)(pathname),
271
+ handle: (req, sock, head) => new Promise(resolve => {
272
+ let wss = new _ws.default.Server({
273
+ noServer: true,
274
+ clientTracking: false
275
+ });
276
+ wss.handleUpgrade(req, sock, head, resolve);
277
+ }).then(ws => handle(ws, req))
278
+ });
279
+
280
+ if (pathname) {
281
+ _classPrivateFieldGet(this, _up).sort((a, b) => (a.match ? -1 : 1) - (b.match ? -1 : 1));
282
+ }
283
+
284
+ return this;
285
+ }
286
+
287
+ // set request routing and handling for pathnames and methods
288
+ route(method, pathname, handle) {
289
+ if (arguments.length === 1) [handle, method] = [method];
290
+ if (arguments.length === 2) [handle, pathname] = [pathname];
291
+ if (arguments.length === 2 && !Array.isArray(method) && method[0] === '/') [pathname, method] = [method];
292
+ return _classPrivateMethodGet(this, _route, _route2).call(this, {
293
+ priority: !pathname ? 0 : !method ? 1 : 2,
294
+ methods: method && [].concat(method).map(m => m.toUpperCase()),
295
+ match: pathname && (0, _pathToRegexp.match)(pathname),
296
+ handle
297
+ });
298
+ } // install a route that serves requested files from the provided directory
299
+
300
+
301
+ serve(pathname, directory, options) {
302
+ var _options;
303
+
304
+ if (typeof directory !== 'string') [options, directory] = [directory];
305
+ if (!directory) [pathname, directory] = ['/', pathname];
306
+
307
+ let root = _path.default.resolve(directory);
308
+
309
+ let mountPattern = (0, _pathToRegexp.pathToRegexp)(pathname, null, {
310
+ end: false
311
+ });
312
+ let rewritePath = createRewriter((_options = options) === null || _options === void 0 ? void 0 : _options.rewrites, (pathname, rewrite) => {
313
+ try {
314
+ let filepath = decodeURIComponent(pathname.replace(mountPattern, ''));
315
+ if (!isPathInside(root, filepath)) ServerError.throw();
316
+ return rewrite(filepath);
317
+ } catch {
318
+ throw new ServerError(400);
319
+ }
320
+ });
321
+ return _classPrivateMethodGet(this, _route, _route2).call(this, {
322
+ priority: 2,
323
+ methods: ['GET'],
324
+ match: pathname => mountPattern.test(pathname),
325
+ handle: async (req, res, next) => {
326
+ try {
327
+ var _options2;
328
+
329
+ let pathname = rewritePath(req.url.pathname);
330
+ let file = await getFile(root, pathname, (_options2 = options) === null || _options2 === void 0 ? void 0 : _options2.cleanUrls);
331
+ if (!(file !== null && file !== void 0 && file.stats.isFile())) return await next();
332
+ return res.file(file.path);
333
+ } catch (err) {
334
+ let statusPage = _path.default.join(root, `${err.status}.html`);
335
+
336
+ if (!_fs.default.existsSync(statusPage)) throw err;
337
+ return res.file(err.status, statusPage);
338
+ }
339
+ }
340
+ });
341
+ } // route and respond to requests; handling errors if necessary
342
+
343
+
344
+ } // create a url rewriter from provided rewrite rules
345
+
346
+
347
+ exports.Server = Server;
348
+
349
+ function _handleUpgrade2(req, sock, head) {
350
+ let up = _classPrivateFieldGet(this, _up).find(u => !u.match || u.match(req.url));
351
+
352
+ if (up) return up.handle(req, sock, head);
353
+ sock.write(`HTTP/1.1 400 ${_http.default.STATUS_CODES[400]}\r\n` + 'Connection: close\r\n\r\n');
354
+ sock.destroy();
355
+ }
356
+
357
+ function _route2(route) {
358
+ let i = _classPrivateFieldGet(this, _routes).findIndex(r => r.priority >= route.priority);
359
+
360
+ _classPrivateFieldGet(this, _routes).splice(i, 0, route);
361
+
362
+ return this;
116
363
  }
117
364
 
118
- function createPercyServer(percy) {
119
- let log = (0, _logger.default)('core:server');
120
- let context = createServer({
121
- // healthcheck returns meta info on success
122
- '/percy/healthcheck': () => [200, 'application/json', {
123
- success: true,
124
- config: percy.config,
125
- loglevel: percy.loglevel(),
126
- build: percy.build
127
- }],
128
- // remotely get and set percy config options
129
- '/percy/config': ({
130
- body
131
- }) => [200, 'application/json', {
132
- config: body ? percy.setConfig(body) : percy.config,
133
- success: true
134
- }],
135
- // responds when idle
136
- '/percy/idle': () => percy.idle().then(() => [200, 'application/json', {
137
- success: true
138
- }]),
139
- // serves @percy/dom as a convenience
140
- '/percy/dom.js': () => _fs.default.promises.readFile(require.resolve('@percy/dom'), 'utf-8').then(content => [200, 'applicaton/javascript', content]),
141
- // serves the new DOM library, wrapped for compatability to `@percy/agent`
142
- '/percy-agent.js': () => _fs.default.promises.readFile(require.resolve('@percy/dom'), 'utf-8').then(content => {
143
- let wrapper = '(window.PercyAgent = class PercyAgent { snapshot(n, o) { return PercyDOM.serialize(o); } });';
144
- log.deprecated('It looks like you’re using @percy/cli with an older SDK. Please upgrade to the latest version' + ' to fix this warning. See these docs for more info: https://docs.percy.io/docs/migrating-to-percy-cli');
145
- return [200, 'applicaton/javascript', content.concat(wrapper)];
146
- }),
147
- // forward snapshot requests
148
- '/percy/snapshot': async ({
149
- body,
150
- params
151
- }) => {
152
- let snapshot = percy.snapshot(body);
153
- if (!params.has('async')) await snapshot;
154
- return [200, 'application/json', {
155
- success: true
156
- }];
157
- },
158
- // stops the instance async at the end of the event loop
159
- '/percy/stop': () => {
160
- setImmediate(async () => await percy.stop());
161
- return [200, 'application/json', {
162
- success: true
163
- }];
164
- },
165
- // other routes 404
166
- default: () => [404, 'application/json', {
167
- error: 'Not found',
168
- success: false
169
- }],
170
- // generic error handler
171
- catch: ({
365
+ async function _handleRequest2(req, res) {
366
+ var _res$req;
367
+
368
+ // support node < 15.7.0
369
+ (_res$req = res.req) !== null && _res$req !== void 0 ? _res$req : res.req = req;
370
+
371
+ try {
372
+ // invoke routes like middleware
373
+ await async function cont(routes, i = 0) {
374
+ let next = () => cont(routes, i + 1);
375
+
376
+ let {
377
+ methods,
378
+ match,
379
+ handle
380
+ } = routes[i];
381
+ let result = !methods || methods.includes(req.method);
382
+ result && (result = !match || match(req.url.pathname));
383
+ if (result) req.params = result.params;
384
+ return result ? handle(req, res, next) : next();
385
+ }(_classPrivateFieldGet(this, _routes));
386
+ } catch (error) {
387
+ var _req$headers$accept, _req$headers$content;
388
+
389
+ let {
390
+ status = 500,
172
391
  message
173
- }) => [500, 'application/json', {
174
- error: message,
175
- success: false
176
- }]
177
- }); // start a websocket server
178
-
179
- context.wss = new _ws.Server({
180
- noServer: true
181
- }); // manually handle upgrades to avoid wss handling all events
182
-
183
- context.server.on('upgrade', (req, sock, head) => {
184
- context.wss.handleUpgrade(req, sock, head, socket => {
185
- // allow remote logging connections
186
- let disconnect = _logger.default.connect(socket);
187
-
188
- socket.once('close', () => disconnect());
189
- });
190
- });
191
- return context;
392
+ } = error; // fallback error handling
393
+
394
+ 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')) {
395
+ res.json(status, {
396
+ error: message
397
+ });
398
+ } else {
399
+ res.text(status, message);
400
+ }
401
+ }
192
402
  }
193
403
 
194
- var _default = createPercyServer;
404
+ function createRewriter(rewrites = [], cb) {
405
+ let normalize = p => _path.default.posix.normalize(_path.default.posix.join('/', p));
406
+
407
+ if (!Array.isArray(rewrites)) rewrites = Object.entries(rewrites);
408
+ let rewrite = [{
409
+ // resolve and normalize the path before rewriting
410
+ apply: p => _path.default.posix.resolve(normalize(p))
411
+ }].concat(rewrites.map(([src, dest]) => {
412
+ // compile rewrite rules into functions
413
+ let match = (0, _pathToRegexp.match)(normalize(src));
414
+ let toPath = (0, _pathToRegexp.compile)(normalize(dest));
415
+ return {
416
+ match,
417
+ apply: r => toPath(r.params)
418
+ };
419
+ })).reduceRight((next, rule) => pathname => {
420
+ var _rule$match, _rule$match2;
421
+
422
+ // compose all rewrites into a single function
423
+ let result = (_rule$match = (_rule$match2 = rule.match) === null || _rule$match2 === void 0 ? void 0 : _rule$match2.call(rule, pathname)) !== null && _rule$match !== void 0 ? _rule$match : pathname;
424
+ if (result) pathname = rule.apply(result);
425
+ return next(pathname);
426
+ }, p => p); // allow additional pathname processing around the rewriter
427
+
428
+ return p => cb(p, rewrite);
429
+ } // returns true if the pathname is inside the root pathname
430
+
431
+
432
+ function isPathInside(root, pathname) {
433
+ let abs = _path.default.resolve(_path.default.join(root, pathname));
434
+
435
+ return !abs.lastIndexOf(root, 0) && (abs[root.length] === _path.default.sep || !abs[root.length]);
436
+ } // get the absolute path and stats of a possible file
437
+
438
+
439
+ async function getFile(root, pathname, cleanUrls) {
440
+ for (let filename of [pathname].concat(cleanUrls ? _path.default.join(pathname, 'index.html') : [], cleanUrls && pathname.length > 2 ? pathname.replace(/\/?$/, '.html') : [])) {
441
+ let filepath = _path.default.resolve(_path.default.join(root, filename));
442
+
443
+ let stats = await _fs.default.promises.lstat(filepath).catch(() => {});
444
+ if (stats !== null && stats !== void 0 && stats.isFile()) return {
445
+ path: filepath,
446
+ stats
447
+ };
448
+ }
449
+ } // returns the start and end of a byte range or undefined if unable to parse
450
+
451
+
452
+ const RANGE_REGEXP = /^bytes=(\d*)?-(\d*)?(?:\b|$)/;
453
+
454
+ function parseByteRange(range, size) {
455
+ var _range$match;
456
+
457
+ let [, start, end = size] = (_range$match = range === null || range === void 0 ? void 0 : range.match(RANGE_REGEXP)) !== null && _range$match !== void 0 ? _range$match : [0, 0, 0];
458
+ start = Math.max(parseInt(start, 10), 0);
459
+ end = Math.min(parseInt(end, 10), size - 1);
460
+ if (isNaN(start)) [start, end] = [size - end, size - 1];
461
+ if (start >= 0 && start < end) return {
462
+ start,
463
+ end
464
+ };
465
+ } // include ServerError and createRewriter as static properties
466
+
467
+
468
+ Server.Error = ServerError;
469
+ Server.createRewriter = createRewriter;
470
+ var _default = Server;
195
471
  exports.default = _default;
package/dist/utils.js CHANGED
@@ -155,14 +155,16 @@ function waitFor(predicate, options) {
155
155
  timeout: options
156
156
  } : options || {};
157
157
  return generatePromise(async function* check(start, done) {
158
- if (timeout && Date.now() - start >= timeout) {
159
- throw new Error(`Timeout of ${timeout}ms exceeded.`);
160
- } else if (!predicate()) {
161
- yield new Promise(r => setTimeout(r, poll));
162
- return yield* check(start);
163
- } else if (idle && !done) {
164
- yield new Promise(r => setTimeout(r, idle));
165
- return yield* check(start, true);
158
+ while (true) {
159
+ if (timeout && Date.now() - start >= timeout) {
160
+ throw new Error(`Timeout of ${timeout}ms exceeded.`);
161
+ } else if (!predicate()) {
162
+ yield new Promise(r => setTimeout(r, poll, done = false));
163
+ } else if (idle && !done) {
164
+ yield new Promise(r => setTimeout(r, idle, done = true));
165
+ } else {
166
+ return;
167
+ }
166
168
  }
167
169
  }(Date.now()));
168
170
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.0.0-beta.73",
3
+ "version": "1.0.0-beta.76",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,14 +30,17 @@
30
30
  "test:types": "tsd"
31
31
  },
32
32
  "dependencies": {
33
- "@percy/client": "1.0.0-beta.73",
34
- "@percy/config": "1.0.0-beta.73",
35
- "@percy/dom": "1.0.0-beta.73",
36
- "@percy/logger": "1.0.0-beta.73",
33
+ "@percy/client": "1.0.0-beta.76",
34
+ "@percy/config": "1.0.0-beta.76",
35
+ "@percy/dom": "1.0.0-beta.76",
36
+ "@percy/logger": "1.0.0-beta.76",
37
+ "content-disposition": "^0.5.4",
37
38
  "cross-spawn": "^7.0.3",
38
39
  "extract-zip": "^2.0.1",
40
+ "mime-types": "^2.1.34",
41
+ "path-to-regexp": "^6.2.0",
39
42
  "rimraf": "^3.0.2",
40
43
  "ws": "^8.0.0"
41
44
  },
42
- "gitHead": "aa8160e02bea3e04ab1d3605762f89fbe79605d4"
45
+ "gitHead": "445af68d8e270e2a35fc74e26422ed5d3c91d2ae"
43
46
  }
@@ -1,20 +1,33 @@
1
1
  // aliased to src for coverage during tests without needing to compile this file
2
- const { createServer } = require('@percy/core/dist/server');
2
+ const { default: Server } = require('@percy/core/dist/server');
3
3
 
4
- function createTestServer(routes, port = 8000) {
5
- let context = createServer(routes);
4
+ function createTestServer({ default: defaultReply, ...replies }, port = 8000) {
5
+ let server = new Server();
6
6
 
7
- // handle route errors
8
- context.routes.catch = ({ message }) => [500, 'text/plain', message];
9
-
10
- // track requests
11
- context.requests = [];
12
- context.routes.middleware = ({ url, body }) => {
13
- context.requests.push(body ? [url, body] : [url]);
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);
14
12
  };
15
13
 
14
+ // map replies to alternate route handlers
15
+ server.reply = (p, reply) => (replies[p] = handleReply(reply));
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
+
16
29
  // automatically listen
17
- return context.listen(port);
30
+ return server.listen(port);
18
31
  };
19
32
 
20
33
  // support commonjs environments