321 lines
9.3 KiB
JavaScript
321 lines
9.3 KiB
JavaScript
"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) {
|
|
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();
|
|
}
|
|
};
|
|
};
|