blob: 1b56fd96981ec55637a675ac7557ebd9f3653299 [file] [log] [blame]
/* 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)
}
cb()
}
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