text: const assert = require("assert");
  const convertSourceMap = require("convert-source-map");
  const util = require("util");
  const debuglog = util.debuglog("c8");
  const { dirname, isAbsolute, join, resolve } = require("path");
  const { fileURLToPath } = require("url");
  const CovBranch = require("./branch");
  const CovFunction = require("./function");
  const CovSource = require("./source");
  const { sliceRange } = require("./range");
  const compatError = Error(
    `requires Node.js ${require("../package.json").engines.node}`,
  );
  let readFile = () => {
    throw compatError;
  };
  try {
    readFile = require("fs").promises.readFile;
  } catch (_err) {
    // most likely we're on an older version of Node.js.
  }
  const { SourceMapConsumer } = require("source-map");
  const isOlderNode10 = /^v10\.(([0-9]\.)|(1[0-5]\.))/u.test(process.version);
  const isNode8 = /^v8\./.test(process.version);
  
  // Injected when Node.js is loading script into isolate pre Node 10.16.x.
  // see: https://github.com/nodejs/node/pull/21573.
  const cjsWrapperLength = isOlderNode10
    ? require("module").wrapper[0].length
    : 0;
  
  module.exports = class V8ToIstanbul {
    constructor(scriptPath, wrapperLength, sources, excludePath) {
      assert(typeof scriptPath === "string", "scriptPath must be a string");
      assert(
        !isNode8,
        "This module does not support node 8 or lower, please upgrade to node 10",
      );
      this.path = parsePath(scriptPath);
      this.wrapperLength = wrapperLength === undefined
        ? cjsWrapperLength
        : wrapperLength;
      this.excludePath = excludePath || (() => false);
      this.sources = sources || {};
      this.generatedLines = [];
      this.branches = {};
      this.functions = {};
      this.covSources = [];
      this.rawSourceMap = undefined;
      this.sourceMap = undefined;
      this.sourceTranspiled = undefined;
      // Indicate that this report was generated with placeholder data from
      // running --all:
      this.all = false;
    }
  
    async load() {
      const rawSource = this.sources.source || await readFile(this.path, "utf8");
      this.rawSourceMap = this.sources.sourceMap ||
        // if we find a source-map (either inline, or a .map file) we load
        // both the transpiled and original source, both of which are used during
        // the backflips we perform to remap absolute to relative positions.
        convertSourceMap.fromSource(rawSource) ||
        convertSourceMap.fromMapFileSource(rawSource, dirname(this.path));
  
      if (this.rawSourceMap) {
        if (this.rawSourceMap.sourcemap.sources.length > 1) {
          this.sourceMap = await new SourceMapConsumer(
            this.rawSourceMap.sourcemap,
          );
          if (!this.sourceMap.sourcesContent) {
            this.sourceMap.sourcesContent = await this
              .sourcesContentFromSources();
          }
          this.covSources = this.sourceMap.sourcesContent.map((rawSource, i) => ({
            source: new CovSource(rawSource, this.wrapperLength),
            path: this.sourceMap.sources[i],
          }));
          this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength);
        } else {
          const candidatePath = this.rawSourceMap.sourcemap.sources.length >= 1
            ? this.rawSourceMap.sourcemap.sources[0]
            : this.rawSourceMap.sourcemap.file;
          this.path = this._resolveSource(
            this.rawSourceMap,
            candidatePath || this.path,
          );
          this.sourceMap = await new SourceMapConsumer(
            this.rawSourceMap.sourcemap,
          );
  
          let originalRawSource;
          if (
            this.sources.sourceMap && this.sources.sourceMap.sourcemap &&
            this.sources.sourceMap.sourcemap.sourcesContent &&
            this.sources.sourceMap.sourcemap.sourcesContent.length === 1
          ) {
            // If the sourcesContent field has been provided, return it rather than attempting
            // to load the original source from disk.
            // TODO: investigate whether there's ever a case where we hit this logic with 1:many sources.
            originalRawSource =
              this.sources.sourceMap.sourcemap.sourcesContent[0];
          } else if (this.sources.originalSource) {
            // Original source may be populated on the sources object.
            originalRawSource = this.sources.originalSource;
          } else if (
            this.sourceMap.sourcesContent && this.sourceMap.sourcesContent[0]
          ) {
            // perhaps we loaded sourcesContent was populated by an inline source map, or .map file?
            // TODO: investigate whether there's ever a case where we hit this logic with 1:many sources.
            originalRawSource = this.sourceMap.sourcesContent[0];
          } else {
            // We fallback to reading the original source from disk.
            originalRawSource = await readFile(this.path, "utf8");
          }
          this.covSources = [{
            source: new CovSource(originalRawSource, this.wrapperLength),
            path: this.path,
          }];
          this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength);
        }
      } else {
        this.covSources = [{
          source: new CovSource(rawSource, this.wrapperLength),
          path: this.path,
        }];
      }
    }
  
    async sourcesContentFromSources() {
      const fileList = this.sourceMap.sources.map((relativePath) => {
        const realPath = this._resolveSource(this.rawSourceMap, relativePath);
        return readFile(realPath, "utf-8")
          .then((result) => result)
          .catch((err) => {
            debuglog(`failed to load ${realPath}: ${err.message}`);
          });
      });
      return await Promise.all(fileList);
    }
  
    destroy() {
      if (this.sourceMap) {
        this.sourceMap.destroy();
        this.sourceMap = undefined;
      }
    }
  
    _resolveSource(rawSourceMap, sourcePath) {
      if (sourcePath.startsWith("file://")) {
        return fileURLToPath(sourcePath);
      }
      sourcePath = sourcePath.replace(/^webpack:\/\//, "");
      const sourceRoot = rawSourceMap.sourcemap.sourceRoot
        ? rawSourceMap.sourcemap.sourceRoot.replace("file://", "")
        : "";
      const candidatePath = join(sourceRoot, sourcePath);
  
      if (isAbsolute(candidatePath)) {
        return candidatePath;
      } else {
        return resolve(dirname(this.path), candidatePath);
      }
    }
  
    applyCoverage(blocks) {
      blocks.forEach((block) => {
        block.ranges.forEach((range, i) => {
          const { startCol, endCol, path, covSource } = this
            ._maybeRemapStartColEndCol(range);
          if (this.excludePath(path)) {
            return;
          }
          let lines;
          if (block.functionName === "(empty-report)") {
            // (empty-report), this will result in a report that has all lines zeroed out.
            lines = covSource.lines.filter((line) => {
              line.count = 0;
              return true;
            });
            this.all = lines.length > 0;
          } else {
            lines = sliceRange(covSource.lines, startCol, endCol);
          }
          if (!lines.length) {
            return;
          }
  
          const startLineInstance = lines[0];
          const endLineInstance = lines[lines.length - 1];
  
          if (block.isBlockCoverage) {
            this.branches[path] = this.branches[path] || [];
            // record branches.
            this.branches[path].push(
              new CovBranch(
                startLineInstance.line,
                startCol - startLineInstance.startCol,
                endLineInstance.line,
                endCol - endLineInstance.startCol,
                range.count,
              ),
            );
  
            // if block-level granularity is enabled, we still create a single
            // CovFunction tracking object for each set of ranges.
            if (block.functionName && i === 0) {
              this.functions[path] = this.functions[path] || [];
              this.functions[path].push(
                new CovFunction(
                  block.functionName,
                  startLineInstance.line,
                  startCol - startLineInstance.startCol,
                  endLineInstance.line,
                  endCol - endLineInstance.startCol,
                  range.count,
                ),
              );
            }
          } else if (block.functionName) {
            this.functions[path] = this.functions[path] || [];
            // record functions.
            this.functions[path].push(
              new CovFunction(
                block.functionName,
                startLineInstance.line,
                startCol - startLineInstance.startCol,
                endLineInstance.line,
                endCol - endLineInstance.startCol,
                range.count,
              ),
            );
          }
  
          // record the lines (we record these as statements, such that we're
          // compatible with Istanbul 2.0).
          lines.forEach((line) => {
            // make sure branch spans entire line; don't record 'goodbye'
            // branch in `const foo = true ? 'hello' : 'goodbye'` as a
            // 0 for line coverage.
            //
            // All lines start out with coverage of 1, and are later set to 0
            // if they are not invoked; line.ignore prevents a line from being
            // set to 0, and is set if the special comment /* c8 ignore next */
            // is used.
  
            if (
              startCol <= line.startCol && endCol >= line.endCol && !line.ignore
            ) {
              line.count = range.count;
            }
          });
        });
      });
    }
  
    _maybeRemapStartColEndCol(range) {
      let covSource = this.covSources[0].source;
      let startCol = Math.max(0, range.startOffset - covSource.wrapperLength);
      let endCol = Math.min(
        covSource.eof,
        range.endOffset - covSource.wrapperLength,
      );
      let path = this.path;
  
      if (this.sourceMap) {
        startCol = Math.max(
          0,
          range.startOffset - this.sourceTranspiled.wrapperLength,
        );
        endCol = Math.min(
          this.sourceTranspiled.eof,
          range.endOffset - this.sourceTranspiled.wrapperLength,
        );
  
        const { startLine, relStartCol, endLine, relEndCol, source } = this
          .sourceTranspiled.offsetToOriginalRelative(
            this.sourceMap,
            startCol,
            endCol,
          );
  
        const matchingSource = this.covSources.find((covSource) =>
          covSource.path === source
        );
        covSource = matchingSource
          ? matchingSource.source
          : this.covSources[0].source;
        path = matchingSource ? matchingSource.path : this.covSources[0].path;
  
        // next we convert these relative positions back to absolute positions
        // in the original source (which is the format expected in the next step).
        startCol = covSource.relativeToOffset(startLine, relStartCol);
        endCol = covSource.relativeToOffset(endLine, relEndCol);
      }
  
      return {
        path,
        covSource,
        startCol,
        endCol,
      };
    }
  
    getInnerIstanbul(source, path) {
      // We apply the "Resolving Sources" logic (as defined in
      // sourcemaps.info/spec.html) as a final step for 1:many source maps.
      // for 1:1 source maps, the resolve logic is applied while loading.
      //
      // TODO: could we move the resolving logic for 1:1 source maps to the final
      // step as well? currently this breaks some tests in c8.
      let resolvedPath = path;
      if (this.rawSourceMap && this.rawSourceMap.sourcemap.sources.length > 1) {
        resolvedPath = this._resolveSource(this.rawSourceMap, path);
      }
  
      if (this.excludePath(resolvedPath)) {
        return;
      }
  
      return {
        [resolvedPath]: {
          path: resolvedPath,
          all: this.all,
          ...this._statementsToIstanbul(source, path),
          ...this._branchesToIstanbul(source, path),
          ...this._functionsToIstanbul(source, path),
        },
      };
    }
  
    toIstanbul() {
      return this.covSources.reduce(
        (istanbulOuter, { source, path }) =>
          Object.assign(istanbulOuter, this.getInnerIstanbul(source, path)),
        {},
      );
    }
  
    _statementsToIstanbul(source, path) {
      const statements = {
        statementMap: {},
        s: {},
      };
      source.lines.forEach((line, index) => {
        statements.statementMap[`${index}`] = line.toIstanbul();
        statements.s[`${index}`] = line.count;
      });
      return statements;
    }
  
    _branchesToIstanbul(source, path) {
      const branches = {
        branchMap: {},
        b: {},
      };
      this.branches[path] = this.branches[path] || [];
      this.branches[path].forEach((branch, index) => {
        const srcLine = source.lines[branch.startLine - 1];
        const ignore = srcLine === undefined ? true : srcLine.ignore;
        branches.branchMap[`${index}`] = branch.toIstanbul();
        branches.b[`${index}`] = [ignore ? 1 : branch.count];
      });
      return branches;
    }
  
    _functionsToIstanbul(source, path) {
      const functions = {
        fnMap: {},
        f: {},
      };
      this.functions[path] = this.functions[path] || [];
      this.functions[path].forEach((fn, index) => {
        const srcLine = source.lines[fn.startLine - 1];
        const ignore = srcLine === undefined ? true : srcLine.ignore;
        functions.fnMap[`${index}`] = fn.toIstanbul();
        functions.f[`${index}`] = ignore ? 1 : fn.count;
      });
      return functions;
    }
  };
  
  function parsePath(scriptPath) {
    return scriptPath.startsWith("file://")
      ? fileURLToPath(scriptPath)
      : scriptPath;
  }
  