123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439 |
- /* -*- 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 base64VLQ = require('./base64-vlq')
- const util = require('./util')
- const ArraySet = require('./array-set').ArraySet
- const 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.
- */
- class SourceMapGenerator {
- constructor(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
- }
- /**
- * Creates a new SourceMapGenerator based on a SourceMapConsumer
- *
- * @param aSourceMapConsumer The SourceMap.
- */
- static fromSourceMap(aSourceMapConsumer) {
- const sourceRoot = aSourceMapConsumer.sourceRoot
- const generator = new SourceMapGenerator({
- file: aSourceMapConsumer.file,
- sourceRoot,
- })
- aSourceMapConsumer.eachMapping(function (mapping) {
- const 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) {
- let sourceRelative = sourceFile
- if (sourceRoot !== null) {
- sourceRelative = util.relative(sourceRoot, sourceFile)
- }
- if (!generator._sources.has(sourceRelative)) {
- generator._sources.add(sourceRelative)
- }
- const 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.
- */
- addMapping(aArgs) {
- const generated = util.getArg(aArgs, 'generated')
- const original = util.getArg(aArgs, 'original', null)
- let source = util.getArg(aArgs, 'source', null)
- let 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,
- name,
- })
- }
- /**
- * Set the source content for a source file.
- */
- setSourceContent(aSourceFile, aSourceContent) {
- let 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.
- */
- applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
- let 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
- }
- const 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.
- const newSources =
- this._mappings.toArray().length > 0 ? new ArraySet() : this._sources
- const 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.
- const 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
- }
- }
- }
- const source = mapping.source
- if (source != null && !newSources.has(source)) {
- newSources.add(source)
- }
- const 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 (srcFile) {
- const content = aSourceMapConsumer.sourceContentFor(srcFile)
- if (content != null) {
- if (aSourceMapPath != null) {
- srcFile = util.join(aSourceMapPath, srcFile)
- }
- if (sourceRoot != null) {
- srcFile = util.relative(sourceRoot, srcFile)
- }
- this.setSourceContent(srcFile, 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.
- */
- _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.
- } 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.
- } 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.
- */
- _serializeMappings() {
- let previousGeneratedColumn = 0
- let previousGeneratedLine = 1
- let previousOriginalColumn = 0
- let previousOriginalLine = 0
- let previousName = 0
- let previousSource = 0
- let result = ''
- let next
- let mapping
- let nameIdx
- let sourceIdx
- const mappings = this._mappings.toArray()
- for (let 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
- }
- _generateSourcesContent(aSources, aSourceRoot) {
- return aSources.map(function (source) {
- if (!this._sourcesContents) {
- return null
- }
- if (aSourceRoot != null) {
- source = util.relative(aSourceRoot, source)
- }
- const key = util.toSetString(source)
- return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)
- ? this._sourcesContents[key]
- : null
- }, this)
- }
- /**
- * Externalize the source map.
- */
- toJSON() {
- const 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.
- */
- toString() {
- return JSON.stringify(this.toJSON())
- }
- }
- SourceMapGenerator.prototype._version = 3
- exports.SourceMapGenerator = SourceMapGenerator
|