uglifyjs 24 KB

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