Skip to content

Commit 62bc80c

Browse files
bcoetargos
authored andcommitted
process: add lineLength to source-map-cache
Without the line lengths of in-memory transpiled source, it's not possible to convert from byte ofsets to line/column offsets. PR-URL: #29863 Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: David Carlier <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 735ec1b commit 62bc80c

File tree

6 files changed

+118
-55
lines changed

6 files changed

+118
-55
lines changed

doc/api/cli.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -1199,8 +1199,9 @@ If found, Source Map data is appended to the top-level key `source-map-cache`
11991199
on the JSON coverage object.
12001200

12011201
`source-map-cache` is an object with keys representing the files source maps
1202-
were extracted from, and the values include the raw source-map URL
1203-
(in the key `url`) and the parsed Source Map V3 information (in the key `data`).
1202+
were extracted from, and values which include the raw source-map URL
1203+
(in the key `url`), the parsed Source Map V3 information (in the key `data`),
1204+
and the line lengths of the source file (in the key `lineLengths`).
12041205

12051206
```json
12061207
{
@@ -1226,7 +1227,13 @@ were extracted from, and the values include the raw source-map URL
12261227
],
12271228
"mappings": "MAAMA,IACJC,YAAaC",
12281229
"sourceRoot": "./"
1229-
}
1230+
},
1231+
"lineLengths": [
1232+
13,
1233+
62,
1234+
38,
1235+
27
1236+
]
12301237
}
12311238
}
12321239
}

lib/internal/bootstrap/pre_execution.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function prepareMainThreadExecution(expandArgv1 = false) {
2525
// prepareStackTrace method, replacing the default in errors.js.
2626
if (getOptionValue('--enable-source-maps')) {
2727
const { prepareStackTrace } =
28-
require('internal/source_map/source_map_cache');
28+
require('internal/source_map/prepare_stack_trace');
2929
const { setPrepareStackTraceCallback } = internalBinding('errors');
3030
setPrepareStackTraceCallback(prepareStackTrace);
3131
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
const debug = require('internal/util/debuglog').debuglog('source_map');
4+
const { findSourceMap } = require('internal/source_map/source_map_cache');
5+
const { overrideStackTrace } = require('internal/errors');
6+
7+
// Create a prettified stacktrace, inserting context from source maps
8+
// if possible.
9+
const ErrorToString = Error.prototype.toString; // Capture original toString.
10+
const prepareStackTrace = (globalThis, error, trace) => {
11+
// API for node internals to override error stack formatting
12+
// without interfering with userland code.
13+
// TODO(bcoe): add support for source-maps to repl.
14+
if (overrideStackTrace.has(error)) {
15+
const f = overrideStackTrace.get(error);
16+
overrideStackTrace.delete(error);
17+
return f(error, trace);
18+
}
19+
20+
const { SourceMap } = require('internal/source_map/source_map');
21+
const errorString = ErrorToString.call(error);
22+
23+
if (trace.length === 0) {
24+
return errorString;
25+
}
26+
const preparedTrace = trace.map((t, i) => {
27+
let str = i !== 0 ? '\n at ' : '';
28+
str = `${str}${t}`;
29+
try {
30+
const sourceMap = findSourceMap(t.getFileName(), error);
31+
if (sourceMap && sourceMap.data) {
32+
const sm = new SourceMap(sourceMap.data);
33+
// Source Map V3 lines/columns use zero-based offsets whereas, in
34+
// stack traces, they start at 1/1.
35+
const [, , url, line, col] =
36+
sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
37+
if (url && line !== undefined && col !== undefined) {
38+
str +=
39+
`\n -> ${url.replace('file://', '')}:${line + 1}:${col + 1}`;
40+
}
41+
}
42+
} catch (err) {
43+
debug(err.stack);
44+
}
45+
return str;
46+
});
47+
return `${errorString}\n at ${preparedTrace.join('')}`;
48+
};
49+
50+
module.exports = {
51+
prepareStackTrace,
52+
};

lib/internal/source_map/source_map_cache.js

+24-51
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ const cjsSourceMapCache = new WeakMap();
1717
// on filenames.
1818
const esmSourceMapCache = new Map();
1919
const { fileURLToPath, URL } = require('url');
20-
const { overrideStackTrace } = require('internal/errors');
2120

2221
let experimentalSourceMaps;
2322
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
@@ -38,18 +37,22 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
3837

3938
const match = content.match(/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/);
4039
if (match) {
40+
const data = dataFromUrl(basePath, match.groups.sourceMappingURL);
41+
const url = data ? null : match.groups.sourceMappingURL;
4142
if (cjsModuleInstance) {
4243
cjsSourceMapCache.set(cjsModuleInstance, {
4344
filename,
44-
url: match.groups.sourceMappingURL,
45-
data: dataFromUrl(basePath, match.groups.sourceMappingURL)
45+
lineLengths: lineLengths(content),
46+
data,
47+
url
4648
});
4749
} else {
4850
// If there is no cjsModuleInstance assume we are in a
4951
// "modules/esm" context.
5052
esmSourceMapCache.set(filename, {
51-
url: match.groups.sourceMappingURL,
52-
data: dataFromUrl(basePath, match.groups.sourceMappingURL)
53+
lineLengths: lineLengths(content),
54+
data,
55+
url
5356
});
5457
}
5558
}
@@ -73,6 +76,18 @@ function dataFromUrl(basePath, sourceMappingURL) {
7376
}
7477
}
7578

79+
// Cache the length of each line in the file that a source map was extracted
80+
// from. This allows translation from byte offset V8 coverage reports,
81+
// to line/column offset Source Map V3.
82+
function lineLengths(content) {
83+
// We purposefully keep \r as part of the line-length calculation, in
84+
// cases where there is a \r\n separator, so that this can be taken into
85+
// account in coverage calculations.
86+
return content.split(/\n|\u2028|\u2029/).map((line) => {
87+
return line.length;
88+
});
89+
}
90+
7691
function sourceMapFromFile(sourceMapFile) {
7792
try {
7893
const content = fs.readFileSync(sourceMapFile, 'utf8');
@@ -161,56 +176,14 @@ function appendCJSCache(obj) {
161176
const value = cjsSourceMapCache.get(Module._cache[key]);
162177
if (value) {
163178
obj[`file://${key}`] = {
164-
url: value.url,
165-
data: value.data
179+
lineLengths: value.lineLengths,
180+
data: value.data,
181+
url: value.url
166182
};
167183
}
168184
});
169185
}
170186

