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
|
<!-- YAML
|
||||||
added: v0.3.1
|
added: v0.3.1
|
||||||
changes:
|
changes:
|
||||||
|
- version: REPLACEME
|
||||||
|
pr-url: https://github.com/nodejs/node/pull/50360
|
||||||
|
description: The `importModuleDynamically` option is supported now.
|
||||||
- version: v14.6.0
|
- version: v14.6.0
|
||||||
pr-url: https://github.com/nodejs/node/pull/34023
|
pr-url: https://github.com/nodejs/node/pull/34023
|
||||||
description: The `microtaskMode` option is supported now.
|
description: The `microtaskMode` option is supported now.
|
||||||
@ -1084,6 +1087,21 @@ changes:
|
|||||||
scheduled through `Promise`s and `async function`s) will be run immediately
|
scheduled through `Promise`s and `async function`s) will be run immediately
|
||||||
after a script has run through [`script.runInContext()`][].
|
after a script has run through [`script.runInContext()`][].
|
||||||
They are included in the `timeout` and `breakOnSigint` scopes in that case.
|
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.
|
* Returns: {Object} contextified object.
|
||||||
|
|
||||||
If given a `contextObject`, the `vm.createContext()` method will [prepare
|
If given a `contextObject`, the `vm.createContext()` method will [prepare
|
||||||
|
@ -113,6 +113,16 @@ function getConditionsSet(conditions) {
|
|||||||
*/
|
*/
|
||||||
const moduleRegistries = new SafeWeakMap();
|
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
|
* 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
|
* 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.
|
* 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
|
* 2 is only there so that we can get the id symbol to configure the
|
||||||
* weak map.
|
* 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
|
* get the id symbol from. This is different from callbackReferrer which
|
||||||
* could be set by the caller.
|
* could be set by the caller.
|
||||||
* @param {ModuleRegistry} registry
|
* @param {ModuleRegistry} registry
|
||||||
@ -163,20 +173,20 @@ function initializeImportMetaObject(symbol, meta) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously imports a module dynamically using a callback function. The native callback.
|
* 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 {string} specifier - The module specifier string.
|
||||||
* @param {Record<string, string>} attributes - The import attributes object.
|
* @param {Record<string, string>} attributes - The import attributes object.
|
||||||
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module 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.
|
* @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing.
|
||||||
*/
|
*/
|
||||||
async function importModuleDynamicallyCallback(symbol, specifier, attributes) {
|
async function importModuleDynamicallyCallback(referrerSymbol, specifier, attributes) {
|
||||||
if (moduleRegistries.has(symbol)) {
|
if (moduleRegistries.has(referrerSymbol)) {
|
||||||
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(symbol);
|
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol);
|
||||||
if (importModuleDynamically !== undefined) {
|
if (importModuleDynamically !== undefined) {
|
||||||
return importModuleDynamically(specifier, callbackReferrer, attributes);
|
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_FLAG();
|
||||||
}
|
}
|
||||||
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
|
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
|
||||||
|
@ -34,7 +34,7 @@ function isContext(object) {
|
|||||||
return _isContext(object);
|
return _isContext(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHostDefinedOptionId(importModuleDynamically, filename) {
|
function getHostDefinedOptionId(importModuleDynamically, hint) {
|
||||||
if (importModuleDynamically !== undefined) {
|
if (importModuleDynamically !== undefined) {
|
||||||
// Check that it's either undefined or a function before we pass
|
// Check that it's either undefined or a function before we pass
|
||||||
// it into the native constructor.
|
// it into the native constructor.
|
||||||
@ -57,7 +57,7 @@ function getHostDefinedOptionId(importModuleDynamically, filename) {
|
|||||||
return vm_dynamic_import_missing_flag;
|
return vm_dynamic_import_missing_flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Symbol(filename);
|
return Symbol(hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerImportModuleDynamically(referrer, importModuleDynamically) {
|
function registerImportModuleDynamically(referrer, importModuleDynamically) {
|
||||||
|
10
lib/vm.js
10
lib/vm.js
@ -218,6 +218,7 @@ function createContext(contextObject = {}, options = kEmptyObject) {
|
|||||||
origin,
|
origin,
|
||||||
codeGeneration,
|
codeGeneration,
|
||||||
microtaskMode,
|
microtaskMode,
|
||||||
|
importModuleDynamically,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
validateString(name, 'options.name');
|
validateString(name, 'options.name');
|
||||||
@ -239,7 +240,14 @@ function createContext(contextObject = {}, options = kEmptyObject) {
|
|||||||
['afterEvaluate', undefined]);
|
['afterEvaluate', undefined]);
|
||||||
const microtaskQueue = (microtaskMode === 'afterEvaluate');
|
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;
|
return contextObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,22 +564,20 @@ static MaybeLocal<Promise> ImportModuleDynamically(
|
|||||||
|
|
||||||
Local<Function> import_callback =
|
Local<Function> import_callback =
|
||||||
env->host_import_module_dynamically_callback();
|
env->host_import_module_dynamically_callback();
|
||||||
|
Local<Value> id;
|
||||||
|
|
||||||
Local<FixedArray> options = host_defined_options.As<FixedArray>();
|
Local<FixedArray> options = host_defined_options.As<FixedArray>();
|
||||||
if (options->Length() != HostDefinedOptions::kLength) {
|
// Get referrer id symbol from the host-defined options.
|
||||||
Local<Promise::Resolver> resolver;
|
// If the host-defined options are empty, get the referrer id symbol
|
||||||
if (!Promise::Resolver::New(context).ToLocal(&resolver)) return {};
|
// from the realm global object.
|
||||||
resolver
|
if (options->Length() == HostDefinedOptions::kLength) {
|
||||||
->Reject(context,
|
id = options->Get(context, HostDefinedOptions::kID).As<Symbol>();
|
||||||
v8::Exception::TypeError(FIXED_ONE_BYTE_STRING(
|
} else {
|
||||||
context->GetIsolate(), "Invalid host defined options")))
|
id = context->Global()
|
||||||
.ToChecked();
|
->GetPrivate(context, env->host_defined_option_symbol())
|
||||||
return handle_scope.Escape(resolver->GetPromise());
|
.ToLocalChecked();
|
||||||
}
|
}
|
||||||
|
|
||||||
Local<Symbol> id =
|
|
||||||
options->Get(context, HostDefinedOptions::kID).As<Symbol>();
|
|
||||||
|
|
||||||
Local<Object> attributes =
|
Local<Object> attributes =
|
||||||
createImportAttributesContainer(env, isolate, import_attributes);
|
createImportAttributesContainer(env, isolate, import_attributes);
|
||||||
|
|
||||||
|
@ -288,6 +288,19 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
|
|||||||
.IsNothing()) {
|
.IsNothing()) {
|
||||||
return BaseObjectPtr<ContextifyContext>();
|
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);
|
env->AssignToContext(v8_context, nullptr, info);
|
||||||
|
|
||||||
if (!env->contextify_wrapper_template()
|
if (!env->contextify_wrapper_template()
|
||||||
@ -308,6 +321,16 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
|
|||||||
.IsNothing()) {
|
.IsNothing()) {
|
||||||
return BaseObjectPtr<ContextifyContext>();
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
@ -344,7 +367,7 @@ void ContextifyContext::RegisterExternalReferences(
|
|||||||
void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
|
void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
|
||||||
Environment* env = Environment::GetCurrent(args);
|
Environment* env = Environment::GetCurrent(args);
|
||||||
|
|
||||||
CHECK_EQ(args.Length(), 6);
|
CHECK_EQ(args.Length(), 7);
|
||||||
CHECK(args[0]->IsObject());
|
CHECK(args[0]->IsObject());
|
||||||
Local<Object> sandbox = args[0].As<Object>();
|
Local<Object> sandbox = args[0].As<Object>();
|
||||||
|
|
||||||
@ -375,6 +398,9 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
|
|||||||
MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit);
|
MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CHECK(args[6]->IsSymbol());
|
||||||
|
options.host_defined_options_id = args[6].As<Symbol>();
|
||||||
|
|
||||||
TryCatchScope try_catch(env);
|
TryCatchScope try_catch(env);
|
||||||
BaseObjectPtr<ContextifyContext> context_ptr =
|
BaseObjectPtr<ContextifyContext> context_ptr =
|
||||||
ContextifyContext::New(env, sandbox, &options);
|
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_strings;
|
||||||
v8::Local<v8::Boolean> allow_code_gen_wasm;
|
v8::Local<v8::Boolean> allow_code_gen_wasm;
|
||||||
std::unique_ptr<v8::MicrotaskQueue> own_microtask_queue;
|
std::unique_ptr<v8::MicrotaskQueue> own_microtask_queue;
|
||||||
|
v8::Local<v8::Symbol> host_defined_options_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ContextifyContext : public BaseObject {
|
class ContextifyContext : public BaseObject {
|
||||||
|
@ -69,4 +69,10 @@ function expectFsNamespace(result) {
|
|||||||
// If the specifier is an origin-relative URL, it should
|
// If the specifier is an origin-relative URL, it should
|
||||||
// be treated as a file: URL.
|
// be treated as a file: URL.
|
||||||
expectOkNamespace(import(targetURL.pathname));
|
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