Skip to content

Commit 93eff3f

Browse files
guybedfordBethGriggs
authored andcommitted
esm: refine ERR_REQUIRE_ESM errors
PR-URL: #39175 Reviewed-By: Bradley Farias <bradley.meck@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 9b15e5c commit 93eff3f

9 files changed

Lines changed: 182 additions & 63 deletions

File tree

‎lib/internal/errors.js‎

Lines changed: 56 additions & 15 deletions
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+
functionsetArrowMessage(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+
functionhideInternalStackFrames(error){
806+
overrideStackTrace.set(error,(error,stackFrames)=>{
807+
letframes=stackFrames;
808+
if(typeofstackFrames==='object'){
809+
frames=ArrayPrototypeFilter(
810+
stackFrames,
811+
(frm)=>!StringPrototypeStartsWith(frm.getFileName(),
812+
'node:internal')
813+
);
814+
}
815+
ArrayPrototypeUnshift(frames,error);
816+
returnArrayPrototypeJoin(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-
letmsg=`Must use import to load ES Module: ${filename}`;
1411-
if(parentPath&&packageJsonPath){
1412-
constpath=require('path');
1413-
constbasename=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+
letmsg=`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+
returnmsg;
1450+
}
1451+
constpath=require('path');
1452+
constbasename=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
returnmsg;
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
returnmsg;
14271468
},Error);
14281469
E('ERR_SCRIPT_EXECUTION_INTERRUPTED',

‎lib/internal/modules/cjs/helpers.js‎

Lines changed: 18 additions & 0 deletions
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
returnnewURL(referrer).href;
185186
}
186187

188+
// For error messages only - used to check if ESM syntax is in use.
189+
functionhasEsmSyntax(code){
190+
constparser=require('internal/deps/acorn/acorn/dist/acorn').Parser;
191+
letroot;
192+
try{
193+
root=parser.parse(code,{sourceType: 'module',ecmaVersion: 'latest'});
194+
}catch{
195+
returnfalse;
196+
}
197+
198+
returnArrayPrototypeSome(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‎

Lines changed: 47 additions & 18 deletions
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
constpackageJsonReader=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
constpreserveSymlinks=getOptionValue('--preserve-symlinks');
@@ -107,11 +110,14 @@ const policy = getOptionValue('--experimental-policy') ?
107110
lethasLoadedAnyUserCJSModule=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
constpendingDeprecation=getOptionValue('--pending-deprecation');
117123

@@ -970,7 +976,7 @@ Module.prototype.load = function(filename) {
970976
constextension=findLongestRegisteredExtension(filename);
971977
// allow .mjs to be overridden
972978
if(StringPrototypeEndsWith(filename,'.mjs')&&!Module._extensions['.mjs'])
973-
thrownewERR_REQUIRE_ESM(filename);
979+
thrownewERR_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-
constpkg=readPackageScope(filename);
1107-
// Function require shouldn't be used in ES modules.
1108-
if(pkg?.data?.type==='module'){
1109-
constparent=moduleParentCache.get(module);
1110-
constparentPath=parent?.filename;
1111-
constpackageJsonPath=path.resolve(pkg.path,'package.json');
1112-
thrownewERR_REQUIRE_ESM(filename,parentPath,packageJsonPath);
1113-
}
1114-
}
11151111
// If already analyzed the source, then it will be cached.
11161112
constcached=cjsParseCache.get(module);
11171113
letcontent;
@@ -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+
constpkg=readPackageScope(filename);
1122+
// Function require shouldn't be used in ES modules.
1123+
if(pkg?.data?.type==='module'){
1124+
constparent=moduleParentCache.get(module);
1125+
constparentPath=parent?.filename;
1126+
constpackageJsonPath=path.resolve(pkg.path,'package.json');
1127+
constusesEsm=hasEsmSyntax(content);
1128+
consterr=newERR_REQUIRE_ESM(filename,usesEsm,parentPath,
1129+
packageJsonPath);
1130+
// Attempt to reconstruct the parent require frame.
1131+
if(Module._cache[parentPath]){
1132+
letparentSource;
1133+
try{
1134+
parentSource=fs.readFileSync(parentPath,'utf8');
1135+
}catch{}
1136+
if(parentSource){
1137+
consterrLine=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+
constsrcLine=StringPrototypeSplit(parentSource,'\n')[line-1];
1144+
constframe=`${parentPath}:${line}\n${srcLine}\n${
1145+
StringPrototypeRepeat(' ',col-1)}^\n`;
1146+
setArrowMessage(err,frame);
1147+
}
1148+
}
1149+
}
1150+
throwerr;
1151+
}
1152+
}
11241153
module._compile(content,filename);
11251154
};
11261155

‎src/node_errors.cc‎

Lines changed: 6 additions & 0 deletions
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‎

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,59 @@ const { spawn } = require('child_process');
66
constassert=require('assert');
77
constpath=require('path');
88

9-
constrequiring=path.resolve(fixtures.path('/es-modules/cjs-esm.js'));
10-
constrequired=path.resolve(
11-
fixtures.path('/es-modules/package-type-module/cjs.js')
12-
);
9+
constrequiringCjsAsEsm=path.resolve(fixtures.path('/es-modules/cjs-esm.js'));
10+
constrequiringEsm=path.resolve(fixtures.path('/es-modules/cjs-esm-esm.js'));
1311
constpjson=path.resolve(
1412
fixtures.path('/es-modules/package-type-module/package.json')
1513
);
1614

17-
constbasename='cjs.js';
15+
{
16+
constrequired=path.resolve(
17+
fixtures.path('/es-modules/package-type-module/cjs.js')
18+
);
19+
constbasename='cjs.js';
20+
constchild=spawn(process.execPath,[requiringCjsAsEsm]);
21+
letstderr='';
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-
constchild=spawn(process.execPath,[requiring]);
20-
letstderr='';
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+
constrequired=path.resolve(
44+
fixtures.path('/es-modules/package-type-module/esm.js')
45+
);
46+
constbasename='esm.js';
47+
constchild=spawn(process.execPath,[requiringEsm]);
48+
letstderr='';
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‎

Lines changed: 2 additions & 2 deletions
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(/MustuseimporttoloadESModule/g));
33-
assert(e.message.match(/MustuseimporttoloadESModule/g));
32+
assert(e.toString().match(/require\(\)ofESModule/g));
33+
assert(e.message.match(/require\(\)ofESModule/g));
3434
}
3535

3636
functionexpect(opt='',inputFile,want,wantsError=false){
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require('./package-type-module/esm.js');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exportvarp=5;

‎test/parallel/test-require-mjs.js‎

Lines changed: 1 addition & 1 deletion
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: /MustuseimporttoloadESModule/,
8+
message: /dynamicimport\(\)whichisavailableinallCommonJSmodules/,
99
code: 'ERR_REQUIRE_ESM'
1010
}
1111
);

0 commit comments

Comments
 (0)