middleware.js 7.0 KB

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