1238 lines
41 KiB
JavaScript
1238 lines
41 KiB
JavaScript
/* -*- Mode: js; js-indent-level: 2; -*- */
|
|
/*
|
|
* Copyright 2011 Mozilla Foundation and contributors
|
|
* Licensed under the New BSD license. See LICENSE or:
|
|
* http://opensource.org/licenses/BSD-3-Clause
|
|
*/
|
|
|
|
const util = require("./util");
|
|
const binarySearch = require("./binary-search");
|
|
const ArraySet = require("./array-set").ArraySet;
|
|
const base64VLQ = require("./base64-vlq"); // eslint-disable-line no-unused-vars
|
|
const readWasm = require("../lib/read-wasm");
|
|
const wasm = require("./wasm");
|
|
|
|
const INTERNAL = Symbol("smcInternal");
|
|
|
|
class SourceMapConsumer {
|
|
constructor(aSourceMap, aSourceMapURL) {
|
|
// If the constructor was called by super(), just return Promise<this>.
|
|
// Yes, this is a hack to retain the pre-existing API of the base-class
|
|
// constructor also being an async factory function.
|
|
if (aSourceMap == INTERNAL) {
|
|
return Promise.resolve(this);
|
|
}
|
|
|
|
return _factory(aSourceMap, aSourceMapURL);
|
|
}
|
|
|
|
static initialize(opts) {
|
|
readWasm.initialize(opts["lib/mappings.wasm"]);
|
|
}
|
|
|
|
static fromSourceMap(aSourceMap, aSourceMapURL) {
|
|
return _factoryBSM(aSourceMap, aSourceMapURL);
|
|
}
|
|
|
|
/**
|
|
* Construct a new `SourceMapConsumer` from `rawSourceMap` and `sourceMapUrl`
|
|
* (see the `SourceMapConsumer` constructor for details. Then, invoke the `async
|
|
* function f(SourceMapConsumer) -> T` with the newly constructed consumer, wait
|
|
* for `f` to complete, call `destroy` on the consumer, and return `f`'s return
|
|
* value.
|
|
*
|
|
* You must not use the consumer after `f` completes!
|
|
*
|
|
* By using `with`, you do not have to remember to manually call `destroy` on
|
|
* the consumer, since it will be called automatically once `f` completes.
|
|
*
|
|
* ```js
|
|
* const xSquared = await SourceMapConsumer.with(
|
|
* myRawSourceMap,
|
|
* null,
|
|
* async function (consumer) {
|
|
* // Use `consumer` inside here and don't worry about remembering
|
|
* // to call `destroy`.
|
|
*
|
|
* const x = await whatever(consumer);
|
|
* return x * x;
|
|
* }
|
|
* );
|
|
*
|
|
* // You may not use that `consumer` anymore out here; it has
|
|
* // been destroyed. But you can use `xSquared`.
|
|
* console.log(xSquared);
|
|
* ```
|
|
*/
|
|
static async with(rawSourceMap, sourceMapUrl, f) {
|
|
const consumer = await new SourceMapConsumer(rawSourceMap, sourceMapUrl);
|
|
try {
|
|
return await f(consumer);
|
|
} finally {
|
|
consumer.destroy();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse the mappings in a string in to a data structure which we can easily
|
|
* query (the ordered arrays in the `this.__generatedMappings` and
|
|
* `this.__originalMappings` properties).
|
|
*/
|
|
_parseMappings(aStr, aSourceRoot) {
|
|
throw new Error("Subclasses must implement _parseMappings");
|
|
}
|
|
|
|
/**
|
|
* Iterate over each mapping between an original source/line/column and a
|
|
* generated line/column in this source map.
|
|
*
|
|
* @param Function aCallback
|
|
* The function that is called with each mapping.
|
|
* @param Object aContext
|
|
* Optional. If specified, this object will be the value of `this` every
|
|
* time that `aCallback` is called.
|
|
* @param aOrder
|
|
* Either `SourceMapConsumer.GENERATED_ORDER` or
|
|
* `SourceMapConsumer.ORIGINAL_ORDER`. Specifies whether you want to
|
|
* iterate over the mappings sorted by the generated file's line/column
|
|
* order or the original's source/line/column order, respectively. Defaults to
|
|
* `SourceMapConsumer.GENERATED_ORDER`.
|
|
*/
|
|
eachMapping(aCallback, aContext, aOrder) {
|
|
throw new Error("Subclasses must implement eachMapping");
|
|
}
|
|
|
|
/**
|
|
* Returns all generated line and column information for the original source,
|
|
* line, and column provided. If no column is provided, returns all mappings
|
|
* corresponding to a either the line we are searching for or the next
|
|
* closest line that has any mappings. Otherwise, returns all mappings
|
|
* corresponding to the given line and either the column we are searching for
|
|
* or the next closest column that has any offsets.
|
|
*
|
|
* The only argument is an object with the following properties:
|
|
*
|
|
* - source: The filename of the original source.
|
|
* - line: The line number in the original source. The line number is 1-based.
|
|
* - column: Optional. the column number in the original source.
|
|
* The column number is 0-based.
|
|
*
|
|
* and an array of objects is returned, each with the following properties:
|
|
*
|
|
* - line: The line number in the generated source, or null. The
|
|
* line number is 1-based.
|
|
* - column: The column number in the generated source, or null.
|
|
* The column number is 0-based.
|
|
*/
|
|
allGeneratedPositionsFor(aArgs) {
|
|
throw new Error("Subclasses must implement allGeneratedPositionsFor");
|
|
}
|
|
|
|
destroy() {
|
|
throw new Error("Subclasses must implement destroy");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The version of the source mapping spec that we are consuming.
|
|
*/
|
|
SourceMapConsumer.prototype._version = 3;
|
|
SourceMapConsumer.GENERATED_ORDER = 1;
|
|
SourceMapConsumer.ORIGINAL_ORDER = 2;
|
|
|
|
SourceMapConsumer.GREATEST_LOWER_BOUND = 1;
|
|
SourceMapConsumer.LEAST_UPPER_BOUND = 2;
|
|
|
|
exports.SourceMapConsumer = SourceMapConsumer;
|
|
|
|
/**
|
|
* A BasicSourceMapConsumer instance represents a parsed source map which we can
|
|
* query for information about the original file positions by giving it a file
|
|
* position in the generated source.
|
|
*
|
|
* The first parameter is the raw source map (either as a JSON string, or
|
|
* already parsed to an object). According to the spec, source maps have the
|
|
* following attributes:
|
|
*
|
|
* - version: Which version of the source map spec this map is following.
|
|
* - sources: An array of URLs to the original source files.
|
|
* - names: An array of identifiers which can be referenced by individual mappings.
|
|
* - sourceRoot: Optional. The URL root from which all sources are relative.
|
|
* - sourcesContent: Optional. An array of contents of the original source files.
|
|
* - mappings: A string of base64 VLQs which contain the actual mappings.
|
|
* - file: Optional. The generated file this source map is associated with.
|
|
*
|
|
* Here is an example source map, taken from the source map spec[0]:
|
|
*
|
|
* {
|
|
* version : 3,
|
|
* file: "out.js",
|
|
* sourceRoot : "",
|
|
* sources: ["foo.js", "bar.js"],
|
|
* names: ["src", "maps", "are", "fun"],
|
|
* mappings: "AA,AB;;ABCDE;"
|
|
* }
|
|
*
|
|
* The second parameter, if given, is a string whose value is the URL
|
|
* at which the source map was found. This URL is used to compute the
|
|
* sources array.
|
|
*
|
|
* [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
|
|
*/
|
|
class BasicSourceMapConsumer extends SourceMapConsumer {
|
|
constructor(aSourceMap, aSourceMapURL) {
|
|
return super(INTERNAL).then(that => {
|
|
let sourceMap = aSourceMap;
|
|
if (typeof aSourceMap === "string") {
|
|
sourceMap = util.parseSourceMapInput(aSourceMap);
|
|
}
|
|
|
|
const version = util.getArg(sourceMap, "version");
|
|
let sources = util.getArg(sourceMap, "sources");
|
|
// Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
|
|
// requires the array) to play nice here.
|
|
const names = util.getArg(sourceMap, "names", []);
|
|
let sourceRoot = util.getArg(sourceMap, "sourceRoot", null);
|
|
const sourcesContent = util.getArg(sourceMap, "sourcesContent", null);
|
|
const mappings = util.getArg(sourceMap, "mappings");
|
|
const file = util.getArg(sourceMap, "file", null);
|
|
|
|
// Once again, Sass deviates from the spec and supplies the version as a
|
|
// string rather than a number, so we use loose equality checking here.
|
|
if (version != that._version) {
|
|
throw new Error("Unsupported version: " + version);
|
|
}
|
|
|
|
if (sourceRoot) {
|
|
sourceRoot = util.normalize(sourceRoot);
|
|
}
|
|
|
|
sources = sources
|
|
.map(String)
|
|
// Some source maps produce relative source paths like "./foo.js" instead of
|
|
// "foo.js". Normalize these first so that future comparisons will succeed.
|
|
// See bugzil.la/1090768.
|
|
.map(util.normalize)
|
|
// Always ensure that absolute sources are internally stored relative to
|
|
// the source root, if the source root is absolute. Not doing this would
|
|
// be particularly problematic when the source root is a prefix of the
|
|
// source (valid, but why??). See github issue #199 and bugzil.la/1188982.
|
|
.map(function(source) {
|
|
return sourceRoot && util.isAbsolute(sourceRoot) && util.isAbsolute(source)
|
|
? util.relative(sourceRoot, source)
|
|
: source;
|
|
});
|
|
|
|
// Pass `true` below to allow duplicate names and sources. While source maps
|
|
// are intended to be compressed and deduplicated, the TypeScript compiler
|
|
// sometimes generates source maps with duplicates in them. See Github issue
|
|
// #72 and bugzil.la/889492.
|
|
that._names = ArraySet.fromArray(names.map(String), true);
|
|
that._sources = ArraySet.fromArray(sources, true);
|
|
|
|
that._absoluteSources = that._sources.toArray().map(function(s) {
|
|
return util.computeSourceURL(sourceRoot, s, aSourceMapURL);
|
|
});
|
|
|
|
that.sourceRoot = sourceRoot;
|
|
that.sourcesContent = sourcesContent;
|
|
that._mappings = mappings;
|
|
that._sourceMapURL = aSourceMapURL;
|
|
that.file = file;
|
|
|
|
that._computedColumnSpans = false;
|
|
that._mappingsPtr = 0;
|
|
that._wasm = null;
|
|
|
|
return wasm().then(w => {
|
|
that._wasm = w;
|
|
return that;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Utility function to find the index of a source. Returns -1 if not
|
|
* found.
|
|
*/
|
|
_findSourceIndex(aSource) {
|
|
let relativeSource = aSource;
|
|
if (this.sourceRoot != null) {
|
|
relativeSource = util.relative(this.sourceRoot, relativeSource);
|
|
}
|
|
|
|
if (this._sources.has(relativeSource)) {
|
|
return this._sources.indexOf(relativeSource);
|
|
}
|
|
|
|
// Maybe aSource is an absolute URL as returned by |sources|. In
|
|
// this case we can't simply undo the transform.
|
|
for (let i = 0; i < this._absoluteSources.length; ++i) {
|
|
if (this._absoluteSources[i] == aSource) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Create a BasicSourceMapConsumer from a SourceMapGenerator.
|
|
*
|
|
* @param SourceMapGenerator aSourceMap
|
|
* The source map that will be consumed.
|
|
* @param String aSourceMapURL
|
|
* The URL at which the source map can be found (optional)
|
|
* @returns BasicSourceMapConsumer
|
|
*/
|
|
static fromSourceMap(aSourceMap, aSourceMapURL) {
|
|
return new BasicSourceMapConsumer(aSourceMap.toString());
|
|
}
|
|
|
|
get sources() {
|
|
return this._absoluteSources.slice();
|
|
}
|
|
|
|
_getMappingsPtr() {
|
|
if (this._mappingsPtr === 0) {
|
|
this._parseMappings(this._mappings, this.sourceRoot);
|
|
}
|
|
|
|
return this._mappingsPtr;
|
|
}
|
|
|
|
/**
|
|
* Parse the mappings in a string in to a data structure which we can easily
|
|
* query (the ordered arrays in the `this.__generatedMappings` and
|
|
* `this.__originalMappings` properties).
|
|
*/
|
|
_parseMappings(aStr, aSourceRoot) {
|
|
const size = aStr.length;
|
|
|
|
const mappingsBufPtr = this._wasm.exports.allocate_mappings(size);
|
|
const mappingsBuf = new Uint8Array(this._wasm.exports.memory.buffer, mappingsBufPtr, size);
|
|
for (let i = 0; i < size; i++) {
|
|
mappingsBuf[i] = aStr.charCodeAt(i);
|
|
}
|
|
|
|
const mappingsPtr = this._wasm.exports.parse_mappings(mappingsBufPtr);
|
|
|
|
if (!mappingsPtr) {
|
|
const error = this._wasm.exports.get_last_error();
|
|
let msg = `Error parsing mappings (code ${error}): `;
|
|
|
|
// XXX: keep these error codes in sync with `fitzgen/source-map-mappings`.
|
|
switch (error) {
|
|
case 1:
|
|
msg += "the mappings contained a negative line, column, source index, or name index";
|
|
break;
|
|
case 2:
|
|
msg += "the mappings contained a number larger than 2**32";
|
|
break;
|
|
case 3:
|
|
msg += "reached EOF while in the middle of parsing a VLQ";
|
|
break;
|
|
case 4:
|
|
msg += "invalid base 64 character while parsing a VLQ";
|
|
break;
|
|
default:
|
|
msg += "unknown error code";
|
|
break;
|
|
}
|
|
|
|
throw new Error(msg);
|
|
}
|
|
|
|
this._mappingsPtr = mappingsPtr;
|
|
}
|
|
|
|
eachMapping(aCallback, aContext, aOrder) {
|
|
const context = aContext || null;
|
|
const order = aOrder || SourceMapConsumer.GENERATED_ORDER;
|
|
const sourceRoot = this.sourceRoot;
|
|
|
|
this._wasm.withMappingCallback(
|
|
mapping => {
|
|
if (mapping.source !== null) {
|
|
mapping.source = this._sources.at(mapping.source);
|
|
mapping.source = util.computeSourceURL(sourceRoot, mapping.source, this._sourceMapURL);
|
|
|
|
if (mapping.name !== null) {
|
|
mapping.name = this._names.at(mapping.name);
|
|
}
|
|
}
|
|
|
|
aCallback.call(context, mapping);
|
|
},
|
|
() => {
|
|
switch (order) {
|
|
case SourceMapConsumer.GENERATED_ORDER:
|
|
this._wasm.exports.by_generated_location(this._getMappingsPtr());
|
|
break;
|
|
case SourceMapConsumer.ORIGINAL_ORDER:
|
|
this._wasm.exports.by_original_location(this._getMappingsPtr());
|
|
break;
|
|
default:
|
|
throw new Error("Unknown order of iteration.");
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
allGeneratedPositionsFor(aArgs) {
|
|
let source = util.getArg(aArgs, "source");
|
|
const originalLine = util.getArg(aArgs, "line");
|
|
const originalColumn = aArgs.column || 0;
|
|
|
|
source = this._findSourceIndex(source);
|
|
if (source < 0) {
|
|
return [];
|
|
}
|
|
|
|
if (originalLine < 1) {
|
|
throw new Error("Line numbers must be >= 1");
|
|
}
|
|
|
|
if (originalColumn < 0) {
|
|
throw new Error("Column numbers must be >= 0");
|
|
}
|
|
|
|
const mappings = [];
|
|
|
|
this._wasm.withMappingCallback(
|
|
m => {
|
|
let lastColumn = m.lastGeneratedColumn;
|
|
if (this._computedColumnSpans && lastColumn === null) {
|
|
lastColumn = Infinity;
|
|
}
|
|
mappings.push({
|
|
line: m.generatedLine,
|
|
column: m.generatedColumn,
|
|
lastColumn,
|
|
});
|
|
}, () => {
|
|
this._wasm.exports.all_generated_locations_for(
|
|
this._getMappingsPtr(),
|
|
source,
|
|
originalLine - 1,
|
|
"column" in aArgs,
|
|
originalColumn
|
|
);
|
|
}
|
|
);
|
|
|
|
return mappings;
|
|
}
|
|
|
|
destroy() {
|
|
if (this._mappingsPtr !== 0) {
|
|
this._wasm.exports.free_mappings(this._mappingsPtr);
|
|
this._mappingsPtr = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compute the last column for each generated mapping. The last column is
|
|
* inclusive.
|
|
*/
|
|
computeColumnSpans() {
|
|
if (this._computedColumnSpans) {
|
|
return;
|
|
}
|
|
|
|
this._wasm.exports.compute_column_spans(this._getMappingsPtr());
|
|
this._computedColumnSpans = true;
|
|
}
|
|
|
|
/**
|
|
* Returns the original source, line, and column information for the generated
|
|
* source's line and column positions provided. The only argument is an object
|
|
* with the following properties:
|
|
*
|
|
* - line: The line number in the generated source. The line number
|
|
* is 1-based.
|
|
* - column: The column number in the generated source. The column
|
|
* number is 0-based.
|
|
* - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
|
|
* 'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
|
|
* closest element that is smaller than or greater than the one we are
|
|
* searching for, respectively, if the exact element cannot be found.
|
|
* Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
|
|
*
|
|
* and an object is returned with the following properties:
|
|
*
|
|
* - source: The original source file, or null.
|
|
* - line: The line number in the original source, or null. The
|
|
* line number is 1-based.
|
|
* - column: The column number in the original source, or null. The
|
|
* column number is 0-based.
|
|
* - name: The original identifier, or null.
|
|
*/
|
|
originalPositionFor(aArgs) {
|
|
const needle = {
|
|
generatedLine: util.getArg(aArgs, "line"),
|
|
generatedColumn: util.getArg(aArgs, "column")
|
|
};
|
|
|
|
if (needle.generatedLine < 1) {
|
|
throw new Error("Line numbers must be >= 1");
|
|
}
|
|
|
|
if (needle.generatedColumn < 0) {
|
|
throw new Error("Column numbers must be >= 0");
|
|
}
|
|
|
|
let bias = util.getArg(aArgs, "bias", SourceMapConsumer.GREATEST_LOWER_BOUND);
|
|
if (bias == null) {
|
|
bias = SourceMapConsumer.GREATEST_LOWER_BOUND;
|
|
}
|
|
|
|
let mapping;
|
|
this._wasm.withMappingCallback(m => mapping = m, () => {
|
|
this._wasm.exports.original_location_for(
|
|
this._getMappingsPtr(),
|
|
needle.generatedLine - 1,
|
|
needle.generatedColumn,
|
|
bias
|
|
);
|
|
});
|
|
|
|
if (mapping) {
|
|
if (mapping.generatedLine === needle.generatedLine) {
|
|
let source = util.getArg(mapping, "source", null);
|
|
if (source !== null) {
|
|
source = this._sources.at(source);
|
|
source = util.computeSourceURL(this.sourceRoot, source, this._sourceMapURL);
|
|
}
|
|
|
|
let name = util.getArg(mapping, "name", null);
|
|
if (name !== null) {
|
|
name = this._names.at(name);
|
|
}
|
|
|
|
return {
|
|
source,
|
|
line: util.getArg(mapping, "originalLine", null),
|
|
column: util.getArg(mapping, "originalColumn", null),
|
|
name
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
source: null,
|
|
line: null,
|
|
column: null,
|
|
name: null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Return true if we have the source content for every source in the source
|
|
* map, false otherwise.
|
|
*/
|
|
hasContentsOfAllSources() {
|
|
if (!this.sourcesContent) {
|
|
return false;
|
|
}
|
|
return this.sourcesContent.length >= this._sources.size() &&
|
|
!this.sourcesContent.some(function(sc) { return sc == null; });
|
|
}
|
|
|
|
/**
|
|
* Returns the original source content. The only argument is the url of the
|
|
* original source file. Returns null if no original source content is
|
|
* available.
|
|
*/
|
|
sourceContentFor(aSource, nullOnMissing) {
|
|
if (!this.sourcesContent) {
|
|
return null;
|
|
}
|
|
|
|
const index = this._findSourceIndex(aSource);
|
|
if (index >= 0) {
|
|
return this.sourcesContent[index];
|
|
}
|
|
|
|
let relativeSource = aSource;
|
|
if (this.sourceRoot != null) {
|
|
relativeSource = util.relative(this.sourceRoot, relativeSource);
|
|
}
|
|
|
|
let url;
|
|
if (this.sourceRoot != null
|
|
&& (url = util.urlParse(this.sourceRoot))) {
|
|
// XXX: file:// URIs and absolute paths lead to unexpected behavior for
|
|
// many users. We can help them out when they expect file:// URIs to
|
|
// behave like it would if they were running a local HTTP server. See
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
|
|
const fileUriAbsPath = relativeSource.replace(/^file:\/\//, "");
|
|
if (url.scheme == "file"
|
|
&& this._sources.has(fileUriAbsPath)) {
|
|
return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)];
|
|
}
|
|
|
|
if ((!url.path || url.path == "/")
|
|
&& this._sources.has("/" + relativeSource)) {
|
|
return this.sourcesContent[this._sources.indexOf("/" + relativeSource)];
|
|
}
|
|
}
|
|
|
|
// This function is used recursively from
|
|
// IndexedSourceMapConsumer.prototype.sourceContentFor. In that case, we
|
|
// don't want to throw if we can't find the source - we just want to
|
|
// return null, so we provide a flag to exit gracefully.
|
|
if (nullOnMissing) {
|
|
return null;
|
|
}
|
|
|
|
throw new Error('"' + relativeSource + '" is not in the SourceMap.');
|
|
}
|
|
|
|
/**
|
|
* Returns the generated line and column information for the original source,
|
|
* line, and column positions provided. The only argument is an object with
|
|
* the following properties:
|
|
*
|
|
* - source: The filename of the original source.
|
|
* - line: The line number in the original source. The line number
|
|
* is 1-based.
|
|
* - column: The column number in the original source. The column
|
|
* number is 0-based.
|
|
* - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
|
|
* 'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
|
|
* closest element that is smaller than or greater than the one we are
|
|
* searching for, respectively, if the exact element cannot be found.
|
|
* Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
|
|
*
|
|
* and an object is returned with the following properties:
|
|
*
|
|
* - line: The line number in the generated source, or null. The
|
|
* line number is 1-based.
|
|
* - column: The column number in the generated source, or null.
|
|
* The column number is 0-based.
|
|
*/
|
|
generatedPositionFor(aArgs) {
|
|
let source = util.getArg(aArgs, "source");
|
|
source = this._findSourceIndex(source);
|
|
if (source < 0) {
|
|
return {
|
|
line: null,
|
|
column: null,
|
|
lastColumn: null
|
|
};
|
|
}
|
|
|
|
const needle = {
|
|
source,
|
|
originalLine: util.getArg(aArgs, "line"),
|
|
originalColumn: util.getArg(aArgs, "column")
|
|
};
|
|
|
|
if (needle.originalLine < 1) {
|
|
throw new Error("Line numbers must be >= 1");
|
|
}
|
|
|
|
if (needle.originalColumn < 0) {
|
|
throw new Error("Column numbers must be >= 0");
|
|
}
|
|
|
|
let bias = util.getArg(aArgs, "bias", SourceMapConsumer.GREATEST_LOWER_BOUND);
|
|
if (bias == null) {
|
|
bias = SourceMapConsumer.GREATEST_LOWER_BOUND;
|
|
}
|
|
|
|
let mapping;
|
|
this._wasm.withMappingCallback(m => mapping = m, () => {
|
|
this._wasm.exports.generated_location_for(
|
|
this._getMappingsPtr(),
|
|
needle.source,
|
|
needle.originalLine - 1,
|
|
needle.originalColumn,
|
|
bias
|
|
);
|
|
});
|
|
|
|
if (mapping) {
|
|
if (mapping.source === needle.source) {
|
|
let lastColumn = mapping.lastGeneratedColumn;
|
|
if (this._computedColumnSpans && lastColumn === null) {
|
|
lastColumn = Infinity;
|
|
}
|
|
return {
|
|
line: util.getArg(mapping, "generatedLine", null),
|
|
column: util.getArg(mapping, "generatedColumn", null),
|
|
lastColumn,
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
line: null,
|
|
column: null,
|
|
lastColumn: null
|
|
};
|
|
}
|
|
}
|
|
|
|
BasicSourceMapConsumer.prototype.consumer = SourceMapConsumer;
|
|
exports.BasicSourceMapConsumer = BasicSourceMapConsumer;
|
|
|
|
/**
|
|
* An IndexedSourceMapConsumer instance represents a parsed source map which
|
|
* we can query for information. It differs from BasicSourceMapConsumer in
|
|
* that it takes "indexed" source maps (i.e. ones with a "sections" field) as
|
|
* input.
|
|
*
|
|
* The first parameter is a raw source map (either as a JSON string, or already
|
|
* parsed to an object). According to the spec for indexed source maps, they
|
|
* have the following attributes:
|
|
*
|
|
* - version: Which version of the source map spec this map is following.
|
|
* - file: Optional. The generated file this source map is associated with.
|
|
* - sections: A list of section definitions.
|
|
*
|
|
* Each value under the "sections" field has two fields:
|
|
* - offset: The offset into the original specified at which this section
|
|
* begins to apply, defined as an object with a "line" and "column"
|
|
* field.
|
|
* - map: A source map definition. This source map could also be indexed,
|
|
* but doesn't have to be.
|
|
*
|
|
* Instead of the "map" field, it's also possible to have a "url" field
|
|
* specifying a URL to retrieve a source map from, but that's currently
|
|
* unsupported.
|
|
*
|
|
* Here's an example source map, taken from the source map spec[0], but
|
|
* modified to omit a section which uses the "url" field.
|
|
*
|
|
* {
|
|
* version : 3,
|
|
* file: "app.js",
|
|
* sections: [{
|
|
* offset: {line:100, column:10},
|
|
* map: {
|
|
* version : 3,
|
|
* file: "section.js",
|
|
* sources: ["foo.js", "bar.js"],
|
|
* names: ["src", "maps", "are", "fun"],
|
|
* mappings: "AAAA,E;;ABCDE;"
|
|
* }
|
|
* }],
|
|
* }
|
|
*
|
|
* The second parameter, if given, is a string whose value is the URL
|
|
* at which the source map was found. This URL is used to compute the
|
|
* sources array.
|
|
*
|
|
* [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.535es3xeprgt
|
|
*/
|
|
class IndexedSourceMapConsumer extends SourceMapConsumer {
|
|
constructor(aSourceMap, aSourceMapURL) {
|
|
return super(INTERNAL).then(that => {
|
|
let sourceMap = aSourceMap;
|
|
if (typeof aSourceMap === "string") {
|
|
sourceMap = util.parseSourceMapInput(aSourceMap);
|
|
}
|
|
|
|
const version = util.getArg(sourceMap, "version");
|
|
const sections = util.getArg(sourceMap, "sections");
|
|
|
|
if (version != that._version) {
|
|
throw new Error("Unsupported version: " + version);
|
|
}
|
|
|
|
that._sources = new ArraySet();
|
|
that._names = new ArraySet();
|
|
that.__generatedMappings = null;
|
|
that.__originalMappings = null;
|
|
that.__generatedMappingsUnsorted = null;
|
|
that.__originalMappingsUnsorted = null;
|
|
|
|
let lastOffset = {
|
|
line: -1,
|
|
column: 0
|
|
};
|
|
return Promise.all(sections.map(s => {
|
|
if (s.url) {
|
|
// The url field will require support for asynchronicity.
|
|
// See https://github.com/mozilla/source-map/issues/16
|
|
throw new Error("Support for url field in sections not implemented.");
|
|
}
|
|
const offset = util.getArg(s, "offset");
|
|
const offsetLine = util.getArg(offset, "line");
|
|
const offsetColumn = util.getArg(offset, "column");
|
|
|
|
if (offsetLine < lastOffset.line ||
|
|
(offsetLine === lastOffset.line && offsetColumn < lastOffset.column)) {
|
|
throw new Error("Section offsets must be ordered and non-overlapping.");
|
|
}
|
|
lastOffset = offset;
|
|
|
|
const cons = new SourceMapConsumer(util.getArg(s, "map"), aSourceMapURL);
|
|
return cons.then(consumer => {
|
|
return {
|
|
generatedOffset: {
|
|
// The offset fields are 0-based, but we use 1-based indices when
|
|
// encoding/decoding from VLQ.
|
|
generatedLine: offsetLine + 1,
|
|
generatedColumn: offsetColumn + 1
|
|
},
|
|
consumer
|
|
};
|
|
});
|
|
})).then(s => {
|
|
that._sections = s;
|
|
return that;
|
|
});
|
|
});
|
|
}
|
|
|
|
// `__generatedMappings` and `__originalMappings` are arrays that hold the
|
|
// parsed mapping coordinates from the source map's "mappings" attribute. They
|
|
// are lazily instantiated, accessed via the `_generatedMappings` and
|
|
// `_originalMappings` getters respectively, and we only parse the mappings
|
|
// and create these arrays once queried for a source location. We jump through
|
|
// these hoops because there can be many thousands of mappings, and parsing
|
|
// them is expensive, so we only want to do it if we must.
|
|
//
|
|
// Each object in the arrays is of the form:
|
|
//
|
|
// {
|
|
// generatedLine: The line number in the generated code,
|
|
// generatedColumn: The column number in the generated code,
|
|
// source: The path to the original source file that generated this
|
|
// chunk of code,
|
|
// originalLine: The line number in the original source that
|
|
// corresponds to this chunk of generated code,
|
|
// originalColumn: The column number in the original source that
|
|
// corresponds to this chunk of generated code,
|
|
// name: The name of the original symbol which generated this chunk of
|
|
// code.
|
|
// }
|
|
//
|
|
// All properties except for `generatedLine` and `generatedColumn` can be
|
|
// `null`.
|
|
//
|
|
// `_generatedMappings` is ordered by the generated positions.
|
|
//
|
|
// `_originalMappings` is ordered by the original positions.
|
|
get _generatedMappings() {
|
|
if (!this.__generatedMappings) {
|
|
this._sortGeneratedMappings();
|
|
}
|
|
|
|
return this.__generatedMappings;
|
|
}
|
|
|
|
get _originalMappings() {
|
|
if (!this.__originalMappings) {
|
|
this._sortOriginalMappings();
|
|
}
|
|
|
|
return this.__originalMappings;
|
|
}
|
|
|
|
get _generatedMappingsUnsorted() {
|
|
if (!this.__generatedMappingsUnsorted) {
|
|
this._parseMappings(this._mappings, this.sourceRoot);
|
|
}
|
|
|
|
return this.__generatedMappingsUnsorted;
|
|
}
|
|
|
|
get _originalMappingsUnsorted() {
|
|
if (!this.__originalMappingsUnsorted) {
|
|
this._parseMappings(this._mappings, this.sourceRoot);
|
|
}
|
|
|
|
return this.__originalMappingsUnsorted;
|
|
}
|
|
|
|
_sortGeneratedMappings() {
|
|
const mappings = this._generatedMappingsUnsorted;
|
|
mappings.sort(util.compareByGeneratedPositionsDeflated);
|
|
this.__generatedMappings = mappings;
|
|
}
|
|
|
|
_sortOriginalMappings() {
|
|
const mappings = this._originalMappingsUnsorted;
|
|
mappings.sort(util.compareByOriginalPositions);
|
|
this.__originalMappings = mappings;
|
|
}
|
|
|
|
/**
|
|
* The list of original sources.
|
|
*/
|
|
get sources() {
|
|
const sources = [];
|
|
for (let i = 0; i < this._sections.length; i++) {
|
|
for (let j = 0; j < this._sections[i].consumer.sources.length; j++) {
|
|
sources.push(this._sections[i].consumer.sources[j]);
|
|
}
|
|
}
|
|
return sources;
|
|
}
|
|
|
|
/**
|
|
* Returns the original source, line, and column information for the generated
|
|
* source's line and column positions provided. The only argument is an object
|
|
* with the following properties:
|
|
*
|
|
* - line: The line number in the generated source. The line number
|
|
* is 1-based.
|
|
* - column: The column number in the generated source. The column
|
|
* number is 0-based.
|
|
*
|
|
* and an object is returned with the following properties:
|
|
*
|
|
* - source: The original source file, or null.
|
|
* - line: The line number in the original source, or null. The
|
|
* line number is 1-based.
|
|
* - column: The column number in the original source, or null. The
|
|
* column number is 0-based.
|
|
* - name: The original identifier, or null.
|
|
*/
|
|
originalPositionFor(aArgs) {
|
|
const needle = {
|
|
generatedLine: util.getArg(aArgs, "line"),
|
|
generatedColumn: util.getArg(aArgs, "column")
|
|
};
|
|
|
|
// Find the section containing the generated position we're trying to map
|
|
// to an original position.
|
|
const sectionIndex = binarySearch.search(needle, this._sections,
|
|
function(aNeedle, section) {
|
|
const cmp = aNeedle.generatedLine - section.generatedOffset.generatedLine;
|
|
if (cmp) {
|
|
return cmp;
|
|
}
|
|
|
|
return (aNeedle.generatedColumn -
|
|
section.generatedOffset.generatedColumn);
|
|
});
|
|
const section = this._sections[sectionIndex];
|
|
|
|
if (!section) {
|
|
return {
|
|
source: null,
|
|
line: null,
|
|
column: null,
|
|
name: null
|
|
};
|
|
}
|
|
|
|
return section.consumer.originalPositionFor({
|
|
line: needle.generatedLine -
|
|
(section.generatedOffset.generatedLine - 1),
|
|
column: needle.generatedColumn -
|
|
(section.generatedOffset.generatedLine === needle.generatedLine
|
|
? section.generatedOffset.generatedColumn - 1
|
|
: 0),
|
|
bias: aArgs.bias
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Return true if we have the source content for every source in the source
|
|
* map, false otherwise.
|
|
*/
|
|
hasContentsOfAllSources() {
|
|
return this._sections.every(function(s) {
|
|
return s.consumer.hasContentsOfAllSources();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns the original source content. The only argument is the url of the
|
|
* original source file. Returns null if no original source content is
|
|
* available.
|
|
*/
|
|
sourceContentFor(aSource, nullOnMissing) {
|
|
for (let i = 0; i < this._sections.length; i++) {
|
|
const section = this._sections[i];
|
|
|
|
const content = section.consumer.sourceContentFor(aSource, true);
|
|
if (content) {
|
|
return content;
|
|
}
|
|
}
|
|
if (nullOnMissing) {
|
|
return null;
|
|
}
|
|
throw new Error('"' + aSource + '" is not in the SourceMap.');
|
|
}
|
|
|
|
/**
|
|
* Returns the generated line and column information for the original source,
|
|
* line, and column positions provided. The only argument is an object with
|
|
* the following properties:
|
|
*
|
|
* - source: The filename of the original source.
|
|
* - line: The line number in the original source. The line number
|
|
* is 1-based.
|
|
* - column: The column number in the original source. The column
|
|
* number is 0-based.
|
|
*
|
|
* and an object is returned with the following properties:
|
|
*
|
|
* - line: The line number in the generated source, or null. The
|
|
* line number is 1-based.
|
|
* - column: The column number in the generated source, or null.
|
|
* The column number is 0-based.
|
|
*/
|
|
generatedPositionFor(aArgs) {
|
|
for (let i = 0; i < this._sections.length; i++) {
|
|
const section = this._sections[i];
|
|
|
|
// Only consider this section if the requested source is in the list of
|
|
// sources of the consumer.
|
|
if (section.consumer._findSourceIndex(util.getArg(aArgs, "source")) === -1) {
|
|
continue;
|
|
}
|
|
const generatedPosition = section.consumer.generatedPositionFor(aArgs);
|
|
if (generatedPosition) {
|
|
const ret = {
|
|
line: generatedPosition.line +
|
|
(section.generatedOffset.generatedLine - 1),
|
|
column: generatedPosition.column +
|
|
(section.generatedOffset.generatedLine === generatedPosition.line
|
|
? section.generatedOffset.generatedColumn - 1
|
|
: 0)
|
|
};
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return {
|
|
line: null,
|
|
column: null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parse the mappings in a string in to a data structure which we can easily
|
|
* query (the ordered arrays in the `this.__generatedMappings` and
|
|
* `this.__originalMappings` properties).
|
|
*/
|
|
_parseMappings(aStr, aSourceRoot) {
|
|
const generatedMappings = this.__generatedMappingsUnsorted = [];
|
|
const originalMappings = this.__originalMappingsUnsorted = [];
|
|
for (let i = 0; i < this._sections.length; i++) {
|
|
const section = this._sections[i];
|
|
|
|
const sectionMappings = [];
|
|
section.consumer.eachMapping(m => sectionMappings.push(m));
|
|
|
|
for (let j = 0; j < sectionMappings.length; j++) {
|
|
const mapping = sectionMappings[j];
|
|
|
|
// TODO: test if null is correct here. The original code used
|
|
// `source`, which would actually have gotten used as null because
|
|
// var's get hoisted.
|
|
// See: https://github.com/mozilla/source-map/issues/333
|
|
let source = util.computeSourceURL(section.consumer.sourceRoot, null, this._sourceMapURL);
|
|
this._sources.add(source);
|
|
source = this._sources.indexOf(source);
|
|
|
|
let name = null;
|
|
if (mapping.name) {
|
|
this._names.add(mapping.name);
|
|
name = this._names.indexOf(mapping.name);
|
|
}
|
|
|
|
// The mappings coming from the consumer for the section have
|
|
// generated positions relative to the start of the section, so we
|
|
// need to offset them to be relative to the start of the concatenated
|
|
// generated file.
|
|
const adjustedMapping = {
|
|
source,
|
|
generatedLine: mapping.generatedLine +
|
|
(section.generatedOffset.generatedLine - 1),
|
|
generatedColumn: mapping.generatedColumn +
|
|
(section.generatedOffset.generatedLine === mapping.generatedLine
|
|
? section.generatedOffset.generatedColumn - 1
|
|
: 0),
|
|
originalLine: mapping.originalLine,
|
|
originalColumn: mapping.originalColumn,
|
|
name
|
|
};
|
|
|
|
generatedMappings.push(adjustedMapping);
|
|
if (typeof adjustedMapping.originalLine === "number") {
|
|
originalMappings.push(adjustedMapping);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
eachMapping(aCallback, aContext, aOrder) {
|
|
const context = aContext || null;
|
|
const order = aOrder || SourceMapConsumer.GENERATED_ORDER;
|
|
|
|
let mappings;
|
|
switch (order) {
|
|
case SourceMapConsumer.GENERATED_ORDER:
|
|
mappings = this._generatedMappings;
|
|
break;
|
|
case SourceMapConsumer.ORIGINAL_ORDER:
|
|
mappings = this._originalMappings;
|
|
break;
|
|
default:
|
|
throw new Error("Unknown order of iteration.");
|
|
}
|
|
|
|
const sourceRoot = this.sourceRoot;
|
|
mappings.map(function(mapping) {
|
|
let source = null;
|
|
if (mapping.source !== null) {
|
|
source = this._sources.at(mapping.source);
|
|
source = util.computeSourceURL(sourceRoot, source, this._sourceMapURL);
|
|
}
|
|
return {
|
|
source,
|
|
generatedLine: mapping.generatedLine,
|
|
generatedColumn: mapping.generatedColumn,
|
|
originalLine: mapping.originalLine,
|
|
originalColumn: mapping.originalColumn,
|
|
name: mapping.name === null ? null : this._names.at(mapping.name)
|
|
};
|
|
}, this).forEach(aCallback, context);
|
|
}
|
|
|
|
/**
|
|
* Find the mapping that best matches the hypothetical "needle" mapping that
|
|
* we are searching for in the given "haystack" of mappings.
|
|
*/
|
|
_findMapping(aNeedle, aMappings, aLineName,
|
|
aColumnName, aComparator, aBias) {
|
|
// To return the position we are searching for, we must first find the
|
|
// mapping for the given position and then return the opposite position it
|
|
// points to. Because the mappings are sorted, we can use binary search to
|
|
// find the best mapping.
|
|
|
|
if (aNeedle[aLineName] <= 0) {
|
|
throw new TypeError("Line must be greater than or equal to 1, got "
|
|
+ aNeedle[aLineName]);
|
|
}
|
|
if (aNeedle[aColumnName] < 0) {
|
|
throw new TypeError("Column must be greater than or equal to 0, got "
|
|
+ aNeedle[aColumnName]);
|
|
}
|
|
|
|
return binarySearch.search(aNeedle, aMappings, aComparator, aBias);
|
|
}
|
|
|
|
allGeneratedPositionsFor(aArgs) {
|
|
const line = util.getArg(aArgs, "line");
|
|
|
|
// When there is no exact match, BasicSourceMapConsumer.prototype._findMapping
|
|
// returns the index of the closest mapping less than the needle. By
|
|
// setting needle.originalColumn to 0, we thus find the last mapping for
|
|
// the given line, provided such a mapping exists.
|
|
const needle = {
|
|
source: util.getArg(aArgs, "source"),
|
|
originalLine: line,
|
|
originalColumn: util.getArg(aArgs, "column", 0)
|
|
};
|
|
|
|
needle.source = this._findSourceIndex(needle.source);
|
|
if (needle.source < 0) {
|
|
return [];
|
|
}
|
|
|
|
if (needle.originalLine < 1) {
|
|
throw new Error("Line numbers must be >= 1");
|
|
}
|
|
|
|
if (needle.originalColumn < 0) {
|
|
throw new Error("Column numbers must be >= 0");
|
|
}
|
|
|
|
const mappings = [];
|
|
|
|
let index = this._findMapping(needle,
|
|
this._originalMappings,
|
|
"originalLine",
|
|
"originalColumn",
|
|
util.compareByOriginalPositions,
|
|
binarySearch.LEAST_UPPER_BOUND);
|
|
if (index >= 0) {
|
|
let mapping = this._originalMappings[index];
|
|
|
|
if (aArgs.column === undefined) {
|
|
const originalLine = mapping.originalLine;
|
|
|
|
// Iterate until either we run out of mappings, or we run into
|
|
// a mapping for a different line than the one we found. Since
|
|
// mappings are sorted, this is guaranteed to find all mappings for
|
|
// the line we found.
|
|
while (mapping && mapping.originalLine === originalLine) {
|
|
let lastColumn = mapping.lastGeneratedColumn;
|
|
if (this._computedColumnSpans && lastColumn === null) {
|
|
lastColumn = Infinity;
|
|
}
|
|
mappings.push({
|
|
line: util.getArg(mapping, "generatedLine", null),
|
|
column: util.getArg(mapping, "generatedColumn", null),
|
|
lastColumn,
|
|
});
|
|
|
|
mapping = this._originalMappings[++index];
|
|
}
|
|
} else {
|
|
const originalColumn = mapping.originalColumn;
|
|
|
|
// Iterate until either we run out of mappings, or we run into
|
|
// a mapping for a different line than the one we were searching for.
|
|
// Since mappings are sorted, this is guaranteed to find all mappings for
|
|
// the line we are searching for.
|
|
while (mapping &&
|
|
mapping.originalLine === line &&
|
|
mapping.originalColumn == originalColumn) {
|
|
let lastColumn = mapping.lastGeneratedColumn;
|
|
if (this._computedColumnSpans && lastColumn === null) {
|
|
lastColumn = Infinity;
|
|
}
|
|
mappings.push({
|
|
line: util.getArg(mapping, "generatedLine", null),
|
|
column: util.getArg(mapping, "generatedColumn", null),
|
|
lastColumn,
|
|
});
|
|
|
|
mapping = this._originalMappings[++index];
|
|
}
|
|
}
|
|
}
|
|
|
|
return mappings;
|
|
}
|
|
|
|
destroy() {
|
|
for (let i = 0; i < this._sections.length; i++) {
|
|
this._sections[i].consumer.destroy();
|
|
}
|
|
}
|
|
}
|
|
exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer;
|
|
|
|
/*
|
|
* Cheat to get around inter-twingled classes. `factory()` can be at the end
|
|
* where it has access to non-hoisted classes, but it gets hoisted itself.
|
|
*/
|
|
function _factory(aSourceMap, aSourceMapURL) {
|
|
let sourceMap = aSourceMap;
|
|
if (typeof aSourceMap === "string") {
|
|
sourceMap = util.parseSourceMapInput(aSourceMap);
|
|
}
|
|
|
|
const consumer = sourceMap.sections != null
|
|
? new IndexedSourceMapConsumer(sourceMap, aSourceMapURL)
|
|
: new BasicSourceMapConsumer(sourceMap, aSourceMapURL);
|
|
return Promise.resolve(consumer);
|
|
}
|
|
|
|
function _factoryBSM(aSourceMap, aSourceMapURL) {
|
|
return BasicSourceMapConsumer.fromSourceMap(aSourceMap, aSourceMapURL);
|
|
}
|