Skip to content

Commit 440abda

Browse files
committed
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: nodejs#55520 Fixes: nodejs#55516 Refs: nodejs#52697 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Jacob Smith <jacob@frende.me>
1 parent dc7686d commit 440abda

File tree

5 files changed

+40
-11
lines changed

5 files changed

+40
-11
lines changed

lib/internal/errors.js

+3
Original file line numberDiff line numberDiff line change
@@ -1692,6 +1692,9 @@ E('ERR_PARSE_ARGS_UNKNOWN_OPTION', (option, allowPositionals) => {
16921692
E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
16931693
'%d is not a valid timestamp', TypeError);
16941694
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError);
1695+
E('ERR_REQUIRE_ASYNC_MODULE', 'require() cannot be used on an ESM ' +
1696+
'graph with top-level await. Use import() instead. To see where the' +
1697+
' top-level await comes from, use --experimental-print-required-tla.', Error);
16951698
E('ERR_REQUIRE_CYCLE_MODULE', '%s', Error);
16961699
E('ERR_REQUIRE_ESM',
16971700
function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) {

lib/internal/modules/esm/loader.js

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const { imported_cjs_symbol } = internalBinding('symbols');
2222

2323
const assert = require('internal/assert');
2424
const {
25+
ERR_REQUIRE_ASYNC_MODULE,
2526
ERR_REQUIRE_CYCLE_MODULE,
2627
ERR_REQUIRE_ESM,
2728
ERR_NETWORK_IMPORT_DISALLOWED,
@@ -293,6 +294,9 @@ class ModuleLoader {
293294
// evaluated at this point.
294295
if (job !== undefined) {
295296
mod[kRequiredModuleSymbol] = job.module;
297+
if (job.module.async) {
298+
throw new ERR_REQUIRE_ASYNC_MODULE();
299+
}
296300
if (job.module.getStatus() !== kEvaluated) {
297301
const parentFilename = urlToFilename(parent?.filename);
298302
let message = `Cannot require() ES Module ${filename} in a cycle.`;

lib/internal/modules/esm/module_job.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ const resolvedPromise = PromiseResolve();
3232
const {
3333
setHasStartedUserESMExecution,
3434
} = require('internal/modules/helpers');
35+
const { getOptionValue } = require('internal/options');
3536
const noop = FunctionPrototype;
36-
37+
const {
38+
ERR_REQUIRE_ASYNC_MODULE,
39+
} = require('internal/errors').codes;
3740
let hasPausedEntry = false;
3841

3942
const CJSGlobalLike = [
@@ -370,7 +373,16 @@ class ModuleJobSync extends ModuleJobBase {
370373

371374
runSync() {
372375
// TODO(joyeecheung): add the error decoration logic from the async instantiate.
373-
this.module.instantiateSync();
376+
this.module.async = this.module.instantiateSync();
377+
// If --experimental-print-required-tla is true, proceeds to evaluation even
378+
// if it's async because we want to search for the TLA and help users locate
379+
// them.
380+
// TODO(joyeecheung): track the asynchroniticy using v8::Module::HasTopLevelAwait()
381+
// and we'll be able to throw right after compilation of the modules, using acron
382+
// to find and print the TLA.
383+
if (this.module.async && !getOptionValue('--experimental-print-required-tla')) {
384+
throw new ERR_REQUIRE_ASYNC_MODULE();
385+
}
374386
setHasStartedUserESMExecution();
375387
const namespace = this.module.evaluateSync();
376388
return { __proto__: null, module: this.module, namespace };

src/module_wrap.cc

+3-9
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ using v8::FunctionTemplate;
3131
using v8::HandleScope;
3232
using v8::Int32;
3333
using v8::Integer;
34-
using v8::IntegrityLevel;
3534
using v8::Isolate;
3635
using v8::Local;
3736
using v8::MaybeLocal;
@@ -290,7 +289,6 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
290289

291290
obj->contextify_context_ = contextify_context;
292291

293-
that->SetIntegrityLevel(context, IntegrityLevel::kFrozen);
294292
args.GetReturnValue().Set(that);
295293
}
296294

@@ -581,13 +579,9 @@ void ModuleWrap::InstantiateSync(const FunctionCallbackInfo<Value>& args) {
581579
}
582580
}
583581

584-
// If --experimental-print-required-tla is true, proceeds to evaluation even
585-
// if it's async because we want to search for the TLA and help users locate
586-
// them.
587-
if (module->IsGraphAsync() && !env->options()->print_required_tla) {
588-
THROW_ERR_REQUIRE_ASYNC_MODULE(env);
589-
return;
590-
}
582+
// TODO(joyeecheung): record Module::HasTopLevelAwait() in every ModuleWrap
583+
// and infer the asynchronicity from a module's children during linking.
584+
args.GetReturnValue().Set(module->IsGraphAsync());
591585
}
592586

593587
void ModuleWrap::EvaluateSync(const FunctionCallbackInfo<Value>& args) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// This tests that after failing to require an ESM that contains TLA,
2+
// retrying with require() still throws, and produces consistent results.
3+
'use strict';
4+
require('../common');
5+
const assert = require('assert');
6+
7+
assert.throws(() => {
8+
require('../fixtures/es-modules/tla/resolved.mjs');
9+
}, {
10+
code: 'ERR_REQUIRE_ASYNC_MODULE'
11+
});
12+
assert.throws(() => {
13+
require('../fixtures/es-modules/tla/resolved.mjs');
14+
}, {
15+
code: 'ERR_REQUIRE_ASYNC_MODULE'
16+
});

0 commit comments

Comments
 (0)