123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- /*!
- * Stylus - middleware
- * Copyright (c) Automattic <developer.wordpress.com>
- * MIT Licensed
- */
- /**
- * Module dependencies.
- */
- var stylus = require('./stylus')
- , fs = require('fs')
- , url = require('url')
- , dirname = require('path').dirname
- , join = require('path').join
- , sep = require('path').sep
- , debug = require('debug')('stylus:middleware')
- , mkdir = fs.mkdir;
- /**
- * 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;
- mkdir(dirname(cssPath), { mode: parseInt('0700', 8), recursive: true }, 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('/');
- }
|