chromium/tools/code_coverage/js_source_maps/create_js_source_maps/create_js_source_maps.js

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview A simple wrapper around mozilla/source-map. Scans a file
 * processed by preprocess_if_expr.py looking for erasure comments. It creates
 * a sourcemap mapping the post-processed TypeScript or JavaScript back to the
 * original TypeScript or JavaScript.
 */

import fs from 'fs';
import path from 'path';

import {ArgumentParser} from '../../../../third_party/js_code_coverage/node_modules/argparse/argparse.js';
import {SourceMapGenerator} from '../../../../third_party/js_code_coverage/node_modules/source-map/source-map.js';

// Regex matching the comment indicating that preprocess_if_expr removed lines.
// The capture group contains the number of lines removed. Must match the
// comment added by tools/grit/grit/format/html_inline.py
const GRIT_REMOVED_LINES_REGEX = /grit-removed-lines:(\d+)/g;

/**
 * Processes the TypeScript or JavaScript files and produces
 * source map file / appends a source map.
 *
 * @param {Array} originals Original paths of `inputs`.
 * @param {Array} inputs The TypeScript or JavaScript files to read from.
 * @param {Array} outputs If `inlineSourcemaps`, the output TypeScript
 *                         or JavaScript files with the appended source map.
 *                         Otherwise, the standalone map file.
 * @param {boolean} verbose If true, print detailed information about the
 *                          mappings as they are added.
 * @param {string} sourceRoot The root for all source files.
 * @param {boolean} inlineSourcemaps If true, append source map instead of
 *                                   creating standalone map file.
 */
function processAllFiles(
    originals, inputs, outputs, verbose, sourceRoot, inlineSourcemaps) {
  if (originals.length != inputs.length || originals.length != outputs.length) {
    console.error(
        `The original files count(${originals.length}), ` +
        `the input files count(${inputs.length}) and the ` +
        `output files count(${outputs.length}) should be the same.`);
    process.exit(1);
  }

  for (let i = 0; i < originals.length; i++) {
    processOneFile(
        originals[i], inputs[i], outputs[i], verbose, sourceRoot,
        inlineSourcemaps);
  }
}

/**
 * Adds a mapping for a line. We only map lines, not columns -- we don't have
 * enough information to map columns within a line. (And the usual usage of
 * preprocess_if_expr means we don't expect to see partial line removals with
 * code after the removal.)
 *
 * @param {SourceMapGenerator} map The SourceMapGenerator.
 * @param {string} sourceFileName The name of the original file.
 * @param {number} originalLine The current line in the original source file.
 * @param {number} generatedLine The current line in the generated (processed)
 *                               source file.
 * @param {boolean} verbose If true, print detailed information about the
 *                          mappings as they are added.
 */
function addMapping(map, sourceFileName, originalLine, generatedLine, verbose) {
  const mapping = {
    source: sourceFileName,
    original: {
      line: originalLine,
      column: 0,
    },
    generated: {
      line: generatedLine,
      column: 0,
    },
  };
  if (verbose) {
    console.info(mapping);
  }
  map.addMapping(mapping);
}

/**
 * Processes one processed TypeScript or JavaScript file and produces one
 * source map file / appends a source map.
 *
 * @param {string} originalFileName Original path of `inputFileName`.
 * @param {string} inputFileName The TypeScript or JavaScript file to read from.
 * @param {string} outputFileName If `inlineSourcemaps`, the output TypeScript
 *                                or JavaScript file with the append source map.
 *                                Otherwise, the standalone map file.
 * @param {boolean} verbose If true, print detailed information about the
 *                          mappings as they are added.
 * @param {string} sourceRoot The root for all source files.
 * @param {boolean} inlineSourcemaps If true, append source map instead of
 *                                   creating standalone map file.
 */
function processOneFile(
    originalFileName, inputFileName, outputFileName, verbose, sourceRoot,
    inlineSourcemaps) {
  const inputFile = fs.readFileSync(inputFileName, 'utf8');
  const inputLines = inputFile.split('\n');
  const map = new SourceMapGenerator(
      {file: path.resolve(outputFileName), sourceRoot: sourceRoot});

  let originalLine = 0;
  let generatedLine = 0;

  for (const line of inputLines) {
    generatedLine++;
    originalLine++;

    // Add to sourcemap before looking for removal comments. The beginning of
    // the generated line came from the parts before the removal comment.
    addMapping(map, originalFileName, originalLine, generatedLine, verbose);

    for (const removal of line.matchAll(GRIT_REMOVED_LINES_REGEX)) {
      const removedLines = Number.parseInt(removal[1], 10);
      if (verbose) {
        console.info(`Found grit-removed-lines:${removedLines} on line ${
            generatedLine}`);
      }
      originalLine += removedLines;
    }
  }

  // Inline the source content.
  map.setSourceContent(
      originalFileName, fs.readFileSync(originalFileName).toString());

  if (!inlineSourcemaps) {
    fs.writeFileSync(outputFileName, map.toString());
  } else {
    const mapBase64 = Buffer.from(map.toString()).toString('base64');
    const output =
        `${inputFile}\n//# sourceMappingURL=data:application/json;base64,${
            mapBase64}`;
    fs.writeFileSync(outputFileName, output);
  }
}

function main() {
  const parser = new ArgumentParser({
    description:
        'Creates source maps for files preprocessed by preprocess_if_expr',
  });

  parser.addArgument(
      ['-v', '--verbose'],
      {help: 'Print each mapping & removed-line comment', action: 'storeTrue'});
  parser.addArgument(['--inline-sourcemaps'], {
    help: 'Copies contents of input to output and appends inline source maps',
    action: 'storeTrue',
  });
  parser.addArgument(
      '--originals', {help: 'Original file name', nargs: '*', action: 'store'});
  parser.addArgument(
      '--inputs', {help: 'Input file name', nargs: '*', action: 'store'});
  parser.addArgument(
      '--outputs', {help: 'Output file name', nargs: '*', action: 'store'});
  parser.addArgument('--sourceRoot', {
    help: 'Source directory to store the source map',
    required: false,
    default: process.cwd(),
  });

  const argv = parser.parseArgs();

  processAllFiles(
      argv.originals, argv.inputs, argv.outputs, argv.verbose, argv.sourceRoot,
      argv.inline_sourcemaps);
}

main();