wintersmith
Version:
A flexible static site generator.
477 lines (445 loc) • 14.6 kB
JavaScript
/* server.coffee */
(function() {
var Config, ContentPlugin, ContentTree, Stream, async, buildLookupMap, chalk, chokidar, colorCode, enableDestroy, http, keyForValue, loadContent, lookupCharset, mime, minimatch, normalizeUrl, pump, ref, renderView, replaceInArray, run, runGenerator, setup, sleep, url, urlEqual;
async = require('async');
chokidar = require('chokidar');
chalk = require('chalk');
http = require('http');
mime = require('mime');
url = require('url');
minimatch = require('minimatch');
enableDestroy = require('server-destroy');
Stream = require('stream').Stream;
Config = require('./config').Config;
ref = require('./content'), ContentTree = ref.ContentTree, ContentPlugin = ref.ContentPlugin, loadContent = ref.loadContent;
pump = require('./utils').pump;
renderView = require('./renderer').renderView;
runGenerator = require('./generator').runGenerator;
colorCode = function(code) {
switch (Math.floor(code / 100)) {
case 2:
return chalk.green(code);
case 4:
return chalk.yellow(code);
case 5:
return chalk.red(code);
default:
return code.toString();
}
};
sleep = function(callback) {
return setTimeout(callback, 50);
};
normalizeUrl = function(anUrl) {
if (anUrl[anUrl.length - 1] === '/') {
anUrl += 'index.html';
}
if (anUrl.match(/^([^.]*[^\/])$/)) {
anUrl += '/index.html';
}
anUrl = decodeURI(anUrl);
return anUrl;
};
urlEqual = function(urlA, urlB) {
return normalizeUrl(urlA) === normalizeUrl(urlB);
};
keyForValue = function(object, value) {
var key;
for (key in object) {
if (object[key] === value) {
return key;
}
}
return null;
};
replaceInArray = function(array, oldItem, newItem) {
var idx;
idx = array.indexOf(oldItem);
if (idx === -1) {
return false;
}
array[idx] = newItem;
return true;
};
buildLookupMap = function(contents) {
var i, item, len, map, ref1;
map = {};
ref1 = ContentTree.flatten(contents);
for (i = 0, len = ref1.length; i < len; i++) {
item = ref1[i];
map[normalizeUrl(item.url)] = item;
}
return map;
};
lookupCharset = function(mimeType) {
if (/^text\/|^application\/(javascript|json)/.test(mimeType)) {
return 'UTF-8';
} else {
return null;
}
};
setup = function(env) {
/* Create a preview request handler. */
var block, changeHandler, contentHandler, contentWatcher, contents, isReady, loadContents, loadLocals, loadTemplates, loadViews, locals, logop, lookup, requestHandler, templateWatcher, templates, viewsWatcher;
contents = null;
templates = null;
locals = null;
lookup = {};
block = {
contentsLoad: false,
templatesLoad: false,
viewsLoad: false,
localsLoad: false
};
isReady = function() {
/* Returns true if we have no running tasks */
var k, v;
for (k in block) {
v = block[k];
if (v === true) {
return false;
}
}
return true;
};
logop = function(error) {
if (error != null) {
return env.logger.error(error.message, error);
}
};
changeHandler = function(error, path) {
/* Emits a change event if called without error */
if (error == null) {
env.emit('change', path, false);
}
return logop(error);
};
loadContents = function(callback) {
if (callback == null) {
callback = logop;
}
block.contentsLoad = true;
lookup = {};
contents = null;
return ContentTree.fromDirectory(env, env.contentsPath, function(error, result) {
if (error == null) {
contents = result;
lookup = buildLookupMap(result);
}
block.contentsLoad = false;
return callback(error);
});
};
loadTemplates = function(callback) {
if (callback == null) {
callback = logop;
}
block.templatesLoad = true;
templates = null;
return env.getTemplates(function(error, result) {
if (error == null) {
templates = result;
}
block.templatesLoad = false;
return callback(error);
});
};
loadViews = function(callback) {
if (callback == null) {
callback = logop;
}
block.viewsLoad = true;
return env.loadViews(function(error) {
block.viewsLoad = false;
return callback(error);
});
};
loadLocals = function(callback) {
if (callback == null) {
callback = logop;
}
block.localsLoad = true;
locals = null;
return env.getLocals(function(error, result) {
if (error == null) {
locals = result;
}
block.localsLoad = false;
return callback(error);
});
};
contentWatcher = chokidar.watch(env.contentsPath, {
ignoreInitial: true
});
contentWatcher.on('all', function(type, filename) {
var i, len, pattern, ref1, relpath;
if (block.contentsLoad) {
return;
}
relpath = env.relativeContentsPath(filename);
ref1 = env.config.ignore;
for (i = 0, len = ref1.length; i < len; i++) {
pattern = ref1[i];
if (minimatch(relpath, pattern)) {
env.emit('change', relpath, true);
return;
}
}
return loadContents(function(error) {
var content, contentFilename, j, len1, ref2;
contentFilename = null;
if ((error == null) && (filename != null)) {
ref2 = ContentTree.flatten(contents);
for (j = 0, len1 = ref2.length; j < len1; j++) {
content = ref2[j];
if (content.__filename === filename) {
contentFilename = content.filename;
break;
}
}
}
return changeHandler(error, contentFilename);
});
});
templateWatcher = chokidar.watch(env.templatesPath, {
ignoreInitial: true
});
templateWatcher.on('all', function(event, path) {
if (!block.templatesLoad) {
return loadTemplates(changeHandler);
}
});
if (env.config.views != null) {
viewsWatcher = chokidar.watch(env.resolvePath(env.config.views), {
ignoreInitial: true
});
viewsWatcher.on('all', function(event, path) {
if (!block.viewsLoad) {
delete require.cache[path];
return loadViews(changeHandler);
}
});
}
contentHandler = function(request, response, callback) {
var uri;
uri = normalizeUrl(url.parse(request.url).pathname);
env.logger.verbose("contentHandler - " + uri);
return async.waterfall([
function(callback) {
return async.mapSeries(env.generators, function(generator, callback) {
return runGenerator(env, contents, generator, callback);
}, callback);
}, function(generated, callback) {
var error, gentree, i, len, map, tree;
if (generated.length > 0) {
try {
tree = new ContentTree('', env.getContentGroups());
for (i = 0, len = generated.length; i < len; i++) {
gentree = generated[i];
ContentTree.merge(tree, gentree);
}
map = buildLookupMap(generated);
ContentTree.merge(tree, contents);
} catch (error1) {
error = error1;
return callback(error);
}
return callback(null, tree, map);
} else {
return callback(null, contents, {});
}
}, function(tree, generatorLookup, callback) {
var content, pluginName;
content = generatorLookup[uri] || lookup[uri];
if (content != null) {
pluginName = content.constructor.name;
return renderView(env, content, locals, tree, templates, function(error, result) {
var charset, contentType, mimeType, ref1;
if (error) {
return callback(error, 500, pluginName);
} else if (result != null) {
mimeType = (ref1 = mime.getType(content.filename)) != null ? ref1 : mime.getType(uri);
charset = lookupCharset(mimeType);
if (charset) {
contentType = mimeType + "; charset=" + charset;
} else {
contentType = mimeType;
}
if (result instanceof Stream) {
response.writeHead(200, {
'Content-Type': contentType
});
return pump(result, response, function(error) {
return callback(error, 200, pluginName);
});
} else if (result instanceof Buffer) {
response.writeHead(200, {
'Content-Type': contentType
});
response.write(result);
response.end();
return callback(null, 200, pluginName);
} else {
return callback(new Error("View for content '" + content.filename + "' returned invalid response. Expected Buffer or Stream."));
}
} else {
response.writeHead(404, {
'Content-Type': 'text/plain'
});
response.end('404 Not Found\n');
return callback(null, 404, pluginName);
}
});
} else {
return callback();
}
}
], callback);
};
requestHandler = function(request, response) {
var start, uri;
start = Date.now();
uri = url.parse(request.url).pathname;
return async.waterfall([
function(callback) {
if (!block.contentsLoad && (contents == null)) {
return loadContents(callback);
} else {
return callback();
}
}, function(callback) {
if (!block.templatesLoad && (templates == null)) {
return loadTemplates(callback);
} else {
return callback();
}
}, function(callback) {
return async.until(isReady, sleep, callback);
}, function(callback) {
return contentHandler(request, response, callback);
}
], function(error, responseCode, pluginName) {
var delta, logstr;
if ((error != null) || (responseCode == null)) {
responseCode = error != null ? 500 : 404;
response.writeHead(responseCode, {
'Content-Type': 'text/plain'
});
response.end(error != null ? error.message : '404 Not Found\n');
}
delta = Date.now() - start;
logstr = (colorCode(responseCode)) + " " + (chalk.bold(uri));
if (pluginName != null) {
logstr += " " + (chalk.grey(pluginName));
}
logstr += chalk.grey(" " + delta + "ms");
env.logger.info(logstr);
if (error) {
return env.logger.error(error.message, error);
}
});
};
loadContents();
loadTemplates();
loadViews();
loadLocals();
requestHandler.destroy = function() {
contentWatcher.close();
templateWatcher.close();
return viewsWatcher != null ? viewsWatcher.close() : void 0;
};
return requestHandler;
};
run = function(env, callback) {
var configWatcher, handler, restart, server, start, stop;
server = null;
handler = null;
if (env.config._restartOnConfChange && (env.config.__filename != null)) {
env.logger.verbose("watching config file " + env.config.__filename + " for changes");
configWatcher = chokidar.watch(env.config.__filename);
configWatcher.on('change', function() {
var cliopts, config, error, key, value;
try {
config = Config.fromFileSync(env.config.__filename);
} catch (error1) {
error = error1;
env.logger.error("Error reloading config: " + error.message, error);
}
if (config != null) {
if (cliopts = env.config._cliopts) {
config._cliopts = {};
for (key in cliopts) {
value = cliopts[key];
config[key] = config._cliopts[key] = value;
}
}
env.setConfig(config);
return restart(function(error) {
if (error) {
throw error;
}
env.logger.verbose('config file change detected, server reloaded');
return env.emit('change');
});
}
});
}
restart = function(callback) {
env.logger.info('restarting server');
return async.waterfall([stop, start], callback);
};
stop = function(callback) {
if (server != null) {
return server.destroy(function(error) {
handler.destroy();
env.reset();
return callback(error);
});
} else {
return callback();
}
};
start = function(callback) {
return async.series([
function(callback) {
return env.loadPlugins(callback);
}, function(callback) {
handler = setup(env);
server = http.createServer(handler);
enableDestroy(server);
server.on('error', function(error) {
if (typeof callback === "function") {
callback(error);
}
return callback = null;
});
server.on('listening', function() {
if (typeof callback === "function") {
callback(null, server);
}
return callback = null;
});
return server.listen(env.config.port, env.config.hostname);
}
], callback);
};
process.on('uncaughtException', function(error) {
env.logger.error(error.message, error);
return process.exit(1);
});
env.logger.verbose('starting preview server');
return start(function(error, server) {
var host, serverUrl;
if (error == null) {
host = env.config.hostname || 'localhost';
serverUrl = "http://" + host + ":" + env.config.port + env.config.baseUrl;
env.logger.info("server running on: " + (chalk.bold(serverUrl)));
}
return callback(error, server);
});
};
module.exports = {
run: run,
setup: setup
};
}).call(this);