Skip to content

Commit 3f12dd4

Browse files
author
Benjamin E. Coe
authored
feat(all): handle base64 inline source maps (bcoe#283)
Pulls in Source Map handling logic from Node.js
1 parent 7249293 commit 3f12dd4

6 files changed

Lines changed: 115 additions & 32 deletions

File tree

lib/report.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@ class Report {
185185
if (ext === '.js' || ext === '.ts' || ext === '.mjs') {
186186
const stat = statSync(fullPath)
187187
const sourceMap = getSourceMapFromFile(fullPath)
188-
if (sourceMap !== undefined) {
189-
this.sourceMapCache[`file://${fullPath}`] = { data: JSON.parse(readFileSync(sourceMap).toString()) }
188+
if (sourceMap) {
189+
this.sourceMapCache[`file://${fullPath}`] = { data: sourceMap }
190190
}
191191
emptyReports.push({
192192
scriptId: 0,

lib/source-map-from-file.js

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,99 @@
1-
const { isAbsolute, join, dirname } = require('path')
1+
/*
2+
* Copyright Node.js contributors. All rights reserved.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to
6+
* deal in the Software without restriction, including without limitation the
7+
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8+
* sell copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20+
* IN THE SOFTWARE.
21+
*/
22+
// TODO(bcoe): this logic is ported from Node.js' internal source map
23+
// helpers:
24+
// https://github.com/nodejs/node/blob/master/lib/internal/source_map/source_map_cache.js
25+
// we should to upstream and downstream fixes.
26+
227
const { readFileSync } = require('fs')
28+
const { fileURLToPath, pathToFileURL } = require('url')
29+
const util = require('util')
30+
const debuglog = util.debuglog('c8')
31+
332
/**
433
* Extract the sourcemap url from a source file
534
* reference: https://sourcemaps.info/spec.html
635
* @param {String} file - compilation target file
736
* @returns {String} full path to source map file
837
* @private
938
*/
10-
function getSourceMapFromFile (file) {
11-
const fileBody = readFileSync(file).toString()
12-
const sourceMapLineRE = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/mg
39+
function getSourceMapFromFile (filename) {
40+
const fileBody = readFileSync(filename).toString()
41+
const sourceMapLineRE = /\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/
1342
const results = fileBody.match(sourceMapLineRE)
1443
if (results !== null) {
15-
const sourceMap = results[results.length - 1].split('=')[1]
16-
if (isAbsolute(sourceMap)) {
17-
return sourceMap.trim()
18-
} else {
19-
const base = dirname(file)
20-
return join(base, sourceMap).trim()
44+
const sourceMappingURL = results.groups.sourceMappingURL
45+
const sourceMap = dataFromUrl(pathToFileURL(filename), sourceMappingURL)
46+
return sourceMap
47+
} else {
48+
return null
49+
}
50+
}
51+
52+
function dataFromUrl (sourceURL, sourceMappingURL) {
53+
try {
54+
const url = new URL(sourceMappingURL)
55+
switch (url.protocol) {
56+
case 'data:':
57+
return sourceMapFromDataUrl(url.pathname)
58+
default:
59+
return null
60+
}
61+
} catch (err) {
62+
debuglog(err)
63+
// If no scheme is present, we assume we are dealing with a file path.
64+
const mapURL = new URL(sourceMappingURL, sourceURL).href
65+
return sourceMapFromFile(mapURL)
66+
}
67+
}
68+
69+
function sourceMapFromFile (mapURL) {
70+
try {
71+
const content = readFileSync(fileURLToPath(mapURL), 'utf8')
72+
return JSON.parse(content)
73+
} catch (err) {
74+
debuglog(err)
75+
return null
76+
}
77+
}
78+
79+
// data:[<mediatype>][;base64],<data> see:
80+
// https://tools.ietf.org/html/rfc2397#section-2
81+
function sourceMapFromDataUrl (url) {
82+
const { 0: format, 1: data } = url.split(',')
83+
const splitFormat = format.split(';')
84+
const contentType = splitFormat[0]
85+
const base64 = splitFormat[splitFormat.length - 1] === 'base64'
86+
if (contentType === 'application/json') {
87+
const decodedData = base64 ? Buffer.from(data, 'base64').toString('utf8') : data
88+
try {
89+
return JSON.parse(decodedData)
90+
} catch (err) {
91+
debuglog(err)
92+
return null
2193
}
94+
} else {
95+
debuglog(`unexpected content-type ${contentType}`)
96+
return null
2297
}
2398
}
2499

test/fixtures/source-maps/inline.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/source-maps/padded.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/integration.js_10.snap

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -139,24 +139,24 @@ hey
139139
--------------------------|---------|----------|---------|---------|--------------------------------
140140
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
141141
--------------------------|---------|----------|---------|---------|--------------------------------
142-
All files | 75.76 | 58.23 | 66.67 | 75.76 |
142+
All files | 72.64 | 58.23 | 61.11 | 72.64 |
143143
bin | 78.85 | 60 | 66.67 | 78.85 |
144144
c8.js | 78.85 | 60 | 66.67 | 78.85 | 22,27-29,32-33,41-43,50-51
145-
lib | 80.75 | 51.85 | 83.33 | 80.75 |
145+
lib | 75.95 | 51.85 | 71.43 | 75.95 |
146146
is-cjs-esm-bridge.js | 90 | 25 | 100 | 90 | 9
147147
parse-args.js | 96.13 | 45.45 | 100 | 96.13 | 109-110,118-119,132-133
148148
report.js | 75.35 | 58.82 | 83.33 | 75.35 | ...208,238-239,266-267,273-275
149-
source-map-from-file.js | 44 | 100 | 0 | 44 | 10-23
149+
source-map-from-file.js | 45 | 100 | 0 | 45 | 39-50,52-67,69-77,81-98
150150
lib/commands | 44.44 | 75 | 16.67 | 44.44 |
151151
check-coverage.js | 21.31 | 100 | 0 | 21.31 | 9-11,14-27,30-44,46-61
152152
report.js | 93.1 | 71.43 | 50 | 93.1 | 9-10
153153
test/fixtures | 83.33 | 85.71 | 66.67 | 83.33 |
154154
async.js | 100 | 100 | 100 | 100 |
155155
normal.js | 75 | 66.67 | 33.33 | 75 | 14-16,18-20
156156
--------------------------|---------|----------|---------|---------|--------------------------------
157-
,ERROR: Coverage for lines (75.76%) does not meet global threshold (101%)
157+
,ERROR: Coverage for lines (72.64%) does not meet global threshold (101%)
158158
ERROR: Coverage for branches (58.23%) does not meet global threshold (82%)
159-
ERROR: Coverage for statements (75.76%) does not meet global threshold (95%)
159+
ERROR: Coverage for statements (72.64%) does not meet global threshold (95%)
160160
"
161161
`;
162162

@@ -177,8 +177,8 @@ ERROR: Coverage for branches (45.45%) does not meet threshold (82%) for lib/pars
177177
ERROR: Coverage for lines (75.35%) does not meet threshold (101%) for lib/report.js
178178
ERROR: Coverage for branches (58.82%) does not meet threshold (82%) for lib/report.js
179179
ERROR: Coverage for statements (75.35%) does not meet threshold (95%) for lib/report.js
180-
ERROR: Coverage for lines (44%) does not meet threshold (101%) for lib/source-map-from-file.js
181-
ERROR: Coverage for statements (44%) does not meet threshold (95%) for lib/source-map-from-file.js
180+
ERROR: Coverage for lines (45%) does not meet threshold (101%) for lib/source-map-from-file.js
181+
ERROR: Coverage for statements (45%) does not meet threshold (95%) for lib/source-map-from-file.js
182182
ERROR: Coverage for lines (100%) does not meet threshold (101%) for test/fixtures/async.js
183183
ERROR: Coverage for lines (75%) does not meet threshold (101%) for test/fixtures/normal.js
184184
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
189189
exports[`c8 check-coverage exits with 0 if coverage within threshold 1`] = `",,"`;
190190

191191
exports[`c8 check-coverage exits with 1 if coverage is below threshold 1`] = `
192-
",,ERROR: Coverage for lines (75.76%) does not meet global threshold (101%)
192+
",,ERROR: Coverage for lines (72.64%) does not meet global threshold (101%)
193193
ERROR: Coverage for branches (58.23%) does not meet global threshold (82%)
194-
ERROR: Coverage for statements (75.76%) does not meet global threshold (95%)
194+
ERROR: Coverage for statements (72.64%) does not meet global threshold (95%)
195195
"
196196
`;
197197

@@ -274,14 +274,14 @@ exports[`c8 report generates report from existing temporary files 1`] = `
274274
",--------------------------|---------|----------|---------|---------|--------------------------------
275275
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
276276
--------------------------|---------|----------|---------|---------|--------------------------------
277-
All files | 75.76 | 58.23 | 66.67 | 75.76 |
277+
All files | 72.64 | 58.23 | 61.11 | 72.64 |
278278
bin | 78.85 | 60 | 66.67 | 78.85 |
279279
c8.js | 78.85 | 60 | 66.67 | 78.85 | 22,27-29,32-33,41-43,50-51
280-
lib | 80.75 | 51.85 | 83.33 | 80.75 |
280+
lib | 75.95 | 51.85 | 71.43 | 75.95 |
281281
is-cjs-esm-bridge.js | 90 | 25 | 100 | 90 | 9
282282
parse-args.js | 96.13 | 45.45 | 100 | 96.13 | 109-110,118-119,132-133
283283
report.js | 75.35 | 58.82 | 83.33 | 75.35 | ...208,238-239,266-267,273-275
284-
source-map-from-file.js | 44 | 100 | 0 | 44 | 10-23
284+
source-map-from-file.js | 45 | 100 | 0 | 45 | 39-50,52-67,69-77,81-98
285285
lib/commands | 44.44 | 75 | 16.67 | 44.44 |
286286
check-coverage.js | 21.31 | 100 | 0 | 21.31 | 9-11,14-27,30-44,46-61
287287
report.js | 93.1 | 71.43 | 50 | 93.1 | 9-10
@@ -296,24 +296,24 @@ exports[`c8 report supports --check-coverage, when generating reports 1`] = `
296296
",--------------------------|---------|----------|---------|---------|--------------------------------
297297
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
298298
--------------------------|---------|----------|---------|---------|--------------------------------
299-
All files | 75.76 | 58.23 | 66.67 | 75.76 |
299+
All files | 72.64 | 58.23 | 61.11 | 72.64 |
300300
bin | 78.85 | 60 | 66.67 | 78.85 |
301301
c8.js | 78.85 | 60 | 66.67 | 78.85 | 22,27-29,32-33,41-43,50-51
302-
lib | 80.75 | 51.85 | 83.33 | 80.75 |
302+
lib | 75.95 | 51.85 | 71.43 | 75.95 |
303303
is-cjs-esm-bridge.js | 90 | 25 | 100 | 90 | 9
304304
parse-args.js | 96.13 | 45.45 | 100 | 96.13 | 109-110,118-119,132-133
305305
report.js | 75.35 | 58.82 | 83.33 | 75.35 | ...208,238-239,266-267,273-275
306-
source-map-from-file.js | 44 | 100 | 0 | 44 | 10-23
306+
source-map-from-file.js | 45 | 100 | 0 | 45 | 39-50,52-67,69-77,81-98
307307
lib/commands | 44.44 | 75 | 16.67 | 44.44 |
308308
check-coverage.js | 21.31 | 100 | 0 | 21.31 | 9-11,14-27,30-44,46-61
309309
report.js | 93.1 | 71.43 | 50 | 93.1 | 9-10
310310
test/fixtures | 83.33 | 85.71 | 66.67 | 83.33 |
311311
async.js | 100 | 100 | 100 | 100 |
312312
normal.js | 75 | 66.67 | 33.33 | 75 | 14-16,18-20
313313
--------------------------|---------|----------|---------|---------|--------------------------------
314-
,ERROR: Coverage for lines (75.76%) does not meet global threshold (101%)
314+
,ERROR: Coverage for lines (72.64%) does not meet global threshold (101%)
315315
ERROR: Coverage for branches (58.23%) does not meet global threshold (82%)
316-
ERROR: Coverage for statements (75.76%) does not meet global threshold (95%)
316+
ERROR: Coverage for statements (72.64%) does not meet global threshold (95%)
317317
"
318318
`;
319319

test/source-map-from-file.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
/* global describe, it */
22
const getSourceMapFromFile = require('../lib/source-map-from-file')
33
const assert = require('assert')
4-
const path = require('path')
4+
const { readFileSync } = require('fs')
55
describe('source-map-from-file', () => {
66
it('should parse source maps from compiled targets', () => {
77
const sourceMap = getSourceMapFromFile('./test/fixtures/all/ts-compiled/main.js')
8-
assert.strictEqual(sourceMap, ['test', 'fixtures', 'all', 'ts-compiled', 'main.js.map'].join(path.sep))
8+
const expected = JSON.parse(readFileSync(require.resolve('./fixtures/all/ts-compiled/main.js.map'), 'utf8'))
9+
assert.deepStrictEqual(sourceMap, expected)
910
})
1011
it('should handle extra whitespace characters', () => {
1112
const sourceMap = getSourceMapFromFile('./test/fixtures/source-maps/padded.js')
12-
assert.strictEqual(sourceMap, ['test', 'fixtures', 'source-maps', 'padded.js.map'].join(path.sep))
13+
assert.deepStrictEqual(sourceMap, { version: 3 })
14+
})
15+
it('should support base64 encoded inline source maps', () => {
16+
const sourceMap = getSourceMapFromFile('./test/fixtures/source-maps/inline.js')
17+
assert.strictEqual(sourceMap.version, 3)
1318
})
1419
})

0 commit comments

Comments
 (0)