Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Babel/ES2015 Support #58

Merged
merged 4 commits into from
Nov 29, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Change Log

### v4.0.0 (2015/11/29 10:13 -07:00)

- [#58](https://github.com/bcoe/nyc/pull/58) adds support for Babel (@bcoe)

### v3.2.2 (2015/09/11 22:02 -07:00)

- [#47](https://github.com/bcoe/nyc/pull/47) make the default exclude rules work on Windows (@bcoe)
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@ If you're so inclined, you can simply add nyc to the test stanza in your package
}
```

## Support For Babel/ES2015

nyc is the easiest way to add ES2015 support to your project:

1. install the appropriate babel dependencies for your project (`npm i babel-core babel-preset-es2015 --save`).
2. create a `.babelrc` file:

```json
{
"presets": ["es2015"]
}
```

3. install nyc, and run it with the appropriate `--require` flags:

```sh
nyc --require babel-core/register mocha
```

nyc uses source-maps to map coverage information back to the appropriate lines of the pre-transpiled code:

<img width="350" src="screen.png">

## Checking Coverage

nyc exposes istanbul's check-coverage tool. After running your tests with nyc,
Expand Down Expand Up @@ -100,6 +123,13 @@ adding the following configuration:
By default nyc does not collect coverage for files that have not
been required, run nyc with the flag `--all` to enable this.

## Require additional modules

The `--require` flag can be provided to `nyc` to indicate that additional
modules should be required in the subprocess collecting coverage:

`nyc --require babel-core/register --require babel-polyfill mocha`

## Configuring Istanbul

Behind the scenes nyc uses [istanbul](https://www.npmjs.com/package/istanbul). You
Expand Down
14 changes: 12 additions & 2 deletions bin/nyc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ var path = require('path')
var sw = require('spawn-wrap')

if (process.env.NYC_CWD) {
;(new NYC()).wrap()
;(new NYC({
require: process.env.NYC_REQUIRE.split(',')
})).wrap()

// make sure we can run coverage on
// our own index.js, I like turtles.
Expand Down Expand Up @@ -72,10 +74,16 @@ if (process.env.NYC_CWD) {
type: 'boolean',
describe: 'whether or not to instrument all files of the project (not just the ones touched by your test suite)'
})
.option('i', {
alias: 'require',
default: [],
describe: 'a list of additional modules that nyc should attempt to require in its subprocess, e.g., babel-register, babel-polyfill.'
})
.help('h')
.alias('h', 'help')
.version(require('../package.json').version)
.example('$0 npm test', 'instrument your tests with coverage')
.example('$0 --require babel-core/polyfill --require babel-core/register npm test', 'instrument your tests with coverage and babel')
.example('$0 report --reporter=text-lcov', 'output lcov report after running your tests')
.epilog('visit http://git.io/vTJJB for list of available reporters')
var argv = yargs.argv
Expand Down Expand Up @@ -104,9 +112,11 @@ if (process.env.NYC_CWD) {
nyc.cleanup()

if (argv.all) nyc.addAllFiles()
if (!Array.isArray(argv.require)) argv.require = [argv.require]

sw([__filename], {
NYC_CWD: process.cwd()
NYC_CWD: process.cwd(),
NYC_REQUIRE: argv.require.join(',')
})

foreground(nyc.mungeArgs(argv), function (done) {
Expand Down
91 changes: 77 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var path = require('path')
var rimraf = require('rimraf')
var onExit = require('signal-exit')
var stripBom = require('strip-bom')
var SourceMapCache = require('./lib/source-map-cache')

function NYC (opts) {
_.extend(this, {
Expand All @@ -17,24 +18,38 @@ function NYC (opts) {
tempDirectory: './.nyc_output',
cwd: process.env.NYC_CWD || process.cwd(),
reporter: 'text',
istanbul: require('istanbul')
istanbul: require('istanbul'),
sourceMapCache: new SourceMapCache(),
require: []
}, opts)

if (!Array.isArray(this.reporter)) this.reporter = [this.reporter]

// you can specify config in the nyc stanza of package.json.
var config = require(path.resolve(this.cwd, './package.json')).config || {}
config = config.nyc || {}

// load exclude stanza from config.
this.exclude = config.exclude || ['node_modules[\/\\\\]', 'test[\/\\\\]', 'test\\.js']
if (!Array.isArray(this.exclude)) this.exclude = [this.exclude]
this.exclude = _.map(this.exclude, function (p) {
return new RegExp(p)
})

// require extensions can be provided as config in package.json.
this.require = config.require ? config.require : this.require

this.instrumenter = this._createInstrumenter()
this._createOutputDirectory()
}

NYC.prototype._loadAdditionalModules = function () {
require.main.paths.push(path.resolve(this.cwd, '/node_modules'))
this.require.forEach(function (r) {
require(r)
})
}

NYC.prototype._createInstrumenter = function () {
var configFile = path.resolve(this.cwd, './.istanbul.yml')

Expand All @@ -51,19 +66,26 @@ NYC.prototype._createInstrumenter = function () {
}

NYC.prototype.addFile = function (filename, returnImmediately) {
var instrument = true
var relFile = path.relative(this.cwd, filename)
var instrument = this.shouldInstrumentFile(relFile)
var content = stripBom(fs.readFileSync(filename, 'utf8'))

// only instrument a file if it's not on the exclude list.
for (var i = 0, exclude; (exclude = this.exclude[i]) !== undefined; i++) {
if (exclude.test(relFile)) {
if (returnImmediately) return {}
instrument = false
break
}
if (instrument) {
content = this.instrumenter.instrumentSync(content, './' + relFile)
} else if (returnImmediately) {
return {}
}

var content = stripBom(fs.readFileSync(filename, 'utf8'))
return {
instrument: instrument,
content: content,
relFile: relFile
}
}

NYC.prototype.addContent = function (filename, content) {
var relFile = path.relative(this.cwd, filename)
var instrument = this.shouldInstrumentFile(relFile)

if (instrument) {
content = this.instrumenter.instrumentSync(content, './' + relFile)
Expand All @@ -76,6 +98,16 @@ NYC.prototype.addFile = function (filename, returnImmediately) {
}
}

NYC.prototype.shouldInstrumentFile = function (relFile, returnImmediately) {
// only instrument a file if it's not on the exclude list.
for (var i = 0, exclude; (exclude = this.exclude[i]) !== undefined; i++) {
if (exclude.test(relFile)) {
return false
}
}
return true
}

NYC.prototype.addAllFiles = function () {
var _this = this

Expand All @@ -97,11 +129,41 @@ NYC.prototype.addAllFiles = function () {
NYC.prototype._wrapRequire = function () {
var _this = this

// any JS you require should get coverage added.
require.extensions['.js'] = function (module, filename) {
var obj = _this.addFile(filename)
var babelRequireHook = null
var requireHook = function (module, filename) {
// allow babel's compile hoook to compile
// the code -- ignore node_modules, this
// helps avoid cyclical require behavior.
var content = null
if (babelRequireHook && filename.indexOf('node_modules/') === -1) {
babelRequireHook({
_compile: function (compiledSrc) {
_this.sourceMapCache.add(filename, compiledSrc)
content = compiledSrc
}
}, filename)
}

// now instrument the compiled code.
var obj = null
if (content) {
obj = _this.addContent(filename, content)
} else {
obj = _this.addFile(filename, false)
}

module._compile(obj.content, filename)
}

// use a getter and setter to capture any external
// require hooks that are registered, e.g., babel-core/register
require.extensions.__defineGetter__('.js', function () {
return requireHook
})

require.extensions.__defineSetter__('.js', function (value) {
babelRequireHook = value
})
}

NYC.prototype.cleanup = function () {
Expand All @@ -125,6 +187,7 @@ NYC.prototype._wrapExit = function () {
NYC.prototype.wrap = function (bin) {
this._wrapRequire()
this._wrapExit()
this._loadAdditionalModules()
return this
}

Expand All @@ -135,7 +198,7 @@ NYC.prototype.writeCoverageFile = function () {

fs.writeFileSync(
path.resolve(this.tmpDirectory(), './', process.pid + '.json'),
JSON.stringify(coverage),
JSON.stringify(this.sourceMapCache.applySourceMaps(coverage)),
'utf-8'
)
}
Expand Down
127 changes: 127 additions & 0 deletions lib/source-map-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
var _ = require('lodash')
var path = require('path')
var convertSourceMap = require('convert-source-map')
var SourceMapConsumer = require('source-map').SourceMapConsumer

function SourceMapCache (opts) {
_.extend(this, {
cache: {},
cwd: process.env.NYC_CWD || process.cwd()
}, opts)
}

SourceMapCache.prototype.add = function (filename, source) {
var sourceMap = convertSourceMap.fromSource(source)
if (sourceMap) this.cache['./' + path.relative(this.cwd, filename)] = new SourceMapConsumer(sourceMap.sourcemap)
}

SourceMapCache.prototype.applySourceMaps = function (coverage) {
var _this = this
var mappedCoverage = _.cloneDeep(coverage)

Object.keys(coverage).forEach(function (key) {
if (_this.cache[key]) {
_this._rewriteStatements(mappedCoverage[key], _this.cache[key])
_this._rewriteFunctions(mappedCoverage[key], _this.cache[key])
_this._rewriteBranches(mappedCoverage[key], _this.cache[key])
}
})

return mappedCoverage
}

SourceMapCache.prototype._rewriteStatements = function (coverage, sourceMap) {
var start = null
var end = null

var s = {}
var statementMap = {}
var index = 1

Object.keys(coverage.statementMap).forEach(function (k) {
start = sourceMap.originalPositionFor({line: coverage.statementMap[k].start.line, column: coverage.statementMap[k].start.column})
end = sourceMap.originalPositionFor({line: coverage.statementMap[k].end.line, column: coverage.statementMap[k].end.column})
if (start.line && end.line) {
s[index + ''] = coverage.s[k]
statementMap[index + ''] = {
start: {line: start.line, column: start.column},
end: {line: end.line, column: end.column}
}
index++
}
})

coverage.statementMap = statementMap
coverage.s = s
}

SourceMapCache.prototype._rewriteFunctions = function (coverage, sourceMap) {
var start = null
var end = null
var line = null

var f = {}
var fnMap = {}
var index = 1

Object.keys(coverage.fnMap).forEach(function (k) {
start = sourceMap.originalPositionFor({line: coverage.fnMap[k].loc.start.line, column: coverage.fnMap[k].loc.start.column})
end = sourceMap.originalPositionFor({line: coverage.fnMap[k].loc.end.line, column: coverage.fnMap[k].loc.end.column})
line = sourceMap.originalPositionFor({line: coverage.fnMap[k].line, column: null})

if (line.line && start.line && end.line) {
f[index + ''] = coverage.f[k]
fnMap[index + ''] = {
name: coverage.fnMap[k].name,
line: line.line,
loc: {
start: {line: start.line, column: start.column},
end: {line: end.line, column: end.column}
}
}
index++
}
})

coverage.fnMap = fnMap
coverage.f = f
}

SourceMapCache.prototype._rewriteBranches = function (coverage, sourceMap) {
var start = null
var end = null
var line = null

var b = {}
var branchMap = {}
var index = 1

Object.keys(coverage.branchMap).forEach(function (k) {
line = sourceMap.originalPositionFor({line: coverage.branchMap[k].line, column: null})
if (line.line) {
b[index + ''] = []
branchMap[index + ''] = {
line: line.line,
type: coverage.branchMap[k].type,
locations: []
}
for (var i = 0, location; (location = coverage.branchMap[k].locations[i]) !== undefined; i++) {
start = sourceMap.originalPositionFor({line: location.start.line, column: location.start.column})
end = sourceMap.originalPositionFor({line: location.end.line, column: location.end.column})
if (start.line && end.line) {
b[index + ''].push(coverage.b[k][i])
branchMap[index + ''].locations.push({
start: {source: location.source, line: start.line, column: start.column},
end: {source: location.source, line: end.line, column: end.column}
})
}
}
index++
}
})

coverage.branchMap = branchMap
coverage.b = b
}

module.exports = SourceMapCache
Loading