Skip to content

Commit 7f1891d

Browse files
committed
vm: expose import phase on SourceTextModule.moduleRequests
1 parent 100c6da commit 7f1891d

File tree

6 files changed

+210
-38
lines changed

6 files changed

+210
-38
lines changed

doc/api/vm.md

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -575,16 +575,6 @@ const contextifiedObject = vm.createContext({
575575
})();
576576
```
577577

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

590580
* {any}
@@ -889,6 +879,36 @@ const cachedData = module.createCachedData();
889879
const module2 = new vm.SourceTextModule('const a = 1;', { cachedData });
890880
```
891881
882+
### `sourceTextModule.dependencySpecifiers`
883+
884+
<!-- YAML
885+
changes:
886+
- version: REPLACEME
887+
pr-url: https://github.com/nodejs/node/pull/20300
888+
description: This is deprecated in favour of `sourceTextModule.moduleRequests`.
889+
-->
890+
891+
> Stability: 0 - Deprecated: Use [`sourceTextModule.moduleRequests`][] instead.
892+
893+
* {string\[]}
894+
895+
The specifiers of all dependencies of this module. The returned array is frozen
896+
to disallow any changes to it.
897+
898+
Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in
899+
the ECMAScript specification.
900+
901+
### `sourceTextModule.moduleRequests`
902+
903+
<!-- YAML
904+
added: REPLACEME
905+
-->
906+
907+
* {ModuleRequest\[]} Dependencies of this module.
908+
909+
The requested import dependencies of this module. The returned array is frozen
910+
to disallow any changes to it.
911+
892912
## Class: `vm.SyntheticModule`
893913
894914
<!-- YAML
@@ -985,6 +1005,21 @@ const vm = require('node:vm');
9851005
})();
9861006
```
9871007
1008+
## Type: `ModuleRequest`
1009+
1010+
<!-- YAML
1011+
added: REPLACEME
1012+
-->
1013+
1014+
* {Object}
1015+
* `specifier` {string} The specifier of the requested module.
1016+
* `attributes` {Object} The `"with"` value passed to the
1017+
[WithClause][] in a [ImportDeclaration][], or an empty object if no value was
1018+
provided.
1019+
* `phase` {string} The phase of the requested module (`"source"` or `"evaluation"`).
1020+
1021+
A `ModuleRequest` represents the request to import a module with given import attributes and phase.
1022+
9881023
## `vm.compileFunction(code[, params[, options]])`
9891024
9901025
<!-- YAML
@@ -1958,12 +1993,14 @@ const { Script, SyntheticModule } = require('node:vm');
19581993
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
19591994
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
19601995
[HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule
1996+
[ImportDeclaration]: https://tc39.es/ecma262/#prod-ImportDeclaration
19611997
[Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
19621998
[Module Record]: https://262.ecma-international.org/14.0/#sec-abstract-module-records
19631999
[Source Text Module Record]: https://tc39.es/ecma262/#sec-source-text-module-records
19642000
[Support of dynamic `import()` in compilation APIs]: #support-of-dynamic-import-in-compilation-apis
19652001
[Synthetic Module Record]: https://heycam.github.io/webidl/#synthetic-module-records
19662002
[V8 Embedder's Guide]: https://v8.dev/docs/embed#contexts
2003+
[WithClause]: https://tc39.es/ecma262/#prod-WithClause
19672004
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag
19682005
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.md#err_vm_dynamic_import_callback_missing
19692006
[`ERR_VM_MODULE_STATUS`]: errors.md#err_vm_module_status
@@ -1973,6 +2010,7 @@ const { Script, SyntheticModule } = require('node:vm');
19732010
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
19742011
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
19752012
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
2013+
[`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests
19762014
[`url.origin`]: url.md#urlorigin
19772015
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
19782016
[`vm.constants.DONT_CONTEXTIFY`]: #vmconstantsdont_contextify

lib/internal/vm/module.js

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const {
6262
kEvaluated,
6363
kErrored,
6464
kSourcePhase,
65+
kEvaluationPhase,
6566
} = binding;
6667

