diff --git a/README.md b/README.md index c57e658ac49..643d6dac0f6 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ a double dash to prevent input files being used as option arguments: `url` If specified, path to the source map to append in `//# sourceMappingURL`. --stats Display operations run time on STDERR. - --toplevel Compress and/or mangle variables in toplevel scope. + --toplevel Compress and/or mangle variables in top level scope. --verbose Print diagnostic messages. --warn Print warning messages. --wrap Embed everything in a big function, making the @@ -199,7 +199,7 @@ Example: To enable the mangler you need to pass `--mangle` (`-m`). The following (comma-separated) options are supported: -- `toplevel` — mangle names declared in the toplevel scope (disabled by +- `toplevel` — mangle names declared in the top level scope (disabled by default). - `eval` — mangle names visible in scopes where `eval` or `with` are used @@ -298,9 +298,10 @@ like this: var UglifyJS = require("uglify-es"); ``` -There is a single high level minification function, `minify(code, options)`, which will -performs all the steps in a configurable manner. -Example: +There is a single high level function, **`minify(code, options)`**, +which will perform all minification [phases](#minify-options) in a configurable +manner. By default `minify()` will enable the options [`compress`](#compress-options) +and [`mangle`](#mangle-options). Example: ```javascript var code = "function add(first, second) { return first + second; }"; var result = UglifyJS.minify(code); @@ -308,14 +309,17 @@ console.log(result.error); // runtime error, or `undefined` if no error console.log(result.code); // minified output: function add(n,d){return n+d} ``` -You can also compress multiple files: +You can `minify` more than one JavaScript file at a time by using an object +for the first argument where the keys are file names and the values are source +code: ```javascript var code = { "file1.js": "function add(first, second) { return first + second; }", "file2.js": "console.log(add(1 + 2, 3 + 4));" }; var result = UglifyJS.minify(code); -console.log(result.code); // function add(d,n){return d+n}console.log(add(3,7)); +console.log(result.code); +// function add(d,n){return d+n}console.log(add(3,7)); ``` The `toplevel` option: @@ -326,7 +330,33 @@ var code = { }; var options = { toplevel: true }; var result = UglifyJS.minify(code, options); -console.log(result.code); // console.log(function(n,o){return n+o}(3,7)); +console.log(result.code); +// console.log(function(n,o){return n+o}(3,7)); +``` + +An example of a combination of `minify()` options: +```javascript +var code = { + "file1.js": "function add(first, second) { return first + second; }", + "file2.js": "console.log(add(1 + 2, 3 + 4));" +}; +var options = { + toplevel: true, + compress: { + global_defs: { + "@console.log": "alert" + }, + passes: 2 + }, + output: { + beautify: false, + preamble: "/* uglified */" + } +}; +var result = UglifyJS.minify(code, options); +console.log(result.code); +// /* uglified */ +// alert(10);" ``` To produce warnings: @@ -345,7 +375,7 @@ var result = UglifyJS.minify({"foo.js" : "if (0) else console.log(1);"}); console.log(JSON.stringify(result.error)); // {"message":"Unexpected token: keyword (else)","filename":"foo.js","line":1,"col":7,"pos":7} ``` -Note: unlike `uglify-js@2.x`, the `3.x` API does not throw errors. To +Note: unlike `uglify-js@2.x`, the `3.x` API does not throw errors. To achieve a similar effect one could do the following: ```javascript var result = UglifyJS.minify(code, options); @@ -354,7 +384,7 @@ if (result.error) throw result.error; ## Minify options -- `warnings` (default `false`) — pass `true` to return compressor warnings +- `warnings` (default `false`) — pass `true` to return compressor warnings in `result.warnings`. Use the value `"verbose"` for more detailed warnings. - `parse` (default `{}`) — pass an object if you wish to specify some @@ -518,7 +548,7 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u assignments do not count as references unless set to `"keep_assign"`) - `toplevel` -- drop unreferenced functions (`"funcs"`) and/or variables (`"vars"`) - in the toplevel scope (`false` by default, `true` to drop both unreferenced + in the top level scope (`false` by default, `true` to drop both unreferenced functions and variables) - `top_retain` -- prevent specific toplevel functions and variables from `unused` @@ -536,7 +566,7 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `cascade` -- small optimization for sequences, transform `x, x` into `x` and `x = something(), x` into `x = something()` -- `collapse_vars` -- Collapse single-use non-constant variables - side +- `collapse_vars` -- Collapse single-use non-constant variables - side effects permitting. - `reduce_vars` -- Improve optimization on variables assigned with and @@ -598,7 +628,7 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `reserved` - pass an array of identifiers that should be excluded from mangling -- `toplevel` — mangle names declared in the toplevel scope (disabled by +- `toplevel` — mangle names declared in the top level scope (disabled by default). - `eval` — mangle names visible in scopes where eval or with are used @@ -783,7 +813,7 @@ You can also use conditional compilation via the programmatic API. With the diff property name is `global_defs` and is a compressor property: ```javascript -var result = uglifyJS.minify(fs.readFileSync("input.js", "utf8"), { +var result = UglifyJS.minify(fs.readFileSync("input.js", "utf8"), { compress: { dead_code: true, global_defs: { @@ -793,6 +823,32 @@ var result = uglifyJS.minify(fs.readFileSync("input.js", "utf8"), { }); ``` +To replace an identifier with an arbitrary non-constant expression it is +necessary to prefix the `global_defs` key with `"@"` to instruct UglifyJS +to parse the value as an expression: +```javascript +UglifyJS.minify("alert('hello');", { + compress: { + global_defs: { + "@alert": "console.log" + } + } +}).code; +// returns: 'console.log("hello");' +``` + +Otherwise it would be replaced as string literal: +```javascript +UglifyJS.minify("alert('hello');", { + compress: { + global_defs: { + "alert": "console.log" + } + } +}).code; +// returns: '"console.log"("hello");' +``` + ### Using native Uglify AST with `minify()` ```javascript // example: parse only, produce native Uglify AST diff --git a/lib/compress.js b/lib/compress.js index c1f89bf72a4..0d6aae80757 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -88,6 +88,17 @@ function Compressor(options, false_by_default) { unused : !false_by_default, warnings : false, }, true); + var global_defs = this.options["global_defs"]; + if (typeof global_defs == "object") for (var key in global_defs) { + if (/^@/.test(key) && HOP(global_defs, key)) { + var ast = parse(global_defs[key]); + if (ast.body.length == 1 && ast.body[0] instanceof AST_SimpleStatement) { + global_defs[key.slice(1)] = ast.body[0].body; + } else throw new Error(string_template("Can't handle expression: {value}", { + value: global_defs[key] + })); + } + } var pure_funcs = this.options["pure_funcs"]; if (typeof pure_funcs == "function") { this.pure_funcs = pure_funcs; diff --git a/lib/output.js b/lib/output.js index 75c2b1c9ea8..5c45629d0b5 100644 --- a/lib/output.js +++ b/lib/output.js @@ -212,12 +212,43 @@ function OutputStream(options) { var might_need_semicolon = false; var might_add_newline = 0; var last = ""; + var mapping_token, mapping_name, mappings = options.source_map && []; + + var do_add_mapping = mappings ? function() { + mappings.forEach(function(mapping) { + try { + options.source_map.add( + mapping.token.file, + mapping.line, mapping.col, + mapping.token.line, mapping.token.col, + !mapping.name && mapping.token.type == "name" ? mapping.token.value : mapping.name + ); + } catch(ex) { + AST_Node.warn("Couldn't figure out mapping for {file}:{line},{col} → {cline},{ccol} [{name}]", { + file: mapping.token.file, + line: mapping.token.line, + col: mapping.token.col, + cline: mapping.line, + ccol: mapping.col, + name: mapping.name || "" + }) + } + }); + mappings = []; + } : noop; var ensure_line_len = options.max_line_len ? function() { if (current_col > options.max_line_len) { if (might_add_newline) { var left = OUTPUT.slice(0, might_add_newline); var right = OUTPUT.slice(might_add_newline); + if (mappings) { + var delta = right.length - current_col; + mappings.forEach(function(mapping) { + mapping.line++; + mapping.col += delta; + }); + } OUTPUT = left + "\n" + right; current_line++; current_pos++; @@ -227,7 +258,10 @@ function OutputStream(options) { AST_Node.warn("Output exceeds {max_line_len} characters", options); } } - might_add_newline = 0; + if (might_add_newline) { + might_add_newline = 0; + do_add_mapping(); + } } : noop; var requireSemicolonChars = makePredicate("( [ + * / - , ."); @@ -287,6 +321,18 @@ function OutputStream(options) { } might_need_space = false; } + + if (mapping_token) { + mappings.push({ + token: mapping_token, + name: mapping_name, + line: current_line, + col: current_col + }); + mapping_token = false; + if (!might_add_newline) do_add_mapping(); + } + OUTPUT += str; current_pos += str.length; var a = str.split(/\r?\n/), n = a.length - 1; @@ -385,24 +431,9 @@ function OutputStream(options) { space(); }; - var add_mapping = options.source_map ? function(token, name) { - try { - if (token) options.source_map.add( - token.file || "?", - current_line, current_col, - token.line, token.col, - (!name && token.type == "name") ? token.value : name - ); - } catch(ex) { - AST_Node.warn("Couldn't figure out mapping for {file}:{line},{col} → {cline},{ccol} [{name}]", { - file: token.file, - line: token.line, - col: token.col, - cline: current_line, - ccol: current_col, - name: name || "" - }) - } + var add_mapping = mappings ? function(token, name) { + mapping_token = token; + mapping_name = name; } : noop; function get() { diff --git a/package.json b/package.json index 3a4735c08f0..dcdc2f1dc30 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "https://github.com/mishoo/UglifyJS2/tree/harmony", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "3.0.10", + "version": "3.0.11", "engines": { "node": ">=0.8.0" }, diff --git a/test/compress/global_defs.js b/test/compress/global_defs.js index f1ba8f323b8..30c3232fe08 100644 --- a/test/compress/global_defs.js +++ b/test/compress/global_defs.js @@ -160,3 +160,17 @@ issue_1801: { console.log(!0); } } + +issue_1986: { + options = { + global_defs: { + "@alert": "console.log", + }, + } + input: { + alert(42); + } + expect: { + console.log(42); + } +} diff --git a/test/input/issue-505/input.js b/test/input/issue-505/input.js new file mode 100644 index 00000000000..600b12cf6d3 --- /dev/null +++ b/test/input/issue-505/input.js @@ -0,0 +1,5 @@ +function test(callback) { + 'aaaaaaaaaaaaaaaa'; + callback(err, data); + callback(err, data); +} diff --git a/test/input/issue-505/output.js b/test/input/issue-505/output.js new file mode 100644 index 00000000000..5a1bc018b5a --- /dev/null +++ b/test/input/issue-505/output.js @@ -0,0 +1,5 @@ +function test(a){ +"aaaaaaaaaaaaaaaa" +;a(err,data),a(err,data) +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIjAiXSwibmFtZXMiOlsidGVzdCIsImNhbGxiYWNrIiwiZXJyIiwiZGF0YSJdLCJtYXBwaW5ncyI6IkFBQUEsU0FBU0EsS0FBS0M7QUFDVjtDQUNBQSxFQUFTQyxJQUFLQyxNQUNkRixFQUFTQyxJQUFLQyJ9 \ No newline at end of file diff --git a/test/input/issue-520/output.js b/test/input/issue-520/output.js index 8d19855b988..f6d13804711 100644 --- a/test/input/issue-520/output.js +++ b/test/input/issue-520/output.js @@ -1,2 +1,2 @@ new function(){console.log(3)}; -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0ZGluIl0sIm5hbWVzIjpbImNvbnNvbGUiLCJsb2ciXSwibWFwcGluZ3MiOiJBQUErQyxHQUFyQyxZQUFnQkEsUUFBUUMsSUFBSSIsInNvdXJjZXNDb250ZW50IjpbImNsYXNzIEZvbyB7IGNvbnN0cnVjdG9yKCl7Y29uc29sZS5sb2coMSsyKTt9IH0gbmV3IEZvbygpO1xuIl19 +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0ZGluIl0sIm5hbWVzIjpbImNvbnNvbGUiLCJsb2ciXSwibWFwcGluZ3MiOiJBQUErQyxJQUFyQyxXQUFnQkEsUUFBUUMsSUFBSSIsInNvdXJjZXNDb250ZW50IjpbImNsYXNzIEZvbyB7IGNvbnN0cnVjdG9yKCl7Y29uc29sZS5sb2coMSsyKTt9IH0gbmV3IEZvbygpO1xuIl19 diff --git a/test/mocha/cli.js b/test/mocha/cli.js index abb0f3689d3..cf4a933a826 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -63,7 +63,7 @@ describe("bin/uglifyjs", function () { 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"); + "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvaW5wdXQvaXNzdWUtMTMyMy9zYW1wbGUuanMiXSwibmFtZXMiOlsiYmFyIiwiZm9vIl0sIm1hcHBpbmdzIjoiQUFBQSxJQUFJQSxJQUFNLFdBQ04sU0FBU0MsSUFBS0QsS0FDVixPQUFPQSxJQUdYLE9BQU9DIn0=\n"); done(); }); }); @@ -175,7 +175,7 @@ describe("bin/uglifyjs", function () { assert.strictEqual(stdout, [ "var bar=function(){function foo(bar){return bar}return foo}();", - "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvaW5wdXQvaXNzdWUtMTMyMy9zYW1wbGUuanMiXSwibmFtZXMiOlsiYmFyIiwiZm9vIl0sIm1hcHBpbmdzIjoiQUFBQSxHQUFJQSxLQUFNLFdBQ04sUUFBU0MsS0FBS0QsS0FDVixNQUFPQSxLQUdYLE1BQU9DIn0=", + "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvaW5wdXQvaXNzdWUtMTMyMy9zYW1wbGUuanMiXSwibmFtZXMiOlsiYmFyIiwiZm9vIl0sIm1hcHBpbmdzIjoiQUFBQSxJQUFJQSxJQUFNLFdBQ04sU0FBU0MsSUFBS0QsS0FDVixPQUFPQSxJQUdYLE9BQU9DIn0=", "", ].join("\n")); assert.strictEqual(stderr, "WARN: inline source map not found\n"); @@ -529,7 +529,7 @@ describe("bin/uglifyjs", function () { assert.strictEqual(stdout, [ '"use strict";var foo=function foo(x){return"foo "+x};console.log(foo("bar"));', - "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImluZGV4LmpzIl0sIm5hbWVzIjpbImZvbyIsIngiLCJjb25zb2xlIiwibG9nIl0sIm1hcHBpbmdzIjoiWUFBQSxJQUFJQSxLQUFNLFFBQU5BLEtBQU1DLEdBQUEsTUFBSyxPQUFTQSxFQUN4QkMsU0FBUUMsSUFBSUgsSUFBSSJ9", + "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImluZGV4LmpzIl0sIm5hbWVzIjpbImZvbyIsIngiLCJjb25zb2xlIiwibG9nIl0sIm1hcHBpbmdzIjoiYUFBQSxJQUFJQSxJQUFNLFNBQU5BLElBQU1DLEdBQUEsTUFBSyxPQUFTQSxHQUN4QkMsUUFBUUMsSUFBSUgsSUFBSSJ9", "" ].join("\n")); done(); diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 7812fe6b82e..77b798ec892 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -138,13 +138,25 @@ describe("minify", function() { }); var code = result.code; assert.strictEqual(code, "var a=function(n){return n};\n" + - "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIjAiXSwibmFtZXMiOlsiYSIsImZvbyJdLCJtYXBwaW5ncyI6IkFBQUEsR0FBSUEsR0FBSSxTQUFTQyxHQUFPLE1BQU9BIn0="); + "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIjAiXSwibmFtZXMiOlsiYSIsImZvbyJdLCJtYXBwaW5ncyI6IkFBQUEsSUFBSUEsRUFBSSxTQUFTQyxHQUFPLE9BQU9BIn0="); }); 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; };'); var code = result.code; assert.strictEqual(code, "var a=function(n){return n};"); }); + it("should work with max_line_len", function() { + var result = Uglify.minify(read("./test/input/issue-505/input.js"), { + output: { + max_line_len: 20 + }, + sourceMap: { + url: "inline" + } + }); + assert.strictEqual(result.error, undefined); + assert.strictEqual(result.code, read("./test/input/issue-505/output.js")); + }); }); describe("#__PURE__", function() { @@ -181,4 +193,19 @@ describe("minify", function() { assert.strictEqual(err.col, 12); }); }); + + describe("global_defs", function() { + it("should throw for non-trivial expressions", function() { + var result = Uglify.minify("alert(42);", { + compress: { + global_defs: { + "@alert": "debugger" + } + } + }); + var err = result.error; + assert.ok(err instanceof Error); + assert.strictEqual(err.stack.split(/\n/)[0], "Error: Can't handle expression: debugger"); + }); + }); }); diff --git a/test/sourcemaps.js b/test/sourcemaps.js index 4757d1e08d2..2717eff4e62 100644 --- a/test/sourcemaps.js +++ b/test/sourcemaps.js @@ -1,4 +1,4 @@ -var UglifyJS = require("./node"); +var UglifyJS = require(".."); var ok = require("assert"); module.exports = function () { @@ -26,11 +26,11 @@ module.exports = function () { } function source_map(js) { - var source_map = UglifyJS.SourceMap(); - var stream = UglifyJS.OutputStream({ source_map: source_map }); - var parsed = UglifyJS.parse(js); - parsed.print(stream); - return JSON.parse(source_map.toString()); + return JSON.parse(UglifyJS.minify(js, { + compress: false, + mangle: false, + sourceMap: true + }).map); } // Run standalone