Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src: add support for top level await #30370

Merged
merged 1 commit into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,11 @@ provided.
Encoding provided to `TextDecoder()` API was not one of the
[WHATWG Supported Encodings][].

<a id="ERR_EVAL_ESM_CANNOT_PRINT"></a>
### `ERR_EVAL_ESM_CANNOT_PRINT`

`--print` cannot be used with ESM input.
devsnek marked this conversation as resolved.
Show resolved Hide resolved

<a id="ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE"></a>
### `ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE`

Expand Down
41 changes: 34 additions & 7 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ initial input, or when referenced by `import` statements within ES module code:
* Files ending in `.js` when the nearest parent `package.json` file contains a
top-level field `"type"` with a value of `"module"`.

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

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

* Strings passed in as an argument to `--eval` or `--print`, or piped to
devsnek marked this conversation as resolved.
Show resolved Hide resolved
`node` via `STDIN`, with the flag `--input-type=commonjs`.
* Strings passed in as an argument to `--eval` or `--print`, or piped to `node`
via `STDIN`, with the flag `--input-type=commonjs`.

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

Expand Down Expand Up @@ -159,9 +159,9 @@ package scope:

### `--input-type` flag

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

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

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

## Experimental Top-Level `await`

When the `--experimental-top-level-await` flag is provided, `await` may be used
in the top level (outside of async functions) within modules. This implements
the [ECMAScript Top-Level `await` proposal][].

Assuming an `a.mjs` with

<!-- eslint-skip -->
```js
export const five = await Promise.resolve(5);
```

And a `b.mjs` with

```js
import { five } from './a.mjs';

console.log(five); // Logs `5`
```

```bash
node b.mjs # fails
node --experimental-top-level-await b.mjs # works
```

## Experimental Loaders

