vm: import call should return a promise in the current context

A `import` call should returns a promise created in the context where
the `import` was called, not the context of `importModuleDynamically`
callback.

PR-URL: https://github.com/nodejs/node/pull/58309
Fixes: https://github.com/nodejs/node/issues/53575
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
This commit is contained in:
Chengzhong Wu 2025-05-16 08:43:48 +01:00 committed by GitHub
parent 0315283cbd
commit 1d0b4e8b91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 150 additions and 8 deletions

View File

@ -1020,16 +1020,23 @@ static MaybeLocal<Promise> ImportModuleDynamicallyWithPhase(
};
Local<Value> result;
if (import_callback->Call(
context,
Undefined(isolate),
arraysize(import_args),
import_args).ToLocal(&result)) {
CHECK(result->IsPromise());
return handle_scope.Escape(result.As<Promise>());
if (!import_callback
->Call(
context, Undefined(isolate), arraysize(import_args), import_args)
.ToLocal(&result)) {
return {};
}
return MaybeLocal<Promise>();
// Wrap the returned value in a promise created in the referrer context to
// avoid dynamic scopes.
Local<Promise::Resolver> resolver;
if (!Promise::Resolver::New(context).ToLocal(&resolver)) {
return {};
}
if (resolver->Resolve(context, result).IsNothing()) {
return {};
}
return handle_scope.Escape(resolver->GetPromise());
}
static MaybeLocal<Promise> ImportModuleDynamically(

View File

@ -0,0 +1,135 @@
// Flags: --experimental-vm-modules
'use strict';
const common = require('../common');
const assert = require('assert');
const { createContext, Script, SourceTextModule } = require('vm');
// Verifies that a `import` call returns a promise created in the context
// where the `import` was called, not the context of `importModuleDynamically`
// callback.
async function testScript() {
const ctx = createContext();
const mod1 = new SourceTextModule('export const a = 1;', {
context: ctx,
});
// No import statements, so must not link statically.
await mod1.link(common.mustNotCall());
const script2 = new Script(`
const promise = import("mod1");
if (Object.getPrototypeOf(promise) !== Promise.prototype) {
throw new Error('Expected promise to be created in the current context');
}
globalThis.__result = promise;
`, {
importModuleDynamically: common.mustCall((specifier, referrer) => {
assert.strictEqual(specifier, 'mod1');
assert.strictEqual(referrer, script2);
return mod1;
}),
});
script2.runInContext(ctx);
// Wait for the promise to resolve.
await ctx.__result;
}
async function testScriptImportFailed() {
const ctx = createContext();
const mod1 = new SourceTextModule('export const a = 1;', {
context: ctx,
});
// No import statements, so must not link statically.
await mod1.link(common.mustNotCall());
const err = new Error('import failed');
const script2 = new Script(`
const promise = import("mod1");
if (Object.getPrototypeOf(promise) !== Promise.prototype) {
throw new Error('Expected promise to be created in the current context');
}
globalThis.__result = promise;
`, {
importModuleDynamically: common.mustCall((specifier, referrer) => {
throw err;
}),
});
script2.runInContext(ctx);
// Wait for the promise to reject.
await assert.rejects(ctx.__result, err);
}
async function testModule() {
const ctx = createContext();
const mod1 = new SourceTextModule('export const a = 1;', {
context: ctx,
});
// No import statements, so must not link statically.
await mod1.link(common.mustNotCall());
const mod2 = new SourceTextModule(`
const promise = import("mod1");
if (Object.getPrototypeOf(promise) !== Promise.prototype) {
throw new Error('Expected promise to be created in the current context');
}
await promise;
`, {
context: ctx,
importModuleDynamically: common.mustCall((specifier, referrer) => {
assert.strictEqual(specifier, 'mod1');
assert.strictEqual(referrer, mod2);
return mod1;
}),
});
// No import statements, so must not link statically.
await mod2.link(common.mustNotCall());
await mod2.evaluate();
}
async function testModuleImportFailed() {
const ctx = createContext();
const mod1 = new SourceTextModule('export const a = 1;', {
context: ctx,
});
// No import statements, so must not link statically.
await mod1.link(common.mustNotCall());
const err = new Error('import failed');
ctx.__err = err;
const mod2 = new SourceTextModule(`
const promise = import("mod1");
if (Object.getPrototypeOf(promise) !== Promise.prototype) {
throw new Error('Expected promise to be created in the current context');
}
await promise.then(() => {
throw new Error('Expected promise to be rejected');
}, (e) => {
if (e !== globalThis.__err) {
throw new Error('Expected promise to be rejected with "import failed"');
}
});
`, {
context: ctx,
importModuleDynamically: common.mustCall((specifier, referrer) => {
throw err;
}),
});
// No import statements, so must not link statically.
await mod2.link(common.mustNotCall());
await mod2.evaluate();
}
Promise.all([
testScript(),
testScriptImportFailed(),
testModule(),
testModuleImportFailed(),
]).then(common.mustCall());