Skip to content

Commit 4dced02

Browse 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

8 files changed

Lines changed: 252 additions & 32 deletions

File tree

‎doc/api/modules.md‎

Lines changed: 86 additions & 0 deletions
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+
#### `newSourceMap(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

‎lib/internal/source_map/prepare_stack_trace.js‎

Lines 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)returnglobalOverride;
3131

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

3534
if(trace.length===0){
@@ -39,16 +38,19 @@ const prepareStackTrace = (globalThis, error, trace) => {
3938
letstr=i!==0 ? '\n at ' : '';
4039
str=`${str}${t}`;
4140
try{
42-
constsourceMap=findSourceMap(t.getFileName(),error);
43-
if(sourceMap&&sourceMap.data){
44-
constsm=newSourceMap(sourceMap.data);
41+
constsm=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){

‎lib/internal/source_map/source_map.js‎

Lines 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
letbase64Map;
7078

7179
constVLQ_BASE_SHIFT=5;
@@ -112,6 +120,7 @@ class StringCharIterator {
112120
* @param {SourceMapV3} payload
113121
*/
114122
classSourceMap{
123+
#payload;
115124
#reverseMappingsBySourceURL =[];
116125
#mappings =[];
117126
#sources ={};
@@ -129,17 +138,25 @@ class SourceMap {
129138
for(leti=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+
getpayload(){
149+
returncloneSourceMapV3(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
constentry=this.#mappings[first];
176193
if(!first&&entry&&(lineNumber<entry[0]||
177194
(lineNumber===entry[0]&&columnNumber<entry[1]))){
178-
returnnull;
195+
return{};
196+
}elseif(!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-
returnentry;
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-
constmappings=this.#reverseMappingsBySourceURL[sourceURL];
190-
for(;lineNumber<mappings.length;++lineNumber){
191-
constmapping=mappings[lineNumber];
192-
if(mapping)
193-
returnmapping;
194-
}
195-
returnthis.#mappings[0];
196207
}
197208

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

310+
/**
311+
* @param {SourceMapV3} payload
312+
* @return {SourceMapV3}
313+
*/
314+
functioncloneSourceMapV3(payload){
315+
if(typeofpayload!=='object'){
316+
thrownewERR_INVALID_ARG_TYPE('payload',['Object'],payload);
317+
}
318+
payload={ ...payload};
319+
for(constkeyinpayload){
320+
if(payload.hasOwnProperty(key)&&Array.isArray(payload[key])){
321+
payload[key]=payload[key].slice(0);
322+
}
323+
}
324+
returnpayload;
325+
}
326+
299327
module.exports={
300328
SourceMap
301329
};

‎lib/internal/source_map/source_map_cache.js‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const cjsSourceMapCache = new WeakMap();
3737
constesmSourceMapCache=newMap();
3838
const{ fileURLToPath,URL}=require('url');
3939
letModule;
40+
letSourceMap;
4041

4142
letexperimentalSourceMaps;
4243
functionmaybeCacheSourceMap(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
functionfindSourceMap(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
letsourceMap=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-
returnsourceMap;
244+
if(sourceMap&&sourceMap.data){
245+
returnnewSourceMap(sourceMap.data);
246+
}else{
247+
returnundefined;
248+
}
239249
}
240250

241251
module.exports={

‎lib/module.js‎

Lines 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;
Lines 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+
constassert=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+
constsourceMap=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+
letcallSite;
30+
letsourceMap;
31+
Error.prepareStackTrace=(error,trace)=>{
32+
constthrowingRequireCallSite=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+
constpayload=JSON.parse(readFileSync(
68+
require.resolve('../fixtures/source-map/disk.map'),'utf8'
69+
));
70+
constsourceMap=newSourceMap(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+
}

‎tools/doc/type-parser.js‎

Lines 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)