**Note: This API is currently being redesigned and will still change.**
Expand Down Expand Up @@ -1779,6 +1805,7 @@ success!
[Conditional Exports]: #esm_conditional_exports
[Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
[ECMAScript Top-Level `await` proposal]: https://github.com/tc39/proposal-top-level-await/
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
[Terminology]: #esm_terminology
Expand Down
32 changes: 14 additions & 18 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,10 @@ support is planned.
```js
const vm = require('vm');

const contextifiedObject = vm.createContext({ secret: 42 });
const contextifiedObject = vm.createContext({
secret: 42,
print: console.log,
});

(async () => {
// Step 1
Expand All @@ -418,6 +421,7 @@ const contextifiedObject = vm.createContext({ secret: 42 });
const bar = new vm.SourceTextModule(`
import s from 'foo';
s;
print(s);
`, { context: contextifiedObject });

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

// Step 3
//
// 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.
// Evaluate the Module. The evaluate() method returns a promise which will
// resolve after the module has finished evaluating.

const { result } = await bar.evaluate();

console.log(result);
// Prints 42.
await bar.evaluate();
})();
```

Expand Down Expand Up @@ -512,17 +511,14 @@ in the ECMAScript specification.

Evaluate the module.

This must be called after the module has been linked; 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 must be called after the module has been linked; otherwise it will reject.
It could be called also when the module has already been evaluated, in which
case it will either do nothing if the initial evaluation ended in success
(`module.status` is `'evaluated'`) or it will re-throw the exception that the
initial evaluation resulted in (`module.status` is `'errored'`).

This method cannot be called while the module is being evaluated
(`module.status` is `'evaluating'`) to prevent infinite recursion.
(`module.status` is `'evaluating'`).

Corresponds to the [Evaluate() concrete method][] field of [Cyclic Module
Record][]s in the ECMAScript specification.
Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) {
}, TypeError);
E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported',
RangeError);
E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error);
E('ERR_FALSY_VALUE_REJECTION', function(reason) {
this.reason = reason;
return 'Promise was rejected with falsy value';
Expand Down
3 changes: 1 addition & 2 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,9 @@ class Loader {
};
const job = new ModuleJob(this, url, evalInstance, false, false);
this.moduleMap.set(url, job);
const { module, result } = await job.run();
const { module } = await job.run();
MylesBorins marked this conversation as resolved.
Show resolved Hide resolved
return {
namespace: module.getNamespace(),
result
};
}

Expand Down
38 changes: 20 additions & 18 deletions lib/internal/modules/esm/module_job.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,26 @@ class ModuleJob {
this.isMain = isMain;
this.inspectBrk = inspectBrk;

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

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

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

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

// instantiated == deep dependency jobs wrappers instantiated,
// module wrapper instantiated
// instantiated == deep dependency jobs wrappers are instantiated,
// and module wrapper is instantiated.
this.instantiated = undefined;
}

async instantiate() {
if (!this.instantiated) {
return this.instantiated = this._instantiate();
instantiate() {
devsnek marked this conversation as resolved.
Show resolved Hide resolved
if (this.instantiated === undefined) {
this.instantiated = this._instantiate();
}
await this.instantiated;
return this.module;
return this.instantiated;
}

// This method instantiates the module associated with this job and its
// entire dependency graph, i.e. creates all the module namespaces and the
// exported/imported variables.
async _instantiate() {
const jobsInGraph = new SafeSet();

const addJobsToDependencyGraph = async (moduleJob) => {
if (jobsInGraph.has(moduleJob)) {
return;
Expand All @@ -87,6 +87,7 @@ class ModuleJob {
return PromiseAll(dependencyJobs.map(addJobsToDependencyGraph));
};
await addJobsToDependencyGraph(this);

try {
if (!hasPausedEntry && this.inspectBrk) {
hasPausedEntry = true;
Expand Down Expand Up @@ -122,19 +123,20 @@ class ModuleJob {
}
throw e;
}

for (const dependencyJob of jobsInGraph) {
// Calling `this.module.instantiate()` instantiates not only the
// ModuleWrap in this module, but all modules in the graph.
dependencyJob.instantiated = resolvedPromise;
}
return this.module;
}

async run() {
const module = await this.instantiate();
await this.instantiate();
const timeout = -1;
const breakOnSigint = false;
return { module, result: module.evaluate(timeout, breakOnSigint) };
await this.module.evaluate(timeout, breakOnSigint);
return { module: this.module };
}
}
ObjectSetPrototypeOf(ModuleJob.prototype, null);
Expand Down
8 changes: 6 additions & 2 deletions lib/internal/process/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ const path = require('path');
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET
}
ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET,
ERR_EVAL_ESM_CANNOT_PRINT,
},
} = require('internal/errors');

const {
Expand Down Expand Up @@ -39,6 +40,9 @@ function tryGetCwd() {
}

function evalModule(source, print) {
if (print) {
throw new ERR_EVAL_ESM_CANNOT_PRINT();
}
const { log, error } = require('internal/console/global');
const { decorateErrorStack } = require('internal/util');
const asyncESM = require('internal/process/esm_loader');
Expand Down
3 changes: 1 addition & 2 deletions lib/internal/vm/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,7 @@ class Module {
'must be one of linked, evaluated, or errored'
);
}
const result = this[kWrap].evaluate(timeout, breakOnSigint);
return { __proto__: null, result };
await this[kWrap].evaluate(timeout, breakOnSigint);
}

[customInspectSymbol](depth, options) {
Expand Down
20 changes: 15 additions & 5 deletions src/module_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,13 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
return;
}

args.GetReturnValue().Set(result.ToLocalChecked());
// If TLA is enabled, `result` is the evaluation's promise.
// Otherwise, `result` is the last evaluated value of the module,
// which could be a promise, which would result in it being incorrectly
// unwrapped when the higher level code awaits the evaluation.
if (env->isolate_data()->options()->experimental_top_level_await) {
guybedford marked this conversation as resolved.
Show resolved Hide resolved
args.GetReturnValue().Set(result.ToLocalChecked());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you leave a comment explaining this change?

}

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

switch (module->GetStatus()) {
default:
devsnek marked this conversation as resolved.
Show resolved Hide resolved
case v8::Module::Status::kUninstantiated:
case v8::Module::Status::kInstantiating:
return env->ThrowError(
"cannot get namespace, Module has not been instantiated");
"cannot get namespace, module has not been instantiated");
case v8::Module::Status::kInstantiated:
case v8::Module::Status::kEvaluating:
case v8::Module::Status::kEvaluated:
case v8::Module::Status::kErrored:
break;
default:
UNREACHABLE();
}

Local<Value> result = module->GetModuleNamespace();
Expand Down Expand Up @@ -616,19 +626,19 @@ MaybeLocal<Value> ModuleWrap::SyntheticModuleEvaluationStepsCallback(
TryCatchScope try_catch(env);
Local<Function> synthetic_evaluation_steps =
obj->synthetic_evaluation_steps_.Get(isolate);
obj->synthetic_evaluation_steps_.Reset();
MaybeLocal<Value> ret = synthetic_evaluation_steps->Call(context,
obj->object(), 0, nullptr);
if (ret.IsEmpty()) {
CHECK(try_catch.HasCaught());
}
obj->synthetic_evaluation_steps_.Reset();
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
CHECK(!try_catch.Message().IsEmpty());
CHECK(!try_catch.Exception().IsEmpty());
try_catch.ReThrow();
return MaybeLocal<Value>();
}
return ret;
return Undefined(isolate);
}

void ModuleWrap::SetSyntheticExport(const FunctionCallbackInfo<Value>& args) {
Expand Down
Loading