@percy/core 1.0.0-beta.75 → 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 +76 -0
- package/dist/percy.js +7 -6
- package/dist/server.js +424 -148
- package/package.json +9 -6
- package/test/helpers/server.js +24 -11
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
18
|
+
var _contentDisposition = _interopRequireDefault(require("content-disposition"));
|
|
17
19
|
|
|
18
|
-
var
|
|
20
|
+
var _pathToRegexp = require("path-to-regexp");
|
|
19
21
|
|
|
20
22
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
21
23
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; }
|
|
50
37
|
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
67
|
-
let context = {
|
|
68
|
-
version: _package.default.version,
|
|
64
|
+
exports.IncomingMessage = IncomingMessage;
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/core",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
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.
|
|
34
|
-
"@percy/config": "1.0.0-beta.
|
|
35
|
-
"@percy/dom": "1.0.0-beta.
|
|
36
|
-
"@percy/logger": "1.0.0-beta.
|
|
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": "
|
|
45
|
+
"gitHead": "445af68d8e270e2a35fc74e26422ed5d3c91d2ae"
|
|
43
46
|
}
|
package/test/helpers/server.js
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
// aliased to src for coverage during tests without needing to compile this file
|
|
2
|
-
const {
|
|
2
|
+
const { default: Server } = require('@percy/core/dist/server');
|
|
3
3
|
|
|
4
|
-
function createTestServer(
|
|
5
|
-
let
|
|
4
|
+
function createTestServer({ default: defaultReply, ...replies }, port = 8000) {
|
|
5
|
+
let server = new Server();
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
30
|
+
return server.listen(port);
|
|
18
31
|
};
|
|
19
32
|
|
|
20
33
|
// support commonjs environments
|