'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