1#!/usr/bin/env node
2
3var path = require('path'),
4    fs = require('../lib/less-node/fs'),
5    os = require("os"),
6    errno,
7    mkdirp;
8
9try {
10    errno = require('errno');
11} catch (err) {
12    errno = null;
13}
14
15var less = require('../lib/less-node'),
16    pluginLoader = new less.PluginLoader(less),
17    plugin,
18    plugins = [];
19
20var args = process.argv.slice(1);
21var silent = false,
22    verbose = false,
23    options = {
24    depends: false,
25    compress: false,
26    max_line_len: -1,
27    lint: false,
28    paths: [],
29    color: true,
30    strictImports: false,
31    insecure: false,
32    rootpath: '',
33    relativeUrls: false,
34    ieCompat: true,
35    strictMath: false,
36    strictUnits: false,
37    globalVars: null,
38    modifyVars: null,
39    urlArgs: '',
40    plugins: plugins
41};
42var sourceMapOptions = {};
43var continueProcessing = true;
44
45// Calling process.exit does not flush stdout always. Instead of exiting the process, we set the process' exitCode,
46// close all handles and wait for the event loop to exit the process.
47// @see https://github.com/nodejs/node/issues/6409
48// Unfortunately, node 0.10.x does not support setting process.exitCode, so we need to call reallyExit() explicitly.
49// @see https://nodejs.org/api/process.html#process_process_exitcode
50// Additionally we also need to make sure that uncaughtExceptions are never swallowed.
51// @see https://github.com/less/less.js/issues/2881
52// This code can safely be removed if node 0.10.x is not supported anymore.
53process.on("exit", function() { process.reallyExit(process.exitCode); });
54process.on("uncaughtException", function(err) {
55    console.error(err);
56    process.exitCode = 1;
57});
58// This code will still be required because otherwise rejected promises would not be reported to the user
59process.on("unhandledRejection", function(err) {
60    console.error(err);
61    process.exitCode = 1;
62});
63
64var checkArgFunc = function(arg, option) {
65    if (!option) {
66        console.error(arg + " option requires a parameter");
67        continueProcessing = false;
68        process.exitCode = 1;
69        return false;
70    }
71    return true;
72};
73
74var checkBooleanArg = function(arg) {
75    var onOff = /^((on|t|true|y|yes)|(off|f|false|n|no))$/i.exec(arg);
76    if (!onOff) {
77        console.error(" unable to parse " + arg + " as a boolean. use one of on/t/true/y/yes/off/f/false/n/no");
78        continueProcessing = false;
79        process.exitCode = 1;
80        return false;
81    }
82    return Boolean(onOff[2]);
83};
84
85var parseVariableOption = function(option, variables) {
86    var parts = option.split('=', 2);
87    variables[parts[0]] = parts[1];
88};
89
90var sourceMapFileInline = false;
91
92function printUsage() {
93    less.lesscHelper.printUsage();
94    pluginLoader.printUsage(plugins);
95    continueProcessing = false;
96}
97
98// self executing function so we can return
99(function() {
100    args = args.filter(function (arg) {
101        var match;
102
103        match = arg.match(/^-I(.+)$/);
104        if (match) {
105            options.paths.push(match[1]);
106            return false;
107        }
108
109        match = arg.match(/^--?([a-z][0-9a-z-]*)(?:=(.*))?$/i);
110        if (match) {
111            arg = match[1];
112        } else {
113            return arg;
114        }
115
116        switch (arg) {
117            case 'v':
118            case 'version':
119                console.log("lessc " + less.version.join('.') + " (Less Compiler) [JavaScript]");
120                continueProcessing = false;
121                break;
122            case 'verbose':
123                verbose = true;
124                break;
125            case 's':
126            case 'silent':
127                silent = true;
128                break;
129            case 'l':
130            case 'lint':
131                options.lint = true;
132                break;
133            case 'strict-imports':
134                options.strictImports = true;
135                break;
136            case 'h':
137            case 'help':
138                printUsage();
139                break;
140            case 'x':
141            case 'compress':
142                options.compress = true;
143                break;
144            case 'insecure':
145                options.insecure = true;
146                break;
147            case 'M':
148            case 'depends':
149                options.depends = true;
150                break;
151            case 'max-line-len':
152                if (checkArgFunc(arg, match[2])) {
153                    options.maxLineLen = parseInt(match[2], 10);
154                    if (options.maxLineLen <= 0) {
155                        options.maxLineLen = -1;
156                    }
157                }
158                break;
159            case 'no-color':
160                options.color = false;
161                break;
162            case 'no-ie-compat':
163                options.ieCompat = false;
164                break;
165            case 'no-js':
166                options.javascriptEnabled = false;
167                break;
168            case 'include-path':
169                if (checkArgFunc(arg, match[2])) {
170                    // ; supported on windows.
171                    // : supported on windows and linux, excluding a drive letter like C:\ so C:\file:D:\file parses to 2
172                    options.paths = match[2]
173                        .split(os.type().match(/Windows/) ? /:(?!\\)|;/ : ':')
174                        .map(function(p) {
175                            if (p) {
176                                return path.resolve(process.cwd(), p);
177                            }
178                        });
179                }
180                break;
181            case 'line-numbers':
182                if (checkArgFunc(arg, match[2])) {
183                    options.dumpLineNumbers = match[2];
184                }
185                break;
186            case 'source-map':
187                options.sourceMap = true;
188                if (match[2]) {
189                    sourceMapOptions.sourceMapFullFilename = match[2];
190                }
191                break;
192            case 'source-map-rootpath':
193                if (checkArgFunc(arg, match[2])) {
194                    sourceMapOptions.sourceMapRootpath = match[2];
195                }
196                break;
197            case 'source-map-basepath':
198                if (checkArgFunc(arg, match[2])) {
199                    sourceMapOptions.sourceMapBasepath = match[2];
200                }
201                break;
202            case 'source-map-map-inline':
203                sourceMapFileInline = true;
204                options.sourceMap = true;
205                break;
206            case 'source-map-less-inline':
207                sourceMapOptions.outputSourceFiles = true;
208                break;
209            case 'source-map-url':
210                if (checkArgFunc(arg, match[2])) {
211                    sourceMapOptions.sourceMapURL = match[2];
212                }
213                break;
214            case 'rp':
215            case 'rootpath':
216                if (checkArgFunc(arg, match[2])) {
217                    options.rootpath = match[2].replace(/\\/g, '/');
218                }
219                break;
220            case "ru":
221            case "relative-urls":
222                options.relativeUrls = true;
223                break;
224            case "sm":
225            case "strict-math":
226                if (checkArgFunc(arg, match[2])) {
227                    options.strictMath = checkBooleanArg(match[2]);
228                }
229                break;
230            case "su":
231            case "strict-units":
232                if (checkArgFunc(arg, match[2])) {
233                    options.strictUnits = checkBooleanArg(match[2]);
234                }
235                break;
236            case "global-var":
237                if (checkArgFunc(arg, match[2])) {
238                    if (!options.globalVars) {
239                        options.globalVars = {};
240                    }
241                    parseVariableOption(match[2], options.globalVars);
242                }
243                break;
244            case "modify-var":
245                if (checkArgFunc(arg, match[2])) {
246                    if (!options.modifyVars) {
247                        options.modifyVars = {};
248                    }
249
250                    parseVariableOption(match[2], options.modifyVars);
251                }
252                break;
253            case 'url-args':
254                if (checkArgFunc(arg, match[2])) {
255                    options.urlArgs = match[2];
256                }
257                break;
258            case 'plugin':
259                var splitupArg = match[2].match(/^([^=]+)(=(.*))?/),
260                    name = splitupArg[1],
261                    pluginOptions = splitupArg[3];
262
263                plugin = pluginLoader.tryLoadPlugin(name, pluginOptions);
264                if (plugin) {
265                    plugins.push(plugin);
266                } else {
267                    console.error("Unable to load plugin " + name +
268                        " please make sure that it is installed under or at the same level as less");
269                    process.exitCode = 1;
270                }
271                break;
272            default:
273                plugin = pluginLoader.tryLoadPlugin("less-plugin-" + arg, match[2]);
274                if (plugin) {
275                    plugins.push(plugin);
276                } else {
277                    console.error("Unable to interpret argument " + arg +
278                        " - if it is a plugin (less-plugin-" + arg + "), make sure that it is installed under or at" +
279                        " the same level as less");
280                    process.exitCode = 1;
281                }
282                break;
283        }
284    });
285
286    if (!continueProcessing) {
287        return;
288    }
289
290    var input = args[1];
291    if (input && input != '-') {
292        input = path.resolve(process.cwd(), input);
293    }
294    var output = args[2];
295    var outputbase = args[2];
296    if (output) {
297        output = path.resolve(process.cwd(), output);
298    }
299
300    if (options.sourceMap) {
301
302        sourceMapOptions.sourceMapInputFilename = input;
303        if (!sourceMapOptions.sourceMapFullFilename) {
304            if (!output && !sourceMapFileInline) {
305                console.error("the sourcemap option only has an optional filename if the css filename is given");
306                console.error("consider adding --source-map-map-inline which embeds the sourcemap into the css");
307                process.exitCode = 1;
308                return;
309            }
310            // its in the same directory, so always just the basename
311            if (output) {
312                sourceMapOptions.sourceMapOutputFilename = path.basename(output);
313                sourceMapOptions.sourceMapFullFilename = output + ".map";
314            }
315            // its in the same directory, so always just the basename
316            if ('sourceMapFullFilename' in sourceMapOptions) {
317                sourceMapOptions.sourceMapFilename = path.basename(sourceMapOptions.sourceMapFullFilename);
318            }
319        } else if (options.sourceMap && !sourceMapFileInline) {
320            var mapFilename = path.resolve(process.cwd(), sourceMapOptions.sourceMapFullFilename),
321                mapDir = path.dirname(mapFilename),
322                outputDir = path.dirname(output);
323            // find the path from the map to the output file
324            sourceMapOptions.sourceMapOutputFilename = path.join(
325                path.relative(mapDir, outputDir), path.basename(output));
326
327            // make the sourcemap filename point to the sourcemap relative to the css file output directory
328            sourceMapOptions.sourceMapFilename = path.join(
329                path.relative(outputDir, mapDir), path.basename(sourceMapOptions.sourceMapFullFilename));
330        }
331    }
332
333    if (sourceMapOptions.sourceMapBasepath === undefined) {
334        sourceMapOptions.sourceMapBasepath = input ? path.dirname(input) : process.cwd();
335    }
336
337    if (sourceMapOptions.sourceMapRootpath === undefined) {
338        var pathToMap = path.dirname((sourceMapFileInline ? output : sourceMapOptions.sourceMapFullFilename) || '.'),
339            pathToInput = path.dirname(sourceMapOptions.sourceMapInputFilename || '.');
340        sourceMapOptions.sourceMapRootpath = path.relative(pathToMap, pathToInput);
341    }
342
343    if (! input) {
344        console.error("lessc: no input files");
345        console.error("");
346        printUsage();
347        process.exitCode = 1;
348        return;
349    }
350
351    var ensureDirectory = function (filepath) {
352        var dir = path.dirname(filepath),
353            cmd,
354            existsSync = fs.existsSync || path.existsSync;
355        if (!existsSync(dir)) {
356            if (mkdirp === undefined) {
357                try {mkdirp = require('mkdirp');}
358                catch(e) { mkdirp = null; }
359            }
360            cmd = mkdirp && mkdirp.sync || fs.mkdirSync;
361            cmd(dir);
362        }
363    };
364
365    if (options.depends) {
366        if (!outputbase) {
367            console.error("option --depends requires an output path to be specified");
368            process.exitCode = 1;
369            return;
370        }
371        process.stdout.write(outputbase + ": ");
372    }
373
374    if (!sourceMapFileInline) {
375        var writeSourceMap = function(output, onDone) {
376            output = output || "";
377            var filename = sourceMapOptions.sourceMapFullFilename;
378            ensureDirectory(filename);
379            fs.writeFile(filename, output, 'utf8', function (err) {
380                if (err) {
381                    var description = "Error: ";
382                    if (errno && errno.errno[err.errno]) {
383                        description += errno.errno[err.errno].description;
384                    } else {
385                        description += err.code + " " + err.message;
386                    }
387                    console.error('lessc: failed to create file ' + filename);
388                    console.error(description);
389                    process.exitCode = 1;
390                } else {
391                    less.logger.info('lessc: wrote ' + filename);
392                }
393                onDone();
394            });
395        };
396    }
397
398    var writeSourceMapIfNeeded = function(output, onDone) {
399        if (options.sourceMap && !sourceMapFileInline) {
400            writeSourceMap(output, onDone);
401        } else {
402            onDone();
403        }
404    };
405
406    var writeOutput = function(output, result, onSuccess) {
407        if (options.depends) {
408            onSuccess();
409        } else if (output) {
410            ensureDirectory(output);
411            fs.writeFile(output, result.css, {encoding: 'utf8'}, function (err) {
412                if (err) {
413                    var description = "Error: ";
414                    if (errno && errno.errno[err.errno]) {
415                        description += errno.errno[err.errno].description;
416                    } else {
417                        description += err.code + " " + err.message;
418                    }
419                    console.error('lessc: failed to create file ' + output);
420                    console.error(description);
421                    process.exitCode = 1;
422                } else {
423                    less.logger.info('lessc: wrote ' + output);
424                    onSuccess();
425                }
426            });
427        } else if (!options.depends) {
428            process.stdout.write(result.css);
429            onSuccess();
430        }
431    };
432
433    var logDependencies = function(options, result) {
434        if (options.depends) {
435            var depends = "";
436            for (var i = 0; i < result.imports.length; i++) {
437                depends += result.imports[i] + " ";
438            }
439            console.log(depends);
440        }
441    };
442
443    var parseLessFile = function (e, data) {
444        if (e) {
445            console.error("lessc: " + e.message);
446            process.exitCode = 1;
447            return;
448        }
449
450        data = data.replace(/^\uFEFF/, '');
451
452        options.paths = [path.dirname(input)].concat(options.paths);
453        options.filename = input;
454
455        if (options.lint) {
456            options.sourceMap = false;
457        }
458        sourceMapOptions.sourceMapFileInline = sourceMapFileInline;
459
460        if (options.sourceMap) {
461            options.sourceMap = sourceMapOptions;
462        }
463
464        less.logger.addListener({
465            info: function(msg) {
466                if (verbose) {
467                    console.log(msg);
468                }
469            },
470            warn: function(msg) {
471                // do not show warning if the silent option is used
472                if (!silent) {
473                    console.warn(msg);
474                }
475            },
476            error: function(msg) {
477                console.error(msg);
478            }
479        });
480
481        less.render(data, options)
482            .then(function(result) {
483                if (!options.lint) {
484                    writeOutput(output, result, function() {
485                        writeSourceMapIfNeeded(result.map, function() {
486                            logDependencies(options, result);
487                        });
488                    });
489                }
490            },
491            function(err) {
492                less.writeError(err, options);
493                process.exitCode = 1;
494            });
495    };
496
497    if (input != '-') {
498        fs.readFile(input, 'utf8', parseLessFile);
499    } else {
500        process.stdin.resume();
501        process.stdin.setEncoding('utf8');
502
503        var buffer = '';
504        process.stdin.on('data', function(data) {
505            buffer += data;
506        });
507
508        process.stdin.on('end', function() {
509            parseLessFile(false, buffer);
510        });
511    }
512})();
513