Skip to content

Commit a1f4333

Browse files
legendecastargos
authored andcommitted
vm: expose import phase on SourceTextModule.moduleRequests
PR-URL: #58829 Refs: #37648 Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Marco Ippolito <[email protected]>
1 parent efe19b5 commit a1f4333

File tree

6 files changed

+260
-38
lines changed

6 files changed

+260
-38
lines changed

doc/api/vm.md

Lines changed: 94 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,82 @@ 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+
912+
For example, given a source text:
913+
914+
<!-- eslint-disable no-duplicate-imports -->
915+
916+
```mjs
917+
import foo from 'foo';
918+
import fooAlias from 'foo';
919+
import bar from './bar.js';
920+
import withAttrs from '../with-attrs.ts' with { arbitraryAttr: 'attr-val' };
921+
import source Module from 'wasm-mod.wasm';
922+
```
923+
924+
<!-- eslint-enable no-duplicate-imports -->
925+
926+
The value of the `sourceTextModule.moduleRequests` will be:
927+
928+
```js
929+
[
930+
{
931+
specifier: 'foo',
932+
attributes: {},
933+
phase: 'evaluation',
934+
},
935+
{
936+
specifier: 'foo',
937+
attributes: {},
938+
phase: 'evaluation',
939+
},
940+
{
941+
specifier: './bar.js',
942+
attributes: {},
943+
phase: 'evaluation',
944+
},
945+
{
946+
specifier: '../with-attrs.ts',
947+
attributes: { arbitraryAttr: 'attr-val' },
948+
phase: 'evaluation',
949+
},
950+
{
951+
specifier: 'wasm-mod.wasm',
952+
attributes: {},
953+
phase: 'source',
954+
},
955+
];
956+
```
957+
892958
## Class: `vm.SyntheticModule`
893959
894960
<!-- YAML
@@ -985,6 +1051,21 @@ const vm = require('node:vm');
9851051
})();
9861052
```
9871053
1054+
## Type: `ModuleRequest`
1055+
1056+
<!-- YAML
1057+
added: REPLACEME
1058+
-->
1059+
1060+
* {Object}
1061+
* `specifier` {string} The specifier of the requested module.
1062+
* `attributes` {Object} The `"with"` value passed to the
1063+
[WithClause][] in a [ImportDeclaration][], or an empty object if no value was
1064+
provided.
1065+
* `phase` {string} The phase of the requested module (`"source"` or `"evaluation"`).
1066+
1067+
A `ModuleRequest` represents the request to import a module with given import attributes and phase.
1068+
9881069
## `vm.compileFunction(code[, params[, options]])`
9891070
9901071
<!-- YAML
@@ -1958,12 +2039,14 @@ const { Script, SyntheticModule } = require('node:vm');
19582039
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
19592040
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
19602041
[HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule
2042+
[ImportDeclaration]: https://tc39.es/ecma262/#prod-ImportDeclaration
19612043
[Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
19622044
[Module Record]: https://262.ecma-international.org/14.0/#sec-abstract-module-records
19632045
[Source Text Module Record]: https://tc39.es/ecma262/#sec-source-text-module-records
19642046
[Support of dynamic `import()` in compilation APIs]: #support-of-dynamic-import-in-compilation-apis
19652047
[Synthetic Module Record]: https://heycam.github.io/webidl/#synthetic-module-records
19662048
[V8 Embedder's Guide]: https://v8.dev/docs/embed#contexts
2049+
[WithClause]: https://tc39.es/ecma262/#prod-WithClause
19672050
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag
19682051
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.md#err_vm_dynamic_import_callback_missing
19692052
[`ERR_VM_MODULE_STATUS`]: errors.md#err_vm_module_status
@@ -1973,6 +2056,7 @@ const { Script, SyntheticModule } = require('node:vm');
19732056
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
19742057
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
19752058
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
2059+
[`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests
19762060
[`url.origin`]: url.md#urlorigin
19772061
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
19782062
[`vm.constants.DONT_CONTEXTIFY`]: #vmconstantsdont_contextify

lib/internal/vm/module.js

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

