Skip to content

Commit 882a64e

Browse files
GeoffreyBoothmarco-ippolito
authored andcommitted
module: fix detect-module not retrying as esm for cjs-only errors
PR-URL: #52024 Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
1 parent d891275 commit 882a64e

File tree

4 files changed

+187
-7
lines changed

4 files changed

+187
-7
lines changed

doc/api/cli.md

+10-4
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ added:
763763
- v20.10.0
764764
-->
765765

766-
> Stability: 1.0 - Early development
766+
> Stability: 1.1 - Active development
767767
768768
Node.js will inspect the source code of ambiguous input to determine whether it
769769
contains ES module syntax; if such syntax is detected, the input will be treated
@@ -778,9 +778,15 @@ Ambiguous input is defined as:
778778
`--experimental-default-type` are specified.
779779

780780
ES module syntax is defined as syntax that would throw when evaluated as
781-
CommonJS. This includes `import` and `export` statements and `import.meta`
782-
references. It does _not_ include `import()` expressions, which are valid in
783-
CommonJS.
781+
CommonJS. This includes the following:
782+
783+
* `import` statements (but _not_ `import()` expressions, which are valid in
784+
CommonJS).
785+
* `export` statements.
786+
* `import.meta` references.
787+
* `await` at the top level of a module.
788+
* Lexical redeclarations of the CommonJS wrapper variables (`require`, `module`,
789+
`exports`, `__dirname`, `__filename`).
784790

785791
### `--experimental-import-meta-resolve`
786792

src/node_contextify.cc

+78-3
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,25 @@ constexpr std::array<std::string_view, 3> esm_syntax_error_messages = {
14101410
"Unexpected token 'export'", // `export` statements
14111411
"Cannot use 'import.meta' outside a module"}; // `import.meta` references
14121412

1413+
// Another class of error messages that we need to check for are syntax errors
1414+
// where the syntax throws when parsed as CommonJS but succeeds when parsed as
1415+
// ESM. So far, the cases we've found are:
1416+
// - CommonJS module variables (`module`, `exports`, `require`, `__filename`,
1417+
// `__dirname`): if the user writes code such as `const module =` in the top
1418+
// level of a CommonJS module, it will throw a syntax error; but the same
1419+
// code is valid in ESM.
1420+
// - Top-level `await`: if the user writes `await` at the top level of a
1421+
// CommonJS module, it will throw a syntax error; but the same code is valid
1422+
// in ESM.
1423+
constexpr std::array<std::string_view, 6> throws_only_in_cjs_error_messages = {
1424+
"Identifier 'module' has already been declared",
1425+
"Identifier 'exports' has already been declared",
1426+
"Identifier 'require' has already been declared",
1427+
"Identifier '__filename' has already been declared",
1428+
"Identifier '__dirname' has already been declared",
1429+
"await is only valid in async functions and "
1430+
"the top level bodies of modules"};
1431+
14131432
void ContextifyContext::ContainsModuleSyntax(
14141433
const FunctionCallbackInfo<Value>& args) {
14151434
Environment* env = Environment::GetCurrent(args);
@@ -1477,19 +1496,75 @@ void ContextifyContext::ContainsModuleSyntax(
14771496
id_symbol,
14781497
try_catch);
14791498

1480-
bool found_error_message_caused_by_module_syntax = false;
1499+
bool should_retry_as_esm = false;
14811500
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
14821501
Utf8Value message_value(env->isolate(), try_catch.Message()->Get());
14831502
auto message = message_value.ToStringView();
14841503

14851504
for (const auto& error_message : esm_syntax_error_messages) {
14861505
if (message.find(error_message) != std::string_view::npos) {
1487-
found_error_message_caused_by_module_syntax = true;
1506+
should_retry_as_esm = true;
14881507
break;
14891508
}
14901509
}
1510+
1511+
if (!should_retry_as_esm) {
1512+
for (const auto& error_message : throws_only_in_cjs_error_messages) {
1513+
if (message.find(error_message) != std::string_view::npos) {
1514+
// Try parsing again where the CommonJS wrapper is replaced by an
1515+
// async function wrapper. If the new parse succeeds, then the error
1516+
// was caused by either a top-level declaration of one of the CommonJS
1517+
// module variables, or a top-level `await`.
1518+
TryCatchScope second_parse_try_catch(env);
1519+
code =
1520+
String::Concat(isolate,
1521+
String::NewFromUtf8(isolate, "(async function() {")
1522+
.ToLocalChecked(),
1523+
code);
1524+
code = String::Concat(
1525+
isolate,
1526+
code,
1527+
String::NewFromUtf8(isolate, "})();").ToLocalChecked());
1528+
ScriptCompiler::Source wrapped_source = GetCommonJSSourceInstance(
1529+
isolate, code, filename, 0, 0, host_defined_options, nullptr);
1530+
std::ignore = ScriptCompiler::CompileFunction(
1531+
context,
1532+
&wrapped_source,
1533+
params.size(),
1534+
params.data(),
1535+
0,
1536+
nullptr,
1537+
options,
1538+
v8::ScriptCompiler::NoCacheReason::kNoCacheNoReason);
1539+
if (!second_parse_try_catch.HasTerminated()) {
1540+
if (second_parse_try_catch.HasCaught()) {
1541+
// If on the second parse an error is thrown by ESM syntax, then
1542+
// what happened was that the user had top-level `await` or a
1543+
// top-level declaration of one of the CommonJS module variables
1544+
// above the first `import` or `export`.
1545+
Utf8Value second_message_value(
1546+
env->isolate(), second_parse_try_catch.Message()->Get());
1547+
auto second_message = second_message_value.ToStringView();
1548+
for (const auto& error_message : esm_syntax_error_messages) {
1549+
if (second_message.find(error_message) !=
1550+
std::string_view::npos) {
1551+
should_retry_as_esm = true;
1552+
break;
1553+
}
1554+
}
1555+
} else {
1556+
// No errors thrown in the second parse, so most likely the error
1557+
// was caused by a top-level `await` or a top-level declaration of
1558+
// one of the CommonJS module variables.
1559+
should_retry_as_esm = true;
1560+
}
1561+
}
1562+
break;
1563+
}
1564+
}
1565+
}
14911566
}
1492-
args.GetReturnValue().Set(found_error_message_caused_by_module_syntax);
1567+
args.GetReturnValue().Set(should_retry_as_esm);
14931568
}
14941569

