From 25fc02743af0e3fec6b10ffdb5cee3b7f22b9889 Mon Sep 17 00:00:00 2001 From: kzc Date: Thu, 1 Sep 2016 09:24:56 -0400 Subject: [PATCH 01/10] Account for side effects in `string + expr` optimization --- lib/compress.js | 4 +-- test/compress/issue-1275.js | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 test/compress/issue-1275.js diff --git a/lib/compress.js b/lib/compress.js index 8f786d73..8a08572f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2499,8 +2499,8 @@ merge(Compressor.prototype, { case "+": var ll = self.left.evaluate(compressor); var rr = self.right.evaluate(compressor); - if ((ll.length > 1 && ll[0] instanceof AST_String && ll[1]) || - (rr.length > 1 && rr[0] instanceof AST_String && rr[1])) { + if ((ll.length > 1 && ll[0] instanceof AST_String && ll[1] && !self.right.has_side_effects(compressor)) || + (rr.length > 1 && rr[0] instanceof AST_String && rr[1] && !self.left.has_side_effects(compressor))) { compressor.warn("+ in boolean context always true [{file}:{line},{col}]", self.start); return make_node(AST_True, self); } diff --git a/test/compress/issue-1275.js b/test/compress/issue-1275.js new file mode 100644 index 00000000..e88e284c --- /dev/null +++ b/test/compress/issue-1275.js @@ -0,0 +1,49 @@ +string_plus_optimization: { + options = { + side_effects : true, + evaluate : true, + conditionals : true, + comparisons : true, + dead_code : true, + booleans : true, + unused : true, + if_return : true, + join_vars : true, + cascade : true, + hoist_funs : true, + }; + input: { + function foo(anything) { + function throwing_function() { + throw "nope"; + } + try { + console.log('0' + throwing_function() ? "yes" : "no"); + } catch (ex) { + console.log(ex); + } + console.log('0' + anything ? "yes" : "no"); + console.log(anything + '0' ? "Yes" : "No"); + console.log('' + anything); + console.log(anything + ''); + } + foo(); + } + expect: { + function foo(anything) { + function throwing_function() { + throw "nope"; + } + try { + console.log('0' + throwing_function() ? "yes" : "no"); + } catch (ex) { + console.log(ex); + } + console.log("yes"); + console.log("Yes"); + console.log('' + anything); + console.log(anything + ''); + } + foo(); + } +} From 0111497fc98d5098f81bc749f77da5734add37bb Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sat, 3 Sep 2016 23:26:31 +0200 Subject: [PATCH 02/10] Make all comment options in cli available in js api Also removing more code within "loop" while at it. --- README.md | 6 ++-- bin/uglifyjs | 23 +++----------- lib/output.js | 59 ++++++++++++++++++++++++++--------- test/input/comments/filter.js | 3 ++ test/mocha/cli.js | 34 ++++++++++++++++++-- test/mocha/comment-filter.js | 15 +++++++++ 6 files changed, 103 insertions(+), 37 deletions(-) create mode 100644 test/input/comments/filter.js diff --git a/README.md b/README.md index 4f5b21a0..e0aa4ba9 100644 --- a/README.md +++ b/README.md @@ -849,8 +849,10 @@ which we care about here are `source_map` and `comments`. #### Keeping comments in the output In order to keep certain comments in the output you need to pass the -`comments` option. Pass a RegExp or a function. If you pass a RegExp, only -those comments whose body matches the regexp will be kept. Note that body +`comments` option. Pass a RegExp, boolean or a function. Stringified options +`all` and `some` can be passed too, where `some` behaves like it's cli +equivalent `--comments` without passing a value. If you pass a RegExp, +only those comments whose body matches the regexp will be kept. Note that body means without the initial `//` or `/*`. If you pass a function, it will be called for every comment in the tree and will receive two arguments: the node that the comment is attached to, and the comment token itself. diff --git a/bin/uglifyjs b/bin/uglifyjs index 3f0c8254..8d7bd759 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -250,25 +250,10 @@ if (ARGS.keep_fnames) { if (BEAUTIFY) UglifyJS.merge(OUTPUT_OPTIONS, BEAUTIFY); -if (ARGS.comments != null) { - if (/^\/.*\/[a-zA-Z]*$/.test(ARGS.comments)) { - try { - OUTPUT_OPTIONS.comments = extractRegex(ARGS.comments); - } catch (e) { - print_error("ERROR: Invalid --comments: " + e.message); - } - } else if (ARGS.comments == "all") { - OUTPUT_OPTIONS.comments = true; - } else { - OUTPUT_OPTIONS.comments = function(node, comment) { - var text = comment.value; - var type = comment.type; - if (type == "comment2") { - // multiline comment - return /@preserve|@license|@cc_on/i.test(text); - } - } - } +if (ARGS.comments === "") { + OUTPUT_OPTIONS.comments = "some"; +} else { + OUTPUT_OPTIONS.comments = ARGS.comments; } var files = ARGS._.slice(); diff --git a/lib/output.js b/lib/output.js index 801f7516..a3c6b4ab 100644 --- a/lib/output.js +++ b/lib/output.js @@ -70,6 +70,49 @@ function OutputStream(options) { keep_quoted_props: false }, true); + // Convert comment option to RegExp if neccessary and set up comments filter + if (typeof options.comments === "string" && /^\/.*\/[a-zA-Z]*$/.test(options.comments)) { + var regex_pos = options.comments.lastIndexOf("/"); + options.comments = new RegExp( + options.comments.substr(1, regex_pos - 1), + options.comments.substr(regex_pos + 1) + ); + } + if (options.comments instanceof RegExp) { + options.comments = (function(f) { + return function(comment) { + return comment.type == "comment5" || f.test(comment.value); + } + })(options.comments); + } + else if (typeof options.comments === "function") { + options.comments = (function(f) { + return function(comment) { + return comment.type == "comment5" || f(this, comment); + } + })(options.comments); + } + else if (options.comments === "some") { + options.comments = function(comment) { + var text = comment.value; + var type = comment.type; + if (type == "comment2") { + // multiline comment + return /@preserve|@license|@cc_on/i.test(text); + } + } + } + else if (options.comments){ // NOTE includes "all" option + options.comments = function() { + return true; + } + } else { + // Falsy case, so reject all comments, except shebangs + options.comments = function(comment) { + return comment.type == "comment5"; + } + } + var indentation = 0; var current_col = 0; var current_line = 1; @@ -435,7 +478,7 @@ function OutputStream(options) { AST_Node.DEFMETHOD("add_comments", function(output){ if (output._readonly) return; - var c = output.option("comments"), self = this; + var self = this; var start = self.start; if (start && !start._comments_dumped) { start._comments_dumped = true; @@ -458,19 +501,7 @@ function OutputStream(options) { })); } - if (!c) { - comments = comments.filter(function(comment) { - return comment.type == "comment5"; - }); - } else if (c.test) { - comments = comments.filter(function(comment){ - return comment.type == "comment5" || c.test(comment.value); - }); - } else if (typeof c == "function") { - comments = comments.filter(function(comment){ - return comment.type == "comment5" || c(self, comment); - }); - } + comments = comments.filter(output.option("comments"), self); // Keep single line comments after nlb, after nlb if (!output.option("beautify") && comments.length > 0 && diff --git a/test/input/comments/filter.js b/test/input/comments/filter.js new file mode 100644 index 00000000..c752080f --- /dev/null +++ b/test/input/comments/filter.js @@ -0,0 +1,3 @@ +// foo +/*@preserve*/ +// bar diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 38b57cd7..495b0076 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -2,11 +2,11 @@ var assert = require("assert"); var exec = require("child_process").exec; describe("bin/uglifyjs", function () { + var uglifyjscmd = '"' + process.argv[0] + '" bin/uglifyjs'; it("should produce a functional build when using --self", function (done) { this.timeout(5000); - var uglifyjs = '"' + process.argv[0] + '" bin/uglifyjs'; - var command = uglifyjs + ' --self -cm --wrap WrappedUglifyJS'; + var command = uglifyjscmd + ' --self -cm --wrap WrappedUglifyJS'; exec(command, function (err, stdout) { if (err) throw err; @@ -19,4 +19,34 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should be able to filter comments correctly with `--comment all`", function (done) { + var command = uglifyjscmd + ' test/input/comments/filter.js --comments all'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "// foo\n/*@preserve*/\n// bar\n\n"); + done(); + }); + }); + it("Should be able to filter comments correctly with `--comment `", function (done) { + var command = uglifyjscmd + ' test/input/comments/filter.js --comments /r/'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "/*@preserve*/\n// bar\n\n"); + done(); + }); + }); + it("Should be able to filter comments correctly with just `--comment`", function (done) { + var command = uglifyjscmd + ' test/input/comments/filter.js --comments'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "/*@preserve*/\n\n"); + done(); + }); + }); }); diff --git a/test/mocha/comment-filter.js b/test/mocha/comment-filter.js index ea2ec2eb..4330d1eb 100644 --- a/test/mocha/comment-filter.js +++ b/test/mocha/comment-filter.js @@ -7,6 +7,16 @@ describe("comment filters", function() { assert.strictEqual(ast.print_to_string({comments: /^!/}), "/*!test1*/\n//!test3\n//!test6\n//!test8\n"); }); + it("Should be able to filter comments with the 'all' option", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + assert.strictEqual(ast.print_to_string({comments: "all"}), "/*!test1*/\n/*test2*/\n//!test3\n//test4\n//test5\n//!test6\n//test7\n//!test8\n"); + }); + + it("Should be able to filter commments with the 'some' option", function() { + var ast = UglifyJS.parse("// foo\n/*@preserve*/\n// bar\n/*@license*/\n//@license with the wrong comment type\n/*@cc_on something*/"); + assert.strictEqual(ast.print_to_string({comments: "some"}), "/*@preserve*/\n/*@license*/\n/*@cc_on something*/\n"); + }); + it("Should be able to filter comments by passing a function", function() { var ast = UglifyJS.parse("/*TEST 123*/\n//An other comment\n//8 chars."); var f = function(node, comment) { @@ -16,6 +26,11 @@ describe("comment filters", function() { assert.strictEqual(ast.print_to_string({comments: f}), "/*TEST 123*/\n//8 chars.\n"); }); + it("Should be able to filter comments by passing regex in string format", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + assert.strictEqual(ast.print_to_string({comments: "/^!/"}), "/*!test1*/\n//!test3\n//!test6\n//!test8\n"); + }); + it("Should be able to get the comment and comment type when using a function", function() { var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); var f = function(node, comment) { From 4761d07e0bc3d4c53e0c9c72fc9c322c95cb090e Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Tue, 20 Sep 2016 22:23:27 +0800 Subject: [PATCH 03/10] Optimize unmodified variables --- README.md | 3 + lib/compress.js | 3 +- lib/scope.js | 12 ++- test/compress/reduce_vars.js | 171 +++++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 test/compress/reduce_vars.js diff --git a/README.md b/README.md index e0aa4ba9..ed2630f1 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,9 @@ to set `true`; it's effectively a shortcut for `foo=true`). - `collapse_vars` -- default `false`. Collapse single-use `var` and `const` definitions when possible. +- `reduce_vars` -- default `false`. Improve optimization on variables assigned + with and used as constant values. + - `warnings` -- display warnings when dropping unreachable code or unused declarations etc. diff --git a/lib/compress.js b/lib/compress.js index 8a08572f..14fb8f1e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -67,6 +67,7 @@ function Compressor(options, false_by_default) { if_return : !false_by_default, join_vars : !false_by_default, collapse_vars : false, + reduce_vars : false, cascade : !false_by_default, side_effects : !false_by_default, pure_getters : false, @@ -1107,7 +1108,7 @@ merge(Compressor.prototype, { this._evaluating = true; try { var d = this.definition(); - if (d && d.constant && d.init) { + if (d && (d.constant || compressor.option("reduce_vars") && !d.modified) && d.init) { return ev(d.init, compressor); } } finally { diff --git a/lib/scope.js b/lib/scope.js index 606a5a2f..fb583291 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -197,7 +197,8 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } if (node instanceof AST_SymbolRef) { var name = node.name; - if (name == "eval" && tw.parent() instanceof AST_Call) { + var parent = tw.parent(); + if (name == "eval" && parent instanceof AST_Call) { for (var s = node.scope; s && !s.uses_eval; s = s.parent_scope) { s.uses_eval = true; } @@ -213,12 +214,15 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ g.global = true; globals.set(name, g); } - node.thedef = g; + sym = g; if (func && name == "arguments") { func.uses_arguments = true; } - } else { - node.thedef = sym; + } + node.thedef = sym; + if (parent instanceof AST_Unary && (parent.operator === '++' || parent.operator === '--') + || parent instanceof AST_Assign && parent.left === node) { + sym.modified = true; } node.reference(); return true; diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js new file mode 100644 index 00000000..a1d05012 --- /dev/null +++ b/test/compress/reduce_vars.js @@ -0,0 +1,171 @@ +reduce_vars: { + options = { + conditionals : true, + evaluate : true, + global_defs : { + C : 0 + }, + reduce_vars : true, + unused : true + } + input: { + var A = 1; + (function f0() { + var a = 2; + console.log(a - 5); + console.log(A - 5); + })(); + (function f1() { + var a = 2; + console.log(a - 5); + eval("console.log(a);"); + })(); + (function f2(eval) { + var a = 2; + console.log(a - 5); + eval("console.log(a);"); + })(eval); + (function f3() { + var b = typeof C !== "undefined"; + var c = 4; + if (b) { + return 'yes'; + } else { + return 'no'; + } + })(); + console.log(A + 1); + } + expect: { + var A = 1; + (function() { + console.log(-3); + console.log(-4); + })(); + (function f1() { + var a = 2; + console.log(-3); + eval("console.log(a);"); + })(); + (function f2(eval) { + var a = 2; + console.log(-3); + eval("console.log(a);"); + })(eval); + (function() { + return "yes"; + })(); + console.log(2); + } +} + +modified: { + options = { + conditionals : true, + evaluate : true, + reduce_vars : true, + unused : true + } + input: { + function f0() { + var a = 1, b = 2; + b++; + console.log(a + 1); + console.log(b + 1); + } + + function f1() { + var a = 1, b = 2; + --b; + console.log(a + 1); + console.log(b + 1); + } + + function f2() { + var a = 1, b = 2, c = 3; + b = c; + console.log(a + b); + console.log(b + c); + console.log(a + c); + console.log(a + b + c); + } + + function f3() { + var a = 1, b = 2, c = 3; + b *= c; + console.log(a + b); + console.log(b + c); + console.log(a + c); + console.log(a + b + c); + } + + function f4() { + var a = 1, b = 2, c = 3; + if (a) { + b = c; + } else { + c = b; + } + console.log(a + b); + console.log(b + c); + // TODO: as "modified" is determined in "figure_out_scope", + // even "passes" wouldn't improve this any further + console.log(a + c); + console.log(a + b + c); + } + + function f5(a) { + B = a; + console.log(A ? 'yes' : 'no'); + console.log(B ? 'yes' : 'no'); + } + } + expect: { + function f0() { + var b = 2; + b++; + console.log(2); + console.log(b + 1); + } + + function f1() { + var b = 2; + --b; + console.log(2); + console.log(b + 1); + } + + function f2() { + var a = 1, b = 2, c = 3; + b = c; + console.log(a + b); + console.log(b + c); + console.log(4); + console.log(a + b + c); + } + + function f3() { + var a = 1, b = 2, c = 3; + b *= c; + console.log(a + b); + console.log(b + c); + console.log(4); + console.log(a + b + c); + } + + function f4() { + var a = 1, b = 2, c = 3; + b = c; + console.log(a + b); + console.log(b + c); + console.log(a + c); + console.log(a + b + c); + } + + function f5(a) { + B = a; + console.log(A ? 'yes' : 'no'); + console.log(B ? 'yes' : 'no'); + } + } +} \ No newline at end of file From fc9804b90955d3b38ac6e6c903a8c0e3f7945913 Mon Sep 17 00:00:00 2001 From: kzc Date: Sun, 2 Oct 2016 10:46:09 -0400 Subject: [PATCH 04/10] Fix (typeof side_effect()) in boolean context Fixes #1289 with suggestion by @rvanvelzen --- lib/compress.js | 6 ++++++ test/compress/typeof.js | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 14fb8f1e..1845717c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2314,6 +2314,12 @@ merge(Compressor.prototype, { // typeof always returns a non-empty string, thus it's // always true in booleans compressor.warn("Boolean expression always true [{file}:{line},{col}]", self.start); + if (self.expression.has_side_effects(compressor)) { + return make_node(AST_Seq, self, { + car: self.expression, + cdr: make_node(AST_True, self) + }); + } return make_node(AST_True, self); } if (e instanceof AST_Binary && self.operator == "!") { diff --git a/test/compress/typeof.js b/test/compress/typeof.js index cefdd43c..fb391573 100644 --- a/test/compress/typeof.js +++ b/test/compress/typeof.js @@ -23,3 +23,25 @@ typeof_evaluation: { h='undefined'; } } + +typeof_in_boolean_context: { + options = { + booleans : true, + evaluate : true, + conditionals : true, + }; + input: { + function f1(x) { return typeof x ? "yes" : "no"; } + function f2() { return typeof g()? "Yes" : "No"; } + typeof 0 ? foo() : bar(); + !typeof console.log(1); + var a = !typeof console.log(2); + } + expect: { + function f1(x) { return "yes"; } + function f2() { return g(), "Yes"; } + foo(); + !(console.log(1), !0); + var a = !(console.log(2), !0); + } +} From e05510f3bce78943f76649a4102c2f10bfccdaef Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Fri, 30 Sep 2016 12:27:45 +0200 Subject: [PATCH 05/10] Add an option to wrap IIFEs in parenthesis For #1307. --- bin/uglifyjs | 7 +++++++ lib/output.js | 15 +++++++++++++-- test/compress/wrap_iife.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 test/compress/wrap_iife.js diff --git a/bin/uglifyjs b/bin/uglifyjs index 8d7bd759..da9e0f10 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -76,6 +76,7 @@ You need to pass an argument to this option to specify the name that your module .describe("name-cache", "File to hold mangled names mappings") .describe("pure-funcs", "List of functions that can be safely removed if their return value is not used") .describe("dump-spidermonkey-ast", "Dump SpiderMonkey AST to stdout.") + .describe("wrap-iife", "Wrap IIFEs in parenthesis. Note: this disables the negate_iife compression option") .alias("p", "prefix") .alias("o", "output") @@ -130,6 +131,7 @@ You need to pass an argument to this option to specify the name that your module .boolean("bare-returns") .boolean("keep-fnames") .boolean("reserve-domprops") + .boolean("wrap-iife") .wrap(80) @@ -247,6 +249,11 @@ if (ARGS.keep_fnames) { if (MANGLE) MANGLE.keep_fnames = true; } +if (ARGS.wrap_iife) { + if (COMPRESS) COMPRESS.negate_iife = false; + OUTPUT_OPTIONS.wrap_iife = true; +} + if (BEAUTIFY) UglifyJS.merge(OUTPUT_OPTIONS, BEAUTIFY); diff --git a/lib/output.js b/lib/output.js index a3c6b4ab..c20a0365 100644 --- a/lib/output.js +++ b/lib/output.js @@ -67,7 +67,8 @@ function OutputStream(options) { screw_ie8 : true, preamble : null, quote_style : 0, - keep_quoted_props: false + keep_quoted_props: false, + wrap_iife : false, }, true); // Convert comment option to RegExp if neccessary and set up comments filter @@ -552,7 +553,17 @@ function OutputStream(options) { // a function expression needs parens around it when it's provably // the first token to appear in a statement. PARENS(AST_Function, function(output){ - return first_in_statement(output); + if (first_in_statement(output)) { + return true; + } + + if (output.option('wrap_iife')) { + var p = output.parent(); + console.log() + return p instanceof AST_Call && p.expression === this; + } + + return false; }); // same goes for an object literal, because otherwise it would be diff --git a/test/compress/wrap_iife.js b/test/compress/wrap_iife.js new file mode 100644 index 00000000..b1b88ac1 --- /dev/null +++ b/test/compress/wrap_iife.js @@ -0,0 +1,33 @@ +wrap_iife: { + options = { + negate_iife: false, + } + beautify = { + wrap_iife: true, + } + input: { + (function() { + return function() { + console.log('test') + }; + })()(); + } + expect_exact: '(function(){return function(){console.log("test")}})()();' +} + +wrap_iife_in_return_call: { + options = { + negate_iife: false, + } + beautify = { + wrap_iife: true, + } + input: { + (function() { + return (function() { + console.log('test') + })(); + })()(); + } + expect_exact: '(function(){return(function(){console.log("test")})()})()();' +} From 6389e52305f0d2e75a7bc6f8703c8d163d04cb99 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Thu, 6 Oct 2016 14:11:19 +0200 Subject: [PATCH 06/10] Remove console.log and add extra test case --- lib/output.js | 1 - test/compress/wrap_iife.js | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/output.js b/lib/output.js index c20a0365..014dac46 100644 --- a/lib/output.js +++ b/lib/output.js @@ -559,7 +559,6 @@ function OutputStream(options) { if (output.option('wrap_iife')) { var p = output.parent(); - console.log() return p instanceof AST_Call && p.expression === this; } diff --git a/test/compress/wrap_iife.js b/test/compress/wrap_iife.js index b1b88ac1..5c45853a 100644 --- a/test/compress/wrap_iife.js +++ b/test/compress/wrap_iife.js @@ -15,6 +15,21 @@ wrap_iife: { expect_exact: '(function(){return function(){console.log("test")}})()();' } +wrap_iife_in_expression: { + options = { + negate_iife: false, + } + beautify = { + wrap_iife: true, + } + input: { + foo = (function () { + return bar(); + })(); + } + expect_exact: 'foo=(function(){return bar()})();' +} + wrap_iife_in_return_call: { options = { negate_iife: false, From e51c6ba38014fc73a4804a69c556d96f777bc0b3 Mon Sep 17 00:00:00 2001 From: pengzhenqing Date: Sun, 9 Oct 2016 14:15:25 +0800 Subject: [PATCH 07/10] Add an option for writing inline source map --- README.md | 5 ++++- bin/uglifyjs | 23 +++++++++++++++-------- test/input/issue-1323/sample.js | 7 +++++++ test/mocha/cli.js | 21 +++++++++++++++++++++ test/mocha/minify.js | 19 +++++++++++++++++++ tools/node.js | 14 +++++++++----- 6 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 test/input/issue-1323/sample.js diff --git a/README.md b/README.md index ed2630f1..87d828a6 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ The available options are: --source-map-include-sources Pass this flag if you want to include the content of source files in the source map as sourcesContent property. + --source-map-inline Write base64-encoded source map to the end of js output. --in-source-map Input source map, useful if you're compressing JS that was generated from some other original code. @@ -641,7 +642,9 @@ var result = UglifyJS.minify({"file1.js": "var a = function () {};"}, { Note that the source map is not saved in a file, it's just returned in `result.map`. The value passed for `outSourceMap` is only used to set the -`file` attribute in the source map (see [the spec][sm-spec]). +`file` attribute in the source map (see [the spec][sm-spec]). You can set +option `sourceMapInline` to be `true` and source map will be appended to +code. You can also specify sourceRoot property to be included in source map: ```javascript diff --git a/bin/uglifyjs b/bin/uglifyjs index da9e0f10..76438961 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -23,6 +23,7 @@ mangling you need to use `-c` and `-m`.\ .describe("source-map", "Specify an output file where to generate source map.") .describe("source-map-root", "The path to the original source to be included in the source map.") .describe("source-map-url", "The path to the source map to be added in //# sourceMappingURL. Defaults to the value passed with --source-map.") + .describe("source-map-inline", "Write base64-encoded source map to the end of js output. Disabled by default") .describe("source-map-include-sources", "Pass this flag if you want to include the content of source files in the source map as sourcesContent property.") .describe("in-source-map", "Input source map, useful if you're compressing JS that was generated from some other original code.") .describe("screw-ie8", "Do not support Internet Explorer 6-8 quirks. This flag is enabled by default.") @@ -113,6 +114,7 @@ You need to pass an argument to this option to specify the name that your module .array("pure-funcs") .boolean("expr") + .boolean("source-map-inline") .boolean("source-map-include-sources") .boolean("screw-ie8") .boolean("support-ie8") @@ -309,7 +311,7 @@ var TOPLEVEL = null; var P_RELATIVE = ARGS.p && ARGS.p == "relative"; var SOURCES_CONTENT = {}; -var SOURCE_MAP = ARGS.source_map ? UglifyJS.SourceMap({ +var SOURCE_MAP = (ARGS.source_map || ARGS.source_map_inline) ? UglifyJS.SourceMap({ file: P_RELATIVE ? path.relative(path.dirname(ARGS.source_map), OUTPUT_FILE) : OUTPUT_FILE, root: ARGS.source_map_root, orig: ORIG_MAP, @@ -474,13 +476,18 @@ async.eachLimit(files, 1, function (file, cb) { output = output.get(); if (SOURCE_MAP) { - fs.writeFileSync(ARGS.source_map, SOURCE_MAP, "utf8"); - var source_map_url = ARGS.source_map_url || ( - P_RELATIVE - ? path.relative(path.dirname(OUTPUT_FILE), ARGS.source_map) - : ARGS.source_map - ); - output += "\n//# sourceMappingURL=" + source_map_url; + if (ARGS.source_map_inline) { + var base64_string = new Buffer(SOURCE_MAP.toString()).toString('base64'); + output += "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," + base64_string; + } else { + fs.writeFileSync(ARGS.source_map, SOURCE_MAP, "utf8"); + var source_map_url = ARGS.source_map_url || ( + P_RELATIVE + ? path.relative(path.dirname(OUTPUT_FILE), ARGS.source_map) + : ARGS.source_map + ); + output += "\n//# sourceMappingURL=" + source_map_url; + } } if (OUTPUT_FILE) { diff --git a/test/input/issue-1323/sample.js b/test/input/issue-1323/sample.js new file mode 100644 index 00000000..ff56acc3 --- /dev/null +++ b/test/input/issue-1323/sample.js @@ -0,0 +1,7 @@ +var bar = (function () { + function foo (bar) { + return bar; + } + + return foo; +})(); \ No newline at end of file diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 495b0076..bebd4d9d 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -49,4 +49,25 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should append source map to output when using --source-map-inline", function (done) { + var command = uglifyjscmd + ' test/input/issue-1323/sample.js --source-map-inline'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "var bar=function(){function foo(bar){return bar}return foo}();\n" + + "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvaW5wdXQvaXNzdWUtMTMyMy9zYW1wbGUuanMiXSwibmFtZXMiOlsiYmFyIiwiZm9vIl0sIm1hcHBpbmdzIjoiQUFBQSxHQUFJQSxLQUFNLFdBQ04sUUFBU0MsS0FBS0QsS0FDVixNQUFPQSxLQUdYLE1BQU9DIn0=\n"); + done(); + }); + }); + it("should not append source map to output when not using --source-map-inline", function (done) { + var command = uglifyjscmd + ' test/input/issue-1323/sample.js'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "var bar=function(){function foo(bar){return bar}return foo}();\n"); + done(); + }); + }); }); diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 2adbadcb..ce5e8497 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -75,4 +75,23 @@ describe("minify", function() { 'let foo = x => "foo " + x;\nconsole.log(foo("bar"));'); }); }); + + describe("sourceMapInline", function() { + it("should append source map to output js when sourceMapInline is enabled", function() { + var result = Uglify.minify('var a = function(foo) { return foo; };', { + fromString: true, + sourceMapInline: true + }); + var code = result.code; + assert.strictEqual(code, "var a=function(n){return n};\n" + + "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIj8iXSwibmFtZXMiOlsiYSIsImZvbyJdLCJtYXBwaW5ncyI6IkFBQUEsR0FBSUEsR0FBSSxTQUFTQyxHQUFPLE1BQU9BIn0="); + }); + it("should not append source map to output js when sourceMapInline is not enabled", function() { + var result = Uglify.minify('var a = function(foo) { return foo; };', { + fromString: true + }); + var code = result.code; + assert.strictEqual(code, "var a=function(n){return n};"); + }); + }); }); diff --git a/tools/node.js b/tools/node.js index 6712ccf6..a16169b1 100644 --- a/tools/node.js +++ b/tools/node.js @@ -44,6 +44,7 @@ exports.minify = function(files, options) { sourceRoot : null, inSourceMap : null, sourceMapUrl : null, + sourceMapInline : false, fromString : false, warnings : false, mangle : {}, @@ -117,7 +118,7 @@ exports.minify = function(files, options) { if (typeof options.inSourceMap == "string") { inMap = JSON.parse(fs.readFileSync(options.inSourceMap, "utf8")); } - if (options.outSourceMap) { + if (options.outSourceMap || options.sourceMapInline) { output.source_map = UglifyJS.SourceMap({ file: options.outSourceMap, orig: inMap, @@ -138,16 +139,19 @@ exports.minify = function(files, options) { var stream = UglifyJS.OutputStream(output); toplevel.print(stream); - var mappingUrlPrefix = "\n//# sourceMappingURL="; - if (options.outSourceMap && typeof options.outSourceMap === "string" && options.sourceMapUrl !== false) { - stream += mappingUrlPrefix + (typeof options.sourceMapUrl === "string" ? options.sourceMapUrl : options.outSourceMap); - } var source_map = output.source_map; if (source_map) { source_map = source_map + ""; } + var mappingUrlPrefix = "\n//# sourceMappingURL="; + if (options.sourceMapInline) { + stream += mappingUrlPrefix + "data:application/json;charset=utf-8;base64," + new Buffer(source_map).toString("base64"); + } else if (options.outSourceMap && typeof options.outSourceMap === "string" && options.sourceMapUrl !== false) { + stream += mappingUrlPrefix + (typeof options.sourceMapUrl === "string" ? options.sourceMapUrl : options.outSourceMap); + } + return { code : stream + "", map : source_map From 266ddd96399afcdee56d9d58b287f912b8728342 Mon Sep 17 00:00:00 2001 From: Jann Horn Date: Fri, 14 Oct 2016 18:04:39 +0200 Subject: [PATCH 08/10] fix uses_arguments handling (broken since 6605d1578351) Using the symbol declaration tracking of UglifyJS doesn't make sense here anyway, `arguments` always comes from something in the current scope. fixes #1299 --- lib/scope.js | 6 +++--- test/mocha/arguments.js | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index fb583291..4943b568 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -204,6 +204,9 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } } var sym = node.scope.find_variable(name); + if (node.scope instanceof AST_Lambda && name == "arguments") { + node.scope.uses_arguments = true; + } if (!sym) { var g; if (globals.has(name)) { @@ -215,9 +218,6 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ globals.set(name, g); } sym = g; - if (func && name == "arguments") { - func.uses_arguments = true; - } } node.thedef = sym; if (parent instanceof AST_Unary && (parent.operator === '++' || parent.operator === '--') diff --git a/test/mocha/arguments.js b/test/mocha/arguments.js index 089826fc..73993a73 100644 --- a/test/mocha/arguments.js +++ b/test/mocha/arguments.js @@ -19,4 +19,12 @@ describe("arguments", function() { value // Select function as scope ); }); + + it("Should recognize when a function uses arguments", function() { + var ast = UglifyJS.parse("function a(){function b(){function c(){}; return arguments[0];}}"); + ast.figure_out_scope(); + assert.strictEqual(ast.body[0].uses_arguments, false); + assert.strictEqual(ast.body[0].body[0].uses_arguments, true); + assert.strictEqual(ast.body[0].body[0].body[0].uses_arguments, false); + }); }); \ No newline at end of file From 8d74f3437352e22b3fd18ce602a4378170ec6598 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sun, 16 Oct 2016 20:57:28 +0200 Subject: [PATCH 09/10] Don't filter shebangs when using the 'some' comment filter Also clarify documentation a bit more about using regexp as filter --- README.md | 9 +++++---- bin/uglifyjs | 2 +- lib/output.js | 1 + test/mocha/comment-filter.js | 5 +++++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 87d828a6..789219dc 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,8 @@ The available options are: "@preserve". You can optionally pass one of the following arguments to this flag: - "all" to keep all comments - - a valid JS regexp (needs to start with a - slash) to keep only comments that match. + - a valid JS RegExp like `/foo/` or `/^!/` to + keep only matching comments. Note that currently not *all* comments can be kept when compression is on, because of dead code removal or cascading statements into @@ -855,10 +855,11 @@ which we care about here are `source_map` and `comments`. #### Keeping comments in the output In order to keep certain comments in the output you need to pass the -`comments` option. Pass a RegExp, boolean or a function. Stringified options +`comments` option. Pass a RegExp (as string starting and closing with `/` +or pass a RegExp object), a boolean or a function. Stringified options `all` and `some` can be passed too, where `some` behaves like it's cli equivalent `--comments` without passing a value. If you pass a RegExp, -only those comments whose body matches the regexp will be kept. Note that body +only those comments whose body matches the RegExp will be kept. Note that body means without the initial `//` or `/*`. If you pass a function, it will be called for every comment in the tree and will receive two arguments: the node that the comment is attached to, and the comment token itself. diff --git a/bin/uglifyjs b/bin/uglifyjs index 76438961..ce2e9411 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -47,7 +47,7 @@ Use -c with no argument to use the default compression options.") By default this works like Google Closure, keeping JSDoc-style comments that contain \"@license\" or \"@preserve\". \ You can optionally pass one of the following arguments to this flag:\n\ - \"all\" to keep all comments\n\ -- a valid JS regexp (needs to start with a slash) to keep only comments that match.\n\ +- a valid JS RegExp like `/foo/`or `/^!/` to keep only matching comments.\n\ \ Note that currently not *all* comments can be kept when compression is on, \ because of dead code removal or cascading statements into sequences.") diff --git a/lib/output.js b/lib/output.js index 014dac46..63a78c60 100644 --- a/lib/output.js +++ b/lib/output.js @@ -101,6 +101,7 @@ function OutputStream(options) { // multiline comment return /@preserve|@license|@cc_on/i.test(text); } + return type == "comment5"; } } else if (options.comments){ // NOTE includes "all" option diff --git a/test/mocha/comment-filter.js b/test/mocha/comment-filter.js index 4330d1eb..79162755 100644 --- a/test/mocha/comment-filter.js +++ b/test/mocha/comment-filter.js @@ -57,4 +57,9 @@ describe("comment filters", function() { assert.strictEqual(ast.print_to_string({comments: f}), "#!Random comment\n//test1\n/*test2*/\n"); }); + + it("Should never be able to filter comment5 when using 'some' as filter", function() { + var ast = UglifyJS.parse("#!foo\n//foo\n/*@preserve*/\n/* please hide me */"); + assert.strictEqual(ast.print_to_string({comments: "some"}), "#!foo\n/*@preserve*/\n"); + }); }); From 557b3e412fb44906e0a41dc2c209e5712bd67974 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Sun, 23 Oct 2016 21:46:22 +0200 Subject: [PATCH 10/10] v2.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4c3a807e..2741f026 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.7.3", + "version": "2.7.4", "engines": { "node": ">=0.8.0" },