UNPKG

9.49 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));
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, 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, function(err) {
133 if (err) throw err;
134 });
135 });
136 }
137 }, options || {});
138
139 // The log function is determined by the debug option.
140 var log = (options.debug ? utilities.logDebug : utilities.log);
141
142 if (options.cacheFile && !cacheFileInitialized) {
143 initCacheFile(options.cacheFile, log);
144 }
145
146 // Expose for testing.
147 less.middleware._saveCacheToFile = _saveCacheToFile;
148
149 // Actual middleware.
150 return function(req, res, next) {
151 if ('GET' != req.method.toUpperCase() && 'HEAD' != req.method.toUpperCase()) { return next(); }
152
153 var pathname = url.parse(req.url).pathname;
154
155 // Only handle the matching files in this middleware.
156 if (utilities.isValidPath(pathname)) {
157 var isSourceMap = utilities.isSourceMap(pathname);
158
159 // Translate source maps to a normal .css request which will update the associated source-map.
160 if( isSourceMap ){
161 pathname = pathname.replace( /\.map$/, '' );
162 }
163 var lessPath = path.join(source, utilities.maybeCompressedSource(pathname));
164 var cssPath = path.join(options.dest, pathname);
165
166 if (options.pathRoot) {
167 pathname = pathname.replace(options.dest, '');
168 cssPath = path.join(options.pathRoot, options.dest, pathname);
169 lessPath = path.join(options.pathRoot, source, utilities.maybeCompressedSource(pathname));
170 }
171
172 var sourcemapPath = cssPath + '.map';
173
174 // Allow for preprocessing the source filename.
175 lessPath = options.preprocess.path(lessPath, req);
176
177 log('pathname', pathname);
178 log('source', lessPath);
179 log('destination', cssPath);
180
181 // Ignore ENOENT to fall through as 404.
182 var error = function(err) {
183 return next('ENOENT' == err.code ? null : err);
184 };
185
186 var compile = function() {
187 fs.readFile(lessPath, 'utf8', function(err, lessSrc){
188 if (err) {
189 return error(err);
190 }
191
192 delete lessFiles[lessPath];
193
194 try {
195 var renderOptions = extend(true, {}, options.render, {
196 filename: lessPath,
197 paths: options.preprocess.importPaths(options.render.paths, req)
198 });
199 lessSrc = options.preprocess.less(lessSrc, req);
200
201 less.render(lessSrc, renderOptions, function(err, output){
202 if (err) {
203 utilities.lessError(err);
204 return next(err);
205 }
206
207 // Determine the imports used and check modified times.
208 var imports = [];
209 output.imports.forEach(function(imported) {
210 var currentImport = {
211 path: imported,
212 mtime: null
213 };
214
215 imports.push(currentImport);
216
217 // Update the mtime of the import async.
218 fs.stat(imported, function(err, lessStats){
219 if (err) {
220 return error(err);
221 }
222
223 currentImport.mtime = lessStats.mtime;
224 });
225 });
226
227 // Store the less paths for simple cache invalidation.
228 lessFiles[lessPath] = {
229 mtime: Date.now(),
230 imports: imports
231 };
232
233 if(output.map) {
234 // Postprocessing on the sourcemap.
235 var map = options.postprocess.sourcemap(output.map, req);
236
237 // Custom sourcemap storage.
238 options.storeSourcemap(sourcemapPath, map, req);
239 }
240
241 // Postprocessing on the css.
242 var css = options.postprocess.css(output.css, req);
243
244 // Custom css storage.
245 options.storeCss(cssPath, css, req, next);
246 });
247 } catch (err) {
248 utilities.lessError(err);
249 return next(err);
250 }
251 });
252 };
253
254 // Force recompile of all files.
255 if (options.force) {
256 return compile();
257 }
258
259 // Only compile once, disregarding the file changes.
260 if (options.once && lessFiles[lessPath]) {
261 return next();
262 }
263
264 // Compile on (uncached) server restart and new files.
265 if (!lessFiles[lessPath]) {
266 return compile();
267 }
268
269 // Compare mtimes to determine if changed.
270 fs.stat(lessPath, function(err, lessStats){
271 if (err) {
272 return error(err);
273 }
274
275 fs.stat(cssPath, function(err, cssStats){
276 // CSS has not been compiled, compile it!
277 if (err) {
278 if ('ENOENT' == err.code) {
279 log('not found', cssPath);
280
281 // No CSS file found in dest
282 return compile();
283 }
284
285 return next(err);
286 }
287
288 if (lessStats.mtime > cssStats.mtime) {
289 // Source has changed, compile it
290 log('modified', cssPath);
291
292 return compile();
293 } else if (lessStats.mtime > lessFiles[lessPath].mtime) {
294 // This can happen if lessFiles[lessPath] was copied from
295 // cacheFile above, but the cache file was out of date (which
296 // can happen e.g. if node is killed and we were unable to write out
297 // lessFiles on exit). Since imports might have changed, we need to
298 // recompile.
299 log('cache file out of date for', lessPath);
300
301 return compile();
302 } else {
303 // Check if any of the less imports were changed
304 checkImports(lessPath, function(changed){
305 if(typeof changed != "undefined" && changed.length) {
306 log('modified import', changed);
307
308 return compile();
309 }
310
311 return next();
312 });
313 }
314 });
315 });
316 } else {
317 return next();
318 }
319 };
320};