blob: c08adf63a16e0ce87cfe80eecc769fabe4f9555e [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8
9var _fs = require('fs');
10
11var _fs2 = _interopRequireDefault(_fs);
12
13var _http = require('http');
14
15var _http2 = _interopRequireDefault(_http);
16
17var _https = require('https');
18
19var _https2 = _interopRequireDefault(_https);
20
21var _events = require('events');
22
23var _events2 = _interopRequireDefault(_events);
24
25var _url = require('url');
26
27var _client = require('./client');
28
29var _client2 = _interopRequireDefault(_client);
30
31var _package = require('../package.json');
32
33var _package2 = _interopRequireDefault(_package);
34
35var _any = require('body/any');
36
37var _any2 = _interopRequireDefault(_any);
38
39var _qs = require('qs');
40
41var _qs2 = _interopRequireDefault(_qs);
42
43function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
44
45function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
46
47function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
48
49function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
50
51var debug = require('debug')('tinylr:server');
52
53var CONTENT_TYPE = 'content-type';
54var FORM_TYPE = 'application/x-www-form-urlencoded';
55
56function buildRootPath() {
57 var prefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '/';
58
59 var rootUrl = prefix;
60
61 // Add trailing slash
62 if (prefix[prefix.length - 1] !== '/') {
63 rootUrl = rootUrl + '/';
64 }
65
66 // Add leading slash
67 if (prefix[0] !== '/') {
68 rootUrl = '/' + rootUrl;
69 }
70
71 return rootUrl;
72}
73
74var Server = function (_events$EventEmitter) {
75 _inherits(Server, _events$EventEmitter);
76
77 function Server() {
78 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
79
80 _classCallCheck(this, Server);
81
82 var _this = _possibleConstructorReturn(this, (Server.__proto__ || Object.getPrototypeOf(Server)).call(this));
83
84 _this.options = options;
85
86 options.livereload = options.livereload || require.resolve('livereload-js/dist/livereload.js');
87
88 // todo: change falsy check to allow 0 for random port
89 options.port = parseInt(options.port || 35729, 10);
90
91 if (options.errorListener) {
92 _this.errorListener = options.errorListener;
93 }
94
95 _this.rootPath = buildRootPath(options.prefix);
96
97 _this.clients = {};
98 _this.configure(options.app);
99 _this.routes(options.app);
100 return _this;
101 }
102
103 _createClass(Server, [{
104 key: 'routes',
105 value: function routes() {
106 if (!this.options.dashboard) {
107 this.on('GET ' + this.rootPath, this.index.bind(this));
108 }
109
110 this.on('GET ' + this.rootPath + 'changed', this.changed.bind(this));
111 this.on('POST ' + this.rootPath + 'changed', this.changed.bind(this));
112 this.on('POST ' + this.rootPath + 'alert', this.alert.bind(this));
113 this.on('GET ' + this.rootPath + 'livereload.js', this.livereload.bind(this));
114 this.on('GET ' + this.rootPath + 'kill', this.close.bind(this));
115 }
116 }, {
117 key: 'configure',
118 value: function configure(app) {
119 var _this2 = this;
120
121 debug('Configuring %s', app ? 'connect / express application' : 'HTTP server');
122
123 var handler = this.options.handler || this.handler;
124
125 if (!app) {
126 if (this.options.key && this.options.cert || this.options.pfx) {
127 this.server = _https2.default.createServer(this.options, handler.bind(this));
128 } else {
129 this.server = _http2.default.createServer(handler.bind(this));
130 }
131
132 this.server.on('upgrade', this.websocketify.bind(this));
133 this.server.on('error', this.error.bind(this));
134 return this;
135 }
136
137 this.app = app;
138 this.app.listen = function (port, done) {
139 done = done || function () {};
140 if (port !== _this2.options.port) {
141 debug('Warn: LiveReload port is not standard (%d). You are listening on %d', _this2.options.port, port);
142 debug('You\'ll need to rely on the LiveReload snippet');
143 debug('> http://feedback.livereload.com/knowledgebase/articles/86180-how-do-i-add-the-script-tag-manually-');
144 }
145
146 var srv = _this2.server = _http2.default.createServer(app);
147 srv.on('upgrade', _this2.websocketify.bind(_this2));
148 srv.on('error', _this2.error.bind(_this2));
149 srv.on('close', _this2.close.bind(_this2));
150 return srv.listen(port, done);
151 };
152
153 return this;
154 }
155 }, {
156 key: 'handler',
157 value: function handler(req, res, next) {
158 var _this3 = this;
159
160 var middleware = typeof next === 'function';
161 debug('LiveReload handler %s (middleware: %s)', req.url, middleware ? 'on' : 'off');
162
163 next = next || this.defaultHandler.bind(this, res);
164 req.headers[CONTENT_TYPE] = req.headers[CONTENT_TYPE] || FORM_TYPE;
165 return (0, _any2.default)(req, res, function (err, body) {
166 if (err) return next(err);
167 req.body = body;
168
169 if (!req.query) {
170 req.query = req.url.indexOf('?') !== -1 ? _qs2.default.parse((0, _url.parse)(req.url).query) : {};
171 }
172
173 return _this3.handle(req, res, next);
174 });
175 }
176 }, {
177 key: 'index',
178 value: function index(req, res) {
179 res.setHeader('Content-Type', 'application/json');
180 res.write(JSON.stringify({
181 tinylr: 'Welcome',
182 version: _package2.default.version
183 }));
184
185 res.end();
186 }
187 }, {
188 key: 'handle',
189 value: function handle(req, res, next) {
190 var url = (0, _url.parse)(req.url);
191 debug('Request:', req.method, url.href);
192 var middleware = typeof next === 'function';
193
194 // do the routing
195 var route = req.method + ' ' + url.pathname;
196 var respond = this.emit(route, req, res);
197 if (respond) return;
198
199 if (middleware) return next();
200
201 // Only apply content-type on non middleware setup #70
202 return this.notFound(res);
203 }
204 }, {
205 key: 'defaultHandler',
206 value: function defaultHandler(res, err) {
207 if (!err) return this.notFound(res);
208
209 this.error(err);
210 res.setHeader('Content-Type', 'text/plain');
211 res.statusCode = 500;
212 res.end('Error: ' + err.stack);
213 }
214 }, {
215 key: 'notFound',
216 value: function notFound(res) {
217 res.setHeader('Content-Type', 'application/json');
218 res.writeHead(404);
219 res.write(JSON.stringify({
220 error: 'not_found',
221 reason: 'no such route'
222 }));
223 res.end();
224 }
225 }, {
226 key: 'websocketify',
227 value: function websocketify(req, socket, head) {
228 var _this4 = this;
229
230 var client = new _client2.default(req, socket, head, this.options);
231 this.clients[client.id] = client;
232
233 // handle socket error to prevent possible app crash, such as ECONNRESET
234 socket.on('error', function (e) {
235 // ignore frequent ECONNRESET error (seems inevitable when refresh)
236 if (e.code === 'ECONNRESET') return;
237 _this4.error(e);
238 });
239
240 client.once('info', function (data) {
241 debug('Create client %s (url: %s)', data.id, data.url);
242 _this4.emit('MSG /create', data.id, data.url);
243 });
244
245 client.once('end', function () {
246 debug('Destroy client %s (url: %s)', client.id, client.url);
247 _this4.emit('MSG /destroy', client.id, client.url);
248 delete _this4.clients[client.id];
249 });
250 }
251 }, {
252 key: 'listen',
253 value: function listen(port, host, fn) {
254 port = port || this.options.port;
255
256 // Last used port for error display
257 this.port = port;
258
259 if (typeof host === 'function') {
260 fn = host;
261 host = undefined;
262 }
263
264 this.server.listen(port, host, fn);
265 }
266 }, {
267 key: 'close',
268 value: function close(req, res) {
269 Object.keys(this.clients).forEach(function (id) {
270 this.clients[id].close();
271 }, this);
272
273 if (this.server._handle) this.server.close(this.emit.bind(this, 'close'));
274
275 if (res) res.end();
276 }
277 }, {
278 key: 'error',
279 value: function error(e) {
280 if (this.errorListener) {
281 this.errorListener(e);
282 return;
283 }
284
285 console.error();
286 if (typeof e === 'undefined') {
287 console.error('... Uhoh. Got error %s ...', e);
288 } else {
289 console.error('... Uhoh. Got error %s ...', e.message);
290 console.error(e.stack);
291
292 if (e.code !== 'EADDRINUSE') return;
293 console.error();
294 console.error('You already have a server listening on %s', this.port);
295 console.error('You should stop it and try again.');
296 console.error();
297 }
298 }
299
300 // Routes
301
302 }, {
303 key: 'livereload',
304 value: function livereload(req, res) {
305 res.setHeader('Content-Type', 'application/javascript');
306 _fs2.default.createReadStream(this.options.livereload).pipe(res);
307 }
308 }, {
309 key: 'changed',
310 value: function changed(req, res) {
311 var files = this.param('files', req);
312
313 debug('Changed event (Files: %s)', files.join(' '));
314 var clients = this.notifyClients(files);
315
316 if (!res) return;
317
318 res.setHeader('Content-Type', 'application/json');
319 res.write(JSON.stringify({
320 clients: clients,
321 files: files
322 }));
323
324 res.end();
325 }
326 }, {
327 key: 'alert',
328 value: function alert(req, res) {
329 var message = this.param('message', req);
330
331 debug('Alert event (Message: %s)', message);
332 var clients = this.alertClients(message);
333
334 if (!res) return;
335
336 res.setHeader('Content-Type', 'application/json');
337 res.write(JSON.stringify({
338 clients: clients,
339 message: message
340 }));
341
342 res.end();
343 }
344 }, {
345 key: 'notifyClients',
346 value: function notifyClients(files) {
347 var clients = Object.keys(this.clients).map(function (id) {
348 var client = this.clients[id];
349 debug('Reloading client %s (url: %s)', client.id, client.url);
350 client.reload(files);
351 return {
352 id: client.id,
353 url: client.url
354 };
355 }, this);
356
357 return clients;
358 }
359 }, {
360 key: 'alertClients',
361 value: function alertClients(message) {
362 var clients = Object.keys(this.clients).map(function (id) {
363 var client = this.clients[id];
364 debug('Alert client %s (url: %s)', client.id, client.url);
365 client.alert(message);
366 return {
367 id: client.id,
368 url: client.url
369 };
370 }, this);
371
372 return clients;
373 }
374
375 // Lookup param from body / params / query.
376
377 }, {
378 key: 'param',
379 value: function param(name, req) {
380 var param = void 0;
381 if (req.body && req.body[name]) param = req.body[name];else if (req.params && req.params[name]) param = req.params[name];else if (req.query && req.query[name]) param = req.query[name];
382
383 // normalize files array
384 if (name === 'files') {
385 param = Array.isArray(param) ? param : typeof param === 'string' ? param.split(/[\s,]/) : [];
386 }
387
388 return param;
389 }
390 }]);
391
392 return Server;
393}(_events2.default.EventEmitter);
394
395exports.default = Server;
396module.exports = exports['default'];