Skip to content

Commit 9807ffd

Browse files
legendecasrichardlau
authored andcommitted
vm: expose import attributes on SourceTextModule.moduleRequests
PR-URL: #58829 Backport-PR-URL: #59962 Refs: #37648 Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Marco Ippolito <[email protected]>
1 parent d302cb3 commit 9807ffd

File tree

6 files changed

+221
-37
lines changed

6 files changed

+221
-37
lines changed

doc/api/vm.md

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -573,16 +573,6 @@ const contextifiedObject = vm.createContext({
573573
})();
574574
```
575575

576-
### `module.dependencySpecifiers`
577-
578-
* {string\[]}
579-
580-
The specifiers of all dependencies of this module. The returned array is frozen
581-
to disallow any changes to it.
582-
583-
Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in
584-
the ECMAScript specification.
585-
586576
### `module.error`
587577

588578
* Type: {any}
@@ -887,6 +877,72 @@ const cachedData = module.createCachedData();
887877
const module2 = new vm.SourceTextModule('const a = 1;', { cachedData });
888878
```
889879
880+
### `sourceTextModule.dependencySpecifiers`
881+
882+
<!-- YAML
883+
changes:
884+
- version: REPLACEME
885+
pr-url: https://github.com/nodejs/node/pull/20300
886+
description: This is deprecated in favour of `sourceTextModule.moduleRequests`.
887+
-->
888+
889+
> Stability: 0 - Deprecated: Use [`sourceTextModule.moduleRequests`][] instead.
890+
891+
* {string\[]}
892+
893+
The specifiers of all dependencies of this module. The returned array is frozen
894+
to disallow any changes to it.
895+
896+
Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in
897+
the ECMAScript specification.
898+
899+
### `sourceTextModule.moduleRequests`
900+
901+
<!-- YAML
902+
added: REPLACEME
903+
-->
904+
905+
* {ModuleRequest\[]} Dependencies of this module.
906+
907+
The requested import dependencies of this module. The returned array is frozen
908+
to disallow any changes to it.
909+
910+
For example, given a source text:
911+
912+
<!-- eslint-disable no-duplicate-imports -->
913+
914+
```mjs
915+
import foo from 'foo';
916+
import fooAlias from 'foo';
917+
import bar from './bar.js';
918+
import withAttrs from '../with-attrs.ts' with { arbitraryAttr: 'attr-val' };
919+
```
920+
921+
<!-- eslint-enable no-duplicate-imports -->
922+
923+
The value of the `sourceTextModule.moduleRequests` will be:
924+
925+
```js
926+
[
927+
{
928+
specifier: 'foo',
929+
attributes: {},
930+
},
931+
{
932+
specifier: 'foo',
933+
attributes: {},
934+
},
935+
{
936+
specifier: './bar.js',
937+
attributes: {},
938+
},
939+
{
940+
specifier: '../with-attrs.ts',
941+
attributes: { arbitraryAttr: 'attr-val' },
942+
},
943+
];
944+
```
945+
890946
## Class: `vm.SyntheticModule`
891947
892948
<!-- YAML
@@ -983,6 +1039,20 @@ const vm = require('node:vm');
9831039
})();
9841040
```
9851041
1042+
## Type: `ModuleRequest`
1043+
1044+
<!-- YAML
1045+
added: REPLACEME
1046+
-->
1047+
1048+
* {Object}
1049+
* `specifier` {string} The specifier of the requested module.
1050+
* `attributes` {Object} The `"with"` value passed to the
1051+
[WithClause][] in a [ImportDeclaration][], or an empty object if no value was
1052+
provided.
1053+
1054+
A `ModuleRequest` represents the request to import a module with given import attributes and phase.
1055+
9861056
## `vm.compileFunction(code[, params[, options]])`
9871057
9881058
<!-- YAML
@@ -1953,12 +2023,14 @@ const { Script, SyntheticModule } = require('node:vm');
19532023
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
19542024
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
19552025
[HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule
2026+
[ImportDeclaration]: https://tc39.es/ecma262/#prod-ImportDeclaration
19562027
[Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
19572028
[Module Record]: https://tc39.es/ecma262/#sec-abstract-module-records
19582029
[Source Text Module Record]: https://tc39.es/ecma262/#sec-source-text-module-records
19592030
[Support of dynamic `import()` in compilation APIs]: #support-of-dynamic-import-in-compilation-apis
19602031
[Synthetic Module Record]: https://tc39.es/ecma262/#sec-synthetic-module-records
19612032
[V8 Embedder's Guide]: https://v8.dev/docs/embed#contexts
2033+
[WithClause]: https://tc39.es/ecma262/#prod-WithClause
19622034
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag
19632035
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.md#err_vm_dynamic_import_callback_missing
19642036
[`ERR_VM_MODULE_STATUS`]: errors.md#err_vm_module_status
@@ -1968,6 +2040,7 @@ const { Script, SyntheticModule } = require('node:vm');
19682040
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
19692041
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
19702042
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
2043+
[`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests
19712044
[`url.origin`]: url.md#urlorigin
19722045
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
19732046
[`vm.constants.DONT_CONTEXTIFY`]: #vmconstantsdont_contextify

