Skip to content

Commit 00bcf8f

Browse files
committed
module: fix wrong error annotation for require of ESM
When a CommonJS module requires an ES module with --no-experimental-require-module, the error annotation (arrow message) was pointing to an internal frame (TracingChannel.traceSync in node:diagnostics_channel) instead of the user's actual require() call. This happened because reconstructErrorStack() was always picking the first 'at' frame from the error stack trace. When the require() call is wrapped by TracingChannel.traceSync (added in v22.4.0 via wrapModuleLoad), the first frame is the internal tracing wrapper, not the user's code. Fix reconstructErrorStack() to search the stack frames for one that matches the parent module's file path, instead of blindly using the first frame. This ensures the error annotation correctly points to the user's require() call. Also remove a TODO comment that this change addresses. Fixes: #55350
1 parent 9f0a3e6 commit 00bcf8f

2 files changed

Lines changed: 121 additions & 30 deletions

File tree

lib/internal/modules/cjs/loader.js

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -444,13 +444,13 @@ ObjectDefineProperty(Module.prototype, 'parent', {
444444
get: pendingDeprecate(
445445
getModuleParent,
446446
'module.parent is deprecated due to accuracy issues. Please use ' +
447-
'require.main to find program entry point instead.',
447+
'require.main to find program entry point instead.',
448448
'DEP0144',
449449
),
450450
set: pendingDeprecate(
451451
setModuleParent,
452452
'module.parent is deprecated due to accuracy issues. Please use ' +
453-
'require.main to find program entry point instead.',
453+
'require.main to find program entry point instead.',
454454
'DEP0144',
455455
),
456456
});
@@ -539,7 +539,7 @@ function tryPackage(requestPath, exts, isMain, originalPath) {
539539
} else {
540540
process.emitWarning(
541541
`Invalid 'main' field in '${pjsonPath}' of '${pkg}'. ` +
542-
'Please either fix that or report it to the module author',
542+
'Please either fix that or report it to the module author',
543543
'DeprecationWarning',
544544
'DEP0128',
545545
);
@@ -814,7 +814,7 @@ Module._findPath = function(request, paths, isMain, conditions = getCjsCondition
814814
};
815815

816816
/** `node_modules` character codes reversed */
817-
const nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ];
817+
const nmChars = [115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110];
818818
const nmLen = nmChars.length;
819819
if (isWindows) {
820820
/**
@@ -833,8 +833,8 @@ if (isWindows) {
833833
// return root node_modules when path is 'D:\\'.
834834
// path.resolve will make sure from.length >=3 in Windows.
835835
if (StringPrototypeCharCodeAt(from, from.length - 1) ===
836-
CHAR_BACKWARD_SLASH &&
837-
StringPrototypeCharCodeAt(from, from.length - 2) === CHAR_COLON) {
836+
CHAR_BACKWARD_SLASH &&
837+
StringPrototypeCharCodeAt(from, from.length - 2) === CHAR_COLON) {
838838
return [from + 'node_modules'];
839839
}
840840

@@ -848,8 +848,8 @@ if (isWindows) {
848848
// path for drive root like 'C:\node_modules' and don't need to
849849
// parse drive name.
850850
if (code === CHAR_BACKWARD_SLASH ||
851-
code === CHAR_FORWARD_SLASH ||
852-
code === CHAR_COLON) {
851+
code === CHAR_FORWARD_SLASH ||
852+
code === CHAR_COLON) {
853853
if (p !== nmLen) {
854854
ArrayPrototypePush(
855855
paths,
@@ -930,7 +930,7 @@ Module._resolveLookupPaths = function(request, parent) {
930930

931931
// Check for node modules paths.
932932
if (StringPrototypeCharAt(request, 0) !== '.' ||
933-
(request.length > 1 &&
933+
(request.length > 1 &&
934934
StringPrototypeCharAt(request, 1) !== '.' &&
935935
StringPrototypeCharAt(request, 1) !== '/' &&
936936
(!isWindows || StringPrototypeCharAt(request, 1) !== '\\'))) {
@@ -1019,13 +1019,13 @@ function getExportsForCircularRequire(module) {
10191019
}
10201020

10211021
if (module.exports &&
1022-
!isProxy(module.exports) &&
1023-
ObjectGetPrototypeOf(module.exports) === ObjectPrototype &&
1024-
// Exclude transpiled ES6 modules / TypeScript code because those may
1025-
// employ unusual patterns for accessing 'module.exports'. That should
1026-
// be okay because ES6 modules have a different approach to circular
1027-
// dependencies anyway.
1028-
!module.exports.__esModule) {
1022+
!isProxy(module.exports) &&
1023+
ObjectGetPrototypeOf(module.exports) === ObjectPrototype &&
1024+
// Exclude transpiled ES6 modules / TypeScript code because those may
1025+
// employ unusual patterns for accessing 'module.exports'. That should
1026+
// be okay because ES6 modules have a different approach to circular
1027+
// dependencies anyway.
1028+
!module.exports.__esModule) {
10291029
// This is later unset once the module is done loading.
10301030
ObjectSetPrototypeOf(
10311031
module.exports, CircularRequirePrototypeWarningProxy);
@@ -1367,9 +1367,9 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty
13671367
}
13681368
}
13691369
} else if (module.exports &&
1370-
!isProxy(module.exports) &&
1371-
ObjectGetPrototypeOf(module.exports) ===
1372-
CircularRequirePrototypeWarningProxy) {
1370+
!isProxy(module.exports) &&
1371+
ObjectGetPrototypeOf(module.exports) ===
1372+
CircularRequirePrototypeWarningProxy) {
13731373
ObjectSetPrototypeOf(module.exports, ObjectPrototype);
13741374
}
13751375
}
@@ -1451,7 +1451,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
14511451
const selfResolved = trySelf(parentPath, request, conditions);
14521452
if (selfResolved) {
14531453
const cacheKey = request + '\x00' +
1454-
(paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00'));
1454+
(paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00'));
14551455
Module._pathCache[cacheKey] = selfResolved;
14561456
return selfResolved;
14571457
}
@@ -1469,7 +1469,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
14691469
let message = `Cannot find module '${request}'`;
14701470
if (requireStack.length > 0) {
14711471
message = message + '\nRequire stack:\n- ' +
1472-
ArrayPrototypeJoin(requireStack, '\n- ');
1472+
ArrayPrototypeJoin(requireStack, '\n- ');
14731473
}
14741474
// eslint-disable-next-line no-restricted-syntax
14751475
const err = new Error(message);
@@ -1872,9 +1872,49 @@ function loadSource(mod, filename, formatFromNode) {
18721872
}
18731873

18741874
function reconstructErrorStack(err, parentPath, parentSource) {
1875-
const errLine = StringPrototypeSplit(
1876-
StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
1877-
err.stack, ' at ')), '\n', 1)[0];
1875+
// Find the stack frame that matches the parent module path.
1876+
// We cannot simply use the first frame because internal wrappers
1877+
// like TracingChannel.traceSync may appear before the user's frame.
1878+
const stackLines = StringPrototypeSplit(err.stack, '\n');
1879+
let errLine;
1880+
for (let i = 0; i < stackLines.length; i++) {
1881+
if (StringPrototypeIndexOf(stackLines[i], parentPath) !== -1) {
1882+
errLine = stackLines[i];
1883+
break;
1884+
}
1885+
}
1886+
// Fallback: if no frame matched the parent path, prefer a user-land
1887+
// frame (skip node:internal/* and other node:-scheme frames) so the
1888+
// annotation points at user code, not internal wrappers.
1889+
if (errLine === undefined) {
1890+
for (let i = 0; i < stackLines.length; i++) {
1891+
const line = stackLines[i];
1892+
if (StringPrototypeIndexOf(line, ' at ') === -1) {
1893+
continue;
1894+
}
1895+
if (StringPrototypeIndexOf(line, 'node:internal/') !== -1) {
1896+
continue;
1897+
}
1898+
if (StringPrototypeIndexOf(line, '(node:') !== -1) {
1899+
continue;
1900+
}
1901+
errLine = line;
1902+
break;
1903+
}
1904+
}
1905+
// Last resort: if all frames are internal, use the first 'at' frame
1906+
// so the user still gets some error annotation rather than none.
1907+
if (errLine === undefined) {
1908+
for (let i = 0; i < stackLines.length; i++) {
1909+
if (StringPrototypeIndexOf(stackLines[i], ' at ') !== -1) {
1910+
errLine = stackLines[i];
1911+
break;
1912+
}
1913+
}
1914+
}
1915+
if (errLine === undefined) {
1916+
return;
1917+
}
18781918
const { 1: line, 2: col } =
18791919
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
18801920
if (line && col) {
@@ -1910,7 +1950,6 @@ function getRequireESMError(mod, pkg, content, filename) {
19101950
// Continue regardless of error.
19111951
}
19121952
if (parentSource) {
1913-
// TODO(joyeecheung): trim off internal frames from the stack.
19141953
reconstructErrorStack(err, parentPath, parentSource);
19151954
}
19161955
}
@@ -2036,7 +2075,7 @@ function createRequire(filenameOrURL) {
20362075
let filepath, fileURL;
20372076

20382077
if (isURL(filenameOrURL) ||
2039-
(typeof filenameOrURL === 'string' && !path.isAbsolute(filenameOrURL))) {
2078+
(typeof filenameOrURL === 'string' && !path.isAbsolute(filenameOrURL))) {
20402079
try {
20412080
// It might be an URL, try to convert it.
20422081
// If it's a relative path, it would not parse and would be considered invalid per
@@ -2064,10 +2103,10 @@ function isRelative(path) {
20642103
if (StringPrototypeCharCodeAt(path, 0) !== CHAR_DOT) { return false; }
20652104

20662105
return path.length === 1 || path === '..' ||
2067-
StringPrototypeStartsWith(path, './') ||
2068-
StringPrototypeStartsWith(path, '../') ||
2069-
((isWindows && StringPrototypeStartsWith(path, '.\\')) ||
2070-
StringPrototypeStartsWith(path, '..\\'));
2106+
StringPrototypeStartsWith(path, './') ||
2107+
StringPrototypeStartsWith(path, '../') ||
2108+
((isWindows && StringPrototypeStartsWith(path, '.\\')) ||
2109+
StringPrototypeStartsWith(path, '..\\'));
20712110
}
20722111

20732112
Module.createRequire = createRequire;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
// This test verifies that when a CommonJS module requires an ES module,
4+
// the error annotation (arrow message) points to the user's require()
5+
// call in their source file, not to an internal frame such as
6+
// TracingChannel.traceSync in node:diagnostics_channel.
7+
// Regression test for https://github.com/nodejs/node/issues/55350.
8+
9+
const { spawnPromisified } = require('../common');
10+
const fixtures = require('../common/fixtures.js');
11+
const assert = require('node:assert');
12+
const path = require('node:path');
13+
const { execPath } = require('node:process');
14+
const { describe, it } = require('node:test');
15+
16+
const requiringEsm = path.resolve(
17+
fixtures.path('/es-modules/cjs-esm-esm.js')
18+
);
19+
20+
describe('ERR_REQUIRE_ESM error annotation', { concurrency: !process.env.TEST_PARALLEL }, () => {
21+
it('should point to the user require() call, not internal frames', async () => {
22+
const { code, stderr } = await spawnPromisified(execPath, [
23+
'--no-experimental-require-module', requiringEsm,
24+
]);
25+
26+
assert.strictEqual(code, 1);
27+
28+
const stderrStr = stderr.replaceAll('\r', '');
29+
30+
// The error annotation should reference the user's file, not
31+
// node:diagnostics_channel or any other internal module.
32+
assert.ok(
33+
stderrStr.includes(requiringEsm),
34+
`Expected error output to reference ${requiringEsm}, got:\n${stderrStr}`
35+
);
36+
37+
// The error annotation line should contain the path to the requiring
38+
// file. Do not assume it is the very first stderr line — internal
39+
// throw-location lines may precede the arrow annotation.
40+
const lines = stderrStr.split('\n');
41+
const fileAnnotationLine = lines.find((l) => l.includes(requiringEsm));
42+
assert.ok(
43+
fileAnnotationLine,
44+
`Expected an annotation line referencing the requiring file, got:\n` +
45+
lines.slice(0, 10).join('\n')
46+
);
47+
assert.ok(
48+
!fileAnnotationLine.includes('diagnostics_channel'),
49+
`Annotation line should not reference diagnostics_channel, got: "${fileAnnotationLine}"`
50+
);
51+
});
52+
});

0 commit comments

Comments
 (0)