6768
const STATUS_MAP = {
@@ -90,6 +91,15 @@ function isModule(object) {
9091
return true;
9192
}
9293

94+
function phaseEnumToPhaseName(phase) {
95+
if (phase === kSourcePhase) {
96+
return 'source';
97+
} else if (phase === kEvaluationPhase) {
98+
return 'evaluation';
99+
}
100+
assert.fail(`Invalid phase: ${phase}`);
101+
}
102+
93103
class Module {
94104
constructor(options) {
95105
emitExperimentalWarning('VM Modules');
@@ -252,13 +262,15 @@ class Module {
252262
}
253263
}
254264

255-
const kDependencySpecifiers = Symbol('kDependencySpecifiers');
256265
const kNoError = Symbol('kNoError');
257266

258267
class SourceTextModule extends Module {
259268
#error = kNoError;
260269
#statusOverride;
261270

271+
#moduleRequests;
272+
#dependencySpecifiers;
273+
262274
constructor(sourceText, options = kEmptyObject) {
263275
validateString(sourceText, 'sourceText');
264276
validateObject(options, 'options');
@@ -299,20 +311,26 @@ class SourceTextModule extends Module {
299311
importModuleDynamically,
300312
});
301313

302-
this[kDependencySpecifiers] = undefined;
314+
this.#moduleRequests = ObjectFreeze(ArrayPrototypeMap(this[kWrap].getModuleRequests(), (request) => {
315+
return ObjectFreeze({
316+
__proto__: null,
317+
specifier: request.specifier,
318+
attributes: request.attributes,
319+
phase: phaseEnumToPhaseName(request.phase),
320+
});
321+
}));
303322
}
304323

