542 lines
15 KiB
JavaScript
Executable File
542 lines
15 KiB
JavaScript
Executable File
/* 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
|