UNPKG

6.62 kBJavaScriptView Raw
1"use strict";
2
3/*!
4 * Less - middleware (adapted from the stylus middleware)
5 *
6 * Copyright(c) 2014 Randy Merrill <Zoramite+github@gmail.com>
7 * MIT Licensed
8 */
9
10var determine_imports = require('./determine-imports');
11var extend = require('node.extend');
12var fs = require('fs');
13var less = require('less');
14var mkdirp = require('mkdirp');
15var path = require('path');
16var url = require('url');
17var utilities = require('./utilities');
18
19// Import mapping.
20var imports = {};
21
22// Check imports for changes.
23var checkImports = function(path, next) {
24 var nodes = imports[path];
25
26 if (!nodes || !nodes.length) {
27 return next();
28 }
29
30 var pending = nodes.length;
31 var changed = [];
32
33 nodes.forEach(function(imported){
34 fs.stat(imported.path, function(err, stat) {
35 // error or newer mtime
36 if (err || !imported.mtime || stat.mtime > imported.mtime) {
37 changed.push(imported.path);
38 }
39
40 --pending || next(changed);
41 });
42 });
43};
44
45/**
46 * Return Connect middleware with the given `options`.
47 */
48module.exports = less.middleware = function(source, options, parserOptions, compilerOptions){
49 // Check for 0.1.x usage.
50 if (typeof source == 'object') {
51 throw new Error('Please update your less-middleware usage: http://goo.gl/YnK8p0');
52 }
53
54 // Source dir is required.
55 if (!source) {
56 throw new Error('less.middleware() requires `source` directory');
57 }
58
59 // Override the defaults for the middleware.
60 options = extend(true, {
61 debug: false,
62 dest: source,
63 force: false,
64 once: false,
65 pathRoot: null,
66 postprocess: {
67 css: function(css, req) { return css; }
68 },
69 preprocess: {
70 less: function(src, req) { return src; },
71 path: function(pathname, req) { return pathname; }
72 },
73 storeCss: function(pathname, css, next) {
74 mkdirp(path.dirname(pathname), 511 /* 0777 */, function(err){
75 if (err) return next(err);
76
77 fs.writeFile(pathname, css, 'utf8', next);
78 });
79 }
80 }, options || {});
81
82 // Override the defaults for the parser.
83 parserOptions = extend(true, {
84 dumpLineNumbers: 0,
85 paths: [source],
86 optimization: 0,
87 relativeUrls: false
88 }, parserOptions || {});
89
90 // Override the defaults for the compiler.
91 compilerOptions = extend(true, {
92 compress: 'auto',
93 sourceMap: false,
94 yuicompress: false
95 }, compilerOptions || {});
96
97 // The log function is determined by the debug option.
98 var log = (options.debug ? utilities.logDebug : utilities.log);
99
100 // Parse and compile the CSS from the source string.
101 var render = function(str, lessPath, cssPath, callback) {
102 var parser = new less.Parser(extend({}, parserOptions, {
103 filename: lessPath
104 }));
105
106 parser.parse(str, function(err, tree) {
107 if(err) {
108 return callback(err);
109 }
110
111 try {
112 var css = tree.toCSS(extend({}, compilerOptions, {
113 compress: (options.compress == 'auto' ? utilities.isCompressedPath(cssPath) : options.compress)
114 }));
115
116 // Store the less import paths for cache invalidation.
117 imports[lessPath] = determine_imports(tree, lessPath, parserOptions.paths);
118
119 callback(err, css);
120 } catch(parseError) {
121 callback(parseError, null);
122 }
123 });
124 };
125
126 // Actual middleware.
127 return function(req, res, next) {
128 if ('GET' != req.method.toUpperCase() && 'HEAD' != req.method.toUpperCase()) { return next(); }
129
130 var pathname = url.parse(req.url).pathname;
131
132 // Only handle the matching files in this middleware.
133 if (utilities.isValidPath(pathname)) {
134 var cssPath = path.join(options.dest, pathname);
135 var lessPath = path.join(source, utilities.maybeCompressedSource(pathname));
136
137 if (options.pathRoot) {
138 pathname = pathname.replace(options.dest, '');
139 cssPath = path.join(options.pathRoot, options.dest, pathname);
140 lessPath = path.join(options.pathRoot, source, utilities.maybeCompressedSource(pathname));
141 }
142
143 // Allow for preprocessing the source filename.
144 lessPath = options.preprocess.path(lessPath, req);
145
146 log('pathname', pathname);
147 log('source', lessPath);
148 log('destination', cssPath);
149
150 // Ignore ENOENT to fall through as 404.
151 var error = function(err) {
152 return next('ENOENT' == err.code ? null : err);
153 };
154
155 var compile = function() {
156 fs.readFile(lessPath, 'utf8', function(err, lessSrc){
157 if (err) {
158 return error(err);
159 }
160
161 delete imports[lessPath];
162
163 try {
164 lessSrc = options.preprocess.less(lessSrc, req);
165 render(lessSrc, lessPath, cssPath, function(err, css){
166 if (err) {
167 utilities.lessError(err);
168 return next(err);
169 }
170
171 // Allow postprocessing on the css.
172 css = options.postprocess.css(css, req);
173
174 // Allow postprocessing for custom storage.
175 options.storeCss(cssPath, css, next);
176 });
177 } catch (err) {
178 utilities.lessError(err);
179 return next(err);
180 }
181 });
182 };
183
184 // Force recompile of all files.
185 if (options.force) {
186 return compile();
187 }
188
189 // Compile on server restart and new files.
190 if (!imports[lessPath]) {
191 return compile();
192 }
193
194 // Only compile once, disregarding the file changes.
195 if (options.once && imports[lessPath]) {
196 return next();
197 }
198
199 // Compare mtimes to determine if changed.
200 fs.stat(lessPath, function(err, lessStats){
201 if (err) {
202 return error(err);
203 }
204
205 fs.stat(cssPath, function(err, cssStats){
206 // CSS has not been compiled, compile it!
207 if (err) {
208 if ('ENOENT' == err.code) {
209 log('not found', cssPath);
210
211 // No CSS file found in dest
212 return compile();
213 } else {
214 return next(err);
215 }
216 } else if (lessStats.mtime > cssStats.mtime) {
217 // Source has changed, compile it
218 log('modified', cssPath);
219
220 return compile();
221 } else {
222 // Check if any of the less imports were changed
223 checkImports(lessPath, function(changed){
224 if(typeof changed != "undefined" && changed.length) {
225 log('modified import', changed);
226
227 return compile();
228 }
229
230 return next();
231 });
232 }
233 });
234 });
235 } else {
236 return next();
237 }
238 };
239};