Skip to content

Commit 0d77eec

Browse files
devsnekMylesBorins
authored andcommitted
src: add support for TLA
PR-URL: #30370 Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Myles Borins <[email protected]>
1 parent fd9c7c2 commit 0d77eec

29 files changed

+243
-124
lines changed

doc/api/errors.md

+5
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,11 @@ provided.
889889
Encoding provided to `TextDecoder()` API was not one of the
890890
[WHATWG Supported Encodings][].
891891

892+
<a id="ERR_EVAL_ESM_CANNOT_PRINT"></a>
893+
### `ERR_EVAL_ESM_CANNOT_PRINT`
894+
895+
`--print` cannot be used with ESM input.
896+
892897
<a id="ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE"></a>
893898
### `ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE`
894899

doc/api/esm.md

+34-7
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ initial input, or when referenced by `import` statements within ES module code:
3636
* Files ending in `.js` when the nearest parent `package.json` file contains a
3737
top-level field `"type"` with a value of `"module"`.
3838

39-
* Strings passed in as an argument to `--eval` or `--print`, or piped to
40-
`node` via `STDIN`, with the flag `--input-type=module`.
39+
* Strings passed in as an argument to `--eval`, or piped to `node` via `STDIN`,
40+
with the flag `--input-type=module`.
4141

4242
Node.js will treat as CommonJS all other forms of input, such as `.js` files
4343
where the nearest parent `package.json` file contains no top-level `"type"`
@@ -52,8 +52,8 @@ or when referenced by `import` statements within ES module code:
5252
* Files ending in `.js` when the nearest parent `package.json` file contains a
5353
top-level field `"type"` with a value of `"commonjs"`.
5454

55-
* Strings passed in as an argument to `--eval` or `--print`, or piped to
56-
`node` via `STDIN`, with the flag `--input-type=commonjs`.
55+
* Strings passed in as an argument to `--eval` or `--print`, or piped to `node`
56+
via `STDIN`, with the flag `--input-type=commonjs`.
5757

5858
### `package.json` `"type"` field
5959

@@ -159,9 +159,9 @@ package scope:
159159

160160
### `--input-type` flag
161161

162-
Strings passed in as an argument to `--eval` or `--print` (or `-e` or `-p`), or
163-
piped to `node` via `STDIN`, will be treated as ES modules when the
164-
`--input-type=module` flag is set.
162+
Strings passed in as an argument to `--eval` (or `-e`), or piped to `node` via
163+
`STDIN`, will be treated as ES modules when the `--input-type=module` flag is
164+
set.
165165

166166
```sh
167167
node --input-type=module --eval "import { sep } from 'path'; console.log(sep);"
@@ -1076,6 +1076,32 @@ node --experimental-wasm-modules index.mjs
10761076

10771077
would provide the exports interface for the instantiation of `module.wasm`.
10781078

1079+
## Experimental Top-Level `await`
1080+
1081+
When the `--experimental-top-level-await` flag is provided, `await` may be used
1082+
in the top level (outside of async functions) within modules. This implements
1083+
the [ECMAScript Top-Level `await` proposal][].
1084+
1085+
Assuming an `a.mjs` with
1086+
1087+
<!-- eslint-skip -->
1088+
```js
1089+
export const five = await Promise.resolve(5);
1090+
```
1091+
1092+
And a `b.mjs` with
1093+
1094+
```js
1095+
import { five } from './a.mjs';
1096+
1097+
console.log(five); // Logs `5`
1098+
```
1099+
1100+
```bash
1101+
node b.mjs # fails
1102+
node --experimental-top-level-await b.mjs # works
1103+
```
1104+
10791105
## Experimental Loaders
10801106

10811107
**Note: This API is currently being redesigned and will still change.**
@@ -1779,6 +1805,7 @@ success!
17791805
[Conditional Exports]: #esm_conditional_exports
17801806
[Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
17811807
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
1808+
[ECMAScript Top-Level `await` proposal]: https://github.com/tc39/proposal-top-level-await/
17821809
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
17831810
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
17841811
[Terminology]: #esm_terminology

doc/api/vm.md

+14-18
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,10 @@ support is planned.
402402
```js
403403
const vm = require('vm');
404404

405-
const contextifiedObject = vm.createContext({ secret: 42 });
405+
const contextifiedObject = vm.createContext({
406+
secret: 42,
407+
print: console.log,
408+
});
406409

