bem
Version:
313 lines (234 loc) • 9.51 kB
JavaScript
;
var INHERIT = require('inherit'),
Q = require('q'),
QFS = require('q-fs'),
QHTTP = require('q-http'),
QS = require('querystring'),
UTIL = require('util'),
URL = require('url'),
MIME = require('mime'),
PATH = require('./path'),
MAKE = require('./make'),
LOGGER = require('./logger'),
LEVEL = require('./level'),
U = require('./util');
var defaultDocument = 'index.html';
exports.Server = INHERIT({
__constructor: function(opts) {
this.opts = opts || {};
},
start: function() {
return this.rootExists(this.opts.root)()
.then(this.showInfo())
.then(this.initBuildRunner())
.then(this.startServers())
.then(function(servers) {
return Q.all(servers.map(function(s) {
return s.stopped;
}));
})
.then(function() {
LOGGER.info('Servers were stopped');
});
},
/* jshint -W109 */
showInfo: function() {
var _this = this;
return function() {
LOGGER.finfo("Project root is '%s'", _this.opts.root);
LOGGER.fverbose('Options are %j', _this.opts);
};
},
/* jshint +W109 */
/* jshint -W109 */
rootExists: function(root) {
return function() {
return QFS.exists(root).then(function(exists) {
if (!exists) return Q.reject(UTIL.format("Project root '%s' doesn't exist", root));
});
};
},
/* jshint +W109 */
startServers: function() {
var _this = this;
return function(runner) {
var requestHandler = _this.createRequestHandler(_this.opts.root, runner);
return Q.when(_this.startServer.call(_this, requestHandler),
function(servers) {
return Q.all(servers).then(function(servers) {
// Stop all servers on Control + C
servers.forEach(function(server) {
process.once('SIGINT', _this.stopServer(server));
});
return servers;
});
});
};
},
startServer: function(requestHandler) {
var _this = this,
netServer = QHTTP.Server(requestHandler),
started = [];
netServer.node.on('error', this.errorHandler.bind(this));
// Start server on net socket
started.push(netServer.listen(_this.opts.port, _this.opts.host).then(function(listener) {
LOGGER.finfo(
'Server is listening on port %s. Point your browser to http://%s:%s/',
_this.opts.port,
_this.opts.host || 'localhost',
_this.opts.port
);
return listener;
}));
return started;
},
stopServer: function(server) {
return function() {
server.stop();
};
},
errorHandler: function(port, error) {
if (!error) {
error = port;
port = this.opts.port;
}
switch (error.code) {
case 'EADDRINUSE':
LOGGER.error('port ' + port + ' is in use. Specify a different port or stop the service which is using it.');
break;
case 'EACCES':
LOGGER.error('insufficient permissions to listen port ' + port + '. Specify a different port.');
break;
default:
LOGGER.error(error.message);
break;
}
},
initBuildRunner: function() {
var _this = this;
return function() {
return Q.when(MAKE.createArch(_this.opts), function(arch) {
return new MAKE.APW(arch, _this.opts.workers, {
root: _this.opts.root,
verbose: _this.opts.verbose,
force: _this.opts.force
});
});
};
},
_targetsInProcess: 0,
createRequestHandler: function(root, runner) {
var _this = this;
return function(request, response) {
var reqPath = URL.parse(request.path).pathname,
relPath = QS.unescape(reqPath).replace(/^\/|\/$/g, ''),
fullPath = PATH.join(root, relPath);
if (PATH.dirSep === '\\') relPath = PATH.unixToOs(relPath);
LOGGER.fverbose('*** trying to access %s', fullPath);
// try to find node in arch
LOGGER.fverbose('*** searching for node "%s"', relPath);
return runner.findNode(relPath)
.fail(function(err) {
if (typeof err === 'string') {
LOGGER.fverbose('*** node not found "%s"', relPath);
return;
}
return Q.reject(err);
})
.then(function(id) {
if (!id) return;
_this._targetsInProcess++;
// found, run build
LOGGER.fverbose('*** node found, building "%s"', id);
LOGGER.fdebug('targets: %s', _this._targetsInProcess);
LOGGER.time('[t] Build total for "%s"', id);
return runner.process(id).fin(function() {
_this._targetsInProcess--;
if (_this._targetsInProcess === 0) LEVEL.resetLevelsCache();
LOGGER.fdebug('targets: %s', _this._targetsInProcess);
LOGGER.timeEnd('[t] Build total for "%s"', id);
});
})
// not found or successfully build, try to find path on disk
.then(_this.processPath(response, fullPath, root))
// any error, 500 internal server error
.fail(_this.httpError(response, 500));
};
},
processPath: function(response, path, root) {
var _this = this;
return function() {
return QFS.exists(path).then(function(exists) {
// 404 not found
if (!exists) {
return _this.httpError(response, 404)(path);
}
// found, process file/directory
return QFS.isDirectory(path).then(function(isDir) {
if (isDir) {
// TODO: make defaultDocument buildable
var def = PATH.join(path, defaultDocument);
return QFS.isFile(def).then(function(isFile) {
if (isFile) return _this.streamFile(response, def)();
return _this.processDirectory(response, path, root)();
});
}
return _this.streamFile(response, path)();
});
});
};
},
processDirectory: function(response, path, root) {
var _this = this;
return function() {
response.status = 200;
response.charset = 'utf8';
response.headers = { 'content-type': 'text/html' };
var body = response.body = [],
base = '/' + PATH.relative(root, path);
_this.pushFormatted(body,
'<!DOCTYPE html><html><head><meta charset="utf-8"/><title>%s</title></head>' +
'<body><b>Index of %s</b><ul style=\'list-style-type: none\'>',
path, path);
var files = [],
dirs = ['..'];
body.push(U.getDirsFiles(path, dirs, files).then(function() {
var listing = [],
pushListing = function(objs, str) {
objs.sort();
objs.forEach(function(o) {
_this.pushFormatted(listing, str, PATH.join(base, o), o);
});
};
pushListing(dirs, '<li><a href="%s">/%s</a></li>');
pushListing(files, '<li><a href="%s">%s</a></li>');
return listing.join('');
}));
body.push('</ul></body></html>');
return response;
};
},
streamFile: function(response, path) {
return function() {
response.status = 200;
response.charset = 'binary';
response.headers = { 'content-type': MIME.lookup(path) };
response.body = QFS.open(path);
return response;
};
},
httpError: function(response, code) {
return function(err) {
LOGGER.fwarn('*** HTTP error: %s, %s', code, (err && err.stack) || err);
response.status = code;
response.charset = 'utf8';
response.headers = { 'content-type': 'text/html' };
response.body = ['<h1>HTTP error ' + code + '</h1>'].concat(err? ['<pre>', '' + (err.stack || err), '</pre>'] : []);
return response;
};
},
pushFormatted: function(arr/*, str*/) {
arr.push(UTIL.format.apply(UTIL, Array.prototype.slice.call(arguments, 1)));
return arr;
}
});