1#! /usr/bin/env node 2// -*- js -*- 3 4"use strict"; 5 6require("../tools/tty"); 7 8var fs = require("fs"); 9var info = require("../package.json"); 10var path = require("path"); 11var UglifyJS = require("../tools/node"); 12 13var skip_keys = [ "cname", "fixed", "in_arg", "inlined", "length_read", "parent_scope", "redef", "scope", "unused" ]; 14var truthy_keys = [ "optional", "pure", "terminal", "uses_arguments", "uses_eval", "uses_with" ]; 15 16var files = {}; 17var options = {}; 18var short_forms = { 19 b: "beautify", 20 c: "compress", 21 d: "define", 22 e: "enclose", 23 h: "help", 24 m: "mangle", 25 o: "output", 26 O: "output-opts", 27 p: "parse", 28 v: "version", 29 V: "version", 30}; 31var args = process.argv.slice(2); 32var paths = []; 33var output, nameCache; 34var specified = {}; 35while (args.length) { 36 var arg = args.shift(); 37 if (arg[0] != "-") { 38 paths.push(arg); 39 } else if (arg == "--") { 40 paths = paths.concat(args); 41 break; 42 } else if (arg[1] == "-") { 43 process_option(arg.slice(2)); 44 } else [].forEach.call(arg.slice(1), function(letter, index, arg) { 45 if (!(letter in short_forms)) fatal("invalid option -" + letter); 46 process_option(short_forms[letter], index + 1 < arg.length); 47 }); 48} 49 50function process_option(name, no_value) { 51 specified[name] = true; 52 switch (name) { 53 case "help": 54 switch (read_value()) { 55 case "ast": 56 print(UglifyJS.describe_ast()); 57 break; 58 case "options": 59 var text = []; 60 var toplevels = []; 61 var padding = ""; 62 var defaults = UglifyJS.default_options(); 63 for (var name in defaults) { 64 var option = defaults[name]; 65 if (option && typeof option == "object") { 66 text.push("--" + ({ 67 output: "beautify", 68 sourceMap: "source-map", 69 }[name] || name) + " options:"); 70 text.push(format_object(option)); 71 text.push(""); 72 } else { 73 if (padding.length < name.length) padding = Array(name.length + 1).join(" "); 74 toplevels.push([ { 75 keep_fargs: "keep-fargs", 76 keep_fnames: "keep-fnames", 77 nameCache: "name-cache", 78 }[name] || name, option ]); 79 } 80 } 81 toplevels.forEach(function(tokens) { 82 text.push("--" + tokens[0] + padding.slice(tokens[0].length - 2) + tokens[1]); 83 }); 84 print(text.join("\n")); 85 break; 86 default: 87 print([ 88 "Usage: uglifyjs [files...] [options]", 89 "", 90 "Options:", 91 " -h, --help Print usage information.", 92 " `--help options` for details on available options.", 93 " -v, -V, --version Print version number.", 94 " -p, --parse <options> Specify parser options.", 95 " -c, --compress [options] Enable compressor/specify compressor options.", 96 " -m, --mangle [options] Mangle names/specify mangler options.", 97 " --mangle-props [options] Mangle properties/specify mangler options.", 98 " -b, --beautify [options] Beautify output/specify output options.", 99 " -O, --output-opts <options> Output options (beautify disabled).", 100 " -o, --output <file> Output file (default STDOUT).", 101 " --annotations Process and preserve comment annotations.", 102 " --no-annotations Ignore and discard comment annotations.", 103 " --comments [filter] Preserve copyright comments in the output.", 104 " --config-file <file> Read minify() options from JSON file.", 105 " -d, --define <expr>[=value] Global definitions.", 106 " -e, --enclose [arg[,...][:value[,...]]] Embed everything in a big function, with configurable argument(s) & value(s).", 107 " --expression Parse a single expression, rather than a program.", 108 " --ie Support non-standard Internet Explorer.", 109 " --keep-fargs Do not mangle/drop function arguments.", 110 " --keep-fnames Do not mangle/drop function names. Useful for code relying on Function.prototype.name.", 111 " --module Process input as ES module (implies --toplevel)", 112 " --name-cache <file> File to hold mangled name mappings.", 113 " --rename Force symbol expansion.", 114 " --no-rename Disable symbol expansion.", 115 " --self Build UglifyJS as a library (implies --wrap UglifyJS)", 116 " --source-map [options] Enable source map/specify source map options.", 117 " --timings Display operations run time on STDERR.", 118 " --toplevel Compress and/or mangle variables in toplevel scope.", 119 " --v8 Support non-standard Chrome & Node.js.", 120 " --validate Perform validation during AST manipulations.", 121 " --verbose Print diagnostic messages.", 122 " --warn Print warning messages.", 123 " --webkit Support non-standard Safari/Webkit.", 124 " --wrap <name> Embed everything as a function with “exports” corresponding to “name” globally.", 125 "", 126 "(internal debug use only)", 127 " --in-situ Warning: replaces original source files with minified output.", 128 " --reduce-test Reduce a standalone test case (assumes cloned repository).", 129 ].join("\n")); 130 } 131 process.exit(); 132 case "version": 133 print(info.name + " " + info.version); 134 process.exit(); 135 case "config-file": 136 var config = JSON.parse(read_file(read_value(true))); 137 if (config.mangle && config.mangle.properties && config.mangle.properties.regex) { 138 config.mangle.properties.regex = UglifyJS.parse(config.mangle.properties.regex, { 139 expression: true, 140 }).value; 141 } 142 for (var key in config) if (!(key in options)) options[key] = config[key]; 143 break; 144 case "compress": 145 case "mangle": 146 options[name] = parse_js(read_value(), options[name]); 147 break; 148 case "source-map": 149 options.sourceMap = parse_js(read_value(), options.sourceMap); 150 break; 151 case "enclose": 152 options[name] = read_value(); 153 break; 154 case "annotations": 155 case "expression": 156 case "ie": 157 case "ie8": 158 case "module": 159 case "timings": 160 case "toplevel": 161 case "v8": 162 case "validate": 163 case "webkit": 164 options[name] = true; 165 break; 166 case "no-annotations": 167 options.annotations = false; 168 break; 169 case "keep-fargs": 170 options.keep_fargs = true; 171 break; 172 case "keep-fnames": 173 options.keep_fnames = true; 174 break; 175 case "wrap": 176 options[name] = read_value(true); 177 break; 178 case "verbose": 179 options.warnings = "verbose"; 180 break; 181 case "warn": 182 if (!options.warnings) options.warnings = true; 183 break; 184 case "beautify": 185 options.output = parse_js(read_value(), options.output); 186 if (!("beautify" in options.output)) options.output.beautify = true; 187 break; 188 case "output-opts": 189 options.output = parse_js(read_value(true), options.output); 190 break; 191 case "comments": 192 if (typeof options.output != "object") options.output = {}; 193 options.output.comments = read_value(); 194 if (options.output.comments === true) options.output.comments = "some"; 195 break; 196 case "define": 197 if (typeof options.compress != "object") options.compress = {}; 198 options.compress.global_defs = parse_js(read_value(true), options.compress.global_defs, "define"); 199 break; 200 case "mangle-props": 201 if (typeof options.mangle != "object") options.mangle = {}; 202 options.mangle.properties = parse_js(read_value(), options.mangle.properties); 203 break; 204 case "name-cache": 205 nameCache = read_value(true); 206 options.nameCache = JSON.parse(read_file(nameCache, "{}")); 207 break; 208 case "output": 209 output = read_value(true); 210 break; 211 case "parse": 212 options.parse = parse_js(read_value(true), options.parse); 213 break; 214 case "rename": 215 options.rename = true; 216 break; 217 case "no-rename": 218 options.rename = false; 219 break; 220 case "in-situ": 221 case "reduce-test": 222 case "self": 223 break; 224 default: 225 fatal("invalid option --" + name); 226 } 227 228 function read_value(required) { 229 if (no_value || !args.length || args[0][0] == "-") { 230 if (required) fatal("missing option argument for --" + name); 231 return true; 232 } 233 return args.shift(); 234 } 235} 236if (!output && options.sourceMap && options.sourceMap.url != "inline") fatal("cannot write source map to STDOUT"); 237if (specified["beautify"] && specified["output-opts"]) fatal("--beautify cannot be used with --output-opts"); 238[ "compress", "mangle" ].forEach(function(name) { 239 if (!(name in options)) options[name] = false; 240}); 241if (/^ast|spidermonkey$/.test(output)) { 242 if (typeof options.output != "object") options.output = {}; 243 options.output.ast = true; 244 options.output.code = false; 245} 246if (options.parse && (options.parse.acorn || options.parse.spidermonkey) 247 && options.sourceMap && options.sourceMap.content == "inline") { 248 fatal("inline source map only works with built-in parser"); 249} 250if (options.warnings) { 251 UglifyJS.AST_Node.log_function(print_error, options.warnings == "verbose"); 252 delete options.warnings; 253} 254var convert_path = function(name) { 255 return name; 256}; 257if (typeof options.sourceMap == "object" && "base" in options.sourceMap) { 258 convert_path = function() { 259 var base = options.sourceMap.base; 260 delete options.sourceMap.base; 261 return function(name) { 262 return path.relative(base, name); 263 }; 264 }(); 265} 266if (specified["self"]) { 267 if (paths.length) UglifyJS.AST_Node.warn("Ignoring input files since --self was passed"); 268 if (!options.wrap) options.wrap = "UglifyJS"; 269 paths = UglifyJS.FILES; 270} else if (paths.length) { 271 paths = simple_glob(paths); 272} 273if (specified["in-situ"]) { 274 if (output && output != "spidermonkey" || specified["reduce-test"] || specified["self"]) { 275 fatal("incompatible options specified"); 276 } 277 paths.forEach(function(name) { 278 print(name); 279 if (/^ast|spidermonkey$/.test(name)) fatal("invalid file name specified"); 280 files = {}; 281 files[convert_path(name)] = read_file(name); 282 output = name; 283 run(); 284 }); 285} else if (paths.length) { 286 paths.forEach(function(name) { 287 files[convert_path(name)] = read_file(name); 288 }); 289 run(); 290} else { 291 var timerId = process.stdin.isTTY && process.argv.length < 3 && setTimeout(function() { 292 print_error("Waiting for input... (use `--help` to print usage information)"); 293 }, 1500); 294 var chunks = []; 295 process.stdin.setEncoding("utf8"); 296 process.stdin.once("data", function() { 297 clearTimeout(timerId); 298 }).on("data", function(chunk) { 299 chunks.push(chunk); 300 }).on("end", function() { 301 files = { STDIN: chunks.join("") }; 302 run(); 303 }); 304 process.stdin.resume(); 305} 306 307function convert_ast(fn) { 308 return UglifyJS.AST_Node.from_mozilla_ast(Object.keys(files).reduce(fn, null)); 309} 310 311function run() { 312 var content = options.sourceMap && options.sourceMap.content; 313 if (content && content != "inline") { 314 UglifyJS.AST_Node.info("Using input source map: {content}", { 315 content : content, 316 }); 317 options.sourceMap.content = read_file(content, content); 318 } 319 try { 320 if (options.parse) { 321 if (options.parse.acorn) { 322 var annotations = Object.create(null); 323 files = convert_ast(function(toplevel, name) { 324 var content = files[name]; 325 var list = annotations[name] = []; 326 var prev = -1; 327 return require("acorn").parse(content, { 328 allowHashBang: true, 329 ecmaVersion: "latest", 330 locations: true, 331 onComment: function(block, text, start, end) { 332 var match = /[@#]__PURE__/.exec(text); 333 if (!match) { 334 if (start != prev) return; 335 match = [ list[prev] ]; 336 } 337 while (/\s/.test(content[end])) end++; 338 list[end] = match[0]; 339 prev = end; 340 }, 341 preserveParens: true, 342 program: toplevel, 343 sourceFile: name, 344 sourceType: "module", 345 }); 346 }); 347 files.walk(new UglifyJS.TreeWalker(function(node) { 348 if (!(node instanceof UglifyJS.AST_Call)) return; 349 var list = annotations[node.start.file]; 350 var pure = list[node.start.pos]; 351 if (!pure) { 352 var tokens = node.start.parens; 353 if (tokens) for (var i = 0; !pure && i < tokens.length; i++) { 354 pure = list[tokens[i].pos]; 355 } 356 } 357 if (pure) node.pure = pure; 358 })); 359 } else if (options.parse.spidermonkey) { 360 files = convert_ast(function(toplevel, name) { 361 var obj = JSON.parse(files[name]); 362 if (!toplevel) return obj; 363 toplevel.body = toplevel.body.concat(obj.body); 364 return toplevel; 365 }); 366 } 367 } 368 } catch (ex) { 369 fatal(ex); 370 } 371 var result; 372 if (specified["reduce-test"]) { 373 // load on demand - assumes cloned repository 374 var reduce_test = require("../test/reduce"); 375 if (Object.keys(files).length != 1) fatal("can only test on a single file"); 376 result = reduce_test(files[Object.keys(files)[0]], options, { 377 log: print_error, 378 verbose: true, 379 }); 380 } else { 381 result = UglifyJS.minify(files, options); 382 } 383 if (result.error) { 384 var ex = result.error; 385 if (ex.name == "SyntaxError") { 386 print_error("Parse error at " + ex.filename + ":" + ex.line + "," + ex.col); 387 var file = files[ex.filename]; 388 if (file) { 389 var col = ex.col; 390 var lines = file.split(/\r?\n/); 391 var line = lines[ex.line - 1]; 392 if (!line && !col) { 393 line = lines[ex.line - 2]; 394 col = line.length; 395 } 396 if (line) { 397 var limit = 70; 398 if (col > limit) { 399 line = line.slice(col - limit); 400 col = limit; 401 } 402 print_error(line.slice(0, 80)); 403 print_error(line.slice(0, col).replace(/\S/g, " ") + "^"); 404 } 405 } 406 } else if (ex.defs) { 407 print_error("Supported options:"); 408 print_error(format_object(ex.defs)); 409 } 410 fatal(ex); 411 } else if (output == "ast") { 412 if (!options.compress && !options.mangle) { 413 var toplevel = result.ast; 414 if (!(toplevel instanceof UglifyJS.AST_Toplevel)) { 415 if (!(toplevel instanceof UglifyJS.AST_Statement)) toplevel = new UglifyJS.AST_SimpleStatement({ 416 body: toplevel, 417 }); 418 toplevel = new UglifyJS.AST_Toplevel({ 419 body: [ toplevel ], 420 }); 421 } 422 toplevel.figure_out_scope({}); 423 } 424 print(JSON.stringify(result.ast, function(key, value) { 425 if (value) switch (key) { 426 case "enclosed": 427 return value.length ? value.map(symdef) : undefined; 428 case "functions": 429 case "globals": 430 case "variables": 431 return value.size() ? value.map(symdef) : undefined; 432 case "thedef": 433 return symdef(value); 434 } 435 if (skip_property(key, value)) return; 436 if (value instanceof UglifyJS.AST_Token) return; 437 if (value instanceof UglifyJS.Dictionary) return; 438 if (value instanceof UglifyJS.AST_Node) { 439 var result = { 440 _class: "AST_" + value.TYPE 441 }; 442 value.CTOR.PROPS.forEach(function(prop) { 443 result[prop] = value[prop]; 444 }); 445 return result; 446 } 447 return value; 448 }, 2)); 449 } else if (output == "spidermonkey") { 450 print(JSON.stringify(result.ast.to_mozilla_ast(), null, 2)); 451 } else if (output) { 452 var code; 453 if (result.ast) { 454 var opts = {}; 455 for (var name in options.output) { 456 if (!/^ast|code$/.test(name)) opts[name] = options.output[name]; 457 } 458 code = UglifyJS.AST_Node.from_mozilla_ast(result.ast.to_mozilla_ast()).print_to_string(opts); 459 } else { 460 code = result.code; 461 } 462 fs.writeFileSync(output, code); 463 if (result.map) fs.writeFileSync(output + ".map", result.map); 464 } else { 465 print(result.code); 466 } 467 if (nameCache) fs.writeFileSync(nameCache, JSON.stringify(options.nameCache)); 468 if (result.timings) for (var phase in result.timings) { 469 print_error("- " + phase + ": " + result.timings[phase].toFixed(3) + "s"); 470 } 471} 472 473function fatal(message) { 474 if (message instanceof Error) { 475 message = message.stack.replace(/^\S*?Error:/, "ERROR:") 476 } else { 477 message = "ERROR: " + message; 478 } 479 print_error(message); 480 process.exit(1); 481} 482 483// A file glob function that only supports "*" and "?" wildcards in the basename. 484// Example: "foo/bar/*baz??.*.js" 485// Argument `paths` must be an array of strings. 486// Returns an array of strings. Garbage in, garbage out. 487function simple_glob(paths) { 488 return paths.reduce(function(paths, glob) { 489 if (/\*|\?/.test(glob)) { 490 var dir = path.dirname(glob); 491 try { 492 var entries = fs.readdirSync(dir).filter(function(name) { 493 try { 494 return fs.statSync(path.join(dir, name)).isFile(); 495 } catch (ex) { 496 return false; 497 } 498 }); 499 } catch (ex) {} 500 if (entries) { 501 var pattern = "^" + path.basename(glob) 502 .replace(/[.+^$[\]\\(){}]/g, "\\$&") 503 .replace(/\*/g, "[^/\\\\]*") 504 .replace(/\?/g, "[^/\\\\]") + "$"; 505 var mod = process.platform === "win32" ? "i" : ""; 506 var rx = new RegExp(pattern, mod); 507 var results = entries.filter(function(name) { 508 return rx.test(name); 509 }).sort().map(function(name) { 510 return path.join(dir, name); 511 }); 512 if (results.length) { 513 [].push.apply(paths, results); 514 return paths; 515 } 516 } 517 } 518 paths.push(glob); 519 return paths; 520 }, []); 521} 522 523function read_file(path, default_value) { 524 try { 525 return fs.readFileSync(path, "utf8"); 526 } catch (ex) { 527 if (ex.code == "ENOENT" && default_value != null) return default_value; 528 fatal(ex); 529 } 530} 531 532function parse_js(value, options, flag) { 533 if (!options || typeof options != "object") options = Object.create(null); 534 if (typeof value == "string") try { 535 UglifyJS.parse(value, { 536 expression: true 537 }).walk(new UglifyJS.TreeWalker(function(node) { 538 if (node instanceof UglifyJS.AST_Assign) { 539 var name = node.left.print_to_string(); 540 var value = node.right; 541 if (flag) { 542 options[name] = value; 543 } else if (value instanceof UglifyJS.AST_Array) { 544 options[name] = value.elements.map(to_string); 545 } else { 546 options[name] = to_string(value); 547 } 548 return true; 549 } 550 if (node instanceof UglifyJS.AST_Symbol || node instanceof UglifyJS.AST_PropAccess) { 551 var name = node.print_to_string(); 552 options[name] = true; 553 return true; 554 } 555 if (!(node instanceof UglifyJS.AST_Sequence)) throw node; 556 557 function to_string(value) { 558 return value instanceof UglifyJS.AST_Constant ? value.value : value.print_to_string({ 559 quote_keys: true 560 }); 561 } 562 })); 563 } catch (ex) { 564 if (flag) { 565 fatal("cannot parse arguments for '" + flag + "': " + value); 566 } else { 567 options[value] = null; 568 } 569 } 570 return options; 571} 572 573function skip_property(key, value) { 574 return skip_keys.indexOf(key) >= 0 575 // only skip truthy_keys if their value is falsy 576 || truthy_keys.indexOf(key) >= 0 && !value; 577} 578 579function symdef(def) { 580 var ret = (1e6 + def.id) + " " + def.name; 581 if (def.mangled_name) ret += " " + def.mangled_name; 582 return ret; 583} 584 585function format_object(obj) { 586 var lines = []; 587 var padding = ""; 588 Object.keys(obj).map(function(name) { 589 if (padding.length < name.length) padding = Array(name.length + 1).join(" "); 590 return [ name, JSON.stringify(obj[name]) ]; 591 }).forEach(function(tokens) { 592 lines.push(" " + tokens[0] + padding.slice(tokens[0].length - 2) + tokens[1]); 593 }); 594 return lines.join("\n"); 595} 596 597function print_error(msg) { 598 process.stderr.write(msg); 599 process.stderr.write("\n"); 600} 601 602function print(txt) { 603 process.stdout.write(txt); 604 process.stdout.write("\n"); 605} 606