6768
const STATUS_MAP = {
69+
__proto__: null,
6870
[kUninstantiated]: 'unlinked',
6971
[kInstantiating]: 'linking',
7072
[kInstantiated]: 'linked',
@@ -73,6 +75,12 @@ const STATUS_MAP = {
7375
[kErrored]: 'errored',
7476
};
7577

78+
const PHASE_MAP = {
79+
__proto__: null,
80+
[kSourcePhase]: 'source',
81+
[kEvaluationPhase]: 'evaluation',
82+
};
83+
7684
let globalModuleId = 0;
7785
const defaultModuleName = 'vm:module';
7886

@@ -90,6 +98,12 @@ function isModule(object) {
9098
return true;
9199
}
92100

101+
function phaseEnumToPhaseName(phase) {
102+
const phaseName = PHASE_MAP[phase];
103+
assert(phaseName !== undefined, `Invalid phase value: ${phase}`);
104+
return phaseName;
105+
}
106+
93107
class Module {
94108
constructor(options) {
95109
emitExperimentalWarning('VM Modules');
@@ -252,13 +266,15 @@ class Module {
252266
}
253267
}
254268

255-
const kDependencySpecifiers = Symbol('kDependencySpecifiers');
256269
const kNoError = Symbol('kNoError');
257270

258271
class SourceTextModule extends Module {
259272
#error = kNoError;
260273
#statusOverride;
261274

275+
#moduleRequests;
276+
#dependencySpecifiers;
277+
262278
constructor(sourceText, options = kEmptyObject) {
263279
validateString(sourceText, 'sourceText');
264280
validateObject(options, 'options');
@@ -299,20 +315,26 @@ class SourceTextModule extends Module {
299315
importModuleDynamically,
300316
});
301317

302-
this[kDependencySpecifiers] = undefined;
318+
this.#moduleRequests = ObjectFreeze(ArrayPrototypeMap(this[kWrap].getModuleRequests(), (request) => {
319+
return ObjectFreeze({
320+
__proto__: null,
321+
specifier: request.specifier,
322+
attributes: request.attributes,
323+
phase: phaseEnumToPhaseName(request.phase),
324+
});
325+
}));
303326
}
304327

305328
async [kLink](linker) {
306329
this.#statusOverride = 'linking';
307330

308-
const moduleRequests = this[kWrap].getModuleRequests();
309331
// Iterates the module requests and links with the linker.
310332
// Specifiers should be aligned with the moduleRequests array in order.
311-
const specifiers = Array(moduleRequests.length);
312-
const modulePromises = Array(moduleRequests.length);
333+
const specifiers = Array(this.#moduleRequests.length);
334+
const modulePromises = Array(this.#moduleRequests.length);
313335
// 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];
336+
for (let idx = 0; idx < this.#moduleRequests.length; idx++) {
337+
const { specifier, attributes } = this.#moduleRequests[idx];
316338

317339
const linkerResult = linker(specifier, this, {
318340
attributes,
@@ -350,16 +372,16 @@ class SourceTextModule extends Module {
350372
}
351373

352374
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];
375+
this.#dependencySpecifiers ??= ObjectFreeze(
376+
ArrayPrototypeMap(this.#moduleRequests, (request) => request.specifier));
377+
return this.#dependencySpecifiers;
378+
}
379+
380+
get moduleRequests() {
381+
return this.#moduleRequests;
359382
}
360383

361384
get status() {
362-
validateThisInternalField(this, kDependencySpecifiers, 'SourceTextModule');
363385
if (this.#error !== kNoError) {
364386
return 'errored';
365387
}
@@ -370,7 +392,6 @@ class SourceTextModule extends Module {
370392
}
371393

372394
get error() {
373-
validateThisInternalField(this, kDependencySpecifiers, 'SourceTextModule');
374395
if (this.#error !== kNoError) {
375396
return this.#error;
376397
}
@@ -447,9 +468,12 @@ class SyntheticModule extends Module {
447468
*/
448469
function importModuleDynamicallyWrap(importModuleDynamically) {
449470
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]);
471+
const phaseName = phaseEnumToPhaseName(phase);
472+
const m = await ReflectApply(
473+
importModuleDynamically,
474+
this,
475+
[specifier, referrer, attributes, phaseName],
476+
);
453477
if (isModuleNamespaceObject(m)) {
454478
if (phase === kSourcePhase) throw new ERR_VM_MODULE_NOT_MODULE();
455479
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();

0 commit comments

Comments
 (0)