'use strict'
|
|
/* global __coverage__ */
|
|
const cachingTransform = require('caching-transform')
|
const cpFile = require('cp-file')
|
const findCacheDir = require('find-cache-dir')
|
const fs = require('fs')
|
const glob = require('glob')
|
const Hash = require('./lib/hash')
|
const libCoverage = require('istanbul-lib-coverage')
|
const libHook = require('istanbul-lib-hook')
|
const libReport = require('istanbul-lib-report')
|
const mkdirp = require('make-dir')
|
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')
|
const util = require('util')
|
const uuid = require('uuid/v4')
|
|
const debugLog = util.debuglog('nyc')
|
|
const ProcessInfo = require('./lib/process.js')
|
|
/* istanbul ignore next */
|
if (/self-coverage/.test(__dirname)) {
|
require('../self-coverage-helper')
|
}
|
|
function coverageFinder () {
|
var coverage = global.__coverage__
|
if (typeof __coverage__ === 'object') coverage = __coverage__
|
if (!coverage) coverage = global['__coverage__'] = {}
|
return coverage
|
}
|
|
class NYC {
|
constructor (config) {
|
config = 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(config && config._processInfo)
|
this.rootId = this.processInfo.root || uuid()
|
|
this.hashCache = {}
|
}
|
|
_createTransform (ext) {
|
var opts = {
|
salt: Hash.salt(this.config),
|
hashData: (input, metadata) => [metadata.filename],
|
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 () {
|
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,
|
plugins: 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
|
}
|
|
addAllFiles () {
|
this._loadAdditionalModules()
|
|
this.fakeRequire = true
|
this.exclude.globSync(this.cwd).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
|
}
|
})
|
this.fakeRequire = false
|
|
this.writeCoverageFile()
|
}
|
|
instrumentAllFiles (input, output, cb) {
|
let inputDir = '.' + path.sep
|
const visitor = relFile => {
|
const inFile = path.resolve(inputDir, relFile)
|
const inCode = fs.readFileSync(inFile, 'utf-8')
|
const outCode = this._transform(inCode, inFile) || inCode
|
|
if (output) {
|
const mode = fs.statSync(inFile).mode
|
const outFile = path.resolve(output, relFile)
|
mkdirp.sync(path.dirname(outFile))
|
fs.writeFileSync(outFile, outCode)
|
fs.chmodSync(outFile, mode)
|
} else {
|
console.log(outCode)
|
}
|
}
|
|
this._loadAdditionalModules()
|
|
try {
|
const stats = fs.lstatSync(input)
|
if (stats.isDirectory()) {
|
inputDir = input
|
|
const filesToInstrument = this.exclude.globSync(input)
|
|
if (this.config.completeCopy && output) {
|
const globOptions = { dot: true, nodir: true, ignore: ['**/.git', '**/.git/**', path.join(output, '**')] }
|
glob.sync(path.resolve(input, '**'), globOptions)
|
.forEach(src => cpFile.sync(src, path.join(output, path.relative(input, src))))
|
}
|
filesToInstrument.forEach(visitor)
|
} else {
|
visitor(input)
|
}
|
} catch (err) {
|
return cb(err)
|
}
|
cb()
|
}
|
|
_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
|
let sourceMap = null
|
|
if (this._sourceMap) sourceMap = this.sourceMaps.extractAndRegister(code, filename, hash)
|
|
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')
|
}
|
}
|
|
cleanup () {
|
if (!process.env.NYC_CWD) rimraf.sync(this.tempDirectory())
|
}
|
|
clearCache () {
|
if (this.cache) {
|
rimraf.sync(this.cacheDirectory)
|
}
|
}
|
|
createTempDirectory () {
|
mkdirp.sync(this.tempDirectory())
|
if (this.cache) mkdirp.sync(this.cacheDirectory)
|
|
mkdirp.sync(this.processInfoDirectory())
|
}
|
|
reset () {
|
this.cleanup()
|
this.createTempDirectory()
|
}
|
|
_wrapExit () {
|
// we always want to write coverage
|
// regardless of how the process exits.
|
onExit(() => {
|
this.writeCoverageFile()
|
}, { alwaysLast: true })
|
}
|
|
wrap (bin) {
|
process.env.NYC_PROCESS_ID = this.processInfo.uuid
|
this._addRequireHooks()
|
this._wrapExit()
|
this._loadAdditionalModules()
|
return this
|
}
|
|
writeCoverageFile () {
|
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.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)
|
|
fs.writeFileSync(
|
path.resolve(this.processInfoDirectory(), id + '.json'),
|
JSON.stringify(this.processInfo),
|
'utf-8'
|
)
|
}
|
|
getCoverageMapFromAllCoverageFiles (baseDirectory) {
|
const map = libCoverage.createCoverageMap({})
|
|
this.eachReport(undefined, (report) => {
|
map.merge(report)
|
}, baseDirectory)
|
|
map.data = 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
|
}
|
|
report () {
|
var tree
|
var map = this.getCoverageMapFromAllCoverageFiles()
|
var context = libReport.createContext({
|
dir: this.reportDirectory(),
|
watermarks: this.config.watermarks
|
})
|
|
tree = libReport.summarizers.pkg(map)
|
|
this.reporter.forEach((_reporter) => {
|
tree.visit(reports.create(_reporter, {
|
skipEmpty: this.config.skipEmpty,
|
skipFull: this.config.skipFull
|
}), context)
|
})
|
|
if (this._showProcessTree) {
|
this.showProcessTree()
|
}
|
}
|
|
// XXX(@isaacs) Index generation should move to istanbul-lib-processinfo
|
writeProcessIndex () {
|
const dir = this.processInfoDirectory()
|
const pidToUid = new Map()
|
const infoByUid = new Map()
|
const eidToUid = new Map()
|
const infos = fs.readdirSync(dir).filter(f => f !== 'index.json').map(f => {
|
try {
|
const info = JSON.parse(fs.readFileSync(path.resolve(dir, f), 'utf-8'))
|
info.children = []
|
pidToUid.set(info.uuid, info.pid)
|
pidToUid.set(info.pid, info.uuid)
|
infoByUid.set(info.uuid, info)
|
if (info.externalId) {
|
eidToUid.set(info.externalId, info.uuid)
|
}
|
return info
|
} catch (er) {
|
return null
|
}
|
}).filter(Boolean)
|
|
// create all the parent-child links and write back the updated info
|
infos.forEach(info => {
|
if (info.parent) {
|
const parentInfo = infoByUid.get(info.parent)
|
if (parentInfo && !parentInfo.children.includes(info.uuid)) {
|
parentInfo.children.push(info.uuid)
|
}
|
}
|
})
|
|
// figure out which files were touched by each process.
|
const files = infos.reduce((files, info) => {
|
info.files.forEach(f => {
|
files[f] = files[f] || []
|
files[f].push(info.uuid)
|
})
|
return files
|
}, {})
|
|
// build the actual index!
|
const index = infos.reduce((index, info) => {
|
index.processes[info.uuid] = {}
|
index.processes[info.uuid].parent = info.parent
|
if (info.externalId) {
|
if (index.externalIds[info.externalId]) {
|
throw new Error(`External ID ${info.externalId} used by multiple processes`)
|
}
|
index.processes[info.uuid].externalId = info.externalId
|
index.externalIds[info.externalId] = {
|
root: info.uuid,
|
children: info.children
|
}
|
}
|
index.processes[info.uuid].children = Array.from(info.children)
|
return index
|
}, { processes: {}, files: files, externalIds: {} })
|
|
// flatten the descendant sets of all the externalId procs
|
Object.keys(index.externalIds).forEach(eid => {
|
const { children } = index.externalIds[eid]
|
// push the next generation onto the list so we accumulate them all
|
for (let i = 0; i < children.length; i++) {
|
const nextGen = index.processes[children[i]].children
|
if (nextGen && nextGen.length) {
|
children.push(...nextGen.filter(uuid => children.indexOf(uuid) === -1))
|
}
|
}
|
})
|
|
fs.writeFileSync(path.resolve(dir, 'index.json'), JSON.stringify(index))
|
}
|
|
showProcessTree () {
|
var processTree = ProcessInfo.buildProcessTree(this._loadProcessInfos())
|
|
console.log(processTree.render(this))
|
}
|
|
checkCoverage (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)
|
}
|
}
|
|
_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] + '%)')
|
}
|
}
|
})
|
}
|
|
_loadProcessInfos () {
|
return fs.readdirSync(this.processInfoDirectory()).map(f => {
|
let data
|
try {
|
data = JSON.parse(fs.readFileSync(
|
path.resolve(this.processInfoDirectory(), f),
|
'utf-8'
|
))
|
} catch (e) { // handle corrupt JSON output.
|
return null
|
}
|
if (f !== 'index.json') {
|
data.nodes = []
|
data = new ProcessInfo(data)
|
}
|
return { file: path.basename(f, '.json'), data: data }
|
}).filter(Boolean).reduce((infos, info) => {
|
infos[info.file] = info.data
|
return infos
|
}, {})
|
}
|
|
eachReport (filenames, iterator, baseDirectory) {
|
baseDirectory = baseDirectory || this.tempDirectory()
|
|
if (typeof filenames === 'function') {
|
iterator = filenames
|
filenames = undefined
|
}
|
|
var _this = this
|
var files = filenames || fs.readdirSync(baseDirectory)
|
|
files.forEach(function (f) {
|
var report
|
try {
|
report = JSON.parse(fs.readFileSync(
|
path.resolve(baseDirectory, f),
|
'utf-8'
|
))
|
|
_this.sourceMaps.reloadCachedSourceMaps(report)
|
} catch (e) { // handle corrupt JSON output.
|
report = {}
|
}
|
|
iterator(report)
|
})
|
}
|
|
loadReports (filenames) {
|
var reports = []
|
|
this.eachReport(filenames, (report) => {
|
reports.push(report)
|
})
|
|
return reports
|
}
|
|
tempDirectory () {
|
return path.resolve(this.cwd, this._tempDirectory)
|
}
|
|
reportDirectory () {
|
return path.resolve(this.cwd, this._reportDir)
|
}
|
|
processInfoDirectory () {
|
return path.resolve(this.tempDirectory(), 'processinfo')
|
}
|
}
|
|
module.exports = NYC
|