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 | var mkdirp = require('mkdirp');
|
14 | var path = require('path');
|
15 | var url = require('url');
|
16 | var utilities = require('./utilities');
|
17 |
|
18 |
|
19 | var lessFiles = {};
|
20 | var cacheFileInitialized = false;
|
21 |
|
22 | var _saveCacheToFile = function() {};
|
23 |
|
24 |
|
25 | var 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 |
|
38 | if (err || !imported.mtime || stat.mtime > imported.mtime) {
|
39 | changed.push(imported.path);
|
40 | }
|
41 |
|
42 | --pending || next(changed);
|
43 | });
|
44 | });
|
45 | };
|
46 |
|
47 | var initCacheFile = function(cacheFile, log) {
|
48 | cacheFileInitialized = true;
|
49 | var cacheFileSaved = false;
|
50 | _saveCacheToFile = function() {
|
51 | if (cacheFileSaved) {
|
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() {
|
66 | _saveCacheToFile();
|
67 | process.kill(process.pid, 'SIGUSR2');
|
68 | });
|
69 | process.once('SIGINT', function() {
|
70 | _saveCacheToFile();
|
71 | process.kill(process.pid, 'SIGINT');
|
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 |
|
89 |
|
90 | module.exports = less.middleware = function(source, options){
|
91 |
|
92 | if (!source) {
|
93 | throw new Error('less.middleware() requires `source` directory');
|
94 | }
|
95 |
|
96 |
|
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 , 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 , 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 |
|
140 | var log = (options.debug ? utilities.logDebug : utilities.log);
|
141 |
|
142 | if (options.cacheFile && !cacheFileInitialized) {
|
143 | initCacheFile(options.cacheFile, log);
|
144 | }
|
145 |
|
146 |
|
147 | less.middleware._saveCacheToFile = _saveCacheToFile;
|
148 |
|
149 |
|
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 |
|
156 | if (utilities.isValidPath(pathname)) {
|
157 | var isSourceMap = utilities.isSourceMap(pathname);
|
158 |
|
159 |
|
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 |
|
175 | lessPath = options.preprocess.path(lessPath, req);
|
176 |
|
177 | log('pathname', pathname);
|
178 | log('source', lessPath);
|
179 | log('destination', cssPath);
|
180 |
|
181 |
|
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 |
|
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 |
|
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 |
|
228 | lessFiles[lessPath] = {
|
229 | mtime: Date.now(),
|
230 | imports: imports
|
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 | options.storeCss(cssPath, css, req, next);
|
246 | });
|
247 | } catch (err) {
|
248 | utilities.lessError(err);
|
249 | return next(err);
|
250 | }
|
251 | });
|
252 | };
|
253 |
|
254 |
|
255 | if (options.force) {
|
256 | return compile();
|
257 | }
|
258 |
|
259 |
|
260 | if (options.once && lessFiles[lessPath]) {
|
261 | return next();
|
262 | }
|
263 |
|
264 |
|
265 | if (!lessFiles[lessPath]) {
|
266 | return compile();
|
267 | }
|
268 |
|
269 |
|
270 | fs.stat(lessPath, function(err, lessStats){
|
271 | if (err) {
|
272 | return error(err);
|
273 | }
|
274 |
|
275 | fs.stat(cssPath, function(err, cssStats){
|
276 |
|
277 | if (err) {
|
278 | if ('ENOENT' == err.code) {
|
279 | log('not found', cssPath);
|
280 |
|
281 |
|
282 | return compile();
|
283 | }
|
284 |
|
285 | return next(err);
|
286 | }
|
287 |
|
288 | if (lessStats.mtime > cssStats.mtime) {
|
289 |
|
290 | log('modified', cssPath);
|
291 |
|
292 | return compile();
|
293 | } else if (lessStats.mtime > lessFiles[lessPath].mtime) {
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 | log('cache file out of date for', lessPath);
|
300 |
|
301 | return compile();
|
302 | } else {
|
303 |
|
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 | };
|