From cf0898358b6392a2779cd7bc072550de5cf3b40d Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 29 Oct 2024 22:15:19 +0100 Subject: [PATCH] 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. PR-URL: https://github.com/nodejs/node/pull/55520 Fixes: https://github.com/nodejs/node/issues/55516 Refs: https://github.com/nodejs/node/issues/52697 Reviewed-By: Paolo Insogna Reviewed-By: Antoine du Hamel Reviewed-By: Chengzhong Wu Reviewed-By: Jacob Smith --- lib/internal/errors.js | 3 +++ lib/internal/modules/esm/loader.js | 4 ++++ lib/internal/modules/esm/module_job.js | 16 ++++++++++++++-- src/module_wrap.cc | 12 +++--------- .../test-require-module-tla-retry-require.js | 16 ++++++++++++++++ 5 files changed, 40 insertions(+), 11 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 a14c0f9511251e..f31bac27e3fd40 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1652,6 +1652,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 ece30c3864d6a7..8fba05e7b8f699 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 = [ @@ -378,7 +381,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..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; @@ -332,7 +331,6 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { obj->contextify_context_ = contextify_context; - that->SetIntegrityLevel(context, IntegrityLevel::kFrozen); args.GetReturnValue().Set(that); } @@ -635,13 +633,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..d440698fc22b52 --- /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 require() still throws, 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' +});