From 827bcec1860534442e82e9ccdfc2aca808fada5e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 28 Mar 2020 14:18:56 +0000 Subject: [PATCH] handle `source-map` operations internally (#3754) --- lib/minify.js | 5 +- lib/sourcemap.js | 170 +++++++++++++++++++++++--------- package.json | 3 +- test/exports.js | 1 + test/input/issue-3040/expect.js | 2 +- test/input/issue-3294/output.js | 2 +- test/input/issue-520/output.js | 2 +- test/mocha/cli.js | 4 +- test/mocha/sourcemaps.js | 65 ++++++------ test/node.js | 4 +- tools/node.js | 4 +- 11 files changed, 173 insertions(+), 89 deletions(-) diff --git a/lib/minify.js b/lib/minify.js index 72d99576..ab397f5e 100644 --- a/lib/minify.js +++ b/lib/minify.js @@ -203,6 +203,7 @@ function minify(files, options) { if (!HOP(options.output, "code") || options.output.code) { if (options.sourceMap) { options.output.source_map = SourceMap({ + content: options.sourceMap.includeSources, file: options.sourceMap.filename, orig: source_maps, root: options.sourceMap.root @@ -211,10 +212,8 @@ function minify(files, options) { if (files instanceof AST_Toplevel) { throw new Error("original source content unavailable"); } else for (var name in files) if (HOP(files, name)) { - options.output.source_map.get().setSourceContent(name, files[name]); + options.output.source_map.setSourceContent(name, files[name]); } - } else { - options.output.source_map.get()._sourcesContents = null; } } delete options.output.ast; diff --git a/lib/sourcemap.js b/lib/sourcemap.js index 2feec45d..3ca9f9a8 100644 --- a/lib/sourcemap.js +++ b/lib/sourcemap.js @@ -43,62 +43,144 @@ "use strict"; -// a small wrapper around fitzgen's source-map library +var vlq_char = characters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); +var vlq_bits = vlq_char.reduce(function(map, ch, bits) { + map[ch] = bits; + return map; +}, Object.create(null)); + +function vlq_decode(indices, str) { + var value = 0; + var shift = 0; + for (var i = 0, j = 0; i < str.length; i++) { + var bits = vlq_bits[str[i]]; + value += (bits & 31) << shift; + if (bits & 32) { + shift += 5; + } else { + indices[j++] += value & 1 ? 0x80000000 | -(value >> 1) : value >> 1; + value = shift = 0; + } + } + return j; +} + +function vlq_encode(num) { + var result = ""; + num = Math.abs(num) << 1 | num >>> 31; + do { + var bits = num & 31; + if (num >>>= 5) bits |= 32; + result += vlq_char[bits]; + } while (num); + return result; +} + +function create_array_map() { + var map = Object.create(null); + var array = []; + array.index = function(name) { + if (!HOP(map, name)) { + map[name] = array.length; + array.push(name); + } + return map[name]; + }; + return array; +} + function SourceMap(options) { options = defaults(options, { + content: false, file: null, root: null, orig: null, - orig_line_diff: 0, - dest_line_diff: 0, }, true); - var generator = new MOZ_SourceMap.SourceMapGenerator({ - file: options.file, - sourceRoot: options.root - }); - var maps = options.orig && Object.create(null); - if (maps) for (var source in options.orig) { - var map = new MOZ_SourceMap.SourceMapConsumer(options.orig[source]); - if (Array.isArray(options.orig[source].sources)) { - map._sources.toArray().forEach(function(source) { - var sourceContent = map.sourceContentFor(source, true); - if (sourceContent) generator.setSourceContent(source, sourceContent); + var sources = create_array_map(); + var sources_content = options.content && Object.create(null); + var names = create_array_map(); + var mappings = ""; + if (options.orig) Object.keys(options.orig).forEach(function(name) { + var map = options.orig[name]; + var indices = [ 0, 0, 1, 0, 0 ]; + map.mappings = map.mappings.split(/;/).map(function(line) { + indices[0] = 0; + return line.split(/,/).map(function(segment) { + return indices.slice(0, vlq_decode(indices, segment)); }); + }); + if (!sources_content || !map.sourcesContent) return; + for (var i = 0; i < map.sources.length; i++) { + var content = map.sourcesContent[i]; + if (content) sources_content[map.sources[i]] = content; } - maps[source] = map; - } + }); + var generated_line = 1; + var generated_column = 0; + var source_index = 0; + var original_line = 1; + var original_column = 0; + var name_index = 0; return { - add: function(source, gen_line, gen_col, orig_line, orig_col, name) { - var map = maps && maps[source]; + add: options.orig ? function(source, gen_line, gen_col, orig_line, orig_col, name) { + var map = options.orig[source]; if (map) { - var info = map.originalPositionFor({ - line: orig_line, - column: orig_col - }); - if (info.source === null) return; - source = info.source; - orig_line = info.line; - orig_col = info.column; - name = info.name || name; - } - generator.addMapping({ - name: name, - source: source, - generated: { - line: gen_line + options.dest_line_diff, - column: gen_col - }, - original: { - line: orig_line + options.orig_line_diff, - column: orig_col + var segments = map.mappings[orig_line - 1]; + if (!segments) return; + var indices; + for (var i = 0; i < segments.length; i++) { + var col = segments[i][0]; + if (orig_col >= col) indices = segments[i]; + if (orig_col <= col) break; } - }); - }, - get: function() { - return generator; - }, + if (!indices) return; + source = map.sources[indices[1]]; + orig_line = indices[2]; + orig_col = indices[3]; + if (indices.length > 4) name = map.names[indices[4]]; + } + add(source, gen_line, gen_col, orig_line, orig_col, name); + } : add, + setSourceContent: sources_content ? function(source, content) { + sources_content[source] = content; + } : noop, toString: function() { - return JSON.stringify(generator.toJSON()); + return JSON.stringify({ + version: 3, + file: options.file || undefined, + sourceRoot: options.root || undefined, + sources: sources, + sourcesContent: sources_content ? sources.map(function(source) { + return sources_content[source] || null; + }) : undefined, + names: names, + mappings: mappings, + }); } }; + + function add(source, gen_line, gen_col, orig_line, orig_col, name) { + if (generated_line < gen_line) { + generated_column = 0; + do { + mappings += ";"; + } while (++generated_line < gen_line); + } else if (mappings) { + mappings += ","; + } + mappings += vlq_encode(gen_col - generated_column); + generated_column = gen_col; + var src_idx = sources.index(source); + mappings += vlq_encode(src_idx - source_index); + source_index = src_idx; + mappings += vlq_encode(orig_line - original_line); + original_line = orig_line; + mappings += vlq_encode(orig_col - original_column); + original_column = orig_col; + if (name != null) { + var name_idx = names.index(name); + mappings += vlq_encode(name_idx - name_index); + name_index = name_idx; + } + } } diff --git a/package.json b/package.json index d4e4d74b..463afcf5 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,7 @@ "LICENSE" ], "dependencies": { - "commander": "~2.20.3", - "source-map": "~0.6.1" + "commander": "~2.20.3" }, "devDependencies": { "acorn": "~7.1.0", diff --git a/test/exports.js b/test/exports.js index 1274ec6e..fd1237f3 100644 --- a/test/exports.js +++ b/test/exports.js @@ -13,3 +13,4 @@ exports["to_ascii"] = to_ascii; exports["tokenizer"] = tokenizer; exports["TreeTransformer"] = TreeTransformer; exports["TreeWalker"] = TreeWalker; +exports["vlq_decode"] = vlq_decode; diff --git a/test/input/issue-3040/expect.js b/test/input/issue-3040/expect.js index b911f3f8..199da039 100644 --- a/test/input/issue-3040/expect.js +++ b/test/input/issue-3040/expect.js @@ -1,2 +1,2 @@ function _toConsumableArray(arr){if(Array.isArray(arr)){for(var i=0,arr2=Array(arr.length);i "foo " + x;\nconsole.log(foo("bar"));'); }); it("Should process inline source map", function() { - var result = UglifyJS.minify(read("./test/input/issue-520/input.js"), { + var result = UglifyJS.minify(read("test/input/issue-520/input.js"), { compress: { toplevel: true }, sourceMap: { content: "inline", @@ -131,10 +130,10 @@ describe("sourcemaps", function() { } }); if (result.error) throw result.error; - assert.strictEqual(result.code + "\n", readFileSync("test/input/issue-520/output.js", "utf8")); + assert.strictEqual(result.code + "\n", read("test/input/issue-520/output.js")); }); it("Should warn for missing inline source map", function() { - var result = UglifyJS.minify(read("./test/input/issue-1323/sample.js"), { + var result = UglifyJS.minify(read("test/input/issue-1323/sample.js"), { mangle: false, sourceMap: { content: "inline" @@ -146,8 +145,8 @@ describe("sourcemaps", function() { }); it("Should handle multiple input and inline source map", function() { var result = UglifyJS.minify([ - read("./test/input/issue-520/input.js"), - read("./test/input/issue-1323/sample.js"), + read("test/input/issue-520/input.js"), + read("test/input/issue-1323/sample.js"), ], { sourceMap: { content: "inline", @@ -163,7 +162,7 @@ describe("sourcemaps", function() { assert.deepEqual(result.warnings, [ "WARN: inline source map not found: 1" ]); }); it("Should drop source contents for includeSources=false", function() { - var result = UglifyJS.minify(read("./test/input/issue-520/input.js"), { + var result = UglifyJS.minify(read("test/input/issue-520/input.js"), { compress: false, mangle: false, sourceMap: { @@ -186,7 +185,7 @@ describe("sourcemaps", function() { assert.ok(!("sourcesContent" in map)); }); it("Should parse the correct sourceMappingURL", function() { - var result = UglifyJS.minify(read("./test/input/issue-3294/input.js"), { + var result = UglifyJS.minify(read("test/input/issue-3294/input.js"), { compress: { toplevel: true }, sourceMap: { content: "inline", @@ -195,10 +194,10 @@ describe("sourcemaps", function() { } }); if (result.error) throw result.error; - assert.strictEqual(result.code + "\n", readFileSync("test/input/issue-3294/output.js", "utf8")); + assert.strictEqual(result.code + "\n", read("test/input/issue-3294/output.js")); }); it("Should work in presence of unrecognised annotations", function() { - var result = UglifyJS.minify(read("./test/input/issue-3441/input.js"), { + var result = UglifyJS.minify(read("test/input/issue-3441/input.js"), { compress: false, mangle: false, sourceMap: { @@ -230,7 +229,7 @@ describe("sourcemaps", function() { assert.strictEqual(code, "var a=function(n){return n};"); }); it("Should work with max_line_len", function() { - var result = UglifyJS.minify(read("./test/input/issue-505/input.js"), { + var result = UglifyJS.minify(read("test/input/issue-505/input.js"), { compress: { directives: false, }, @@ -242,7 +241,7 @@ describe("sourcemaps", function() { } }); if (result.error) throw result.error; - assert.strictEqual(result.code, read("./test/input/issue-505/output.js")); + assert.strictEqual(result.code, read("test/input/issue-505/output.js")); }); it("Should work with unicode characters", function() { var code = [ @@ -281,29 +280,33 @@ describe("sourcemaps", function() { it("Should copy over original sourcesContent", function() { var orig = get_map(); var map = prepare_map(orig); - assert.equal(map.sourceContentFor("index.js"), orig.sourcesContent[0]); + assert.strictEqual(map.sources.length, 1); + assert.strictEqual(map.sources[0], "index.js"); + assert.strictEqual(map.sourcesContent.length, 1); + assert.equal(map.sourcesContent[0], orig.sourcesContent[0]); }); it("Should copy sourcesContent if sources are relative", function() { var relativeMap = get_map(); relativeMap.sources = ['./index.js']; var map = prepare_map(relativeMap); - assert.notEqual(map.sourcesContent, null); - assert.equal(map.sourcesContent.length, 1); - assert.equal(map.sourceContentFor("index.js"), relativeMap.sourcesContent[0]); + assert.strictEqual(map.sources.length, 1); + assert.strictEqual(map.sources[0], "./index.js"); + assert.strictEqual(map.sourcesContent.length, 1); + assert.equal(map.sourcesContent[0], relativeMap.sourcesContent[0]); }); it("Should not have invalid mappings from inputSourceMap", function() { var map = prepare_map(get_map()); // 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"; - map.eachMapping(function(mapping) { - assert.ok(mapping.originalLine <= 2, msg); - }); - map.allGeneratedPositionsFor({ - source: "index.js", - line: 1, - column: 1 - }).forEach(function(pos) { - assert.ok(pos.line <= 2, msg); + var lines = map.mappings.split(/;/); + assert.ok(lines.length <= 2, msg); + var indices = [ 0, 0, 1, 0, 0]; + lines.forEach(function(segments) { + indices[0] = 0; + segments.split(/,/).forEach(function(segment) { + UglifyJS.vlq_decode(indices, segment); + assert.ok(indices[2] <= 2, msg); + }); }); }); }); diff --git a/test/node.js b/test/node.js index 22bf325d..fc3a14dc 100644 --- a/test/node.js +++ b/test/node.js @@ -1,6 +1,6 @@ var fs = require("fs"); -new Function("MOZ_SourceMap", "exports", require("../tools/node").FILES.map(function(file) { +new Function("exports", require("../tools/node").FILES.map(function(file) { if (/exports\.js$/.test(file)) file = require.resolve("./exports"); return fs.readFileSync(file, "utf8"); -}).join("\n\n"))(require("source-map"), exports); +}).join("\n\n"))(exports); diff --git a/tools/node.js b/tools/node.js index 58150e6b..ef05f571 100644 --- a/tools/node.js +++ b/tools/node.js @@ -15,13 +15,13 @@ exports.FILES = [ require.resolve("./exports.js"), ]; -new Function("MOZ_SourceMap", "exports", function() { +new Function("exports", function() { var code = exports.FILES.map(function(file) { return fs.readFileSync(file, "utf8"); }); code.push("exports.describe_ast = " + describe_ast.toString()); return code.join("\n\n"); -}())(require("source-map"), exports); +}())(exports); function describe_ast() { var out = OutputStream({ beautify: true }); -- 2.34.1