lib/internal/vm/module.js

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const {
6464
} = binding;
6565

6666
const STATUS_MAP = {
67+
__proto__: null,
6768
[kUninstantiated]: 'unlinked',
6869
[kInstantiating]: 'linking',
6970
[kInstantiated]: 'linked',
@@ -251,13 +252,15 @@ class Module {
251252
}
252253
}
253254

254-
const kDependencySpecifiers = Symbol('kDependencySpecifiers');
255255
const kNoError = Symbol('kNoError');
256256

257257
class SourceTextModule extends Module {
258258
#error = kNoError;
259259
#statusOverride;
260260

261+
#moduleRequests;
262+
#dependencySpecifiers;
263+
261264
constructor(sourceText, options = kEmptyObject) {
262265
validateString(sourceText, 'sourceText');
263266
validateObject(options, 'options');
@@ -298,20 +301,25 @@ class SourceTextModule extends Module {
298301
importModuleDynamically,
299302
});
300303

301-
this[kDependencySpecifiers] = undefined;
304+
this.#moduleRequests = ObjectFreeze(ArrayPrototypeMap(this[kWrap].getModuleRequests(), (request) => {
305+
return ObjectFreeze({
306+
__proto__: null,
307+
specifier: request.specifier,
308+
attributes: request.attributes,
309+
});
310+
}));
302311
}
303312

