UNPKG

11.1 kBJavaScriptView Raw
1/* global __coverage__ */
2var fs = require('fs')
3var glob = require('glob')
4var micromatch = require('micromatch')
5var mkdirp = require('mkdirp')
6var Module = require('module')
7var appendTransform = require('append-transform')
8var cachingTransform = require('caching-transform')
9var path = require('path')
10var rimraf = require('rimraf')
11var onExit = require('signal-exit')
12var resolveFrom = require('resolve-from')
13var arrify = require('arrify')
14var SourceMapCache = require('./lib/source-map-cache')
15var convertSourceMap = require('convert-source-map')
16var md5hex = require('md5-hex')
17var findCacheDir = require('find-cache-dir')
18var js = require('default-require-extensions/js')
19var pkgUp = require('pkg-up')
20var yargs = require('yargs/yargs')
21
22/* istanbul ignore next */
23if (/index\.covered\.js$/.test(__filename)) {
24 require('./lib/self-coverage-helper')
25}
26
27function NYC (opts) {
28 var config = this._loadConfig(opts || {})
29
30 this._istanbul = config.istanbul
31 this.subprocessBin = config.subprocessBin || path.resolve(__dirname, './bin/nyc.js')
32 this._tempDirectory = config.tempDirectory || './.nyc_output'
33 this._reportDir = config.reportDir
34 this.cwd = config.cwd
35
36 this.reporter = arrify(config.reporter || 'text')
37
38 // load exclude stanza from config.
39 this.include = false
40 if (config.include && config.include.length > 0) {
41 this.include = this._prepGlobPatterns(arrify(config.include))
42 }
43
44 this.exclude = this._prepGlobPatterns(
45 ['**/node_modules/**'].concat(arrify(
46 config.exclude && config.exclude.length > 0
47 ? config.exclude
48 : ['test/**', 'test{,-*}.js', '**/*.test.js', '**/__tests__/**']
49 ))
50 )
51
52 this.cacheDirectory = findCacheDir({name: 'nyc', cwd: this.cwd})
53
54 this.enableCache = Boolean(this.cacheDirectory && (config.enableCache === true || process.env.NYC_CACHE === 'enable'))
55
56 // require extensions can be provided as config in package.json.
57 this.require = arrify(config.require)
58
59 this.extensions = arrify(config.extension).concat('.js').map(function (ext) {
60 return ext.toLowerCase()
61 })
62
63 this.transforms = this.extensions.reduce(function (transforms, ext) {
64 transforms[ext] = this._createTransform(ext)
65 return transforms
66 }.bind(this), {})
67
68 this.sourceMapCache = new SourceMapCache()
69
70 this.hashCache = {}
71 this.loadedMaps = null
72}
73
74NYC.prototype._loadConfig = function (opts) {
75 var cwd = opts.cwd || process.env.NYC_CWD || process.cwd()
76 var pkgPath = pkgUp.sync(cwd)
77
78 if (pkgPath) {
79 cwd = path.dirname(pkgPath)
80 }
81
82 opts.cwd = cwd
83
84 return yargs([])
85 .pkgConf('nyc', cwd)
86 .default(opts)
87 .argv
88}
89
90NYC.prototype._createTransform = function (ext) {
91 var _this = this
92 return cachingTransform({
93 salt: JSON.stringify({
94 istanbul: require('istanbul/package.json').version,
95 nyc: require('./package.json').version
96 }),
97 hash: function (code, metadata, salt) {
98 var hash = md5hex([code, metadata.filename, salt])
99 _this.hashCache[metadata.filename] = hash
100 return hash
101 },
102 factory: this._transformFactory.bind(this),
103 cacheDir: this.cacheDirectory,
104 disableCache: !this.enableCache,
105 ext: ext
106 })
107}
108
109NYC.prototype._loadAdditionalModules = function () {
110 var _this = this
111 this.require.forEach(function (r) {
112 // first attempt to require the module relative to
113 // the directory being instrumented.
114 var p = resolveFrom(_this.cwd, r)
115 if (p) {
116 require(p)
117 return
118 }
119 // now try other locations, .e.g, the nyc node_modules folder.
120 require(r)
121 })
122}
123
124NYC.prototype.instrumenter = function () {
125 return this._instrumenter || (this._instrumenter = this._createInstrumenter())
126}
127
128NYC.prototype._createInstrumenter = function () {
129 var configFile = path.resolve(this.cwd, './.istanbul.yml')
130
131 if (!fs.existsSync(configFile)) configFile = undefined
132
133 var istanbul = this.istanbul()
134
135 var instrumenterConfig = istanbul.config.loadFile(configFile).instrumentation.config
136
137 return new istanbul.Instrumenter({
138 coverageVariable: '__coverage__',
139 embedSource: instrumenterConfig['embed-source'],
140 noCompact: !instrumenterConfig.compact,
141 preserveComments: instrumenterConfig['preserve-comments']
142 })
143}
144
145NYC.prototype._prepGlobPatterns = function (patterns) {
146 if (!patterns) return patterns
147
148 var result = []
149
150 function add (pattern) {
151 if (result.indexOf(pattern) === -1) {
152 result.push(pattern)
153 }
154 }
155
156 patterns.forEach(function (pattern) {
157 // Allow gitignore style of directory exclusion
158 if (!/\/\*\*$/.test(pattern)) {
159 add(pattern.replace(/\/$/, '') + '/**')
160 }
161
162 add(pattern)
163 })
164
165 return result
166}
167
168NYC.prototype.addFile = function (filename) {
169 var relFile = path.relative(this.cwd, filename)
170 var source = this._readTranspiledSource(path.resolve(this.cwd, filename))
171 var instrumentedSource = this._maybeInstrumentSource(source, filename, relFile)
172
173 return {
174 instrument: !!instrumentedSource,
175 relFile: relFile,
176 content: instrumentedSource || source
177 }
178}
179
180NYC.prototype._readTranspiledSource = function (path) {
181 var source = null
182 Module._extensions['.js']({
183 _compile: function (content, filename) {
184 source = content
185 }
186 }, path)
187 return source
188}
189
190NYC.prototype.shouldInstrumentFile = function (filename, relFile) {
191 // Don't instrument files that are outside of the current working directory.
192 if (/^\.\./.test(path.relative(this.cwd, filename))) return false
193
194 relFile = relFile.replace(/^\.[\\\/]/, '') // remove leading './' or '.\'.
195 return (!this.include || micromatch.any(relFile, this.include)) && !micromatch.any(relFile, this.exclude)
196}
197
198NYC.prototype.addAllFiles = function () {
199 var _this = this
200
201 this._loadAdditionalModules()
202
203 var pattern = null
204 if (this.extensions.length === 1) {
205 pattern = '**/*' + this.extensions[0]
206 } else {
207 pattern = '**/*{' + this.extensions.join() + '}'
208 }
209
210 glob.sync(pattern, {cwd: this.cwd, nodir: true, ignore: this.exclude}).forEach(function (filename) {
211 var obj = _this.addFile(path.join(_this.cwd, filename))
212 if (obj.instrument) {
213 module._compile(
214 _this.instrumenter().getPreamble(obj.content, obj.relFile),
215 filename
216 )
217 }
218 })
219
220 this.writeCoverageFile()
221}
222
223NYC.prototype._maybeInstrumentSource = function (code, filename, relFile) {
224 var instrument = this.shouldInstrumentFile(filename, relFile)
225
226 if (!instrument) {
227 return null
228 }
229
230 var ext, transform
231 for (ext in this.transforms) {
232 if (filename.toLowerCase().substr(-ext.length) === ext) {
233 transform = this.transforms[ext]
234 break
235 }
236 }
237
238 return transform ? transform(code, {filename: filename, relFile: relFile}) : null
239}
240
241NYC.prototype._transformFactory = function (cacheDir) {
242 var _this = this
243 var instrumenter = this.instrumenter()
244
245 return function (code, metadata, hash) {
246 var filename = metadata.filename
247
248 var sourceMap = convertSourceMap.fromSource(code) || convertSourceMap.fromMapFileSource(code, path.dirname(filename))
249 if (sourceMap) {
250 if (hash) {
251 var mapPath = path.join(cacheDir, hash + '.map')
252 fs.writeFileSync(mapPath, sourceMap.toJSON())
253 } else {
254 _this.sourceMapCache.addMap(filename, sourceMap.toJSON())
255 }
256 }
257
258 return instrumenter.instrumentSync(code, filename)
259 }
260}
261
262NYC.prototype._handleJs = function (code, filename) {
263 var relFile = path.relative(this.cwd, filename)
264 return this._maybeInstrumentSource(code, filename, relFile) || code
265}
266
267NYC.prototype._wrapRequire = function () {
268 var handleJs = this._handleJs.bind(this)
269
270 this.extensions.forEach(function (ext) {
271 require.extensions[ext] = js
272 appendTransform(handleJs, ext)
273 })
274}
275
276NYC.prototype.cleanup = function () {
277 if (!process.env.NYC_CWD) rimraf.sync(this.tempDirectory())
278}
279
280NYC.prototype.clearCache = function () {
281 if (this.enableCache) {
282 rimraf.sync(this.cacheDirectory)
283 }
284}
285
286NYC.prototype.createTempDirectory = function () {
287 mkdirp.sync(this.tempDirectory())
288}
289
290NYC.prototype.reset = function () {
291 this.cleanup()
292 this.createTempDirectory()
293}
294
295NYC.prototype._wrapExit = function () {
296 var _this = this
297
298 // we always want to write coverage
299 // regardless of how the process exits.
300 onExit(function () {
301 _this.writeCoverageFile()
302 }, {alwaysLast: true})
303}
304
305NYC.prototype.wrap = function (bin) {
306 this._wrapRequire()
307 this._wrapExit()
308 this._loadAdditionalModules()
309 return this
310}
311
312NYC.prototype.writeCoverageFile = function () {
313 var coverage = global.__coverage__
314 if (typeof __coverage__ === 'object') coverage = __coverage__
315 if (!coverage) return
316
317 if (this.enableCache) {
318 Object.keys(coverage).forEach(function (absFile) {
319 if (this.hashCache[absFile] && coverage[absFile]) {
320 coverage[absFile].contentHash = this.hashCache[absFile]
321 }
322 }, this)
323 } else {
324 this.sourceMapCache.applySourceMaps(coverage)
325 }
326
327 fs.writeFileSync(
328 path.resolve(this.tempDirectory(), './', process.pid + '.json'),
329 JSON.stringify(coverage),
330 'utf-8'
331 )
332}
333
334NYC.prototype.istanbul = function () {
335 return this._istanbul || (this._istanbul = require('istanbul'))
336}
337
338NYC.prototype.report = function (cb, _collector, _reporter) {
339 cb = cb || function () {}
340
341 var istanbul = this.istanbul()
342 var collector = _collector || new istanbul.Collector()
343 var reporter = _reporter || new istanbul.Reporter(null, this._reportDir)
344
345 this._loadReports().forEach(function (report) {
346 collector.add(report)
347 })
348
349 this.reporter.forEach(function (_reporter) {
350 reporter.add(_reporter)
351 })
352
353 reporter.write(collector, true, cb)
354}
355
356NYC.prototype._loadReports = function () {
357 var _this = this
358 var files = fs.readdirSync(this.tempDirectory())
359
360 var cacheDir = _this.cacheDirectory
361
362 var loadedMaps = this.loadedMaps || (this.loadedMaps = {})
363
364 return files.map(function (f) {
365 var report
366 try {
367 report = JSON.parse(fs.readFileSync(
368 path.resolve(_this.tempDirectory(), './', f),
369 'utf-8'
370 ))
371 } catch (e) { // handle corrupt JSON output.
372 return {}
373 }
374
375 Object.keys(report).forEach(function (absFile) {
376 var fileReport = report[absFile]
377 if (fileReport && fileReport.contentHash) {
378 var hash = fileReport.contentHash
379 if (!(hash in loadedMaps)) {
380 try {
381 var mapPath = path.join(cacheDir, hash + '.map')
382 loadedMaps[hash] = JSON.parse(fs.readFileSync(mapPath, 'utf8'))
383 } catch (e) {
384 // set to false to avoid repeatedly trying to load the map
385 loadedMaps[hash] = false
386 }
387 }
388 if (loadedMaps[hash]) {
389 _this.sourceMapCache.addMap(absFile, loadedMaps[hash])
390 }
391 }
392 })
393 _this.sourceMapCache.applySourceMaps(report)
394 return report
395 })
396}
397
398NYC.prototype.tempDirectory = function () {
399 return path.resolve(this.cwd, './', this._tempDirectory)
400}
401
402NYC.prototype.mungeArgs = function (yargv) {
403 var argv = process.argv.slice(1)
404 argv = argv.slice(argv.indexOf(yargv._[0]))
405 return argv
406}
407
408module.exports = NYC