UNPKG

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