Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vm: allow dynamic import with a referrer realm #50360

Merged
merged 2 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
22 changes: 16 additions & 6 deletions lib/internal/modules/esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -57,7 +57,7 @@ function getHostDefinedOptionId(importModuleDynamically, filename) {
return vm_dynamic_import_missing_flag;
}

return Symbol(filename);
return Symbol(hint);
}

function registerImportModuleDynamically(referrer, importModuleDynamically) {
Expand Down
10 changes: 9 additions & 1 deletion lib/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ function createContext(contextObject = {}, options = kEmptyObject) {
origin,
codeGeneration,
microtaskMode,
importModuleDynamically,
} = options;

validateString(name, 'options.name');
Expand All @@ -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;
}

Expand Down
22 changes: 10 additions & 12 deletions src/module_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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()
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
->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);

Expand Down
28 changes: 27 additions & 1 deletion src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
->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()
Expand All @@ -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;
}
Expand Down Expand Up @@ -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>();

Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/node_contextify.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions test/es-module/test-esm-dynamic-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 70 additions & 0 deletions test/parallel/test-vm-module-referrer-realm.mjs
Original file line number Diff line number Diff line change
@@ -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