tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

source-map-generator.js (13893B)


      1 /* -*- Mode: js; js-indent-level: 2; -*- */
      2 /*
      3 * Copyright 2011 Mozilla Foundation and contributors
      4 * Licensed under the New BSD license. See LICENSE or:
      5 * http://opensource.org/licenses/BSD-3-Clause
      6 */
      7 
      8 const base64VLQ = require("./base64-vlq");
      9 const util = require("./util");
     10 const ArraySet = require("./array-set").ArraySet;
     11 const MappingList = require("./mapping-list").MappingList;
     12 
     13 /**
     14 * An instance of the SourceMapGenerator represents a source map which is
     15 * being built incrementally. You may pass an object with the following
     16 * properties:
     17 *
     18 *   - file: The filename of the generated source.
     19 *   - sourceRoot: A root for all relative URLs in this source map.
     20 */
     21 class SourceMapGenerator {
     22  constructor(aArgs) {
     23    if (!aArgs) {
     24      aArgs = {};
     25    }
     26    this._file = util.getArg(aArgs, "file", null);
     27    this._sourceRoot = util.getArg(aArgs, "sourceRoot", null);
     28    this._skipValidation = util.getArg(aArgs, "skipValidation", false);
     29    this._sources = new ArraySet();
     30    this._names = new ArraySet();
     31    this._mappings = new MappingList();
     32    this._sourcesContents = null;
     33  }
     34 
     35  /**
     36   * Creates a new SourceMapGenerator based on a SourceMapConsumer
     37   *
     38   * @param aSourceMapConsumer The SourceMap.
     39   */
     40  static fromSourceMap(aSourceMapConsumer) {
     41    const sourceRoot = aSourceMapConsumer.sourceRoot;
     42    const generator = new SourceMapGenerator({
     43      file: aSourceMapConsumer.file,
     44      sourceRoot,
     45    });
     46    aSourceMapConsumer.eachMapping(function (mapping) {
     47      const newMapping = {
     48        generated: {
     49          line: mapping.generatedLine,
     50          column: mapping.generatedColumn,
     51        },
     52      };
     53 
     54      if (mapping.source != null) {
     55        newMapping.source = mapping.source;
     56        if (sourceRoot != null) {
     57          newMapping.source = util.relative(sourceRoot, newMapping.source);
     58        }
     59 
     60        newMapping.original = {
     61          line: mapping.originalLine,
     62          column: mapping.originalColumn,
     63        };
     64 
     65        if (mapping.name != null) {
     66          newMapping.name = mapping.name;
     67        }
     68      }
     69 
     70      generator.addMapping(newMapping);
     71    });
     72    aSourceMapConsumer.sources.forEach(function (sourceFile) {
     73      let sourceRelative = sourceFile;
     74      if (sourceRoot != null) {
     75        sourceRelative = util.relative(sourceRoot, sourceFile);
     76      }
     77 
     78      if (!generator._sources.has(sourceRelative)) {
     79        generator._sources.add(sourceRelative);
     80      }
     81 
     82      const content = aSourceMapConsumer.sourceContentFor(sourceFile);
     83      if (content != null) {
     84        generator.setSourceContent(sourceFile, content);
     85      }
     86    });
     87    return generator;
     88  }
     89 
     90  /**
     91   * Add a single mapping from original source line and column to the generated
     92   * source's line and column for this source map being created. The mapping
     93   * object should have the following properties:
     94   *
     95   *   - generated: An object with the generated line and column positions.
     96   *   - original: An object with the original line and column positions.
     97   *   - source: The original source file (relative to the sourceRoot).
     98   *   - name: An optional original token name for this mapping.
     99   */
    100  addMapping(aArgs) {
    101    const generated = util.getArg(aArgs, "generated");
    102    const original = util.getArg(aArgs, "original", null);
    103    let source = util.getArg(aArgs, "source", null);
    104    let name = util.getArg(aArgs, "name", null);
    105 
    106    if (!this._skipValidation) {
    107      this._validateMapping(generated, original, source, name);
    108    }
    109 
    110    if (source != null) {
    111      source = String(source);
    112      if (!this._sources.has(source)) {
    113        this._sources.add(source);
    114      }
    115    }
    116 
    117    if (name != null) {
    118      name = String(name);
    119      if (!this._names.has(name)) {
    120        this._names.add(name);
    121      }
    122    }
    123 
    124    this._mappings.add({
    125      generatedLine: generated.line,
    126      generatedColumn: generated.column,
    127      originalLine: original && original.line,
    128      originalColumn: original && original.column,
    129      source,
    130      name,
    131    });
    132  }
    133 
    134  /**
    135   * Set the source content for a source file.
    136   */
    137  setSourceContent(aSourceFile, aSourceContent) {
    138    let source = aSourceFile;
    139    if (this._sourceRoot != null) {
    140      source = util.relative(this._sourceRoot, source);
    141    }
    142 
    143    if (aSourceContent != null) {
    144      // Add the source content to the _sourcesContents map.
    145      // Create a new _sourcesContents map if the property is null.
    146      if (!this._sourcesContents) {
    147        this._sourcesContents = Object.create(null);
    148      }
    149      this._sourcesContents[util.toSetString(source)] = aSourceContent;
    150    } else if (this._sourcesContents) {
    151      // Remove the source file from the _sourcesContents map.
    152      // If the _sourcesContents map is empty, set the property to null.
    153      delete this._sourcesContents[util.toSetString(source)];
    154      if (Object.keys(this._sourcesContents).length === 0) {
    155        this._sourcesContents = null;
    156      }
    157    }
    158  }
    159 
    160  /**
    161   * Applies the mappings of a sub-source-map for a specific source file to the
    162   * source map being generated. Each mapping to the supplied source file is
    163   * rewritten using the supplied source map. Note: The resolution for the
    164   * resulting mappings is the minimium of this map and the supplied map.
    165   *
    166   * @param aSourceMapConsumer The source map to be applied.
    167   * @param aSourceFile Optional. The filename of the source file.
    168   *        If omitted, SourceMapConsumer's file property will be used.
    169   * @param aSourceMapPath Optional. The dirname of the path to the source map
    170   *        to be applied. If relative, it is relative to the SourceMapConsumer.
    171   *        This parameter is needed when the two source maps aren't in the same
    172   *        directory, and the source map to be applied contains relative source
    173   *        paths. If so, those relative source paths need to be rewritten
    174   *        relative to the SourceMapGenerator.
    175   */
    176  applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
    177    let sourceFile = aSourceFile;
    178    // If aSourceFile is omitted, we will use the file property of the SourceMap
    179    if (aSourceFile == null) {
    180      if (aSourceMapConsumer.file == null) {
    181        throw new Error(
    182          "SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, " +
    183            'or the source map\'s "file" property. Both were omitted.'
    184        );
    185      }
    186      sourceFile = aSourceMapConsumer.file;
    187    }
    188    const sourceRoot = this._sourceRoot;
    189    // Make "sourceFile" relative if an absolute Url is passed.
    190    if (sourceRoot != null) {
    191      sourceFile = util.relative(sourceRoot, sourceFile);
    192    }
    193    // Applying the SourceMap can add and remove items from the sources and
    194    // the names array.
    195    const newSources =
    196      this._mappings.toArray().length > 0 ? new ArraySet() : this._sources;
    197    const newNames = new ArraySet();
    198 
    199    // Find mappings for the "sourceFile"
    200    this._mappings.unsortedForEach(function (mapping) {
    201      if (mapping.source === sourceFile && mapping.originalLine != null) {
    202        // Check if it can be mapped by the source map, then update the mapping.
    203        const original = aSourceMapConsumer.originalPositionFor({
    204          line: mapping.originalLine,
    205          column: mapping.originalColumn,
    206        });
    207        if (original.source != null) {
    208          // Copy mapping
    209          mapping.source = original.source;
    210          if (aSourceMapPath != null) {
    211            mapping.source = util.join(aSourceMapPath, mapping.source);
    212          }
    213          if (sourceRoot != null) {
    214            mapping.source = util.relative(sourceRoot, mapping.source);
    215          }
    216          mapping.originalLine = original.line;
    217          mapping.originalColumn = original.column;
    218          if (original.name != null) {
    219            mapping.name = original.name;
    220          }
    221        }
    222      }
    223 
    224      const source = mapping.source;
    225      if (source != null && !newSources.has(source)) {
    226        newSources.add(source);
    227      }
    228 
    229      const name = mapping.name;
    230      if (name != null && !newNames.has(name)) {
    231        newNames.add(name);
    232      }
    233    }, this);
    234    this._sources = newSources;
    235    this._names = newNames;
    236 
    237    // Copy sourcesContents of applied map.
    238    aSourceMapConsumer.sources.forEach(function (srcFile) {
    239      const content = aSourceMapConsumer.sourceContentFor(srcFile);
    240      if (content != null) {
    241        if (aSourceMapPath != null) {
    242          srcFile = util.join(aSourceMapPath, srcFile);
    243        }
    244        if (sourceRoot != null) {
    245          srcFile = util.relative(sourceRoot, srcFile);
    246        }
    247        this.setSourceContent(srcFile, content);
    248      }
    249    }, this);
    250  }
    251 
    252  /**
    253   * A mapping can have one of the three levels of data:
    254   *
    255   *   1. Just the generated position.
    256   *   2. The Generated position, original position, and original source.
    257   *   3. Generated and original position, original source, as well as a name
    258   *      token.
    259   *
    260   * To maintain consistency, we validate that any new mapping being added falls
    261   * in to one of these categories.
    262   */
    263  _validateMapping(aGenerated, aOriginal, aSource, aName) {
    264    // When aOriginal is truthy but has empty values for .line and .column,
    265    // it is most likely a programmer error. In this case we throw a very
    266    // specific error message to try to guide them the right way.
    267    // For example: https://github.com/Polymer/polymer-bundler/pull/519
    268    if (
    269      aOriginal &&
    270      typeof aOriginal.line !== "number" &&
    271      typeof aOriginal.column !== "number"
    272    ) {
    273      throw new Error(
    274        "original.line and original.column are not numbers -- you probably meant to omit " +
    275          "the original mapping entirely and only map the generated position. If so, pass " +
    276          "null for the original mapping instead of an object with empty or null values."
    277      );
    278    }
    279 
    280    if (
    281      aGenerated &&
    282      "line" in aGenerated &&
    283      "column" in aGenerated &&
    284      aGenerated.line > 0 &&
    285      aGenerated.column >= 0 &&
    286      !aOriginal &&
    287      !aSource &&
    288      !aName
    289    ) {
    290      // Case 1.
    291    } else if (
    292      aGenerated &&
    293      "line" in aGenerated &&
    294      "column" in aGenerated &&
    295      aOriginal &&
    296      "line" in aOriginal &&
    297      "column" in aOriginal &&
    298      aGenerated.line > 0 &&
    299      aGenerated.column >= 0 &&
    300      aOriginal.line > 0 &&
    301      aOriginal.column >= 0 &&
    302      aSource
    303    ) {
    304      // Cases 2 and 3.
    305    } else {
    306      throw new Error(
    307        "Invalid mapping: " +
    308          JSON.stringify({
    309            generated: aGenerated,
    310            source: aSource,
    311            original: aOriginal,
    312            name: aName,
    313          })
    314      );
    315    }
    316  }
    317 
    318  /**
    319   * Serialize the accumulated mappings in to the stream of base 64 VLQs
    320   * specified by the source map format.
    321   */
    322  _serializeMappings() {
    323    let previousGeneratedColumn = 0;
    324    let previousGeneratedLine = 1;
    325    let previousOriginalColumn = 0;
    326    let previousOriginalLine = 0;
    327    let previousName = 0;
    328    let previousSource = 0;
    329    let result = "";
    330    let next;
    331    let mapping;
    332    let nameIdx;
    333    let sourceIdx;
    334 
    335    const mappings = this._mappings.toArray();
    336    for (let i = 0, len = mappings.length; i < len; i++) {
    337      mapping = mappings[i];
    338      next = "";
    339 
    340      if (mapping.generatedLine !== previousGeneratedLine) {
    341        previousGeneratedColumn = 0;
    342        while (mapping.generatedLine !== previousGeneratedLine) {
    343          next += ";";
    344          previousGeneratedLine++;
    345        }
    346      } else if (i > 0) {
    347        if (
    348          !util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])
    349        ) {
    350          continue;
    351        }
    352        next += ",";
    353      }
    354 
    355      next += base64VLQ.encode(
    356        mapping.generatedColumn - previousGeneratedColumn
    357      );
    358      previousGeneratedColumn = mapping.generatedColumn;
    359 
    360      if (mapping.source != null) {
    361        sourceIdx = this._sources.indexOf(mapping.source);
    362        next += base64VLQ.encode(sourceIdx - previousSource);
    363        previousSource = sourceIdx;
    364 
    365        // lines are stored 0-based in SourceMap spec version 3
    366        next += base64VLQ.encode(
    367          mapping.originalLine - 1 - previousOriginalLine
    368        );
    369        previousOriginalLine = mapping.originalLine - 1;
    370 
    371        next += base64VLQ.encode(
    372          mapping.originalColumn - previousOriginalColumn
    373        );
    374        previousOriginalColumn = mapping.originalColumn;
    375 
    376        if (mapping.name != null) {
    377          nameIdx = this._names.indexOf(mapping.name);
    378          next += base64VLQ.encode(nameIdx - previousName);
    379          previousName = nameIdx;
    380        }
    381      }
    382 
    383      result += next;
    384    }
    385 
    386    return result;
    387  }
    388 
    389  _generateSourcesContent(aSources, aSourceRoot) {
    390    return aSources.map(function (source) {
    391      if (!this._sourcesContents) {
    392        return null;
    393      }
    394      if (aSourceRoot != null) {
    395        source = util.relative(aSourceRoot, source);
    396      }
    397      const key = util.toSetString(source);
    398      return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)
    399        ? this._sourcesContents[key]
    400        : null;
    401    }, this);
    402  }
    403 
    404  /**
    405   * Externalize the source map.
    406   */
    407  toJSON() {
    408    const map = {
    409      version: this._version,
    410      sources: this._sources.toArray(),
    411      names: this._names.toArray(),
    412      mappings: this._serializeMappings(),
    413    };
    414    if (this._file != null) {
    415      map.file = this._file;
    416    }
    417    if (this._sourceRoot != null) {
    418      map.sourceRoot = this._sourceRoot;
    419    }
    420    if (this._sourcesContents) {
    421      map.sourcesContent = this._generateSourcesContent(
    422        map.sources,
    423        map.sourceRoot
    424      );
    425    }
    426 
    427    return map;
    428  }
    429 
    430  /**
    431   * Render the source map being generated to a string.
    432   */
    433  toString() {
    434    return JSON.stringify(this.toJSON());
    435  }
    436 }
    437 
    438 SourceMapGenerator.prototype._version = 3;
    439 exports.SourceMapGenerator = SourceMapGenerator;