305324
async [kLink](linker) {
306325
this.#statusOverride = 'linking';
307326

308-
const moduleRequests = this[kWrap].getModuleRequests();
309327
// Iterates the module requests and links with the linker.
310328
// Specifiers should be aligned with the moduleRequests array in order.
311-
const specifiers = Array(moduleRequests.length);
312-
const modulePromises = Array(moduleRequests.length);
329+
const specifiers = Array(this.#moduleRequests.length);
330+
const modulePromises = Array(this.#moduleRequests.length);
313331
// Iterates with index to avoid calling into userspace with `Symbol.iterator`.
314-
for (let idx = 0; idx < moduleRequests.length; idx++) {
315-
const { specifier, attributes } = moduleRequests[idx];
332+
for (let idx = 0; idx < this.#moduleRequests.length; idx++) {
333+
const { specifier, attributes } = this.#moduleRequests[idx];
316334

317335
const linkerResult = linker(specifier, this, {
318336
attributes,
@@ -350,16 +368,16 @@ class SourceTextModule extends Module {
350368
}
351369

352370
get dependencySpecifiers() {
353-
validateThisInternalField(this, kDependencySpecifiers, 'SourceTextModule');
354-
// TODO(legendecas): add a new getter to expose the import attributes as the value type
355-
// of [[RequestedModules]] is changed in https://tc39.es/proposal-import-attributes/#table-cyclic-module-fields.
356-
this[kDependencySpecifiers] ??= ObjectFreeze(
357-
ArrayPrototypeMap(this[kWrap].getModuleRequests(), (request) => request.specifier));
358-
return this[kDependencySpecifiers];
371+
this.#dependencySpecifiers ??= ObjectFreeze(
372+
ArrayPrototypeMap(this.#moduleRequests, (request) => request.specifier));
373+
return this.#dependencySpecifiers;
374+
}
375+
376+
get moduleRequests() {
377+
return this.#moduleRequests;
359378
}
360379

361380
get status() {
362-
validateThisInternalField(this, kDependencySpecifiers, 'SourceTextModule');
363381
if (this.#error !== kNoError) {
364382
return 'errored';
365383
}
@@ -370,7 +388,6 @@ class SourceTextModule extends Module {
370388
}
371389

372390
get error() {
373-
validateThisInternalField(this, kDependencySpecifiers, 'SourceTextModule');
374391
if (this.#error !== kNoError) {
375392
return this.#error;
376393
}
@@ -447,9 +464,12 @@ class SyntheticModule extends Module {
447464
*/
448465
function importModuleDynamicallyWrap(importModuleDynamically) {
449466
const importModuleDynamicallyWrapper = async (specifier, referrer, attributes, phase) => {
450-
const phaseString = phase === kSourcePhase ? 'source' : 'evaluation';
451-
const m = await ReflectApply(importModuleDynamically, this, [specifier, referrer, attributes,
452-
phaseString]);
467+
const phaseName = phaseEnumToPhaseName(phase);
468+
const m = await ReflectApply(
469+
importModuleDynamically,
470+
this,
471+
[specifier, referrer, attributes, phaseName],
472+
);
453473
if (isModuleNamespaceObject(m)) {
454474
if (phase === kSourcePhase) throw new ERR_VM_MODULE_NOT_MODULE();
455475
return m;

src/module_wrap.cc

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

451-
return Object::New(
451+
Local<Object> attributes = Object::New(
452452
isolate, Null(isolate), names.data(), values.data(), num_attributes);
453+
attributes->SetIntegrityLevel(realm->context(), v8::IntegrityLevel::kFrozen)
454+
.Check();
455+
return attributes;
453456
}
454457

455458
static Local<Array> createModuleRequestsContainer(
456459
Realm* realm, Isolate* isolate, Local<FixedArray> raw_requests) {
460+
EscapableHandleScope scope(isolate);
461+
Local<Context> context = realm->context();
457462
LocalVector<Value> requests(isolate, raw_requests->Length());
458463

459464
for (int i = 0; i < raw_requests->Length(); i++) {
@@ -483,11 +488,12 @@ static Local<Array> createModuleRequestsContainer(
483488

484489
Local<Object> request =
485490
Object::New(isolate, Null(isolate), names, values, arraysize(names));
491+
request->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen).Check();
486492

487493
requests[i] = request;
488494
}
489495

490-
return Array::New(isolate, requests.data(), requests.size());
496+
return scope.Escape(Array::New(isolate, requests.data(), requests.size()));
491497
}
492498

493499
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: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
'use strict';
2+
3+
// Flags: --experimental-vm-modules --js-source-phase-imports
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+
import source Module from './source-module';
19+
export { foo, bar, quz, quz2 };
20+
`);
21+
22+
const requests = m.moduleRequests;
23+
assert.strictEqual(requests.length, 5);
24+
assert.deepStrictEqual(requests[0], {
25+
__proto__: null,
26+
specifier: './foo.js',
27+
attributes: {
28+
__proto__: null,
29+
},
30+
phase: 'evaluation',
31+
});
32+
assert.deepStrictEqual(requests[1], {
33+
__proto__: null,
34+
specifier: './bar.json',
35+
attributes: {
36+
__proto__: null,
37+
type: 'json'
38+
},
39+
phase: 'evaluation',
40+
});
41+
assert.deepStrictEqual(requests[2], {
42+
__proto__: null,
43+
specifier: './quz.js',
44+
attributes: {
45+
__proto__: null,
46+
attr1: 'quz',
47+
},
48+
phase: 'evaluation',
49+
});
50+
assert.deepStrictEqual(requests[3], {
51+
__proto__: null,
52+
specifier: './quz.js',
53+
attributes: {
54+
__proto__: null,
55+
attr2: 'quark',
56+
attr3: 'baz',
57+
},
58+
phase: 'evaluation',
59+
});
60+
assert.deepStrictEqual(requests[4], {
61+
__proto__: null,
62+
specifier: './source-module',
63+
attributes: {
64+
__proto__: null,
65+
},
66+
phase: 'source',
67+
});
68+
69+
// Check the deprecated dependencySpecifiers property.
70+
// The dependencySpecifiers items are not unique.
71+
assert.deepStrictEqual(m.dependencySpecifiers, [
72+
'./foo.js',
73+
'./bar.json',
74+
'./quz.js',
75+
'./quz.js',
76+
'./source-module',
77+
]);
78+
});
79+
80+
test('SourceTextModule.moduleRequests items are frozen', (t) => {
81+
const m = new SourceTextModule(`
82+
import { foo } from './foo.js';
83+
`);
84+
85+
const requests = m.moduleRequests;
86+
assert.strictEqual(requests.length, 1);
87+
88+
const propertyNames = ['specifier', 'attributes', 'phase'];
89+
for (const propertyName of propertyNames) {
90+
assert.throws(() => {
91+
requests[0][propertyName] = 'bar.js';
92+
}, {
93+
name: 'TypeError',
94+
});
95+
}
96+
assert.throws(() => {
97+
requests[0].attributes.type = 'json';
98+
}, {
99+
name: 'TypeError',
100+
});
101+
});

0 commit comments

Comments
 (0)