diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 51aa863dd615f5..4c3a5373543538 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -72,9 +72,16 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) => * @param {string} url * @returns {void} */ -const explainCommonJSGlobalLikeNotDefinedError = (e, url) => { +const explainCommonJSGlobalLikeNotDefinedError = (e, url, hasTopLevelAwait) => { if (e?.name === 'ReferenceError' && isCommonJSGlobalLikeNotDefinedError(e.message)) { + + if (hasTopLevelAwait) { + e.message = `Cannot determine intended module format because both require() and top-level await are present. If the code is intended to be CommonJS, wrap await in an async function. If the code is intended to be an ES module, replace require() with import.`; + e.code = 'ERR_AMBIGUOUS_MODULE_SYNTAX'; + return; + } + e.message += ' in ES module scope'; if (StringPrototypeStartsWith(e.message, 'require ')) { @@ -357,7 +364,7 @@ class ModuleJob extends ModuleJobBase { try { await this.module.evaluate(timeout, breakOnSigint); } catch (e) { - explainCommonJSGlobalLikeNotDefinedError(e, this.module.url); + explainCommonJSGlobalLikeNotDefinedError(e, this.module.url, this.module.hasTopLevelAwait()); throw e; } return { __proto__: null, module: this.module }; @@ -491,7 +498,7 @@ class ModuleJobSync extends ModuleJobBase { const namespace = this.module.evaluateSync(filename, parentFilename); return { __proto__: null, module: this.module, namespace }; } catch (e) { - explainCommonJSGlobalLikeNotDefinedError(e, this.module.url); + explainCommonJSGlobalLikeNotDefinedError(e, this.module.url, this.module.hasTopLevelAwait()); throw e; } } diff --git a/src/module_wrap.cc b/src/module_wrap.cc index dd0f74c5580561..ad59a0ec4c121e 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -877,6 +877,27 @@ void ModuleWrap::IsGraphAsync(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(module->IsGraphAsync()); } +void ModuleWrap::HasTopLevelAwait(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + + Local module = obj->module_.Get(isolate); + + // Check if module is valid + if (module.IsEmpty()) { + args.GetReturnValue().Set(false); + return; + } + + // For source text modules, check if the graph is async + // For synthetic modules, it's always false + bool has_top_level_await = + module->IsSourceTextModule() && module->IsGraphAsync(); + + args.GetReturnValue().Set(has_top_level_await); +} + void ModuleWrap::GetError(const FunctionCallbackInfo& args) { Isolate* isolate = args.GetIsolate(); ModuleWrap* obj; @@ -1305,6 +1326,8 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data, SetProtoMethodNoSideEffect(isolate, tpl, "getNamespace", GetNamespace); SetProtoMethodNoSideEffect(isolate, tpl, "getStatus", GetStatus); SetProtoMethodNoSideEffect(isolate, tpl, "isGraphAsync", IsGraphAsync); + SetProtoMethodNoSideEffect( + isolate, tpl, "hasTopLevelAwait", HasTopLevelAwait); SetProtoMethodNoSideEffect(isolate, tpl, "getError", GetError); SetConstructorFunction(isolate, target, "ModuleWrap", tpl); isolate_data->set_module_wrap_constructor_template(tpl); @@ -1367,6 +1390,7 @@ void ModuleWrap::RegisterExternalReferences( registry->Register(GetStatus); registry->Register(GetError); registry->Register(IsGraphAsync); + registry->Register(HasTopLevelAwait); registry->Register(CreateRequiredModuleFacade); diff --git a/src/module_wrap.h b/src/module_wrap.h index 1172fb0332b48a..3a79bd4ba61e72 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -64,6 +64,7 @@ class ModuleWrap : public BaseObject { void MemoryInfo(MemoryTracker* tracker) const override { tracker->TrackField("resolve_cache", resolve_cache_); } + static void HasTopLevelAwait(const v8::FunctionCallbackInfo& args); v8::Local context() const; v8::Maybe CheckUnsettledTopLevelAwait(); diff --git a/test/es-module/test-esm-detect-ambiguous.mjs b/test/es-module/test-esm-detect-ambiguous.mjs index 8b630b8cff6314..a583da981fed5c 100644 --- a/test/es-module/test-esm-detect-ambiguous.mjs +++ b/test/es-module/test-esm-detect-ambiguous.mjs @@ -281,7 +281,10 @@ describe('Module syntax detection', { concurrency: !process.env.TEST_PARALLEL }, 'const fs = require("node:fs"); await Promise.resolve();', ]); - match(stderr, /ReferenceError: require is not defined in ES module scope/); + match( + stderr, + /ReferenceError: Cannot determine intended module format because both require\(\) and top-level await are present\. If the code is intended to be CommonJS, wrap await in an async function\. If the code is intended to be an ES module, replace require\(\) with import\./ + ); strictEqual(stdout, ''); strictEqual(code, 1); strictEqual(signal, null); @@ -423,3 +426,24 @@ describe('when working with Worker threads', () => { strictEqual(signal, null); }); }); + +describe('cjs & esm ambiguous syntax case', () => { + it('should throw an ambiguous syntax error when using top-level await with require', async () => { + const { stderr, code, signal } = await spawnPromisified( + process.execPath, + [ + '--input-type=module', + '--eval', + `await 1;\nconst fs = require('fs');`, + ] + ); + + match( + stderr, + /ReferenceError: Cannot determine intended module format because both require\(\) and top-level await are present\. If the code is intended to be CommonJS, wrap await in an async function\. If the code is intended to be an ES module, replace require\(\) with import\./ + ); + + strictEqual(code, 1); + strictEqual(signal, null); + }); +});