Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sourceMap.content "auto" option #298

Closed
wants to merge 13 commits into from
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,11 @@ a double dash to prevent input files being used as option arguments:
`base` Path to compute relative paths from input files.
`content` Input source map, useful if you're compressing
JS that was generated from some other original
code. Specify "inline" if the source map is
code. Specify "auto" to enable automatic source
map resolution. Specify "inline" if the source map is
included within the sources.
`contents` Provide overrides for the auto source map resolution
strategy. See "Composed source map" below.
`filename` Name and/or location of the output source.
`includeSources` Pass this flag if you want to include
the content of source files in the
Expand Down Expand Up @@ -203,9 +206,19 @@ CoffeeScript → compiled JS, Terser can generate a map from CoffeeScript →
compressed JS by mapping every token in the compiled JS to its original
location.

To use this feature pass `--source-map "content='/path/to/input/source.map'"`
or `--source-map "content=inline"` if the source map is included inline with
the sources.
The easiest way to compose source maps is to use the `--source-map "content=auto"`
option. If an input file specifies a sourceMappingURL comment, the source map will be fetched.
If there is no comment, given an input file of `src/file.js`, the map will be looked for
at `src/file.js.map` and `src/file.map`. Maps can fetched via HTTP too.

If easier to specify the link manually, you can use the `--source-map "contents=..."` option.
The format looks like this: `contents=file1.js*file1.js.map|file2.js*path/to/map.js.map|...`.
These source map locations will take precedence over the auto resolution strategy.

Previous versions of UglifyJS provided partial support for this via the
`--source-map "content=inline"` option (only looking for base64 inline maps)
and `--source-map "content='/path/to/input/source.map'"` (only supporting one file).
These options still work, but "auto" is recommended.

## CLI compress options

Expand Down
57 changes: 56 additions & 1 deletion bin/uglifyjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

require("../tools/exit.js");

