diff options
author | Gus Caplan <me@gus.host> | 2018-01-13 23:35:51 -0800 |
---|---|---|
committer | Myles Borins <mylesborins@google.com> | 2018-02-21 14:20:22 -0500 |
commit | 1cbd76a100845ae3f7e5a94993245c6bae5e1c9e (patch) | |
tree | 4d76bbaaa434be9918ba0380f4cd514f8f9fecc2 | |
parent | a258f6b5cea9f027032067406eceda5c7abdb4dd (diff) | |
download | node-new-1cbd76a100845ae3f7e5a94993245c6bae5e1c9e.tar.gz |
vm: add modules
Adds vm.Module, which wraps around ModuleWrap to provide an interface
for developers to work with modules in a more reflective manner.
Co-authored-by: Timothy Gu <timothygu99@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/17560
Reviewed-By: Michaƫl Zasso <targos@protonmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
-rw-r--r-- | doc/api/errors.md | 37 | ||||
-rw-r--r-- | doc/api/vm.md | 322 | ||||
-rw-r--r-- | lib/internal/errors.js | 9 | ||||
-rw-r--r-- | lib/internal/vm/Module.js | 205 | ||||
-rw-r--r-- | lib/vm.js | 5 | ||||
-rw-r--r-- | node.gyp | 1 | ||||
-rw-r--r-- | src/module_wrap.cc | 181 | ||||
-rw-r--r-- | src/module_wrap.h | 8 | ||||
-rw-r--r-- | src/node.cc | 12 | ||||
-rw-r--r-- | src/node_config.cc | 3 | ||||
-rw-r--r-- | src/node_internals.h | 5 | ||||
-rw-r--r-- | test/parallel/test-vm-module-basic.js | 54 | ||||
-rw-r--r-- | test/parallel/test-vm-module-dynamic-import.js | 27 | ||||
-rw-r--r-- | test/parallel/test-vm-module-errors.js | 264 | ||||
-rw-r--r-- | test/parallel/test-vm-module-link.js | 135 | ||||
-rw-r--r-- | test/parallel/test-vm-module-reevaluate.js | 49 |
16 files changed, 1281 insertions, 36 deletions
diff --git a/doc/api/errors.md b/doc/api/errors.md index 25b57ddac5..25f3a55194 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1552,6 +1552,43 @@ entry types were found. A given value is out of the accepted range. +<a id="ERR_VM_MODULE_ALREADY_LINKED"></a> +### ERR_VM_MODULE_ALREADY_LINKED + +The module attempted to be linked is not eligible for linking, because of one of +the following reasons: + +- It has already been linked (`linkingStatus` is `'linked'`) +- It is being linked (`linkingStatus` is `'linking'`) +- Linking has failed for this module (`linkingStatus` is `'errored'`) + +<a id="ERR_VM_MODULE_DIFFERENT_CONTEXT"></a> +### ERR_VM_MODULE_DIFFERENT_CONTEXT + +The module being returned from the linker function is from a different context +than the parent module. Linked modules must share the same context. + +<a id="ERR_VM_MODULE_LINKING_ERRORED"></a> +### ERR_VM_MODULE_LINKING_ERRORED + +The linker function returned a module for which linking has failed. + +<a id="ERR_VM_MODULE_NOT_LINKED"></a> +### ERR_VM_MODULE_NOT_LINKED + +The module must be successfully linked before instantiation. + +<a id="ERR_VM_MODULE_NOT_MODULE"></a> +### ERR_VM_MODULE_NOT_MODULE + +The fulfilled value of a linking promise is not a `vm.Module` object. + +<a id="ERR_VM_MODULE_STATUS"></a> +### ERR_VM_MODULE_STATUS + +The current module's status does not allow for this operation. The specific +meaning of the error depends on the specific function. + <a id="ERR_ZLIB_BINDING_CLOSED"></a> ### ERR_ZLIB_BINDING_CLOSED diff --git a/doc/api/vm.md b/doc/api/vm.md index a26ee4ed94..8fe17dfb50 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -43,6 +43,322 @@ console.log(x); // 1; y is not defined. *Note*: The vm module is not a security mechanism. **Do not use it to run untrusted code**. +## Class: vm.Module +<!-- YAML +added: REPLACEME +--> + +> Stability: 1 - Experimental + +*This feature is only available with the `--experimental-vm-modules` command +flag enabled.* + +The `vm.Module` class provides a low-level interface for using ECMAScript +modules in VM contexts. It is the counterpart of the `vm.Script` class that +closely mirrors [Source Text Module Record][]s as defined in the ECMAScript +specification. + +Unlike `vm.Script` however, every `vm.Module` object is bound to a context from +its creation. Operations on `vm.Module` objects are intrinsically asynchronous, +in contrast with the synchronous nature of `vm.Script` objects. With the help +of async functions, however, manipulating `vm.Module` objects is fairly +straightforward. + +Using a `vm.Module` object requires four distinct steps: creation/parsing, +linking, instantiation, and evaluation. These four steps are illustrated in the +following example. + +*Note*: This implementation lies at a lower level than the [ECMAScript Module +loader][]. There is also currently no way to interact with the Loader, though +support is planned. + +```js +const vm = require('vm'); + +const contextifiedSandbox = vm.createContext({ secret: 42 }); + +(async () => { + // Step 1 + // + // Create a Module by constructing a new `vm.Module` object. This parses the + // provided source text, throwing a `SyntaxError` if anything goes wrong. By + // default, a Module is created in the top context. But here, we specify + // `contextifiedSandbox` as the context this Module belongs to. + // + // Here, we attempt to obtain the default export from the module "foo", and + // put it into local binding "secret". + + const bar = new vm.Module(` + import s from 'foo'; + s; + `, { context: contextifiedSandbox }); + + + // Step 2 + // + // "Link" the imported dependencies of this Module to it. + // + // The provided linking callback (the "linker") accepts two arguments: the + // parent module (`bar` in this case) and the string that is the specifier of + // the imported module. The callback is expected to return a Module that + // corresponds to the provided specifier, with certain requirements documented + // in `module.link()`. + // + // If linking has not started for the returned Module, the same linker + // callback will be called on the returned Module. + // + // Even top-level Modules without dependencies must be explicitly linked. The + // callback provided would never be called, however. + // + // The link() method returns a Promise that will be resolved when all the + // Promises returned by the linker resolve. + // + // Note: This is a contrived example in that the linker function creates a new + // "foo" module every time it is called. In a full-fledged module system, a + // cache would probably be used to avoid duplicated modules. + + async function linker(referencingModule, specifier) { + if (specifier === 'foo') { + return new vm.Module(` + // The "secret" variable refers to the global variable we added to + // "contextifiedSandbox" when creating the context. + export default secret; + `, { context: referencingModule.context }); + + // Using `contextifiedSandbox` instead of `referencingModule.context` + // here would work as well. + } + throw new Error(`Unable to resolve dependency: ${specifier}`); + } + await bar.link(linker); + + + // Step 3 + // + // Instantiate the top-level Module. + // + // Only the top-level Module needs to be explicitly instantiated; its + // dependencies will be recursively instantiated by instantiate(). + + bar.instantiate(); + + + // Step 4 + // + // Evaluate the Module. The evaluate() method returns a Promise with a single + // property "result" that contains the result of the very last statement + // executed in the Module. In the case of `bar`, it is `s;`, which refers to + // the default export of the `foo` module, the `secret` we set in the + // beginning to 42. + + const { result } = await bar.evaluate(); + + console.log(result); + // Prints 42. +})(); +``` + +### Constructor: new vm.Module(code[, options]) + +* `code` {string} JavaScript Module code to parse +* `options` + * `url` {string} URL used in module resolution and stack traces. **Default**: + `'vm:module(i)'` where `i` is a context-specific ascending index. + * `context` {Object} The [contextified][] object as returned by the + `vm.createContext()` method, to compile and evaluate this Module in. + * `lineOffset` {integer} Specifies the line number offset that is displayed + in stack traces produced by this Module. + * `columnOffset` {integer} Spcifies the column number offset that is displayed + in stack traces produced by this Module. + +Creates a new ES `Module` object. + +### module.dependencySpecifiers + +* {string[]} + +The specifiers of all dependencies of this module. The returned array is frozen +to disallow any changes to it. + +Corresponds to the [[RequestedModules]] field of [Source Text Module Record][]s +in the ECMAScript specification. + +### module.error + +* {any} + +If the `module.status` is `'errored'`, this property contains the exception thrown +by the module during evaluation. If the status is anything else, accessing this +property will result in a thrown exception. + +*Note*: `undefined` cannot be used for cases where there is not a thrown +exception due to possible ambiguity with `throw undefined;`. + +Corresponds to the [[EvaluationError]] field of [Source Text Module Record][]s +in the ECMAScript specification. + +### module.linkingStatus + +* {string} + +The current linking status of `module`. It will be one of the following values: + +- `'unlinked'`: `module.link()` has not yet been called. +- `'linking'`: `module.link()` has been called, but not all Promises returned by + the linker function have been resolved yet. +- `'linked'`: `module.link()` has been called, and all its dependencies have + been successfully linked. +- `'errored'`: `module.link()` has been called, but at least one of its + dependencies failed to link, either because the callback returned a Promise + that is rejected, or because the Module the callback returned is invalid. + +### module.namespace + +* {Object} + +The namespace object of the module. This is only available after instantiation +(`module.instantiate()`) has completed. + +Corresponds to the [GetModuleNamespace][] abstract operation in the ECMAScript +specification. + +### module.status + +* {string} + +The current status of the module. Will be one of: + +- `'uninstantiated'`: The module is not instantiated. It may because of any of + the following reasons: + + - The module was just created. + - `module.instantiate()` has been called on this module, but it failed for + some reason. + + This status does not convey any information regarding if `module.link()` has + been called. See `module.linkingStatus` for that. + +- `'instantiating'`: The module is currently being instantiated through a + `module.instantiate()` call on itself or a parent module. + +- `'instantiated'`: The module has been instantiated successfully, but + `module.evaluate()` has not yet been called. + +- `'evaluating'`: The module is being evaluated through a `module.evaluate()` on + itself or a parent module. + +- `'evaluated'`: The module has been successfully evaluated. + +- `'errored'`: The module has been evaluated, but an exception was thrown. + +Other than `'errored'`, this status string corresponds to the specification's +[Source Text Module Record][]'s [[Status]] field. `'errored'` corresponds to +`'evaluated'` in the specification, but with [[EvaluationError]] set to a value +that is not `undefined`. + +### module.url + +* {string} + +The URL of the current module, as set in the constructor. + +### module.evaluate([options]) + +* `options` {Object} + * `timeout` {number} Specifies the number of milliseconds to evaluate + before terminating execution. If execution is interrupted, an [`Error`][] + will be thrown. + * `breakOnSigint` {boolean} If `true`, the execution will be terminated when + `SIGINT` (Ctrl+C) is received. Existing handlers for the event that have + been attached via `process.on("SIGINT")` will be disabled during script + execution, but will continue to work after that. If execution is + interrupted, an [`Error`][] will be thrown. +* Returns: {Promise} + +Evaluate the module. + +This must be called after the module has been instantiated; otherwise it will +throw an error. It could be called also when the module has already been +evaluated, in which case it will do one of the following two things: + +- return `undefined` if the initial evaluation ended in success (`module.status` + is `'evaluated'`) +- rethrow the same exception the initial evaluation threw if the initial + evaluation ended in an error (`module.status` is `'errored'`) + +This method cannot be called while the module is being evaluated +(`module.status` is `'evaluating'`) to prevent infinite recursion. + +Corresponds to the [Evaluate() concrete method][] field of [Source Text Module +Record][]s in the ECMAScript specification. + +### module.instantiate() + +Instantiate the module. This must be called after linking has completed +(`linkingStatus` is `'linked'`); otherwise it will throw an error. It may also +throw an exception if one of the dependencies does not provide an export the +parent module requires. + +However, if this function succeeded, further calls to this function after the +initial instantiation will be no-ops, to be consistent with the ECMAScript +specification. + +Unlike other methods operating on `Module`, this function completes +synchronously and returns nothing. + +Corresponds to the [Instantiate() concrete method][] field of [Source Text +Module Record][]s in the ECMAScript specification. + +### module.link(linker) + +* `linker` {Function} +* Returns: {Promise} + +Link module dependencies. This method must be called before instantiation, and +can only be called once per module. + +Two parameters will be passed to the `linker` function: + +- `referencingModule` The `Module` object `link()` is called on. +- `specifier` The specifier of the requested module: + + <!-- eslint-skip --> + ```js + import foo from 'foo'; + // ^^^^^ the module specifier + ``` + +The function is expected to return a `Module` object or a `Promise` that +eventually resolves to a `Module` object. The returned `Module` must satisfy the +following two invariants: + +- It must belong to the same context as the parent `Module`. +- Its `linkingStatus` must not be `'errored'`. + +If the returned `Module`'s `linkingStatus` is `'unlinked'`, this method will be +recursively called on the returned `Module` with the same provided `linker` +function. + +`link()` returns a `Promise` that will either get resolved when all linking +instances resolve to a valid `Module`, or rejected if the linker function either +throws an exception or returns an invalid `Module`. + +The linker function roughly corresponds to the implementation-defined +[HostResolveImportedModule][] abstract operation in the ECMAScript +specification, with a few key differences: + +- The linker function is allowed to be asynchronous while + [HostResolveImportedModule][] is synchronous. +- The linker function is executed during linking, a Node.js-specific stage + before instantiation, while [HostResolveImportedModule][] is called during + instantiation. + +The actual [HostResolveImportedModule][] implementation used during module +instantiation is one that returns the modules linked during linking. Since at +that point all modules would have been fully linked already, the +[HostResolveImportedModule][] implementation is fully synchronous per +specification. + ## Class: vm.Script <!-- YAML added: v0.3.1 @@ -548,9 +864,15 @@ associating it with the `sandbox` object is what this document refers to as [`vm.createContext()`]: #vm_vm_createcontext_sandbox_options [`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedsandbox_options [`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options +[GetModuleNamespace]: https://tc39.github.io/ecma262/#sec-getmodulenamespace +[ECMAScript Module Loader]: esm.html#esm_ecmascript_modules +[Evaluate() concrete method]: https://tc39.github.io/ecma262/#sec-moduleevaluation +[HostResolveImportedModule]: https://tc39.github.io/ecma262/#sec-hostresolveimportedmodule +[Instantiate() concrete method]: https://tc39.github.io/ecma262/#sec-moduledeclarationinstantiation [V8 Embedder's Guide]: https://github.com/v8/v8/wiki/Embedder's%20Guide#contexts [command line option]: cli.html [contextified]: #vm_what_does_it_mean_to_contextify_an_object [global object]: https://es5.github.io/#x15.1 [indirect `eval()` call]: https://es5.github.io/#x10.4.2 [origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin +[Source Text Module Record]: https://tc39.github.io/ecma262/#sec-source-text-module-records diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 70569fd843..2643d555cd 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -383,6 +383,15 @@ E('ERR_VALID_PERFORMANCE_ENTRY_TYPE', E('ERR_VALUE_OUT_OF_RANGE', (start, end, value) => { return `The value of "${start}" must be ${end}. Received "${value}"`; }); +E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked'); +E('ERR_VM_MODULE_DIFFERENT_CONTEXT', + 'Linked modules must use the same context'); +E('ERR_VM_MODULE_LINKING_ERRORED', + 'Linking has already failed for the provided module'); +E('ERR_VM_MODULE_NOT_LINKED', + 'Module must be linked before it can be instantiated'); +E('ERR_VM_MODULE_NOT_MODULE', 'Provided module is not an instance of Module'); +E('ERR_VM_MODULE_STATUS', 'Module status %s'); E('ERR_ZLIB_BINDING_CLOSED', 'zlib binding closed'); E('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed'); diff --git a/lib/internal/vm/Module.js b/lib/internal/vm/Module.js new file mode 100644 index 0000000000..a8625ef76b --- /dev/null +++ b/lib/internal/vm/Module.js @@ -0,0 +1,205 @@ +'use strict'; + +const { emitExperimentalWarning } = require('internal/util'); +const { URL } = require('internal/url'); +const { kParsingContext, isContext } = process.binding('contextify'); +const errors = require('internal/errors'); +const { + getConstructorOf, + customInspectSymbol, +} = require('internal/util'); + +const { + ModuleWrap, + kUninstantiated, + kInstantiating, + kInstantiated, + kEvaluating, + kEvaluated, + kErrored, +} = internalBinding('module_wrap'); + +const STATUS_MAP = { + [kUninstantiated]: 'uninstantiated', + [kInstantiating]: 'instantiating', + [kInstantiated]: 'instantiated', + [kEvaluating]: 'evaluating', + [kEvaluated]: 'evaluated', + [kErrored]: 'errored', +}; + +let globalModuleId = 0; +const perContextModuleId = new WeakMap(); +const wrapMap = new WeakMap(); +const dependencyCacheMap = new WeakMap(); +const linkingStatusMap = new WeakMap(); + +class Module { + constructor(src, options = {}) { + emitExperimentalWarning('vm.Module'); + + if (typeof src !== 'string') + throw new errors.TypeError( + 'ERR_INVALID_ARG_TYPE', 'src', 'string', src); + if (typeof options !== 'object' || options === null) + throw new errors.TypeError( + 'ERR_INVALID_ARG_TYPE', 'options', 'object', options); + + let context; + if (options.context !== undefined) { + if (isContext(options.context)) { + context = options.context; + } else { + throw new errors.TypeError( + 'ERR_INVALID_ARG_TYPE', 'options.context', 'vm.Context'); + } + } + + let url = options.url; + if (url !== undefined) { + if (typeof url !== 'string') { + throw new errors.TypeError( + 'ERR_INVALID_ARG_TYPE', 'options.url', 'string', url); + } + url = new URL(url).href; + } else if (context === undefined) { + url = `vm:module(${globalModuleId++})`; + } else if (perContextModuleId.has(context)) { + const curId = perContextModuleId.get(context); + url = `vm:module(${curId})`; + perContextModuleId.set(context, curId + 1); + } else { + url = 'vm:module(0)'; + perContextModuleId.set(context, 1); + } + + const wrap = new ModuleWrap(src, url, { + [kParsingContext]: context, + lineOffset: options.lineOffset, + columnOffset: options.columnOffset + }); + + wrapMap.set(this, wrap); + linkingStatusMap.set(this, 'unlinked'); + + Object.defineProperties(this, { + url: { value: url, enumerable: true }, + context: { value: context, enumerable: true }, + }); + } + + get linkingStatus() { + return linkingStatusMap.get(this); + } + + get status() { + return STATUS_MAP[wrapMap.get(this).getStatus()]; + } + + get namespace() { + const wrap = wrapMap.get(this); + if (wrap.getStatus() < kInstantiated) + throw new errors.Error('ERR_VM_MODULE_STATUS', + 'must not be uninstantiated or instantiating'); + return wrap.namespace(); + } + + get dependencySpecifiers() { + let deps = dependencyCacheMap.get(this); + if (deps !== undefined) + return deps; + + deps = wrapMap.get(this).getStaticDependencySpecifiers(); + Object.freeze(deps); + dependencyCacheMap.set(this, deps); + return deps; + } + + get error() { + const wrap = wrapMap.get(this); + if (wrap.getStatus() !== kErrored) + throw new errors.Error('ERR_VM_MODULE_STATUS', 'must be errored'); + return wrap.getError(); + } + + async link(linker) { + if (typeof linker !== 'function') + throw new errors.TypeError( + 'ERR_INVALID_ARG_TYPE', 'linker', 'function', linker); + if (linkingStatusMap.get(this) !== 'unlinked') + throw new errors.Error('ERR_VM_MODULE_ALREADY_LINKED'); + const wrap = wrapMap.get(this); + if (wrap.getStatus() !== kUninstantiated) + throw new errors.Error('ERR_VM_MODULE_STATUS', 'must be uninstantiated'); + linkingStatusMap.set(this, 'linking'); + const promises = []; + wrap.link((specifier) => { + const p = (async () => { + const m = await linker(this, specifier); + if (!m || !wrapMap.has(m)) + throw new errors.Error('ERR_VM_MODULE_NOT_MODULE'); + if (m.context !== this.context) + throw new errors.Error('ERR_VM_MODULE_DIFFERENT_CONTEXT'); + const childLinkingStatus = linkingStatusMap.get(m); + if (childLinkingStatus === 'errored') + throw new errors.Error('ERR_VM_MODULE_LINKING_ERRORED'); + if (childLinkingStatus === 'unlinked') + await m.link(linker); + return wrapMap.get(m); + })(); + promises.push(p); + return p; + }); + try { + await Promise.all(promises); + linkingStatusMap.set(this, 'linked'); + } catch (err) { + linkingStatusMap.set(this, 'errored'); + throw err; + } + } + + instantiate() { + const wrap = wrapMap.get(this); + const status = wrap.getStatus(); + if (status === kInstantiating || status === kEvaluating) + throw new errors.Error( + 'ERR_VM_MODULE_STATUS', 'must not be instantiating or evaluating'); + if (linkingStatusMap.get(this) !== 'linked') + throw new errors.Error('ERR_VM_MODULE_NOT_LINKED'); + wrap.instantiate(); + } + + async evaluate(options) { + const wrap = wrapMap.get(this); + const status = wrap.getStatus(); + if (status !== kInstantiated && + status !== kEvaluated && + status !== kErrored) { + throw new errors.Error( + 'ERR_VM_MODULE_STATUS', + 'must be one of instantiated, evaluated, and errored'); + } + const result = wrap.evaluate(options); + return { result, __proto__: null }; + } + + [customInspectSymbol](depth, options) { + let ctor = getConstructorOf(this); + ctor = ctor === null ? Module : ctor; + + if (typeof depth === 'number' && depth < 0) + return options.stylize(`[${ctor.name}]`, 'special'); + + const o = Object.create({ constructor: ctor }); + o.status = this.status; + o.linkingStatus = this.linkingStatus; + o.url = this.url; + o.context = this.context; + return require('util').inspect(o, options); + } +} + +module.exports = { + Module +}; @@ -207,5 +207,8 @@ module.exports = { runInContext, runInNewContext, runInThisContext, - isContext + isContext, }; + +if (process.binding('config').experimentalVMModules) + module.exports.Module = require('internal/vm/Module').Module; @@ -137,6 +137,7 @@ 'lib/internal/http2/util.js', 'lib/internal/v8_prof_polyfill.js', 'lib/internal/v8_prof_processor.js', + 'lib/internal/vm/Module.js', 'lib/internal/streams/lazy_transform.js', 'lib/internal/streams/BufferList.js', 'lib/internal/streams/legacy.js', diff --git a/src/module_wrap.cc b/src/module_wrap.cc index eddc0d6d6e..daa7f9036a 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -7,6 +7,8 @@ #include "node_url.h" #include "util-inl.h" #include "node_internals.h" +#include "node_contextify.h" +#include "node_watchdog.h" namespace node { namespace loader { @@ -59,6 +61,7 @@ ModuleWrap::~ModuleWrap() { } module_.Reset(); + context_.Reset(); } void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { @@ -71,12 +74,6 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { return; } - if (args.Length() != 2) { - env->ThrowError("constructor must have exactly 2 arguments " - "(string, string)"); - return; - } - if (!args[0]->IsString()) { env->ThrowError("first argument is not a string"); return; @@ -91,20 +88,39 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { Local<String> url = args[1].As<String>(); + Local<Object> that = args.This(); + + Environment::ShouldNotAbortOnUncaughtScope no_abort_scope(env); + TryCatch try_catch(isolate); + + Local<Value> options = args[2]; + MaybeLocal<Integer> line_offset = contextify::GetLineOffsetArg(env, options); + MaybeLocal<Integer> column_offset = + contextify::GetColumnOffsetArg(env, options); + MaybeLocal<Context> maybe_context = contextify::GetContextArg(env, options); + + + if (try_catch.HasCaught()) { + no_abort_scope.Close(); + try_catch.ReThrow(); + return; + } + + Local<Context> context = maybe_context.FromMaybe(that->CreationContext()); Local<Module> module; // compile { ScriptOrigin origin(url, - Integer::New(isolate, 0), // line offset - Integer::New(isolate, 0), // column offset + line_offset.ToLocalChecked(), // line offset + column_offset.ToLocalChecked(), // column offset False(isolate), // is cross origin Local<Integer>(), // script id Local<Value>(), // source map URL False(isolate), // is opaque (?) False(isolate), // is WASM True(isolate)); // is ES6 module - TryCatch try_catch(isolate); + Context::Scope context_scope(context); ScriptCompiler::Source source(source_text, origin); if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) { CHECK(try_catch.HasCaught()); @@ -117,8 +133,6 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { } } - Local<Object> that = args.This(); - Local<Context> context = that->CreationContext(); Local<String> url_str = FIXED_ONE_BYTE_STRING(isolate, "url"); if (!that->Set(context, url_str, url).FromMaybe(false)) { @@ -126,6 +140,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { } ModuleWrap* obj = new ModuleWrap(env, that, module, url); + obj->context_.Reset(isolate, context); env->module_map.emplace(module->GetIdentityHash(), obj); Wrap(that, obj); @@ -142,15 +157,19 @@ void ModuleWrap::Link(const FunctionCallbackInfo<Value>& args) { return; } - Local<Function> resolver_arg = args[0].As<Function>(); - Local<Object> that = args.This(); - ModuleWrap* obj = Unwrap<ModuleWrap>(that); - CHECK_NE(obj, nullptr); - Local<Context> mod_context = that->CreationContext(); - if (obj->linked_) return; + + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, that); + + if (obj->linked_) + return; obj->linked_ = true; - Local<Module> module(obj->module_.Get(isolate)); + + Local<Function> resolver_arg = args[0].As<Function>(); + + Local<Context> mod_context = obj->context_.Get(isolate); + Local<Module> module = obj->module_.Get(isolate); Local<Array> promises = Array::New(isolate, module->GetModuleRequestsLength()); @@ -187,11 +206,9 @@ void ModuleWrap::Link(const FunctionCallbackInfo<Value>& args) { void ModuleWrap::Instantiate(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = args.GetIsolate(); - Local<Object> that = args.This(); - Local<Context> context = that->CreationContext(); - - ModuleWrap* obj = Unwrap<ModuleWrap>(that); - CHECK_NE(obj, nullptr); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + Local<Context> context = obj->context_.Get(isolate); Local<Module> module = obj->module_.Get(isolate); TryCatch try_catch(isolate); Maybe<bool> ok = @@ -214,14 +231,60 @@ void ModuleWrap::Instantiate(const FunctionCallbackInfo<Value>& args) { } void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); Isolate* isolate = args.GetIsolate(); - Local<Object> that = args.This(); - Local<Context> context = that->CreationContext(); - ModuleWrap* obj = Unwrap<ModuleWrap>(that); - CHECK_NE(obj, nullptr); - MaybeLocal<Value> result = obj->module_.Get(isolate)->Evaluate(context); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + Local<Context> context = obj->context_.Get(isolate); + Local<Module> module = obj->module_.Get(isolate); - if (result.IsEmpty()) { + Environment::ShouldNotAbortOnUncaughtScope no_abort_scope(env); + TryCatch try_catch(isolate); + Maybe<int64_t> maybe_timeout = + contextify::GetTimeoutArg(env, args[0]); + Maybe<bool> maybe_break_on_sigint = + contextify::GetBreakOnSigintArg(env, args[0]); + + if (try_catch.HasCaught()) { + no_abort_scope.Close(); + try_catch.ReThrow(); + return; + } + + int64_t timeout = maybe_timeout.ToChecked(); + bool break_on_sigint = maybe_break_on_sigint.ToChecked(); + + bool timed_out = false; + bool received_signal = false; + MaybeLocal<Value> result; + if (break_on_sigint && timeout != -1) { + Watchdog wd(isolate, timeout, &timed_out); + SigintWatchdog swd(isolate, &received_signal); + result = module->Evaluate(context); + } else if (break_on_sigint) { + SigintWatchdog swd(isolate, &received_signal); + result = module->Evaluate(context); + } else if (timeout != -1) { + Watchdog wd(isolate, timeout, &timed_out); + result = module->Evaluate(context); + } else { + result = module->Evaluate(context); + } + + if (timed_out || received_signal) { + // It is possible that execution was terminated by another timeout in + // which this timeout is nested, so check whether one of the watchdogs + // from this invocation is responsible for termination. + if (timed_out) { + env->ThrowError("Script execution timed out."); + } else if (received_signal) { + env->ThrowError("Script execution interrupted."); + } + env->isolate()->CancelTerminateExecution(); + } + + if (try_catch.HasCaught()) { + try_catch.ReThrow(); return; } @@ -231,9 +294,8 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) { void ModuleWrap::Namespace(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = args.GetIsolate(); - Local<Object> that = args.This(); - ModuleWrap* obj = Unwrap<ModuleWrap>(that); - CHECK_NE(obj, nullptr); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); Local<Module> module = obj->module_.Get(isolate); @@ -251,6 +313,44 @@ void ModuleWrap::Namespace(const FunctionCallbackInfo<Value>& args) { args.GetReturnValue().Set(result); } +void ModuleWrap::GetStatus(const FunctionCallbackInfo<Value>& args) { + Isolate* isolate = args.GetIsolate(); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + + Local<Module> module = obj->module_.Get(isolate); + + args.GetReturnValue().Set(module->GetStatus()); +} + +void ModuleWrap::GetStaticDependencySpecifiers( + const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + + Local<Module> module = obj->module_.Get(env->isolate()); + + int count = module->GetModuleRequestsLength(); + + Local<Array> specifiers = Array::New(env->isolate(), count); + + for (int i = 0; i < count; i++) + specifiers->Set(env->context(), i, module->GetModuleRequest(i)).FromJust(); + + args.GetReturnValue().Set(specifiers); +} + +void ModuleWrap::GetError(const FunctionCallbackInfo<Value>& args) { + Isolate* isolate = args.GetIsolate(); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + + Local<Module> module = obj->module_.Get(isolate); + + args.GetReturnValue().Set(module->GetException()); +} + MaybeLocal<Module> ModuleWrap::ResolveCallback(Local<Context> context, Local<String> specifier, Local<Module> referrer) { @@ -642,12 +742,29 @@ void ModuleWrap::Initialize(Local<Object> target, env->SetProtoMethod(tpl, "instantiate", Instantiate); env->SetProtoMethod(tpl, "evaluate", Evaluate); env->SetProtoMethod(tpl, "namespace", Namespace); + env->SetProtoMethod(tpl, "getStatus", GetStatus); + env->SetProtoMethod(tpl, "getError", GetError); + env->SetProtoMethod(tpl, "getStaticDependencySpecifiers", + GetStaticDependencySpecifiers); target->Set(FIXED_ONE_BYTE_STRING(isolate, "ModuleWrap"), tpl->GetFunction()); env->SetMethod(target, "resolve", node::loader::ModuleWrap::Resolve); env->SetMethod(target, "setImportModuleDynamicallyCallback", node::loader::ModuleWrap::SetImportModuleDynamicallyCallback); + +#define V(name) \ + target->Set(context, \ + FIXED_ONE_BYTE_STRING(env->isolate(), #name), \ + Integer::New(env->isolate(), Module::Status::name)) \ + .FromJust() + V(kUninstantiated); + V(kInstantiating); + V(kInstantiated); + V(kEvaluating); + V(kEvaluated); + V(kErrored); +#undef V } } // namespace loader diff --git a/src/module_wrap.h b/src/module_wrap.h index ec4d6bf577..bedf665165 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -36,8 +36,11 @@ class ModuleWrap : public BaseObject { static void Instantiate(const v8::FunctionCallbackInfo<v8::Value>& args); static void Evaluate(const v8::FunctionCallbackInfo<v8::Value>& args); static void Namespace(const v8::FunctionCallbackInfo<v8::Value>& args); - static void GetUrl(v8::Local<v8::String> property, - const v8::PropertyCallbackInfo<v8::Value>& info); + static void GetStatus(const v8::FunctionCallbackInfo<v8::Value>& args); + static void GetError(const v8::FunctionCallbackInfo<v8::Value>& args); + static void GetStaticDependencySpecifiers( + const v8::FunctionCallbackInfo<v8::Value>& args); + static void Resolve(const v8::FunctionCallbackInfo<v8::Value>& args); static void SetImportModuleDynamicallyCallback( const v8::FunctionCallbackInfo<v8::Value>& args); @@ -50,6 +53,7 @@ class ModuleWrap : public BaseObject { v8::Persistent<v8::String> url_; bool linked_ = false; std::unordered_map<std::string, v8::Persistent<v8::Promise>> resolve_cache_; + v8::Persistent<v8::Context> context_; }; } // namespace loader diff --git a/src/node.cc b/src/node.cc index 39f441b1ea..0a466d20c9 100644 --- a/src/node.cc +++ b/src/node.cc @@ -241,6 +241,11 @@ bool config_preserve_symlinks = false; // that is used by lib/module.js bool config_experimental_modules = false; +// Set in node.cc by ParseArgs when --experimental-vm-modules is used. +// Used in node_config.cc to set a constant on process.binding('config') +// that is used by lib/vm.js +bool config_experimental_vm_modules = false; + // Set in node.cc by ParseArgs when --loader is used. // Used in node_config.cc to set a constant on process.binding('config') // that is used by lib/internal/bootstrap_node.js @@ -3775,6 +3780,8 @@ static void PrintHelp() { " --preserve-symlinks preserve symbolic links when resolving\n" " --experimental-modules experimental ES Module support\n" " and caching modules\n" + " --experimental-vm-modules experimental ES Module support\n" + " in vm module\n" #endif "\n" "Environment variables:\n" @@ -3854,6 +3861,7 @@ static void CheckIfAllowedInEnv(const char* exe, bool is_env, "--napi-modules", "--expose-http2", // keep as a non-op through v9.x "--experimental-modules", + "--experimental-vm-modules", "--loader", "--trace-warnings", "--redirect-warnings", @@ -4025,7 +4033,9 @@ static void ParseArgs(int* argc, config_experimental_modules = true; new_v8_argv[new_v8_argc] = "--harmony-dynamic-import"; new_v8_argc += 1; - } else if (strcmp(arg, "--loader") == 0) { + } else if (strcmp(arg, "--experimental-vm-modules") == 0) { + config_experimental_vm_modules = true; + } else if (strcmp(arg, "--loader") == 0) { const char* module = argv[index + 1]; if (!config_experimental_modules) { fprintf(stderr, "%s: %s requires --experimental-modules be enabled\n", diff --git a/src/node_config.cc b/src/node_config.cc index 2e9ad2ed13..cac551ad2c 100644 --- a/src/node_config.cc +++ b/src/node_config.cc @@ -82,6 +82,9 @@ static void InitConfig(Local<Object> target, } } + if (config_experimental_vm_modules) + READONLY_BOOLEAN_PROPERTY("experimentalVMModules"); + if (config_pending_deprecation) READONLY_BOOLEAN_PROPERTY("pendingDeprecation"); diff --git a/src/node_internals.h b/src/node_internals.h index 469d872336..ced92da321 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -172,6 +172,11 @@ extern bool config_preserve_symlinks; // that is used by lib/module.js extern bool config_experimental_modules; +// Set in node.cc by ParseArgs when --experimental-vm-modules is used. +// Used in node_config.cc to set a constant on process.binding('config') +// that is used by lib/vm.js +extern bool config_experimental_vm_modules; + // Set in node.cc by ParseArgs when --loader is used. // Used in node_config.cc to set a constant on process.binding('config') // that is used by lib/internal/bootstrap_node.js diff --git a/test/parallel/test-vm-module-basic.js b/test/parallel/test-vm-module-basic.js new file mode 100644 index 0000000000..4bbe0a95ee --- /dev/null +++ b/test/parallel/test-vm-module-basic.js @@ -0,0 +1,54 @@ +'use strict'; + +// Flags: --experimental-vm-modules + +const common = require('../common'); +const assert = require('assert'); +const { Module, createContext } = require('vm'); + +common.crashOnUnhandledRejection(); + +(async function test1() { + const context = createContext({ + foo: 'bar', + baz: undefined, + typeofProcess: undefined, + }); + const m = new Module( + 'baz = foo; typeofProcess = typeof process; typeof Object;', + { context } + ); + assert.strictEqual(m.status, 'uninstantiated'); + await m.link(common.mustNotCall()); + m.instantiate(); + assert.strictEqual(m.status, 'instantiated'); + const result = await m.evaluate(); + assert.strictEqual(m.status, 'evaluated'); + assert.strictEqual(Object.getPrototypeOf(result), null); + assert.deepStrictEqual(context, { + foo: 'bar', + baz: 'bar', + typeofProcess: 'undefined' + }); + assert.strictEqual(result.result, 'function'); +}()); + +(async () => { + const m = new Module( + 'global.vmResult = "foo"; Object.prototype.toString.call(process);' + ); + await m.link(common.mustNotCall()); + m.instantiate(); + const { result } = await m.evaluate(); + assert.strictEqual(global.vmResult, 'foo'); + assert.strictEqual(result, '[object process]'); + delete global.vmResult; +})(); + +(async () => { + const m = new Module('while (true) {}'); + await m.link(common.mustNotCall()); + m.instantiate(); + await m.evaluate({ timeout: 500 }) + .then(() => assert(false), () => {}); +})(); diff --git a/test/parallel/test-vm-module-dynamic-import.js b/test/parallel/test-vm-module-dynamic-import.js new file mode 100644 index 0000000000..ca4dceb5de --- /dev/null +++ b/test/parallel/test-vm-module-dynamic-import.js @@ -0,0 +1,27 @@ +'use strict'; + +// Flags: --experimental-vm-modules --experimental-modules --harmony-dynamic-import + +const common = require('../common'); +common.crashOnUnhandledRejection(); + +const assert = require('assert'); +const { Module, createContext } = require('vm'); + +const finished = common.mustCall(); + +(async function() { + const m = new Module('import("foo")', { context: createContext() }); + await m.link(common.mustNotCall()); + m.instantiate(); + const { result } = await m.evaluate(); + let threw = false; + try { + await result; + } catch (err) { + threw = true; + assert.strictEqual(err.message, 'import() called outside of main context'); + } + assert(threw); + finished(); +}()); diff --git a/test/parallel/test-vm-module-errors.js b/test/parallel/test-vm-module-errors.js new file mode 100644 index 0000000000..8bcb101ccc --- /dev/null +++ b/test/parallel/test-vm-module-errors.js @@ -0,0 +1,264 @@ +'use strict'; + +// Flags: --experimental-vm-modules + +const common = require('../common'); +common.crashOnUnhandledRejection(); + +const assert = require('assert'); + +const { Module, createContext } = require('vm'); + +async function expectsRejection(fn, settings) { + const validateError = common.expectsError(settings); + // Retain async context. + const storedError = new Error('Thrown from:'); + try { + await fn(); + } catch (err) { + try { + validateError(err); + } catch (validationError) { + console.error(validationError); + console.error('Original error:'); + console.error(err); + throw storedError; + } + return; + } + assert.fail('Missing expected exception'); +} + +async function createEmptyLinkedModule() { + const m = new Module(''); + await m.link(common.mustNotCall()); + return m; +} + +async function checkArgType() { + common.expectsError(() => { + new Module(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError + }); + + for (const invalidOptions of [ + 0, 1, null, true, 'str', () => {}, Symbol.iterator + ]) { + common.expectsError(() => { + new Module('', invalidOptions); + }, { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError + }); + } + + for (const invalidLinker of [ + 0, 1, undefined, null, true, 'str', {}, Symbol.iterator + ]) { + await expectsRejection(async () => { + const m = new Module(''); + await m.link(invalidLinker); + }, { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError + }); + } +} + +// Check methods/properties can only be used under a specific state. +async function checkModuleState() { + await expectsRejection(async () => { + const m = new Module(''); + await m.link(common.mustNotCall()); + assert.strictEqual(m.linkingStatus, 'linked'); + await m.link(common.mustNotCall()); + }, { + code: 'ERR_VM_MODULE_ALREADY_LINKED' + }); + + await expectsRejection(async () => { + const m = new Module(''); + m.link(common.mustNotCall()); + assert.strictEqual(m.linkingStatus, 'linking'); + await m.link(common.mustNotCall()); + }, { + code: 'ERR_VM_MODULE_ALREADY_LINKED' + }); + + common.expectsError(() => { + const m = new Module(''); + m.instantiate(); + }, { + code: 'ERR_VM_MODULE_NOT_LINKED' + }); + + await expectsRejection(async () => { + const m = new Module('import "foo";'); + try { + await m.link(common.mustCall(() => ({}))); + } catch (err) { + assert.strictEqual(m.linkingStatus, 'errored'); + m.instantiate(); + } + assert.fail('Unreachable'); + }, { + code: 'ERR_VM_MODULE_NOT_LINKED' + }); + + { + const m = new Module('import "foo";'); + await m.link(common.mustCall(async (module, specifier) => { + assert.strictEqual(module, m); + assert.strictEqual(specifier, 'foo'); + assert.strictEqual(m.linkingStatus, 'linking'); + common.expectsError(() => { + m.instantiate(); + }, { + code: 'ERR_VM_MODULE_NOT_LINKED' + }); + return new Module(''); + })); + m.instantiate(); + await m.evaluate(); + } + + await expectsRejection(async () => { + const m = new Module(''); + await m.evaluate(); + }, { + code: 'ERR_VM_MODULE_STATUS', + message: 'Module status must be one of instantiated, evaluated, and errored' + }); + + await expectsRejection(async () => { + const m = await createEmptyLinkedModule(); + await m.evaluate(); + }, { + code: 'ERR_VM_MODULE_STATUS', + message: 'Module status must be one of instantiated, evaluated, and errored' + }); + + common.expectsError(() => { + const m = new Module(''); + m.error; + }, { + code: 'ERR_VM_MODULE_STATUS', + message: 'Module status must be errored' + }); + + await expectsRejection(async () => { + const m = await createEmptyLinkedModule(); + m.instantiate(); + await m.evaluate(); + m.error; + }, { + code: 'ERR_VM_MODULE_STATUS', + message: 'Module status must be errored' + }); + + common.expectsError(() => { + const m = new Module(''); + m.namespace; + }, { + code: 'ERR_VM_MODULE_STATUS', + message: 'Module status must not be uninstantiated or instantiating' + }); + + await expectsRejection(async () => { + const m = await createEmptyLinkedModule(); + m.namespace; + }, { + code: 'ERR_VM_MODULE_STATUS', + message: 'Module status must not be uninstantiated or instantiating' + }); +} + +// Check link() fails when the returned module is not valid. +async function checkLinking() { + await expectsRejection(async () => { + const m = new Module('import "foo";'); + try { + await m.link(common.mustCall(() => ({}))); + } catch (err) { + assert.strictEqual(m.linkingStatus, 'errored'); + throw err; + } + assert.fail('Unreachable'); + }, { + code: 'ERR_VM_MODULE_NOT_MODULE' + }); + + await expectsRejection(async () => { + const c = createContext({ a: 1 }); + const foo = new Module('', { context: c }); + await foo.link(common.mustNotCall()); + const bar = new Module('import "foo";'); + try { + await bar.link(common.mustCall(() => foo)); + } catch (err) { + assert.strictEqual(bar.linkingStatus, 'errored'); + throw err; + } + assert.fail('Unreachable'); + }, { + code: 'ERR_VM_MODULE_DIFFERENT_CONTEXT' + }); + + await expectsRejection(async () => { + const erroredModule = new Module('import "foo";'); + try { + await erroredModule.link(common.mustCall(() => ({}))); + } catch (err) { + // ignored + } finally { + assert.strictEqual(erroredModule.linkingStatus, 'errored'); + } + + const rootModule = new Module('import "errored";'); + await rootModule.link(common.mustCall(() => erroredModule)); + }, { + code: 'ERR_VM_MODULE_LINKING_ERRORED' + }); +} + +// Check the JavaScript engine deals with exceptions correctly +async function checkExecution() { + await (async () => { + const m = new Module('import { nonexistent } from "module";'); + await m.link(common.mustCall(() => new Module(''))); + + // There is no code for this exception since it is thrown by the JavaScript + // engine. + assert.throws(() => { + m.instantiate(); + }, SyntaxError); + })(); + + await (async () => { + const m = new Module('throw new Error();'); + await m.link(common.mustNotCall()); + m.instantiate(); + const evaluatePromise = m.evaluate(); + await evaluatePromise.catch(() => {}); + assert.strictEqual(m.status, 'errored'); + try { + await evaluatePromise; + } catch (err) { + assert.strictEqual(m.error, err); + return; + } + assert.fail('Missing expected exception'); + })(); +} + +const finished = common.mustCall(); + +(async function main() { + await checkArgType(); + await checkModuleState(); + await checkLinking(); + await checkExecution(); + finished(); +})(); diff --git a/test/parallel/test-vm-module-link.js b/test/parallel/test-vm-module-link.js new file mode 100644 index 0000000000..870427e91b --- /dev/null +++ b/test/parallel/test-vm-module-link.js @@ -0,0 +1,135 @@ +'use strict'; + +// Flags: --experimental-vm-modules + +const common = require('../common'); +common.crashOnUnhandledRejection(); + +const assert = require('assert'); +const { URL } = require('url'); + +const { Module } = require('vm'); + +async function simple() { + const foo = new Module('export default 5;'); + await foo.link(common.mustNotCall()); + + const bar = new Module('import five from "foo"; five'); + + assert.deepStrictEqual(bar.dependencySpecifiers, ['foo']); + + await bar.link(common.mustCall((module, specifier) => { + assert.strictEqual(module, bar); + assert.strictEqual(specifier, 'foo'); + return foo; + })); + + bar.instantiate(); + + assert.strictEqual((await bar.evaluate()).result, 5); +} + +async function depth() { + const foo = new Module('export default 5'); + await foo.link(common.mustNotCall()); + + async function getProxy(parentName, parentModule) { + const mod = new Module(` + import ${parentName} from '${parentName}'; + export default ${parentName}; + `); + await mod.link(common.mustCall((module, specifier) => { + assert.strictEqual(module, mod); + assert.strictEqual(specifier, parentName); + return parentModule; + })); + return mod; + } + + const bar = await getProxy('foo', foo); + const baz = await getProxy('bar', bar); + const barz = await getProxy('baz', baz); + + barz.instantiate(); + await barz.evaluate(); + + assert.strictEqual(barz.namespace.default, 5); +} + +async function circular() { + const foo = new Module(` + import getFoo from 'bar'; + export let foo = 42; + export default getFoo(); + `); + const bar = new Module(` + import { foo } from 'foo'; + export default function getFoo() { + return foo; + } + `); + await foo.link(common.mustCall(async (fooModule, fooSpecifier) => { + assert.strictEqual(fooModule, foo); + assert.strictEqual(fooSpecifier, 'bar'); + await bar.link(common.mustCall((barModule, barSpecifier) => { + assert.strictEqual(barModule, bar); + assert.strictEqual(barSpecifier, 'foo'); + assert.strictEqual(foo.linkingStatus, 'linking'); + return foo; + })); + assert.strictEqual(bar.linkingStatus, 'linked'); + return bar; + })); + + foo.instantiate(); + await foo.evaluate(); + assert.strictEqual(foo.namespace.default, 42); +} + +async function circular2() { + const sourceMap = { + root: ` + import * as a from './a.mjs'; + import * as b from './b.mjs'; + if (!('fromA' in a)) + throw new Error(); + if (!('fromB' in a)) + throw new Error(); + if (!('fromA' in b)) + throw new Error(); + if (!('fromB' in b)) + throw new Error(); + `, + './a.mjs': ` + export * from './b.mjs'; + export var fromA; + `, + './b.mjs': ` + export * from './a.mjs'; + export var fromB; + ` + }; + const moduleMap = new Map(); + const rootModule = new Module(sourceMap.root, { url: 'vm:root' }); + async function link(referencingModule, specifier) { + if (moduleMap.has(specifier)) { + return moduleMap.get(specifier); + } + const mod = new Module(sourceMap[specifier], { url: new URL(specifier, 'file:///').href }); + moduleMap.set(specifier, mod); + return mod; + } + await rootModule.link(link); + rootModule.instantiate(); + await rootModule.evaluate(); +} + +const finished = common.mustCall(); + +(async function main() { + await simple(); + await depth(); + await circular(); + await circular2(); + finished(); +})(); diff --git a/test/parallel/test-vm-module-reevaluate.js b/test/parallel/test-vm-module-reevaluate.js new file mode 100644 index 0000000000..e4f5858800 --- /dev/null +++ b/test/parallel/test-vm-module-reevaluate.js @@ -0,0 +1,49 @@ +'use strict'; + +// Flags: --experimental-vm-modules + +const common = require('../common'); +common.crashOnUnhandledRejection(); + +const assert = require('assert'); + +const { Module } = require('vm'); + +const finished = common.mustCall(); + +(async function main() { + { + const m = new Module('1'); + await m.link(common.mustNotCall()); + m.instantiate(); + assert.strictEqual((await m.evaluate()).result, 1); + assert.strictEqual((await m.evaluate()).result, undefined); + assert.strictEqual((await m.evaluate()).result, undefined); + } + + { + const m = new Module('throw new Error()'); + await m.link(common.mustNotCall()); + m.instantiate(); + + let threw = false; + try { + await m.evaluate(); + } catch (err) { + assert(err instanceof Error); + threw = true; + } + assert(threw); + + threw = false; + try { + await m.evaluate(); + } catch (err) { + assert(err instanceof Error); + threw = true; + } + assert(threw); + } + + finished(); +})(); |