Skip to content

Commit 0993fbe

Browse files
devsnekTimothyGu
andcommitted
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 <[email protected]> PR-URL: #17560 Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Tiancheng "Timothy" Gu <[email protected]>
1 parent 2033a9f commit 0993fbe

16 files changed

+1281
-35
lines changed

doc/api/errors.md

+37
Original file line numberDiff line numberDiff line change
@@ -1639,6 +1639,43 @@ entry types were found.
16391639

16401640
Superseded by `ERR_OUT_OF_RANGE`
16411641

1642+
<a id="ERR_VM_MODULE_ALREADY_LINKED"></a>
1643+
### ERR_VM_MODULE_ALREADY_LINKED
1644+
1645+
The module attempted to be linked is not eligible for linking, because of one of
1646+
the following reasons:
1647+
1648+
- It has already been linked (`linkingStatus` is `'linked'`)
1649+
- It is being linked (`linkingStatus` is `'linking'`)
1650+
- Linking has failed for this module (`linkingStatus` is `'errored'`)
1651+
1652+
<a id="ERR_VM_MODULE_DIFFERENT_CONTEXT"></a>
1653+
### ERR_VM_MODULE_DIFFERENT_CONTEXT
1654+
1655+
The module being returned from the linker function is from a different context
1656+
than the parent module. Linked modules must share the same context.
1657+
1658+
<a id="ERR_VM_MODULE_LINKING_ERRORED"></a>
1659+
### ERR_VM_MODULE_LINKING_ERRORED
1660+
1661+
The linker function returned a module for which linking has failed.
1662+
1663+
<a id="ERR_VM_MODULE_NOT_LINKED"></a>
1664+
### ERR_VM_MODULE_NOT_LINKED
1665+
1666+
The module must be successfully linked before instantiation.
1667+
1668+
<a id="ERR_VM_MODULE_NOT_MODULE"></a>
1669+
### ERR_VM_MODULE_NOT_MODULE
1670+
1671+
The fulfilled value of a linking promise is not a `vm.Module` object.
1672+
1673+
<a id="ERR_VM_MODULE_STATUS"></a>
1674+
### ERR_VM_MODULE_STATUS
1675+
1676+
The current module's status does not allow for this operation. The specific
1677+
meaning of the error depends on the specific function.
1678+
16421679
<a id="ERR_ZLIB_BINDING_CLOSED"></a>
16431680
### ERR_ZLIB_BINDING_CLOSED
16441681

doc/api/vm.md

+322
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,322 @@ console.log(x); // 1; y is not defined.
4343
*Note*: The vm module is not a security mechanism.
4444
**Do not use it to run untrusted code**.
4545

