Skip to content

Commit

Permalink
Switch to GenMapping for sourcemap generation
Browse files Browse the repository at this point in the history
[`gen-mapping`](https://github.com/jridgewell/gen-mapping) is faster, smaller, and lighter  sourcemap generation library.

This also exposes a new `decodedMap` property on the result object. Decoded maps are free to create (it's a shallow clone of the `GenMapping` instance), and passing them to `@jridgewell/trace-mapping` is copy-free. With Babel  [recently](babel/babel#14497) adding a `decodedMap` field, a dev could pass from the Babel transpilation to Terser without any added memory use for sourcemaps.

And if there's a multi stage build process, a dev could use `@ampproject/remapping` to remap Babel, Terser, and (eg) a bundler's outputs without having to feed input maps into each stage.
  • Loading branch information
jridgewell committed Apr 30, 2022
1 parent 423b304 commit d23354e
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 85 deletions.
38 changes: 21 additions & 17 deletions lib/minify.js
Expand Up @@ -271,18 +271,15 @@ async function minify(files, options, _fs_module) {
}
if (!HOP(options.format, "code") || options.format.code) {
if (options.sourceMap) {
options.format.source_map = await SourceMap({
if (options.sourceMap.includeSources && files instanceof AST_Toplevel) {
throw new Error("original source content unavailable");
}
options.format.source_map = SourceMap({
file: options.sourceMap.filename,
orig: options.sourceMap.content,
root: options.sourceMap.root
root: options.sourceMap.root,
files: options.sourceMap.includeSources ? files : null,
});
if (options.sourceMap.includeSources) {
if (files instanceof AST_Toplevel) {
throw new Error("original source content unavailable");
} else for (var name in files) if (HOP(files, name)) {
options.format.source_map.get().setSourceContent(name, files[name]);
}
}
}
delete options.format.ast;
delete options.format.code;
Expand All @@ -291,11 +288,21 @@ async function minify(files, options, _fs_module) {
toplevel.print(stream);
result.code = stream.get();
if (options.sourceMap) {
if(options.sourceMap.asObject) {
result.map = options.format.source_map.get().toJSON();
} else {
result.map = options.format.source_map.toString();
}
Object.defineProperty(result, "map", {
configurable: true,
enumerable: true,
get() {
const map = options.format.source_map.getEncoded();
return (result.map = options.sourceMap.asObject ? map : JSON.stringify(map));
},
set(value) {
Object.defineProperty(result, "map", {
value,
writable: true,
});
}
});
result.decodedMap = options.format.source_map.getDecoded();
if (options.sourceMap.url == "inline") {
var sourceMap = typeof result.map === "object" ? JSON.stringify(result.map) : result.map;
result.code += "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," + to_base64(sourceMap);
Expand All @@ -310,9 +317,6 @@ async function minify(files, options, _fs_module) {
options.nameCache.props = cache_to_json(options.mangle.properties.cache);
}
}
if (options.format && options.format.source_map) {
options.format.source_map.destroy();
}
if (timings) {
timings.end = Date.now();
result.timings = {
Expand Down
43 changes: 27 additions & 16 deletions lib/sourcemap.js
Expand Up @@ -43,34 +43,37 @@

"use strict";

import MOZ_SourceMap from "source-map";
import {GenMapping, maybeAddMapping, toDecodedMap, toEncodedMap, setSourceContent} from "@jridgewell/gen-mapping";
import {AnyMap, originalPositionFor} from "@jridgewell/trace-mapping";
import {defaults} from "./utils/index.js";
import {defaults, HOP} from "./utils/index.js";

// a small wrapper around source-map and @jridgewell/trace-mapping
function SourceMap(options) {
options = defaults(options, {
file : null,
root : null,
orig : null,

orig_line_diff : 0,
dest_line_diff : 0,
files: {},
});

var orig_map;
var generator = new MOZ_SourceMap.SourceMapGenerator({
var generator = new GenMapping({
file : options.file,
sourceRoot : options.root
});

let sourcesContent = {__proto__: null};
let files = options.files;
for (var name in files) if (HOP(files, name)) {
sourcesContent[name] = files[name];
}
if (options.orig) {
orig_map = new AnyMap(options.orig);
if (orig_map.sourcesContent) {
orig_map.resolvedSources.forEach(function(source, i) {
var sourceContent = orig_map.sourcesContent[i];
if (sourceContent) {
generator.setSourceContent(source, sourceContent);
var content = orig_map.sourcesContent[i];
if (content) {
sourcesContent[source] = content;
}
});
}
Expand All @@ -90,19 +93,27 @@ function SourceMap(options) {
orig_col = info.column;
name = info.name || name;
}
generator.addMapping({
generated : { line: gen_line + options.dest_line_diff, column: gen_col },
original : { line: orig_line + options.orig_line_diff, column: orig_col },
maybeAddMapping(generator, {
generated : { line: gen_line, column: gen_col },
original : { line: orig_line, column: orig_col },
source : source,
name : name
});
setSourceContent(generator, source, sourcesContent[source]);
}

function clean(map) {
const allNull = map.sourcesContent.every(c => c == null);
if (allNull) delete map.sourcesContent;
if (map.file === undefined) delete map.file;
if (map.sourceRoot === undefined) delete map.sourceRoot;
return map;
}

return {
add : add,
get : function() { return generator; },
toString : function() { return generator.toString(); },
destroy : function() {}
add : add,
getDecoded : function() { return clean(toDecodedMap(generator)); },
getEncoded : function() { return clean(toEncodedMap(generator)); },
};
}

Expand Down
65 changes: 28 additions & 37 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -43,10 +43,10 @@
"main.js"
],
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.5",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.8.0-beta.0",
"source-map-support": "~0.5.20"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion test/input/issue-505/output.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/input/issue-520/output.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/input/source-maps/expect.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions test/mocha/cli-2.js
Expand Up @@ -21,7 +21,7 @@ describe("bin/terser (2)", 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,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImluZGV4LmpzIl0sIm5hbWVzIjpbImZvbyIsIngiLCJjb25zb2xlIiwibG9nIl0sIm1hcHBpbmdzIjoiYUFBQSxJQUFJQSxJQUFNLFNBQU5BLElBQU1DLEdBQUEsTUFBSyxPQUFTQSxHQUN4QkMsUUFBUUMsSUFBSUgsSUFBSSJ9",
"//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmb28iLCJ4IiwiY29uc29sZSIsImxvZyJdLCJzb3VyY2VzIjpbImluZGV4LmpzIl0sIm1hcHBpbmdzIjoiYUFBQSxJQUFJQSxJQUFNLFNBQU5BLElBQU1DLEdBQUEsTUFBSyxPQUFTQSxHQUN4QkMsUUFBUUMsSUFBSUgsSUFBSSJ9",
""
].join("\n"));
done();
Expand All @@ -46,7 +46,7 @@ describe("bin/terser (2)", function() {

assert.strictEqual(stdout, [
'function foo(){return function(){console.log("PASS")}}foo()();',
"//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvaW5wdXQvaXNzdWUtMjMxMC9pbnB1dC5qcyJdLCJuYW1lcyI6WyJmb28iLCJjb25zb2xlIiwibG9nIiwiZiJdLCJtYXBwaW5ncyI6IkFBQUEsU0FBU0EsTUFDTCxPQUFPLFdBQ0hDLFFBQVFDLElBQUksU0FLUkYsS0FDUkcifQ==",
"//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmb28iLCJjb25zb2xlIiwibG9nIiwiZiJdLCJzb3VyY2VzIjpbInRlc3QvaW5wdXQvaXNzdWUtMjMxMC9pbnB1dC5qcyJdLCJtYXBwaW5ncyI6IkFBQUEsU0FBU0EsTUFDTCxPQUFPLFdBQ0hDLFFBQVFDLElBQUksU0FLUkYsS0FDUkcifQ==",
""
].join("\n"));
done();
Expand Down
2 changes: 1 addition & 1 deletion test/mocha/minify.js
Expand Up @@ -321,7 +321,7 @@ 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,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIjAiXSwibmFtZXMiOlsiYSIsImZvbyJdLCJtYXBwaW5ncyI6IkFBQUEsSUFBSUEsRUFBSSxTQUFTQyxHQUFPLE9BQU9BIn0=");
"//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJhIiwiZm9vIl0sInNvdXJjZXMiOlsiMCJdLCJtYXBwaW5ncyI6IkFBQUEsSUFBSUEsRUFBSSxTQUFTQyxHQUFPLE9BQU9BIn0=");
});
it("should not append source map to output js when sourceMapInline is not enabled", async function() {
var result = await minify("var a = function(foo) { return foo; };");
Expand Down
37 changes: 29 additions & 8 deletions test/mocha/sourcemaps.js
@@ -1,11 +1,11 @@
import assert from "assert";
import { readFileSync } from "fs";
import source_map_module from "source-map"
import source_map_module from "source-map";
import { assertCodeWithInlineMapEquals } from "./utils.js";
import { to_ascii } from "../../lib/minify.js";
import { minify } from "../../main.js";

const { SourceMapConsumer } = source_map_module
const { SourceMapConsumer } = source_map_module;

function read(path) {
return readFileSync(path, "utf8");
Expand Down Expand Up @@ -96,7 +96,12 @@ describe("sourcemaps", function() {
});
if (result.error) throw result.error;
assert.strictEqual(result.code, "({}).wat([]);");
assert.strictEqual(result.map, '{"version":3,"sources":["0"],"names":["wat"],"mappings":"CAAU,IACNA,IAAI"}');
assert.deepStrictEqual(JSON.parse(result.map), {
version: 3,
sources: ["0"],
names: ["wat"],
mappings: "CAAU,IACNA,IAAI",
});
});
it("Should mark class literals", async function() {
var result = await minify([
Expand All @@ -109,7 +114,12 @@ describe("sourcemaps", function() {
});
if (result.error) throw result.error;
assert.strictEqual(result.code, "(class{}).wat(class{});");
assert.strictEqual(result.map, '{"version":3,"sources":["0"],"names":["wat"],"mappings":"CACU,SACNA,IAFJ"}');
assert.deepStrictEqual(JSON.parse(result.map), {
version: 3,
sources: ["0"],
names: ["wat"],
mappings: "CACU,SACNA,IAFJ",
});
});
it("Should give correct sourceRoot", async function() {
var code = "console.log(42);";
Expand All @@ -120,7 +130,13 @@ describe("sourcemaps", function() {
});
if (result.error) throw result.error;
assert.strictEqual(result.code, code);
assert.strictEqual(result.map, '{"version":3,"sources":["0"],"names":["console","log"],"mappings":"AAAAA,QAAQC,IAAI","sourceRoot":"//foo.bar/"}');
assert.deepStrictEqual(JSON.parse(result.map), {
version: 3,
sources: ["0"],
names: ["console","log"],
mappings: "AAAAA,QAAQC,IAAI",
sourceRoot: "//foo.bar/",
});
});
it("Should return source map as object when asObject is given", async function() {
var code = "console.log(42);";
Expand All @@ -131,7 +147,12 @@ describe("sourcemaps", function() {
});
if (result.error) throw result.error;
assert.strictEqual(result.code, code);
assert.deepStrictEqual(result.map, {"version":3,"sources":["0"],"names":["console","log"],"mappings":"AAAAA,QAAQC,IAAI"});
assert.deepStrictEqual(result.map, {
version: 3,
sources: ["0"],
names: ["console","log"],
mappings: "AAAAA,QAAQC,IAAI",
});
});

it("Should grab names from methods and properties correctly", async () => {
Expand Down Expand Up @@ -191,8 +212,8 @@ describe("sourcemaps", function() {
if (result.error) throw result.error;
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"));');
assert.deepEqual(map.sources, ["index.js"]);
assert.deepEqual(map.sourcesContent, ['let foo = x => "foo " + x;\nconsole.log(foo("bar"));']);
});
it("Should process inline source map", async function() {
var result = await minify(read("./test/input/issue-520/input.js"), {
Expand Down

0 comments on commit d23354e

Please sign in to comment.