UNPKG

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