From f1ffcc397e58b946ede372125699bc2a65c548d9 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Thu, 24 Oct 2024 14:39:20 +0200 Subject: [PATCH 1/3] module: fix error thrown from require(esm) hitting TLA repeatedly This tracks the asynchronicity in the ModuleWraps when they turn out to contain TLA after instantiation, and throw the right error (ERR_REQUIRE_ASYNC_MODULE) when it's required again. It removes the freezing of ModuleWraps since it's not meaningful to freeze this when the rest of the module loader is mutable, and we can record the asynchronicity in the ModuleWrap right after compilation after we get a V8 upgrade that contains v8::Module::HasTopLevelAwait() instead of searching through the module graph repeatedly which can be slow. --- lib/internal/errors.js | 3 +++ lib/internal/modules/esm/loader.js | 4 ++++ lib/internal/modules/esm/module_job.js | 16 ++++++++++++++-- src/module_wrap.cc | 11 +++-------- .../test-require-module-tla-retry-require.js | 16 ++++++++++++++++ 5 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 test/es-module/test-require-module-tla-retry-require.js diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 8a97384201341f..9045046c100308 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1650,6 +1650,9 @@ E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError); E('ERR_QUIC_CONNECTION_FAILED', 'QUIC connection failed', Error); E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error); E('ERR_QUIC_OPEN_STREAM_FAILED', 'Failed to open QUIC stream', Error); +E('ERR_REQUIRE_ASYNC_MODULE', 'require() cannot be used on an ESM ' + + 'graph with top-level await. Use import() instead. To see where the' + + ' top-level await comes from, use --experimental-print-required-tla.', Error); E('ERR_REQUIRE_CYCLE_MODULE', '%s', Error); E('ERR_REQUIRE_ESM', function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) { diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 262006f8b3a542..c5594e07d667c3 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -23,6 +23,7 @@ const { imported_cjs_symbol } = internalBinding('symbols'); const assert = require('internal/assert'); const { + ERR_REQUIRE_ASYNC_MODULE, ERR_REQUIRE_CYCLE_MODULE, ERR_REQUIRE_ESM, ERR_NETWORK_IMPORT_DISALLOWED, @@ -302,6 +303,9 @@ class ModuleLoader { // evaluated at this point. if (job !== undefined) { mod[kRequiredModuleSymbol] = job.module; + if (job.module.async) { + throw new ERR_REQUIRE_ASYNC_MODULE(); + } if (job.module.getStatus() !== kEvaluated) { const parentFilename = urlToFilename(parent?.filename); let message = `Cannot require() ES Module ${filename} in a cycle.`; diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 413fcd27703e3e..4c2a061f8e6b1c 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -37,8 +37,11 @@ const resolvedPromise = PromiseResolve(); const { setHasStartedUserESMExecution, } = require('internal/modules/helpers'); +const { getOptionValue } = require('internal/options'); const noop = FunctionPrototype; - +const { + ERR_REQUIRE_ASYNC_MODULE, +} = require('internal/errors').codes; let hasPausedEntry = false; const CJSGlobalLike = [ @@ -362,7 +365,16 @@ class ModuleJobSync extends ModuleJobBase { runSync() { // TODO(joyeecheung): add the error decoration logic from the async instantiate. - this.module.instantiateSync(); + this.module.async = this.module.instantiateSync(); + // If --experimental-print-required-tla is true, proceeds to evaluation even + // if it's async because we want to search for the TLA and help users locate + // them. + // TODO(joyeecheung): track the asynchroniticy using v8::Module::HasTopLevelAwait() + // and we'll be able to throw right after compilation of the modules, using acron + // to find and print the TLA. + if (this.module.async && !getOptionValue('--experimental-print-required-tla')) { + throw new ERR_REQUIRE_ASYNC_MODULE(); + } setHasStartedUserESMExecution(); const namespace = this.module.evaluateSync(); return { __proto__: null, module: this.module, namespace }; diff --git a/src/module_wrap.cc b/src/module_wrap.cc index e2252639cf4518..71a6b4f0362752 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -332,7 +332,6 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { obj->contextify_context_ = contextify_context; - that->SetIntegrityLevel(context, IntegrityLevel::kFrozen); args.GetReturnValue().Set(that); } @@ -635,13 +634,9 @@ void ModuleWrap::InstantiateSync(const FunctionCallbackInfo& args) { } } - // If --experimental-print-required-tla is true, proceeds to evaluation even - // if it's async because we want to search for the TLA and help users locate - // them. - if (module->IsGraphAsync() && !env->options()->print_required_tla) { - THROW_ERR_REQUIRE_ASYNC_MODULE(env); - return; - } + // TODO(joyeecheung): record Module::HasTopLevelAwait() in every ModuleWrap and + // infer the asynchronicity from a module's children during linking. + args.GetReturnValue().Set(module->IsGraphAsync()); } void ModuleWrap::EvaluateSync(const FunctionCallbackInfo& args) { diff --git a/test/es-module/test-require-module-tla-retry-require.js b/test/es-module/test-require-module-tla-retry-require.js new file mode 100644 index 00000000000000..79d18f7d1d8bcf --- /dev/null +++ b/test/es-module/test-require-module-tla-retry-require.js @@ -0,0 +1,16 @@ +// This tests that after failing to require an ESM that contains TLA, +// retrying with import() still works, and produces consistent results. +'use strict'; +require('../common'); +const assert = require('assert'); + +assert.throws(() => { + require('../fixtures/es-modules/tla/resolved.mjs'); +}, { + code: 'ERR_REQUIRE_ASYNC_MODULE' +}); +assert.throws(() => { + require('../fixtures/es-modules/tla/resolved.mjs'); +}, { + code: 'ERR_REQUIRE_ASYNC_MODULE' +}); From 8e2df477e24d25711ada0da500926397a88fafc5 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 28 Oct 2024 13:25:40 +0100 Subject: [PATCH 2/3] fixup! module: fix error thrown from require(esm) hitting TLA repeatedly --- src/module_wrap.cc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 71a6b4f0362752..6f8c9d10955b88 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -31,7 +31,6 @@ using v8::FunctionTemplate; using v8::HandleScope; using v8::Int32; using v8::Integer; -using v8::IntegrityLevel; using v8::Isolate; using v8::Local; using v8::MaybeLocal; @@ -634,8 +633,8 @@ void ModuleWrap::InstantiateSync(const FunctionCallbackInfo& args) { } } - // TODO(joyeecheung): record Module::HasTopLevelAwait() in every ModuleWrap and - // infer the asynchronicity from a module's children during linking. + // TODO(joyeecheung): record Module::HasTopLevelAwait() in every ModuleWrap + // and infer the asynchronicity from a module's children during linking. args.GetReturnValue().Set(module->IsGraphAsync()); } From ee2822b35d3e9a9afd06ef8b0871cdde55371c32 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 29 Oct 2024 18:28:10 +0100 Subject: [PATCH 3/3] fixup! Open module: fix error thrown from require(esm) hitting TLA repeatedly --- test/es-module/test-require-module-tla-retry-require.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/es-module/test-require-module-tla-retry-require.js b/test/es-module/test-require-module-tla-retry-require.js index 79d18f7d1d8bcf..d440698fc22b52 100644 --- a/test/es-module/test-require-module-tla-retry-require.js +++ b/test/es-module/test-require-module-tla-retry-require.js @@ -1,5 +1,5 @@ // This tests that after failing to require an ESM that contains TLA, -// retrying with import() still works, and produces consistent results. +// retrying with require() still throws, and produces consistent results. 'use strict'; require('../common'); const assert = require('assert');