Skip to content

Commit

Permalink
module: add module.mapCallSite()
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-ippolito committed Oct 30, 2024
1 parent 4354a1d commit 2bc03cf
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 3 deletions.
32 changes: 32 additions & 0 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,37 @@ isBuiltin('fs'); // true
isBuiltin('wss'); // false
```
### `module.mapCallSite(callSite)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.1 - Active development
* `callSite` {Object | Array} A [CallSite][] object or an array of CallSite objects.
* Returns: {Object | Array} The original source code location(s) for the given CallSite object(s).
Reconstructs the original source code location from a [CallSite][] object through the source map.
```mjs
import { mapCallSite } from 'node:module';
import { getCallSite } from 'node:util';

mapCallSite(getCallSite()); // Reconstructs the original source code location for the whole stack

mapCallSite(getCallSite()[0]); // Reconstructs the original source code location for the first frame
```
```cjs
const { mapCallSite } = require('node:module');
const { getCallSite } = require('node:util');

mapCallSite(getCallSite()); // Reconstructs the original source code location for the whole stack

mapCallSite(getCallSite()[0]); // Reconstructs the original source code location for the first frame
```
### `module.register(specifier[, parentURL][, options])`
<!-- YAML
Expand Down Expand Up @@ -1397,6 +1428,7 @@ returned object contains the following keys:
* columnNumber: {number} The 1-indexed columnNumber of the
corresponding call site in the original source
[CallSite]: util.md#utilgetcallsiteframes
[CommonJS]: modules.md
[Conditional exports]: packages.md#conditional-exports
[Customization hooks]: #customization-hooks
Expand Down
55 changes: 54 additions & 1 deletion lib/internal/source_map/source_map_cache.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ArrayIsArray,
ArrayPrototypePush,
JSONParse,
RegExpPrototypeExec,
Expand All @@ -15,7 +16,7 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
debug = fn;
});

const { validateBoolean } = require('internal/validators');
const { validateBoolean, validateCallSite } = require('internal/validators');
const {
setSourceMapsEnabled: setSourceMapsNative,
} = internalBinding('errors');
Expand Down Expand Up @@ -351,11 +352,63 @@ function findSourceMap(sourceURL) {
return sourceMap;
}

/**
* @typedef {object} CallSite // The call site
* @property {string} scriptName // The name of the resource that contains the
* script for the function for this StackFrame
* @property {string} functionName // The name of the function associated with this stack frame
* @property {number} lineNumber // The number, 1-based, of the line for the associate function call
* @property {number} columnNumber // The 1-based column offset on the line for the associated function call
*/

/**
* @param {CallSite} callSite // The call site object to reconstruct from source map
* @returns {CallSite | undefined} // The reconstructed call site object
*/
function reconstructCallSite(callSite) {
const { scriptName, lineNumber, column } = callSite;
const sourceMap = findSourceMap(scriptName);
if (!sourceMap) return;
const entry = sourceMap.findEntry(lineNumber - 1, column - 1);
if (!entry?.originalSource) return;
return {
__proto__: null,
// If the name is not found, it is an empty string to match the behavior of `util.getCallSite()`
functionName: entry.name ?? '',
lineNumber: entry.originalLine + 1,
column: entry.originalColumn + 1,
scriptName: entry.originalSource,
};
}

/**
*
* The call site object or array of object to map (ex `util.getCallSite()`)
* @param {CallSite | CallSite[]} callSites
* An object or array of objects with the reconstructed call site
* @returns {CallSite | CallSite[]}
*/
function mapCallSite(callSites) {
if (ArrayIsArray(callSites)) {
const result = [];
for (let i = 0; i < callSites.length; ++i) {
const callSite = callSites[i];
validateCallSite(callSite);
const found = reconstructCallSite(callSite);
ArrayPrototypePush(result, found ?? callSite);
}
return result;
}
validateCallSite(callSites);
return reconstructCallSite(callSites) ?? callSites;
}

module.exports = {
findSourceMap,
getSourceMapsEnabled,
setSourceMapsEnabled,
maybeCacheSourceMap,
maybeCacheGeneratedSourceMap,
mapCallSite,
sourceMapCacheToObject,
};
14 changes: 14 additions & 0 deletions lib/internal/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,19 @@ const checkRangesOrGetDefault = hideStackFrames(
},
);

/**
*
* @param {Record<string, unknown>} callSite The call site object (ex: `util.getCallSite()[0]`)
* @returns {void}
*/
function validateCallSite(callSite) {
validateObject(callSite, 'callSite');
validateString(callSite.scriptName, 'callSite.scriptName');
validateString(callSite.functionName, 'callSite.functionName');
validateNumber(callSite.lineNumber, 'callSite.lineNumber');
validateNumber(callSite.column, 'callSite.column');
}

