You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
416 lines
14 KiB
416 lines
14 KiB
/* -*- 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 |
|
*/ |
|
|
|
var base64VLQ = require('./base64-vlq'); |
|
var util = require('./util'); |
|
var ArraySet = require('./array-set').ArraySet; |
|
var MappingList = require('./mapping-list').MappingList; |
|
|
|
/** |
|
* An instance of the SourceMapGenerator represents a source map which is |
|
* being built incrementally. You may pass an object with the following |
|
* properties: |
|
* |
|
* - file: The filename of the generated source. |
|
* - sourceRoot: A root for all relative URLs in this source map. |
|
*/ |
|
function SourceMapGenerator(aArgs) { |
|
if (!aArgs) { |
|
aArgs = {}; |
|
} |
|
this._file = util.getArg(aArgs, 'file', null); |
|
this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null); |
|
this._skipValidation = util.getArg(aArgs, 'skipValidation', false); |
|
this._sources = new ArraySet(); |
|
this._names = new ArraySet(); |
|
this._mappings = new MappingList(); |
|
this._sourcesContents = null; |
|
} |
|
|
|
SourceMapGenerator.prototype._version = 3; |
|
|
|
/** |
|
* Creates a new SourceMapGenerator based on a SourceMapConsumer |
|
* |
|
* @param aSourceMapConsumer The SourceMap. |
|
*/ |
|
SourceMapGenerator.fromSourceMap = |
|
function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) { |
|
var sourceRoot = aSourceMapConsumer.sourceRoot; |
|
var generator = new SourceMapGenerator({ |
|
file: aSourceMapConsumer.file, |
|
sourceRoot: sourceRoot |
|
}); |
|
aSourceMapConsumer.eachMapping(function (mapping) { |
|
var newMapping = { |
|
generated: { |
|
line: mapping.generatedLine, |
|
column: mapping.generatedColumn |
|
} |
|
}; |
|
|
|
if (mapping.source != null) { |
|
newMapping.source = mapping.source; |
|
if (sourceRoot != null) { |
|
newMapping.source = util.relative(sourceRoot, newMapping.source); |
|
} |
|
|
|
newMapping.original = { |
|
line: mapping.originalLine, |
|
column: mapping.originalColumn |
|
}; |
|
|
|
if (mapping.name != null) { |
|
newMapping.name = mapping.name; |
|
} |
|
} |
|
|
|
generator.addMapping(newMapping); |
|
}); |
|
aSourceMapConsumer.sources.forEach(function (sourceFile) { |
|
var content = aSourceMapConsumer.sourceContentFor(sourceFile); |
|
if (content != null) { |
|
generator.setSourceContent(sourceFile, content); |
|
} |
|
}); |
|
return generator; |
|
}; |
|
|
|
/** |
|
* Add a single mapping from original source line and column to the generated |
|
* source's line and column for this source map being created. The mapping |
|
* object should have the following properties: |
|
* |
|
* - generated: An object with the generated line and column positions. |
|
* - original: An object with the original line and column positions. |
|
* - source: The original source file (relative to the sourceRoot). |
|
* - name: An optional original token name for this mapping. |
|
*/ |
|
SourceMapGenerator.prototype.addMapping = |
|
function SourceMapGenerator_addMapping(aArgs) { |
|
var generated = util.getArg(aArgs, 'generated'); |
|
var original = util.getArg(aArgs, 'original', null); |
|
var source = util.getArg(aArgs, 'source', null); |
|
var name = util.getArg(aArgs, 'name', null); |
|
|
|
if (!this._skipValidation) { |
|
this._validateMapping(generated, original, source, name); |
|
} |
|
|
|
if (source != null) { |
|
source = String(source); |
|
if (!this._sources.has(source)) { |
|
this._sources.add(source); |
|
} |
|
} |
|
|
|
if (name != null) { |
|
name = String(name); |
|
if (!this._names.has(name)) { |
|
this._names.add(name); |
|
} |
|
} |
|
|
|
this._mappings.add({ |
|
generatedLine: generated.line, |
|
generatedColumn: generated.column, |
|
originalLine: original != null && original.line, |
|
originalColumn: original != null && original.column, |
|
source: source, |
|
name: name |
|
}); |
|
}; |
|
|
|
/** |
|
* Set the source content for a source file. |
|
*/ |
|
SourceMapGenerator.prototype.setSourceContent = |
|
function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) { |
|
var source = aSourceFile; |
|
if (this._sourceRoot != null) { |
|
source = util.relative(this._sourceRoot, source); |
|
} |
|
|
|
if (aSourceContent != null) { |
|
// Add the source content to the _sourcesContents map. |
|
// Create a new _sourcesContents map if the property is null. |
|
if (!this._sourcesContents) { |
|
this._sourcesContents = Object.create(null); |
|
} |
|
this._sourcesContents[util.toSetString(source)] = aSourceContent; |
|
} else if (this._sourcesContents) { |
|
// Remove the source file from the _sourcesContents map. |
|
// If the _sourcesContents map is empty, set the property to null. |
|
delete this._sourcesContents[util.toSetString(source)]; |
|
if (Object.keys(this._sourcesContents).length === 0) { |
|
this._sourcesContents = null; |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* Applies the mappings of a sub-source-map for a specific source file to the |
|
* source map being generated. Each mapping to the supplied source file is |
|
* rewritten using the supplied source map. Note: The resolution for the |
|
* resulting mappings is the minimium of this map and the supplied map. |
|
* |
|
* @param aSourceMapConsumer The source map to be applied. |
|
* @param aSourceFile Optional. The filename of the source file. |
|
* If omitted, SourceMapConsumer's file property will be used. |
|
* @param aSourceMapPath Optional. The dirname of the path to the source map |
|
* to be applied. If relative, it is relative to the SourceMapConsumer. |
|
* This parameter is needed when the two source maps aren't in the same |
|
* directory, and the source map to be applied contains relative source |
|
* paths. If so, those relative source paths need to be rewritten |
|
* relative to the SourceMapGenerator. |
|
*/ |
|
SourceMapGenerator.prototype.applySourceMap = |
|
function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) { |
|
var sourceFile = aSourceFile; |
|
// If aSourceFile is omitted, we will use the file property of the SourceMap |
|
if (aSourceFile == null) { |
|
if (aSourceMapConsumer.file == null) { |
|
throw new Error( |
|
'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' + |
|
'or the source map\'s "file" property. Both were omitted.' |
|
); |
|
} |
|
sourceFile = aSourceMapConsumer.file; |
|
} |
|
var sourceRoot = this._sourceRoot; |
|
// Make "sourceFile" relative if an absolute Url is passed. |
|
if (sourceRoot != null) { |
|
sourceFile = util.relative(sourceRoot, sourceFile); |
|
} |
|
// Applying the SourceMap can add and remove items from the sources and |
|
// the names array. |
|
var newSources = new ArraySet(); |
|
var newNames = new ArraySet(); |
|
|
|
// Find mappings for the "sourceFile" |
|
this._mappings.unsortedForEach(function (mapping) { |
|
if (mapping.source === sourceFile && mapping.originalLine != null) { |
|
// Check if it can be mapped by the source map, then update the mapping. |
|
var original = aSourceMapConsumer.originalPositionFor({ |
|
line: mapping.originalLine, |
|
column: mapping.originalColumn |
|
}); |
|
if (original.source != null) { |
|
// Copy mapping |
|
mapping.source = original.source; |
|
if (aSourceMapPath != null) { |
|
mapping.source = util.join(aSourceMapPath, mapping.source) |
|
} |
|
if (sourceRoot != null) { |
|
mapping.source = util.relative(sourceRoot, mapping.source); |
|
} |
|
mapping.originalLine = original.line; |
|
mapping.originalColumn = original.column; |
|
if (original.name != null) { |
|
mapping.name = original.name; |
|
} |
|
} |
|
} |
|
|
|
var source = mapping.source; |
|
if (source != null && !newSources.has(source)) { |
|
newSources.add(source); |
|
} |
|
|
|
var name = mapping.name; |
|
if (name != null && !newNames.has(name)) { |
|
newNames.add(name); |
|
} |
|
|
|
}, this); |
|
this._sources = newSources; |
|
this._names = newNames; |
|
|
|
// Copy sourcesContents of applied map. |
|
aSourceMapConsumer.sources.forEach(function (sourceFile) { |
|
var content = aSourceMapConsumer.sourceContentFor(sourceFile); |
|
if (content != null) { |
|
if (aSourceMapPath != null) { |
|
sourceFile = util.join(aSourceMapPath, sourceFile); |
|
} |
|
if (sourceRoot != null) { |
|
sourceFile = util.relative(sourceRoot, sourceFile); |
|
} |
|
this.setSourceContent(sourceFile, content); |
|
} |
|
}, this); |
|
}; |
|
|
|
/** |
|
* A mapping can have one of the three levels of data: |
|
* |
|
* 1. Just the generated position. |
|
* 2. The Generated position, original position, and original source. |
|
* 3. Generated and original position, original source, as well as a name |
|
* token. |
|
* |
|
* To maintain consistency, we validate that any new mapping being added falls |
|
* in to one of these categories. |
|
*/ |
|
SourceMapGenerator.prototype._validateMapping = |
|
function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource, |
|
aName) { |
|
// When aOriginal is truthy but has empty values for .line and .column, |
|
// it is most likely a programmer error. In this case we throw a very |
|
// specific error message to try to guide them the right way. |
|
// For example: https://github.com/Polymer/polymer-bundler/pull/519 |
|
if (aOriginal && typeof aOriginal.line !== 'number' && typeof aOriginal.column !== 'number') { |
|
throw new Error( |
|
'original.line and original.column are not numbers -- you probably meant to omit ' + |
|
'the original mapping entirely and only map the generated position. If so, pass ' + |
|
'null for the original mapping instead of an object with empty or null values.' |
|
); |
|
} |
|
|
|
if (aGenerated && 'line' in aGenerated && 'column' in aGenerated |
|
&& aGenerated.line > 0 && aGenerated.column >= 0 |
|
&& !aOriginal && !aSource && !aName) { |
|
// Case 1. |
|
return; |
|
} |
|
else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated |
|
&& aOriginal && 'line' in aOriginal && 'column' in aOriginal |
|
&& aGenerated.line > 0 && aGenerated.column >= 0 |
|
&& aOriginal.line > 0 && aOriginal.column >= 0 |
|
&& aSource) { |
|
// Cases 2 and 3. |
|
return; |
|
} |
|
else { |
|
throw new Error('Invalid mapping: ' + JSON.stringify({ |
|
generated: aGenerated, |
|
source: aSource, |
|
original: aOriginal, |
|
name: aName |
|
})); |
|
} |
|
}; |
|
|
|
/** |
|
* Serialize the accumulated mappings in to the stream of base 64 VLQs |
|
* specified by the source map format. |
|
*/ |
|
SourceMapGenerator.prototype._serializeMappings = |
|
function SourceMapGenerator_serializeMappings() { |
|
var previousGeneratedColumn = 0; |
|
var previousGeneratedLine = 1; |
|
var previousOriginalColumn = 0; |
|
var previousOriginalLine = 0; |
|
var previousName = 0; |
|
var previousSource = 0; |
|
var result = ''; |
|
var next; |
|
var mapping; |
|
var nameIdx; |
|
var sourceIdx; |
|
|
|
var mappings = this._mappings.toArray(); |
|
for (var i = 0, len = mappings.length; i < len; i++) { |
|
mapping = mappings[i]; |
|
next = '' |
|
|
|
if (mapping.generatedLine !== previousGeneratedLine) { |
|
previousGeneratedColumn = 0; |
|
while (mapping.generatedLine !== previousGeneratedLine) { |
|
next += ';'; |
|
previousGeneratedLine++; |
|
} |
|
} |
|
else { |
|
if (i > 0) { |
|
if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) { |
|
continue; |
|
} |
|
next += ','; |
|
} |
|
} |
|
|
|
next += base64VLQ.encode(mapping.generatedColumn |
|
- previousGeneratedColumn); |
|
previousGeneratedColumn = mapping.generatedColumn; |
|
|
|
if (mapping.source != null) { |
|
sourceIdx = this._sources.indexOf(mapping.source); |
|
next += base64VLQ.encode(sourceIdx - previousSource); |
|
previousSource = sourceIdx; |
|
|
|
// lines are stored 0-based in SourceMap spec version 3 |
|
next += base64VLQ.encode(mapping.originalLine - 1 |
|
- previousOriginalLine); |
|
previousOriginalLine = mapping.originalLine - 1; |
|
|
|
next += base64VLQ.encode(mapping.originalColumn |
|
- previousOriginalColumn); |
|
previousOriginalColumn = mapping.originalColumn; |
|
|
|
if (mapping.name != null) { |
|
nameIdx = this._names.indexOf(mapping.name); |
|
next += base64VLQ.encode(nameIdx - previousName); |
|
previousName = nameIdx; |
|
} |
|
} |
|
|
|
result += next; |
|
} |
|
|
|
return result; |
|
}; |
|
|
|
SourceMapGenerator.prototype._generateSourcesContent = |
|
function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) { |
|
return aSources.map(function (source) { |
|
if (!this._sourcesContents) { |
|
return null; |
|
} |
|
if (aSourceRoot != null) { |
|
source = util.relative(aSourceRoot, source); |
|
} |
|
var key = util.toSetString(source); |
|
return Object.prototype.hasOwnProperty.call(this._sourcesContents, key) |
|
? this._sourcesContents[key] |
|
: null; |
|
}, this); |
|
}; |
|
|
|
/** |
|
* Externalize the source map. |
|
*/ |
|
SourceMapGenerator.prototype.toJSON = |
|
function SourceMapGenerator_toJSON() { |
|
var map = { |
|
version: this._version, |
|
sources: this._sources.toArray(), |
|
names: this._names.toArray(), |
|
mappings: this._serializeMappings() |
|
}; |
|
if (this._file != null) { |
|
map.file = this._file; |
|
} |
|
if (this._sourceRoot != null) { |
|
map.sourceRoot = this._sourceRoot; |
|
} |
|
if (this._sourcesContents) { |
|
map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot); |
|
} |
|
|
|
return map; |
|
}; |
|
|
|
/** |
|
* Render the source map being generated to a string. |
|
*/ |
|
SourceMapGenerator.prototype.toString = |
|
function SourceMapGenerator_toString() { |
|
return JSON.stringify(this.toJSON()); |
|
}; |
|
|
|
exports.SourceMapGenerator = SourceMapGenerator;
|
|
|