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>
This commit is contained in:
parent
2033a9f436
commit
0993fbe5b2
@ -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
|
||||
|
||||
|
322
doc/api/vm.md
322
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
|
||||
|
@ -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');
|
||||
|
||||
|
205
lib/internal/vm/Module.js
Normal file
205
lib/internal/vm/Module.js
Normal file
@ -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
|
||||
};
|
@ -192,5 +192,8 @@ module.exports = {
|
||||
runInContext,
|
||||
runInNewContext,
|
||||
runInThisContext,
|
||||
isContext
|
||||
isContext,
|
||||
};
|
||||
|
||||
if (process.binding('config').experimentalVMModules)
|
||||
module.exports.Module = require('internal/vm/Module').Module;
|
||||
|
1
node.gyp
1
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',
|
||||
|
@ -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<Object> that = args.This();
|
||||
|
||||
ModuleWrap* obj;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&obj, that);
|
||||
|
||||
if (obj->linked_)
|
||||
return;
|
||||
obj->linked_ = true;
|
||||
|
||||
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;
|
||||
obj->linked_ = true;
|
||||
Local<Module> module(obj->module_.Get(isolate));
|
||||
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
|
||||
|
@ -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
|
||||
|
10
src/node.cc
10
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) {
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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
|
||||
|
54
test/parallel/test-vm-module-basic.js
Normal file
54
test/parallel/test-vm-module-basic.js
Normal file
@ -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), () => {});
|
||||
})();
|
27
test/parallel/test-vm-module-dynamic-import.js
Normal file
27
test/parallel/test-vm-module-dynamic-import.js
Normal file
@ -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();
|
||||
}());
|
264
test/parallel/test-vm-module-errors.js
Normal file
264
test/parallel/test-vm-module-errors.js
Normal file
@ -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();
|
||||
})();
|
135
test/parallel/test-vm-module-link.js
Normal file
135
test/parallel/test-vm-module-link.js
Normal file
@ -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();
|
||||
})();
|
49
test/parallel/test-vm-module-reevaluate.js
Normal file
49
test/parallel/test-vm-module-reevaluate.js
Normal file
@ -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();
|
||||
})();
|
Loading…
x
Reference in New Issue
Block a user