You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
256 lines
6.9 KiB
256 lines
6.9 KiB
/*! |
|
* 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 |
|
, 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('/'); |
|
}
|
|
|