nodulator
Version:
Complete NodeJS Framework for Restfull APIs
176 lines (147 loc) • 5.11 kB
JavaScript
/*!
* cofee-script - middleware (adapted from the less-middleware)
*
* Original Copyright(c) 2010 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
var coffee = require('coffee-script'),
fs = require('fs'),
url = require('url'),
path = require('path'),
mkdirp = require('mkdirp'),
convertSourceMap = require('convert-source-map');
/**
* Return Connect middleware with the given `options`.
*
* Options:
*
* `force` Always re-compile
* `once` Only re-compile the one time
* `debug` Output debugging information
* `bare` Compile the JavaScript without the top-level function safety wrapper
* `src` Source directory used to find .coffee files
* `encodeSrc` Encode CoffeeScript source file as base64 comment in compiled JavaScript
* `prefix` Path which should be stripped from `pathname`.
*
* Examples:
*
* Pass the middleware to Connect, grabbing .coffee files from this directory
* and saving .js files to _./public_. Also supplying our custom `compile` function.
*
* Following that we have a `static` layer setup to serve the .js
* files generated by Less.
*
* var server = connect.createServer(
* coffee.middleware({
* src: __dirname + '/public'
* })
* , connect.static(__dirname + '/public')
* );
*
* @param {Object} options
* @return {Function}
* @api public
*/
module.exports = coffee.middleware = function(options){
var regex = {
handle: /\.js$/
};
options = options || {};
// Accept src/dest dir
if ('string' === typeof options) {
options = { src: options };
}
// Only log if in debug mode
var log = function(key, val, type) {
if(options.debug || type === 'error') {
switch(type) {
case 'log':
case 'info':
case 'error':
case 'warn':
break;
default:
type = 'log';
}
console[type](' \033[90m%s :\033[0m \033[36m%s\033[0m', key, val);
}
};
var coffeeError = function(err) {
log("COFFEE " + err.name, err.message, 'error');
};
// Once option
options.once = options.once || false;
// Bare option
options.bare = options.bare || false;
// Encode CoffeeScript source file option
options.encodeSrc = options.encodeSrc === undefined ? true : options.encodeSrc;
// Source dir required
var src = options.src;
if (!src) { throw new Error('coffee.middleware() requires "src" directory'); }
// Default compile callback
options.compile = options.compile || coffee.compile;
// Middleware
return function(req, res, next) {
if ('GET' != req.method.toUpperCase() && 'HEAD' != req.method.toUpperCase()) { return next(); }
var pathname = url.parse(req.url).pathname;
// Only handle the matching files
if (regex.handle.test(pathname)) {
if (options.prefix && 0 === pathname.indexOf(options.prefix)) {
pathname = pathname.substring(options.prefix.length);
}
var coffeePath = path.join(src, pathname.replace('.js', '.coffee'));
if (/^\.\./.test(path.relative(src, coffeePath))) {
return res.send(403); // Forbidden
}
log('source', coffeePath);
// Ignore ENOENT to fall through as 404
var error = function(err) {
return next('ENOENT' == err.code ? null : err);
};
// Compile coffee
var compile = function() {
log('read', coffeePath);
fs.readFile(coffeePath, 'utf8', function(err, str){
if (err) { return error(err); }
try {
// Compile the JS to Coffee
var opts = { bare: options.bare,
filename: path.basename(coffeePath),
sourceMap: true }
var compiledObj = options.compile(str, opts);
log('render', coffeePath);
// Create own source map object (the other seems frozen)
var sourceMapObj = {
version: 3,
file: path.basename(pathname),
sources: [path.basename(coffeePath)],
names: [],
mappings: JSON.parse(compiledObj.v3SourceMap).mappings
};
var compiledJs = compiledObj.js;
// Optionally, append the comment to our source
if (options.encodeSrc) {
// Translate the sourcemap into a base64 comment
var sourceMapStr = convertSourceMap.fromObject(sourceMapObj).toComment();
compiledJs += '\n' + sourceMapStr;
}
// Specify the type as JS and send back content
res.type('js');
res.send(compiledJs);
} catch (err) {
coffeeError(err);
return next(err);
}
});
};
// Force
if (options.force) { return compile(); }
// Only check/recompile if it has not been done at before
if (options.once) { return next(); }
// Compile the files
return compile();
} else {
return next();
}
};
};