From 011e6e003241a0c45aca3907a604a148dd72a815 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 Backport-PR-URL: https://github.com/nodejs/node/pull/56927 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 c03e285607c9cf..b0efb9c1071425 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1692,6 +1692,9 @@ E('ERR_PARSE_ARGS_UNKNOWN_OPTION', (option, allowPositionals) => { E('ERR_PERFORMANCE_INVALID_TIMESTAMP', '%d is not a valid timestamp', TypeError); E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError); +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 7047925f2d02fb..271c094b99b4de 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -22,6 +22,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, @@ -293,6 +294,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 d791a3b51f476b..0c8f031080a314 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -32,8 +32,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 = [ @@ -370,7 +373,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 64bb1ec3db9b14..fe76e772cc9a38 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; @@ -290,7 +289,6 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { obj->contextify_context_ = contextify_context; - that->SetIntegrityLevel(context, IntegrityLevel::kFrozen); args.GetReturnValue().Set(that); } @@ -581,13 +579,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' +});