Skip to content

Commit 31488f5

Browse files
authored
feat: adds support for source-map production (#439)
1 parent 4d15610 commit 31488f5

File tree

7 files changed

+98
-13
lines changed

7 files changed

+98
-13
lines changed

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ and a `text-lcov` coverage report.
5151
nyc --reporter=lcov --reporter=text-lcov npm test
5252
```
5353

54+
## Accurate stack traces using source maps
55+
56+
When `produce-source-map` is set to true, then the instrumented source files will
57+
include inline source maps for the instrumenter transform. When combined with
58+
[source-map-support](https://github.com/evanw/node-source-map-support),
59+
stack traces for instrumented code will reflect their original lines.
60+
5461
## Support for custom require hooks (babel, webpack, etc.)
5562

5663
nyc supports custom require hooks like

index.js

+9-6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ if (/index\.covered\.js$/.test(__filename)) {
3939

4040
function NYC (config) {
4141
config = config || {}
42+
this.config = config
4243

4344
this.subprocessBin = config.subprocessBin || path.resolve(__dirname, './bin/nyc.js')
4445
this._tempDirectory = config.tempDirectory || './.nyc_output'
@@ -84,8 +85,6 @@ function NYC (config) {
8485

8586
this.processInfo = new ProcessInfo(config && config._processInfo)
8687
this.rootId = this.processInfo.root || this.generateUniqueID()
87-
this.instrument = config.instrument
88-
this.all = config.all
8988
}
9089

9190
NYC.prototype._createTransform = function (ext) {
@@ -128,7 +127,9 @@ NYC.prototype.instrumenter = function () {
128127
}
129128

130129
NYC.prototype._createInstrumenter = function () {
131-
return this._instrumenterLib(this.cwd)
130+
return this._instrumenterLib(this.cwd, {
131+
produceSourceMap: this.config.produceSourceMap
132+
})
132133
}
133134

134135
NYC.prototype.addFile = function (filename) {
@@ -261,14 +262,15 @@ NYC.prototype._transformFactory = function (cacheDir) {
261262

262263
return function (code, metadata, hash) {
263264
var filename = metadata.filename
265+
var sourceMap = null
264266

265-
if (_this._sourceMap) _this._handleSourceMap(cacheDir, code, hash, filename)
267+
if (_this._sourceMap) sourceMap = _this._handleSourceMap(cacheDir, code, hash, filename)
266268

267269
try {
268-
instrumented = instrumenter.instrumentSync(code, filename)
270+
instrumented = instrumenter.instrumentSync(code, filename, sourceMap)
269271
} catch (e) {
270272
// don't fail external tests due to instrumentation bugs.
271-
console.warn('failed to instrument', filename, 'with error:', e.message)
273+
console.warn('failed to instrument', filename, 'with error:', e.stack)
272274
instrumented = code
273275
}
274276

@@ -290,6 +292,7 @@ NYC.prototype._handleSourceMap = function (cacheDir, code, hash, filename) {
290292
this.sourceMapCache.registerMap(filename, sourceMap.sourcemap)
291293
}
292294
}
295+
return sourceMap
293296
}
294297

295298
NYC.prototype._handleJs = function (code, filename) {

lib/config-util.js

+5
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ Config.buildYargs = function (cwd) {
168168
type: 'boolean',
169169
description: 'should nyc detect and handle source maps?'
170170
})
171+
.option('produce-source-map', {
172+
default: false,
173+
type: 'boolean',
174+
description: "should nyc's instrumenter produce source maps?"
175+
})
171176
.option('instrument', {
172177
default: true,
173178
type: 'boolean',

lib/instrumenters/istanbul.js

+34-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,43 @@
1-
function InstrumenterIstanbul (cwd) {
2-
var istanbul = InstrumenterIstanbul.istanbul()
1+
'use strict'
2+
3+
var convertSourceMap = require('convert-source-map')
4+
var mergeSourceMap = require('merge-source-map')
35

4-
return istanbul.createInstrumenter({
6+
function InstrumenterIstanbul (cwd, options) {
7+
var istanbul = InstrumenterIstanbul.istanbul()
8+
var instrumenter = istanbul.createInstrumenter({
59
autoWrap: true,
610
coverageVariable: '__coverage__',
711
embedSource: true,
812
noCompact: false,
9-
preserveComments: true
13+
preserveComments: true,
14+
produceSourceMap: options.produceSourceMap
1015
})
16+
17+
return {
18+
instrumentSync: function (code, filename, sourceMap) {
19+
var instrumented = instrumenter.instrumentSync(code, filename)
20+
// the instrumenter can optionally produce source maps,
21+
// this is useful for features like remapping stack-traces.
22+
// TODO: test source-map merging logic.
23+
if (options.produceSourceMap) {
24+
var lastSourceMap = instrumenter.lastSourceMap()
25+
if (lastSourceMap) {
26+
if (sourceMap) {
27+
lastSourceMap = mergeSourceMap(
28+
sourceMap.toObject(),
29+
lastSourceMap
30+
)
31+
}
32+
instrumented += '\n' + convertSourceMap.fromObject(lastSourceMap).toComment()
33+
}
34+
}
35+
return instrumented
36+
},
37+
lastFileCoverage: function () {
38+
return instrumenter.lastFileCoverage()
39+
}
40+
}
1141
}
1242

1343
InstrumenterIstanbul.istanbul = function () {

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"istanbul-lib-source-maps": "^1.0.2",
8989
"istanbul-reports": "^1.0.0",
9090
"md5-hex": "^1.2.0",
91+
"merge-source-map": "^1.0.2",
9192
"micromatch": "^2.3.11",
9293
"mkdirp": "^0.5.0",
9394
"resolve-from": "^2.0.0",
@@ -111,8 +112,8 @@
111112
"requirejs": "^2.3.0",
112113
"sanitize-filename": "^1.5.3",
113114
"sinon": "^1.15.3",
114-
"source-map-support": "^0.4.2",
115115
"split-lines": "^1.0.0",
116+
"source-map-support": "^0.4.6",
116117
"standard": "^8.0.0",
117118
"standard-version": "^3.0.0",
118119
"tap": "^8.0.0",
@@ -140,6 +141,7 @@
140141
"istanbul-lib-source-maps",
141142
"istanbul-reports",
142143
"md5-hex",
144+
"merge-source-map",
143145
"micromatch",
144146
"mkdirp",
145147
"resolve-from",
@@ -155,4 +157,4 @@
155157
"find-up"
156158
]
157159
}
158-
}
160+
}

test/fixtures/stack-trace.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
function blah() {
4+
throw new Error('Blarrh')
5+
}
6+
7+
var stack;
8+
try {
9+
blah();
10+
} catch(err) {
11+
stack = err.stack;
12+
}
13+
14+
module.exports = function() {
15+
return stack;
16+
}

test/src/nyc-test.js

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* global describe, it */
22

3-
require('source-map-support').install()
3+
require('source-map-support').install({hookRequire: true})
44
var _ = require('lodash')
55
var ap = require('any-path')
66
var configUtil = require('../../lib/config-util')
@@ -202,6 +202,28 @@ describe('nyc', function () {
202202
})
203203
})
204204

205+
describe('produce source map', function () {
206+
it('handles stack traces', function () {
207+
var nyc = new NYC(configUtil.loadConfig('--produce-source-map'))
208+
nyc.reset()
209+
nyc.wrap()
210+
211+
var check = require('../fixtures/stack-trace')
212+
check().should.match(/stack-trace.js:4:/)
213+
})
214+
215+
it('does not handle stack traces when disabled', function () {
216+
var nyc = new NYC(configUtil.loadConfig())
217+
nyc.reset()
218+
nyc.wrap()
219+
220+
var check = require('../fixtures/stack-trace')
221+
check().should.match(/stack-trace.js:1:/)
222+
})
223+
224+
// TODO: add test for merge source-map logic.
225+
})
226+
205227
describe('compile handlers for custom extensions are assigned', function () {
206228
it('assigns a function to custom extensions', function () {
207229
var nyc = new NYC(configUtil.loadConfig([],

0 commit comments

Comments
 (0)