Skip to content

Commit 3b2f28e

Browse files
committed
module: fix detect-module not retrying as esm for cjs-only errors
1 parent 4e109e5 commit 3b2f28e

File tree

3 files changed

+101
-3
lines changed

3 files changed

+101
-3
lines changed

src/node_contextify.cc

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,25 @@ constexpr std::array<std::string_view, 3> esm_syntax_error_messages = {
13981398
"Unexpected token 'export'", // `export` statements
13991399
"Cannot use 'import.meta' outside a module"}; // `import.meta` references
14001400

1401+
// Another class of error messages that we need to check for are syntax errors
1402+
// where the syntax throws when parsed as CommonJS but succeeds when parsed as
1403+
// ESM. So far, the cases we've found are:
1404+
// - CommonJS module variables (`module`, `exports`, `require`, `__filename`,
1405+
// `__dirname`): if the user writes code such as `const module =` in the top
1406+
// level of a CommonJS module, it will throw a syntax error; but the same
1407+
// code is valid in ESM.
1408+
// - Top-level `await`: if the user writes `await` at the top level of a
1409+
// CommonJS module, it will throw a syntax error; but the same code is valid
1410+
// in ESM.
1411+
constexpr std::array<std::string_view, 6> throws_only_in_cjs_error_messages = {
1412+
"Identifier 'module' has already been declared",
1413+
"Identifier 'exports' has already been declared",
1414+
"Identifier 'require' has already been declared",
1415+
"Identifier '__filename' has already been declared",
1416+
"Identifier '__dirname' has already been declared",
1417+
"await is only valid in async functions and "
1418+
"the top level bodies of modules"};
1419+
14011420
void ContextifyContext::ContainsModuleSyntax(
14021421
const FunctionCallbackInfo<Value>& args) {
14031422
Environment* env = Environment::GetCurrent(args);
@@ -1470,19 +1489,51 @@ void ContextifyContext::ContainsModuleSyntax(
14701489
id_symbol,
14711490
try_catch);
14721491

1473-
bool found_error_message_caused_by_module_syntax = false;
1492+
bool should_retry_as_esm = false;
14741493
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
14751494
Utf8Value message_value(env->isolate(), try_catch.Message()->Get());
14761495
auto message = message_value.ToStringView();
14771496

14781497
for (const auto& error_message : esm_syntax_error_messages) {
14791498
if (message.find(error_message) != std::string_view::npos) {
1480-
found_error_message_caused_by_module_syntax = true;
1499+
should_retry_as_esm = true;
1500+
break;
1501+
}
1502+
}
1503+
1504+
for (const auto& error_message : throws_only_in_cjs_error_messages) {
1505+
if (message.find(error_message) != std::string_view::npos) {
1506+
// Try parsing again where the user's code is wrapped within an async
1507+
// function. If the new parse succeeds, then the error was caused by
1508+
// either a top-level declaration of one of the CommonJS module
1509+
// variables, or a top-level `await`.
1510+
TryCatchScope second_parse_try_catch(env);
1511+
Local<String> wrapped_code = String::Concat(isolate,
1512+
String::NewFromUtf8(isolate, "(async function() {").ToLocalChecked(),
1513+
code);
1514+
wrapped_code = String::Concat(isolate, wrapped_code,
1515+
String::NewFromUtf8(isolate, "})();").ToLocalChecked());
1516+
ScriptCompiler::Source wrapped_source = GetCommonJSSourceInstance(
1517+
isolate, wrapped_code, filename, 0, 0, host_defined_options,
1518+
nullptr);
1519+
ContextifyContext::CompileFunctionAndCacheResult(env,
1520+
context,
1521+
&wrapped_source,
1522+
std::move(params),
1523+
std::vector<Local<Object>>(),
1524+
options,
1525+
true,
1526+
id_symbol,
1527+
second_parse_try_catch);
1528+
if (!second_parse_try_catch.HasCaught() &&
1529+
!second_parse_try_catch.HasTerminated()) {
1530+
should_retry_as_esm = true;
1531+
}
14811532
break;
14821533
}
14831534
}
14841535
}
1485-
args.GetReturnValue().Set(found_error_message_caused_by_module_syntax);
1536+
args.GetReturnValue().Set(should_retry_as_esm);
14861537
}
14871538

14881539
static void StartSigintWatchdog(const FunctionCallbackInfo<Value>& args) {

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,47 @@ 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('still throws on `await` in an ordinary sync function', async () => {
254+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
255+
'--experimental-detect-module',
256+
'--eval',
257+
'function fn() { await Promise.resolve(); } fn();',
258+
]);
259+
260+
match(stderr, /SyntaxError: await is only valid in async function/);
261+
strictEqual(stdout, '');
262+
strictEqual(code, 1);
263+
strictEqual(signal, null);
264+
});
265+
266+
it('permits declaration of CommonJS module variables', async () => {
267+
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
268+
'--experimental-detect-module',
269+
fixtures.path('es-modules/package-without-type/commonjs-wrapper-variables.js'),
270+
]);
271+
272+
strictEqual(stderr, '');
273+
strictEqual(stdout, 'exports require module __filename __dirname\n');
274+
strictEqual(code, 0);
275+
strictEqual(signal, null);
276+
});
277+
});
237278
});
238279

239280
// Validate temporarily disabling `--abort-on-uncaught-exception`
Lines changed: 6 additions & 0 deletions
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)