Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
lib: add source map support for assert messages
Map source lines in assert messages with cached source maps.
  • Loading branch information
legendecas committed Sep 3, 2025
commit 9e516a35bd638ce1b29a57439e4f0b0763865708
38 changes: 35 additions & 3 deletions lib/internal/errors/error_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ const {
const {
getErrorSourcePositions,
} = internalBinding('errors');
const {
getSourceMapsSupport,
findSourceMap,
getSourceLine,
} = require('internal/source_map/source_map_cache');

/**
* Get the source location of an error.
* Get the source location of an error. If source map is enabled, resolve the source location
* based on the source map.
*
* The `error.stack` must not have been accessed. The resolution is based on the structured
* error stack data.
Expand All @@ -21,10 +27,35 @@ function getErrorSourceLocation(error) {
const pos = getErrorSourcePositions(error);
const {
sourceLine,
scriptResourceName,
lineNumber,
startColumn,
} = pos;

return { sourceLine, startColumn };
// Source map is not enabled. Return the source line directly.
if (!getSourceMapsSupport().enabled) {
return { sourceLine, startColumn };
}

const sm = findSourceMap(scriptResourceName);
if (sm === undefined) {
return;
}
const {
originalLine,
originalColumn,
originalSource,
} = sm.findEntry(lineNumber - 1, startColumn);
const originalSourceLine = getSourceLine(sm, originalSource, originalLine, originalColumn);

if (!originalSourceLine) {
return;
}

return {
sourceLine: originalSourceLine,
startColumn: originalColumn,
};
}

const memberAccessTokens = [ '.', '?.', '[', ']' ];
Expand Down Expand Up @@ -111,7 +142,8 @@ function getFirstExpression(code, startColumn) {
}

/**
* Get the source expression of an error.
* Get the source expression of an error. If source map is enabled, resolve the source location
* based on the source map.
*
* The `error.stack` must not have been accessed, or the source location may be incorrect. The
* resolution is based on the structured error stack data.
Expand Down
52 changes: 6 additions & 46 deletions lib/internal/source_map/prepare_stack_trace.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
'use strict';

const {
ArrayPrototypeIndexOf,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ErrorPrototypeToString,
RegExpPrototypeSymbolSplit,
SafeStringIterator,
StringPrototypeRepeat,
StringPrototypeSlice,
Expand All @@ -16,8 +14,7 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
debug = fn;
});
const { getStringWidth } = require('internal/util/inspect');
const { readFileSync } = require('fs');
const { findSourceMap } = require('internal/source_map/source_map_cache');
const { findSourceMap, getSourceLine } = require('internal/source_map/source_map_cache');
const {
kIsNodeError,
} = require('internal/errors');
Expand Down Expand Up @@ -155,21 +152,13 @@ function getErrorSource(
originalLine,
originalColumn,
) {
const originalSourcePathNoScheme =
StringPrototypeStartsWith(originalSourcePath, 'file://') ?
fileURLToPath(originalSourcePath) : originalSourcePath;
const source = getOriginalSource(
sourceMap.payload,
originalSourcePath,
);
if (typeof source !== 'string') {
return;
}
const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source, originalLine + 1);
const line = lines[originalLine];
const line = getSourceLine(sourceMap, originalSourcePath, originalLine);
if (!line) {
return;
}
const originalSourcePathNoScheme =
StringPrototypeStartsWith(originalSourcePath, 'file://') ?
fileURLToPath(originalSourcePath) : originalSourcePath;

// Display ^ in appropriate position, regardless of whether tabs or
// spaces are used:
Expand All @@ -182,39 +171,10 @@ function getErrorSource(
prefix = StringPrototypeSlice(prefix, 0, -1); // The last character is '^'.

const exceptionLine =
`${originalSourcePathNoScheme}:${originalLine + 1}\n${line}\n${prefix}^\n\n`;
`${originalSourcePathNoScheme}:${originalLine + 1}\n${line}\n${prefix}^\n`;
return exceptionLine;
}

/**
* Retrieve the original source code from the source map's `sources` list or disk.
* @param {import('internal/source_map/source_map').SourceMap.payload} payload
* @param {string} originalSourcePath - path or url of the original source
* @returns {string | undefined} - the source content or undefined if file not found
*/
function getOriginalSource(payload, originalSourcePath) {
let source;
// payload.sources has been normalized to be an array of absolute urls.
const sourceContentIndex =
ArrayPrototypeIndexOf(payload.sources, originalSourcePath);
if (payload.sourcesContent?.[sourceContentIndex]) {
// First we check if the original source content was provided in the
// source map itself:
source = payload.sourcesContent[sourceContentIndex];
} else if (StringPrototypeStartsWith(originalSourcePath, 'file://')) {
// If no sourcesContent was found, attempt to load the original source
// from disk:
debug(`read source of ${originalSourcePath} from filesystem`);
const originalSourcePathNoScheme = fileURLToPath(originalSourcePath);
try {
source = readFileSync(originalSourcePathNoScheme, 'utf8');
} catch (err) {
debug(err);
}
}
return source;
}

/**
* Retrieve exact line in the original source code from the source map's `sources` list or disk.
* @param {string} fileName - actual file name
Expand Down
61 changes: 59 additions & 2 deletions lib/internal/source_map/source_map_cache.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
'use strict';

const {
ArrayPrototypeIndexOf,
ArrayPrototypePush,
JSONParse,
ObjectFreeze,
RegExpPrototypeExec,
RegExpPrototypeSymbolSplit,
SafeMap,
StringPrototypeCodePointAt,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;

// See https://tc39.es/ecma426/ for SourceMap V3 specification.
Expand All @@ -16,6 +19,7 @@ let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
debug = fn;
});

const { readFileSync } = require('fs');
const { validateBoolean, validateObject } = require('internal/validators');
const {
setSourceMapsEnabled: setSourceMapsNative,
Expand Down Expand Up @@ -277,8 +281,7 @@ function lineLengths(content) {
*/
function sourceMapFromFile(mapURL) {
try {
const fs = require('fs');
const content = fs.readFileSync(fileURLToPath(mapURL), 'utf8');
const content = readFileSync(fileURLToPath(mapURL), 'utf8');
const data = JSONParse(content);
return sourcesToAbsolute(mapURL, data);
} catch (err) {
Expand Down Expand Up @@ -400,8 +403,62 @@ function findSourceMap(sourceURL) {
}
}

/**
* Retrieve the original source code from the source map's `sources` list or disk.
* @param {import('internal/source_map/source_map').SourceMap.payload} payload
* @param {string} originalSourcePath - path or url of the original source
* @returns {string | undefined} - the source content or undefined if file not found
*/
function getOriginalSource(payload, originalSourcePath) {
let source;
// payload.sources has been normalized to be an array of absolute urls.
const sourceContentIndex =
ArrayPrototypeIndexOf(payload.sources, originalSourcePath);
if (payload.sourcesContent?.[sourceContentIndex]) {
// First we check if the original source content was provided in the
// source map itself:
source = payload.sourcesContent[sourceContentIndex];
} else if (StringPrototypeStartsWith(originalSourcePath, 'file://')) {
// If no sourcesContent was found, attempt to load the original source
// from disk:
debug(`read source of ${originalSourcePath} from filesystem`);
const originalSourcePathNoScheme = fileURLToPath(originalSourcePath);
try {
source = readFileSync(originalSourcePathNoScheme, 'utf8');
} catch (err) {
debug(err);
}
}
return source;
}

/**
* Get the line of source in the source map.
* @param {import('internal/source_map/source_map').SourceMap} sourceMap
* @param {string} originalSourcePath path or url of the original source
* @param {number} originalLine line number in the original source
* @returns {string|undefined} source line if found
*/
function getSourceLine(
sourceMap,
originalSourcePath,
originalLine,
) {
const source = getOriginalSource(
sourceMap.payload,
originalSourcePath,
);
if (typeof source !== 'string') {
return;
}
const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source, originalLine + 1);
const line = lines[originalLine];
return line;
}

module.exports = {
findSourceMap,
getSourceLine,
getSourceMapsSupport,
setSourceMapsSupport,
maybeCacheSourceMap,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:

assert(false)

at Object.<anonymous> (*/test/fixtures/source-map/output/source_map_assert_source_line.ts:11:3)
*
*
*
*
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
*
*
*
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '==',
diff: 'simple'
}
14 changes: 14 additions & 0 deletions test/fixtures/source-map/output/source_map_assert_source_line.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Flags: --enable-source-maps --experimental-transform-types --no-warnings

require('../../../common');
const assert = require('node:assert');

enum Bar {
makeSureTransformTypes,
}

try {
assert(false);
} catch (e) {
console.log(e);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
throw err
^


Error: an error!
at functionD (*/test/fixtures/source-map/enclosing-call-site.js:16:17)
at functionC (*/test/fixtures/source-map/enclosing-call-site.js:10:3)
Expand Down
1 change: 0 additions & 1 deletion test/fixtures/source-map/output/source_map_eval.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
alert "I knew it!"
^


ReferenceError: alert is not defined
at Object.eval (*/synthesized/workspace/tabs-source-url.coffee:26:2)
at eval (*/synthesized/workspace/tabs-source-url.coffee:1:14)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
alert "I knew it!"
^


ReferenceError: alert is not defined
at Object.<anonymous> (*/test/fixtures/source-map/tabs.coffee:26:2)
at Object.<anonymous> (*/test/fixtures/source-map/tabs.coffee:1:14)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
throw new Error('message')
^


Error: message
at Throw (*/test/fixtures/source-map/output/source_map_throw_async_stack_trace.mts:13:9)
at async Promise.all (index 3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
throw new Error('message');
^


Error: message
at new Foo (*/test/fixtures/source-map/output/source_map_throw_construct.mts:13:11)
at <anonymous> (*/test/fixtures/source-map/output/source_map_throw_construct.mts:17:1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ reachable
throw Error('an exception');
^


Error: an exception
at branch (*/test/fixtures/source-map/typescript-throw.ts:18:11)
at Object.<anonymous> (*/test/fixtures/source-map/typescript-throw.ts:24:1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
("あ 🐕 🐕", throw Error("an error"));
^


Error: an error
at Object.createElement (*/test/fixtures/source-map/icu.jsx:3:23)
at Object.<anonymous> (*/test/fixtures/source-map/icu.jsx:9:5)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
throw Error('goodbye');
^


Error: goodbye
at Hello (*/test/fixtures/source-map/uglify-throw-original.js:5:9)
at Immediate.<anonymous> (*/test/fixtures/source-map/uglify-throw-original.js:9:3)
Expand Down
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 @@ -14,6 +14,7 @@ describe('sourcemaps output', { concurrency: !process.env.TEST_PARALLEL }, () =>
);

const tests = [
{ name: 'source-map/output/source_map_assert_source_line.ts' },
{ name: 'source-map/output/source_map_disabled_by_api.js' },
{ name: 'source-map/output/source_map_disabled_by_process_api.js' },
{ name: 'source-map/output/source_map_enabled_by_api.js' },
Expand Down
Loading