UNPKG

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