Merge branch 'master' into harmony

This commit is contained in:
Richard van Velzen
2016-11-29 20:50:27 +01:00
17 changed files with 464 additions and 87 deletions

View File

@@ -285,6 +285,21 @@ of mangled property names.
Using the name cache is not necessary if you compress all your files in a
single call to UglifyJS.
#### Debugging property name mangling
You can also pass `--mangle-props-debug` in order to mangle property names
without completely obscuring them. For example the property `o.foo`
would mangle to `o._$foo$_` with this option. This allows property mangling
of a large codebase while still being able to debug the code and identify
where mangling is breaking things.
You can also pass a custom suffix using `--mangle-props-debug=XYZ`. This would then
mangle `o.foo` to `o._$foo$XYZ_`. You can change this each time you compile a
script to identify how a property got mangled. One technique is to pass a
random number on every compile to simulate mangling changing with different
inputs (e.g. as you update the input script with new properties), and to help
identify mistakes like writing mangled keys to storage.
## Compressor options
You need to pass `--compress` (`-c`) to enable the compressor. Optionally
@@ -639,15 +654,22 @@ To generate a source map with the fromString option, you can also use an object:
```javascript
var result = UglifyJS.minify({"file1.js": "var a = function () {};"}, {
outSourceMap: "out.js.map",
outFileName: "out.js",
fromString: true
});
```
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]). You can set
option `sourceMapInline` to be `true` and source map will be appended to
code.
`result.map`. The value passed for `outSourceMap` is only used to set
`//# sourceMappingURL=out.js.map` in `result.code`. The value of
`outFileName` is only used to set `file` attribute in source map file.
The `file` attribute in the source map (see [the spec][sm-spec]) will
use `outFileName` firstly, if it's falsy, then will be deduced from
`outSourceMap` (by removing `'.map'`).
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
@@ -752,6 +774,7 @@ Other options:
- `regex` — Pass a RegExp to only mangle certain names (maps to the `--mangle-regex` CLI arguments option)
- `ignore_quoted` Only mangle unquoted property names (maps to the `--mangle-props 2` CLI arguments option)
- `debug` Mangle names with the original name still present (maps to the `--mangle-props-debug` CLI arguments option). Defaults to `false`. Pass an empty string to enable, or a non-empty string to set the suffix.
We could add more options to `UglifyJS.minify` — if you need additional
functionality please suggest!

View File

@@ -98,6 +98,7 @@ You need to pass an argument to this option to specify the name that your module
.string("beautify")
.string("m")
.string("mangle")
.string("mangle-props-debug")
.string("c")
.string("compress")
.string("d")
@@ -419,7 +420,8 @@ async.eachLimit(files, 1, function (file, cb) {
cache : cache,
only_cache : !ARGS.mangle_props,
regex : regex,
ignore_quoted : ARGS.mangle_props == 2
ignore_quoted : ARGS.mangle_props == 2,
debug : typeof ARGS.mangle_props_debug === "undefined" ? false : ARGS.mangle_props_debug
});
writeNameCache("props", cache);
})();

View File

