Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4dced02

Browse filesBrowse files
bcoeMylesBorins
authored andcommitted
module: add API for interacting with source maps
PR-URL: #31132 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent aedbfdb commit 4dced02
Copy full SHA for 4dced02

File tree

Expand file treeCollapse file tree

8 files changed

+252
-32
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

8 files changed

+252
-32
lines changed
Open diff view settings
Collapse file

‎doc/api/modules.md‎

Copy file name to clipboardExpand all lines: doc/api/modules.md
+86Lines changed: 86 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,86 @@ import('fs').then((esmFS) => {
10331033
});
10341034
```
10351035
1036+
## Source Map V3 Support
1037+
<!-- YAML
1038+
added: REPLACEME
1039+
-->
1040+
1041+
> Stability: 1 - Experimental
1042+
1043+
Helpers for for interacting with the source map cache. This cache is
1044+
populated when source map parsing is enabled and
1045+
[source map include directives][] are found in a modules' footer.
1046+
1047+
To enable source map parsing, Node.js must be run with the flag
1048+
[`--enable-source-maps`][], or with code coverage enabled by setting
1049+
[`NODE_V8_COVERAGE=dir`][].
1050+
1051+
```js
1052+
const { findSourceMap, SourceMap } = require('module');
1053+
```
1054+
1055+
### `module.findSourceMap(path[, error])`
1056+
<!-- YAML
1057+
added: REPLACEME
1058+
-->
1059+
1060+
* `path` {string}
1061+
* `error` {Error}
1062+
* Returns: {module.SourceMap}
1063+
1064+
`path` is the resolved path for the file for which a corresponding source map
1065+
should be fetched.
1066+
1067+
The `error` instance should be passed as the second parameter to `findSourceMap`
1068+
in exceptional flows, e.g., when an overridden
1069+
[`Error.prepareStackTrace(error, trace)`][] is invoked. Modules are not added to
1070+
the module cache until they are successfully loaded, in these cases source maps
1071+
will be associated with the `error` instance along with the `path`.
1072+
1073+
### Class: `module.SourceMap`
1074+
<!-- YAML
1075+
added: REPLACEME
1076+
-->
1077+
1078+
#### `new SourceMap(payload)`
1079+
1080+
* `payload` {Object}
1081+
1082+
Creates a new `sourceMap` instance.
1083+
1084+
`payload` is an object with keys matching the [Source Map V3 format][]:
1085+
1086+
* `file`: {string}
1087+
* `version`: {number}
1088+
* `sources`: {string[]}
1089+
* `sourcesContent`: {string[]}
1090+
* `names`: {string[]}
1091+
* `mappings`: {string}
1092+
* `sourceRoot`: {string}
1093+
1094+
#### `sourceMap.payload`
1095+
1096+
* Returns: {Object}
1097+
1098+
Getter for the payload used to construct the [`SourceMap`][] instance.
1099+
1100+
#### `sourceMap.findEntry(lineNumber, columnNumber)`
1101+
1102+
* `lineNumber` {number}
1103+
* `columnNumber` {number}
1104+
* Returns: {Object}
1105+
1106+
Given a line number and column number in the generated source file, returns
1107+
an object representing the position in the original file. The object returned
1108+
consists of the following keys:
1109+
1110+
* generatedLine: {number}
1111+
* generatedColumn: {number}
1112+
* originalSource: {string}
1113+
* originalLine: {number}
1114+
* originalColumn: {number}
1115+
10361116
[GLOBAL_FOLDERS]: #modules_loading_from_the_global_folders
10371117
[`Error`]: errors.html#errors_class_error
10381118
[`__dirname`]: #modules_dirname
@@ -1046,3 +1126,9 @@ import('fs').then((esmFS) => {
10461126
[module resolution]: #modules_all_together
10471127
[module wrapper]: #modules_the_module_wrapper
10481128
[native addons]: addons.html
1129+
[source map include directives]: https://sourcemaps.info/spec.html#h.lmz475t4mvbx
1130+
[`--enable-source-maps`]: cli.html#cli_enable_source_maps
1131+
[`NODE_V8_COVERAGE=dir`]: cli.html#cli_node_v8_coverage_dir
1132+
[`Error.prepareStackTrace(error, trace)`]: https://v8.dev/docs/stack-trace-api#customizing-stack-traces
1133+
[`SourceMap`]: modules.html#modules_class_module_sourcemap
1134+
[Source Map V3 format]: https://sourcemaps.info/spec.html#h.mofvlxcwqzej
Collapse file

‎lib/internal/source_map/prepare_stack_trace.js‎

Copy file name to clipboardExpand all lines: lib/internal/source_map/prepare_stack_trace.js
+10-8Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ const prepareStackTrace = (globalThis, error, trace) => {
2929
maybeOverridePrepareStackTrace(globalThis, error, trace);
3030
if (globalOverride !== kNoOverride) return globalOverride;
3131

32-
const { SourceMap } = require('internal/source_map/source_map');
3332
const errorString = ErrorToString.call(error);
3433

3534
if (trace.length === 0) {
@@ -39,16 +38,19 @@ const prepareStackTrace = (globalThis, error, trace) => {
3938
let str = i !== 0 ? '\n at ' : '';
4039
str = `${str}${t}`;
4140
try {
42-
const sourceMap = findSourceMap(t.getFileName(), error);
43-
if (sourceMap && sourceMap.data) {
44-
const sm = new SourceMap(sourceMap.data);
41+
const sm = findSourceMap(t.getFileName(), error);
42+
if (sm) {
4543
// Source Map V3 lines/columns use zero-based offsets whereas, in
4644
// stack traces, they start at 1/1.
47-
const [, , url, line, col] =
48-
sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
49-
if (url && line !== undefined && col !== undefined) {
45+
const {
46+
originalLine,
47+
originalColumn,
48+
originalSource
49+
} = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
50+
if (originalSource && originalLine !== undefined &&
51+
originalColumn !== undefined) {
5052
str +=
51-
`\n -> ${url.replace('file://', '')}:${line + 1}:${col + 1}`;
53+
`\n -> ${originalSource.replace('file://', '')}:${originalLine + 1}:${originalColumn + 1}`;
5254
}
5355
}
5456
} catch (err) {
Collapse file

‎lib/internal/source_map/source_map.js‎

Copy file name to clipboardExpand all lines: lib/internal/source_map/source_map.js
+50-22Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@
6666

6767
'use strict';
6868

69+
const {
70+
Array
71+
} = primordials;
72+
73+
const {
74+
ERR_INVALID_ARG_TYPE
75+
} = require('internal/errors').codes;
76+
6977
let base64Map;
7078

7179
const VLQ_BASE_SHIFT = 5;
@@ -112,6 +120,7 @@ class StringCharIterator {
112120
* @param {SourceMapV3} payload
113121
*/
114122
class SourceMap {
123+
#payload;
115124
#reverseMappingsBySourceURL = [];
116125
#mappings = [];
117126
#sources = {};
@@ -129,17 +138,25 @@ class SourceMap {
129138
for (let i = 0; i < base64Digits.length; ++i)
130139
base64Map[base64Digits[i]] = i;
131140
}
132-
this.#parseMappingPayload(payload);
141+
this.#payload = cloneSourceMapV3(payload);
142+
this.#parseMappingPayload();
143+
}
144+
145+
/**
146+
* @return {Object} raw source map v3 payload.
147+
*/
148+
get payload() {
149+
return cloneSourceMapV3(this.#payload);
133150
}
134151

135152
/**
136153
* @param {SourceMapV3} mappingPayload
137154
*/
138-
#parseMappingPayload = (mappingPayload) => {
139-
if (mappingPayload.sections)
140-
this.#parseSections(mappingPayload.sections);
155+
#parseMappingPayload = () => {
156+
if (this.#payload.sections)
157+
this.#parseSections(this.#payload.sections);
141158
else
142-
this.#parseMap(mappingPayload, 0, 0);
159+
this.#parseMap(this.#payload, 0, 0);
143160
}
144161

145162
/**
@@ -175,24 +192,18 @@ class SourceMap {
175192
const entry = this.#mappings[first];
176193
if (!first && entry && (lineNumber < entry[0] ||
177194
(lineNumber === entry[0] && columnNumber < entry[1]))) {
178-
return null;
195+
return {};
196+
} else if (!entry) {
197+
return {};
198+
} else {
199+
return {
200+
generatedLine: entry[0],
201+
generatedColumn: entry[1],
202+
originalSource: entry[2],
203+
originalLine: entry[3],
204+
originalColumn: entry[4]
205+
};
179206
}
180-
return entry;
181-
}
182-
183-
/**
184-
* @param {string} sourceURL of the originating resource
185-
* @param {number} lineNumber in the originating resource
186-
* @return {Array}
187-
*/
188-
findEntryReversed(sourceURL, lineNumber) {
189-
const mappings = this.#reverseMappingsBySourceURL[sourceURL];
190-
for (; lineNumber < mappings.length; ++lineNumber) {
191-
const mapping = mappings[lineNumber];
192-
if (mapping)
193-
return mapping;
194-
}
195-
return this.#mappings[0];
196207
}
197208

198209
/**
@@ -296,6 +307,23 @@ function decodeVLQ(stringCharIterator) {
296307
return negative ? -result : result;
297308
}
298309

310+
/**
311+
* @param {SourceMapV3} payload
312+
* @return {SourceMapV3}
313+
*/
314+
function cloneSourceMapV3(payload) {
315+
if (typeof payload !== 'object') {
316+
throw new ERR_INVALID_ARG_TYPE('payload', ['Object'], payload);
317+
}
318+
payload = { ...payload };
319+
for (const key in payload) {
320+
if (payload.hasOwnProperty(key) && Array.isArray(payload[key])) {
321+
payload[key] = payload[key].slice(0);
322+
}
323+
}
324+
return payload;
325+
}
326+
299327
module.exports = {
300328
SourceMap
301329
};
Collapse file

‎lib/internal/source_map/source_map_cache.js‎

Copy file name to clipboardExpand all lines: lib/internal/source_map/source_map_cache.js
+11-1Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const cjsSourceMapCache = new WeakMap();
3737
const esmSourceMapCache = new Map();
3838
const { fileURLToPath, URL } = require('url');
3939
let Module;
40+
let SourceMap;
4041

4142
let experimentalSourceMaps;
4243
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
@@ -222,8 +223,13 @@ function appendCJSCache(obj) {
222223

223224
// Attempt to lookup a source map, which is either attached to a file URI, or
224225
// keyed on an error instance.
226+
// TODO(bcoe): once WeakRefs are available in Node.js, refactor to drop
227+
// requirement of error parameter.
225228
function findSourceMap(uri, error) {
226229
if (!Module) Module = require('internal/modules/cjs/loader').Module;
230+
if (!SourceMap) {
231+
SourceMap = require('internal/source_map/source_map').SourceMap;
232+
}
227233
let sourceMap = cjsSourceMapCache.get(Module._cache[uri]);
228234
if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri);
229235
if (sourceMap === undefined) {
@@ -235,7 +241,11 @@ function findSourceMap(uri, error) {
235241
sourceMap = candidateSourceMap;
236242
}
237243
}
238-
return sourceMap;
244+
if (sourceMap && sourceMap.data) {
245+
return new SourceMap(sourceMap.data);
246+
} else {
247+
return undefined;
248+
}
239249
}
240250

241251
module.exports = {
Collapse file

‎lib/module.js‎

Copy file name to clipboard
+7-1Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
'use strict';
22

3-
module.exports = require('internal/modules/cjs/loader').Module;
3+
const { findSourceMap } = require('internal/source_map/source_map_cache');
4+
const { Module } = require('internal/modules/cjs/loader');
5+
const { SourceMap } = require('internal/source_map/source_map');
6+
7+
Module.findSourceMap = findSourceMap;
8+
Module.SourceMap = SourceMap;
9+
module.exports = Module;
Collapse file
+84Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Flags: --enable-source-maps
2+
'use strict';
3+
4+
require('../common');
5+
const assert = require('assert');
6+
const { findSourceMap, SourceMap } = require('module');
7+
const { readFileSync } = require('fs');
8+
9+
// findSourceMap() can lookup source-maps based on URIs, in the
10+
// non-exceptional case.
11+
{
12+
require('../fixtures/source-map/disk-relative-path.js');
13+
const sourceMap = findSourceMap(
14+
require.resolve('../fixtures/source-map/disk-relative-path.js')
15+
);
16+
const {
17+
originalLine,
18+
originalColumn,
19+
originalSource
20+
} = sourceMap.findEntry(0, 29);
21+
assert.strictEqual(originalLine, 2);
22+
assert.strictEqual(originalColumn, 4);
23+
assert(originalSource.endsWith('disk.js'));
24+
}
25+
26+
// findSourceMap() can be used in Error.prepareStackTrace() to lookup
27+
// source-map attached to error.
28+
{
29+
let callSite;
30+
let sourceMap;
31+
Error.prepareStackTrace = (error, trace) => {
32+
const throwingRequireCallSite = trace[0];
33+
if (throwingRequireCallSite.getFileName().endsWith('typescript-throw.js')) {
34+
sourceMap = findSourceMap(throwingRequireCallSite.getFileName(), error);
35+
callSite = throwingRequireCallSite;
36+
}
37+
};
38+
try {
39+
// Require a file that throws an exception, and has a source map.
40+
require('../fixtures/source-map/typescript-throw.js');
41+
} catch (err) {
42+
err.stack; // Force prepareStackTrace() to be called.
43+
}
44+
assert(callSite);
45+
assert(sourceMap);
46+
const {
47+
generatedLine,
48+
generatedColumn,
49+
originalLine,
50+
originalColumn,
51+
originalSource
52+
} = sourceMap.findEntry(
53+
callSite.getLineNumber() - 1,
54+
callSite.getColumnNumber() - 1
55+
);
56+
57+
assert.strictEqual(generatedLine, 19);
58+
assert.strictEqual(generatedColumn, 14);
59+
60+
assert.strictEqual(originalLine, 17);
61+
assert.strictEqual(originalColumn, 10);
62+
assert(originalSource.endsWith('typescript-throw.ts'));
63+
}
64+
65+
// SourceMap can be instantiated with Source Map V3 object as payload.
66+
{
67+
const payload = JSON.parse(readFileSync(
68+
require.resolve('../fixtures/source-map/disk.map'), 'utf8'
69+
));
70+
const sourceMap = new SourceMap(payload);
71+
const {
72+
originalLine,
73+
originalColumn,
74+
originalSource
75+
} = sourceMap.findEntry(0, 29);
76+
assert.strictEqual(originalLine, 2);
77+
assert.strictEqual(originalColumn, 4);
78+
assert(originalSource.endsWith('disk.js'));
79+
// The stored payload should be a clone:
80+
assert.strictEqual(payload.mappings, sourceMap.payload.mappings);
81+
assert.notStrictEqual(payload, sourceMap.payload);
82+
assert.strictEqual(payload.sources[0], sourceMap.payload.sources[0]);
83+
assert.notStrictEqual(payload.sources, sourceMap.payload.sources);
84+
}
Collapse file

‎tools/doc/type-parser.js‎

Copy file name to clipboardExpand all lines: tools/doc/type-parser.js
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ const customTypesMap = {
101101
'https.Server': 'https.html#https_class_https_server',
102102

103103
'module': 'modules.html#modules_the_module_object',
104+
105+
'module.SourceMap':
106+
'modules.html#modules_class_module_sourcemap',
107+
104108
'require': 'modules.html#modules_require_id',
105109

106110
'Handle': 'net.html#net_server_listen_handle_backlog_callback',

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.