module.exports = {
isInt32,
isUint32,
Expand All @@ -618,6 +631,7 @@ module.exports = {
validateAbortSignalArray,
validateBoolean,
validateBuffer,
validateCallSite,
validateDictionary,
validateEncoding,
validateFunction,
Expand Down
4 changes: 2 additions & 2 deletions lib/module.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const { findSourceMap } = require('internal/source_map/source_map_cache');
const { findSourceMap, mapCallSite } = require('internal/source_map/source_map_cache');
const { Module } = require('internal/modules/cjs/loader');
const { register } = require('internal/modules/esm/loader');
const { SourceMap } = require('internal/source_map/source_map');
Expand All @@ -24,5 +24,5 @@ Module.findPackageJSON = findPackageJSON;
Module.flushCompileCache = flushCompileCache;
Module.getCompileCacheDir = getCompileCacheDir;
Module.stripTypeScriptTypes = stripTypeScriptTypes;

Module.mapCallSite = mapCallSite;
module.exports = Module;
43 changes: 43 additions & 0 deletions test/es-module/test-module-map-callsite.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { strictEqual, match, throws, deepStrictEqual } from 'node:assert';
import { test } from 'node:test';
import { mapCallSite } from 'node:module';
import { getCallSite } from 'node:util';

test('module.mapCallSite', async () => {
throws(() => mapCallSite('not an object'), { code: 'ERR_INVALID_ARG_TYPE' });
deepStrictEqual(mapCallSite([]), []);
throws(() => mapCallSite({}), { code: 'ERR_INVALID_ARG_TYPE' });

const callSite = getCallSite();
deepStrictEqual(callSite, mapCallSite(callSite));
deepStrictEqual(callSite[0], mapCallSite(callSite[0]));
});


test('module.mapCallSite should reconstruct ts callsite', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-transform-types',
fixtures.path('typescript/ts/test-callsite.ts'),
]);
const output = result.stdout.toString().trim();
strictEqual(result.stderr, '');
match(output, /lineNumber: 9/);
match(output, /column: 18/);
match(output, /typescript\/ts\/test-callsite\.ts/);
strictEqual(result.code, 0);
});

test('module.mapCallSite should reconstruct ts callsite', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--enable-source-maps',
fixtures.path('source-map/minified-map-sourcemap.js'),
]);
const output = result.stdout.toString().trim();
match(output, /functionName: 'foo'/);
strictEqual(result.stderr, '');
strictEqual(result.code, 0);
});
13 changes: 13 additions & 0 deletions test/fixtures/source-map/minified-map-sourcemap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const { getCallSite } = require("node:util"), { mapCallSite } = require("node:module"); function foo() { console.log(mapCallSite(getCallSite()[0])) } foo();
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiZm9vLmpzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJjb25zdCB7IGdldENhbGxTaXRlIH0gPSByZXF1aXJlKCdub2RlOnV0aWwnKTtcbmNvbnN0IHsgbWFwQ2FsbFNpdGUgfSA9IHJlcXVpcmUoJ25vZGU6bW9kdWxlJyk7XG5cbmZ1bmN0aW9uIGZvbygpIHtcbiAgICBjb25zb2xlLmxvZyhtYXBDYWxsU2l0ZShnZXRDYWxsU2l0ZSgpWzBdKSk7XG59XG5cbmZvbygpO1xuIl0sCiAgIm1hcHBpbmdzIjogIkFBQUEsR0FBTSxDQUFFLFlBQUFBLENBQVksRUFBSSxRQUFRLFdBQVcsRUFDckMsQ0FBRSxZQUFBQyxDQUFZLEVBQUksUUFBUSxhQUFhLEVBRTdDLFNBQVNDLEdBQU0sQ0FDWCxRQUFRLElBQUlELEVBQVlELEVBQVksRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUM3QyxDQUVBRSxFQUFJIiwKICAibmFtZXMiOiBbImdldENhbGxTaXRlIiwgIm1hcENhbGxTaXRlIiwgImZvbyJdCn0K

// > npx esbuild foo.js --outfile=foo.min.js --bundle --minify --sourcemap=inline --platform=node
//
// const { getCallSite } = require('node:util');
// const { mapCallSite } = require('node:module');
//
// function foo() {
// console.log(mapCallSite(getCallSite()[0]));
// }
//
// foo();
8 changes: 8 additions & 0 deletions test/fixtures/source-map/output/test-map-callsite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';
require('../../../common');
const fixtures = require('../../../common/fixtures');
const spawn = require('node:child_process').spawn;

spawn(process.execPath,
['--no-warnings', '--experimental-transform-types', fixtures.path('typescript/ts/test-callsite.ts')],
{ stdio: 'inherit' });
12 changes: 12 additions & 0 deletions test/fixtures/source-map/output/test-map-callsite.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Without mapCallSite: [Object: null prototype] {
functionName: '',
scriptName: '*fixtures*typescript*ts*test-callsite.ts',
lineNumber: 3,
column: 18
}
With mapCallSite: [Object: null prototype] {
functionName: '',
lineNumber: 9,
column: 18,
scriptName: 'file:**fixtures*typescript*ts*test-callsite.ts'
}
13 changes: 13 additions & 0 deletions test/fixtures/typescript/ts/test-callsite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const { getCallSite } = require('node:util');
const { mapCallSite } = require('node:module');

interface CallSite {
A;
B;
}

const callSite = getCallSite()[0];

console.log('Without mapCallSite: ', callSite);

console.log('With mapCallSite: ', mapCallSite(callSite));
1 change: 1 addition & 0 deletions test/parallel/test-node-output-sourcemaps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('sourcemaps output', { concurrency: !process.env.TEST_PARALLEL }, () =>
{ name: 'source-map/output/source_map_throw_first_tick.js' },
{ name: 'source-map/output/source_map_throw_icu.js' },
{ name: 'source-map/output/source_map_throw_set_immediate.js' },
{ name: 'source-map/output/test-map-callsite.js' },
];
for (const { name, transform } of tests) {
it(name, async () => {
Expand Down

0 comments on commit 2bc03cf

Please sign in to comment.