| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 2f86d50 commit 773cfa5
9 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1052,6 +1052,9 @@ function with the given `params`. | |||
| 1052 | 1052 | <!-- YAML | |
| 1053 | 1053 | added: v0.3.1 | |
| 1054 | 1054 | changes: | |
| 1055 | + - version: REPLACEME | ||
| 1056 | + pr-url: https://github.com/nodejs/node/pull/50360 | ||
| 1057 | + description: The `importModuleDynamically` option is supported now. | ||
| 1055 | 1058 | - version: v14.6.0 | |
| 1056 | 1059 | pr-url: https://github.com/nodejs/node/pull/34023 | |
| 1057 | 1060 | description: The `microtaskMode` option is supported now. | |
@@ -1084,6 +1087,21 @@ changes: | |||
| 1084 | 1087 | scheduled through `Promise`s and `async function`s) will be run immediately | |
| 1085 | 1088 | after a script has run through [`script.runInContext()`][]. | |
| 1086 | 1089 | 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. | ||
| 1087 | 1105 | * Returns: {Object} contextified object. | |
| 1088 | 1106 | ||
| 1089 | 1107 | If given a `contextObject`, the `vm.createContext()` method will [prepare | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -113,6 +113,16 @@ function getConditionsSet(conditions) { | |||
| 113 | 113 | */ | |
| 114 | 114 | const moduleRegistries = new SafeWeakMap(); | |
| 115 | 115 | ||
| 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 | + | ||
| 116 | 126 | /** | |
| 117 | 127 | * V8 would make sure that as long as import() can still be initiated from | |
| 118 | 128 | * the referrer, the symbol referenced by |host_defined_option_symbol| should | |
@@ -127,7 +137,7 @@ const moduleRegistries = new SafeWeakMap(); | |||
| 127 | 137 | * referrer wrap is still around and can be passed into the callbacks. | |
| 128 | 138 | * 2 is only there so that we can get the id symbol to configure the | |
| 129 | 139 | * weak map. | |
| 130 | - * @param {ModuleWrap|ContextifyScript|Function} referrer The referrer to | ||
| 140 | + * @param {Referrer} referrer The referrer to | ||
| 131 | 141 | * get the id symbol from. This is different from callbackReferrer which | |
| 132 | 142 | * could be set by the caller. | |
| 133 | 143 | * @param {ModuleRegistry} registry | |
@@ -163,20 +173,20 @@ function initializeImportMetaObject(symbol, meta) { | |||
| 163 | 173 | ||
| 164 | 174 | /** | |
| 165 | 175 | * 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. | ||
| 167 | 177 | * @param {string} specifier - The module specifier string. | |
| 168 | 178 | * @param {Record<string, string>} attributes - The import attributes object. | |
| 169 | 179 | * @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object. | |
| 170 | 180 | * @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing. | |
| 171 | 181 | */ | |
| 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); | ||
| 175 | 185 | if (importModuleDynamically !== undefined) { | |
| 176 | 186 | return importModuleDynamically(specifier, callbackReferrer, attributes); | |
| 177 | 187 | } | |
| 178 | 188 | } | |
| 179 | - if (symbol === vm_dynamic_import_missing_flag) { | ||
| 189 | + if (referrerSymbol === vm_dynamic_import_missing_flag) { | ||
| 180 | 190 | throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG(); | |
| 181 | 191 | } | |
| 182 | 192 | throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING(); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -34,7 +34,7 @@ function isContext(object) { | |||
| 34 | 34 | return _isContext(object); | |
| 35 | 35 | } | |
| 36 | 36 | ||
| 37 | - function getHostDefinedOptionId(importModuleDynamically, filename) { | ||
| 37 | + function getHostDefinedOptionId(importModuleDynamically, hint) { | ||
| 38 | 38 | if (importModuleDynamically !== undefined) { | |
| 39 | 39 | // Check that it's either undefined or a function before we pass | |
| 40 | 40 | // it into the native constructor. | |
@@ -57,7 +57,7 @@ function getHostDefinedOptionId(importModuleDynamically, filename) { | |||
| 57 | 57 | return vm_dynamic_import_missing_flag; | |
| 58 | 58 | } | |
| 59 | 59 | ||
| 60 | - return Symbol(filename); | ||
| 60 | + return Symbol(hint); | ||
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | 63 | function registerImportModuleDynamically(referrer, importModuleDynamically) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -218,6 +218,7 @@ function createContext(contextObject = {}, options = kEmptyObject) { | |||
| 218 | 218 | origin, | |
| 219 | 219 | codeGeneration, | |
| 220 | 220 | microtaskMode, | |
| 221 | + importModuleDynamically, | ||
| 221 | 222 | } = options; | |
| 222 | 223 | ||
| 223 | 224 | validateString(name, 'options.name'); | |
@@ -239,7 +240,14 @@ function createContext(contextObject = {}, options = kEmptyObject) { | |||
| 239 | 240 | ['afterEvaluate', undefined]); | |
| 240 | 241 | const microtaskQueue = (microtaskMode === 'afterEvaluate'); | |
| 241 | 242 | ||
| 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 | + } | ||
| 243 | 251 | return contextObject; | |
| 244 | 252 | } | |
| 245 | 253 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -564,22 +564,20 @@ static MaybeLocal<Promise> ImportModuleDynamically( | |||
| 564 | 564 | ||
| 565 | 565 | Local<Function> import_callback = | |
| 566 | 566 | env->host_import_module_dynamically_callback(); | |
| 567 | + Local<Value> id; | ||
| 567 | 568 | ||
| 568 | 569 | 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(); | ||
| 578 | 579 | } | |
| 579 | 580 | ||
| 580 | - Local<Symbol> id = | ||
| 581 | - options->Get(context, HostDefinedOptions::kID).As<Symbol>(); | ||
| 582 | - | ||
| 583 | 581 | Local<Object> attributes = | |
| 584 | 582 | createImportAttributesContainer(env, isolate, import_attributes); | |
| 585 | 583 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -288,6 +288,19 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New( | |||
| 288 | 288 | .IsNothing()) { | |
| 289 | 289 | return BaseObjectPtr<ContextifyContext>(); | |
| 290 | 290 | } | |
| 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 | + | ||
| 291 | 304 | env->AssignToContext(v8_context, nullptr, info); | |
| 292 | 305 | ||
| 293 | 306 | if (!env->contextify_wrapper_template() | |
@@ -308,6 +321,16 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New( | |||
| 308 | 321 | .IsNothing()) { | |
| 309 | 322 | return BaseObjectPtr<ContextifyContext>(); | |
| 310 | 323 | } | |
| 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 | + } | ||
| 311 | 334 | ||
| 312 | 335 | return result; | |
| 313 | 336 | } | |
@@ -344,7 +367,7 @@ void ContextifyContext::RegisterExternalReferences( | |||
| 344 | 367 | void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) { | |
| 345 | 368 | Environment* env = Environment::GetCurrent(args); | |
| 346 | 369 | ||
| 347 | - CHECK_EQ(args.Length(), 6); | ||
| 370 | + CHECK_EQ(args.Length(), 7); | ||
| 348 | 371 | CHECK(args[0]->IsObject()); | |
| 349 | 372 | Local<Object> sandbox = args[0].As<Object>(); | |
| 350 | 373 | ||
@@ -375,6 +398,9 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) { | |||
| 375 | 398 | MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit); | |
| 376 | 399 | } | |
| 377 | 400 | ||
| 401 | + CHECK(args[6]->IsSymbol()); | ||
| 402 | + options.host_defined_options_id = args[6].As<Symbol>(); | ||
| 403 | + | ||
| 378 | 404 | TryCatchScope try_catch(env); | |
| 379 | 405 | BaseObjectPtr<ContextifyContext> context_ptr = | |
| 380 | 406 | ContextifyContext::New(env, sandbox, &options); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -18,6 +18,7 @@ struct ContextOptions { | |||
| 18 | 18 | v8::Local<v8::Boolean> allow_code_gen_strings; | |
| 19 | 19 | v8::Local<v8::Boolean> allow_code_gen_wasm; | |
| 20 | 20 | std::unique_ptr<v8::MicrotaskQueue> own_microtask_queue; | |
| 21 | + v8::Local<v8::Symbol> host_defined_options_id; | ||
| 21 | 22 | }; | |
| 22 | 23 | ||
| 23 | 24 | class ContextifyContext : public BaseObject { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -69,4 +69,10 @@ function expectFsNamespace(result) { | |||
| 69 | 69 | // If the specifier is an origin-relative URL, it should | |
| 70 | 70 | // be treated as a file: URL. | |
| 71 | 71 | 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'); | ||
| 72 | 78 | })(); | |
| Original file line number | Diff line number | Diff 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()); | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments