index.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. 'use strict'
  2. /* global __coverage__ */
  3. const arrify = require('arrify')
  4. const cachingTransform = require('caching-transform')
  5. const util = require('util')
  6. const findCacheDir = require('find-cache-dir')
  7. const fs = require('fs')
  8. const glob = require('glob')
  9. const Hash = require('./lib/hash')
  10. const libCoverage = require('istanbul-lib-coverage')
  11. const libHook = require('istanbul-lib-hook')
  12. const libReport = require('istanbul-lib-report')
  13. const mkdirp = require('make-dir')
  14. const Module = require('module')
  15. const onExit = require('signal-exit')
  16. const path = require('path')
  17. const reports = require('istanbul-reports')
  18. const resolveFrom = require('resolve-from')
  19. const rimraf = require('rimraf')
  20. const SourceMaps = require('./lib/source-maps')
  21. const testExclude = require('test-exclude')
  22. const uuid = require('uuid/v4')
  23. const debugLog = util.debuglog('nyc')
  24. var ProcessInfo
  25. try {
  26. ProcessInfo = require('./lib/process.covered.js')
  27. } catch (e) {
  28. /* istanbul ignore next */
  29. ProcessInfo = require('./lib/process.js')
  30. }
  31. /* istanbul ignore next */
  32. if (/index\.covered\.js$/.test(__filename)) {
  33. require('./lib/self-coverage-helper')
  34. }
  35. function NYC (config) {
  36. config = config || {}
  37. this.config = config
  38. this.subprocessBin = config.subprocessBin || path.resolve(__dirname, './bin/nyc.js')
  39. this._tempDirectory = config.tempDirectory || config.tempDir || './.nyc_output'
  40. this._instrumenterLib = require(config.instrumenter || './lib/instrumenters/istanbul')
  41. this._reportDir = config.reportDir || 'coverage'
  42. this._sourceMap = typeof config.sourceMap === 'boolean' ? config.sourceMap : true
  43. this._showProcessTree = config.showProcessTree || false
  44. this._eagerInstantiation = config.eager || false
  45. this.cwd = config.cwd || process.cwd()
  46. this.reporter = arrify(config.reporter || 'text')
  47. this.cacheDirectory = (config.cacheDir && path.resolve(config.cacheDir)) || findCacheDir({ name: 'nyc', cwd: this.cwd })
  48. this.cache = Boolean(this.cacheDirectory && config.cache)
  49. this.exclude = testExclude({
  50. cwd: this.cwd,
  51. include: config.include,
  52. exclude: config.exclude
  53. })
  54. this.sourceMaps = new SourceMaps({
  55. cache: this.cache,
  56. cacheDirectory: this.cacheDirectory
  57. })
  58. // require extensions can be provided as config in package.json.
  59. this.require = arrify(config.require)
  60. this.extensions = arrify(config.extension).concat('.js').map(function (ext) {
  61. return ext.toLowerCase()
  62. }).filter(function (item, pos, arr) {
  63. // avoid duplicate extensions
  64. return arr.indexOf(item) === pos
  65. })
  66. this.transforms = this.extensions.reduce(function (transforms, ext) {
  67. transforms[ext] = this._createTransform(ext)
  68. return transforms
  69. }.bind(this), {})
  70. this.hookRequire = config.hookRequire
  71. this.hookRunInContext = config.hookRunInContext
  72. this.hookRunInThisContext = config.hookRunInThisContext
  73. this.fakeRequire = null
  74. this.processInfo = new ProcessInfo(config && config._processInfo)
  75. this.rootId = this.processInfo.root || this.generateUniqueID()
  76. this.hashCache = {}
  77. }
  78. NYC.prototype._createTransform = function (ext) {
  79. var opts = {
  80. salt: Hash.salt,
  81. hashData: (input, metadata) => [metadata.filename],
  82. onHash: (input, metadata, hash) => {
  83. this.hashCache[metadata.filename] = hash
  84. },
  85. cacheDir: this.cacheDirectory,
  86. // when running --all we should not load source-file from
  87. // cache, we want to instead return the fake source.
  88. disableCache: this._disableCachingTransform(),
  89. ext: ext
  90. }
  91. if (this._eagerInstantiation) {
  92. opts.transform = this._transformFactory(this.cacheDirectory)
  93. } else {
  94. opts.factory = this._transformFactory.bind(this)
  95. }
  96. return cachingTransform(opts)
  97. }
  98. NYC.prototype._disableCachingTransform = function () {
  99. return !(this.cache && this.config.isChildProcess)
  100. }
  101. NYC.prototype._loadAdditionalModules = function () {
  102. var _this = this
  103. this.require.forEach(function (r) {
  104. // first attempt to require the module relative to
  105. // the directory being instrumented.
  106. var p = resolveFrom.silent(_this.cwd, r)
  107. if (p) {
  108. require(p)
  109. return
  110. }
  111. // now try other locations, .e.g, the nyc node_modules folder.
  112. require(r)
  113. })
  114. }
  115. NYC.prototype.instrumenter = function () {
  116. return this._instrumenter || (this._instrumenter = this._createInstrumenter())
  117. }
  118. NYC.prototype._createInstrumenter = function () {
  119. return this._instrumenterLib(this.cwd, {
  120. ignoreClassMethods: [].concat(this.config.ignoreClassMethod).filter(a => a),
  121. produceSourceMap: this.config.produceSourceMap,
  122. compact: this.config.compact,
  123. preserveComments: this.config.preserveComments,
  124. esModules: this.config.esModules,
  125. plugins: this.config.plugins
  126. })
  127. }
  128. NYC.prototype.addFile = function (filename) {
  129. var relFile = path.relative(this.cwd, filename)
  130. var source = this._readTranspiledSource(path.resolve(this.cwd, filename))
  131. var instrumentedSource = this._maybeInstrumentSource(source, filename, relFile)
  132. return {
  133. instrument: !!instrumentedSource,
  134. relFile: relFile,
  135. content: instrumentedSource || source
  136. }
  137. }
  138. NYC.prototype._readTranspiledSource = function (filePath) {
  139. var source = null
  140. var ext = path.extname(filePath)
  141. if (typeof Module._extensions[ext] === 'undefined') {
  142. ext = '.js'
  143. }
  144. Module._extensions[ext]({
  145. _compile: function (content, filename) {
  146. source = content
  147. }
  148. }, filePath)
  149. return source
  150. }
  151. NYC.prototype.addAllFiles = function () {
  152. var _this = this
  153. this._loadAdditionalModules()
  154. this.fakeRequire = true
  155. this.walkAllFiles(this.cwd, function (filename) {
  156. filename = path.resolve(_this.cwd, filename)
  157. if (_this.exclude.shouldInstrument(filename)) {
  158. _this.addFile(filename)
  159. var coverage = coverageFinder()
  160. var lastCoverage = _this.instrumenter().lastFileCoverage()
  161. if (lastCoverage) {
  162. filename = lastCoverage.path
  163. coverage[filename] = lastCoverage
  164. }
  165. }
  166. })
  167. this.fakeRequire = false
  168. this.writeCoverageFile()
  169. }
  170. NYC.prototype.instrumentAllFiles = function (input, output, cb) {
  171. var _this = this
  172. var inputDir = '.' + path.sep
  173. var visitor = function (filename) {
  174. var ext
  175. var transform
  176. var inFile = path.resolve(inputDir, filename)
  177. var code = fs.readFileSync(inFile, 'utf-8')
  178. for (ext in _this.transforms) {
  179. if (filename.toLowerCase().substr(-ext.length) === ext) {
  180. transform = _this.transforms[ext]
  181. break
  182. }
  183. }
  184. if (transform) {
  185. code = transform(code, { filename: filename, relFile: inFile })
  186. }
  187. if (!output) {
  188. console.log(code)
  189. } else {
  190. var outFile = path.resolve(output, filename)
  191. mkdirp.sync(path.dirname(outFile))
  192. fs.writeFileSync(outFile, code, 'utf-8')
  193. }
  194. }
  195. this._loadAdditionalModules()
  196. try {
  197. var stats = fs.lstatSync(input)
  198. if (stats.isDirectory()) {
  199. inputDir = input
  200. this.walkAllFiles(input, visitor)
  201. } else {
  202. visitor(input)
  203. }
  204. } catch (err) {
  205. return cb(err)
  206. }
  207. cb()
  208. }
  209. NYC.prototype.walkAllFiles = function (dir, visitor) {
  210. var pattern = null
  211. if (this.extensions.length === 1) {
  212. pattern = '**/*' + this.extensions[0]
  213. } else {
  214. pattern = '**/*{' + this.extensions.join() + '}'
  215. }
  216. glob.sync(pattern, { cwd: dir, nodir: true, ignore: this.exclude.exclude }).forEach(function (filename) {
  217. visitor(filename)
  218. })
  219. }
  220. NYC.prototype._maybeInstrumentSource = function (code, filename, relFile) {
  221. var instrument = this.exclude.shouldInstrument(filename, relFile)
  222. if (!instrument) {
  223. return null
  224. }
  225. var ext, transform
  226. for (ext in this.transforms) {
  227. if (filename.toLowerCase().substr(-ext.length) === ext) {
  228. transform = this.transforms[ext]
  229. break
  230. }
  231. }
  232. return transform ? transform(code, { filename: filename, relFile: relFile }) : null
  233. }
  234. NYC.prototype._transformFactory = function (cacheDir) {
  235. const instrumenter = this.instrumenter()
  236. let instrumented
  237. return (code, metadata, hash) => {
  238. const filename = metadata.filename
  239. let sourceMap = null
  240. if (this._sourceMap) sourceMap = this.sourceMaps.extractAndRegister(code, filename, hash)
  241. try {
  242. instrumented = instrumenter.instrumentSync(code, filename, sourceMap)
  243. } catch (e) {
  244. debugLog('failed to instrument ' + filename + ' with error: ' + e.stack)
  245. if (this.config.exitOnError) {
  246. console.error('Failed to instrument ' + filename)
  247. process.exit(1)
  248. } else {
  249. instrumented = code
  250. }
  251. }
  252. if (this.fakeRequire) {
  253. return 'function x () {}'
  254. } else {
  255. return instrumented
  256. }
  257. }
  258. }
  259. NYC.prototype._handleJs = function (code, options) {
  260. var filename = options.filename
  261. var relFile = path.relative(this.cwd, filename)
  262. // ensure the path has correct casing (see istanbuljs/nyc#269 and nodejs/node#6624)
  263. filename = path.resolve(this.cwd, relFile)
  264. return this._maybeInstrumentSource(code, filename, relFile) || code
  265. }
  266. NYC.prototype._addHook = function (type) {
  267. var handleJs = this._handleJs.bind(this)
  268. var dummyMatcher = function () { return true } // we do all processing in transformer
  269. libHook['hook' + type](dummyMatcher, handleJs, { extensions: this.extensions })
  270. }
  271. NYC.prototype._addRequireHooks = function () {
  272. if (this.hookRequire) {
  273. this._addHook('Require')
  274. }
  275. if (this.hookRunInContext) {
  276. this._addHook('RunInContext')
  277. }
  278. if (this.hookRunInThisContext) {
  279. this._addHook('RunInThisContext')
  280. }
  281. }
  282. NYC.prototype.cleanup = function () {
  283. if (!process.env.NYC_CWD) rimraf.sync(this.tempDirectory())
  284. }
  285. NYC.prototype.clearCache = function () {
  286. if (this.cache) {
  287. rimraf.sync(this.cacheDirectory)
  288. }
  289. }
  290. NYC.prototype.createTempDirectory = function () {
  291. mkdirp.sync(this.tempDirectory())
  292. if (this.cache) mkdirp.sync(this.cacheDirectory)
  293. if (this._showProcessTree) {
  294. mkdirp.sync(this.processInfoDirectory())
  295. }
  296. }
  297. NYC.prototype.reset = function () {
  298. this.cleanup()
  299. this.createTempDirectory()
  300. }
  301. NYC.prototype._wrapExit = function () {
  302. var _this = this
  303. // we always want to write coverage
  304. // regardless of how the process exits.
  305. onExit(function () {
  306. _this.writeCoverageFile()
  307. }, { alwaysLast: true })
  308. }
  309. NYC.prototype.wrap = function (bin) {
  310. this._addRequireHooks()
  311. this._wrapExit()
  312. this._loadAdditionalModules()
  313. return this
  314. }
  315. NYC.prototype.generateUniqueID = uuid
  316. NYC.prototype.writeCoverageFile = function () {
  317. var coverage = coverageFinder()
  318. if (!coverage) return
  319. // Remove any files that should be excluded but snuck into the coverage
  320. Object.keys(coverage).forEach(function (absFile) {
  321. if (!this.exclude.shouldInstrument(absFile)) {
  322. delete coverage[absFile]
  323. }
  324. }, this)
  325. if (this.cache) {
  326. Object.keys(coverage).forEach(function (absFile) {
  327. if (this.hashCache[absFile] && coverage[absFile]) {
  328. coverage[absFile].contentHash = this.hashCache[absFile]
  329. }
  330. }, this)
  331. } else {
  332. coverage = this.sourceMaps.remapCoverage(coverage)
  333. }
  334. var id = this.generateUniqueID()
  335. var coverageFilename = path.resolve(this.tempDirectory(), id + '.json')
  336. fs.writeFileSync(
  337. coverageFilename,
  338. JSON.stringify(coverage),
  339. 'utf-8'
  340. )
  341. if (!this._showProcessTree) {
  342. return
  343. }
  344. this.processInfo.coverageFilename = coverageFilename
  345. fs.writeFileSync(
  346. path.resolve(this.processInfoDirectory(), id + '.json'),
  347. JSON.stringify(this.processInfo),
  348. 'utf-8'
  349. )
  350. }
  351. function coverageFinder () {
  352. var coverage = global.__coverage__
  353. if (typeof __coverage__ === 'object') coverage = __coverage__
  354. if (!coverage) coverage = global['__coverage__'] = {}
  355. return coverage
  356. }
  357. NYC.prototype.getCoverageMapFromAllCoverageFiles = function (baseDirectory) {
  358. var _this = this
  359. var map = libCoverage.createCoverageMap({})
  360. this.eachReport(undefined, (report) => {
  361. map.merge(report)
  362. }, baseDirectory)
  363. // depending on whether source-code is pre-instrumented
  364. // or instrumented using a JIT plugin like @babel/require
  365. // you may opt to exclude files after applying
  366. // source-map remapping logic.
  367. if (this.config.excludeAfterRemap) {
  368. map.filter(function (filename) {
  369. return _this.exclude.shouldInstrument(filename)
  370. })
  371. }
  372. map.data = this.sourceMaps.remapCoverage(map.data)
  373. return map
  374. }
  375. NYC.prototype.report = function () {
  376. var tree
  377. var map = this.getCoverageMapFromAllCoverageFiles()
  378. var context = libReport.createContext({
  379. dir: this.reportDirectory(),
  380. watermarks: this.config.watermarks
  381. })
  382. tree = libReport.summarizers.pkg(map)
  383. this.reporter.forEach((_reporter) => {
  384. tree.visit(reports.create(_reporter, {
  385. skipEmpty: this.config.skipEmpty,
  386. skipFull: this.config.skipFull
  387. }), context)
  388. })
  389. if (this._showProcessTree) {
  390. this.showProcessTree()
  391. }
  392. }
  393. NYC.prototype.showProcessTree = function () {
  394. var processTree = ProcessInfo.buildProcessTree(this._loadProcessInfos())
  395. console.log(processTree.render(this))
  396. }
  397. NYC.prototype.checkCoverage = function (thresholds, perFile) {
  398. var map = this.getCoverageMapFromAllCoverageFiles()
  399. var nyc = this
  400. if (perFile) {
  401. map.files().forEach(function (file) {
  402. // ERROR: Coverage for lines (90.12%) does not meet threshold (120%) for index.js
  403. nyc._checkCoverage(map.fileCoverageFor(file).toSummary(), thresholds, file)
  404. })
  405. } else {
  406. // ERROR: Coverage for lines (90.12%) does not meet global threshold (120%)
  407. nyc._checkCoverage(map.getCoverageSummary(), thresholds)
  408. }
  409. // process.exitCode was not implemented until v0.11.8.
  410. if (/^v0\.(1[0-1]\.|[0-9]\.)/.test(process.version) && process.exitCode !== 0) process.exit(process.exitCode)
  411. }
  412. NYC.prototype._checkCoverage = function (summary, thresholds, file) {
  413. Object.keys(thresholds).forEach(function (key) {
  414. var coverage = summary[key].pct
  415. if (coverage < thresholds[key]) {
  416. process.exitCode = 1
  417. if (file) {
  418. console.error('ERROR: Coverage for ' + key + ' (' + coverage + '%) does not meet threshold (' + thresholds[key] + '%) for ' + file)
  419. } else {
  420. console.error('ERROR: Coverage for ' + key + ' (' + coverage + '%) does not meet global threshold (' + thresholds[key] + '%)')
  421. }
  422. }
  423. })
  424. }
  425. NYC.prototype._loadProcessInfos = function () {
  426. var _this = this
  427. var files = fs.readdirSync(this.processInfoDirectory())
  428. return files.map(function (f) {
  429. try {
  430. return new ProcessInfo(JSON.parse(fs.readFileSync(
  431. path.resolve(_this.processInfoDirectory(), f),
  432. 'utf-8'
  433. )))
  434. } catch (e) { // handle corrupt JSON output.
  435. return {}
  436. }
  437. })
  438. }
  439. NYC.prototype.eachReport = function (filenames, iterator, baseDirectory) {
  440. baseDirectory = baseDirectory || this.tempDirectory()
  441. if (typeof filenames === 'function') {
  442. iterator = filenames
  443. filenames = undefined
  444. }
  445. var _this = this
  446. var files = filenames || fs.readdirSync(baseDirectory)
  447. files.forEach(function (f) {
  448. var report
  449. try {
  450. report = JSON.parse(fs.readFileSync(
  451. path.resolve(baseDirectory, f),
  452. 'utf-8'
  453. ))
  454. _this.sourceMaps.reloadCachedSourceMaps(report)
  455. } catch (e) { // handle corrupt JSON output.
  456. report = {}
  457. }
  458. iterator(report)
  459. })
  460. }
  461. NYC.prototype.loadReports = function (filenames) {
  462. var reports = []
  463. this.eachReport(filenames, (report) => {
  464. reports.push(report)
  465. })
  466. return reports
  467. }
  468. NYC.prototype.tempDirectory = function () {
  469. return path.resolve(this.cwd, this._tempDirectory)
  470. }
  471. NYC.prototype.reportDirectory = function () {
  472. return path.resolve(this.cwd, this._reportDir)
  473. }
  474. NYC.prototype.processInfoDirectory = function () {
  475. return path.resolve(this.tempDirectory(), 'processinfo')
  476. }
  477. module.exports = NYC