replace-and-update-source-map.ts 3.45 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.
*/

import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from '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
 */
export async function replaceAndUpdateSourceMap({
  jsFilename,
  originalMap,
  originalSource,
  replaceString,
  searchString,
}: {
  jsFilename: string;
  originalMap: RawSourceMap;
  originalSource: string;
  replaceString: string;
  searchString: string;
}): Promise<{map: string; source: string}> {
  const generator = new SourceMapGenerator({
    file: jsFilename,
  });

  const consumer = await new SourceMapConsumer(originalMap);

  let pos: number;
  let src = originalSource;
  const replacements: Array<{line: number; column: number}> = [];
  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();
  // JSON.parse returns any.
  // eslint-disable-next-line  @typescript-eslint/no-unsafe-assignment
  const updatedSourceMap: RawSourceMap = 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,
  };
}