46+
## Class: vm.Module
47+
<!-- YAML
48+
added: REPLACEME
49+
-->
50+
51+
> Stability: 1 - Experimental
52+
53+
*This feature is only available with the `--experimental-vm-modules` command
54+
flag enabled.*
55+
56+
The `vm.Module` class provides a low-level interface for using ECMAScript
57+
modules in VM contexts. It is the counterpart of the `vm.Script` class that
58+
closely mirrors [Source Text Module Record][]s as defined in the ECMAScript
59+
specification.
60+
61+
Unlike `vm.Script` however, every `vm.Module` object is bound to a context from
62+
its creation. Operations on `vm.Module` objects are intrinsically asynchronous,
63+
in contrast with the synchronous nature of `vm.Script` objects. With the help
64+
of async functions, however, manipulating `vm.Module` objects is fairly
65+
straightforward.
66+
67+
Using a `vm.Module` object requires four distinct steps: creation/parsing,
68+
linking, instantiation, and evaluation. These four steps are illustrated in the
69+
following example.
70+
71+
*Note*: This implementation lies at a lower level than the [ECMAScript Module
72+
loader][]. There is also currently no way to interact with the Loader, though
73+
support is planned.
74+
75+
```js
76+
const vm = require('vm');
77+
78+
const contextifiedSandbox = vm.createContext({ secret: 42 });
79+
80+
(async () => {
81+
// Step 1
82+
//
83+
// Create a Module by constructing a new `vm.Module` object. This parses the
84+
// provided source text, throwing a `SyntaxError` if anything goes wrong. By
85+
// default, a Module is created in the top context. But here, we specify
86+
// `contextifiedSandbox` as the context this Module belongs to.
87+
//
88+
// Here, we attempt to obtain the default export from the module "foo", and
89+
// put it into local binding "secret".
90+
91+
const bar = new vm.Module(`
92+
import s from 'foo';
93+
s;
94+
`, { context: contextifiedSandbox });
95+
96+
97+
// Step 2
98+
//
99+
// "Link" the imported dependencies of this Module to it.
100+
//
101+
// The provided linking callback (the "linker") accepts two arguments: the
102+
// parent module (`bar` in this case) and the string that is the specifier of
103+
// the imported module. The callback is expected to return a Module that
104+
// corresponds to the provided specifier, with certain requirements documented
105+
// in `module.link()`.
106+
//
107+
// If linking has not started for the returned Module, the same linker
108+
// callback will be called on the returned Module.
109+
//
110+
// Even top-level Modules without dependencies must be explicitly linked. The
111+
// callback provided would never be called, however.
112+
//
113+
// The link() method returns a Promise that will be resolved when all the
114+
// Promises returned by the linker resolve.
115+
//
116+
// Note: This is a contrived example in that the linker function creates a new
117+
// "foo" module every time it is called. In a full-fledged module system, a
118+
// cache would probably be used to avoid duplicated modules.
119+
120+
async function linker(referencingModule, specifier) {
121+
if (specifier === 'foo') {
122+
return new vm.Module(`
123+
// The "secret" variable refers to the global variable we added to
124+
// "contextifiedSandbox" when creating the context.
125+
export default secret;
126+
`, { context: referencingModule.context });
127+
128+
// Using `contextifiedSandbox` instead of `referencingModule.context`
129+
// here would work as well.
130+
}
131+
throw new Error(`Unable to resolve dependency: ${specifier}`);
132+
}
133+
await bar.link(linker);
134+
135+
136+
// Step 3
137+
//
138+
// Instantiate the top-level Module.
139+
//
140+
// Only the top-level Module needs to be explicitly instantiated; its
141+
// dependencies will be recursively instantiated by instantiate().
142+
143+
bar.instantiate();
144+
145+
146+
// Step 4
147+
//
148+
// Evaluate the Module. The evaluate() method returns a Promise with a single
149+
// property "result" that contains the result of the very last statement
150+
// executed in the Module. In the case of `bar`, it is `s;`, which refers to
151+
// the default export of the `foo` module, the `secret` we set in the
152+
// beginning to 42.
153+
154+
const { result } = await bar.evaluate();
155+
156+
console.log(result);
157+
// Prints 42.
158+
})();
159+
```
160+
161+
### Constructor: new vm.Module(code[, options])
162+
163+
* `code` {string} JavaScript Module code to parse
164+
* `options`
165+
* `url` {string} URL used in module resolution and stack traces. **Default**:
166+
`'vm:module(i)'` where `i` is a context-specific ascending index.
167+
* `context` {Object} The [contextified][] object as returned by the
168+
`vm.createContext()` method, to compile and evaluate this Module in.
169+
* `lineOffset` {integer} Specifies the line number offset that is displayed
170+
in stack traces produced by this Module.
171+
* `columnOffset` {integer} Spcifies the column number offset that is displayed
172+
in stack traces produced by this Module.
173+
174+
Creates a new ES `Module` object.
175+
176+
### module.dependencySpecifiers
177+
178+
* {string[]}
179+
180+
The specifiers of all dependencies of this module. The returned array is frozen
181+
to disallow any changes to it.
182+
183+
Corresponds to the [[RequestedModules]] field of [Source Text Module Record][]s
184+
in the ECMAScript specification.
185+
186+
### module.error
187+
188+
* {any}
189+
190+
If the `module.status` is `'errored'`, this property contains the exception thrown
191+
by the module during evaluation. If the status is anything else, accessing this
192+
property will result in a thrown exception.
193+
194+
*Note*: `undefined` cannot be used for cases where there is not a thrown
195+
exception due to possible ambiguity with `throw undefined;`.
196+
197+
Corresponds to the [[EvaluationError]] field of [Source Text Module Record][]s
198+
in the ECMAScript specification.
199+
200+
### module.linkingStatus
201+
202+
* {string}
203+
204+
The current linking status of `module`. It will be one of the following values:
205+
206+
- `'unlinked'`: `module.link()` has not yet been called.
207+
- `'linking'`: `module.link()` has been called, but not all Promises returned by
208+
the linker function have been resolved yet.
209+
- `'linked'`: `module.link()` has been called, and all its dependencies have
210+
been successfully linked.
211+
- `'errored'`: `module.link()` has been called, but at least one of its
212+
dependencies failed to link, either because the callback returned a Promise
213+
that is rejected, or because the Module the callback returned is invalid.
214+
215+
### module.namespace
216+
217+
* {Object}
218+
219+
The namespace object of the module. This is only available after instantiation
220+
(`module.instantiate()`) has completed.
221+
222+
Corresponds to the [GetModuleNamespace][] abstract operation in the ECMAScript
223+
specification.
224+
225+
### module.status
226+
227+
* {string}
228+
229+
The current status of the module. Will be one of:
230+
231+
- `'uninstantiated'`: The module is not instantiated. It may because of any of
232+
the following reasons:
233+
234+
- The module was just created.
235+
- `module.instantiate()` has been called on this module, but it failed for
236+
some reason.
237+
238+
This status does not convey any information regarding if `module.link()` has
239+
been called. See `module.linkingStatus` for that.
240+
241+
- `'instantiating'`: The module is currently being instantiated through a
242+
`module.instantiate()` call on itself or a parent module.
243+
244+
- `'instantiated'`: The module has been instantiated successfully, but
245+
`module.evaluate()` has not yet been called.
246+
247+
- `'evaluating'`: The module is being evaluated through a `module.evaluate()` on
248+
itself or a parent module.
249+
250+
- `'evaluated'`: The module has been successfully evaluated.
251+
252+
- `'errored'`: The module has been evaluated, but an exception was thrown.
253+
254+
Other than `'errored'`, this status string corresponds to the specification's
255+
[Source Text Module Record][]'s [[Status]] field. `'errored'` corresponds to
256+
`'evaluated'` in the specification, but with [[EvaluationError]] set to a value
257+
that is not `undefined`.
258+
259+
### module.url
260+
261+
* {string}
262+
263+
The URL of the current module, as set in the constructor.
264+
265+
### module.evaluate([options])
266+
267+
* `options` {Object}
268+
* `timeout` {number} Specifies the number of milliseconds to evaluate
269+
before terminating execution. If execution is interrupted, an [`Error`][]
270+
will be thrown.
271+
* `breakOnSigint` {boolean} If `true`, the execution will be terminated when
272+
`SIGINT` (Ctrl+C) is received. Existing handlers for the event that have
273+
been attached via `process.on("SIGINT")` will be disabled during script
274+
execution, but will continue to work after that. If execution is
275+
interrupted, an [`Error`][] will be thrown.
276+
* Returns: {Promise}
277+
278+
Evaluate the module.
279+
280+
This must be called after the module has been instantiated; otherwise it will
281+
throw an error. It could be called also when the module has already been
282+
evaluated, in which case it will do one of the following two things:
283+
284+
- return `undefined` if the initial evaluation ended in success (`module.status`
285+
is `'evaluated'`)
286+
- rethrow the same exception the initial evaluation threw if the initial
287+
evaluation ended in an error (`module.status` is `'errored'`)
288+
289+
This method cannot be called while the module is being evaluated
290+
(`module.status` is `'evaluating'`) to prevent infinite recursion.
291+
292+
Corresponds to the [Evaluate() concrete method][] field of [Source Text Module
293+
Record][]s in the ECMAScript specification.
294+
295+
### module.instantiate()
296+
297+
Instantiate the module. This must be called after linking has completed
298+
(`linkingStatus` is `'linked'`); otherwise it will throw an error. It may also
299+
throw an exception if one of the dependencies does not provide an export the
300+
parent module requires.
301+
302+
However, if this function succeeded, further calls to this function after the
303+
initial instantiation will be no-ops, to be consistent with the ECMAScript
304+
specification.
305+
306+
Unlike other methods operating on `Module`, this function completes
307+
synchronously and returns nothing.
308+
309+
Corresponds to the [Instantiate() concrete method][] field of [Source Text
310+
Module Record][]s in the ECMAScript specification.
311+
312+
### module.link(linker)
313+
314+
* `linker` {Function}
315+
* Returns: {Promise}
316+
317+
Link module dependencies. This method must be called before instantiation, and
318+
can only be called once per module.
319+
320+
Two parameters will be passed to the `linker` function:
321+
322+
- `referencingModule` The `Module` object `link()` is called on.
323+
- `specifier` The specifier of the requested module:
324+
325+
<!-- eslint-skip -->
326+
```js
327+
import foo from 'foo';
328+
// ^^^^^ the module specifier
329+
```
330+
331+
The function is expected to return a `Module` object or a `Promise` that
332+
eventually resolves to a `Module` object. The returned `Module` must satisfy the
333+
following two invariants:
334+
335+
- It must belong to the same context as the parent `Module`.
336+
- Its `linkingStatus` must not be `'errored'`.
337+
338+
If the returned `Module`'s `linkingStatus` is `'unlinked'`, this method will be
339+
recursively called on the returned `Module` with the same provided `linker`
340+
function.
341+
342+
`link()` returns a `Promise` that will either get resolved when all linking
343+
instances resolve to a valid `Module`, or rejected if the linker function either
344+
throws an exception or returns an invalid `Module`.
345+
346+
The linker function roughly corresponds to the implementation-defined
347+
[HostResolveImportedModule][] abstract operation in the ECMAScript
348+
specification, with a few key differences:
349+
350+
- The linker function is allowed to be asynchronous while
351+
[HostResolveImportedModule][] is synchronous.
352+
- The linker function is executed during linking, a Node.js-specific stage
353+
before instantiation, while [HostResolveImportedModule][] is called during
354+
instantiation.
355+
356+
The actual [HostResolveImportedModule][] implementation used during module
357+
instantiation is one that returns the modules linked during linking. Since at
358+
that point all modules would have been fully linked already, the
359+
[HostResolveImportedModule][] implementation is fully synchronous per
360+
specification.
361+
46362
## Class: vm.Script
47363
<!-- YAML
48364
added: v0.3.1
@@ -518,8 +834,14 @@ associating it with the `sandbox` object is what this document refers to as
518834
[`vm.createContext()`]: #vm_vm_createcontext_sandbox_options
519835
[`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedsandbox_options
520836
[`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options
837+
[GetModuleNamespace]: https://tc39.github.io/ecma262/#sec-getmodulenamespace
838+
[ECMAScript Module Loader]: esm.html#esm_ecmascript_modules
839+
[Evaluate() concrete method]: https://tc39.github.io/ecma262/#sec-moduleevaluation
840+
[HostResolveImportedModule]: https://tc39.github.io/ecma262/#sec-hostresolveimportedmodule
841+
[Instantiate() concrete method]: https://tc39.github.io/ecma262/#sec-moduledeclarationinstantiation
521842
[V8 Embedder's Guide]: https://github.com/v8/v8/wiki/Embedder's%20Guide#contexts
522843
[contextified]: #vm_what_does_it_mean_to_contextify_an_object
523844
[global object]: https://es5.github.io/#x15.1
524845
[indirect `eval()` call]: https://es5.github.io/#x10.4.2
525846
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin
847+
[Source Text Module Record]: https://tc39.github.io/ecma262/#sec-source-text-module-records

lib/internal/errors.js

+9
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,15 @@ E('ERR_V8BREAKITERATOR', 'Full ICU data not installed. ' +
676676
'See https://github.com/nodejs/node/wiki/Intl');
677677
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
678678
'At least one valid performance entry type is required');
679+
E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked');
680+
E('ERR_VM_MODULE_DIFFERENT_CONTEXT',
681+
'Linked modules must use the same context');
682+
E('ERR_VM_MODULE_LINKING_ERRORED',
683+
'Linking has already failed for the provided module');
684+
E('ERR_VM_MODULE_NOT_LINKED',
685+
'Module must be linked before it can be instantiated');
686+
E('ERR_VM_MODULE_NOT_MODULE', 'Provided module is not an instance of Module');
687+
E('ERR_VM_MODULE_STATUS', 'Module status %s');
679688
E('ERR_ZLIB_BINDING_CLOSED', 'zlib binding closed');
680689
E('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed');
681690

0 commit comments

Comments
 (0)