text: 'use strict'
  
  /* global __coverage__ */
  
  const cachingTransform = require('caching-transform')
  const findCacheDir = require('find-cache-dir')
  const fs = require('./lib/fs-promises')
  const os = require('os')
  const { debuglog, promisify } = require('util')
  const glob = promisify(require('glob'))
  const Hash = require('./lib/hash')
  const libCoverage = require('istanbul-lib-coverage')
  const libHook = require('istanbul-lib-hook')
  const { ProcessInfo, ProcessDB } = require('istanbul-lib-processinfo')
  const mkdirp = require('make-dir')
  const Module = require('module')
  const onExit = require('signal-exit')
  const path = require('path')
  const rimraf = promisify(require('rimraf'))
  const SourceMaps = require('./lib/source-maps')
  const testExclude = require('test-exclude')
  const pMap = require('p-map')
  
  const debugLog = debuglog('nyc')
  
  let selfCoverageHelper
  
  /* istanbul ignore next */
  if (/self-coverage/.test(__dirname)) {
    selfCoverageHelper = require('../self-coverage-helper')
  } else {
    // Avoid additional conditional code
    selfCoverageHelper = {
      onExit () {}
    }
  }
  
  function coverageFinder () {
    var coverage = global.__coverage__
    if (typeof __coverage__ === 'object') coverage = __coverage__
    if (!coverage) coverage = global['__coverage__'] = {}
    return coverage
  }
  
  class NYC {
    constructor (config) {
      this.config = { ...config }
  
      this.subprocessBin = config.subprocessBin || path.resolve(__dirname, './bin/nyc.js')
      this._tempDirectory = config.tempDirectory || config.tempDir || './.nyc_output'
      this._instrumenterLib = require(config.instrumenter || './lib/instrumenters/istanbul')
      this._reportDir = config.reportDir || 'coverage'
      this._sourceMap = typeof config.sourceMap === 'boolean' ? config.sourceMap : true
      this._showProcessTree = config.showProcessTree || false
      this._eagerInstantiation = config.eager || false
      this.cwd = config.cwd || process.cwd()
      this.reporter = [].concat(config.reporter || 'text')
  
      this.cacheDirectory = (config.cacheDir && path.resolve(config.cacheDir)) || findCacheDir({ name: 'nyc', cwd: this.cwd })
      this.cache = Boolean(this.cacheDirectory && config.cache)
  
      this.extensions = [].concat(config.extension || [])
        .concat('.js')
        .map(ext => ext.toLowerCase())
        .filter((item, pos, arr) => arr.indexOf(item) === pos)
  
      this.exclude = testExclude({
        cwd: this.cwd,
        include: config.include,
        exclude: config.exclude,
        excludeNodeModules: config.excludeNodeModules !== false,
        extension: this.extensions
      })
  
      this.sourceMaps = new SourceMaps({
        cache: this.cache,
        cacheDirectory: this.cacheDirectory
      })
  
      // require extensions can be provided as config in package.json.
      this.require = [].concat(config.require || [])
  
      this.transforms = this.extensions.reduce((transforms, ext) => {
        transforms[ext] = this._createTransform(ext)
        return transforms
      }, {})
  
      this.hookRequire = config.hookRequire
      this.hookRunInContext = config.hookRunInContext
      this.hookRunInThisContext = config.hookRunInThisContext
      this.fakeRequire = null
  
      this.processInfo = new ProcessInfo(Object.assign({}, config._processInfo, {
        directory: path.resolve(this.tempDirectory(), 'processinfo')
      }))
  
      this.hashCache = {}
    }
  
    _createTransform (ext) {
      const opts = {
        salt: Hash.salt(this.config),
        hashData: (input, metadata) => [metadata.filename],
        filenamePrefix: metadata => path.parse(metadata.filename).name + '-',
        onHash: (input, metadata, hash) => {
          this.hashCache[metadata.filename] = hash
        },
        cacheDir: this.cacheDirectory,
        // when running --all we should not load source-file from
        // cache, we want to instead return the fake source.
        disableCache: this._disableCachingTransform(),
        ext: ext
      }
      if (this._eagerInstantiation) {
        opts.transform = this._transformFactory(this.cacheDirectory)
      } else {
        opts.factory = this._transformFactory.bind(this)
      }
      return cachingTransform(opts)
    }
  
    _disableCachingTransform () {
      return !(this.cache && this.config.isChildProcess)
    }
  
    _loadAdditionalModules () {
      if (!this.config.useSpawnWrap || this.require.length === 0) {
        return
      }
  
      const resolveFrom = require('resolve-from')
      this.require.forEach(requireModule => {
        // Attempt to require the module relative to the directory being instrumented.
        // Then try other locations, e.g. the nyc node_modules folder.
        require(resolveFrom.silent(this.cwd, requireModule) || requireModule)
      })
    }
  
    instrumenter () {
      return this._instrumenter || (this._instrumenter = this._createInstrumenter())
    }
  
    _createInstrumenter () {
      return this._instrumenterLib({
        ignoreClassMethods: [].concat(this.config.ignoreClassMethod).filter(a => a),
        produceSourceMap: this.config.produceSourceMap,
        compact: this.config.compact,
        preserveComments: this.config.preserveComments,
        esModules: this.config.esModules,
        parserPlugins: this.config.parserPlugins
      })
    }
  
    addFile (filename) {
      const source = this._readTranspiledSource(filename)
      this._maybeInstrumentSource(source, filename)
    }
  
    _readTranspiledSource (filePath) {
      var source = null
      var ext = path.extname(filePath)
      if (typeof Module._extensions[ext] === 'undefined') {
        ext = '.js'
      }
      Module._extensions[ext]({
        _compile: function (content, filename) {
          source = content
        }
      }, filePath)
      return source
    }
  
    async addAllFiles () {
      this._loadAdditionalModules()
  
      this.fakeRequire = true
      const files = await this.exclude.glob(this.cwd)
      files.forEach(relFile => {
        const filename = path.resolve(this.cwd, relFile)
        this.addFile(filename)
        const coverage = coverageFinder()
        const lastCoverage = this.instrumenter().lastFileCoverage()
        if (lastCoverage) {
          coverage[lastCoverage.path] = {
            ...lastCoverage,
            // Only use this data if we don't have it without `all: true`
            all: true
          }
        }
      })
      this.fakeRequire = false
  
      this.writeCoverageFile()
    }
  
    async instrumentAllFiles (input, output) {
      let inputDir = '.' + path.sep
      const visitor = async relFile => {
        const inFile = path.resolve(inputDir, relFile)
        const inCode = await fs.readFile(inFile, 'utf-8')
        const outCode = this._transform(inCode, inFile) || inCode
  
        if (output) {
          const { mode } = await fs.stat(inFile)
          const outFile = path.resolve(output, relFile)
  
          await mkdirp(path.dirname(outFile))
          await fs.writeFile(outFile, outCode)
          await fs.chmod(outFile, mode)
        } else {
          console.log(outCode)
        }
      }
  
      this._loadAdditionalModules()
  
      const stats = await fs.lstat(input)
      if (stats.isDirectory()) {
        inputDir = input
  
        const filesToInstrument = await this.exclude.glob(input)
  
        const concurrency = output ? os.cpus().length : 1
        if (this.config.completeCopy && output) {
          const files = await glob(path.resolve(input, '**'), {
            dot: true,
            nodir: true,
            ignore: ['**/.git', '**/.git/**', path.join(output, '**')]
          })
          const destDirs = new Set(
            files.map(src => path.dirname(path.join(output, path.relative(input, src))))
          )
  
          await pMap(
            destDirs,
            dir => mkdirp(dir),
            { concurrency }
          )
          await pMap(
            files,
            src => fs.copyFile(src, path.join(output, path.relative(input, src))),
            { concurrency }
          )
        }
  
        await pMap(filesToInstrument, visitor, { concurrency })
      } else {
        await visitor(input)
      }
    }
  
    _transform (code, filename) {
      const extname = path.extname(filename).toLowerCase()
      const transform = this.transforms[extname] || (() => null)
  
      return transform(code, { filename })
    }
  
    _maybeInstrumentSource (code, filename) {
      if (!this.exclude.shouldInstrument(filename)) {
        return null
      }
  
      return this._transform(code, filename)
    }
  
    maybePurgeSourceMapCache () {
      if (!this.cache) {
        this.sourceMaps.purgeCache()
      }
    }
  
    _transformFactory (cacheDir) {
      const instrumenter = this.instrumenter()
      let instrumented
  
      return (code, metadata, hash) => {
        const filename = metadata.filename
        const sourceMap = {}
  
        if (this._sourceMap) {
          sourceMap.sourceMap = this.sourceMaps.extract(code, filename)
          sourceMap.registerMap = () => this.sourceMaps.registerMap(filename, hash, sourceMap.sourceMap)
        } else {
          sourceMap.registerMap = () => {}
        }
  
        try {
          instrumented = instrumenter.instrumentSync(code, filename, sourceMap)
        } catch (e) {
          debugLog('failed to instrument ' + filename + ' with error: ' + e.stack)
          if (this.config.exitOnError) {
            console.error('Failed to instrument ' + filename)
            process.exit(1)
          } else {
            instrumented = code
          }
        }
  
        if (this.fakeRequire) {
          return 'function x () {}'
        } else {
          return instrumented
        }
      }
    }
  
    _handleJs (code, options) {
      // ensure the path has correct casing (see istanbuljs/nyc#269 and nodejs/node#6624)
      const filename = path.resolve(this.cwd, options.filename)
      return this._maybeInstrumentSource(code, filename) || code
    }
  
    _addHook (type) {
      const handleJs = this._handleJs.bind(this)
      const dummyMatcher = () => true // we do all processing in transformer
      libHook['hook' + type](dummyMatcher, handleJs, { extensions: this.extensions })
    }
  
    _addRequireHooks () {
      if (this.hookRequire) {
        this._addHook('Require')
      }
      if (this.hookRunInContext) {
        this._addHook('RunInContext')
      }
      if (this.hookRunInThisContext) {
        this._addHook('RunInThisContext')
      }
    }
  
    async createTempDirectory () {
      await mkdirp(this.tempDirectory())
      if (this.cache) {
        await mkdirp(this.cacheDirectory)
      }
  
      await mkdirp(this.processInfo.directory)
    }
  
    async reset () {
      if (!process.env.NYC_CWD) {
        await rimraf(this.tempDirectory())
      }
  
      await this.createTempDirectory()
    }
  
    _wrapExit () {
      selfCoverageHelper.registered = true
  
      // we always want to write coverage
      // regardless of how the process exits.
      onExit(
        () => {
          this.writeCoverageFile()
          selfCoverageHelper.onExit()
        },
        { alwaysLast: true }
      )
    }
  
    wrap (bin) {
      process.env.NYC_PROCESS_ID = this.processInfo.uuid
      // This is a bug with the spawn-wrap method where
      // we cannot force propagation of NYC_PROCESS_ID.
      if (!this.config.useSpawnWrap) {
        const { propagateEnv } = require('node-preload')
        propagateEnv.NYC_PROCESS_ID = this.processInfo.uuid
      }
      this._addRequireHooks()
      this._wrapExit()
      this._loadAdditionalModules()
      return this
    }
  
    writeCoverageFile () {
      var coverage = coverageFinder()
  
      // Remove any files that should be excluded but snuck into the coverage
      Object.keys(coverage).forEach(function (absFile) {
        if (!this.exclude.shouldInstrument(absFile)) {
          delete coverage[absFile]
        }
      }, this)
  
      if (this.cache) {
        Object.keys(coverage).forEach(function (absFile) {
          if (this.hashCache[absFile] && coverage[absFile]) {
            coverage[absFile].contentHash = this.hashCache[absFile]
          }
        }, this)
      }
  
      var id = this.processInfo.uuid
      var coverageFilename = path.resolve(this.tempDirectory(), id + '.json')
  
      fs.writeFileSync(
        coverageFilename,
        JSON.stringify(coverage),
        'utf-8'
      )
  
      this.processInfo.coverageFilename = coverageFilename
      this.processInfo.files = Object.keys(coverage)
      this.processInfo.saveSync()
    }
  
    async getCoverageMapFromAllCoverageFiles (baseDirectory) {
      const map = libCoverage.createCoverageMap({})
  
      const data = await this.coverageData(baseDirectory)
      data.forEach(report => {
        map.merge(report)
      })
  
      map.data = await this.sourceMaps.remapCoverage(map.data)
  
      // depending on whether source-code is pre-instrumented
      // or instrumented using a JIT plugin like @babel/require
      // you may opt to exclude files after applying
      // source-map remapping logic.
      if (this.config.excludeAfterRemap) {
        map.filter(filename => this.exclude.shouldInstrument(filename))
      }
  
      return map
    }
  
    async report () {
      const libReport = require('istanbul-lib-report')
      const reports = require('istanbul-reports')
  
      const context = libReport.createContext({
        dir: this.reportDirectory(),
        watermarks: this.config.watermarks,
        coverageMap: await this.getCoverageMapFromAllCoverageFiles()
      })
  
      this.reporter.forEach((_reporter) => {
        reports.create(_reporter, {
          skipEmpty: this.config.skipEmpty,
          skipFull: this.config.skipFull,
          maxCols: process.stdout.columns || 100
        }).execute(context)
      })
  
      if (this._showProcessTree) {
        await this.showProcessTree()
      }
    }
  
    async writeProcessIndex () {
      const db = new ProcessDB(this.processInfo.directory)
      await db.writeIndex()
    }
  
    async showProcessTree () {
      const db = new ProcessDB(this.processInfo.directory)
      console.log(await db.renderTree(this))
    }
  
    async checkCoverage (thresholds, perFile) {
      const map = await this.getCoverageMapFromAllCoverageFiles()
  
      if (perFile) {
        map.files().forEach(file => {
          // ERROR: Coverage for lines (90.12%) does not meet threshold (120%) for index.js
          this._checkCoverage(map.fileCoverageFor(file).toSummary(), thresholds, file)
        })
      } else {
        // ERROR: Coverage for lines (90.12%) does not meet global threshold (120%)
        this._checkCoverage(map.getCoverageSummary(), thresholds)
      }
    }
  
    _checkCoverage (summary, thresholds, file) {
      Object.keys(thresholds).forEach(function (key) {
        var coverage = summary[key].pct
        if (coverage < thresholds[key]) {
          process.exitCode = 1
          if (file) {
            console.error('ERROR: Coverage for ' + key + ' (' + coverage + '%) does not meet threshold (' + thresholds[key] + '%) for ' + file)
          } else {
            console.error('ERROR: Coverage for ' + key + ' (' + coverage + '%) does not meet global threshold (' + thresholds[key] + '%)')
          }
        }
      })
    }
  
    coverageFiles (baseDirectory = this.tempDirectory()) {
      return fs.readdir(baseDirectory)
    }
  
    async coverageFileLoad (filename, baseDirectory = this.tempDirectory()) {
      try {
        const report = JSON.parse(await fs.readFile(path.resolve(baseDirectory, filename)), 'utf8')
        await this.sourceMaps.reloadCachedSourceMaps(report)
        return report
      } catch (error) {
        return {}
      }
    }
  
    async coverageData (baseDirectory) {
      const files = await this.coverageFiles(baseDirectory)
      return pMap(
        files,
        f => this.coverageFileLoad(f, baseDirectory),
        { concurrency: os.cpus().length }
      )
    }
  
    tempDirectory () {
      return path.resolve(this.cwd, this._tempDirectory)
    }
  
    reportDirectory () {
      return path.resolve(this.cwd, this._reportDir)
    }
  }
  
  module.exports = NYC
  