304313
async [kLink](linker) {
305314
this.#statusOverride = 'linking';
306315

307-
const moduleRequests = this[kWrap].getModuleRequests();
308316
// Iterates the module requests and links with the linker.
309317
// Specifiers should be aligned with the moduleRequests array in order.
310-
const specifiers = Array(moduleRequests.length);
311-
const modulePromises = Array(moduleRequests.length);
318+
const specifiers = Array(this.#moduleRequests.length);
319+
const modulePromises = Array(this.#moduleRequests.length);
312320
// Iterates with index to avoid calling into userspace with `Symbol.iterator`.
313-
for (let idx = 0; idx < moduleRequests.length; idx++) {
314-
const { specifier, attributes } = moduleRequests[idx];
321+
for (let idx = 0; idx < this.#moduleRequests.length; idx++) {
322+
const { specifier, attributes } = this.#moduleRequests[idx];
315323

316324
const linkerResult = linker(specifier, this, {
317325
attributes,
@@ -349,16 +357,16 @@ class SourceTextModule extends Module {
349357
}
350358

351359
get dependencySpecifiers() {
352-
validateThisInternalField(this, kDependencySpecifiers, 'SourceTextModule');
353-
// TODO(legendecas): add a new getter to expose the import attributes as the value type
354-
// of [[RequestedModules]] is changed in https://tc39.es/proposal-import-attributes/#table-cyclic-module-fields.
355-
this[kDependencySpecifiers] ??= ObjectFreeze(
356-
ArrayPrototypeMap(this[kWrap].getModuleRequests(), (request) => request.specifier));
357-
return this[kDependencySpecifiers];
360+
this.#dependencySpecifiers ??= ObjectFreeze(
361+
ArrayPrototypeMap(this.#moduleRequests, (request) => request.specifier));
362+
return this.#dependencySpecifiers;
363+
}
364+
365+
get moduleRequests() {
366+
return this.#moduleRequests;
358367
}
359368

360369
get status() {
361-
validateThisInternalField(this, kDependencySpecifiers, 'SourceTextModule');
362370
if (this.#error !== kNoError) {
363371
return 'errored';
364372
}
@@ -369,7 +377,6 @@ class SourceTextModule extends Module {
369377
}
370378

371379
get error() {
372-
validateThisInternalField(this, kDependencySpecifiers, 'SourceTextModule');
373380
if (this.#error !== kNoError) {
374381
return this.#error;
375382
}
@@ -432,8 +439,12 @@ class SyntheticModule extends Module {
432439
}
433440

434441
function importModuleDynamicallyWrap(importModuleDynamically) {
435-
const importModuleDynamicallyWrapper = async (...args) => {
436-
const m = await ReflectApply(importModuleDynamically, this, args);
442+
const importModuleDynamicallyWrapper = async (specifier, referrer, attributes) => {
443+
const m = await ReflectApply(
444+
importModuleDynamically,
445+
this,
446+
[specifier, referrer, attributes],
447+
);
437448
if (isModuleNamespaceObject(m)) {
438449
return m;
439450
}

src/module_wrap.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,12 +435,17 @@ static Local<Object> createImportAttributesContainer(
435435
values[idx] = raw_attributes->Get(realm->context(), i + 1).As<Value>();
436436
}
437437

438-
return Object::New(
438+
Local<Object> attributes = Object::New(
439439
isolate, Null(isolate), names.data(), values.data(), num_attributes);
440+
attributes->SetIntegrityLevel(realm->context(), v8::IntegrityLevel::kFrozen)
441+
.Check();
442+
return attributes;
440443
}
441444

442445
static Local<Array> createModuleRequestsContainer(
443446
Realm* realm, Isolate* isolate, Local<FixedArray> raw_requests) {
447+
EscapableHandleScope scope(isolate);
448+
Local<Context> context = realm->context();
444449
LocalVector<Value> requests(isolate, raw_requests->Length());
445450

446451
for (int i = 0; i < raw_requests->Length(); i++) {
@@ -467,11 +472,12 @@ static Local<Array> createModuleRequestsContainer(
467472

468473
Local<Object> request =
469474
Object::New(isolate, Null(isolate), names, values, arraysize(names));
475+
request->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen).Check();
470476

471477
requests[i] = request;
472478
}
473479

474-
return Array::New(isolate, requests.data(), requests.size());
480+
return scope.Escape(Array::New(isolate, requests.data(), requests.size()));
475481
}
476482

477483
void ModuleWrap::GetModuleRequests(const FunctionCallbackInfo<Value>& args) {

test/parallel/test-vm-module-errors.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,23 +237,29 @@ function checkInvalidCachedData() {
237237
}
238238

239239
function checkGettersErrors() {
240-
const expectedError = { code: 'ERR_INVALID_THIS' };
240+
const expectedError = { name: 'TypeError' };
241241
const getters = ['identifier', 'context', 'namespace', 'status', 'error'];
242242
getters.forEach((getter) => {
243243
assert.throws(() => {
244244
// eslint-disable-next-line no-unused-expressions
245245
Module.prototype[getter];
246-
}, expectedError);
246+
}, expectedError, `Module.prototype.${getter} should throw`);
247247
assert.throws(() => {
248248
// eslint-disable-next-line no-unused-expressions
249249
SourceTextModule.prototype[getter];
250-
}, expectedError);
250+
}, expectedError, `SourceTextModule.prototype.${getter} should throw`);
251+
});
252+
253+
const sourceTextModuleGetters = [
254+
'moduleRequests',
255+
'dependencySpecifiers',
256+
];
257+
sourceTextModuleGetters.forEach((getter) => {
258+
assert.throws(() => {
259+
// eslint-disable-next-line no-unused-expressions
260+
SourceTextModule.prototype[getter];
261+
}, expectedError, `SourceTextModule.prototype.${getter} should throw`);
251262
});
252-
// `dependencySpecifiers` getter is just part of SourceTextModule
253-
assert.throws(() => {
254-
// eslint-disable-next-line no-unused-expressions
255-
SourceTextModule.prototype.dependencySpecifiers;
256-
}, expectedError);
257263
}
258264

259265
const finished = common.mustCall();
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
// Flags: --experimental-vm-modules
4+
5+
require('../common');
6+
const assert = require('node:assert');
7+
const {
8+
SourceTextModule,
9+
} = require('node:vm');
10+
const test = require('node:test');
11+
12+
test('SourceTextModule.moduleRequests should return module requests', (t) => {
13+
const m = new SourceTextModule(`
14+
import { foo } from './foo.js';
15+
import { bar } from './bar.json' with { type: 'json' };
16+
import { quz } from './quz.js' with { attr1: 'quz' };
17+
import { quz as quz2 } from './quz.js' with { attr2: 'quark', attr3: 'baz' };
18+
export { foo, bar, quz, quz2 };
19+
`);
20+
21+
const requests = m.moduleRequests;
22+
assert.strictEqual(requests.length, 4);
23+
assert.deepStrictEqual(requests[0], {
24+
__proto__: null,
25+
specifier: './foo.js',
26+
attributes: {
27+
__proto__: null,
28+
},
29+
});
30+
assert.deepStrictEqual(requests[1], {
31+
__proto__: null,
32+
specifier: './bar.json',
33+
attributes: {
34+
__proto__: null,
35+
type: 'json'
36+
},
37+
});
38+
assert.deepStrictEqual(requests[2], {
39+
__proto__: null,
40+
specifier: './quz.js',
41+
attributes: {
42+
__proto__: null,
43+
attr1: 'quz',
44+
},
45+
});
46+
assert.deepStrictEqual(requests[3], {
47+
__proto__: null,
48+
specifier: './quz.js',
49+
attributes: {
50+
__proto__: null,
51+
attr2: 'quark',
52+
attr3: 'baz',
53+
},
54+
});
55+
56+
// Check the deprecated dependencySpecifiers property.
57+
// The dependencySpecifiers items are not unique.
58+
assert.deepStrictEqual(m.dependencySpecifiers, [
59+
'./foo.js',
60+
'./bar.json',
61+
'./quz.js',
62+
'./quz.js',
63+
]);
64+
});
65+
66+
test('SourceTextModule.moduleRequests items are frozen', (t) => {
67+
const m = new SourceTextModule(`
68+
import { foo } from './foo.js';
69+
`);
70+
71+
const requests = m.moduleRequests;
72+
assert.strictEqual(requests.length, 1);
73+
74+
const propertyNames = ['specifier', 'attributes'];
75+
for (const propertyName of propertyNames) {
76+
assert.throws(() => {
77+
requests[0][propertyName] = 'bar.js';
78+
}, {
79+
name: 'TypeError',
80+
});
81+
}
82+
assert.throws(() => {
83+
requests[0].attributes.type = 'json';
84+
}, {
85+
name: 'TypeError',
86+
});
87+
});

0 commit comments

Comments
 (0)