Repository: nodejs/node
Pull request: vm: expose hasTopLevelAwait on SourceTextModule

Expose `hasTopLevelAwait` and `getHasAsyncGraph` on
`vm.SourceTextModule`.

`getHasAsyncGraph` requires the module to be instantiated first.

Fixes: https://github.com/nodejs/node/issues/59656

Changed files:
filename: doc/api/vm.md
patch: @@ -920,6 +920,36 @@ to disallow any changes to it.
 Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in
 the ECMAScript specification.
 
+### `sourceTextModule.hasAsyncGraph()`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {boolean}
+
+Iterates over the dependency graph and returns `true` if any module in its
+dependencies or this module itself contains top-level `await` expressions,
+otherwise returns `false`.
+
+The search may be slow if the graph is big enough.
+
+This requires the module to be instantiated first. If the module is not
+instantiated yet, an error will be thrown.
+
+### `sourceTextModule.hasTopLevelAwait()`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {boolean}
+
+Returns whether the module itself contains any top-level `await` expressions.
+
+This corresponds to the field `[[HasTLA]]` in [Cyclic Module Record][] in the
+ECMAScript specification.
+
 ### `sourceTextModule.instantiate()`
 
 <!-- YAML

filename: lib/internal/vm/module.js
patch: @@ -429,6 +429,19 @@ class SourceTextModule extends Module {
     return super.error;
   }
 
+  hasAsyncGraph() {
+    validateThisInternalField(this, kWrap, 'SourceTextModule');
+    if (this[kWrap].getStatus() < kInstantiated) {
+      throw new ERR_VM_MODULE_STATUS('must be instantiated');
+    }
+    return this[kWrap].hasAsyncGraph;
+  }
+
+  hasTopLevelAwait() {
+    validateThisInternalField(this, kWrap, 'SourceTextModule');
+    return this[kWrap].hasTopLevelAwait;
+  }
+
   createCachedData() {
     const { status } = this;
     if (status === 'evaluating' ||

filename: test/parallel/test-vm-module-hasasyncgraph.js
patch: @@ -0,0 +1,67 @@
+'use strict';
+
+// Flags: --experimental-vm-modules
+
+require('../common');
+
+const assert = require('assert');
+
+const { SourceTextModule } = require('vm');
+const test = require('node:test');
+
+test('module is not instantiated yet', () => {
+  const foo = new SourceTextModule(`
+    export const foo = 4
+    export default 5;
+  `);
+  assert.throws(() => foo.hasAsyncGraph(), {
+    code: 'ERR_VM_MODULE_STATUS',
+  });
+});
+
+test('simple module with top-level await', () => {
+  const foo = new SourceTextModule(`
+    export const foo = 4
+    export default 5;
+
+    await 0;
+  `);
+  foo.linkRequests([]);
+  foo.instantiate();
+
+  assert.strictEqual(foo.hasAsyncGraph(), true);
+});
+
+test('simple module with non top-level await', () => {
+  const foo = new SourceTextModule(`
+    export const foo = 4
+    export default 5;
+
+    export async function f() {
+      await 0;
+    }
+  `);
+  foo.linkRequests([]);
+  foo.instantiate();
+
+  assert.strictEqual(foo.hasAsyncGraph(), false);
+});
+
+test('module with a dependency containing top-level await', () => {
+  const foo = new SourceTextModule(`
+    export const foo = 4
+    export default 5;
+
+    await 0;
+  `);
+  foo.linkRequests([]);
+
+  const bar = new SourceTextModule(`
+    export { foo } from 'foo';
+  `);
+  bar.linkRequests([foo]);
+  bar.instantiate();
+
+  assert.strictEqual(foo.hasAsyncGraph(), true);
+  assert.strictEqual(bar.hasAsyncGraph(), true);
+});

filename: test/parallel/test-vm-module-hastoplevelawait.js
patch: @@ -0,0 +1,59 @@
+'use strict';
+
+// Flags: --experimental-vm-modules
+
+require('../common');
+
+const assert = require('assert');
+
+const { SourceTextModule } = require('vm');
+const test = require('node:test');
+
+test('simple module', () => {
+  const foo = new SourceTextModule(`
+    export const foo = 4
+    export default 5;
+  `);
+  assert.strictEqual(foo.hasTopLevelAwait(), false);
+});
+
+test('simple module with top-level await', () => {
+  const foo = new SourceTextModule(`
+    export const foo = 4
+    export default 5;
+
+    await 0;
+  `);
+  assert.strictEqual(foo.hasTopLevelAwait(), true);
+});
+
+test('simple module with non top-level await', () => {
+  const foo = new SourceTextModule(`
+    export const foo = 4
+    export default 5;
+
+    export async function f() {
+      await 0;
+    }
+  `);
+  assert.strictEqual(foo.hasTopLevelAwait(), false);
+});
+
+test('module with a dependency containing top-level await', () => {
+  const foo = new SourceTextModule(`
+    export const foo = 4
+    export default 5;
+
+    await 0;
+  `);
+  foo.linkRequests([]);
+
+  const bar = new SourceTextModule(`
+    export { foo } from 'foo';
+  `);
+  bar.linkRequests([foo]);
+  bar.instantiate();
+
+  assert.strictEqual(foo.hasTopLevelAwait(), true);
+  assert.strictEqual(bar.hasTopLevelAwait(), false);
+});