vm: allow dynamic import with a referrer realm · nodejs/node@2664012 · GitHub
Skip to content

Commit 2664012

Browse files
legendecasUlisesGascon
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: #50360 Refs: #49726 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent c6c0a74 commit 2664012

9 files changed

Lines changed: 159 additions & 22 deletions

File tree

doc/api/vm.md

Lines changed: 18 additions & 0 deletions

lib/internal/modules/esm/utils.js

Lines changed: 16 additions & 6 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 9 additions & 1 deletion
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

Lines changed: 10 additions & 12 deletions
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

Lines changed: 27 additions & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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
})();
Lines changed: 70 additions & 0 deletions

0 commit comments

Comments
 (0)