mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
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:
parent
ffb326c583
commit
86afefafda
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
10
lib/vm.js
10
lib/vm.js
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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');
|
||||
})();
|
||||
|
70
test/parallel/test-vm-module-referrer-realm.mjs
Normal file
70
test/parallel/test-vm-module-referrer-realm.mjs
Normal 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());
|
Loading…
Reference in New Issue
Block a user