14951570
static void CompileFunctionForCJSLoader(

test/es-module/test-esm-detect-ambiguous.mjs

+93
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,99 @@ describe('--experimental-detect-module', { concurrency: true }, () => {
234234
});
235235
}
236236
});
237+
238+
// https://github.com/nodejs/node/issues/50917
239+
describe('syntax that errors in CommonJS but works in ESM', { concurrency: true }, () => {
240+
it('permits top-level `await`', async () => {
241+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
242+
'--experimental-detect-module',
243+
'--eval',
244+
'await Promise.resolve(); console.log("executed");',
245+
]);
246+
247+
strictEqual(stderr, '');
248+
strictEqual(stdout, 'executed\n');
249+
strictEqual(code, 0);
250+
strictEqual(signal, null);
251+
});
252+
253+
it('permits top-level `await` above import/export syntax', async () => {
254+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
255+
'--experimental-detect-module',
256+
'--eval',
257+
'await Promise.resolve(); import "node:os"; console.log("executed");',
258+
]);
259+
260+
strictEqual(stderr, '');
261+
strictEqual(stdout, 'executed\n');
262+
strictEqual(code, 0);
263+
strictEqual(signal, null);
264+
});
265+
266+
it('still throws on `await` in an ordinary sync function', async () => {
267+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
268+
'--experimental-detect-module',
269+
'--eval',
270+
'function fn() { await Promise.resolve(); } fn();',
271+
]);
272+
273+
match(stderr, /SyntaxError: await is only valid in async function/);
274+
strictEqual(stdout, '');
275+
strictEqual(code, 1);
276+
strictEqual(signal, null);
277+
});
278+
279+
it('throws on undefined `require` when top-level `await` triggers ESM parsing', async () => {
280+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
281+
'--experimental-detect-module',
282+
'--eval',
283+
'const fs = require("node:fs"); await Promise.resolve();',
284+
]);
285+
286+
match(stderr, /ReferenceError: require is not defined in ES module scope/);
287+
strictEqual(stdout, '');
288+
strictEqual(code, 1);
289+
strictEqual(signal, null);
290+
});
291+
292+
it('permits declaration of CommonJS module variables', async () => {
293+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
294+
'--experimental-detect-module',
295+
fixtures.path('es-modules/package-without-type/commonjs-wrapper-variables.js'),
296+
]);
297+
298+
strictEqual(stderr, '');
299+
strictEqual(stdout, 'exports require module __filename __dirname\n');
300+
strictEqual(code, 0);
301+
strictEqual(signal, null);
302+
});
303+
304+
it('permits declaration of CommonJS module variables above import/export', async () => {
305+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
306+
'--experimental-detect-module',
307+
'--eval',
308+
'const module = 3; import "node:os"; console.log("executed");',
309+
]);
310+
311+
strictEqual(stderr, '');
312+
strictEqual(stdout, 'executed\n');
313+
strictEqual(code, 0);
314+
strictEqual(signal, null);
315+
});
316+
317+
it('still throws on double `const` declaration not at the top level', async () => {
318+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
319+
'--experimental-detect-module',
320+
'--eval',
321+
'function fn() { const require = 1; const require = 2; } fn();',
322+
]);
323+
324+
match(stderr, /SyntaxError: Identifier 'require' has already been declared/);
325+
strictEqual(stdout, '');
326+
strictEqual(code, 1);
327+
strictEqual(signal, null);
328+
});
329+
});
237330
});
238331

239332
// Validate temporarily disabling `--abort-on-uncaught-exception`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const exports = "exports";
2+
const require = "require";
3+
const module = "module";
4+
const __filename = "__filename";
5+
const __dirname = "__dirname";
6+
console.log(exports, require, module, __filename, __dirname);

0 commit comments

Comments
 (0)