UNPKG

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