/*! * Stylus - middleware * Copyright (c) Automattic * MIT Licensed */ /** * Module dependencies. */ var stylus = require('./stylus') , fs = require('fs') , url = require('url') , dirname = require('path').dirname , mkdirp = require('mkdirp') , join = require('path').join , sep = require('path').sep , debug = require('debug')('stylus:middleware'); /** * Import map. */ var imports = {}; /** * Return Connect middleware with the given `options`. * * Options: * * `force` Always re-compile * `src` Source directory used to find .styl files, * a string or function accepting `(path)` of request. * `dest` Destination directory used to output .css files, * a string or function accepting `(path)` of request, * when undefined defaults to `src`. * `compile` Custom compile function, accepting the arguments * `(str, path)`. * `compress` Whether the output .css files should be compressed * `firebug` Emits debug infos in the generated CSS that can * be used by the FireStylus Firebug plugin * `linenos` Emits comments in the generated CSS indicating * the corresponding Stylus line * 'sourcemap' Generates a sourcemap in sourcemaps v3 format * * Examples: * * Here we set up the custom compile function so that we may * set the `compress` option, or define additional functions. * * By default the compile function simply sets the `filename` * and renders the CSS. * * function compile(str, path) { * return stylus(str) * .set('filename', path) * .set('compress', true); * } * * Pass the middleware to Connect, grabbing .styl files from this directory * and saving .css files to _./public_. Also supplying our custom `compile` function. * * Following that we have a `static()` layer setup to serve the .css * files generated by Stylus. * * var app = connect(); * * app.middleware({ * src: __dirname * , dest: __dirname + '/public' * , compile: compile * }) * * app.use(connect.static(__dirname + '/public')); * * @param {Object} options * @return {Function} * @api public */ module.exports = function(options){ options = options || {}; // Accept src/dest dir if ('string' == typeof options) { options = { src: options }; } // Force compilation var force = options.force; // Source dir required var src = options.src; if (!src) throw new Error('stylus.middleware() requires "src" directory'); // Default dest dir to source var dest = options.dest || src; // Default compile callback options.compile = options.compile || function(str, path){ // inline sourcemap if (options.sourcemap) { if ('boolean' == typeof options.sourcemap) options.sourcemap = {}; options.sourcemap.inline = true; } return stylus(str) .set('filename', path) .set('compress', options.compress) .set('firebug', options.firebug) .set('linenos', options.linenos) .set('sourcemap', options.sourcemap); }; // Middleware return function stylus(req, res, next){ if ('GET' != req.method && 'HEAD' != req.method) return next(); var path = url.parse(req.url).pathname; if (/\.css$/.test(path)) { if (typeof dest == 'string') { // check for dest-path overlap var overlap = compare(dest, path).length; if ('/' == path.charAt(0)) overlap++; path = path.slice(overlap); } var cssPath, stylusPath; cssPath = (typeof dest == 'function') ? dest(path) : join(dest, path); stylusPath = (typeof src == 'function') ? src(path) : join(src, path.replace('.css', '.styl')); // Ignore ENOENT to fall through as 404 function error(err) { next('ENOENT' == err.code ? null : err); } // Force if (force) return compile(); // Compile to cssPath function compile() { debug('read %s', cssPath); fs.readFile(stylusPath, 'utf8', function(err, str){ if (err) return error(err); var style = options.compile(str, stylusPath); var paths = style.options._imports = []; imports[stylusPath] = null; style.render(function(err, css){ if (err) return next(err); debug('render %s', stylusPath); imports[stylusPath] = paths; mkdirp(dirname(cssPath), parseInt('0700', 8), function(err){ if (err) return error(err); fs.writeFile(cssPath, css, 'utf8', next); }); }); }); } // Re-compile on server restart, disregarding // mtimes since we need to map imports if (!imports[stylusPath]) return compile(); // Compare mtimes fs.stat(stylusPath, function(err, stylusStats){ if (err) return error(err); fs.stat(cssPath, function(err, cssStats){ // CSS has not been compiled, compile it! if (err) { if ('ENOENT' == err.code) { debug('not found %s', cssPath); compile(); } else { next(err); } } else { // Source has changed, compile it if (stylusStats.mtime > cssStats.mtime) { debug('modified %s', cssPath); compile(); // Already compiled, check imports } else { checkImports(stylusPath, function(changed){ if (debug && changed.length) { changed.forEach(function(path) { debug('modified import %s', path); }); } changed.length ? compile() : next(); }); } } }); }); } else { next(); } } }; /** * Check `path`'s imports to see if they have been altered. * * @param {String} path * @param {Function} fn * @api private */ function checkImports(path, fn) { var nodes = imports[path]; if (!nodes) return fn(); if (!nodes.length) return fn(); var pending = nodes.length , changed = []; nodes.forEach(function(imported){ fs.stat(imported.path, function(err, stat){ // error or newer mtime if (err || !imported.mtime || stat.mtime > imported.mtime) { changed.push(imported.path); } --pending || fn(changed); }); }); } /** * get the overlaping path from the end of path A, and the begining of path B. * * @param {String} pathA * @param {String} pathB * @return {String} * @api private */ function compare(pathA, pathB) { pathA = pathA.split(sep); pathB = pathB.split('/'); if (!pathA[pathA.length - 1]) pathA.pop(); if (!pathB[0]) pathB.shift(); var overlap = []; while (pathA[pathA.length - 1] == pathB[0]) { overlap.push(pathA.pop()); pathB.shift(); } return overlap.join('/'); }