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