diff --git a/.travis.yml b/.travis.yml index 5aa65931..efcb002f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,5 @@ os: - osx - windows node_js: - - "12" + - "13" after_success: npm run coverage diff --git a/lib/report.js b/lib/report.js index 5746dc0f..8aad24ff 100644 --- a/lib/report.js +++ b/lib/report.js @@ -32,6 +32,7 @@ class Report { include: include }) this.omitRelative = omitRelative + this.sourceMapCache = {} this.wrapperLength = wrapperLength } @@ -63,8 +64,9 @@ class Report { for (const v8ScriptCov of v8ProcessCov.result) { try { + const sources = this._getSourceMap(v8ScriptCov) const path = resolve(this.resolve, v8ScriptCov.url) - const converter = v8toIstanbul(path, this.wrapperLength) + const converter = v8toIstanbul(path, this.wrapperLength, sources) await converter.load() if (resultCountPerPath.has(path)) { @@ -98,6 +100,34 @@ class Report { return this._allCoverageFiles } + /** + * Returns source-map and fake source file, if cached during Node.js' + * execution. This is used to support tools like ts-node, which transpile + * using runtime hooks. + * + * Note: requires Node.js 13+ + * + * @return {Object} sourceMap and fake source file (created from line #s). + * @private + */ + _getSourceMap (v8ScriptCov) { + const sources = {} + if (this.sourceMapCache[`file://${v8ScriptCov.url}`]) { + const sourceMapAndLineLengths = this.sourceMapCache[`file://${v8ScriptCov.url}`] + sources.sourceMap = { + sourcemap: sourceMapAndLineLengths.data + } + if (sourceMapAndLineLengths.lineLengths) { + let source = '' + sourceMapAndLineLengths.lineLengths.forEach(length => { + source += `${''.padEnd(length, '.')}\n` + }) + sources.source = source + } + } + return sources + } + /** * Returns the merged V8 process coverage. * @@ -111,6 +141,9 @@ class Report { const v8ProcessCovs = [] for (const v8ProcessCov of this._loadReports()) { if (this._isCoverageObject(v8ProcessCov)) { + if (v8ProcessCov['source-map-cache']) { + Object.assign(this.sourceMapCache, v8ProcessCov['source-map-cache']) + } v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov)) } } diff --git a/package-lock.json b/package-lock.json index cc38d786..c22c867b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,6 +99,12 @@ "color-convert": "^1.9.0" } }, + "arg": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz", + "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2592,6 +2598,12 @@ } } }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -3718,6 +3730,16 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -4270,6 +4292,27 @@ "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", "dev": true }, + "ts-node": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.4.1.tgz", + "integrity": "sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + } + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", @@ -4318,6 +4361,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", + "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "dev": true + }, "uglify-js": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.3.tgz", @@ -4362,9 +4411,9 @@ "dev": true }, "v8-to-istanbul": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-3.2.3.tgz", - "integrity": "sha512-B8d/oxMtc/x0TYXr9b+Ywu5KexA/on4QMQ9M1kTYnoGZzKdo8LLk9ySlWePdDOtr2G0/2Injgcp3sOR9gU+3vQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-3.2.6.tgz", + "integrity": "sha512-M6zzkVjsr+6sFdWPCuq7fjg9oCOXlssin05Yhobt9jMqHlEhw8AQ4/ClDiLCVWzXjpS2ezik53mhgSivw0XwmQ==", "requires": { "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^1.6.0", @@ -4684,6 +4733,12 @@ } } } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/package.json b/package.json index c7dbb228..7ad892bc 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "istanbul-reports": "^2.2.6", "rimraf": "^3.0.0", "test-exclude": "^5.2.3", - "v8-to-istanbul": "^3.2.3", + "v8-to-istanbul": "^3.2.6", "yargs": "^14.0.0", "yargs-parser": "^14.0.0" }, @@ -51,7 +51,9 @@ "coveralls": "^3.0.6", "mocha": "^6.2.0", "standard": "^14.1.0", - "standard-version": "^7.0.0" + "standard-version": "^7.0.0", + "ts-node": "^8.4.1", + "typescript": "^3.6.4" }, "engines": { "node": ">=10.12.0" diff --git a/test/fixtures/source-maps/branches/branches.uglify.js.map b/test/fixtures/source-maps/branches/branches.uglify.js.map index d90c5817..6fb5132f 100644 --- a/test/fixtures/source-maps/branches/branches.uglify.js.map +++ b/test/fixtures/source-maps/branches/branches.uglify.js.map @@ -1 +1 @@ -{"version":3,"sources":["branches.js"],"names":["console","info","branch","a","undefined"],"mappings":"AAAA,GAAI,MAAO,CACTA,QAAQC,KAAK,oBACR,GAAI,KAAM,CACfD,QAAQC,KAAK,iBACR,CACLD,QAAQC,KAAK,eAGf,SAASC,OAAQC,GACf,GAAIA,EAAG,CACLH,QAAQC,KAAK,iBACR,GAAIG,UAAW,CACpBJ,QAAQC,KAAK,mBACR,CACLD,QAAQC,KAAK,cAIjBC,OAAO,MACPA,OAAO","sourceRoot":"./"} \ No newline at end of file +{"version":3,"sources":["branches.js"],"names":["console","info","branch","a","undefined"],"mappings":"AAAA,GAAI,MAAO,CACTA,QAAQC,KAAK,oBACR,GAAI,KAAM,CACfD,QAAQC,KAAK,iBACR,CACLD,QAAQC,KAAK,eAGf,SAASC,OAAQC,GACf,GAAIA,EAAG,CACLH,QAAQC,KAAK,iBACR,GAAIG,UAAW,CACpBJ,QAAQC,KAAK,mBACR,CACLD,QAAQC,KAAK,cAIjBC,OAAO,MACPA,OAAO","sourceRoot":""} \ No newline at end of file diff --git a/test/fixtures/source-maps/classes/classes.uglify.js.map b/test/fixtures/source-maps/classes/classes.uglify.js.map index 6d043748..2a5f26b3 100644 --- a/test/fixtures/source-maps/classes/classes.uglify.js.map +++ b/test/fixtures/source-maps/classes/classes.uglify.js.map @@ -1 +1 @@ -{"version":3,"sources":["classes.js"],"names":["Foo","[object Object]","x","this","console","info","methodC","a","b","methodA"],"mappings":"MAAMA,IACJC,YAAaC,EAAE,IACbC,KAAKD,EAAIA,EAAIA,EAAI,GACjB,GAAIC,KAAKD,EAAG,CACVE,QAAQC,KAAK,eACR,CACLD,QAAQC,KAAK,aAEfF,KAAKG,UAEPL,UACEG,QAAQC,KAAK,WAEfJ,UACEG,QAAQC,KAAK,aAEfJ,UACEG,QAAQC,KAAK,WAEfJ,UACEG,QAAQC,KAAK,cAIjB,MAAME,EAAI,IAAIP,IAAI,GAClB,MAAMQ,EAAI,IAAIR,IAAI,IAClBO,EAAEE","sourceRoot":"./classes.uglify.js"} \ No newline at end of file +{"version":3,"sources":["classes.js"],"names":["Foo","[object Object]","x","this","console","info","methodC","a","b","methodA"],"mappings":"MAAMA,IACJC,YAAaC,EAAE,IACbC,KAAKD,EAAIA,EAAIA,EAAI,GACjB,GAAIC,KAAKD,EAAG,CACVE,QAAQC,KAAK,eACR,CACLD,QAAQC,KAAK,aAEfF,KAAKG,UAEPL,UACEG,QAAQC,KAAK,WAEfJ,UACEG,QAAQC,KAAK,aAEfJ,UACEG,QAAQC,KAAK,WAEfJ,UACEG,QAAQC,KAAK,cAIjB,MAAME,EAAI,IAAIP,IAAI,GAClB,MAAMQ,EAAI,IAAIR,IAAI,IAClBO,EAAEE","sourceRoot":""} \ No newline at end of file diff --git a/test/fixtures/ts-node-basic.ts b/test/fixtures/ts-node-basic.ts new file mode 100644 index 00000000..780488c2 --- /dev/null +++ b/test/fixtures/ts-node-basic.ts @@ -0,0 +1,34 @@ +export interface FooOptions { + x: number; +} + +class Foo { + x: number; + constructor (options: FooOptions) { + this.x = options.x ? options.x : 99 + if (this.x) { + console.info('covered') + } else { + console.info('uncovered') + } + this.methodC() + } + methodA (): number { + console.info('covered') + return 33 + } + /* c8 ignore next 3 */ + methodB () { + console.info('uncovered') + } + private methodC () { + console.info('covered') + } + methodD () { + console.info('uncovered') + } +} + +const a = new Foo({x: 0}) +const b = new Foo({x: 33}) +a.methodA() diff --git a/test/integration.js b/test/integration.js index 524674ed..7caaf3c1 100644 --- a/test/integration.js +++ b/test/integration.js @@ -311,4 +311,20 @@ describe('c8', () => { }) }) }) + + describe('ts-node', () => { + beforeEach(cb => rimraf('tmp/source-map', cb)) + + it('reads source-map from cache, and applies to coverage', () => { + const { output } = spawnSync(nodePath, [ + c8Path, + '--exclude="test/*.js"', + '--temp-directory=tmp/source-map', + '--clean=true', + './node_modules/.bin/ts-node', + require.resolve('./fixtures/ts-node-basic.ts') + ]) + output.toString('utf8').should.matchSnapshot() + }) + }) }) diff --git a/test/integration.js.snap b/test/integration.js.snap index 1c45ccbd..b623cec5 100644 --- a/test/integration.js.snap +++ b/test/integration.js.snap @@ -219,3 +219,18 @@ All files | 83.33 | 85.71 | 60 | 83.33 | | -----------|----------|----------|----------|----------|-------------------| ," `; + +exports[`c8 ts-node reads source-map from cache, and applies to coverage 1`] = ` +",covered +covered +covered +covered +covered +------------------|----------|----------|----------|----------|-------------------| +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | +------------------|----------|----------|----------|----------|-------------------| +All files | 88.24 | 87.5 | 80 | 88.24 | | + ts-node-basic.ts | 88.24 | 87.5 | 80 | 88.24 | 12,13,28,29 | +------------------|----------|----------|----------|----------|-------------------| +," +`;