diff --git a/README.md b/README.md index 07175b02c..1fecbb0f1 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,42 @@ of the pre-transpiled code. You'll have to configure your custom require hook to inline the source map in the transpiled code. For Babel that means setting the `sourceMaps` option to `inline`. +## Use babel-plugin__coverage__ for Better ES6/ES7 Support + +[`babel-plugin-__coverage__`](https://github.com/dtinth/babel-plugin-__coverage__) can be used to enable better first-class ES6 support. + +1. enable the `__coverage__` plugin: + + ```json + { + "babel": { + "presets": ["es2015"], + "plugins": ["__coverage__"] + } + } + ``` + +2. disable nyc's instrumentation and source-maps: + + ```json + { + "nyc": { + "include": [ + "src/*.js" + ], + "require": [ + "babel-register" + ], + "sourceMap": false, + "instrumenter": "./lib/instrumenters/noop" + } + } + ``` + +That's all there is to it, better ES6 syntax highlighting awaits: + + + ## Support For Custom File Extensions (.jsx, .es6) Supporting file extensions can be configured through either the configuration arguments or with the `nyc` config section in `package.json`. @@ -249,8 +285,6 @@ That's all there is to it! [codecov](https://codecov.io/) is a great tool for adding coverage reports to your GitHub project, even viewing them inline on GitHub with a browser extension: -![browser extension](https://d234q63orb21db.cloudfront.net/ad63907877249140772dff929ad1c340e424962a/media/images/next/extension.png) - Here's how to get `nyc` integrated with codecov and travis-ci.org: 1. add the codecov and nyc dependencies to your module: diff --git a/bin/nyc.js b/bin/nyc.js index e1b70f589..5fa50f8ea 100755 --- a/bin/nyc.js +++ b/bin/nyc.js @@ -97,6 +97,16 @@ var yargs = require('yargs/yargs')(process.argv.slice(2)) description: 'what % of statements must be covered?', global: true }) + .option('source-map', { + default: true, + type: 'boolean', + description: 'should nyc detect and handle source maps?' + }) + .option('instrumenter', { + default: './lib/instrumenters/istanbul', + type: 'string', + description: 'what library should be used to instrument coverage?' + }) .help('h') .alias('h', 'help') .version() @@ -125,7 +135,8 @@ if (argv._[0] === 'report') { var nyc = (new NYC({ require: argv.require, include: argv.include, - exclude: argv.exclude + exclude: argv.exclude, + sourceMap: !!argv.sourceMap })) nyc.reset() @@ -133,7 +144,9 @@ if (argv._[0] === 'report') { var env = { NYC_CWD: process.cwd(), - NYC_CACHE: argv.cache ? 'enable' : 'disable' + NYC_CACHE: argv.cache ? 'enable' : 'disable', + NYC_SOURCE_MAP: argv.sourceMap ? 'enable' : 'disable', + NYC_INSTRUMENTER: argv.instrumenter } if (argv.require.length) { env.NYC_REQUIRE = argv.require.join(',') diff --git a/bin/wrap.js b/bin/wrap.js index c0c326763..16372ae3d 100644 --- a/bin/wrap.js +++ b/bin/wrap.js @@ -11,7 +11,9 @@ try { extension: process.env.NYC_EXTENSION ? process.env.NYC_EXTENSION.split(',') : [], exclude: process.env.NYC_EXCLUDE ? process.env.NYC_EXCLUDE.split(',') : [], include: process.env.NYC_INCLUDE ? process.env.NYC_INCLUDE.split(',') : [], - enableCache: process.env.NYC_CACHE === 'enable' + enableCache: process.env.NYC_CACHE === 'enable', + sourceMap: process.env.NYC_SOURCE_MAP === 'enable', + instrumenter: process.env.NYC_INSTRUMENTER })).wrap() sw.runMain() diff --git a/index.js b/index.js index 4f72eec28..617791a6d 100755 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ /* global __coverage__ */ var fs = require('fs') var glob = require('glob') -var micromatch = require('micromatch') var mkdirp = require('mkdirp') var Module = require('module') var appendTransform = require('append-transform') @@ -17,7 +16,8 @@ var md5hex = require('md5-hex') var findCacheDir = require('find-cache-dir') var js = require('default-require-extensions/js') var pkgUp = require('pkg-up') -var yargs = require('yargs/yargs') +var testExclude = require('test-exclude') +var yargs = require('yargs') /* istanbul ignore next */ if (/index\.covered\.js$/.test(__filename)) { @@ -30,29 +30,23 @@ function NYC (opts) { this._istanbul = config.istanbul 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 + this._sourceMap = config.sourceMap this.cwd = config.cwd this.reporter = arrify(config.reporter || 'text') - // load exclude stanza from config. - this.include = false - if (config.include && config.include.length > 0) { - this.include = this._prepGlobPatterns(arrify(config.include)) - } - - this.exclude = this._prepGlobPatterns( - ['**/node_modules/**'].concat(arrify( - config.exclude && config.exclude.length > 0 - ? config.exclude - : ['test/**', 'test{,-*}.js', '**/*.test.js', '**/__tests__/**'] - )) - ) - this.cacheDirectory = findCacheDir({name: 'nyc', cwd: this.cwd}) this.enableCache = Boolean(this.cacheDirectory && (config.enableCache === true || process.env.NYC_CACHE === 'enable')) + this.exclude = testExclude({ + cwd: this.cwd, + include: config.include, + exclude: config.exclude + }) + // require extensions can be provided as config in package.json. this.require = arrify(config.require) @@ -129,43 +123,7 @@ NYC.prototype.instrumenter = function () { } NYC.prototype._createInstrumenter = function () { - var configFile = path.resolve(this.cwd, './.istanbul.yml') - - if (!fs.existsSync(configFile)) configFile = undefined - - var istanbul = this.istanbul() - - var instrumenterConfig = istanbul.config.loadFile(configFile).instrumentation.config - - return new istanbul.Instrumenter({ - coverageVariable: '__coverage__', - embedSource: instrumenterConfig['embed-source'], - noCompact: !instrumenterConfig.compact, - preserveComments: instrumenterConfig['preserve-comments'] - }) -} - -NYC.prototype._prepGlobPatterns = function (patterns) { - if (!patterns) return patterns - - var result = [] - - function add (pattern) { - if (result.indexOf(pattern) === -1) { - result.push(pattern) - } - } - - patterns.forEach(function (pattern) { - // Allow gitignore style of directory exclusion - if (!/\/\*\*$/.test(pattern)) { - add(pattern.replace(/\/$/, '') + '/**') - } - - add(pattern) - }) - - return result + return this._instrumenterLib(this.cwd) } NYC.prototype.addFile = function (filename) { @@ -190,14 +148,6 @@ NYC.prototype._readTranspiledSource = function (path) { return source } -NYC.prototype.shouldInstrumentFile = function (filename, relFile) { - // Don't instrument files that are outside of the current working directory. - if (/^\.\./.test(path.relative(this.cwd, filename))) return false - - relFile = relFile.replace(/^\.[\\\/]/, '') // remove leading './' or '.\'. - return (!this.include || micromatch.any(relFile, this.include)) && !micromatch.any(relFile, this.exclude) -} - NYC.prototype.addAllFiles = function () { var _this = this @@ -210,7 +160,7 @@ NYC.prototype.addAllFiles = function () { pattern = '**/*{' + this.extensions.join() + '}' } - glob.sync(pattern, {cwd: this.cwd, nodir: true, ignore: this.exclude}).forEach(function (filename) { + glob.sync(pattern, {cwd: this.cwd, nodir: true, ignore: this.exclude.exclude}).forEach(function (filename) { var obj = _this.addFile(path.join(_this.cwd, filename)) if (obj.instrument) { module._compile( @@ -224,7 +174,7 @@ NYC.prototype.addAllFiles = function () { } NYC.prototype._maybeInstrumentSource = function (code, filename, relFile) { - var instrument = this.shouldInstrumentFile(filename, relFile) + var instrument = this.exclude.shouldInstrument(filename, relFile) if (!instrument) { return null @@ -248,20 +198,24 @@ NYC.prototype._transformFactory = function (cacheDir) { return function (code, metadata, hash) { var filename = metadata.filename - var sourceMap = convertSourceMap.fromSource(code) || convertSourceMap.fromMapFileSource(code, path.dirname(filename)) - if (sourceMap) { - if (hash) { - var mapPath = path.join(cacheDir, hash + '.map') - fs.writeFileSync(mapPath, sourceMap.toJSON()) - } else { - _this.sourceMapCache.addMap(filename, sourceMap.toJSON()) - } - } + if (_this._sourceMap) _this._handleSourceMap(cacheDir, code, hash, filename) return instrumenter.instrumentSync(code, filename) } } +NYC.prototype._handleSourceMap = function (cacheDir, code, hash, filename) { + var sourceMap = convertSourceMap.fromSource(code) || convertSourceMap.fromMapFileSource(code, path.dirname(filename)) + if (sourceMap) { + if (hash) { + var mapPath = path.join(cacheDir, hash + '.map') + fs.writeFileSync(mapPath, sourceMap.toJSON()) + } else { + this.sourceMapCache.addMap(filename, sourceMap.toJSON()) + } + } +} + NYC.prototype._handleJs = function (code, filename) { var relFile = path.relative(this.cwd, filename) return this._maybeInstrumentSource(code, filename, relFile) || code diff --git a/lib/instrumenters/istanbul.js b/lib/instrumenters/istanbul.js new file mode 100644 index 000000000..12a22a628 --- /dev/null +++ b/lib/instrumenters/istanbul.js @@ -0,0 +1,23 @@ +var fs = require('fs') +var path = require('path') + +function InstrumenterIstanbul (cwd) { + var configFile = path.resolve(cwd, './.istanbul.yml') + + if (!fs.existsSync(configFile)) configFile = undefined + var istanbul = InstrumenterIstanbul.istanbul() + var instrumenterConfig = istanbul.config.loadFile(configFile).instrumentation.config + + return new istanbul.Instrumenter({ + coverageVariable: '__coverage__', + embedSource: instrumenterConfig['embed-source'], + noCompact: !instrumenterConfig.compact, + preserveComments: instrumenterConfig['preserve-comments'] + }) +} + +InstrumenterIstanbul.istanbul = function () { + return InstrumenterIstanbul._istanbul || (InstrumenterIstanbul._istanbul = require('istanbul')) +} + +module.exports = InstrumenterIstanbul diff --git a/lib/instrumenters/noop.js b/lib/instrumenters/noop.js new file mode 100644 index 000000000..3159e4870 --- /dev/null +++ b/lib/instrumenters/noop.js @@ -0,0 +1,9 @@ +function NOOP () { + return { + instrumentSync: function (code) { + return code + } + } +} + +module.exports = NOOP diff --git a/lib/source-map-cache.js b/lib/source-map-cache.js index 5bedd894d..fbfd8dea6 100644 --- a/lib/source-map-cache.js +++ b/lib/source-map-cache.js @@ -165,6 +165,11 @@ SourceMapCache.prototype._rewriteBranches = function (fileReport, sourceMap) { if (locations.length > 0) { b[index + ''] = fileReport.b[k] + + /* istanbul ignore next: hard to test for edge-case, + counts for more statements than exist post remapping */ + while (b[index + ''].length > locations.length) b[index + ''].pop() + branchMap[index + ''] = { line: locations[0].start.line, type: item.type, diff --git a/package.json b/package.json index fd9c08cf9..e25ed97d8 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "index.js", "bin/*.js", "lib/*.js", + "lib/instrumenters/*.js", "!**/*covered.js" ], "nyc": { @@ -85,6 +86,7 @@ "signal-exit": "^2.1.1", "source-map": "^0.5.3", "spawn-wrap": "^1.2.2", + "test-exclude": "^1.1.0", "yargs": "^4.7.0" }, "devDependencies": { @@ -131,6 +133,7 @@ "signal-exit", "source-map", "spawn-wrap", + "test-exclude", "yargs" ] } diff --git a/screen2.png b/screen2.png new file mode 100644 index 000000000..9fdd87e93 Binary files /dev/null and b/screen2.png differ diff --git a/test/fixtures/cli/env.js b/test/fixtures/cli/env.js new file mode 100755 index 000000000..df181cbff --- /dev/null +++ b/test/fixtures/cli/env.js @@ -0,0 +1 @@ +console.log(JSON.stringify(process.env)) diff --git a/test/src/nyc-bin.js b/test/src/nyc-bin.js index 69477b943..3a16c28ca 100644 --- a/test/src/nyc-bin.js +++ b/test/src/nyc-bin.js @@ -197,4 +197,46 @@ describe('the nyc cli', function () { }) }) }) + + it('passes configuration via environment variables', function (done) { + var args = [ + bin, + '--silent', + '--require=mkdirp', + '--include=env.js', + '--exclude=batman.js', + '--extension=.js', + '--cache=true', + '--source-map=true', + '--instrumenter=./lib/instrumenters/istanbul.js', + process.execPath, + './env.js' + ] + var expected = { + NYC_REQUIRE: 'mkdirp', + NYC_INCLUDE: 'env.js', + NYC_EXCLUDE: 'batman.js', + NYC_EXTENSION: '.js', + NYC_CACHE: 'enable', + NYC_SOURCE_MAP: 'enable', + NYC_INSTRUMENTER: './lib/instrumenters/istanbul.js' + } + + var proc = spawn(process.execPath, args, { + cwd: fixturesCLI, + env: env + }) + + var stdout = '' + proc.stdout.on('data', function (chunk) { + stdout += chunk + }) + + proc.on('close', function (code) { + code.should.equal(0) + var env = JSON.parse(stdout) + env.should.include(expected) + done() + }) + }) }) diff --git a/test/src/nyc-test.js b/test/src/nyc-test.js index 5a12d8d34..b4d686e4b 100644 --- a/test/src/nyc-test.js +++ b/test/src/nyc-test.js @@ -69,7 +69,7 @@ describe('nyc', function () { cwd: path.resolve(__dirname, '../fixtures') }) - nyc.exclude.length.should.eql(5) + nyc.exclude.exclude.length.should.eql(5) }) it("loads 'extension' patterns from package.json#nyc", function () { @@ -85,14 +85,14 @@ describe('nyc', function () { cwd: path.resolve(__dirname, '../fixtures/conf-empty') }) - nyc1.include.should.equal(false) + nyc1.exclude.include.should.equal(false) var nyc2 = new NYC({ cwd: path.resolve(__dirname, '../fixtures/conf-empty'), include: [] }) - nyc2.include.should.equal(false) + nyc2.exclude.include.should.equal(false) }) it("ignores 'exclude' option if it's falsy or []", function () { @@ -100,127 +100,98 @@ describe('nyc', function () { cwd: path.resolve(__dirname, '../fixtures/conf-empty') }) - nyc1.exclude.length.should.eql(7) + nyc1.exclude.exclude.length.should.eql(7) var nyc2 = new NYC({ cwd: path.resolve(__dirname, '../fixtures/conf-empty'), exclude: [] }) - nyc2.exclude.length.should.eql(7) - }) - }) - - describe('_prepGlobPatterns', function () { - it('should adjust patterns appropriately', function () { - var _prepGlobPatterns = new NYC()._prepGlobPatterns - - var result = _prepGlobPatterns(['./foo', 'bar/**', 'baz/']) - - result.should.deep.equal([ - './foo/**', // Appended `/**` - './foo', - 'bar/**', - 'baz/**', // Removed trailing slash before appending `/**` - 'baz/' - ]) + nyc2.exclude.exclude.length.should.eql(7) }) }) describe('shouldInstrumentFile', function () { it('should exclude appropriately with defaults', function () { var nyc = new NYC({ - cwd: '/cwd/' + cwd: '/cwd/', + exclude: [ + '**/node_modules/**', + 'test/**', + 'test{,-*}.js', + '**/*.test.js', + '**/__tests__/**' + ] }) - // Root package contains config.exclude - // Restore exclude to default patterns - nyc.exclude = nyc._prepGlobPatterns([ - '**/node_modules/**', - 'test/**', - 'test{,-*}.js', - '**/*.test.js', - '**/__tests__/**' - ]) - - var shouldInstrumentFile = nyc.shouldInstrumentFile.bind(nyc) - // nyc always excludes "node_modules/**" - shouldInstrumentFile('/cwd/foo', 'foo').should.equal(true) - shouldInstrumentFile('/cwd/node_modules/bar', 'node_modules/bar').should.equal(false) - shouldInstrumentFile('/cwd/foo/node_modules/bar', 'foo/node_modules/bar').should.equal(false) - shouldInstrumentFile('/cwd/test.js', 'test.js').should.equal(false) - shouldInstrumentFile('/cwd/testfoo.js', 'testfoo.js').should.equal(true) - shouldInstrumentFile('/cwd/test-foo.js', 'test-foo.js').should.equal(false) - shouldInstrumentFile('/cwd/lib/test.js', 'lib/test.js').should.equal(true) - shouldInstrumentFile('/cwd/foo/bar/test.js', './test.js').should.equal(false) - shouldInstrumentFile('/cwd/foo/bar/test.js', '.\\test.js').should.equal(false) - shouldInstrumentFile('/cwd/foo/bar/foo.test.js', './foo.test.js').should.equal(false) - shouldInstrumentFile('/cwd/foo/bar/__tests__/foo.js', './__tests__/foo.js').should.equal(false) + nyc.exclude.shouldInstrument('/cwd/foo', 'foo').should.equal(true) + nyc.exclude.shouldInstrument('/cwd/node_modules/bar', 'node_modules/bar').should.equal(false) + nyc.exclude.shouldInstrument('/cwd/foo/node_modules/bar', 'foo/node_modules/bar').should.equal(false) + nyc.exclude.shouldInstrument('/cwd/test.js', 'test.js').should.equal(false) + nyc.exclude.shouldInstrument('/cwd/testfoo.js', 'testfoo.js').should.equal(true) + nyc.exclude.shouldInstrument('/cwd/test-foo.js', 'test-foo.js').should.equal(false) + nyc.exclude.shouldInstrument('/cwd/lib/test.js', 'lib/test.js').should.equal(true) + nyc.exclude.shouldInstrument('/cwd/foo/bar/test.js', './test.js').should.equal(false) + nyc.exclude.shouldInstrument('/cwd/foo/bar/test.js', '.\\test.js').should.equal(false) + nyc.exclude.shouldInstrument('/cwd/foo/bar/foo.test.js', './foo.test.js').should.equal(false) + nyc.exclude.shouldInstrument('/cwd/foo/bar/__tests__/foo.js', './__tests__/foo.js').should.equal(false) }) it('should exclude appropriately with config.exclude', function () { var nyc = new NYC({ cwd: fixtures }) - var shouldInstrumentFile = nyc.shouldInstrumentFile.bind(nyc) // fixtures/package.json configures excludes: "blarg", "blerg" - shouldInstrumentFile('blarg', 'blarg').should.equal(false) - shouldInstrumentFile('blarg/foo.js', 'blarg/foo.js').should.equal(false) - shouldInstrumentFile('blerg', 'blerg').should.equal(false) - shouldInstrumentFile('./blerg', './blerg').should.equal(false) - shouldInstrumentFile('./blerg', '.\\blerg').should.equal(false) + nyc.exclude.shouldInstrument('blarg', 'blarg').should.equal(false) + nyc.exclude.shouldInstrument('blarg/foo.js', 'blarg/foo.js').should.equal(false) + nyc.exclude.shouldInstrument('blerg', 'blerg').should.equal(false) + nyc.exclude.shouldInstrument('./blerg', './blerg').should.equal(false) + nyc.exclude.shouldInstrument('./blerg', '.\\blerg').should.equal(false) }) it('should exclude outside of the current working directory', function () { var nyc = new NYC({ cwd: '/cwd/foo/' }) - nyc.shouldInstrumentFile('/cwd/bar', '../bar').should.equal(false) + nyc.exclude.shouldInstrument('/cwd/bar', '../bar').should.equal(false) }) it('should not exclude if the current working directory is inside node_modules', function () { var nyc = new NYC({ cwd: '/cwd/node_modules/foo/' }) - nyc.shouldInstrumentFile('/cwd/node_modules/foo/bar', './bar').should.equal(true) - nyc.shouldInstrumentFile('/cwd/node_modules/foo/bar', '.\\bar').should.equal(true) + nyc.exclude.shouldInstrument('/cwd/node_modules/foo/bar', './bar').should.equal(true) + nyc.exclude.shouldInstrument('/cwd/node_modules/foo/bar', '.\\bar').should.equal(true) }) it('allows files to be explicitly included, rather than excluded', function () { var nyc = new NYC({ - cwd: '/cwd/' + cwd: '/cwd/', + include: 'foo.js' }) - nyc.include = nyc._prepGlobPatterns([ - 'foo.js' - ]) - - var shouldInstrumentFile = nyc.shouldInstrumentFile.bind(nyc) - shouldInstrumentFile('/cwd/foo.js', 'foo.js').should.equal(true) - shouldInstrumentFile('/cwd/index.js', 'index.js').should.equal(false) + nyc.exclude.shouldInstrument('/cwd/foo.js', 'foo.js').should.equal(true) + nyc.exclude.shouldInstrument('/cwd/index.js', 'index.js').should.equal(false) }) it('exclude overrides include', function () { var nyc = new NYC({ - cwd: '/cwd/' - }) - - nyc.include = nyc._prepGlobPatterns([ - 'foo.js', - 'test.js' - ]) - // Ensure default exclude patterns apply, which excludes test.js - nyc.exclude = nyc._prepGlobPatterns([ - '**/node_modules/**', - 'test/**', - 'test{,-*}.js' - ]) - - var shouldInstrumentFile = nyc.shouldInstrumentFile.bind(nyc) - shouldInstrumentFile('/cwd/foo.js', 'foo.js').should.equal(true) - shouldInstrumentFile('/cwd/test.js', 'test.js').should.equal(false) + cwd: '/cwd/', + include: [ + 'foo.js', + 'test.js' + ], + exclude: [ + '**/node_modules/**', + 'test/**', + 'test{,-*}.js' + ] + }) + + nyc.exclude.shouldInstrument('/cwd/foo.js', 'foo.js').should.equal(true) + nyc.exclude.shouldInstrument('/cwd/test.js', 'test.js').should.equal(false) }) })