replace-and-update-source-map.js 3.08 KB
/*
  Copyright 2019 Google LLC

  Use of this source code is governed by an MIT-style
  license that can be found in the LICENSE file or at
  https://opensource.org/licenses/MIT.
*/

const {SourceMapConsumer, SourceMapGenerator} = require('source-map');

/**
 * Adapted from https://github.com/nsams/sourcemap-aware-replace, with modern
 * JavaScript updates, along with additional properties copied from originalMap.
 *
 * @param {Object} options
 * @param {string} options.jsFilename The name for the file whose contents
 * correspond to originalSource.
 * @param {Object} options.originalMap The sourcemap for originalSource,
 * prior to any replacements.
 * @param {string} options.originalSource The source code, prior to any
 * replacements.
 * @param {string} options.replaceString A string to swap in for searchString.
 * @param {string} options.searchString A string in originalSource to replace.
 * Only the first occurrence will be replaced.
 * @return {{source: string, map: string}} An object containing both
 * originalSource with the replacement applied, and the modified originalMap.
 *
 * @private
 */
async function replaceAndUpdateSourceMap({
  jsFilename,
  originalMap,
  originalSource,
  replaceString,
  searchString,
}) {
  const generator = new SourceMapGenerator({
    file: jsFilename,
  });

  const consumer = await new SourceMapConsumer(originalMap);

  let pos;
  let src = originalSource;
  const replacements = [];
  let lineNum = 0;
  let filePos = 0;

  const lines = src.split('\n');
  for (let line of lines) {
    lineNum++;
    let searchPos = 0;
    while ((pos = line.indexOf(searchString, searchPos)) !== -1) {
      src = src.substring(0, filePos + pos) + replaceString +
        src.substring(filePos + pos + searchString.length);
      line = line.substring(0, pos) + replaceString +
        line.substring(pos + searchString.length);
      replacements.push({line: lineNum, column: pos});
      searchPos = pos + replaceString.length;
    }
    filePos += line.length + 1;
  }

  replacements.reverse();

  consumer.eachMapping((mapping) => {
    for (const replacement of replacements) {
      if (replacement.line == mapping.generatedLine &&
          mapping.generatedColumn > replacement.column) {
        const offset = searchString.length - replaceString.length;
        mapping.generatedColumn -= offset;
      }
    }

    if (mapping.source) {
      const newMapping = {
        generated: {
          line: mapping.generatedLine,
          column: mapping.generatedColumn,
        },
        original: {
          line: mapping.originalLine,
          column: mapping.originalColumn,
        },
        source: mapping.source,
      };
      return generator.addMapping(newMapping);
    }

    return mapping;
  });

  consumer.destroy();

  const updatedSourceMap = Object.assign(JSON.parse(generator.toString()), {
    names: originalMap.names,
    sourceRoot: originalMap.sourceRoot,
    sources: originalMap.sources,
    sourcesContent: originalMap.sourcesContent,
  });

  return {
    map: JSON.stringify(updatedSourceMap),
    source: src,
  };
}

module.exports = replaceAndUpdateSourceMap;