Skip to content

Commit 2bc03cf

Browse files
module: add module.mapCallSite()
1 parent 4354a1d commit 2bc03cf

File tree

10 files changed

+192
-3
lines changed

10 files changed

+192
-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: 54 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,63 @@ function findSourceMap(sourceURL) {
351352
return sourceMap;
352353
}
353354

355+
/**
356+
* @typedef {object} CallSite // The call site
357+
* @property {string} scriptName // The name of the resource that contains the
358+
* script for the function for this StackFrame
359+
* @property {string} functionName // The name of the function associated with this stack frame
360+
* @property {number} lineNumber // The number, 1-based, of the line for the associate function call
361+
* @property {number} columnNumber // The 1-based column offset on the line for the associated function call
362+
*/
363+
364+
/**
365+
* @param {CallSite} callSite // The call site object to reconstruct from source map
366+
* @returns {CallSite | undefined} // The reconstructed call site object
367+
*/
368+
function reconstructCallSite(callSite) {
369+
const { scriptName, lineNumber, column } = callSite;
370+
const sourceMap = findSourceMap(scriptName);
371+
if (!sourceMap) return;
372+
const entry = sourceMap.findEntry(lineNumber - 1, column - 1);
373+
if (!entry?.originalSource) return;
374+
return {
375+
__proto__: null,
376+
// If the name is not found, it is an empty string to match the behavior of `util.getCallSite()`
377+
functionName: entry.name ?? '',
378+
lineNumber: entry.originalLine + 1,
379+
column: entry.originalColumn + 1,
380+
scriptName: entry.originalSource,
381+
};
382+
}
383+
384+
/**
385+
*
386+
* The call site object or array of object to map (ex `util.getCallSite()`)
387+
* @param {CallSite | CallSite[]} callSites
388+
* An object or array of objects with the reconstructed call site
389+
* @returns {CallSite | CallSite[]}
390+
*/
391+
function mapCallSite(callSites) {
392+
if (ArrayIsArray(callSites)) {
393+
const result = [];
394+
for (let i = 0; i < callSites.length; ++i) {
395+
const callSite = callSites[i];
396+
validateCallSite(callSite);
397+
const found = reconstructCallSite(callSite);
398+
ArrayPrototypePush(result, found ?? callSite);
399+
}
400+
return result;
401+
}
402+
validateCallSite(callSites);
403+
return reconstructCallSite(callSites) ?? callSites;
404+
}
405+
354406
module.exports = {
355407
findSourceMap,
356408
getSourceMapsEnabled,
357409
setSourceMapsEnabled,
358410
maybeCacheSourceMap,
359411
maybeCacheGeneratedSourceMap,
412+
mapCallSite,
360413
sourceMapCacheToObject,
361414
};

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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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: 18/);
29+
match(output, /typescript\/ts\/test-callsite\.ts/);
30+
strictEqual(result.code, 0);
31+
});
32+
33+
test('module.mapCallSite should reconstruct ts callsite', async () => {
34+
const result = await spawnPromisified(process.execPath, [
35+
'--no-warnings',
36+
'--enable-source-maps',
37+
fixtures.path('source-map/minified-map-sourcemap.js'),
38+
]);
39+
const output = result.stdout.toString().trim();
40+
match(output, /functionName: 'foo'/);
41+
strictEqual(result.stderr, '');
42+
strictEqual(result.code, 0);
43+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const { getCallSite } = require("node:util"), { mapCallSite } = require("node:module"); function foo() { console.log(mapCallSite(getCallSite()[0])) } foo();
2+
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiZm9vLmpzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJjb25zdCB7IGdldENhbGxTaXRlIH0gPSByZXF1aXJlKCdub2RlOnV0aWwnKTtcbmNvbnN0IHsgbWFwQ2FsbFNpdGUgfSA9IHJlcXVpcmUoJ25vZGU6bW9kdWxlJyk7XG5cbmZ1bmN0aW9uIGZvbygpIHtcbiAgICBjb25zb2xlLmxvZyhtYXBDYWxsU2l0ZShnZXRDYWxsU2l0ZSgpWzBdKSk7XG59XG5cbmZvbygpO1xuIl0sCiAgIm1hcHBpbmdzIjogIkFBQUEsR0FBTSxDQUFFLFlBQUFBLENBQVksRUFBSSxRQUFRLFdBQVcsRUFDckMsQ0FBRSxZQUFBQyxDQUFZLEVBQUksUUFBUSxhQUFhLEVBRTdDLFNBQVNDLEdBQU0sQ0FDWCxRQUFRLElBQUlELEVBQVlELEVBQVksRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUM3QyxDQUVBRSxFQUFJIiwKICAibmFtZXMiOiBbImdldENhbGxTaXRlIiwgIm1hcENhbGxTaXRlIiwgImZvbyJdCn0K
3+
4+
// > npx esbuild foo.js --outfile=foo.min.js --bundle --minify --sourcemap=inline --platform=node
5+
//
6+
// const { getCallSite } = require('node:util');
7+
// const { mapCallSite } = require('node:module');
8+
//
9+
// function foo() {
10+
// console.log(mapCallSite(getCallSite()[0]));
11+
// }
12+
//
13+
// foo();
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
require('../../../common');
3+
const fixtures = require('../../../common/fixtures');
4+
const spawn = require('node:child_process').spawn;
5+
6+
spawn(process.execPath,
7+
['--no-warnings', '--experimental-transform-types', fixtures.path('typescript/ts/test-callsite.ts')],
8+
{ stdio: 'inherit' });
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Without mapCallSite: [Object: null prototype] {
2+
functionName: '',
3+
scriptName: '*fixtures*typescript*ts*test-callsite.ts',
4+
lineNumber: 3,
5+
column: 18
6+
}
7+
With mapCallSite: [Object: null prototype] {
8+
functionName: '',
9+
lineNumber: 9,
10+
column: 18,
11+
scriptName: 'file:**fixtures*typescript*ts*test-callsite.ts'
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const { getCallSite } = require('node:util');
2+
const { mapCallSite } = require('node:module');
3+
4+
interface CallSite {
5+
A;
6+
B;
7+
}
8+
9+
const callSite = getCallSite()[0];
10+
11+
console.log('Without mapCallSite: ', callSite);
12+
13+
console.log('With mapCallSite: ', mapCallSite(callSite));

test/parallel/test-node-output-sourcemaps.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe('sourcemaps output', { concurrency: !process.env.TEST_PARALLEL }, () =>
4040
{ name: 'source-map/output/source_map_throw_first_tick.js' },
4141
{ name: 'source-map/output/source_map_throw_icu.js' },
4242
{ name: 'source-map/output/source_map_throw_set_immediate.js' },
43+
{ name: 'source-map/output/test-map-callsite.js' },
4344
];
4445
for (const { name, transform } of tests) {
4546
it(name, async () => {

0 commit comments

Comments
 (0)