summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/errors.md37
-rw-r--r--doc/api/vm.md322
-rw-r--r--lib/internal/errors.js9
-rw-r--r--lib/internal/vm/Module.js205
-rw-r--r--lib/vm.js5
-rw-r--r--node.gyp1
-rw-r--r--src/module_wrap.cc182
-rw-r--r--src/module_wrap.h8
-rw-r--r--src/node.cc10
-rw-r--r--src/node_config.cc3
-rw-r--r--src/node_internals.h5
-rw-r--r--test/parallel/test-vm-module-basic.js54
-rw-r--r--test/parallel/test-vm-module-dynamic-import.js27
-rw-r--r--test/parallel/test-vm-module-errors.js264
-rw-r--r--test/parallel/test-vm-module-link.js135
-rw-r--r--test/parallel/test-vm-module-reevaluate.js49
16 files changed, 1281 insertions, 35 deletions
diff --git a/doc/api/errors.md b/doc/api/errors.md
index 0e28b9b91d..d68640d82c 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1639,6 +1639,43 @@ entry types were found.
Superseded by `ERR_OUT_OF_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 3ac70091e5..516e18b6a0 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
@@ -518,8 +834,14 @@ 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
[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 6a52d55bb8..3530d63710 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -676,6 +676,15 @@ E('ERR_V8BREAKITERATOR', 'Full ICU data not installed. ' +
'See https://github.com/nodejs/node/wiki/Intl');
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
'At least one valid performance entry type is required');
+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
+};
diff --git a/lib/vm.js b/lib/vm.js
index ec05ad774a..c9bb44f057 100644
--- a/lib/vm.js
+++ b/lib/vm.js
@@ -192,5 +192,8 @@ module.exports = {
runInContext,
runInNewContext,
runInThisContext,
- isContext
+ isContext,
};
+
+if (process.binding('config').experimentalVMModules)
+ module.exports.Module = require('internal/vm/Module').Module;
diff --git a/node.gyp b/node.gyp
index 95b32053a6..d8546a1373 100644
--- a/node.gyp
+++ b/node.gyp
@@ -138,6 +138,7 @@
'lib/internal/v8.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/async_iterator.js',
'lib/internal/streams/BufferList.js',
diff --git a/src/module_wrap.cc b/src/module_wrap.cc
index b8970d4fb3..ba07fcdc79 100644
--- a/src/module_wrap.cc
+++ b/src/module_wrap.cc
@@ -7,12 +7,15 @@
#include "node_url.h"
#include "util-inl.h"
#include "node_internals.h"
+#include "node_contextify.h"
+#include "node_watchdog.h"
namespace node {
namespace loader {
using node::url::URL;
using node::url::URL_FLAGS_FAILED;
+using v8::Array;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
@@ -58,6 +61,7 @@ ModuleWrap::~ModuleWrap() {
}
module_.Reset();
+ context_.Reset();
}
void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
@@ -70,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;
@@ -90,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());
@@ -116,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)) {
@@ -125,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);
@@ -141,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);
// call the dependency resolve callbacks
for (int i = 0; i < module->GetModuleRequestsLength(); i++) {
@@ -181,11 +201,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 =
@@ -208,14 +226,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;
}
@@ -225,9 +289,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);
@@ -245,6 +308,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) {
@@ -636,12 +737,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 2e1d08f5b5..c82bd73348 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -240,6 +240,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
@@ -3424,6 +3429,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"
@@ -3503,6 +3510,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",
@@ -3670,6 +3678,8 @@ static void ParseArgs(int* argc,
config_preserve_symlinks = true;
} else if (strcmp(arg, "--experimental-modules") == 0) {
config_experimental_modules = true;
+ } 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) {
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 0001d15172..b3e1f5cd9f 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();
+})();