@@ -932,7 +932,7 @@ merge(Compressor.prototype, {
(function (def){
var unary_bool = [ "!", "delete" ];
var binary_bool = [ "in", "instanceof", "==", "!=", "===", "!==", "<", "<=", ">=", ">" ];
def(AST_Node, function(){ return false });
def(AST_Node, return_false);
def(AST_UnaryPrefix, function(){
return member(this.operator, unary_bool);
});
@@ -950,8 +950,8 @@ merge(Compressor.prototype, {
def(AST_Seq, function(){
return this.cdr.is_boolean();
});
def(AST_True, function(){ return true });
def(AST_False, function(){ return true });
def(AST_True, return_true);
def(AST_False, return_true);
})(function(node, func){
node.DEFMETHOD("is_boolean", func);
});
@@ -1228,11 +1228,11 @@ merge(Compressor.prototype, {
// determine if expression has side effects
(function(def){
def(AST_Node, function(compressor){ return true });
def(AST_Node, return_true);
def(AST_EmptyStatement, function(compressor){ return false });
def(AST_Constant, function(compressor){ return false });
def(AST_This, function(compressor){ return false });
def(AST_EmptyStatement, return_false);
def(AST_Constant, return_false);
def(AST_This, return_false);
def(AST_Call, function(compressor){
var pure = compressor.option("pure_funcs");
@@ -1252,15 +1252,15 @@ merge(Compressor.prototype, {
def(AST_SimpleStatement, function(compressor){
return this.body.has_side_effects(compressor);
});
def(AST_Defun, function(compressor){ return true });
def(AST_Function, function(compressor){ return false });
def(AST_Class, function(compressor){ return false });
def(AST_DefClass, function(compressor){ return true });
def(AST_Defun, return_true);
def(AST_Function, return_false);
def(AST_Class, return_false);
def(AST_DefClass, return_true);
def(AST_Binary, function(compressor){
return this.left.has_side_effects(compressor)
|| this.right.has_side_effects(compressor);
});
def(AST_Assign, function(compressor){ return true });
def(AST_Assign, return_true);
def(AST_Conditional, function(compressor){
return this.condition.has_side_effects(compressor)
|| this.consequent.has_side_effects(compressor)

View File

@@ -45,6 +45,20 @@
var EXPECT_DIRECTIVE = /^$|[;{][\s\n]*$/;
function is_some_comments(comment) {
var text = comment.value;
var type = comment.type;
if (type == "comment2") {
// multiline comment
return /@preserve|@license|@cc_on/i.test(text);
}
return type == "comment5";
}
function is_comment5(comment) {
return comment.type == "comment5";
}
function OutputStream(options) {
options = defaults(options, {
@@ -81,46 +95,30 @@ function OutputStream(options) {
options.shorthand = options.ecma > 5;
// 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);
}
return type == "comment5";
var comment_filter = options.shebang ? is_comment5 : return_false; // Default case, throw all comments away except shebangs
if (options.comments) {
var comments = options.comments;
if (typeof options.comments === "string" && /^\/.*\/[a-zA-Z]*$/.test(options.comments)) {
var regex_pos = options.comments.lastIndexOf("/");
comments = new RegExp(
options.comments.substr(1, regex_pos - 1),
options.comments.substr(regex_pos + 1)
);
}
}
else if (options.comments){ // NOTE includes "all" option
options.comments = function() {
return true;
if (comments instanceof RegExp) {
comment_filter = function(comment) {
return comment.type == "comment5" || comments.test(comment.value);
};
}
} else {
// Falsy case, so reject all comments, except shebangs
options.comments = function(comment) {
return comment.type == "comment5";
else if (typeof comments === "function") {
comment_filter = function(comment) {
return comment.type == "comment5" || comments(this, comment);
};
}
else if (comments === "some") {
comment_filter = is_some_comments;
} else { // NOTE includes "all" option
comment_filter = return_true;
}
}
@@ -470,6 +468,7 @@ function OutputStream(options) {
with_square : with_square,
add_mapping : add_mapping,
option : function(opt) { return options[opt] },
comment_filter : comment_filter,
line : function() { return current_line },
col : function() { return current_col },
pos : function() { return current_pos },
@@ -552,7 +551,7 @@ function OutputStream(options) {
}));
}
comments = comments.filter(output.option("comments"), self);
comments = comments.filter(output.comment_filter, self);
// Keep single line comments after nlb, after nlb
if (!output.option("beautify") && comments.length > 0 &&

View File

@@ -83,7 +83,8 @@ function mangle_properties(ast, options) {
cache : null,
only_cache : false,
regex : null,
ignore_quoted : false
ignore_quoted : false,
debug : false
});
var reserved = options.reserved;
@@ -101,6 +102,15 @@ function mangle_properties(ast, options) {
var regex = options.regex;
var ignore_quoted = options.ignore_quoted;
// note debug is either false (disabled), or a string of the debug suffix to use (enabled).
// note debug may be enabled as an empty string, which is falsey. Also treat passing 'true'
// the same as passing an empty string.
var debug = (options.debug !== false);
var debug_name_suffix;
if (debug) {
debug_name_suffix = (options.debug === true ? "" : options.debug);
}
var names_to_mangle = [];
var unmangleable = [];
var ignored = {};
@@ -202,9 +212,25 @@ function mangle_properties(ast, options) {
var mangled = cache.props.get(name);
if (!mangled) {
do {
mangled = base54(++cache.cname);
} while (!can_mangle(mangled));
if (debug) {
// debug mode: use a prefix and suffix to preserve readability, e.g. o.foo -> o._$foo$NNN_.
var debug_mangled = "_$" + name + "$" + debug_name_suffix + "_";
if (can_mangle(debug_mangled) && !(ignore_quoted && debug_mangled in ignored)) {
mangled = debug_mangled;
}
}
// either debug mode is off, or it is on and we could not use the mangled name
if (!mangled) {
// note can_mangle() does not check if the name collides with the 'ignored' set
// (filled with quoted properties when ignore_quoted set). Make sure we add this
// check so we don't collide with a quoted name.
do {
mangled = base54(++cache.cname);
} while (!can_mangle(mangled) || (ignore_quoted && mangled in ignored));
}
cache.props.set(name, mangled);
}
return mangled;

View File

@@ -60,7 +60,7 @@ function SourceMap(options) {
var orig_map = options.orig && new MOZ_SourceMap.SourceMapConsumer(options.orig);
if (orig_map && Array.isArray(options.orig.sources)) {
options.orig.sources.forEach(function(source) {
orig_map._sources.toArray().forEach(function(source) {
var sourceContent = orig_map.sourceContentFor(source, true);
if (sourceContent) {
generator.setSourceContent(source, sourceContent);

View File

@@ -112,6 +112,8 @@ function merge(obj, ext) {
};
function noop() {};
function return_false() { return false; }
function return_true() { return true; }
var MAP = (function(){
function MAP(a, f, backwards) {

View File

@@ -0,0 +1,54 @@
issue_1321_no_debug: {
mangle_props = {
ignore_quoted: true
}
input: {
var x = {};
x.foo = 1;
x["a"] = 2 * x.foo;
console.log(x.foo, x["a"]);
}
expect: {
var x = {};
x.b = 1;
x["a"] = 2 * x.b;
console.log(x.b, x["a"]);
}
}
issue_1321_debug: {
mangle_props = {
ignore_quoted: true,
debug: ""
}
input: {
var x = {};
x.foo = 1;
x["_$foo$_"] = 2 * x.foo;
console.log(x.foo, x["_$foo$_"]);
}
expect: {
var x = {};
x.a = 1;
x["_$foo$_"] = 2 * x.a;
console.log(x.a, x["_$foo$_"]);
}
}
issue_1321_with_quoted: {
mangle_props = {
ignore_quoted: false
}
input: {
var x = {};
x.foo = 1;
x["a"] = 2 * x.foo;
console.log(x.foo, x["a"]);
}
expect: {
var x = {};
x.a = 1;
x["b"] = 2 * x.a;
console.log(x.a, x["b"]);
}
}

View File

@@ -143,6 +143,98 @@ mangle_unquoted_properties: {
}
}
mangle_debug: {
mangle_props = {
debug: ""
};
input: {
a.foo = "bar";
x = { baz: "ban" };
}
expect: {
a._$foo$_ = "bar";
x = { _$baz$_: "ban" };
}
}
mangle_debug_true: {
mangle_props = {
debug: true
};
input: {
a.foo = "bar";
x = { baz: "ban" };
}
expect: {
a._$foo$_ = "bar";
x = { _$baz$_: "ban" };
}
}
mangle_debug_suffix: {
mangle_props = {
debug: "XYZ"
};
input: {
a.foo = "bar";
x = { baz: "ban" };
}
expect: {
a._$foo$XYZ_ = "bar";
x = { _$baz$XYZ_: "ban" };
}
}
mangle_debug_suffix_ignore_quoted: {
options = {
properties: false
}
mangle_props = {
ignore_quoted: true,
debug: "XYZ",
reserved: []
}
beautify = {
beautify: false,
quote_style: 3,
keep_quoted_props: true,
}
input: {
a.top = 1;
function f1() {
a["foo"] = "bar";
a.color = "red";
a.stuff = 2;
x = {"bar": 10, size: 7};
a.size = 9;
}
function f2() {
a.foo = "bar";
a['color'] = "red";
x = {bar: 10, size: 7};
a.size = 9;
a.stuff = 3;
}
}
expect: {
a._$top$XYZ_ = 1;
function f1() {
a["foo"] = "bar";
a.color = "red";
a._$stuff$XYZ_ = 2;
x = {"bar": 10, _$size$XYZ_: 7};
a._$size$XYZ_ = 9;
}
function f2() {
a.foo = "bar";
a['color'] = "red";
x = {bar: 10, _$size$XYZ_: 7};
a._$size$XYZ_ = 9;
a._$stuff$XYZ_ = 3;
}
}
}
first_256_chars_as_properties: {
beautify = {
ascii_only: true,

View File

@@ -15,4 +15,116 @@ dont_screw: {
input: f("\v");
expect_exact: 'f("\\x0B");';
}
}
do_screw_try_catch: {
options = { screw_ie8: true };
mangle = { screw_ie8: true };
beautify = { screw_ie8: true };
input: {
good = function(e){
return function(error){
try{
e()
} catch(e) {
error(e)
}
}
};
}
expect: {
good = function(n){
return function(t){
try{
n()
} catch(n) {
t(n)
}
}
};
}
}
dont_screw_try_catch: {
options = { screw_ie8: false };
mangle = { screw_ie8: false };
beautify = { screw_ie8: false };
input: {
bad = function(e){
return function(error){
try{
e()
} catch(e) {
error(e)
}
}
};
}
expect: {
bad = function(n){
return function(n){
try{
t()
} catch(t) {
n(t)
}
}
};
}
}
do_screw_try_catch_undefined: {
options = { screw_ie8: true };
mangle = { screw_ie8: true };
beautify = { screw_ie8: true };
input: {
function a(b){
try {
throw 'Stuff';
} catch (undefined) {
console.log('caught: ' + undefined);
}
console.log('undefined is ' + undefined);
return b === undefined;
};
}
expect: {
function a(o){
try{
throw "Stuff"
} catch (o) {
console.log("caught: "+o)
}
console.log("undefined is " + void 0);
return void 0===o
}
}
}
dont_screw_try_catch_undefined: {
options = { screw_ie8: false };
mangle = { screw_ie8: false };
beautify = { screw_ie8: false };
input: {
function a(b){
try {
throw 'Stuff';
} catch (undefined) {
console.log('caught: ' + undefined);
}
console.log('undefined is ' + undefined);
return b === undefined;
};
}
expect: {
function a(o){
try{
throw "Stuff"
} catch (n) {
console.log("caught: "+n)
}
console.log("undefined is " + void 0);
return void 0===o
}
}
}

View File

@@ -2,7 +2,7 @@ var UglifyJS = require('../../');
var assert = require("assert");
describe("comment filters", function() {
it("Should be able to filter comments by passing regex", function() {
it("Should be able to filter comments by passing regexp", function() {
var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\n<!--test5\n<!--!test6\n-->test7\n-->!test8");
assert.strictEqual(ast.print_to_string({comments: /^!/}), "/*!test1*/\n//!test3\n//!test6\n//!test8\n");
});
@@ -62,4 +62,14 @@ describe("comment filters", 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");
});
it("Should have no problem on multiple calls", function() {
const options = {
comments: /ok/
};
assert.strictEqual(UglifyJS.parse("/* ok */ function a(){}").print_to_string(options), "/* ok */function a(){}");
assert.strictEqual(UglifyJS.parse("/* ok */ function a(){}").print_to_string(options), "/* ok */function a(){}");
assert.strictEqual(UglifyJS.parse("/* ok */ function a(){}").print_to_string(options), "/* ok */function a(){}");
});
});

View File

@@ -3,32 +3,55 @@ var assert = require("assert");
var SourceMapConsumer = require("source-map").SourceMapConsumer;
describe("input sourcemaps", function() {
var transpiled = '"use strict";\n\n' +
'var foo = function foo(x) {\n return "foo " + x;\n};\n' +
'console.log(foo("bar"));\n\n' +
'//# sourceMappingURL=bundle.js.map';
var transpilemap, map;
var transpilemap = {
"version": 3,
"sources": ["index.js"],
"names": [],
"mappings": ";;AAAA,IAAI,MAAM,SAAN,GAAM;AAAA,SAAK,SAAS,CAAd;AAAA,CAAV;AACA,QAAQ,GAAR,CAAY,IAAI,KAAJ,CAAZ",
"file": "bundle.js",
"sourcesContent": ["let foo = x => \"foo \" + x;\nconsole.log(foo(\"bar\"));"]
};
function getMap() {
return {
"version": 3,
"sources": ["index.js"],
"names": [],
"mappings": ";;AAAA,IAAI,MAAM,SAAN,GAAM;AAAA,SAAK,SAAS,CAAd;AAAA,CAAV;AACA,QAAQ,GAAR,CAAY,IAAI,KAAJ,CAAZ",
"file": "bundle.js",
"sourcesContent": ["let foo = x => \"foo \" + x;\nconsole.log(foo(\"bar\"));"]
};
}
var result = Uglify.minify(transpiled, {
fromString: true,
inSourceMap: transpilemap,
outSourceMap: true
function prepareMap(sourceMap) {
var transpiled = '"use strict";\n\n' +
'var foo = function foo(x) {\n return "foo " + x;\n};\n' +
'console.log(foo("bar"));\n\n' +
'//# sourceMappingURL=bundle.js.map';
transpilemap = sourceMap || getMap();
var result = Uglify.minify(transpiled, {
fromString: true,
inSourceMap: transpilemap,
outSourceMap: true
});
map = new SourceMapConsumer(result.map);
}
beforeEach(function () {
prepareMap();
});
var map = new SourceMapConsumer(result.map);
it("Should copy over original sourcesContent", function() {
assert.equal(map.sourceContentFor("index.js"), transpilemap.sourcesContent[0]);
});
it("Final sourcemap should not have invalid mappings from inputSourceMap (issue #882) ", function() {
it("Should copy sourcesContent if sources are relative", function () {
var relativeMap = getMap();
relativeMap.sources = ['./index.js'];
prepareMap(relativeMap);
assert.notEqual(map.sourcesContent, null);
assert.equal(map.sourcesContent.length, 1);
assert.equal(map.sourceContentFor("index.js"), transpilemap.sourcesContent[0]);
});
it("Final sourcemap should not have invalid mappings from inputSourceMap (issue #882)", function() {
// The original source has only 2 lines, check that mappings don't have more lines
var msg = "Mapping should not have higher line number than the original file had";

View File

@@ -7,10 +7,18 @@ describe("Input file as map", function() {
'/scripts/foo.js': 'var foo = {"x": 1, y: 2, \'z\': 3};'
};
var result = Uglify.minify(jsMap, {fromString: true, outSourceMap: true});
var map = JSON.parse(result.map);
assert.strictEqual(result.code, 'var foo={x:1,y:2,z:3};');
assert.deepEqual(map.sources, ['/scripts/foo.js']);
assert.strictEqual(map.file, undefined);
result = Uglify.minify(jsMap, {fromString: true, outFileName: 'out.js'});
assert.strictEqual(result.map, null);
result = Uglify.minify(jsMap, {fromString: true, outFileName: 'out.js', outSourceMap: true});
map = JSON.parse(result.map);
assert.strictEqual(map.file, 'out.js');
});
it("Should accept array of objects and strings", function() {
@@ -19,7 +27,7 @@ describe("Input file as map", function() {
'var bar = 15;'
];
var result = Uglify.minify(jsSeq, {fromString: true, outSourceMap: true});
var map = JSON.parse(result.map);
assert.strictEqual(result.code, 'var foo={x:1,y:2,z:3},bar=15;');
assert.strictEqual(map.sources[0], '/scripts/foo.js');
@@ -31,7 +39,7 @@ describe("Input file as map", function() {
'var bar = 15;'
];
var result = Uglify.minify(jsSeq, {fromString: true, outSourceMap: true, sourceMapIncludeSources: true});
var map = JSON.parse(result.map);
assert.strictEqual(result.code, 'var foo={x:1,y:2,z:3},bar=15;');
assert.deepEqual(map.sourcesContent, ['var foo = {"x": 1, y: 2, \'z\': 3};', 'var bar = 15;']);

View File

@@ -63,13 +63,14 @@ describe("minify", function() {
describe("inSourceMap", function() {
it("Should read the given string filename correctly when sourceMapIncludeSources is enabled (#1236)", function() {
var result = Uglify.minify('./test/input/issue-1236/simple.js', {
outSourceMap: "simple.js.min.map",
outSourceMap: "simple.min.js.map",
inSourceMap: "./test/input/issue-1236/simple.js.map",
sourceMapIncludeSources: true
});
var map = JSON.parse(result.map);
assert.equal(map.file, 'simple.min.js');
assert.equal(map.sourcesContent.length, 1);
assert.equal(map.sourcesContent[0],
'let foo = x => "foo " + x;\nconsole.log(foo("bar"));');

23
test/mocha/screw-ie8.js Normal file
View File

@@ -0,0 +1,23 @@
var assert = require("assert");
var uglify = require("../../");
describe("screw-ie8", function () {
it("Should be able to minify() with undefined as catch parameter in a try...catch statement", function () {
assert.strictEqual(
uglify.minify(
"function a(b){\
try {\
throw 'Stuff';\
} catch (undefined) {\
console.log('caught: ' + undefined);\
}\
console.log('undefined is ' + undefined);\
return b === undefined;\
};", {
fromString: true
}
).code,
'function a(o){try{throw"Stuff"}catch(o){console.log("caught: "+o)}return console.log("undefined is "+void 0),void 0===o}'
);
});
});

View File

@@ -117,7 +117,7 @@ function run_compress_tests() {
input = U.mangle_properties(input, test.mangle_props);
}
var output = cmp.compress(input);
output.figure_out_scope();
output.figure_out_scope(test.mangle);
if (test.mangle) {
output.compute_char_frequency(test.mangle);
output.mangle_names(test.mangle);

View File

@@ -41,6 +41,7 @@ exports.minify = function(files, options) {
options = UglifyJS.defaults(options, {
spidermonkey : false,
outSourceMap : null,
outFileName : null,
sourceRoot : null,
inSourceMap : null,
sourceMapUrl : null,
@@ -120,7 +121,8 @@ exports.minify = function(files, options) {
}
if (options.outSourceMap || options.sourceMapInline) {
output.source_map = UglifyJS.SourceMap({
file: options.outSourceMap,
// prefer outFileName, otherwise use outSourceMap without .map suffix
file: options.outFileName || (typeof options.outSourceMap === 'string' ? options.outSourceMap.replace(/\.map$/i, '') : null),
orig: inMap,
root: options.sourceRoot
});