/* global __coverage__ */ const arrify = require('arrify') const cachingTransform = require('caching-transform') const debugLog = require('debug-log')('nyc') const findCacheDir = require('find-cache-dir') const fs = require('fs') const glob = require('glob') const Hash = require('./lib/hash') const js = require('default-require-extensions/js') const libCoverage = require('istanbul-lib-coverage') const libHook = require('istanbul-lib-hook') const libReport = require('istanbul-lib-report') const md5hex = require('md5-hex') const mkdirp = require('mkdirp') const Module = require('module') const onExit = require('signal-exit') const path = require('path') const reports = require('istanbul-reports') const resolveFrom = require('resolve-from') const rimraf = require('rimraf') const SourceMaps = require('./lib/source-maps') const testExclude = require('test-exclude') var ProcessInfo try { ProcessInfo = require('./lib/process.covered.js') } catch (e) { /* istanbul ignore next */ ProcessInfo = require('./lib/process.js') } /* istanbul ignore next */ if (/index\.covered\.js$/.test(__filename)) { require('./lib/self-coverage-helper') } function NYC (config) { config = config || {} this.config = config this.subprocessBin = config.subprocessBin || path.resolve(__dirname, './bin/nyc.js') this._tempDirectory = config.tempDirectory || './.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 = arrify(config.reporter || 'text') this.cacheDirectory = config.cacheDir || findCacheDir({name: 'nyc', cwd: this.cwd}) this.cache = Boolean(this.cacheDirectory && config.cache) this.exclude = testExclude({ cwd: this.cwd, include: config.include, exclude: config.exclude }) this.sourceMaps = new SourceMaps({ cache: this.cache, cacheDirectory: this.cacheDirectory }) // require extensions can be provided as config in package.json. this.require = arrify(config.require) this.extensions = arrify(config.extension).concat('.js').map(function (ext) { return ext.toLowerCase() }).filter(function (item, pos, arr) { // avoid duplicate extensions return arr.indexOf(item) === pos }) this.transforms = this.extensions.reduce(function (transforms, ext) { transforms[ext] = this._createTransform(ext) return transforms }.bind(this), {}) this.hookRunInContext = config.hookRunInContext this.hookRunInThisContext = config.hookRunInThisContext this.fakeRequire = null this.processInfo = new ProcessInfo(config && config._processInfo) this.rootId = this.processInfo.root || this.generateUniqueID() this.hashCache = {} } NYC.prototype._createTransform = function (ext) { var _this = this var opts = { salt: Hash.salt, hash: function (code, metadata, salt) { var hash = Hash(code, metadata.filename) _this.hashCache[metadata.filename] = hash return 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) } NYC.prototype._disableCachingTransform = function () { return !(this.cache && this.config.isChildProcess) } NYC.prototype._loadAdditionalModules = function () { var _this = this this.require.forEach(function (r) { // first attempt to require the module relative to // the directory being instrumented. var p = resolveFrom(_this.cwd, r) if (p) { require(p) return } // now try other locations, .e.g, the nyc node_modules folder. require(r) }) } NYC.prototype.instrumenter = function () { return this._instrumenter || (this._instrumenter = this._createInstrumenter()) } NYC.prototype._createInstrumenter = function () { return this._instrumenterLib(this.cwd, { produceSourceMap: this.config.produceSourceMap }) } NYC.prototype.addFile = function (filename) { var relFile = path.relative(this.cwd, filename) var source = this._readTranspiledSource(path.resolve(this.cwd, filename)) var instrumentedSource = this._maybeInstrumentSource(source, filename, relFile) return { instrument: !!instrumentedSource, relFile: relFile, content: instrumentedSource || source } } NYC.prototype._readTranspiledSource = function (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 } NYC.prototype.addAllFiles = function () { var _this = this this._loadAdditionalModules() this.fakeRequire = true this.walkAllFiles(this.cwd, function (filename) { filename = path.resolve(_this.cwd, filename) _this.addFile(filename) var coverage = coverageFinder() var lastCoverage = _this.instrumenter().lastFileCoverage() if (lastCoverage) { filename = lastCoverage.path } if (lastCoverage && _this.exclude.shouldInstrument(filename)) { coverage[filename] = lastCoverage } }) this.fakeRequire = false this.writeCoverageFile() } NYC.prototype.instrumentAllFiles = function (input, output, cb) { var _this = this var inputDir = '.' + path.sep var visitor = function (filename) { var ext var transform var inFile = path.resolve(inputDir, filename) var code = fs.readFileSync(inFile, 'utf-8') for (ext in _this.transforms) { if (filename.toLowerCase().substr(-ext.length) === ext) { transform = _this.transforms[ext] break } } if (transform) { code = transform(code, {filename: filename, relFile: inFile}) } if (!output) { console.log(code) } else { var outFile = path.resolve(output, filename) mkdirp.sync(path.dirname(outFile)) fs.writeFileSync(outFile, code, 'utf-8') } } this._loadAdditionalModules() try { var stats = fs.lstatSync(input) if (stats.isDirectory()) { inputDir = input this.walkAllFiles(input, visitor) } else { visitor(input) } } catch (err) { return cb(err) } } NYC.prototype.walkAllFiles = function (dir, visitor) { var pattern = null if (this.extensions.length === 1) { pattern = '**/*' + this.extensions[0] } else { pattern = '**/*{' + this.extensions.join() + '}' } glob.sync(pattern, {cwd: dir, nodir: true, ignore: this.exclude.exclude}).forEach(function (filename) { visitor(filename) }) } NYC.prototype._maybeInstrumentSource = function (code, filename, relFile) { var instrument = this.exclude.shouldInstrument(filename, relFile) if (!instrument) { return null } var ext, transform for (ext in this.transforms) { if (filename.toLowerCase().substr(-ext.length) === ext) { transform = this.transforms[ext] break } } return transform ? transform(code, {filename: filename, relFile: relFile}) : null } NYC.prototype._transformFactory = function (cacheDir) { var _this = this var instrumenter = this.instrumenter() var instrumented return function (code, metadata, hash) { var filename = metadata.filename var sourceMap = null if (_this._sourceMap) sourceMap = _this.sourceMaps.extractAndRegister(code, filename, hash) try { instrumented = instrumenter.instrumentSync(code, filename, sourceMap) } catch (e) { // don't fail external tests due to instrumentation bugs. debugLog('failed to instrument ' + filename + 'with error: ' + e.stack) instrumented = code } if (_this.fakeRequire) { return 'function x () {}' } else { return instrumented } } } NYC.prototype._handleJs = function (code, filename) { var relFile = path.relative(this.cwd, filename) // ensure the path has correct casing (see istanbuljs/nyc#269 and nodejs/node#6624) filename = path.resolve(this.cwd, relFile) return this._maybeInstrumentSource(code, filename, relFile) || code } NYC.prototype._addHook = function (type) { var handleJs = this._handleJs.bind(this) var dummyMatcher = function () { return true } // we do all processing in transformer libHook['hook' + type](dummyMatcher, handleJs, { extensions: this.extensions }) } NYC.prototype._wrapRequire = function () { this.extensions.forEach(function (ext) { require.extensions[ext] = js }) this._addHook('Require') } NYC.prototype._addOtherHooks = function () { if (this.hookRunInContext) { this._addHook('RunInContext') } if (this.hookRunInThisContext) { this._addHook('RunInThisContext') } } NYC.prototype.cleanup = function () { if (!process.env.NYC_CWD) rimraf.sync(this.tempDirectory()) } NYC.prototype.clearCache = function () { if (this.cache) { rimraf.sync(this.cacheDirectory) } } NYC.prototype.createTempDirectory = function () { mkdirp.sync(this.tempDirectory()) if (this.cache) mkdirp.sync(this.cacheDirectory) if (this._showProcessTree) { mkdirp.sync(this.processInfoDirectory()) } } NYC.prototype.reset = function () { this.cleanup() this.createTempDirectory() } NYC.prototype._wrapExit = function () { var _this = this // we always want to write coverage // regardless of how the process exits. onExit(function () { _this.writeCoverageFile() }, {alwaysLast: true}) } NYC.prototype.wrap = function (bin) { this._wrapRequire() this._addOtherHooks() this._wrapExit() this._loadAdditionalModules() return this } NYC.prototype.generateUniqueID = function () { return md5hex( process.hrtime().concat(process.pid).map(String) ) } NYC.prototype.writeCoverageFile = function () { var coverage = coverageFinder() if (!coverage) return // 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) } else { coverage = this.sourceMaps.remapCoverage(coverage) } var id = this.generateUniqueID() var coverageFilename = path.resolve(this.tempDirectory(), id + '.json') fs.writeFileSync( coverageFilename, JSON.stringify(coverage), 'utf-8' ) if (!this._showProcessTree) { return } this.processInfo.coverageFilename = coverageFilename fs.writeFileSync( path.resolve(this.processInfoDirectory(), id + '.json'), JSON.stringify(this.processInfo), 'utf-8' ) } function coverageFinder () { var coverage = global.__coverage__ if (typeof __coverage__ === 'object') coverage = __coverage__ if (!coverage) coverage = global['__coverage__'] = {} return coverage } NYC.prototype._getCoverageMapFromAllCoverageFiles = function () { var _this = this var map = libCoverage.createCoverageMap({}) this.loadReports().forEach(function (report) { map.merge(report) }) // 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(function (filename) { return _this.exclude.shouldInstrument(filename) }) } map.data = this.sourceMaps.remapCoverage(map.data) return map } NYC.prototype.report = function () { var tree var map = this._getCoverageMapFromAllCoverageFiles() var context = libReport.createContext({ dir: this._reportDir, watermarks: this.config.watermarks }) tree = libReport.summarizers.pkg(map) this.reporter.forEach(function (_reporter) { tree.visit(reports.create(_reporter), context) }) if (this._showProcessTree) { this.showProcessTree() } } NYC.prototype.showProcessTree = function () { var processTree = ProcessInfo.buildProcessTree(this._loadProcessInfos()) console.log(processTree.render(this)) } NYC.prototype.checkCoverage = function (thresholds, perFile) { var map = this._getCoverageMapFromAllCoverageFiles() var nyc = this if (perFile) { map.files().forEach(function (file) { // ERROR: Coverage for lines (90.12%) does not meet threshold (120%) for index.js nyc._checkCoverage(map.fileCoverageFor(file).toSummary(), thresholds, file) }) } else { // ERROR: Coverage for lines (90.12%) does not meet global threshold (120%) nyc._checkCoverage(map.getCoverageSummary(), thresholds) } // process.exitCode was not implemented until v0.11.8. if (/^v0\.(1[0-1]\.|[0-9]\.)/.test(process.version) && process.exitCode !== 0) process.exit(process.exitCode) } NYC.prototype._checkCoverage = function (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] + '%)') } } }) } NYC.prototype._loadProcessInfos = function () { var _this = this var files = fs.readdirSync(this.processInfoDirectory()) return files.map(function (f) { try { return new ProcessInfo(JSON.parse(fs.readFileSync( path.resolve(_this.processInfoDirectory(), f), 'utf-8' ))) } catch (e) { // handle corrupt JSON output. return {} } }) } NYC.prototype.loadReports = function (filenames) { var _this = this var files = filenames || fs.readdirSync(this.tempDirectory()) return files.map(function (f) { var report try { report = JSON.parse(fs.readFileSync( path.resolve(_this.tempDirectory(), f), 'utf-8' )) } catch (e) { // handle corrupt JSON output. return {} } _this.sourceMaps.reloadCachedSourceMaps(report) return report }) } NYC.prototype.tempDirectory = function () { return path.resolve(this.cwd, this._tempDirectory) } NYC.prototype.processInfoDirectory = function () { return path.resolve(this.tempDirectory(), 'processinfo') } module.exports = NYC