UNPKG

7.1 kBJavaScriptView Raw
1var fs = require('fs');
2var qs = require('qs');
3var path = require('path');
4var util = require('util');
5var http = require('http');
6var https = require('https');
7var events = require('events');
8var parse = require('url').parse;
9var debug = require('debug')('tinylr:server');
10var Client = require('./client');
11
12// Middleware fallbacks
13var bodyParser = require('body-parser').json()
14var queryParser = require('./middleware/query')();
15
16var config = require('../package.json');
17
18function Server(options) {
19 options = this.options = options || {};
20 events.EventEmitter.call(this);
21
22 options.livereload = options.livereload || require.resolve('livereload-js/dist/livereload.js');
23 options.port = parseInt(options.port || 35729, 10);
24
25 this.on('GET /', this.index.bind(this));
26 this.on('GET /changed', this.changed.bind(this));
27 this.on('POST /changed', this.changed.bind(this));
28 this.on('GET /livereload.js', this.livereload.bind(this));
29 this.on('GET /kill', this.close.bind(this));
30
31 if(options.errorListener) {
32 this.errorListener = options.errorListener;
33 }
34
35 this.clients = {};
36 this.configure(options.app);
37}
38
39module.exports = Server;
40
41util.inherits(Server, events.EventEmitter);
42
43Server.prototype.configure = function configure(app) {
44 var self = this;
45 debug('Configuring %s', app ? 'connect / express application' : 'HTTP server');
46
47 if (!app) {
48 if ((this.options.key && this.options.cert) || this.options.pfx) {
49 this.server = https.createServer(this.options, this.handler.bind(this));
50 } else {
51 this.server = http.createServer(this.handler.bind(this));
52 }
53 this.server.on('upgrade', this.websocketify.bind(this));
54 this.server.on('error', function() {
55 self.error.apply(self, arguments);
56 });
57 return this;
58 }
59
60 this.app = app;
61
62 this.app.listen = function(port, done) {
63 done = done || function() {};
64 if (port !== self.options.port) {
65 debug('Warn: LiveReload port is not standard (%d). You are listening on %d', self.options.port, port);
66 debug('You\'ll need to rely on the LiveReload snippet');
67 debug('> http://feedback.livereload.com/knowledgebase/articles/86180-how-do-i-add-the-script-tag-manually-');
68 }
69
70 var srv = self.server = http.createServer(app);
71 srv.on('upgrade', self.websocketify.bind(self));
72 srv.on('error', function() {
73 self.error.apply(self, arguments);
74 });
75 srv.on('close', self.close.bind(self));
76 return srv.listen(port, done);
77 };
78
79 return this;
80};
81
82Server.prototype.handler = function handler(req, res, next) {
83 var self = this;
84 var middleware = typeof next === 'function';
85 debug('LiveReload handler %s (middleware: %s)', req.url, middleware ? 'on' : 'off');
86
87 this.parse(req, res, function(err) {
88 debug('query parsed', req.body, err);
89 if (err) return next(err);
90 self.handle(req, res, next);
91 });
92
93 // req
94 // .on('end', this.handle.bind(this, req, res))
95 // .on('data', function(chunk) {
96 // req.data = req.data || '';
97 // req.data += chunk;
98 // });
99
100 return this;
101};
102
103// Ensure body / query are defined, useful as a fallback when the
104// Server is used without express / connect, and shouldn't hurt
105// otherwise
106Server.prototype.parse = function(req, res, next) {
107 debug('Parse', req.body, req.query);
108 bodyParser(req, res, function(err) {
109 debug('Body parsed', req.body);
110 if (err) return next(err);
111
112 queryParser(req, res, next);
113 });
114};
115
116Server.prototype.handle = function handle(req, res, next) {
117 var url = parse(req.url);
118 debug('Request:', req.method, url.href);
119 var middleware = typeof next === 'function';
120
121 // do the routing
122 var route = req.method + ' ' + url.pathname;
123 var respond = this.emit(route, req, res);
124 if (respond) return;
125 if (!middleware) {
126 // Only apply content-type on non middleware setup #70
127 res.setHeader('Content-Type', 'application/json');
128 } else {
129 // Middleware ==> next()ing
130 return next();
131 }
132
133 res.writeHead(404);
134 res.write(JSON.stringify({
135 error: 'not_found',
136 reason: 'no such route'
137 }));
138 res.end();
139};
140
141Server.prototype.websocketify = function websocketify(req, socket, head) {
142 var self = this;
143 var client = new Client(req, socket, head, this.options);
144 this.clients[client.id] = client;
145
146 debug('New LiveReload connection (id: %s)', client.id);
147 client.on('end', function() {
148 debug('Destroy client %s (url: %s)', client.id, client.url);
149 delete self.clients[client.id];
150 });
151};
152
153Server.prototype.listen = function listen(port, host, fn) {
154 port = parseInt(port || 35729, 10);
155 this.port = port;
156
157 if (typeof host === 'function') {
158 fn = host;
159 host = undefined;
160 }
161
162 this.server.listen(port, host, fn);
163};
164
165Server.prototype.close = function close(req, res) {
166 Object.keys(this.clients).forEach(function(id) {
167 this.clients[id].close();
168 }, this);
169
170
171 if (this.server._handle) this.server.close(this.emit.bind(this, 'close'));
172
173 if (res) res.end();
174};
175
176Server.prototype.error = function error(e) {
177 if(this.errorListener) {
178 this.errorListener(e);
179 return
180 }
181
182 console.error();
183 console.error('... Uhoh. Got error %s ...', e.message);
184 console.error(e.stack);
185
186 if (e.code !== 'EADDRINUSE') return;
187 console.error();
188 console.error('You already have a server listening on %s', this.port);
189 console.error('You should stop it and try again.');
190 console.error();
191};
192
193// Routes
194
195Server.prototype.livereload = function livereload(req, res) {
196 res.setHeader('Content-Type', 'application/javascript');
197 fs.createReadStream(this.options.livereload).pipe(res);
198};
199
200Server.prototype.changed = function changed(req, res) {
201 var files = this.param('files', req);
202
203 debug('Changed event (Files: %s)', files.join(' '));
204 var clients = this.notifyClients(files);
205
206 if (!res) return;
207
208 res.setHeader('Content-Type', 'application/json');
209 res.write(JSON.stringify({
210 clients: clients,
211 files: files
212 }));
213
214 res.end();
215};
216
217Server.prototype.notifyClients = function notifyClients(files) {
218 var clients = Object.keys(this.clients).map(function(id) {
219 var client = this.clients[id];
220 debug('Reloading client %s (url: %s)', client.id, client.url);
221 client.reload(files);
222 return {
223 id: client.id,
224 url: client.url
225 };
226 }, this);
227
228 return clients;
229};
230
231// Lookup param from body / params / query.
232Server.prototype.param = function _param(name, req) {
233 var param;
234 if (req.body && req.body[name]) param = req.body.files;
235 else if (req.params && req.params[name]) param = req.params.files;
236 else if (req.query && req.query[name]) param= req.query.files;
237
238 // normalize files array
239 param = Array.isArray(param) ? param :
240 typeof param === 'string' ? param.split(/[\s,]/) :
241 [];
242
243 debug('param %s', name, req.body, req.params, req.query, param);
244 return param;
245};
246
247Server.prototype.index = function index(req, res) {
248 res.setHeader('Content-Type', 'application/json');
249 res.write(JSON.stringify({
250 tinylr: 'Welcome',
251 version: config.version
252 }));
253
254 res.end();
255};