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
242 changes: 161 additions & 81 deletions bin/uglifyjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

"use strict";

require("../tools/exit.js");
// require("../tools/exit.js");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to remove this to get the tests that expect errors to work. I can't reason about this code or how to augment it - ideas?


var fs = require("fs");
var info = require("../package.json");
Expand Down Expand Up @@ -175,15 +175,21 @@ if (filesList) {
simple_glob(filesList).forEach(function(name) {
files[convert_path(name)] = read_file(name);
});
run();
Promise.resolve(run()).catch(err => {
console.error('err', err);
process.exit(1);
});
} else {
var chunks = [];
process.stdin.setEncoding("utf8");
process.stdin.on("data", function(chunk) {
chunks.push(chunk);
}).on("end", function() {
files = [ chunks.join("") ];
run();
Promise.resolve(run()).catch(err => {
console.error('err', err);
process.exit(1);
});
});
process.stdin.resume();
}
Expand All @@ -192,10 +198,75 @@ function convert_ast(fn) {
return UglifyJS.AST_Node.from_mozilla_ast(Object.keys(files).reduce(fn, null));
}

function resolve_sourcemaps() {
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")) {
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 UglifyJS.to_ascii(match[2]);
}

if (sourceMappingURL.indexOf("http") == 0) {
return new Promise((resolve, reject) => {
const lib = sourceMappingURL.startsWith("https") ? require("https") : require("http");
var request = lib.get(sourceMappingURL, res => {
var data = "";
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
resolve(data);
});
});
request.on("error", e => {
reject(e.message);
});
});
}

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
// TODO: replace with async/await when node 6 support is dropped.
return Promise.all(
Object.keys(files).filter(name => !contents[name]).map(name => {
var contentRetVal = resolve_source_map_content(name, files[name]);
if (!contentRetVal || typeof contentRetVal.then !== 'function') {
contentRetVal = Promise.resolve(contentRetVal);
}
return contentRetVal.then(content => {
if (content) contents[name] = content;
})
})
).then(_ => options.sourceMap.contents = contents);
}

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,90 +292,99 @@ function run() {
} catch (ex) {
fatal(ex);
}
var result = UglifyJS.minify(files, options);
if (result.error) {
var ex = result.error;
if (ex.name == "SyntaxError") {
print_error("Parse error at " + ex.filename + ":" + ex.line + "," + ex.col);
var col = ex.col;
var lines = files[ex.filename].split(/\r?\n/);
var line = lines[ex.line - 1];
if (!line && !col) {
line = lines[ex.line - 2];
col = line.length;
}
if (line) {
var limit = 70;
if (col > limit) {
line = line.slice(col - limit);
col = limit;
// TODO: replace with async/await when node 6 support is dropped.
var resolveSourcemapsPromise;
if (content === "auto") {
resolveSourcemapsPromise = resolve_sourcemaps();
} else {
resolveSourcemapsPromise = Promise.resolve();
}
return resolveSourcemapsPromise.then(() => {
var result = UglifyJS.minify(files, options);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

all of this below is just indentation

if (result.error) {
var ex = result.error;
if (ex.name == "SyntaxError") {
print_error("Parse error at " + ex.filename + ":" + ex.line + "," + ex.col);
var col = ex.col;
var lines = files[ex.filename].split(/\r?\n/);
var line = lines[ex.line - 1];
if (!line && !col) {
line = lines[ex.line - 2];
col = line.length;
}
if (line) {
var limit = 70;
if (col > limit) {
line = line.slice(col - limit);
col = limit;
}
print_error(line.slice(0, 80));
print_error(line.slice(0, col).replace(/\S/g, " ") + "^");
}
print_error(line.slice(0, 80));
print_error(line.slice(0, col).replace(/\S/g, " ") + "^");
}
}
if (ex.defs) {
print_error("Supported options:");
print_error(format_object(ex.defs));
}
fatal(ex);
} else if (program.output == "ast") {
if (!options.compress && !options.mangle) {
result.ast.figure_out_scope({});
}
print(JSON.stringify(result.ast, function(key, value) {
if (value) switch (key) {
case "thedef":
return symdef(value);
case "enclosed":
return value.length ? value.map(symdef) : undefined;
case "variables":
case "functions":
case "globals":
return value.size() ? value.map(symdef) : undefined;
if (ex.defs) {
print_error("Supported options:");
print_error(format_object(ex.defs));
}
if (skip_key(key)) return;
if (value instanceof UglifyJS.AST_Token) return;
if (value instanceof UglifyJS.Dictionary) return;
if (value instanceof UglifyJS.AST_Node) {
var result = {
_class: "AST_" + value.TYPE
};
if (value.block_scope) {
result.variables = value.block_scope.variables;
result.functions = value.block_scope.functions;
result.enclosed = value.block_scope.enclosed;
}
value.CTOR.PROPS.forEach(function(prop) {
result[prop] = value[prop];
});
return result;
fatal(ex);
} else if (program.output == "ast") {
if (!options.compress && !options.mangle) {
result.ast.figure_out_scope({});
}
return value;
}, 2));
} else if (program.output == "spidermonkey") {
print(JSON.stringify(UglifyJS.minify(result.code, {
compress: false,
mangle: false,
output: {
ast: true,
code: false
print(JSON.stringify(result.ast, function(key, value) {
if (value) switch (key) {
case "thedef":
return symdef(value);
case "enclosed":
return value.length ? value.map(symdef) : undefined;
case "variables":
case "functions":
case "globals":
return value.size() ? value.map(symdef) : undefined;
}
if (skip_key(key)) return;
if (value instanceof UglifyJS.AST_Token) return;
if (value instanceof UglifyJS.Dictionary) return;
if (value instanceof UglifyJS.AST_Node) {
var result = {
_class: "AST_" + value.TYPE
};
if (value.block_scope) {
result.variables = value.block_scope.variables;
result.functions = value.block_scope.functions;
result.enclosed = value.block_scope.enclosed;
}
value.CTOR.PROPS.forEach(function(prop) {
result[prop] = value[prop];
});
return result;
}
return value;
}, 2));
} else if (program.output == "spidermonkey") {
print(JSON.stringify(UglifyJS.minify(result.code, {
compress: false,
mangle: false,
output: {
ast: true,
code: false
}
}).ast.to_mozilla_ast(), null, 2));
} else if (program.output) {
fs.writeFileSync(program.output, result.code);
if (result.map) {
fs.writeFileSync(program.output + ".map", result.map);
}
}).ast.to_mozilla_ast(), null, 2));
} else if (program.output) {
fs.writeFileSync(program.output, result.code);
if (result.map) {
fs.writeFileSync(program.output + ".map", result.map);
} else {
print(result.code);
}
} else {
print(result.code);
}
if (program.nameCache) {
fs.writeFileSync(program.nameCache, JSON.stringify(options.nameCache));
}
if (result.timings) for (var phase in result.timings) {
print_error("- " + phase + ": " + result.timings[phase].toFixed(3) + "s");
}
if (program.nameCache) {
fs.writeFileSync(program.nameCache, JSON.stringify(options.nameCache));
}
if (result.timings) for (var phase in result.timings) {
print_error("- " + phase + ": " + result.timings[phase].toFixed(3) + "s");
}
});
}

function fatal(message) {
Expand Down