1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | var extend = require('node.extend');
|
11 | var fs = require('fs');
|
12 | var less = require('less');
|
13 |
|
14 |
|
15 | var path = require('path');
|
16 | var url = require('url');
|
17 | var utilities = require('./utilities');
|
18 |
|
19 |
|
20 | var lessFiles = {};
|
21 | var cacheFileInitialized = false;
|
22 |
|
23 | var _saveCacheToFile = function() {};
|
24 |
|
25 |
|
26 | var 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 |
|
39 | if (err || !imported.mtime || stat.mtime > imported.mtime) {
|
40 | changed.push(imported.path);
|
41 | }
|
42 |
|
43 | --pending || next(changed);
|
44 | });
|
45 | });
|
46 | };
|
47 |
|
48 | var initCacheFile = function(cacheFile, log) {
|
49 | cacheFileInitialized = true;
|
50 | var cacheFileSaved = false;
|
51 | _saveCacheToFile = function() {
|
52 | if (cacheFileSaved) {
|
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() {
|
67 | _saveCacheToFile();
|
68 | process.kill(process.pid, 'SIGUSR2');
|
69 | });
|
70 | process.once('SIGINT', function() {
|
71 | _saveCacheToFile();
|
72 | process.kill(process.pid, 'SIGINT');
|
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 |
|
90 |
|
91 | module.exports = less.middleware = function(source, options){
|
92 |
|
93 | if (!source) {
|
94 | throw new Error('less.middleware() requires `source` directory');
|
95 | }
|
96 |
|
97 |
|
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 , 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 , 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 |
|
139 | var log = (options.debug ? utilities.logDebug : utilities.log);
|
140 |
|
141 | if (options.cacheFile && !cacheFileInitialized) {
|
142 | initCacheFile(options.cacheFile, log);
|
143 | }
|
144 |
|
145 |
|
146 | less.middleware._saveCacheToFile = _saveCacheToFile;
|
147 |
|
148 |
|
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 |
|
155 | if (utilities.isValidPath(pathname)) {
|
156 | var isSourceMap = utilities.isSourceMap(pathname);
|
157 |
|
158 |
|
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 |
|
174 | lessPath = options.preprocess.path(lessPath, req);
|
175 |
|
176 | log('pathname', pathname);
|
177 | log('source', lessPath);
|
178 | log('destination', cssPath);
|
179 |
|
180 |
|
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 |
|
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 |
|
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 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 | if(output.map) {
|
234 |
|
235 | var map = options.postprocess.sourcemap(output.map, req);
|
236 |
|
237 |
|
238 | options.storeSourcemap(sourcemapPath, map, req);
|
239 | }
|
240 |
|
241 |
|
242 | var css = options.postprocess.css(output.css, req);
|
243 |
|
244 |
|
245 |
|
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 |
|
256 | if (options.force) {
|
257 | return compile();
|
258 | }
|
259 |
|
260 |
|
261 | if (options.once && lessFiles[lessPath]) {
|
262 | return next();
|
263 | }
|
264 |
|
265 |
|
266 | if (!lessFiles[lessPath]) {
|
267 | return compile();
|
268 | }
|
269 |
|
270 |
|
271 | fs.stat(lessPath, function(err, lessStats){
|
272 | if (err) {
|
273 | return error(err);
|
274 | }
|
275 |
|
276 | fs.stat(cssPath, function(err, cssStats){
|
277 |
|
278 | if (err) {
|
279 | if ('ENOENT' == err.code) {
|
280 | log('not found', cssPath);
|
281 |
|
282 |
|
283 | return compile();
|
284 | }
|
285 |
|
286 | return next(err);
|
287 | }
|
288 |
|
289 | if (lessStats.mtime > cssStats.mtime) {
|
290 |
|
291 | log('modified', cssPath);
|
292 |
|
293 | return compile();
|
294 | } else if (lessStats.mtime > lessFiles[lessPath].mtime) {
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 | log('cache file out of date for', lessPath);
|
301 |
|
302 | return compile();
|
303 | } else {
|
304 |
|
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 | };
|