Always annotate line numbers when a source map is available.

When a source map is available, the line number annotations are now
computed from the source map. If no source map is available, but we are
planning to combine the file with other files in a package, we generate a
simple identity source map for the file and then annotate its line numbers
using that source map. This combination of techniques provides a tolerable
debugging experience in all browsers, even those that do not support
source maps.

The File.prototype._pathForSourceMap method was removed because it is no
longer used (self.servePath is better for source maps anyway, because it
includes parent directories, and the leading / character ensures that
source map paths will not be misinterpreted as relative paths).
This commit is contained in:
Ben Newman
2015-05-22 15:41:12 -04:00
parent b5514d4e9f
commit b556e62262

View File

@@ -295,18 +295,6 @@ _.extend(File.prototype, {
}
},
// Relative path to use in source maps to indicate this file. No
// leading slash.
_pathForSourceMap: function () {
var self = this;
if (self.module.name)
return self.module.name + "/" + self.sourcePath;
else
return require('path').basename(self.sourcePath);
},
// Options:
// - preserveLineNumbers: if true, decorate minimally so that line
// numbers don't change between input and output. In this case,
@@ -318,118 +306,175 @@ _.extend(File.prototype, {
// Returns a SourceNode.
getPrelinkedOutput: function (options) {
var self = this;
// The newline after the source closes a '//' comment.
if (options.preserveLineNumbers) {
// Ugly version
var mapNode;
if (self.sourceMap) {
mapNode = sourcemap.SourceNode.fromStringWithSourceMap(
self.source, new sourcemap.SourceMapConsumer(self.sourceMap));
} else {
// This is an app file that was always JS. The output file here is going
// to be the same name as the input file (because _pathForSourceMap in
// apps is the basename of the source file), and having a JS file
// pointing to a source map pointing to a JS file of the same name will
// (a) be confusing (b) be unnecessary since we aren't renumbering
// anything and (c) confuse at least Chrome.
mapNode = self.source;
}
return new sourcemap.SourceNode(null, null, null, [
self.bare ? "" : "(function(){",
mapNode,
(self.source.length && self.source[self.source.length - 1] !== '\n'
? "\n" : ""),
self.bare ? "" : "\n})();\n"
]);
}
// Pretty version
var chunks = [];
// Prologue
if (! self.bare)
chunks.push("(function () {\n\n");
// Banner
var bannerLines = [self.servePath.slice(1)];
if (self.bare) {
bannerLines.push(
"This file is in bare mode and is not in its own closure.");
}
var width = options.sourceWidth || 70;
var bannerWidth = width + 3;
var padding = bannerPadding(bannerWidth);
chunks.push(banner(bannerLines, bannerWidth));
var blankLine = new Array(width + 1).join(' ') + " //\n";
chunks.push(blankLine);
var result;
var lines;
// Code, with line numbers
// You might prefer your line numbers at the beginning of the
// line, with /* .. */. Well, that requires parsing the source for
// comments, because you have to do something different if you're
// already inside a comment.
var numberifyLines = function (f) {
var num = 1;
var lines = self.source.split('\n');
_.each(lines, function (line) {
var suffix = "\n";
if (! options.noLineNumbers
&& line.length <= width && line[line.length - 1] !== "\\") {
suffix = padding.slice(line.length, width) + " // " + num + "\n";
}
f(line, suffix, num);
num++;
});
};
var lines = self.source.split('\n');
var noLineNumbers = options.noLineNumbers;
var preserveLineNumbers = options.preserveLineNumbers;
if (self.sourceMap) {
var buf = "";
numberifyLines(function (line, suffix) {
buf += line;
buf += suffix;
});
// The existing source map is valid because all we're doing is adding
// things to the end of lines, which doesn't affect the source map. (If
// we wanted to be picky, we could add some explicitly non-mapped regions
// to the source map to cover the suffixes, which would make this
// equivalent to the "no source map coming in" case, but this doesn't seem
// that important.)
chunks.push(sourcemap.SourceNode.fromStringWithSourceMap(
self.source,
new sourcemap.SourceMapConsumer(self.sourceMap)));
// If we have a source map, it is also important to annotate line
// numbers using that source map, since not all browsers support
// source maps.
noLineNumbers = false;
// Honoring options.preserveLineNumbers is likely impossible if we
// have a source map, since self.source has probably already been
// transformed in a way that does not preserve line numbers. That's
// ok, though, because we have a source map, and we also annotate
// line numbers using comments (see above), just in case source maps
// are not supported.
preserveLineNumbers = false;
} else if (noLineNumbers) {
// If we're not annotating line numbers, then we'd better try to
// preserve them, otherwise we won't be able to make any sense of
// line numbers found in stack traces.
preserveLineNumbers = true;
} else if (preserveLineNumbers &&
! _.has(options, noLineNumbers)) {
// If we don't have a source map, and we're supposed to be
// preserving line numbers, and options.noLineNumbers is
// unspecified, then we can get away without annotating line
// numbers, because they won't add any helpful information.
noLineNumbers = true;
}
if (self.sourceMap) {
result = {
code: self.source,
map: self.sourceMap
};
} else if (noLineNumbers) {
// No need to generate a source map if we don't want line numbers.
result = {
code: self.source,
map: null
};
} else {
// There are probably ways to make a more compact source map. For example,
// the only change we make is to append a comment, so we can probably emit
// one mapping for the whole file. For the moment, we'll do it by the book
// just to see how it goes.
numberifyLines(function (line, suffix, num) {
chunks.push(new sourcemap.SourceNode(num, 0, self._pathForSourceMap(),
line));
chunks.push(suffix);
// If we're planning to annotate the source with line number
// comments (e.g. because we're combining this file with others in a
// package), and we don't already have a source map, then we need to
// generate one, so that we don't have to write two different
// versions of the code for annotating line numbers, and also so
// that browsers that support source maps can display a prettier
// version of this file without the line number comments.
var smg = new sourcemap.SourceMapGenerator({
file: self.servePath
});
lines = self.source.split("\n");
_.each(lines, function (line, i) {
var start = { line: i + 1, column: 0 };
smg.addMapping({
original: start,
generated: start,
source: self.servePath
});
});
smg.setSourceContent(self.servePath, self.source);
result = {
code: self.source,
map: smg.toJSON()
};
}
var smc = result.map &&
new sourcemap.SourceMapConsumer(result.map);
if (smc && ! noLineNumbers) {
var padding = bannerPadding(bannerWidth);
// We might have already done this split above.
lines = lines || result.code.split("\n");
// Use the SourceMapConsumer object to compute the original line
// number for each line of result.code.
_.each(lines, function (line, i) {
var len = line.length;
if (len < width &&
line[len - 1] !== "\\") {
var pos = smc.originalPositionFor({
line: i + 1,
column: 0
});
if (pos) {
lines[i] += padding.slice(len, width) + " //";
// Not all source maps define a mapping for every line in the
// output. This is perfectly normal.
if (typeof pos.line === "number") {
lines[i] += " " + pos.line;
}
}
}
});
result.code = lines.join("\n");
}
var chunks = [];
var pathNoSlash = self.servePath.replace(/^\//, "");
if (! self.bare) {
chunks.push(
"(function(){",
preserveLineNumbers ? "" : "\n\n"
);
}
if (! preserveLineNumbers) {
// Banner
var bannerLines = [pathNoSlash];
if (self.bare) {
bannerLines.push(
"This file is in bare mode and is not in its own closure.");
}
chunks.push(banner(bannerLines, bannerWidth));
var blankLine = new Array(width + 1).join(' ') + " //\n";
chunks.push(blankLine);
}
if (result.code) {
chunks.push(
// If we have a source map for result.code, push a SourceNode onto
// the chunks array that encapsulates that source map. If we don't
// have a source map, just push result.code.
smc ? sourcemap.SourceNode.fromStringWithSourceMap(result.code, smc)
: result.code
);
// It's important for the code to end with a newline, so that a
// trailing // comment can't snarf code appended after it.
if (result.code[result.code - 1] !== "\n") {
chunks.push("\n");
}
}
// Footer
if (! self.bare)
chunks.push(dividerLine(bannerWidth) + "\n}).call(this);\n");
if (! self.bare) {
if (preserveLineNumbers) {
chunks.push("}).call(this);\n");
} else {
chunks.push(
dividerLine(bannerWidth),
"\n}).call(this);\n"
);
}
}
var node = new sourcemap.SourceNode(null, null, null, chunks);
// If we're working directly from the original source here (and not from the
// output of a transformation that had a source map), include the original
// source in the source map. (If we are working on generated code, the
// source map we received should have already contained the original
// source.)
if (!self.sourceMap)
node.setSourceContent(self._pathForSourceMap(), self.source);
return node;
return new sourcemap.SourceNode(null, null, null, chunks);
}
});