diff --git a/lib/report.js b/lib/report.js index 1321f3ad..3b95194f 100644 --- a/lib/report.js +++ b/lib/report.js @@ -185,8 +185,8 @@ class Report { if (ext === '.js' || ext === '.ts' || ext === '.mjs') { const stat = statSync(fullPath) const sourceMap = getSourceMapFromFile(fullPath) - if (sourceMap !== undefined) { - this.sourceMapCache[`file://${fullPath}`] = { data: JSON.parse(readFileSync(sourceMap).toString()) } + if (sourceMap) { + this.sourceMapCache[`file://${fullPath}`] = { data: sourceMap } } emptyReports.push({ scriptId: 0, diff --git a/lib/source-map-from-file.js b/lib/source-map-from-file.js index 1911e11f..49b51fec 100644 --- a/lib/source-map-from-file.js +++ b/lib/source-map-from-file.js @@ -1,5 +1,34 @@ -const { isAbsolute, join, dirname } = require('path') +/* +* Copyright Node.js contributors. All rights reserved. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to +* deal in the Software without restriction, including without limitation the +* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +* sell copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +* IN THE SOFTWARE. +*/ +// TODO(bcoe): this logic is ported from Node.js' internal source map +// helpers: +// https://github.com/nodejs/node/blob/master/lib/internal/source_map/source_map_cache.js +// we should to upstream and downstream fixes. + const { readFileSync } = require('fs') +const { fileURLToPath, pathToFileURL } = require('url') +const util = require('util') +const debuglog = util.debuglog('c8') + /** * Extract the sourcemap url from a source file * reference: https://sourcemaps.info/spec.html @@ -7,18 +36,64 @@ const { readFileSync } = require('fs') * @returns {String} full path to source map file * @private */ -function getSourceMapFromFile (file) { - const fileBody = readFileSync(file).toString() - const sourceMapLineRE = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/mg +function getSourceMapFromFile (filename) { + const fileBody = readFileSync(filename).toString() + const sourceMapLineRE = /\/[*/]#\s+sourceMappingURL=(?[^\s]+)/ const results = fileBody.match(sourceMapLineRE) if (results !== null) { - const sourceMap = results[results.length - 1].split('=')[1] - if (isAbsolute(sourceMap)) { - return sourceMap.trim() - } else { - const base = dirname(file) - return join(base, sourceMap).trim() + const sourceMappingURL = results.groups.sourceMappingURL + const sourceMap = dataFromUrl(pathToFileURL(filename), sourceMappingURL) + return sourceMap + } else { + return null + } +} + +function dataFromUrl (sourceURL, sourceMappingURL) { + try { + const url = new URL(sourceMappingURL) + switch (url.protocol) { + case 'data:': + return sourceMapFromDataUrl(url.pathname) + default: + return null + } + } catch (err) { + debuglog(err) + // If no scheme is present, we assume we are dealing with a file path. + const mapURL = new URL(sourceMappingURL, sourceURL).href + return sourceMapFromFile(mapURL) + } +} + +function sourceMapFromFile (mapURL) { + try { + const content = readFileSync(fileURLToPath(mapURL), 'utf8') + return JSON.parse(content) + } catch (err) { + debuglog(err) + return null + } +} + +// data:[][;base64], see: +// https://tools.ietf.org/html/rfc2397#section-2 +function sourceMapFromDataUrl (url) { + const { 0: format, 1: data } = url.split(',') + const splitFormat = format.split(';') + const contentType = splitFormat[0] + const base64 = splitFormat[splitFormat.length - 1] === 'base64' + if (contentType === 'application/json') { + const decodedData = base64 ? Buffer.from(data, 'base64').toString('utf8') : data + try { + return JSON.parse(decodedData) + } catch (err) { + debuglog(err) + return null } + } else { + debuglog(`unexpected content-type ${contentType}`) + return null } } diff --git a/test/fixtures/source-maps/inline.js b/test/fixtures/source-maps/inline.js new file mode 100644 index 00000000..5d71df5b --- /dev/null +++ b/test/fixtures/source-maps/inline.js @@ -0,0 +1,2 @@ +var cov_263bu3eqm8=function(){var path= "./branches.js";var hash="424788076537d051b5bf0e2564aef393124eabc7";var global=new Function("return this")();var gcv="__coverage__";var coverageData={path: "./branches.js",statementMap:{"0":{start:{line:1,column:0},end:{line:7,column:1}},"1":{start:{line:2,column:2},end:{line:2,column:29}},"2":{start:{line:3,column:7},end:{line:7,column:1}},"3":{start:{line:4,column:2},end:{line:4,column:27}},"4":{start:{line:6,column:2},end:{line:6,column:29}},"5":{start:{line:10,column:2},end:{line:16,column:3}},"6":{start:{line:11,column:4},end:{line:11,column:28}},"7":{start:{line:12,column:9},end:{line:16,column:3}},"8":{start:{line:13,column:4},end:{line:13,column:31}},"9":{start:{line:15,column:4},end:{line:15,column:29}},"10":{start:{line:19,column:0},end:{line:19,column:12}},"11":{start:{line:20,column:0},end:{line:20,column:13}}},fnMap:{"0":{name:"branch",decl:{start:{line:9,column:9},end:{line:9,column:15}},loc:{start:{line:9,column:20},end:{line:17,column:1}},line:9}},branchMap:{"0":{loc:{start:{line:1,column:0},end:{line:7,column:1}},type:"if",locations:[{start:{line:1,column:0},end:{line:7,column:1}},{start:{line:1,column:0},end:{line:7,column:1}}],line:1},"1":{loc:{start:{line:3,column:7},end:{line:7,column:1}},type:"if",locations:[{start:{line:3,column:7},end:{line:7,column:1}},{start:{line:3,column:7},end:{line:7,column:1}}],line:3},"2":{loc:{start:{line:10,column:2},end:{line:16,column:3}},type:"if",locations:[{start:{line:10,column:2},end:{line:16,column:3}},{start:{line:10,column:2},end:{line:16,column:3}}],line:10},"3":{loc:{start:{line:12,column:9},end:{line:16,column:3}},type:"if",locations:[{start:{line:12,column:9},end:{line:16,column:3}},{start:{line:12,column:9},end:{line:16,column:3}}],line:12}},s:{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0},f:{"0":0},b:{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0]},_coverageSchema:"43e27e138ebf9cfc5966b082cf9a028302ed4184",hash:"424788076537d051b5bf0e2564aef393124eabc7"};var coverage=global[gcv]||(global[gcv]={});if(coverage[path]&&coverage[path].hash===hash){return coverage[path];}return coverage[path]=coverageData;}();cov_263bu3eqm8.s[0]++;if(false){cov_263bu3eqm8.b[0][0]++;cov_263bu3eqm8.s[1]++;console.info('unreachable');}else{cov_263bu3eqm8.b[0][1]++;cov_263bu3eqm8.s[2]++;if(true){cov_263bu3eqm8.b[1][0]++;cov_263bu3eqm8.s[3]++;console.info('reachable');}else{cov_263bu3eqm8.b[1][1]++;cov_263bu3eqm8.s[4]++;console.info('unreachable');}}function branch(a){cov_263bu3eqm8.f[0]++;cov_263bu3eqm8.s[5]++;if(a){cov_263bu3eqm8.b[2][0]++;cov_263bu3eqm8.s[6]++;console.info('a = true');}else{cov_263bu3eqm8.b[2][1]++;cov_263bu3eqm8.s[7]++;if(undefined){cov_263bu3eqm8.b[3][0]++;cov_263bu3eqm8.s[8]++;console.info('unreachable');}else{cov_263bu3eqm8.b[3][1]++;cov_263bu3eqm8.s[9]++;console.info('a = false');}}}cov_263bu3eqm8.s[10]++;branch(true);cov_263bu3eqm8.s[11]++;branch(false); +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4vYnJhbmNoZXMuanMiXSwibmFtZXMiOlsiY29uc29sZSIsImluZm8iLCJicmFuY2giLCJhIiwidW5kZWZpbmVkIl0sIm1hcHBpbmdzIjoic3VFQUFBLEdBQUksS0FBSixDQUFXLGdEQUNUQSxPQUFPLENBQUNDLElBQVIsQ0FBYSxhQUFiLEVBQ0QsQ0FGRCxJQUVPLG1EQUFJLElBQUosQ0FBVSxnREFDZkQsT0FBTyxDQUFDQyxJQUFSLENBQWEsV0FBYixFQUNELENBRk0sSUFFQSxnREFDTEQsT0FBTyxDQUFDQyxJQUFSLENBQWEsYUFBYixFQUNELEVBRUQsUUFBU0MsQ0FBQUEsTUFBVCxDQUFpQkMsQ0FBakIsQ0FBb0IsNkNBQ2xCLEdBQUlBLENBQUosQ0FBTyxnREFDTEgsT0FBTyxDQUFDQyxJQUFSLENBQWEsVUFBYixFQUNELENBRkQsSUFFTyxtREFBSUcsU0FBSixDQUFlLGdEQUNwQkosT0FBTyxDQUFDQyxJQUFSLENBQWEsYUFBYixFQUNELENBRk0sSUFFQSxnREFDTEQsT0FBTyxDQUFDQyxJQUFSLENBQWEsV0FBYixFQUNELEVBQ0YsQyx1QkFFREMsTUFBTSxDQUFDLElBQUQsQ0FBTixDLHVCQUNBQSxNQUFNLENBQUMsS0FBRCxDQUFOIiwic291cmNlc0NvbnRlbnQiOlsiaWYgKGZhbHNlKSB7XG4gIGNvbnNvbGUuaW5mbygndW5yZWFjaGFibGUnKVxufSBlbHNlIGlmICh0cnVlKSB7XG4gIGNvbnNvbGUuaW5mbygncmVhY2hhYmxlJylcbn0gZWxzZSB7XG4gIGNvbnNvbGUuaW5mbygndW5yZWFjaGFibGUnKVxufVxuXG5mdW5jdGlvbiBicmFuY2ggKGEpIHtcbiAgaWYgKGEpIHtcbiAgICBjb25zb2xlLmluZm8oJ2EgPSB0cnVlJylcbiAgfSBlbHNlIGlmICh1bmRlZmluZWQpIHtcbiAgICBjb25zb2xlLmluZm8oJ3VucmVhY2hhYmxlJylcbiAgfSBlbHNlIHtcbiAgICBjb25zb2xlLmluZm8oJ2EgPSBmYWxzZScpXG4gIH1cbn1cblxuYnJhbmNoKHRydWUpXG5icmFuY2goZmFsc2UpXG4iXX0= diff --git a/test/fixtures/source-maps/padded.js.map b/test/fixtures/source-maps/padded.js.map new file mode 100644 index 00000000..12d8c37b --- /dev/null +++ b/test/fixtures/source-maps/padded.js.map @@ -0,0 +1 @@ +{"version":3} diff --git a/test/integration.js_10.snap b/test/integration.js_10.snap index faee189c..64cdbf15 100644 --- a/test/integration.js_10.snap +++ b/test/integration.js_10.snap @@ -139,14 +139,14 @@ hey --------------------------|---------|----------|---------|---------|-------------------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s --------------------------|---------|----------|---------|---------|-------------------------------- -All files | 75.76 | 58.23 | 66.67 | 75.76 | +All files | 72.64 | 58.23 | 61.11 | 72.64 | bin | 78.85 | 60 | 66.67 | 78.85 | c8.js | 78.85 | 60 | 66.67 | 78.85 | 22,27-29,32-33,41-43,50-51 - lib | 80.75 | 51.85 | 83.33 | 80.75 | + lib | 75.95 | 51.85 | 71.43 | 75.95 | is-cjs-esm-bridge.js | 90 | 25 | 100 | 90 | 9 parse-args.js | 96.13 | 45.45 | 100 | 96.13 | 109-110,118-119,132-133 report.js | 75.35 | 58.82 | 83.33 | 75.35 | ...208,238-239,266-267,273-275 - source-map-from-file.js | 44 | 100 | 0 | 44 | 10-23 + source-map-from-file.js | 45 | 100 | 0 | 45 | 39-50,52-67,69-77,81-98 lib/commands | 44.44 | 75 | 16.67 | 44.44 | check-coverage.js | 21.31 | 100 | 0 | 21.31 | 9-11,14-27,30-44,46-61 report.js | 93.1 | 71.43 | 50 | 93.1 | 9-10 @@ -154,9 +154,9 @@ All files | 75.76 | 58.23 | 66.67 | 75.76 | async.js | 100 | 100 | 100 | 100 | normal.js | 75 | 66.67 | 33.33 | 75 | 14-16,18-20 --------------------------|---------|----------|---------|---------|-------------------------------- -,ERROR: Coverage for lines (75.76%) does not meet global threshold (101%) +,ERROR: Coverage for lines (72.64%) does not meet global threshold (101%) ERROR: Coverage for branches (58.23%) does not meet global threshold (82%) -ERROR: Coverage for statements (75.76%) does not meet global threshold (95%) +ERROR: Coverage for statements (72.64%) does not meet global threshold (95%) " `; @@ -177,8 +177,8 @@ ERROR: Coverage for branches (45.45%) does not meet threshold (82%) for lib/pars ERROR: Coverage for lines (75.35%) does not meet threshold (101%) for lib/report.js ERROR: Coverage for branches (58.82%) does not meet threshold (82%) for lib/report.js ERROR: Coverage for statements (75.35%) does not meet threshold (95%) for lib/report.js -ERROR: Coverage for lines (44%) does not meet threshold (101%) for lib/source-map-from-file.js -ERROR: Coverage for statements (44%) does not meet threshold (95%) for lib/source-map-from-file.js +ERROR: Coverage for lines (45%) does not meet threshold (101%) for lib/source-map-from-file.js +ERROR: Coverage for statements (45%) does not meet threshold (95%) for lib/source-map-from-file.js ERROR: Coverage for lines (100%) does not meet threshold (101%) for test/fixtures/async.js ERROR: Coverage for lines (75%) does not meet threshold (101%) for test/fixtures/normal.js ERROR: Coverage for branches (66.67%) does not meet threshold (82%) for test/fixtures/normal.js @@ -189,9 +189,9 @@ ERROR: Coverage for statements (75%) does not meet threshold (95%) for test/fixt exports[`c8 check-coverage exits with 0 if coverage within threshold 1`] = `",,"`; exports[`c8 check-coverage exits with 1 if coverage is below threshold 1`] = ` -",,ERROR: Coverage for lines (75.76%) does not meet global threshold (101%) +",,ERROR: Coverage for lines (72.64%) does not meet global threshold (101%) ERROR: Coverage for branches (58.23%) does not meet global threshold (82%) -ERROR: Coverage for statements (75.76%) does not meet global threshold (95%) +ERROR: Coverage for statements (72.64%) does not meet global threshold (95%) " `; @@ -274,14 +274,14 @@ exports[`c8 report generates report from existing temporary files 1`] = ` ",--------------------------|---------|----------|---------|---------|-------------------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s --------------------------|---------|----------|---------|---------|-------------------------------- -All files | 75.76 | 58.23 | 66.67 | 75.76 | +All files | 72.64 | 58.23 | 61.11 | 72.64 | bin | 78.85 | 60 | 66.67 | 78.85 | c8.js | 78.85 | 60 | 66.67 | 78.85 | 22,27-29,32-33,41-43,50-51 - lib | 80.75 | 51.85 | 83.33 | 80.75 | + lib | 75.95 | 51.85 | 71.43 | 75.95 | is-cjs-esm-bridge.js | 90 | 25 | 100 | 90 | 9 parse-args.js | 96.13 | 45.45 | 100 | 96.13 | 109-110,118-119,132-133 report.js | 75.35 | 58.82 | 83.33 | 75.35 | ...208,238-239,266-267,273-275 - source-map-from-file.js | 44 | 100 | 0 | 44 | 10-23 + source-map-from-file.js | 45 | 100 | 0 | 45 | 39-50,52-67,69-77,81-98 lib/commands | 44.44 | 75 | 16.67 | 44.44 | check-coverage.js | 21.31 | 100 | 0 | 21.31 | 9-11,14-27,30-44,46-61 report.js | 93.1 | 71.43 | 50 | 93.1 | 9-10 @@ -296,14 +296,14 @@ exports[`c8 report supports --check-coverage, when generating reports 1`] = ` ",--------------------------|---------|----------|---------|---------|-------------------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s --------------------------|---------|----------|---------|---------|-------------------------------- -All files | 75.76 | 58.23 | 66.67 | 75.76 | +All files | 72.64 | 58.23 | 61.11 | 72.64 | bin | 78.85 | 60 | 66.67 | 78.85 | c8.js | 78.85 | 60 | 66.67 | 78.85 | 22,27-29,32-33,41-43,50-51 - lib | 80.75 | 51.85 | 83.33 | 80.75 | + lib | 75.95 | 51.85 | 71.43 | 75.95 | is-cjs-esm-bridge.js | 90 | 25 | 100 | 90 | 9 parse-args.js | 96.13 | 45.45 | 100 | 96.13 | 109-110,118-119,132-133 report.js | 75.35 | 58.82 | 83.33 | 75.35 | ...208,238-239,266-267,273-275 - source-map-from-file.js | 44 | 100 | 0 | 44 | 10-23 + source-map-from-file.js | 45 | 100 | 0 | 45 | 39-50,52-67,69-77,81-98 lib/commands | 44.44 | 75 | 16.67 | 44.44 | check-coverage.js | 21.31 | 100 | 0 | 21.31 | 9-11,14-27,30-44,46-61 report.js | 93.1 | 71.43 | 50 | 93.1 | 9-10 @@ -311,9 +311,9 @@ All files | 75.76 | 58.23 | 66.67 | 75.76 | async.js | 100 | 100 | 100 | 100 | normal.js | 75 | 66.67 | 33.33 | 75 | 14-16,18-20 --------------------------|---------|----------|---------|---------|-------------------------------- -,ERROR: Coverage for lines (75.76%) does not meet global threshold (101%) +,ERROR: Coverage for lines (72.64%) does not meet global threshold (101%) ERROR: Coverage for branches (58.23%) does not meet global threshold (82%) -ERROR: Coverage for statements (75.76%) does not meet global threshold (95%) +ERROR: Coverage for statements (72.64%) does not meet global threshold (95%) " `; diff --git a/test/source-map-from-file.js b/test/source-map-from-file.js index 172cd316..c624dbce 100644 --- a/test/source-map-from-file.js +++ b/test/source-map-from-file.js @@ -1,14 +1,19 @@ /* global describe, it */ const getSourceMapFromFile = require('../lib/source-map-from-file') const assert = require('assert') -const path = require('path') +const { readFileSync } = require('fs') describe('source-map-from-file', () => { it('should parse source maps from compiled targets', () => { const sourceMap = getSourceMapFromFile('./test/fixtures/all/ts-compiled/main.js') - assert.strictEqual(sourceMap, ['test', 'fixtures', 'all', 'ts-compiled', 'main.js.map'].join(path.sep)) + const expected = JSON.parse(readFileSync(require.resolve('./fixtures/all/ts-compiled/main.js.map'), 'utf8')) + assert.deepStrictEqual(sourceMap, expected) }) it('should handle extra whitespace characters', () => { const sourceMap = getSourceMapFromFile('./test/fixtures/source-maps/padded.js') - assert.strictEqual(sourceMap, ['test', 'fixtures', 'source-maps', 'padded.js.map'].join(path.sep)) + assert.deepStrictEqual(sourceMap, { version: 3 }) + }) + it('should support base64 encoded inline source maps', () => { + const sourceMap = getSourceMapFromFile('./test/fixtures/source-maps/inline.js') + assert.strictEqual(sourceMap.version, 3) }) })