Skip to content

Commit 400d3ce

Browse files
legendecasalexfernandez
authored andcommitted
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: nodejs#50360 Refs: nodejs#49726 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 154cffc commit 400d3ce

File tree

9 files changed

+159
-22
lines changed

9 files changed

+159
-22
lines changed

doc/api/vm.md

+18
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,9 @@ function with the given `params`.
10521052
<!-- YAML
10531053
added: v0.3.1
10541054
changes:
1055+
- version: REPLACEME
1056+
pr-url: https://github.com/nodejs/node/pull/50360
1057+
description: The `importModuleDynamically` option is supported now.
10551058
- version: v14.6.0
10561059
pr-url: https://github.com/nodejs/node/pull/34023
10571060
description: The `microtaskMode` option is supported now.
@@ -1084,6 +1087,21 @@ changes:
10841087
scheduled through `Promise`s and `async function`s) will be run immediately
10851088
after a script has run through [`script.runInContext()`][].
10861089
They are included in the `timeout` and `breakOnSigint` scopes in that case.
1090+
* `importModuleDynamically` {Function} Called when `import()` is called in
1091+
this context without a referrer script or module. If this option is not
1092+
specified, calls to `import()` will reject with
1093+
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. If
1094+
`--experimental-vm-modules` isn't set, this callback will be ignored and
1095+
calls to `import()` will reject with
1096+
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`][].
1097+
* `specifier` {string} specifier passed to `import()`
1098+
* `contextObject` {Object} contextified object
1099+
* `importAttributes` {Object} The `"with"` value passed to the
1100+
[`optionsExpression`][] optional parameter, or an empty object if no value
1101+
was provided.
1102+
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
1103+
recommended in order to take advantage of error tracking, and to avoid
1104+
issues with namespaces that contain `then` function exports.
10871105
* Returns: {Object} contextified object.
10881106

10891107
If given a `contextObject`, the `vm.createContext()` method will [prepare

lib/internal/modules/esm/utils.js

+16-6
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ function getConditionsSet(conditions) {
113113
*/
114114
const moduleRegistries = new SafeWeakMap();
115115

116+
/**
117+
* @typedef {ContextifyScript|Function|ModuleWrap|ContextifiedObject} Referrer
118+
* A referrer can be a Script Record, a Cyclic Module Record, or a Realm Record
119+
* as defined in https://tc39.es/ecma262/#sec-HostLoadImportedModule.
120+
*
121+
* In Node.js, a referrer is represented by a wrapper object of these records.
122+
* A referrer object has a field |host_defined_option_symbol| initialized with
123+
* a symbol.
124+
*/
125+
116126
/**
117127
* V8 would make sure that as long as import() can still be initiated from
118128
* the referrer, the symbol referenced by |host_defined_option_symbol| should
@@ -127,7 +137,7 @@ const moduleRegistries = new SafeWeakMap();
127137
* referrer wrap is still around and can be passed into the callbacks.
128138
* 2 is only there so that we can get the id symbol to configure the
129139
* weak map.
130-
* @param {ModuleWrap|ContextifyScript|Function} referrer The referrer to
140+
* @param {Referrer} referrer The referrer to
131141
* get the id symbol from. This is different from callbackReferrer which
132142
* could be set by the caller.
133143
* @param {ModuleRegistry} registry
@@ -163,20 +173,20 @@ function initializeImportMetaObject(symbol, meta) {
163173

164174
/**
165175
* Asynchronously imports a module dynamically using a callback function. The native callback.
166-
* @param {symbol} symbol - Reference to the module.
176+
* @param {symbol} referrerSymbol - Referrer symbol of the registered script, function, module, or contextified object.
167177
* @param {string} specifier - The module specifier string.
168178
* @param {Record<string, string>} attributes - The import attributes object.
169179
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
170180
* @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing.
171181
*/
172-
async function importModuleDynamicallyCallback(symbol, specifier, attributes) {
173-
if (moduleRegistries.has(symbol)) {
174-
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(symbol);
182+
async function importModuleDynamicallyCallback(referrerSymbol, specifier, attributes) {
183+
if (moduleRegistries.has(referrerSymbol)) {
184+
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol);
175185
if (importModuleDynamically !== undefined) {
176186
return importModuleDynamically(specifier, callbackReferrer, attributes);
177187
}
178188
}
179-
if (symbol === vm_dynamic_import_missing_flag) {
189+
if (referrerSymbol === vm_dynamic_import_missing_flag) {
180190
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG();
181191
}
182192
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();

lib/internal/vm.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function isContext(object) {
3434
return _isContext(object);
3535
}
3636

37-
function getHostDefinedOptionId(importModuleDynamically, filename) {
37+
function getHostDefinedOptionId(importModuleDynamically, hint) {
3838
if (importModuleDynamically !== undefined) {
3939
// Check that it's either undefined or a function before we pass
4040
// it into the native constructor.
@@ -57,7 +57,7 @@ function getHostDefinedOptionId(importModuleDynamically, filename) {
5757
return vm_dynamic_import_missing_flag;
5858
}
5959

60-
return Symbol(filename);
60+
return Symbol(hint);
6161
}
6262

6363
function registerImportModuleDynamically(referrer, importModuleDynamically) {

lib/vm.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ function createContext(contextObject = {}, options = kEmptyObject) {
218218
origin,
219219
codeGeneration,
220220
microtaskMode,
221+
importModuleDynamically,
221222
} = options;
222223

223224
validateString(name, 'options.name');
@@ -239,7 +240,14 @@ function createContext(contextObject = {}, options = kEmptyObject) {
239240
['afterEvaluate', undefined]);
240241
const microtaskQueue = (microtaskMode === 'afterEvaluate');
241242

242-
makeContext(contextObject, name, origin, strings, wasm, microtaskQueue);
243+
const hostDefinedOptionId =
244+
getHostDefinedOptionId(importModuleDynamically, name);
245+
246+
makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
247+
// Register the context scope callback after the context was initialized.
248+
if (importModuleDynamically !== undefined) {
249+
registerImportModuleDynamically(contextObject, importModuleDynamically);
250+
}
243251
return contextObject;
244252
}
245253

src/module_wrap.cc

+10-12
Original file line numberDiff line numberDiff line change
@@ -564,22 +564,20 @@ static MaybeLocal<Promise> ImportModuleDynamically(
564564

565565
Local<Function> import_callback =
566566
env->host_import_module_dynamically_callback();
567+
Local<Value> id;
567568

568569
Local<FixedArray> options = host_defined_options.As<FixedArray>();
569-
if (options->Length() != HostDefinedOptions::kLength) {
570-
Local<Promise::Resolver> resolver;
571-
if (!Promise::Resolver::New(context).ToLocal(&resolver)) return {};
572-
resolver
573-
->Reject(context,
574-
v8::Exception::TypeError(FIXED_ONE_BYTE_STRING(
575-
context->GetIsolate(), "Invalid host defined options")))
576-
.ToChecked();
577-
return handle_scope.Escape(resolver->GetPromise());
570+
// Get referrer id symbol from the host-defined options.
571+
// If the host-defined options are empty, get the referrer id symbol
572+
// from the realm global object.
573+
if (options->Length() == HostDefinedOptions::kLength) {
574+
id = options->Get(context, HostDefinedOptions::kID).As<Symbol>();
575+
} else {
576+
id = context->Global()
577+
->GetPrivate(context, env->host_defined_option_symbol())
578+
.ToLocalChecked();
578579
}
579580

580-
Local<Symbol> id =
581-
options->Get(context, HostDefinedOptions::kID).As<Symbol>();
582-
583581
Local<Object> attributes =
584582
createImportAttributesContainer(env, isolate, import_attributes);
585583

src/node_contextify.cc

+27-1
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,19 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
288288
.IsNothing()) {
289289
return BaseObjectPtr<ContextifyContext>();
290290
}
291+
292+
// Assign host_defined_options_id to the global object so that in the
293+
// callback of ImportModuleDynamically, we can get the
294+
// host_defined_options_id from the v8::Context without accessing the
295+
// wrapper object.
296+
if (new_context_global
297+
->SetPrivate(v8_context,
298+
env->host_defined_option_symbol(),
299+
options->host_defined_options_id)
300+
.IsNothing()) {
301+
return BaseObjectPtr<ContextifyContext>();
302+
}
303+
291304
env->AssignToContext(v8_context, nullptr, info);
292305

293306
if (!env->contextify_wrapper_template()
@@ -308,6 +321,16 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
308321
.IsNothing()) {
309322
return BaseObjectPtr<ContextifyContext>();
310323
}
324+
// Assign host_defined_options_id to the sandbox object so that module
325+
// callbacks like importModuleDynamically can be registered once back to the
326+
// JS land.
327+
if (sandbox_obj
328+
->SetPrivate(v8_context,
329+
env->host_defined_option_symbol(),
330+
options->host_defined_options_id)
331+
.IsNothing()) {
332+
return BaseObjectPtr<ContextifyContext>();
333+
}
311334

312335
return result;
313336
}
@@ -344,7 +367,7 @@ void ContextifyContext::RegisterExternalReferences(
344367
void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
345368
Environment* env = Environment::GetCurrent(args);
346369

347-
CHECK_EQ(args.Length(), 6);
370+
CHECK_EQ(args.Length(), 7);
348371
CHECK(args[0]->IsObject());
349372
Local<Object> sandbox = args[0].As<Object>();
350373

@@ -375,6 +398,9 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
375398
MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit);
376399
}
377400

401+
CHECK(args[6]->IsSymbol());
402+
options.host_defined_options_id = args[6].As<Symbol>();
403+
378404
TryCatchScope try_catch(env);
379405
BaseObjectPtr<ContextifyContext> context_ptr =
380406
ContextifyContext::New(env, sandbox, &options);

src/node_contextify.h

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct ContextOptions {
1818
v8::Local<v8::Boolean> allow_code_gen_strings;
1919
v8::Local<v8::Boolean> allow_code_gen_wasm;
2020
std::unique_ptr<v8::MicrotaskQueue> own_microtask_queue;
21+
v8::Local<v8::Symbol> host_defined_options_id;
2122
};
2223

2324
class ContextifyContext : public BaseObject {

test/es-module/test-esm-dynamic-import.js

+6
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,10 @@ function expectFsNamespace(result) {
6969
// If the specifier is an origin-relative URL, it should
7070
// be treated as a file: URL.
7171
expectOkNamespace(import(targetURL.pathname));
72+
73+
// If the referrer is a realm record, there is no way to resolve the
74+
// specifier.
75+
// TODO(legendecas): https://github.com/tc39/ecma262/pull/3195
76+
expectModuleError(Promise.resolve('import("node:fs")').then(eval),
77+
'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING');
7278
})();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Flags: --experimental-vm-modules
2+
import * as common from '../common/index.mjs';
3+
import assert from 'node:assert';
4+
import { Script, SourceTextModule, createContext } from 'node:vm';
5+
6+
async function test() {
7+
const foo = new SourceTextModule('export const a = 1;');
8+
await foo.link(common.mustNotCall());
9+
await foo.evaluate();
10+
11+
const ctx = createContext({}, {
12+
importModuleDynamically: common.mustCall((specifier, wrap) => {
13+
assert.strictEqual(specifier, 'foo');
14+
assert.strictEqual(wrap, ctx);
15+
return foo;
16+
}, 2),
17+
});
18+
{
19+
const s = new Script('Promise.resolve("import(\'foo\')").then(eval)', {
20+
importModuleDynamically: common.mustNotCall(),
21+
});
22+
23+
const result = s.runInContext(ctx);
24+
assert.strictEqual(await result, foo.namespace);
25+
}
26+
27+
{
28+
const m = new SourceTextModule('globalThis.fooResult = Promise.resolve("import(\'foo\')").then(eval)', {
29+
context: ctx,
30+
importModuleDynamically: common.mustNotCall(),
31+
});
32+
await m.link(common.mustNotCall());
33+
await m.evaluate();
34+
assert.strictEqual(await ctx.fooResult, foo.namespace);
35+
delete ctx.fooResult;
36+
}
37+
}
38+
39+
async function testMissing() {
40+
const ctx = createContext({});
41+
{
42+
const s = new Script('Promise.resolve("import(\'foo\')").then(eval)', {
43+
importModuleDynamically: common.mustNotCall(),
44+
});
45+
46+
const result = s.runInContext(ctx);
47+
await assert.rejects(result, {
48+
code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
49+
});
50+
}
51+
52+
{
53+
const m = new SourceTextModule('globalThis.fooResult = Promise.resolve("import(\'foo\')").then(eval)', {
54+
context: ctx,
55+
importModuleDynamically: common.mustNotCall(),
56+
});
57+
await m.link(common.mustNotCall());
58+
await m.evaluate();
59+
60+
await assert.rejects(ctx.fooResult, {
61+
code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
62+
});
63+
delete ctx.fooResult;
64+
}
65+
}
66+
67+
await Promise.all([
68+
test(),
69+
testMissing(),
70+
]).then(common.mustCall());

0 commit comments

Comments
 (0)