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