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