407410
(async () => {
408411
// Step 1
@@ -418,6 +421,7 @@ const contextifiedObject = vm.createContext({ secret: 42 });
418421
const bar = new vm.SourceTextModule(`
419422
import s from 'foo';
420423
s;
424+
print(s);
421425
`, { context: contextifiedObject });
422426

423427
// Step 2
@@ -460,16 +464,11 @@ const contextifiedObject = vm.createContext({ secret: 42 });
460464

461465
// Step 3
462466
//
463-
// Evaluate the Module. The evaluate() method returns a Promise with a single
464-
// property "result" that contains the result of the very last statement
465-
// executed in the Module. In the case of `bar`, it is `s;`, which refers to
466-
// the default export of the `foo` module, the `secret` we set in the
467-
// beginning to 42.
467+
// Evaluate the Module. The evaluate() method returns a promise which will
468+
// resolve after the module has finished evaluating.
468469

469-
const { result } = await bar.evaluate();
470-
471-
console.log(result);
472470
// Prints 42.
471+
await bar.evaluate();
473472
})();
474473
```
475474

@@ -512,17 +511,14 @@ in the ECMAScript specification.
512511

513512
Evaluate the module.
514513

515-
This must be called after the module has been linked; otherwise it will
516-
throw an error. It could be called also when the module has already been
517-
evaluated, in which case it will do one of the following two things:
518-
519-
* return `undefined` if the initial evaluation ended in success (`module.status`
520-
is `'evaluated'`)
521-
* rethrow the same exception the initial evaluation threw if the initial
522-
evaluation ended in an error (`module.status` is `'errored'`)
514+
This must be called after the module has been linked; otherwise it will reject.
515+
It could be called also when the module has already been evaluated, in which
516+
case it will either do nothing if the initial evaluation ended in success
517+
(`module.status` is `'evaluated'`) or it will re-throw the exception that the
518+
initial evaluation resulted in (`module.status` is `'errored'`).
523519

524520
This method cannot be called while the module is being evaluated
525-
(`module.status` is `'evaluating'`) to prevent infinite recursion.
521+
(`module.status` is `'evaluating'`).
526522

527523
Corresponds to the [Evaluate() concrete method][] field of [Cyclic Module
528524
Record][]s in the ECMAScript specification.

lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,7 @@ E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) {
805805
}, TypeError);
806806
E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported',
807807
RangeError);
808+
E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error);
808809
E('ERR_FALSY_VALUE_REJECTION', function(reason) {
809810
this.reason = reason;
810811
return 'Promise was rejected with falsy value';

lib/internal/modules/esm/loader.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,9 @@ class Loader {
167167
};
168168
const job = new ModuleJob(this, url, evalInstance, false, false);
169169
this.moduleMap.set(url, job);
170-
const { module, result } = await job.run();
170+
const { module } = await job.run();
171171
return {
172172
namespace: module.getNamespace(),
173-
result
174173
};
175174
}
176175

lib/internal/modules/esm/module_job.js

+20-18
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,26 @@ class ModuleJob {
3131
this.isMain = isMain;
3232
this.inspectBrk = inspectBrk;
3333

34-
// This is a Promise<{ module, reflect }>, whose fields will be copied
35-
// onto `this` by `link()` below once it has been resolved.
36-
this.modulePromise = moduleProvider.call(loader, url, isMain);
3734
this.module = undefined;
35+
// Expose the promise to the ModuleWrap directly for linking below.
36+
// `this.module` is also filled in below.
37+
this.modulePromise = moduleProvider.call(loader, url, isMain);
3838

3939
// Wait for the ModuleWrap instance being linked with all dependencies.
4040
const link = async () => {
4141
this.module = await this.modulePromise;
4242
assert(this.module instanceof ModuleWrap);
4343

44+
// Explicitly keeping track of dependency jobs is needed in order
45+
// to flatten out the dependency graph below in `_instantiate()`,
46+
// so that circular dependencies can't cause a deadlock by two of
47+
// these `link` callbacks depending on each other.
4448
const dependencyJobs = [];
4549
const promises = this.module.link(async (specifier) => {
4650
const jobPromise = this.loader.getModuleJob(specifier, url);
4751
dependencyJobs.push(jobPromise);
48-
return (await jobPromise).modulePromise;
52+
const job = await jobPromise;
53+
return job.modulePromise;
4954
});
5055

5156
if (promises !== undefined)
@@ -59,25 +64,20 @@ class ModuleJob {
5964
// 'unhandled rejection' warnings.
6065
this.linked.catch(noop);
6166

62-
// instantiated == deep dependency jobs wrappers instantiated,
63-
// module wrapper instantiated
67+
// instantiated == deep dependency jobs wrappers are instantiated,
68+
// and module wrapper is instantiated.
6469
this.instantiated = undefined;
6570
}
6671

67-
async instantiate() {
68-
if (!this.instantiated) {
69-
return this.instantiated = this._instantiate();
72+
instantiate() {
73+
if (this.instantiated === undefined) {
74+
this.instantiated = this._instantiate();
7075
}
71-
await this.instantiated;
72-
return this.module;
76+
return this.instantiated;
7377
}
7478

75-
// This method instantiates the module associated with this job and its
76-
// entire dependency graph, i.e. creates all the module namespaces and the
77-
// exported/imported variables.
7879
async _instantiate() {
7980
const jobsInGraph = new SafeSet();
80-
8181
const addJobsToDependencyGraph = async (moduleJob) => {
8282
if (jobsInGraph.has(moduleJob)) {
8383
return;
@@ -87,6 +87,7 @@ class ModuleJob {
8787
return PromiseAll(dependencyJobs.map(addJobsToDependencyGraph));
8888
};
8989
await addJobsToDependencyGraph(this);
90+
9091
try {
9192
if (!hasPausedEntry && this.inspectBrk) {
9293
hasPausedEntry = true;
@@ -122,19 +123,20 @@ class ModuleJob {
122123
}
123124
throw e;
124125
}
126+
125127
for (const dependencyJob of jobsInGraph) {
126128
// Calling `this.module.instantiate()` instantiates not only the
127129
// ModuleWrap in this module, but all modules in the graph.
128130
dependencyJob.instantiated = resolvedPromise;
129131
}
130-
return this.module;
131132
}
132133

133134
async run() {
134-
const module = await this.instantiate();
135+
await this.instantiate();
135136
const timeout = -1;
136137
const breakOnSigint = false;
137-
return { module, result: module.evaluate(timeout, breakOnSigint) };
138+
await this.module.evaluate(timeout, breakOnSigint);
139+
return { module: this.module };
138140
}
139141
}
140142
ObjectSetPrototypeOf(ModuleJob.prototype, null);

lib/internal/process/execution.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ const path = require('path');
1010
const {
1111
codes: {
1212
ERR_INVALID_ARG_TYPE,
13-
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET
14-
}
13+
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET,
14+
ERR_EVAL_ESM_CANNOT_PRINT,
15+
},
1516
} = require('internal/errors');
1617

1718
const {
@@ -39,6 +40,9 @@ function tryGetCwd() {
3940
}
4041

4142
function evalModule(source, print) {
43+
if (print) {
44+
throw new ERR_EVAL_ESM_CANNOT_PRINT();
45+
}
4246
const { log, error } = require('internal/console/global');
4347
const { decorateErrorStack } = require('internal/util');
4448
const asyncESM = require('internal/process/esm_loader');

lib/internal/vm/module.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,7 @@ class Module {
219219
'must be one of linked, evaluated, or errored'
220220
);
221221
}
222-
const result = this[kWrap].evaluate(timeout, breakOnSigint);
223-
return { __proto__: null, result };
222+
await this[kWrap].evaluate(timeout, breakOnSigint);
224223
}
225224

226225
[customInspectSymbol](depth, options) {

src/module_wrap.cc

+15-5
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,13 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
375375
return;
376376
}
377377

378-
args.GetReturnValue().Set(result.ToLocalChecked());
378+
// If TLA is enabled, `result` is the evaluation's promise.
379+
// Otherwise, `result` is the last evaluated value of the module,
380+
// which could be a promise, which would result in it being incorrectly
381+
// unwrapped when the higher level code awaits the evaluation.
382+
if (env->isolate_data()->options()->experimental_top_level_await) {
383+
args.GetReturnValue().Set(result.ToLocalChecked());
384+
}
379385
}
380386

381387
void ModuleWrap::GetNamespace(const FunctionCallbackInfo<Value>& args) {
@@ -387,13 +393,17 @@ void ModuleWrap::GetNamespace(const FunctionCallbackInfo<Value>& args) {
387393
Local<Module> module = obj->module_.Get(isolate);
388394

389395
switch (module->GetStatus()) {
390-
default:
396+
case v8::Module::Status::kUninstantiated:
397+
case v8::Module::Status::kInstantiating:
391398
return env->ThrowError(
392-
"cannot get namespace, Module has not been instantiated");
399+
"cannot get namespace, module has not been instantiated");
393400
case v8::Module::Status::kInstantiated:
394401
case v8::Module::Status::kEvaluating:
395402
case v8::Module::Status::kEvaluated:
403+
case v8::Module::Status::kErrored:
396404
break;
405+
default:
406+
UNREACHABLE();
397407
}
398408

399409
Local<Value> result = module->GetModuleNamespace();
@@ -616,19 +626,19 @@ MaybeLocal<Value> ModuleWrap::SyntheticModuleEvaluationStepsCallback(
616626
TryCatchScope try_catch(env);
617627
Local<Function> synthetic_evaluation_steps =
618628
obj->synthetic_evaluation_steps_.Get(isolate);
629+
obj->synthetic_evaluation_steps_.Reset();
619630
MaybeLocal<Value> ret = synthetic_evaluation_steps->Call(context,
620631
obj->object(), 0, nullptr);
621632
if (ret.IsEmpty()) {
622633
CHECK(try_catch.HasCaught());
623634
}
624-
obj->synthetic_evaluation_steps_.Reset();
625635
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
626636
CHECK(!try_catch.Message().IsEmpty());
627637
CHECK(!try_catch.Exception().IsEmpty());
628638
try_catch.ReThrow();
629639
return MaybeLocal<Value>();
630640
}
631-
return ret;
641+
return Undefined(isolate);
632642
}
633643

634644
void ModuleWrap::SetSyntheticExport(const FunctionCallbackInfo<Value>& args) {

0 commit comments

Comments
 (0)