171-
// Create a prettified stacktrace, inserting context from source maps
172-
// if possible.
173-
const ErrorToString = Error.prototype.toString; // Capture original toString.
174-
const prepareStackTrace = (globalThis, error, trace) => {
175-
// API for node internals to override error stack formatting
176-
// without interfering with userland code.
177-
// TODO(bcoe): add support for source-maps to repl.
178-
if (overrideStackTrace.has(error)) {
179-
const f = overrideStackTrace.get(error);
180-
overrideStackTrace.delete(error);
181-
return f(error, trace);
182-
}
183-
184-
const { SourceMap } = require('internal/source_map/source_map');
185-
const errorString = ErrorToString.call(error);
186-
187-
if (trace.length === 0) {
188-
return errorString;
189-
}
190-
const preparedTrace = trace.map((t, i) => {
191-
let str = i !== 0 ? '\n at ' : '';
192-
str = `${str}${t}`;
193-
try {
194-
const sourceMap = findSourceMap(t.getFileName(), error);
195-
if (sourceMap && sourceMap.data) {
196-
const sm = new SourceMap(sourceMap.data);
197-
// Source Map V3 lines/columns use zero-based offsets whereas, in
198-
// stack traces, they start at 1/1.
199-
const [, , url, line, col] =
200-
sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
201-
if (url && line !== undefined && col !== undefined) {
202-
str +=
203-
`\n -> ${url.replace('file://', '')}:${line + 1}:${col + 1}`;
204-
}
205-
}
206-
} catch (err) {
207-
debug(err.stack);
208-
}
209-
return str;
210-
});
211-
return `${errorString}\n at ${preparedTrace.join('')}`;
212-
};
213-
214187
// Attempt to lookup a source map, which is either attached to a file URI, or
215188
// keyed on an error instance.
216189
function findSourceMap(uri, error) {
@@ -230,8 +203,8 @@ function findSourceMap(uri, error) {
230203
}
231204

232205
module.exports = {
206+
findSourceMap,
233207
maybeCacheSourceMap,
234-
prepareStackTrace,
235208
rekeySourceMap,
236209
sourceMapCacheToObject,
237210
};

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@
176176
'lib/internal/repl/history.js',
177177
'lib/internal/repl/utils.js',
178178
'lib/internal/socket_list.js',
179+
'lib/internal/source_map/prepare_stack_trace.js',
179180
'lib/internal/source_map/source_map.js',
180181
'lib/internal/source_map/source_map_cache.js',
181182
'lib/internal/test/binding.js',

test/parallel/test-source-map.js

+30
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,36 @@ function nextdir() {
193193
);
194194
}
195195

196+
// Does not persist url parameter if source-map has been parsed.
197+
{
198+
const coverageDirectory = nextdir();
199+
spawnSync(process.execPath, [
200+
require.resolve('../fixtures/source-map/inline-base64.js')
201+
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
202+
const sourceMap = getSourceMapFromCache(
203+
'inline-base64.js',
204+
coverageDirectory
205+
);
206+
assert.strictEqual(sourceMap.url, null);
207+
}
208+
209+
// Persists line lengths for in-memory representation of source file.
210+
{
211+
const coverageDirectory = nextdir();
212+
spawnSync(process.execPath, [
213+
require.resolve('../fixtures/source-map/istanbul-throw.js')
214+
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
215+
const sourceMap = getSourceMapFromCache(
216+
'istanbul-throw.js',
217+
coverageDirectory
218+
);
219+
if (common.isWindows) {
220+
assert.deepStrictEqual(sourceMap.lineLengths, [1086, 31, 185, 649, 0]);
221+
} else {
222+
assert.deepStrictEqual(sourceMap.lineLengths, [1085, 30, 184, 648, 0]);
223+
}
224+
}
225+
196226
function getSourceMapFromCache(fixtureFile, coverageDirectory) {
197227
const jsonFiles = fs.readdirSync(coverageDirectory);
198228
for (const jsonFile of jsonFiles) {

0 commit comments

Comments
 (0)