Skip to content

Commit e2a6399

Browse files
committed
esm: refine ERR_REQUIRE_ESM errors
PR-URL: #39175 Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 499f693 commit e2a6399

File tree

9 files changed

+182
-63
lines changed

9 files changed

+182
-63
lines changed

lib/internal/errors.js

+56-15
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
AggregateError,
1515
ArrayFrom,
1616
ArrayIsArray,
17+
ArrayPrototypeFilter,
1718
ArrayPrototypeIncludes,
1819
ArrayPrototypeIndexOf,
1920
ArrayPrototypeJoin,
@@ -788,6 +789,34 @@ const fatalExceptionStackEnhancers = {
788789
}
789790
};
790791

792+
// Ensures the printed error line is from user code.
793+
let _kArrowMessagePrivateSymbol, _setHiddenValue;
794+
function setArrowMessage(err, arrowMessage) {
795+
if (!_kArrowMessagePrivateSymbol) {
796+
({
797+
arrow_message_private_symbol: _kArrowMessagePrivateSymbol,
798+
setHiddenValue: _setHiddenValue,
799+
} = internalBinding('util'));
800+
}
801+
_setHiddenValue(err, _kArrowMessagePrivateSymbol, arrowMessage);
802+
}
803+
804+
// Hide stack lines before the first user code line.
805+
function hideInternalStackFrames(error) {
806+
overrideStackTrace.set(error, (error, stackFrames) => {
807+
let frames = stackFrames;
808+
if (typeof stackFrames === 'object') {
809+
frames = ArrayPrototypeFilter(
810+
stackFrames,
811+
(frm) => !StringPrototypeStartsWith(frm.getFileName(),
812+
'node:internal')
813+
);
814+
}
815+
ArrayPrototypeUnshift(frames, error);
816+
return ArrayPrototypeJoin(frames, '\n at ');
817+
});
818+
}
819+
791820
// Node uses an AbortError that isn't exactly the same as the DOMException
792821
// to make usage of the error in userland and readable-stream easier.
793822
// It is a regular error with `.code` and `.name`.
@@ -806,8 +835,10 @@ module.exports = {
806835
exceptionWithHostPort,
807836
getMessage,
808837
hideStackFrames,
838+
hideInternalStackFrames,
809839
isErrorStackTraceLimitWritable,
810840
isStackOverflowError,
841+
setArrowMessage,
811842
connResetException,
812843
uvErrmapGet,
813844
uvException,
@@ -842,6 +873,7 @@ module.exports = {
842873
// Note: Please try to keep these in alphabetical order
843874
//
844875
// Note: Node.js specific errors must begin with the prefix ERR_
876+
845877
E('ERR_AMBIGUOUS_ARGUMENT', 'The "%s" argument is ambiguous. %s', TypeError);
846878
E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError);
847879
E('ERR_ASSERTION', '%s', Error);
@@ -1406,23 +1438,32 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
14061438
'%d is not a valid timestamp', TypeError);
14071439
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError);
14081440
E('ERR_REQUIRE_ESM',
1409-
(filename, parentPath = null, packageJsonPath = null) => {
1410-
let msg = `Must use import to load ES Module: ${filename}`;
1411-
if (parentPath && packageJsonPath) {
1412-
const path = require('path');
1413-
const basename = path.basename(filename) === path.basename(parentPath) ?
1414-
filename : path.basename(filename);
1415-
msg +=
1416-
'\nrequire() of ES modules is not supported.\nrequire() of ' +
1417-
`${filename} from ${parentPath} ` +
1418-
'is an ES module file as it is a .js file whose nearest parent ' +
1419-
'package.json contains "type": "module" which defines all .js ' +
1420-
'files in that package scope as ES modules.\nInstead rename ' +
1421-
`${basename} to end in .cjs, change the requiring code to use ` +
1422-
'import(), or remove "type": "module" from ' +
1423-
`${packageJsonPath}.\n`;
1441+
function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) {
1442+
hideInternalStackFrames(this);
1443+
let msg = `require() of ES Module ${filename}${parentPath ? ` from ${
1444+
parentPath}` : ''} not supported.`;
1445+
if (!packageJsonPath) {
1446+
if (StringPrototypeEndsWith(filename, '.mjs'))
1447+
msg += `\nInstead change the require of ${filename} to a dynamic ` +
1448+
'import() which is available in all CommonJS modules.';
1449+
return msg;
1450+
}
1451+
const path = require('path');
1452+
const basename = path.basename(filename) === path.basename(parentPath) ?
1453+
filename : path.basename(filename);
1454+
if (hasEsmSyntax) {
1455+
msg += `\nInstead change the require of ${basename} in ${parentPath} to` +
1456+
' a dynamic import() which is available in all CommonJS modules.';
14241457
return msg;
14251458
}
1459+
msg += `\n${basename} is treated as an ES module file as it is a .js ` +
1460+
'file whose nearest parent package.json contains "type": "module" ' +
1461+
'which declares all .js files in that package scope as ES modules.' +
1462+
`\nInstead rename ${basename} to end in .cjs, change the requiring ` +
1463+
'code to use dynamic import() which is available in all CommonJS ' +
1464+
'modules, or change "type": "module" to "type": "commonjs" in ' +
1465+
`${packageJsonPath} to treat all .js files as CommonJS (using .mjs for ` +
1466+
'all ES modules instead).\n';
14261467
return msg;
14271468
}, Error);
14281469
E('ERR_SCRIPT_EXECUTION_INTERRUPTED',

lib/internal/modules/cjs/helpers.js

+18
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const {
44
ArrayPrototypeForEach,
55
ArrayPrototypeJoin,
6+
ArrayPrototypeSome,
67
ObjectDefineProperty,
78
ObjectPrototypeHasOwnProperty,
89
SafeMap,
@@ -184,9 +185,26 @@ function normalizeReferrerURL(referrer) {
184185
return new URL(referrer).href;
185186
}
186187

188+
// For error messages only - used to check if ESM syntax is in use.
189+
function hasEsmSyntax(code) {
190+
const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser;
191+
let root;
192+
try {
193+
root = parser.parse(code, { sourceType: 'module', ecmaVersion: 'latest' });
194+
} catch {
195+
return false;
196+
}
197+
198+
return ArrayPrototypeSome(root.body, (stmt) =>
199+
stmt.type === 'ExportNamedDeclaration' ||
200+
stmt.type === 'ImportDeclaration' ||
201+
stmt.type === 'ExportAllDeclaration');
202+
}
203+
187204
module.exports = {
188205
addBuiltinLibsToObject,
189206
cjsConditions,
207+
hasEsmSyntax,
190208
loadNativeModule,
191209
makeRequireFunction,
192210
normalizeReferrerURL,

lib/internal/modules/cjs/loader.js

+47-18
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const {
4848
Proxy,
4949
ReflectApply,
5050
ReflectSet,
51+
RegExpPrototypeExec,
5152
RegExpPrototypeTest,
5253
SafeMap,
5354
SafeWeakMap,
@@ -58,6 +59,7 @@ const {
5859
StringPrototypeLastIndexOf,
5960
StringPrototypeIndexOf,
6061
StringPrototypeMatch,
62+
StringPrototypeRepeat,
6163
StringPrototypeSlice,
6264
StringPrototypeSplit,
6365
StringPrototypeStartsWith,
@@ -88,11 +90,12 @@ const { internalModuleStat } = internalBinding('fs');
8890
const packageJsonReader = require('internal/modules/package_json_reader');
8991
const { safeGetenv } = internalBinding('credentials');
9092
const {
93+
cjsConditions,
94+
hasEsmSyntax,
95+
loadNativeModule,
9196
makeRequireFunction,
9297
normalizeReferrerURL,
9398
stripBOM,
94-
cjsConditions,
95-
loadNativeModule
9699
} = require('internal/modules/cjs/helpers');
97100
const { getOptionValue } = require('internal/options');
98101
const preserveSymlinks = getOptionValue('--preserve-symlinks');
@@ -107,11 +110,14 @@ const policy = getOptionValue('--experimental-policy') ?
107110
let hasLoadedAnyUserCJSModule = false;
108111

109112
const {
110-
ERR_INVALID_ARG_VALUE,
111-
ERR_INVALID_MODULE_SPECIFIER,
112-
ERR_REQUIRE_ESM,
113-
ERR_UNKNOWN_BUILTIN_MODULE,
114-
} = require('internal/errors').codes;
113+
codes: {
114+
ERR_INVALID_ARG_VALUE,
115+
ERR_INVALID_MODULE_SPECIFIER,
116+
ERR_REQUIRE_ESM,
117+
ERR_UNKNOWN_BUILTIN_MODULE,
118+
},
119+
setArrowMessage,
120+
} = require('internal/errors');
115121
const { validateString } = require('internal/validators');
116122
const pendingDeprecation = getOptionValue('--pending-deprecation');
117123

@@ -970,7 +976,7 @@ Module.prototype.load = function(filename) {
970976
const extension = findLongestRegisteredExtension(filename);
971977
// allow .mjs to be overridden
972978
if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs'])
973-
throw new ERR_REQUIRE_ESM(filename);
979+
throw new ERR_REQUIRE_ESM(filename, true);
974980

975981
Module._extensions[extension](this, filename);
976982
this.loaded = true;
@@ -1102,16 +1108,6 @@ Module.prototype._compile = function(content, filename) {
11021108

11031109
// Native extension for .js
11041110
Module._extensions['.js'] = function(module, filename) {
1105-
if (StringPrototypeEndsWith(filename, '.js')) {
1106-
const pkg = readPackageScope(filename);
1107-
// Function require shouldn't be used in ES modules.
1108-
if (pkg?.data?.type === 'module') {
1109-
const parent = moduleParentCache.get(module);
1110-
const parentPath = parent?.filename;
1111-
const packageJsonPath = path.resolve(pkg.path, 'package.json');
1112-
throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
1113-
}
1114-
}
11151111
// If already analyzed the source, then it will be cached.
11161112
const cached = cjsParseCache.get(module);
11171113
let content;
@@ -1121,6 +1117,39 @@ Module._extensions['.js'] = function(module, filename) {
11211117
} else {
11221118
content = fs.readFileSync(filename, 'utf8');
11231119
}
1120+
if (StringPrototypeEndsWith(filename, '.js')) {
1121+
const pkg = readPackageScope(filename);
1122+
// Function require shouldn't be used in ES modules.
1123+
if (pkg?.data?.type === 'module') {
1124+
const parent = moduleParentCache.get(module);
1125+
const parentPath = parent?.filename;
1126+
const packageJsonPath = path.resolve(pkg.path, 'package.json');
1127+
const usesEsm = hasEsmSyntax(content);
1128+
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
1129+
packageJsonPath);
1130+
// Attempt to reconstruct the parent require frame.
1131+
if (Module._cache[parentPath]) {
1132+
let parentSource;
1133+
try {
1134+
parentSource = fs.readFileSync(parentPath, 'utf8');
1135+
} catch {}
1136+
if (parentSource) {
1137+
const errLine = StringPrototypeSplit(
1138+
StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
1139+
err.stack, ' at ')), '\n', 1)[0];
1140+
const { 1: line, 2: col } =
1141+
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
1142+
if (line && col) {
1143+
const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
1144+
const frame = `${parentPath}:${line}\n${srcLine}\n${
1145+
StringPrototypeRepeat(' ', col - 1)}^\n`;
1146+
setArrowMessage(err, frame);
1147+
}
1148+
}
1149+
}
1150+
throw err;
1151+
}
1152+
}
11241153
module._compile(content, filename);
11251154
};
11261155

src/node_errors.cc

+6
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ void AppendExceptionLine(Environment* env,
215215
Local<Object> err_obj;
216216
if (!er.IsEmpty() && er->IsObject()) {
217217
err_obj = er.As<Object>();
218+
// If arrow_message is already set, skip.
219+
auto maybe_value = err_obj->GetPrivate(env->context(),
220+
env->arrow_message_private_symbol());
221+
Local<Value> lvalue;
222+
if (!maybe_value.ToLocal(&lvalue) || lvalue->IsString())
223+
return;
218224
}
219225

220226
bool added_exception_line = false;

test/es-module/test-cjs-esm-warn.js

+50-27
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,59 @@ const { spawn } = require('child_process');
66
const assert = require('assert');
77
const path = require('path');
88

9-
const requiring = path.resolve(fixtures.path('/es-modules/cjs-esm.js'));
10-
const required = path.resolve(
11-
fixtures.path('/es-modules/package-type-module/cjs.js')
12-
);
9+
const requiringCjsAsEsm = path.resolve(fixtures.path('/es-modules/cjs-esm.js'));
10+
const requiringEsm = path.resolve(fixtures.path('/es-modules/cjs-esm-esm.js'));
1311
const pjson = path.resolve(
1412
fixtures.path('/es-modules/package-type-module/package.json')
1513
);
1614

17-
const basename = 'cjs.js';
15+
{
16+
const required = path.resolve(
17+
fixtures.path('/es-modules/package-type-module/cjs.js')
18+
);
19+
const basename = 'cjs.js';
20+
const child = spawn(process.execPath, [requiringCjsAsEsm]);
21+
let stderr = '';
22+
child.stderr.setEncoding('utf8');
23+
child.stderr.on('data', (data) => {
24+
stderr += data;
25+
});
26+
child.on('close', common.mustCall((code, signal) => {
27+
assert.strictEqual(code, 1);
28+
assert.strictEqual(signal, null);
29+
30+
assert.ok(stderr.replaceAll('\r', '').includes(
31+
`Error [ERR_REQUIRE_ESM]: require() of ES Module ${required} from ${
32+
requiringCjsAsEsm} not supported.\n`));
33+
assert.ok(stderr.replaceAll('\r', '').includes(
34+
`Instead rename ${basename} to end in .cjs, change the requiring ` +
35+
'code to use dynamic import() which is available in all CommonJS ' +
36+
`modules, or change "type": "module" to "type": "commonjs" in ${pjson} to ` +
37+
'treat all .js files as CommonJS (using .mjs for all ES modules ' +
38+
'instead).\n'));
39+
}));
40+
}
1841

19-
const child = spawn(process.execPath, [requiring]);
20-
let stderr = '';
21-
child.stderr.setEncoding('utf8');
22-
child.stderr.on('data', (data) => {
23-
stderr += data;
24-
});
25-
child.on('close', common.mustCall((code, signal) => {
26-
assert.strictEqual(code, 1);
27-
assert.strictEqual(signal, null);
42+
{
43+
const required = path.resolve(
44+
fixtures.path('/es-modules/package-type-module/esm.js')
45+
);
46+
const basename = 'esm.js';
47+
const child = spawn(process.execPath, [requiringEsm]);
48+
let stderr = '';
49+
child.stderr.setEncoding('utf8');
50+
child.stderr.on('data', (data) => {
51+
stderr += data;
52+
});
53+
child.on('close', common.mustCall((code, signal) => {
54+
assert.strictEqual(code, 1);
55+
assert.strictEqual(signal, null);
2856

29-
assert.ok(stderr.replace(/\r/g, '').includes(
30-
`Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: ${required}` +
31-
'\nrequire() of ES modules is not supported.\nrequire() of ' +
32-
`${required} from ${requiring} ` +
33-
'is an ES module file as it is a .js file whose nearest parent ' +
34-
'package.json contains "type": "module" which defines all .js ' +
35-
'files in that package scope as ES modules.\nInstead rename ' +
36-
`${basename} to end in .cjs, change the requiring code to use ` +
37-
'import(), or remove "type": "module" from ' +
38-
`${pjson}.\n`));
39-
assert.ok(stderr.includes(
40-
'Error [ERR_REQUIRE_ESM]: Must use import to load ES Module'));
41-
}));
57+
assert.ok(stderr.replace(/\r/g, '').includes(
58+
`Error [ERR_REQUIRE_ESM]: require() of ES Module ${required} from ${
59+
requiringEsm} not supported.\n`));
60+
assert.ok(stderr.replace(/\r/g, '').includes(
61+
`Instead change the require of ${basename} in ${requiringEsm} to` +
62+
' a dynamic import() which is available in all CommonJS modules.\n'));
63+
}));
64+
}

test/es-module/test-esm-type-flag-errors.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ try {
2929
} catch (e) {
3030
assert.strictEqual(e.name, 'Error');
3131
assert.strictEqual(e.code, 'ERR_REQUIRE_ESM');
32-
assert(e.toString().match(/Must use import to load ES Module/g));
33-
assert(e.message.match(/Must use import to load ES Module/g));
32+
assert(e.toString().match(/require\(\) of ES Module/g));
33+
assert(e.message.match(/require\(\) of ES Module/g));
3434
}
3535

3636
function expect(opt = '', inputFile, want, wantsError = false) {
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require('./package-type-module/esm.js');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export var p = 5;

test/parallel/test-require-mjs.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const assert = require('assert');
55
assert.throws(
66
() => require('../fixtures/es-modules/test-esm-ok.mjs'),
77
{
8-
message: /Must use import to load ES Module/,
8+
message: /dynamic import\(\) which is available in all CommonJS modules/,
99
code: 'ERR_REQUIRE_ESM'
1010
}
1111
);

0 commit comments

Comments
 (0)