Skip to content

Commit 1f59c8f

Browse files
committed
esm: refine ERR_REQUIRE_ESM errors
1 parent fdb097c commit 1f59c8f

File tree

8 files changed

+217
-69
lines changed

8 files changed

+217
-69
lines changed

lib/internal/errors.js

Lines changed: 80 additions & 22 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,
@@ -116,6 +117,11 @@ const prepareStackTrace = (globalThis, error, trace) => {
116117
// Error: Message
117118
// at function (file)
118119
// at file
120+
const code = 'code' in error ? error.code : null;
121+
if (code && overrideStackTraceByCode.has(code)) {
122+
const f = overrideStackTraceByCode.get(code);
123+
return f(error, trace);
124+
}
119125
const errorString = ErrorPrototypeToString(error);
120126
if (trace.length === 0) {
121127
return errorString;
@@ -186,14 +192,16 @@ function lazyBuffer() {
186192
return buffer;
187193
}
188194

189-
const addCodeToName = hideStackFrames(function addCodeToName(err, name, code) {
195+
const addCodeToName = hideStackFrames(function addCodeToName(err, name, code,
196+
deferStack) {
190197
// Set the stack
191198
err = captureLargerStackTrace(err);
192199
// Add the error code to the name to include it in the stack trace.
193200
err.name = `${name} [${code}]`;
194201
// Access the stack to generate the error message including the error code
195202
// from the name.
196-
err.stack; // eslint-disable-line no-unused-expressions
203+
if (!deferStack)
204+
err.stack; // eslint-disable-line no-unused-expressions
197205
// Reset the name to the actual name.
198206
if (name === 'SystemError') {
199207
ObjectDefineProperty(err, 'name', {
@@ -248,7 +256,7 @@ class SystemError extends Error {
248256
writable: true,
249257
configurable: true
250258
});
251-
addCodeToName(this, 'SystemError', key);
259+
addCodeToName(this, 'SystemError', key, false);
252260

253261
this.code = key;
254262

@@ -338,7 +346,7 @@ function makeSystemErrorWithCode(key) {
338346
};
339347
}
340348

341-
function makeNodeErrorWithCode(Base, key) {
349+
function makeNodeErrorWithCode(Base, key, deferStack) {
342350
return function NodeError(...args) {
343351
const limit = Error.stackTraceLimit;
344352
if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = 0;
@@ -360,7 +368,7 @@ function makeNodeErrorWithCode(Base, key) {
360368
writable: true,
361369
configurable: true,
362370
});
363-
addCodeToName(error, Base.name, key);
371+
addCodeToName(error, Base.name, key, deferStack);
364372
error.code = key;
365373
return error;
366374
};
@@ -382,18 +390,19 @@ function hideStackFrames(fn) {
382390
// Utility function for registering the error codes. Only used here. Exported
383391
// *only* to allow for testing.
384392
function E(sym, val, def, ...otherClasses) {
393+
const deferStack = overrideStackTraceByCode.has(sym);
385394
// Special case for SystemError that formats the error message differently
386395
// The SystemErrors only have SystemError as their base classes.
387396
messages.set(sym, val);
388397
if (def === SystemError) {
389398
def = makeSystemErrorWithCode(sym);
390399
} else {
391-
def = makeNodeErrorWithCode(def, sym);
400+
def = makeNodeErrorWithCode(def, sym, deferStack);
392401
}
393402

394403
if (otherClasses.length !== 0) {
395404
otherClasses.forEach((clazz) => {
396-
def[clazz.name] = makeNodeErrorWithCode(clazz, sym);
405+
def[clazz.name] = makeNodeErrorWithCode(clazz, sym, deferStack);
397406
});
398407
}
399408
codes[sym] = def;
@@ -781,6 +790,40 @@ const fatalExceptionStackEnhancers = {
781790
}
782791
};
783792

793+
// Ensures the printed error line is from user code.
794+
let _kArrowMessagePrivateSymbol, _setHiddenValue;
795+
function setArrowMessage(err, arrowMessage) {
796+
if (!_kArrowMessagePrivateSymbol) {
797+
({
798+
arrow_message_private_symbol: _kArrowMessagePrivateSymbol,
799+
setHiddenValue: _setHiddenValue,
800+
} = internalBinding('util'));
801+
}
802+
_setHiddenValue(err, _kArrowMessagePrivateSymbol, arrowMessage);
803+
}
804+
805+
// Hide stack lines before the first user code line.
806+
function hideLeadingInternalFrames(error, stackFrames) {
807+
let frames = stackFrames;
808+
if (typeof stackFrames === 'object') {
809+
let beforeUserCode = true;
810+
frames = ArrayPrototypeFilter(
811+
stackFrames,
812+
(frm) => {
813+
if (!beforeUserCode)
814+
return true;
815+
const isInternal = StringPrototypeStartsWith(frm.getFileName(),
816+
'node:internal');
817+
if (!isInternal)
818+
beforeUserCode = false;
819+
return !isInternal;
820+
},
821+
);
822+
}
823+
ArrayPrototypeUnshift(frames, error);
824+
return ArrayPrototypeJoin(frames, '\n at ');
825+
}
826+
784827
// Node uses an AbortError that isn't exactly the same as the DOMException
785828
// to make usage of the error in userland and readable-stream easier.
786829
// It is a regular error with `.code` and `.name`.
@@ -802,6 +845,7 @@ module.exports = {
802845
hideStackFrames,
803846
isErrorStackTraceLimitWritable,
804847
isStackOverflowError,
848+
setArrowMessage,
805849
connResetException,
806850
uvErrmapGet,
807851
uvException,
@@ -834,6 +878,12 @@ module.exports = {
834878
// Note: Please try to keep these in alphabetical order
835879
//
836880
// Note: Node.js specific errors must begin with the prefix ERR_
881+
882+
// Custom error stack overrides.
883+
const overrideStackTraceByCode = new SafeMap([
884+
['ERR_REQUIRE_ESM', hideLeadingInternalFrames],
885+
]);
886+
837887
E('ERR_AMBIGUOUS_ARGUMENT', 'The "%s" argument is ambiguous. %s', TypeError);
838888
E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError);
839889
E('ERR_ASSERTION', '%s', Error);
@@ -1397,23 +1447,31 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
13971447
'%d is not a valid timestamp', TypeError);
13981448
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError);
13991449
E('ERR_REQUIRE_ESM',
1400-
(filename, parentPath = null, packageJsonPath = null) => {
1401-
let msg = `Must use import to load ES Module: ${filename}`;
1402-
if (parentPath && packageJsonPath) {
1403-
const path = require('path');
1404-
const basename = path.basename(filename) === path.basename(parentPath) ?
1405-
filename : path.basename(filename);
1406-
msg +=
1407-
'\nrequire() of ES modules is not supported.\nrequire() of ' +
1408-
`${filename} from ${parentPath} ` +
1409-
'is an ES module file as it is a .js file whose nearest parent ' +
1410-
'package.json contains "type": "module" which defines all .js ' +
1411-
'files in that package scope as ES modules.\nInstead rename ' +
1412-
`${basename} to end in .cjs, change the requiring code to use ` +
1413-
'import(), or remove "type": "module" from ' +
1414-
`${packageJsonPath}.\n`;
1450+
function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) {
1451+
let msg = `require() of ES Module ${filename}${parentPath ? ` from ${
1452+
parentPath}` : ''} not supported.`;
1453+
if (!packageJsonPath) {
1454+
if (filename.endsWith('.mjs'))
1455+
msg += `\nInstead change the require of ${filename} to a dynamic ` +
1456+
'import() which is available in all CommonJS modules.';
1457+
return msg;
1458+
}
1459+
const path = require('path');
1460+
const basename = path.basename(filename) === path.basename(parentPath) ?
1461+
filename : path.basename(filename);
1462+
if (hasEsmSyntax) {
1463+
msg += `\nInstead change the require of ${basename} in ${parentPath} to` +
1464+
' a dynamic import() which is available in all CommonJS modules.';
14151465
return msg;
14161466
}
1467+
msg += `\n${basename} is treated as an ES module file as it is a .js ` +
1468+
'file whose nearest parent package.json contains "type": "module" ' +
1469+
'which declares all .js files in that package scope as ES modules.' +
1470+
`\nInstead rename ${basename} to end in .cjs, change the requiring ` +
1471+
'code to use dynamic import() which is available in all CommonJS' +
1472+
`modules, or remove "type": "module" from ${packageJsonPath} to ` +
1473+
'treat all .js files as CommonJS (using .mjs for all ES modules ' +
1474+
'instead).\n';
14171475
return msg;
14181476
}, Error);
14191477
E('ERR_SCRIPT_EXECUTION_INTERRUPTED',

lib/internal/modules/cjs/helpers.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
2929
debug = fn;
3030
});
3131

32+
const acorn = require('internal/deps/acorn/acorn/dist/acorn');
33+
const privateMethods =
34+
require('internal/deps/acorn-plugins/acorn-private-methods/index');
35+
const classFields =
36+
require('internal/deps/acorn-plugins/acorn-class-fields/index');
37+
const staticClassFeatures =
38+
require('internal/deps/acorn-plugins/acorn-static-class-features/index');
39+
3240
// TODO: Use this set when resolving pkg#exports conditions in loader.js.
3341
const cjsConditions = new SafeSet(['require', 'node', ...userConditions]);
3442

@@ -184,9 +192,33 @@ function normalizeReferrerURL(referrer) {
184192
return new URL(referrer).href;
185193
}
186194

195+
// For error messages only - used to check if ESM syntax is in use.
196+
function hasEsmSyntax(code) {
197+
const parser = acorn.Parser.extend(
198+
privateMethods,
199+
classFields,
200+
staticClassFeatures
201+
);
202+
let root;
203+
try {
204+
root = parser.parse(code, { sourceType: 'module', ecmaVersion: 'latest' });
205+
} catch {
206+
return false;
207+
}
208+
209+
for (const stmt of root.body) {
210+
if (stmt.type === 'ExportNamedDeclaration' ||
211+
stmt.type === 'ImportDeclaration' ||
212+
stmt.type === 'ExportAllDeclaration')
213+
return true;
214+
}
215+
return false;
216+
}
217+
187218
module.exports = {
188219
addBuiltinLibsToObject,
189220
cjsConditions,
221+
hasEsmSyntax,
190222
loadNativeModule,
191223
makeRequireFunction,
192224
normalizeReferrerURL,

lib/internal/modules/cjs/loader.js

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const {
5858
StringPrototypeLastIndexOf,
5959
StringPrototypeIndexOf,
6060
StringPrototypeMatch,
61+
StringPrototypeRepeat,
6162
StringPrototypeSlice,
6263
StringPrototypeSplit,
6364
StringPrototypeStartsWith,
@@ -88,11 +89,12 @@ const { internalModuleStat } = internalBinding('fs');
8889
const packageJsonReader = require('internal/modules/package_json_reader');
8990
const { safeGetenv } = internalBinding('credentials');
9091
const {
92+
cjsConditions,
93+
hasEsmSyntax,
94+
loadNativeModule,
9195
makeRequireFunction,
9296
normalizeReferrerURL,
9397
stripBOM,
94-
cjsConditions,
95-
loadNativeModule
9698
} = require('internal/modules/cjs/helpers');
9799
const { getOptionValue } = require('internal/options');
98100
const preserveSymlinks = getOptionValue('--preserve-symlinks');
@@ -107,11 +109,14 @@ const policy = getOptionValue('--experimental-policy') ?
107109
let hasLoadedAnyUserCJSModule = false;
108110

109111
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;
112+
codes: {
113+
ERR_INVALID_ARG_VALUE,
114+
ERR_INVALID_MODULE_SPECIFIER,
115+
ERR_REQUIRE_ESM,
116+
ERR_UNKNOWN_BUILTIN_MODULE,
117+
},
118+
setArrowMessage,
119+
} = require('internal/errors');
115120
const { validateString } = require('internal/validators');
116121
const pendingDeprecation = getOptionValue('--pending-deprecation');
117122

@@ -970,7 +975,7 @@ Module.prototype.load = function(filename) {
970975
const extension = findLongestRegisteredExtension(filename);
971976
// allow .mjs to be overridden
972977
if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs'])
973-
throw new ERR_REQUIRE_ESM(filename);
978+
throw new ERR_REQUIRE_ESM(filename, true);
974979

975980
Module._extensions[extension](this, filename);
976981
this.loaded = true;
@@ -1102,16 +1107,6 @@ Module.prototype._compile = function(content, filename) {
11021107

11031108
// Native extension for .js
11041109
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-
}
11151110
// If already analyzed the source, then it will be cached.
11161111
const cached = cjsParseCache.get(module);
11171112
let content;
@@ -1121,6 +1116,38 @@ Module._extensions['.js'] = function(module, filename) {
11211116
} else {
11221117
content = fs.readFileSync(filename, 'utf8');
11231118
}
1119+
if (StringPrototypeEndsWith(filename, '.js')) {
1120+
const pkg = readPackageScope(filename);
1121+
// Function require shouldn't be used in ES modules.
1122+
if (pkg?.data?.type === 'module') {
1123+
const parent = moduleParentCache.get(module);
1124+
const parentPath = parent?.filename;
1125+
const packageJsonPath = path.resolve(pkg.path, 'package.json');
1126+
const usesEsm = hasEsmSyntax(content);
1127+
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
1128+
packageJsonPath);
1129+
// Attempt to reconstruct the parent require frame.
1130+
if (Module._cache[parentPath]) {
1131+
let parentSource;
1132+
try {
1133+
parentSource = fs.readFileSync(parentPath, 'utf8');
1134+
} catch {}
1135+
if (parentSource) {
1136+
const errLine = StringPrototypeSplit(StringPrototypeSlice(
1137+
err.stack, StringPrototypeIndexOf(err.stack, ' at ')), '\n')[0];
1138+
const { 1: line, 2: col } =
1139+
StringPrototypeMatch(errLine, /(\d+):(\d+)\)/) || [];
1140+
if (line && col) {
1141+
const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
1142+
const frame = `${parentPath}:${line}\n${srcLine}\n${
1143+
StringPrototypeRepeat(' ', col - 1)}^\n`;
1144+
setArrowMessage(err, frame);
1145+
}
1146+
}
1147+
}
1148+
throw err;
1149+
}
1150+
}
11241151
module._compile(content, filename);
11251152
};
11261153

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;

0 commit comments

Comments
 (0)