var execSync = require("child_process").execSync;
var fs = require("fs");
var info = require("../package.json");
var path = require("path");
Expand Down Expand Up @@ -135,7 +136,7 @@ if (program.output == "ast") {
if (program.parse) {
if (!program.parse.acorn && !program.parse.spidermonkey) {
options.parse = program.parse;
} else if (program.sourceMap && program.sourceMap.content == "inline") {
} else if (content == "inline") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm little bit worried about this changes..
ESLint: 'content' is not defined. (no-undef)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops!

fatal("ERROR: inline source map only works with built-in parser");
}
}
Expand Down Expand Up @@ -196,6 +197,7 @@ function run() {
UglifyJS.AST_Node.warn_function = function(msg) {
print_error("WARN: " + msg);
};
var content = program.sourceMap && program.sourceMap.content;
if (program.timings) options.timings = true;
try {
if (program.parse) {
Expand All @@ -221,6 +223,59 @@ function run() {
} catch (ex) {
fatal(ex);
}
if (content == "auto") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to use strict equal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

var to_ascii = function(b64) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use UglifyJS.to_ascii

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

return new Buffer(b64, "base64").toString();
};

var resolve_source_map_content = function(name, code) {
var sourceMappingURLMatch = /\n\/\/# sourceMappingURL=(.*)/.exec(code);
var sourceMappingURL = sourceMappingURLMatch ? sourceMappingURLMatch[1] : null;

if (!sourceMappingURL) {
// infer
if (fs.existsSync(name + '.map')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESLint: Strings must use doublequote. (quotes)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return read_file(name + '.map');
}
if (/\.js$/.test(name) && fs.existsSync(name.slice(0, -2) + 'map')) {
return read_file(name.slice(0, -2) + 'map');
}
return;
}

if (sourceMappingURL.indexOf('data:application') == 0) {
var match = /data:application\/json(;.*?)?;base64,(.*)/.exec(sourceMappingURL);
return to_ascii(match[2]);
}

if (sourceMappingURL.indexOf('http') == 0) {
try {
return execSync('curl -s ' + sourceMappingURL).toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my windows machine curl is not installed.... 😖

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with the http libs. Required some promises.

} catch (ex) {
fatal(ex);
}
}

return read_file(path.join(path.dirname(name), sourceMappingURL));
};

// read source map content locations given via CLI
// contents=file1.js*file1.js.map|file2.js*path/to/map.js.map|...
var contents = {};
if (options.sourceMap.contents) options.sourceMap.contents.replace(/([^|*]+)=([^|]+)/g, function(_, x, y) {
contents[x] = read_file(y, y);
});

// attempt to resolve the remaining
for (var name in files) if (!contents[name]) {
var content = resolve_source_map_content(name, files[name]);
if (content) {
contents[name] = content;
}
}

options.sourceMap.contents = contents;
}
var result = UglifyJS.minify(files, options);
if (result.error) {
var ex = result.error;
Expand Down
43 changes: 32 additions & 11 deletions lib/minify.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,23 @@ var to_base64 = typeof btoa == "undefined" ? function(str) {
return Buffer.from(str).toString("base64");
} : btoa;

function read_source_map(code) {
function read_inline_source_map(name, code) {
var match = /(?:^|[^.])\/\/# sourceMappingURL=data:application\/json(;[\w=-]*)?;base64,([+/0-9A-Za-z]*=*)\s*$/.exec(code);
if (!match) {
AST_Node.warn("inline source map not found");
AST_Node.warn("inline source map not found: " + name);
return null;
}
return to_ascii(match[2]);
}

function parse_source_map(content) {
try {
return JSON.parse(content);
} catch (ex) {
throw new Error("invalid input source map: " + content);
}
}

function set_shorthand(name, options, keys) {
if (options[name]) {
keys.forEach(function(key) {
Expand Down Expand Up @@ -135,6 +143,7 @@ function minify(files, options) {
if (options.sourceMap) {
options.sourceMap = defaults(options.sourceMap, {
content: null,
contents: null,
filename: null,
includeSources: false,
root: null,
Expand All @@ -148,7 +157,7 @@ function minify(files, options) {
};
}
if (timings) timings.parse = Date.now();
var toplevel;
var source_maps, toplevel;
if (files instanceof AST_Toplevel) {
toplevel = files;
} else {
Expand All @@ -157,13 +166,28 @@ function minify(files, options) {
}
options.parse = options.parse || {};
options.parse.toplevel = null;
var source_map_content = options.sourceMap && options.sourceMap.content;
if (typeof source_map_content == "string" && source_map_content != "inline" && source_map_content != "auto") {
source_map_content = parse_source_map(source_map_content);
}
source_maps = source_map_content && Object.create(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to #300 (comment) I guess it would be much better to use smth like:

if (source_map_content) source_maps = new Set();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least it will reduce variety in code which doing same things

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 , but it's actually a Map.

for (var name in files) if (HOP(files, name)) {
options.parse.filename = name;
options.parse.toplevel = parse(files[name], options.parse);
if (options.sourceMap && options.sourceMap.content == "inline") {
if (Object.keys(files).length > 1)
throw new Error("inline source map only works with singular input");
options.sourceMap.content = read_source_map(files[name]);
if (source_map_content) {
if (options.sourceMap.content == "inline") {
var inlined_content = read_inline_source_map(name, files[name]);
if (inlined_content) {
source_maps[name] = parse_source_map(inlined_content);
}
} else if (source_map_content == "auto") {
var content = options.sourceMap.contents[name];
if (content) {
source_maps[name] = parse_source_map(content);
}
} else {
source_maps[name] = source_map_content;
}
}
}
toplevel = options.parse.toplevel;
Expand Down Expand Up @@ -205,12 +229,9 @@ function minify(files, options) {
}
if (!HOP(options.output, "code") || options.output.code) {
if (options.sourceMap) {
if (typeof options.sourceMap.content == "string") {
options.sourceMap.content = JSON.parse(options.sourceMap.content);
}
options.output.source_map = SourceMap({
file: options.sourceMap.filename,
orig: options.sourceMap.content,
orig: source_maps,
root: options.sourceMap.root
});
if (options.sourceMap.includeSources) {
Expand Down
24 changes: 14 additions & 10 deletions lib/sourcemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,24 @@ function SourceMap(options) {
file : options.file,
sourceRoot : options.root
});
var orig_map = options.orig && new MOZ_SourceMap.SourceMapConsumer(options.orig);

if (orig_map) {
orig_map.sources.forEach(function(source) {
var sourceContent = orig_map.sourceContentFor(source, true);
if (sourceContent) {
generator.setSourceContent(source, sourceContent);
}
var maps = options.orig && Object.create(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like Set

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 , but it's actually a Map.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad)

if (maps) for (var source in options.orig) {
var map = new MOZ_SourceMap.SourceMapConsumer(options.orig[source]);
var sourceContents = [...map._sources.toArray()];
if (map._sections) for (const section of map._sections) {
sourceContents.push(...section.consumer._sources.toArray());
}
sourceContents.forEach(function(source) {
var sourceContent = map.sourceContentFor(source, true);
if (sourceContent) generator.setSourceContent(source, sourceContent);
});
maps[source] = map;
}

function add(source, gen_line, gen_col, orig_line, orig_col, name) {
if (orig_map) {
var info = orig_map.originalPositionFor({
var map = maps && maps[source];
if (map) {
var info = map.originalPositionFor({
line: orig_line,
column: orig_col
});
Expand Down
24 changes: 24 additions & 0 deletions test/input/issue-3219/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env sh

tsc --sourceMap --inlineSources file.ts
sed -i '' 's/file/mapping/g' file.js
mv file.js.map mapping.js.map

tsc --sourceMap --inlineSources file.ts --out file2.js
sed -i '' 's/file2/mapping2/g' file2.js
sed -i '' -e '$ d' file2.js
mv file2.js.map mapping2.js.map

tsc --sourceMap --inlineSources file.ts --out file3.js
sed -i '' 's/file3/mapping2/g' file3.js
sed -i '' -e '$ d' file3.js
mv file3.js.map mapping2.js.map

tsc --inlineSourceMap --inlineSources inline.ts

tsc --sourceMap --inlineSources infer1.ts
sed -i '' -e '$ d' infer1.js

tsc --sourceMap --inlineSources infer2.ts
sed -i '' -e '$ d' infer2.js
mv infer2.js.map infer2.map
12 changes: 12 additions & 0 deletions test/input/issue-3219/file.js

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

16 changes: 16 additions & 0 deletions test/input/issue-3219/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
interface ICar {
model: string
speed: number
cost: number
}

class Car implements ICar {
model: string = 'nice'
speed: number = 100
cost: number = 1000000
}

var myCar: Car = new Car()
myCar.cost += 1

const [blah, blah2] = [1,2]
11 changes: 11 additions & 0 deletions test/input/issue-3219/file2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
var Car = /** @class */ (function () {
function Car() {
this.model = 'nice';
this.speed = 100;
this.cost = 1000000;
}
return Car;
}());
var myCar = new Car();
myCar.cost += 1;
var _a = [1, 2], blah = _a[0], blah2 = _a[1];
11 changes: 11 additions & 0 deletions test/input/issue-3219/file3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
var Car = /** @class */ (function () {
function Car() {
this.model = 'nice';
this.speed = 100;
this.cost = 1000000;
}
return Car;
}());
var myCar = new Car();
myCar.cost += 1;
var _a = [1, 2], blah = _a[0], blah2 = _a[1];
3 changes: 3 additions & 0 deletions test/input/issue-3219/http.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions test/input/issue-3219/infer1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('infer1.js');
1 change: 1 addition & 0 deletions test/input/issue-3219/infer1.js.map

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

1 change: 1 addition & 0 deletions test/input/issue-3219/infer1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('infer1.js');
1 change: 1 addition & 0 deletions test/input/issue-3219/infer2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('infer2.js');
1 change: 1 addition & 0 deletions test/input/issue-3219/infer2.map

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

1 change: 1 addition & 0 deletions test/input/issue-3219/infer2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('infer2.js');
12 changes: 12 additions & 0 deletions test/input/issue-3219/inline.js

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

16 changes: 16 additions & 0 deletions test/input/issue-3219/inline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
interface ICar {
model: string
speed: number
cost: number
}

class Car implements ICar {
model: string = 'bad'
speed: number = 10
cost: number = 10000
}

var myCar: Car = new Car()
myCar.cost += 1

const [blah, blah2] = [1,2]
1 change: 1 addition & 0 deletions test/input/issue-3219/mapping.js.map

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

1 change: 1 addition & 0 deletions test/input/issue-3219/mapping2.js.map

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

2 changes: 2 additions & 0 deletions test/input/issue-3219/output1.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions test/input/issue-3219/output2.js

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