UNPKG

9.46 kBJavaScriptView Raw
1"use strict";
2
3/*!
4 * Less - middleware (adapted from the stylus middleware)
5 *
6 * Copyright(c) 2014 Randy Merrill <Zoramite+github@gmail.com>
7 * MIT Licensed
8 */
9
10var extend = require('node.extend');
11var fs = require('fs');
12var less = require('less');
13var mkdirp = require('mkdirp');
14var path = require('path');
15var url = require('url');
16var utilities = require('./utilities');
17
18// Import mapping with mtimes
19var lessFiles = {};
20var cacheFileInitialized = false;
21// Allow tests to force flushing of cacheFile
22var _saveCacheToFile = function() {};
23
24// Check imports for changes.
25var checkImports = function(path, next) {
26 var nodes = lessFiles[path].imports;
27
28 if (!nodes || !nodes.length) {
29 return next();
30 }
31
32 var pending = nodes.length;
33 var changed = [];
34
35 nodes.forEach(function(imported){
36 fs.stat(imported.path, function(err, stat) {
37 // error or newer mtime
38 if (err || !imported.mtime || stat.mtime > imported.mtime) {
39 changed.push(imported.path);
40 }
41
42 --pending || next(changed);
43 });
44 });
45};
46
47var initCacheFile = function(cacheFile, log) {
48 cacheFileInitialized = true;
49 var cacheFileSaved = false;
50 _saveCacheToFile = function() {
51 if (cacheFileSaved) { // We expect to only save to the cache file once, just before exiting
52 log('cache file already appears to be saved, not saving again to', cacheFile);
53 return;
54 } else {
55 cacheFileSaved = true;
56 try {
57 fs.writeFileSync(cacheFile, JSON.stringify(lessFiles), 'utf8');
58 log('successfully cached imports to file', cacheFile);
59 } catch (err) {
60 log('error caching imports to file ' + cacheFile, err);
61 }
62 }
63 };
64 process.on('exit', _saveCacheToFile);
65 process.once('SIGUSR2', function() { // Handle nodemon restarts
66 _saveCacheToFile();
67 process.kill(process.pid, 'SIGUSR2');
68 });
69 process.once('SIGINT', function() {
70 _saveCacheToFile();
71 process.kill(process.pid, 'SIGINT'); // Let other SIGINT handlers run, if there are any
72 });
73
74 fs.readFile(cacheFile, 'utf8', function(err, data) {
75 if (!err) {
76 try {
77 lessFiles = extend(JSON.parse(data), lessFiles);
78 } catch (err) {
79 log('error parsing cached imports in file ' + cacheFile, err);
80 }
81 } else {
82 log('error loading cached imports file ' + cacheFile, err);
83 }
84 });
85}
86
87/**
88 * Return Connect middleware with the given `options`.
89 */
90module.exports = less.middleware = function(source, options){
91 // Source dir is required.
92 if (!source) {
93 throw new Error('less.middleware() requires `source` directory');
94 }
95
96 // Override the defaults for the middleware.
97 options = extend(true, {
98 cacheFile: null,
99 debug: false,
100 dest: source,
101 force: false,
102 once: false,
103 pathRoot: null,
104 postprocess: {
105 css: function(css, req) { return css; },
106 sourcemap: function(sourcemap, req) { return sourcemap; }
107 },
108 preprocess: {
109 less: function(src, req) { return src; },
110 path: function(pathname, req) { return pathname; },
111 importPaths: function(paths, req) { return paths; }
112 },
113 render: {
114 compress: 'auto',
115 yuicompress: false,
116 paths: []
117 },
118 storeCss: function(pathname, css, req, next) {
119 mkdirp(path.dirname(pathname), 511 /* 0777 */, function(err){
120 if (err) return next(err);
121
122 fs.writeFile(pathname, css, 'utf8', next);
123 });
124 },
125 storeSourcemap: function(pathname, sourcemap, req) {
126 mkdirp(path.dirname(pathname), 511 /* 0777 */, function(err){
127 if (err) {
128 utilities.lessError(err);
129 return;
130 }
131
132 fs.writeFile(pathname, sourcemap, 'utf8');
133 });
134 }
135 }, options || {});
136
137 // The log function is determined by the debug option.
138 var log = (options.debug ? utilities.logDebug : utilities.log);
139
140 if (options.cacheFile && !cacheFileInitialized) {
141 initCacheFile(options.cacheFile, log);
142 }
143
144 // Expose for testing.
145 less.middleware._saveCacheToFile = _saveCacheToFile;
146
147 // Actual middleware.
148 return function(req, res, next) {
149 if ('GET' != req.method.toUpperCase() && 'HEAD' != req.method.toUpperCase()) { return next(); }
150
151 var pathname = url.parse(req.url).pathname;
152
153 // Only handle the matching files in this middleware.
154 if (utilities.isValidPath(pathname)) {
155 var isSourceMap = utilities.isSourceMap(pathname);
156
157 // Translate source maps to a normal .css request which will update the associated source-map.
158 if( isSourceMap ){
159 pathname = pathname.replace( /\.map$/, '' );
160 }
161 var lessPath = path.join(source, utilities.maybeCompressedSource(pathname));
162 var cssPath = path.join(options.dest, pathname);
163
164 if (options.pathRoot) {
165 pathname = pathname.replace(options.dest, '');
166 cssPath = path.join(options.pathRoot, options.dest, pathname);
167 lessPath = path.join(options.pathRoot, source, utilities.maybeCompressedSource(pathname));
168 }
169
170 var sourcemapPath = cssPath + '.map';
171
172 // Allow for preprocessing the source filename.
173 lessPath = options.preprocess.path(lessPath, req);
174
175 log('pathname', pathname);
176 log('source', lessPath);
177 log('destination', cssPath);
178
179 // Ignore ENOENT to fall through as 404.
180 var error = function(err) {
181 return next('ENOENT' == err.code ? null : err);
182 };
183
184 var compile = function() {
185 fs.readFile(lessPath, 'utf8', function(err, lessSrc){
186 if (err) {
187 return error(err);
188 }
189
190 delete lessFiles[lessPath];
191
192 try {
193 var renderOptions = extend(true, {}, options.render, {
194 filename: lessPath,
195 paths: options.preprocess.importPaths(options.render.paths, req)
196 });
197 lessSrc = options.preprocess.less(lessSrc, req);
198
199 less.render(lessSrc, renderOptions, function(err, output){
200 if (err) {
201 utilities.lessError(err);
202 return next(err);
203 }
204
205 // Determine the imports used and check modified times.
206 var imports = [];
207 output.imports.forEach(function(imported) {
208 var currentImport = {
209 path: imported,
210 mtime: null
211 };
212
213 imports.push(currentImport);
214
215 // Update the mtime of the import async.
216 fs.stat(imported, function(err, lessStats){
217 if (err) {
218 return error(err);
219 }
220
221 currentImport.mtime = lessStats.mtime;
222 });
223 });
224
225 // Store the less paths for simple cache invalidation.
226 lessFiles[lessPath] = {
227 mtime: Date.now(),
228 imports: imports
229 };
230
231 if(output.map) {
232 // Postprocessing on the sourcemap.
233 var map = options.postprocess.sourcemap(output.map, req);
234
235 // Custom sourcemap storage.
236 options.storeSourcemap(sourcemapPath, map, req);
237 }
238
239 // Postprocessing on the css.
240 var css = options.postprocess.css(output.css, req);
241
242 // Custom css storage.
243 options.storeCss(cssPath, css, req, next);
244 });
245 } catch (err) {
246 utilities.lessError(err);
247 return next(err);
248 }
249 });
250 };
251
252 // Force recompile of all files.
253 if (options.force) {
254 return compile();
255 }
256
257 // Only compile once, disregarding the file changes.
258 if (options.once && lessFiles[lessPath]) {
259 return next();
260 }
261
262 // Compile on (uncached) server restart and new files.
263 if (!lessFiles[lessPath]) {
264 return compile();
265 }
266
267 // Compare mtimes to determine if changed.
268 fs.stat(lessPath, function(err, lessStats){
269 if (err) {
270 return error(err);
271 }
272
273 fs.stat(cssPath, function(err, cssStats){
274 // CSS has not been compiled, compile it!
275 if (err) {
276 if ('ENOENT' == err.code) {
277 log('not found', cssPath);
278
279 // No CSS file found in dest
280 return compile();
281 }
282
283 return next(err);
284 }
285
286 if (lessStats.mtime > cssStats.mtime) {
287 // Source has changed, compile it
288 log('modified', cssPath);
289
290 return compile();
291 } else if (lessStats.mtime > lessFiles[lessPath].mtime) {
292 // This can happen if lessFiles[lessPath] was copied from
293 // cacheFile above, but the cache file was out of date (which
294 // can happen e.g. if node is killed and we were unable to write out
295 // lessFiles on exit). Since imports might have changed, we need to
296 // recompile.
297 log('cache file out of date for', lessPath);
298
299 return compile();
300 } else {
301 // Check if any of the less imports were changed
302 checkImports(lessPath, function(changed){
303 if(typeof changed != "undefined" && changed.length) {
304 log('modified import', changed);
305
306 return compile();
307 }
308
309 return next();
310 });
311 }
312 });
313 });
314 } else {
315 return next();
316 }
317 };
318};