1 | #! /usr/bin/env node
|
2 | // -*- js -*-
|
3 |
|
4 | "use strict";
|
5 |
|
6 | require("../tools/tty");
|
7 |
|
8 | var fs = require("fs");
|
9 | var info = require("../package.json");
|
10 | var path = require("path");
|
11 | var UglifyJS = require("../tools/node");
|
12 |
|
13 | var skip_keys = [ "cname", "fixed", "inlined", "parent_scope", "scope", "uses_eval", "uses_with" ];
|
14 | var files = {};
|
15 | var options = {};
|
16 | var 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 | };
|
29 | var args = process.argv.slice(2);
|
30 | var paths = [];
|
31 | var output, nameCache;
|
32 | var specified = {};
|
33 | while (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 |
|
48 | function 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 | }
|
225 | if (!output && options.sourceMap && options.sourceMap.url != "inline") fatal("cannot write source map to STDOUT");
|
226 | if (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 | });
|
230 | if (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 | }
|
241 | if (/^ast|spidermonkey$/.test(output)) {
|
242 | if (typeof options.output != "object") options.output = {};
|
243 | options.output.ast = true;
|
244 | options.output.code = false;
|
245 | }
|
246 | if (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 | }
|
250 | if (options.warnings) {
|
251 | UglifyJS.AST_Node.log_function(print_error, options.warnings == "verbose");
|
252 | delete options.warnings;
|
253 | }
|
254 | var convert_path = function(name) {
|
255 | return name;
|
256 | };
|
257 | if (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 | }
|
266 | if (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 | }
|
271 | if (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 |
|
305 | function convert_ast(fn) {
|
306 | return UglifyJS.AST_Node.from_mozilla_ast(Object.keys(files).reduce(fn, null));
|
307 | }
|
308 |
|
309 | function 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 |
|
471 | function 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.
|
485 | function 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 |
|
512 | function 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 |
|
521 | function 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 |
|
562 | function skip_key(key) {
|
563 | return skip_keys.indexOf(key) >= 0;
|
564 | }
|
565 |
|
566 | function symdef(def) {
|
567 | var ret = (1e6 + def.id) + " " + def.name;
|
568 | if (def.mangled_name) ret += " " + def.mangled_name;
|
569 | return ret;
|
570 | }
|
571 |
|
572 | function 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 |
|
584 | function print_error(msg) {
|
585 | process.stderr.write(msg);
|
586 | process.stderr.write("\n");
|
587 | }
|
588 |
|
589 | function print(txt) {
|
590 | process.stdout.write(txt);
|
591 | process.stdout.write("\n");
|
592 | }
|