Skip to content

Commit 13ad74f

Browse files
module: add module.mapCallSite()
1 parent 4354a1d commit 13ad74f

File tree

6 files changed

+131
-3
lines changed

6 files changed

+131
-3
lines changed

doc/api/module.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,37 @@ isBuiltin('fs'); // true
317317
isBuiltin('wss'); // false
318318
```
319319
320+
### `module.mapCallSite(callSite)`
321+
322+
<!-- YAML
323+
added: REPLACEME
324+
-->
325+
326+
> Stability: 1.1 - Active development
327+
328+
* `callSite` {Object | Array} A [CallSite][] object or an array of CallSite objects.
329+
* Returns: {Object | Array} The original source code location(s) for the given CallSite object(s).
330+
331+
Reconstructs the original source code location from a [CallSite][] object through the source map.
332+
333+
```mjs
334+
import { mapCallSite } from 'node:module';
335+
import { getCallSite } from 'node:util';
336+
337+
mapCallSite(getCallSite()); // Reconstructs the original source code location for the whole stack
338+
339+
mapCallSite(getCallSite()[0]); // Reconstructs the original source code location for the first frame
340+
```
341+
342+
```cjs
343+
const { mapCallSite } = require('node:module');
344+
const { getCallSite } = require('node:util');
345+
346+
mapCallSite(getCallSite()); // Reconstructs the original source code location for the whole stack
347+
348+
mapCallSite(getCallSite()[0]); // Reconstructs the original source code location for the first frame
349+
```
350+
320351
### `module.register(specifier[, parentURL][, options])`
321352
322353
<!-- YAML
@@ -1397,6 +1428,7 @@ returned object contains the following keys:
13971428
* columnNumber: {number} The 1-indexed columnNumber of the
13981429
corresponding call site in the original source
13991430
1431+
[CallSite]: util.md#utilgetcallsiteframes
14001432
[CommonJS]: modules.md
14011433
[Conditional exports]: packages.md#conditional-exports
14021434
[Customization hooks]: #customization-hooks

lib/internal/source_map/source_map_cache.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const {
4+
ArrayIsArray,
45
ArrayPrototypePush,
56
JSONParse,
67
RegExpPrototypeExec,
@@ -15,7 +16,7 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
1516
debug = fn;
1617
});
1718

18-
const { validateBoolean } = require('internal/validators');
19+
const { validateBoolean, validateCallSite } = require('internal/validators');
1920
const {
2021
setSourceMapsEnabled: setSourceMapsNative,
2122
} = internalBinding('errors');
@@ -351,11 +352,53 @@ function findSourceMap(sourceURL) {
351352
return sourceMap;
352353
}
353354

355+
/**
356+
*
357+
* @param {Record<string, unknown>} callSite // The call site object to reconstruct from source map
358+
* @returns {Record<string, unknown> | undefined} // The reconstructed call site object
359+
*/
360+
function reconstructCallSite(callSite) {
361+
const { scriptName, lineNumber, column } = callSite;
362+
const sourceMap = findSourceMap(scriptName);
363+
if (!sourceMap) return;
364+
const entry = sourceMap.findEntry(lineNumber - 1, column - 1);
365+
if (!entry?.originalSource) return;
366+
return {
367+
__proto__: null,
368+
functionName: callSite.name,
369+
lineNumber: entry.originalLine + 1,
370+
column: entry.originalColumn + 1,
371+
scriptName: entry.originalSource,
372+
};
373+
}
374+
375+
/**
376+
*
377+
* The call site object or array of object to map (ex `util.getCallSite()`)
378+
* @param {Record<string, unknown> | Record<string, unknown>[]} callSites
379+
* An object or array of objects with the reconstructed call site
380+
* @returns {Record<string, unknown> | Record<string, unknown>[]}
381+
*/
382+
function mapCallSite(callSites) {
383+
if (ArrayIsArray(callSites)) {
384+
const result = [];
385+
for (const callSite of callSites) {
386+
validateCallSite(callSite);
387+
const found = reconstructCallSite(callSite);
388+
ArrayPrototypePush(result, found ?? callSite);
389+
}
390+
return result;
391+
}
392+
validateCallSite(callSites);
393+
return reconstructCallSite(callSites) ?? callSites;
394+
}
395+
354396
module.exports = {
355397
findSourceMap,
356398
getSourceMapsEnabled,
357399
setSourceMapsEnabled,
358400
maybeCacheSourceMap,
359401
maybeCacheGeneratedSourceMap,
402+
mapCallSite,
360403
sourceMapCacheToObject,
361404
};

lib/internal/validators.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,19 @@ const checkRangesOrGetDefault = hideStackFrames(
608608
},
609609
);
610610

611+
/**
612+
*
613+
* @param {Record<string, unknown>} callSite The call site object (ex: `util.getCallSite()[0]`)
614+
* @returns {void}
615+
*/
616+
function validateCallSite(callSite) {
617+
validateObject(callSite, 'callSite');
618+
validateString(callSite.scriptName, 'callSite.scriptName');
619+
validateString(callSite.functionName, 'callSite.functionName');
620+
validateNumber(callSite.lineNumber, 'callSite.lineNumber');
621+
validateNumber(callSite.column, 'callSite.column');
622+
}
623+
611624
module.exports = {
612625
isInt32,
613626
isUint32,
@@ -618,6 +631,7 @@ module.exports = {
618631
validateAbortSignalArray,
619632
validateBoolean,
620633
validateBuffer,
634+
validateCallSite,
621635
validateDictionary,
622636
validateEncoding,
623637
validateFunction,

lib/module.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
const { findSourceMap } = require('internal/source_map/source_map_cache');
3+
const { findSourceMap, mapCallSite } = require('internal/source_map/source_map_cache');
44
const { Module } = require('internal/modules/cjs/loader');
55
const { register } = require('internal/modules/esm/loader');
66
const { SourceMap } = require('internal/source_map/source_map');
@@ -24,5 +24,5 @@ Module.findPackageJSON = findPackageJSON;
2424
Module.flushCompileCache = flushCompileCache;
2525
Module.getCompileCacheDir = getCompileCacheDir;
2626
Module.stripTypeScriptTypes = stripTypeScriptTypes;
27-
27+
Module.mapCallSite = mapCallSite;
2828
module.exports = Module;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { spawnPromisified } from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.mjs';
3+
import { strictEqual, match, throws, deepStrictEqual } from 'node:assert';
4+
import { test } from 'node:test';
5+
import { mapCallSite } from 'node:module';
6+
import { getCallSite } from 'node:util';
7+
8+
test('module.mapCallSite', async () => {
9+
throws(() => mapCallSite('not an object'), { code: 'ERR_INVALID_ARG_TYPE' });
10+
deepStrictEqual(mapCallSite([]), []);
11+
throws(() => mapCallSite({}), { code: 'ERR_INVALID_ARG_TYPE' });
12+
13+
const callSite = getCallSite();
14+
deepStrictEqual(callSite, mapCallSite(callSite));
15+
deepStrictEqual(callSite[0], mapCallSite(callSite[0]));
16+
});
17+
18+
19+
test('module.mapCallSite should reconstruct ts callsite', async () => {
20+
const result = await spawnPromisified(process.execPath, [
21+
'--no-warnings',
22+
'--experimental-transform-types',
23+
fixtures.path('typescript/ts/test-callsite.ts'),
24+
]);
25+
const output = result.stdout.toString().trim();
26+
strictEqual(result.stderr, '');
27+
match(output, /lineNumber: 9/);
28+
match(output, /column: 25/);
29+
strictEqual(result.code, 0);
30+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const { getCallSite } = require('node:util');
2+
const { mapCallSite } = require('node:module');
3+
4+
interface CallSite {
5+
A;
6+
B;
7+
}
8+
9+
console.log(mapCallSite(getCallSite()[0]));

0 commit comments

Comments
 (0)