vm: allow dynamic import with a referrer realm

A referrer can be a Script Record, a Cyclic Module Record, or a Realm
Record as defined in https://tc39.es/ecma262/#sec-HostLoadImportedModule.

Add support for dynamic import calls with a realm as the referrer and
allow specifying an `importModuleDynamically` callback in
`vm.createContext`.

PR-URL: https://github.com/nodejs/node/pull/50360
Refs: https://github.com/nodejs/node/issues/49726
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Chengzhong Wu 2023-11-01 14:05:13 +08:00 committed by GitHub
parent ffb326c583
commit 86afefafda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 22 deletions

View File

@ -1052,6 +1052,9 @@ function with the given `params`.
<!-- YAML
added: v0.3.1
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/50360
description: The `importModuleDynamically` option is supported now.
- version: v14.6.0
pr-url: https://github.com/nodejs/node/pull/34023
description: The `microtaskMode` option is supported now.
@ -1084,6 +1087,21 @@ changes:
scheduled through `Promise`s and `async function`s) will be run immediately
after a script has run through [`script.runInContext()`][].
They are included in the `timeout` and `breakOnSigint` scopes in that case.
* `importModuleDynamically` {Function} Called when `import()` is called in
this context without a referrer script or module. If this option is not
specified, calls to `import()` will reject with
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. If
`--experimental-vm-modules` isn't set, this callback will be ignored and
calls to `import()` will reject with
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`][].
* `specifier` {string} specifier passed to `import()`
* `contextObject` {Object} contextified object
* `importAttributes` {Object} The `"with"` value passed to the
[`optionsExpression`][] optional parameter, or an empty object if no value
was provided.
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
recommended in order to take advantage of error tracking, and to avoid
issues with namespaces that contain `then` function exports.
* Returns: {Object} contextified object.
If given a `contextObject`, the `vm.createContext()` method will [prepare

View File

@ -113,6 +113,16 @@ function getConditionsSet(conditions) {
*/
const moduleRegistries = new SafeWeakMap();
/**
* @typedef {ContextifyScript|Function|ModuleWrap|ContextifiedObject} Referrer
* A referrer can be a Script Record, a Cyclic Module Record, or a Realm Record
* as defined in https://tc39.es/ecma262/#sec-HostLoadImportedModule.
*
* In Node.js, a referrer is represented by a wrapper object of these records.
* A referrer object has a field |host_defined_option_symbol| initialized with
* a symbol.
*/
/**
* V8 would make sure that as long as import() can still be initiated from
* the referrer, the symbol referenced by |host_defined_option_symbol| should
@ -127,7 +137,7 @@ const moduleRegistries = new SafeWeakMap();
* referrer wrap is still around and can be passed into the callbacks.
* 2 is only there so that we can get the id symbol to configure the
* weak map.
* @param {ModuleWrap|ContextifyScript|Function} referrer The referrer to
* @param {Referrer} referrer The referrer to
* get the id symbol from. This is different from callbackReferrer which
* could be set by the caller.
* @param {ModuleRegistry} registry
@ -163,20 +173,20 @@ function initializeImportMetaObject(symbol, meta) {
/**
* Asynchronously imports a module dynamically using a callback function. The native callback.
* @param {symbol} symbol - Reference to the module.
* @param {symbol} referrerSymbol - Referrer symbol of the registered script, function, module, or contextified object.
* @param {string} specifier - The module specifier string.
* @param {Record<string, string>} attributes - The import attributes object.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
* @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing.
*/
async function importModuleDynamicallyCallback(symbol, specifier, attributes) {
if (moduleRegistries.has(symbol)) {
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(symbol);
async function importModuleDynamicallyCallback(referrerSymbol, specifier, attributes) {
if (moduleRegistries.has(referrerSymbol)) {
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol);
if (importModuleDynamically !== undefined) {
return importModuleDynamically(specifier, callbackReferrer, attributes);
}
}
if (symbol === vm_dynamic_import_missing_flag) {
if (referrerSymbol === vm_dynamic_import_missing_flag) {
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG();
}
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();

View File

@ -34,7 +34,7 @@ function isContext(object) {
return _isContext(object);
}
function getHostDefinedOptionId(importModuleDynamically, filename) {
function getHostDefinedOptionId(importModuleDynamically, hint) {
if (importModuleDynamically !== undefined) {
// Check that it's either undefined or a function before we pass
// it into the native constructor.
@ -57,7 +57,7 @@ function getHostDefinedOptionId(importModuleDynamically, filename) {
return vm_dynamic_import_missing_flag;
}
return Symbol(filename);
return Symbol(hint);
}
function registerImportModuleDynamically(referrer, importModuleDynamically) {

View File

@ -218,6 +218,7 @@ function createContext(contextObject = {}, options = kEmptyObject) {
origin,
codeGeneration,
microtaskMode,
importModuleDynamically,
} = options;
validateString(name, 'options.name');
@ -239,7 +240,14 @@ function createContext(contextObject = {}, options = kEmptyObject) {
['afterEvaluate', undefined]);
const microtaskQueue = (microtaskMode === 'afterEvaluate');
makeContext(contextObject, name, origin, strings, wasm, microtaskQueue);
const hostDefinedOptionId =
getHostDefinedOptionId(importModuleDynamically, name);
makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
// Register the context scope callback after the context was initialized.
if (importModuleDynamically !== undefined) {
registerImportModuleDynamically(contextObject, importModuleDynamically);
}
return contextObject;
}

View File

@ -564,22 +564,20 @@ static MaybeLocal<Promise> ImportModuleDynamically(
Local<Function> import_callback =
env->host_import_module_dynamically_callback();
Local<Value> id;
Local<FixedArray> options = host_defined_options.As<FixedArray>();
if (options->Length() != HostDefinedOptions::kLength) {
Local<Promise::Resolver> resolver;
if (!Promise::Resolver::New(context).ToLocal(&resolver)) return {};
resolver
->Reject(context,
v8::Exception::TypeError(FIXED_ONE_BYTE_STRING(
context->GetIsolate(), "Invalid host defined options")))
.ToChecked();
return handle_scope.Escape(resolver->GetPromise());
// Get referrer id symbol from the host-defined options.
// If the host-defined options are empty, get the referrer id symbol
// from the realm global object.
if (options->Length() == HostDefinedOptions::kLength) {
id = options->Get(context, HostDefinedOptions::kID).As<Symbol>();
} else {
id = context->Global()
->GetPrivate(context, env->host_defined_option_symbol())
.ToLocalChecked();
}
Local<Symbol> id =
options->Get(context, HostDefinedOptions::kID).As<Symbol>();
Local<Object> attributes =
createImportAttributesContainer(env, isolate, import_attributes);

View File

@ -288,6 +288,19 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
.IsNothing()) {
return BaseObjectPtr<ContextifyContext>();
}
// Assign host_defined_options_id to the global object so that in the
// callback of ImportModuleDynamically, we can get the
// host_defined_options_id from the v8::Context without accessing the
// wrapper object.
if (new_context_global
->SetPrivate(v8_context,
env->host_defined_option_symbol(),
options->host_defined_options_id)
.IsNothing()) {
return BaseObjectPtr<ContextifyContext>();
}
env->AssignToContext(v8_context, nullptr, info);
if (!env->contextify_wrapper_template()
@ -308,6 +321,16 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
.IsNothing()) {
return BaseObjectPtr<ContextifyContext>();
}
// Assign host_defined_options_id to the sandbox object so that module
// callbacks like importModuleDynamically can be registered once back to the
// JS land.
if (sandbox_obj
->SetPrivate(v8_context,
env->host_defined_option_symbol(),
options->host_defined_options_id)
.IsNothing()) {
return BaseObjectPtr<ContextifyContext>();
}
return result;
}
@ -344,7 +367,7 @@ void ContextifyContext::RegisterExternalReferences(
void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_EQ(args.Length(), 6);
CHECK_EQ(args.Length(), 7);
CHECK(args[0]->IsObject());
Local<Object> sandbox = args[0].As<Object>();
@ -375,6 +398,9 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit);
}
CHECK(args[6]->IsSymbol());
options.host_defined_options_id = args[6].As<Symbol>();
TryCatchScope try_catch(env);
BaseObjectPtr<ContextifyContext> context_ptr =
ContextifyContext::New(env, sandbox, &options);

View File

@ -18,6 +18,7 @@ struct ContextOptions {
v8::Local<v8::Boolean> allow_code_gen_strings;
v8::Local<v8::Boolean> allow_code_gen_wasm;
std::unique_ptr<v8::MicrotaskQueue> own_microtask_queue;
v8::Local<v8::Symbol> host_defined_options_id;
};
class ContextifyContext : public BaseObject {

View File

@ -69,4 +69,10 @@ function expectFsNamespace(result) {
// If the specifier is an origin-relative URL, it should
// be treated as a file: URL.
expectOkNamespace(import(targetURL.pathname));
// If the referrer is a realm record, there is no way to resolve the
// specifier.
// TODO(legendecas): https://github.com/tc39/ecma262/pull/3195
expectModuleError(Promise.resolve('import("node:fs")').then(eval),
'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING');
})();

View File

@ -0,0 +1,70 @@
// Flags: --experimental-vm-modules
import * as common from '../common/index.mjs';
import assert from 'node:assert';
import { Script, SourceTextModule, createContext } from 'node:vm';
async function test() {
const foo = new SourceTextModule('export const a = 1;');
await foo.link(common.mustNotCall());
await foo.evaluate();
const ctx = createContext({}, {
importModuleDynamically: common.mustCall((specifier, wrap) => {
assert.strictEqual(specifier, 'foo');
assert.strictEqual(wrap, ctx);
return foo;
}, 2),
});
{
const s = new Script('Promise.resolve("import(\'foo\')").then(eval)', {
importModuleDynamically: common.mustNotCall(),
});
const result = s.runInContext(ctx);
assert.strictEqual(await result, foo.namespace);
}
{
const m = new SourceTextModule('globalThis.fooResult = Promise.resolve("import(\'foo\')").then(eval)', {
context: ctx,
importModuleDynamically: common.mustNotCall(),
});
await m.link(common.mustNotCall());
await m.evaluate();
assert.strictEqual(await ctx.fooResult, foo.namespace);
delete ctx.fooResult;
}
}
async function testMissing() {
const ctx = createContext({});
{
const s = new Script('Promise.resolve("import(\'foo\')").then(eval)', {
importModuleDynamically: common.mustNotCall(),
});
const result = s.runInContext(ctx);
await assert.rejects(result, {
code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
});
}
{
const m = new SourceTextModule('globalThis.fooResult = Promise.resolve("import(\'foo\')").then(eval)', {
context: ctx,
importModuleDynamically: common.mustNotCall(),
});
await m.link(common.mustNotCall());
await m.evaluate();
await assert.rejects(ctx.fooResult, {
code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
});
delete ctx.fooResult;
}
}
await Promise.all([
test(),
testMissing(),
]).then(common.mustCall());