UNPKG

9.52 kBJavaScriptView Raw
1'use strict';
2
3var INHERIT = require('inherit'),
4 Q = require('q'),
5 QFS = require('q-io/fs'),
6 QHTTP = require('q-io/http'),
7 QS = require('querystring'),
8 UTIL = require('util'),
9 URL = require('url'),
10 MIME = require('mime'),
11 PATH = require('./path'),
12 MAKE = require('./make'),
13 LOGGER = require('./logger'),
14 LEVEL = require('./level'),
15 U = require('./util');
16
17var defaultDocument = 'index.html';
18
19exports.Server = INHERIT({
20
21 __constructor: function(opts) {
22 this.opts = opts || {};
23 },
24
25 start: function() {
26
27 return this.rootExists(this.opts.root)()
28 .then(this.showInfo())
29 .then(this.initBuildRunner())
30 .then(this.startServers())
31 .then(function(servers) {
32 return Q.all(servers.map(function(s) {
33 return s.stopped;
34 }));
35 })
36 .then(function() {
37 LOGGER.info('Servers were stopped');
38 });
39
40 },
41
42 /* jshint -W109 */
43 showInfo: function() {
44 var _this = this;
45
46 return function() {
47 LOGGER.finfo("Project root is '%s'", _this.opts.root);
48 LOGGER.fverbose('Options are %j', _this.opts);
49 };
50 },
51 /* jshint +W109 */
52
53 /* jshint -W109 */
54 rootExists: function(root) {
55
56 return function() {
57 return QFS.exists(root).then(function(exists) {
58 if (!exists) return Q.reject(UTIL.format("Project root '%s' doesn't exist", root));
59 });
60 };
61
62 },
63 /* jshint +W109 */
64
65 startServers: function() {
66 var _this = this;
67
68 return function(runner) {
69 var requestHandler = _this.createRequestHandler(_this.opts.root, runner);
70
71 return Q.when(_this.startServer.call(_this, requestHandler),
72 function(servers) {
73
74 return Q.all(servers).then(function(servers) {
75
76 // Stop all servers on Control + C
77 servers.forEach(function(server) {
78 process.once('SIGINT', _this.stopServer(server));
79 });
80
81 return servers;
82 });
83
84 });
85
86 };
87
88 },
89
90 startServer: function(requestHandler) {
91 var _this = this,
92 netServer = QHTTP.Server(requestHandler),
93 started = [];
94
95 netServer.node.on('error', this.errorHandler.bind(this));
96
97 // Start server on net socket
98 started.push(netServer.listen(_this.opts.port, _this.opts.host).then(function(listener) {
99 LOGGER.finfo(
100 'Server is listening on port %s. Point your browser to http://%s:%s/',
101 _this.opts.port,
102 _this.opts.host || 'localhost',
103 _this.opts.port
104 );
105
106 return listener;
107 }));
108
109 return started;
110 },
111
112 stopServer: function(server) {
113 return function() {
114 server.stop();
115 };
116 },
117
118 errorHandler: function(port, error) {
119 if (!error) {
120 error = port;
121 port = this.opts.port;
122 }
123
124 switch (error.code) {
125 case 'EADDRINUSE':
126 LOGGER.error('port ' + port + ' is in use. Specify a different port or stop the service which is using it.');
127 break;
128
129 case 'EACCES':
130 LOGGER.error('insufficient permissions to listen port ' + port + '. Specify a different port.');
131 break;
132
133 default:
134 LOGGER.error(error.message);
135 break;
136 }
137 },
138
139 initBuildRunner: function() {
140 var _this = this;
141
142 return function() {
143 return Q.when(MAKE.createArch(_this.opts), function(arch) {
144 return new MAKE.APW(arch, _this.opts.workers, {
145 root: _this.opts.root,
146 verbose: _this.opts.verbose,
147 force: _this.opts.force
148 });
149 });
150 };
151 },
152
153 _targetsInProcess: 0,
154
155 createRequestHandler: function(root, runner) {
156 var _this = this;
157
158 return function(request, response) {
159
160 var reqPath = URL.parse(request.path).pathname,
161 relPath = QS.unescape(reqPath).replace(/^\/|\/$/g, ''),
162 fullPath = PATH.join(root, relPath);
163
164 if (PATH.dirSep === '\\') relPath = PATH.unixToOs(relPath);
165
166 LOGGER.fverbose('*** trying to access %s', fullPath);
167
168 // try to find node in arch
169 LOGGER.fverbose('*** searching for node "%s"', relPath);
170 return runner.findNode(relPath)
171 .fail(function(err) {
172 if (typeof err === 'string') {
173 LOGGER.fverbose('*** node not found "%s"', relPath);
174 return;
175 }
176 return Q.reject(err);
177 })
178 .then(function(id) {
179 if (!id) return;
180
181 _this._targetsInProcess++;
182
183 // found, run build
184 LOGGER.fverbose('*** node found, building "%s"', id);
185 LOGGER.fdebug('targets: %s', _this._targetsInProcess);
186 LOGGER.time('[t] Build total for "%s"', id);
187 return runner.process(id).fin(function() {
188 _this._targetsInProcess--;
189
190 if (_this._targetsInProcess === 0) LEVEL.resetLevelsCache();
191
192 LOGGER.fdebug('targets: %s', _this._targetsInProcess);
193 LOGGER.timeEnd('[t] Build total for "%s"', id);
194 });
195 })
196
197 // not found or successfully build, try to find path on disk
198 .then(_this.processPath(response, fullPath, root))
199
200 // any error, 500 internal server error
201 .fail(_this.httpError(response, 500));
202
203 };
204
205 },
206
207 processPath: function(response, path, root) {
208 var _this = this;
209
210 return function() {
211
212 return QFS.exists(path).then(function(exists) {
213
214 // 404 not found
215 if (!exists) {
216 return _this.httpError(response, 404)(path);
217 }
218
219 // found, process file/directory
220 return QFS.isDirectory(path).then(function(isDir) {
221
222 if (isDir) {
223
224 // TODO: make defaultDocument buildable
225 var def = PATH.join(path, defaultDocument);
226 return QFS.isFile(def).then(function(isFile) {
227 if (isFile) return _this.streamFile(response, def)();
228 return _this.processDirectory(response, path, root)();
229 });
230
231 }
232
233 return _this.streamFile(response, path)();
234
235 });
236
237 });
238
239 };
240 },
241
242 processDirectory: function(response, path, root) {
243 var _this = this;
244
245 return function() {
246 response.status = 200;
247 response.charset = 'utf8';
248 response.headers = { 'content-type': 'text/html' };
249
250 var body = response.body = [],
251 base = '/' + PATH.relative(root, path);
252
253 _this.pushFormatted(body,
254 '<!DOCTYPE html><html><head><meta charset="utf-8"/><title>%s</title></head>' +
255 '<body><b>Index of %s</b><ul style=\'list-style-type: none\'>',
256 path, path);
257
258 var files = [],
259 dirs = ['..'];
260
261 body.push(U.getDirsFiles(path, dirs, files).then(function() {
262 var listing = [],
263 pushListing = function(objs, str) {
264 objs.sort();
265 objs.forEach(function(o) {
266 _this.pushFormatted(listing, str, PATH.join(base, o), o);
267 });
268 };
269
270 pushListing(dirs, '<li><a href="%s">/%s</a></li>');
271 pushListing(files, '<li><a href="%s">%s</a></li>');
272
273 return listing.join('');
274 }));
275
276 body.push('</ul></body></html>');
277
278 return response;
279 };
280 },
281
282 streamFile: function(response, path) {
283
284 return function() {
285
286 response.status = 200;
287 response.charset = 'binary';
288 response.headers = { 'content-type': MIME.lookup(path) };
289 response.body = QFS.open(path, 'b');
290
291 return response;
292 };
293 },
294
295 httpError: function(response, code) {
296 return function(err) {
297 LOGGER.fwarn('*** HTTP error: %s, %s', code, (err && err.stack) || err);
298
299 response.status = code;
300 response.charset = 'utf8';
301 response.headers = { 'content-type': 'text/html' };
302 response.body = ['<h1>HTTP error ' + code + '</h1>'].concat(err? ['<pre>', '' + (err.stack || err), '</pre>'] : []);
303
304 return response;
305 };
306 },
307
308 pushFormatted: function(arr/*, str*/) {
309 arr.push(UTIL.format.apply(UTIL, Array.prototype.slice.call(arguments, 1)));
310 return arr;
311 }
312});