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), '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() {
|
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, 'utf8', 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, 'utf8');
|
133 | });
|
134 | }
|
135 | }, options || {});
|
136 |
|
137 |
|
138 | var log = (options.debug ? utilities.logDebug : utilities.log);
|
139 |
|
140 | if (options.cacheFile && !cacheFileInitialized) {
|
141 | initCacheFile(options.cacheFile, log);
|
142 | }
|
143 |
|
144 |
|
145 | less.middleware._saveCacheToFile = _saveCacheToFile;
|
146 |
|
147 |
|
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 |
|
154 | if (utilities.isValidPath(pathname)) {
|
155 | var isSourceMap = utilities.isSourceMap(pathname);
|
156 |
|
157 |
|
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 |
|
173 | lessPath = options.preprocess.path(lessPath, req);
|
174 |
|
175 | log('pathname', pathname);
|
176 | log('source', lessPath);
|
177 | log('destination', cssPath);
|
178 |
|
179 |
|
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 |
|
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 |
|
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 |
|
226 | lessFiles[lessPath] = {
|
227 | mtime: Date.now(),
|
228 | imports: imports
|
229 | };
|
230 |
|
231 | if(output.map) {
|
232 |
|
233 | var map = options.postprocess.sourcemap(output.map, req);
|
234 |
|
235 |
|
236 | options.storeSourcemap(sourcemapPath, map, req);
|
237 | }
|
238 |
|
239 |
|
240 | var css = options.postprocess.css(output.css, req);
|
241 |
|
242 |
|
243 | options.storeCss(cssPath, css, req, next);
|
244 | });
|
245 | } catch (err) {
|
246 | utilities.lessError(err);
|
247 | return next(err);
|
248 | }
|
249 | });
|
250 | };
|
251 |
|
252 |
|
253 | if (options.force) {
|
254 | return compile();
|
255 | }
|
256 |
|
257 |
|
258 | if (options.once && lessFiles[lessPath]) {
|
259 | return next();
|
260 | }
|
261 |
|
262 |
|
263 | if (!lessFiles[lessPath]) {
|
264 | return compile();
|
265 | }
|
266 |
|
267 |
|
268 | fs.stat(lessPath, function(err, lessStats){
|
269 | if (err) {
|
270 | return error(err);
|
271 | }
|
272 |
|
273 | fs.stat(cssPath, function(err, cssStats){
|
274 |
|
275 | if (err) {
|
276 | if ('ENOENT' == err.code) {
|
277 | log('not found', cssPath);
|
278 |
|
279 |
|
280 | return compile();
|
281 | }
|
282 |
|
283 | return next(err);
|
284 | }
|
285 |
|
286 | if (lessStats.mtime > cssStats.mtime) {
|
287 |
|
288 | log('modified', cssPath);
|
289 |
|
290 | return compile();
|
291 | } else if (lessStats.mtime > lessFiles[lessPath].mtime) {
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 | log('cache file out of date for', lessPath);
|
298 |
|
299 | return compile();
|
300 | } else {
|
301 |
|
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 | };
|