middleware.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. /*!
  2. * Stylus - middleware
  3. * Copyright (c) Automattic <developer.wordpress.com>
  4. * MIT Licensed
  5. */
  6. /**
  7. * Module dependencies.
  8. */
  9. var stylus = require('./stylus')
  10. , fs = require('fs')
  11. , url = require('url')
  12. , dirname = require('path').dirname
  13. , join = require('path').join
  14. , sep = require('path').sep
  15. , debug = require('debug')('stylus:middleware')
  16. , mkdir = fs.mkdir;
  17. /**
  18. * Import map.
  19. */
  20. var imports = {};
  21. /**
  22. * Return Connect middleware with the given `options`.
  23. *
  24. * Options:
  25. *
  26. * `force` Always re-compile
  27. * `src` Source directory used to find .styl files,
  28. * a string or function accepting `(path)` of request.
  29. * `dest` Destination directory used to output .css files,
  30. * a string or function accepting `(path)` of request,
  31. * when undefined defaults to `src`.
  32. * `compile` Custom compile function, accepting the arguments
  33. * `(str, path)`.
  34. * `compress` Whether the output .css files should be compressed
  35. * `firebug` Emits debug infos in the generated CSS that can
  36. * be used by the FireStylus Firebug plugin
  37. * `linenos` Emits comments in the generated CSS indicating
  38. * the corresponding Stylus line
  39. * 'sourcemap' Generates a sourcemap in sourcemaps v3 format
  40. *
  41. * Examples:
  42. *
  43. * Here we set up the custom compile function so that we may
  44. * set the `compress` option, or define additional functions.
  45. *
  46. * By default the compile function simply sets the `filename`
  47. * and renders the CSS.
  48. *
  49. * function compile(str, path) {
  50. * return stylus(str)
  51. * .set('filename', path)
  52. * .set('compress', true);
  53. * }
  54. *
  55. * Pass the middleware to Connect, grabbing .styl files from this directory
  56. * and saving .css files to _./public_. Also supplying our custom `compile` function.
  57. *
  58. * Following that we have a `static()` layer setup to serve the .css
  59. * files generated by Stylus.
  60. *
  61. * var app = connect();
  62. *
  63. * app.middleware({
  64. * src: __dirname
  65. * , dest: __dirname + '/public'
  66. * , compile: compile
  67. * })
  68. *
  69. * app.use(connect.static(__dirname + '/public'));
  70. *
  71. * @param {Object} options
  72. * @return {Function}
  73. * @api public
  74. */
  75. module.exports = function(options){
  76. options = options || {};
  77. // Accept src/dest dir
  78. if ('string' == typeof options) {
  79. options = { src: options };
  80. }
  81. // Force compilation
  82. var force = options.force;
  83. // Source dir required
  84. var src = options.src;
  85. if (!src) throw new Error('stylus.middleware() requires "src" directory');
  86. // Default dest dir to source
  87. var dest = options.dest || src;
  88. // Default compile callback
  89. options.compile = options.compile || function(str, path){
  90. // inline sourcemap
  91. if (options.sourcemap) {
  92. if ('boolean' == typeof options.sourcemap)
  93. options.sourcemap = {};
  94. options.sourcemap.inline = true;
  95. }
  96. return stylus(str)
  97. .set('filename', path)
  98. .set('compress', options.compress)
  99. .set('firebug', options.firebug)
  100. .set('linenos', options.linenos)
  101. .set('sourcemap', options.sourcemap);
  102. };
  103. // Middleware
  104. return function stylus(req, res, next){
  105. if ('GET' != req.method && 'HEAD' != req.method) return next();
  106. var path = url.parse(req.url).pathname;
  107. if (/\.css$/.test(path)) {
  108. if (typeof dest == 'string') {
  109. // check for dest-path overlap
  110. var overlap = compare(dest, path).length;
  111. if ('/' == path.charAt(0)) overlap++;
  112. path = path.slice(overlap);
  113. }
  114. var cssPath, stylusPath;
  115. cssPath = (typeof dest == 'function')
  116. ? dest(path)
  117. : join(dest, path);
  118. stylusPath = (typeof src == 'function')
  119. ? src(path)
  120. : join(src, path.replace('.css', '.styl'));
  121. // Ignore ENOENT to fall through as 404
  122. function error(err) {
  123. next('ENOENT' == err.code
  124. ? null
  125. : err);
  126. }
  127. // Force
  128. if (force) return compile();
  129. // Compile to cssPath
  130. function compile() {
  131. debug('read %s', cssPath);
  132. fs.readFile(stylusPath, 'utf8', function(err, str){
  133. if (err) return error(err);
  134. var style = options.compile(str, stylusPath);
  135. var paths = style.options._imports = [];
  136. imports[stylusPath] = null;
  137. style.render(function(err, css){
  138. if (err) return next(err);
  139. debug('render %s', stylusPath);
  140. imports[stylusPath] = paths;
  141. mkdir(dirname(cssPath), { mode: parseInt('0700', 8), recursive: true }, function(err){
  142. if (err) return error(err);
  143. fs.writeFile(cssPath, css, 'utf8', next);
  144. });
  145. });
  146. });
  147. }
  148. // Re-compile on server restart, disregarding
  149. // mtimes since we need to map imports
  150. if (!imports[stylusPath]) return compile();
  151. // Compare mtimes
  152. fs.stat(stylusPath, function(err, stylusStats){
  153. if (err) return error(err);
  154. fs.stat(cssPath, function(err, cssStats){
  155. // CSS has not been compiled, compile it!
  156. if (err) {
  157. if ('ENOENT' == err.code) {
  158. debug('not found %s', cssPath);
  159. compile();
  160. } else {
  161. next(err);
  162. }
  163. } else {
  164. // Source has changed, compile it
  165. if (stylusStats.mtime > cssStats.mtime) {
  166. debug('modified %s', cssPath);
  167. compile();
  168. // Already compiled, check imports
  169. } else {
  170. checkImports(stylusPath, function(changed){
  171. if (debug && changed.length) {
  172. changed.forEach(function(path) {
  173. debug('modified import %s', path);
  174. });
  175. }
  176. changed.length ? compile() : next();
  177. });
  178. }
  179. }
  180. });
  181. });
  182. } else {
  183. next();
  184. }
  185. }
  186. };
  187. /**
  188. * Check `path`'s imports to see if they have been altered.
  189. *
  190. * @param {String} path
  191. * @param {Function} fn
  192. * @api private
  193. */
  194. function checkImports(path, fn) {
  195. var nodes = imports[path];
  196. if (!nodes) return fn();
  197. if (!nodes.length) return fn();
  198. var pending = nodes.length
  199. , changed = [];
  200. nodes.forEach(function(imported){
  201. fs.stat(imported.path, function(err, stat){
  202. // error or newer mtime
  203. if (err || !imported.mtime || stat.mtime > imported.mtime) {
  204. changed.push(imported.path);
  205. }
  206. --pending || fn(changed);
  207. });
  208. });
  209. }
  210. /**
  211. * get the overlaping path from the end of path A, and the begining of path B.
  212. *
  213. * @param {String} pathA
  214. * @param {String} pathB
  215. * @return {String}
  216. * @api private
  217. */
  218. function compare(pathA, pathB) {
  219. pathA = pathA.split(sep);
  220. pathB = pathB.split('/');
  221. if (!pathA[pathA.length - 1]) pathA.pop();
  222. if (!pathB[0]) pathB.shift();
  223. var overlap = [];
  224. while (pathA[pathA.length - 1] == pathB[0]) {
  225. overlap.push(pathA.pop());
  226. pathB.shift();
  227. }
  228. return overlap.join('/');
  229. }