lts/lib/internal/vm/module.js
'use strict';
const assert = require('internal/assert');
const {
ArrayIsArray,
ObjectCreate,
ObjectDefineProperty,
SafePromise,
Symbol,
WeakMap,
} = primordials;
const { isContext } = internalBinding('contextify');
const {
isModuleNamespaceObject,
isArrayBufferView,
} = require('internal/util/types');
const {
getConstructorOf,
customInspectSymbol,
emitExperimentalWarning,
} = require('internal/util');
const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_VM_MODULE_ALREADY_LINKED,
ERR_VM_MODULE_DIFFERENT_CONTEXT,
ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA,
ERR_VM_MODULE_LINKING_ERRORED,
ERR_VM_MODULE_NOT_MODULE,
ERR_VM_MODULE_STATUS,
} = require('internal/errors').codes;
const {
validateInt32,
validateUint32,
validateString,
} = require('internal/validators');
const binding = internalBinding('module_wrap');
const {
ModuleWrap,
kUninstantiated,
kInstantiating,
kInstantiated,
kEvaluating,
kEvaluated,
kErrored,
} = binding;
const STATUS_MAP = {
[kUninstantiated]: 'unlinked',
[kInstantiating]: 'linking',
[kInstantiated]: 'linked',
[kEvaluating]: 'evaluating',
[kEvaluated]: 'evaluated',
[kErrored]: 'errored',
};
let globalModuleId = 0;
const defaultModuleName = 'vm:module';
const wrapToModuleMap = new WeakMap();
const kWrap = Symbol('kWrap');
const kContext = Symbol('kContext');
const kPerContextModuleId = Symbol('kPerContextModuleId');
const kLink = Symbol('kLink');
class Module {
constructor(options) {
emitExperimentalWarning('VM Modules');
if (new.target === Module) {
// eslint-disable-next-line no-restricted-syntax
throw new TypeError('Module is not a constructor');
}
const {
context,
sourceText,
syntheticExportNames,
syntheticEvaluationSteps,
} = options;
if (context !== undefined) {
if (typeof context !== 'object' || context === null) {
throw new ERR_INVALID_ARG_TYPE('options.context', 'Object', context);
}
if (!isContext(context)) {
throw new ERR_INVALID_ARG_TYPE('options.context', 'vm.Context',
context);
}
}
let { identifier } = options;
if (identifier !== undefined) {
validateString(identifier, 'options.identifier');
} else if (context === undefined) {
identifier = `${defaultModuleName}(${globalModuleId++})`;
} else if (context[kPerContextModuleId] !== undefined) {
const curId = context[kPerContextModuleId];
identifier = `${defaultModuleName}(${curId})`;
context[kPerContextModuleId] += 1;
} else {
identifier = `${defaultModuleName}(0)`;
ObjectDefineProperty(context, kPerContextModuleId, {
value: 1,
writable: true,
enumerable: false,
configurable: true,
});
}
if (sourceText !== undefined) {
this[kWrap] = new ModuleWrap(identifier, context, sourceText,
options.lineOffset, options.columnOffset,
options.cachedData);
binding.callbackMap.set(this[kWrap], {
initializeImportMeta: options.initializeImportMeta,
importModuleDynamically: options.importModuleDynamically ?
importModuleDynamicallyWrap(options.importModuleDynamically) :
undefined,
});
} else {
assert(syntheticEvaluationSteps);
this[kWrap] = new ModuleWrap(identifier, context,
syntheticExportNames,
syntheticEvaluationSteps);
}
wrapToModuleMap.set(this[kWrap], this);
this[kContext] = context;
}
get identifier() {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
return this[kWrap].url;
}
get context() {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
return this[kContext];
}
get namespace() {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this[kWrap].getStatus() < kInstantiated) {
throw new ERR_VM_MODULE_STATUS('must not be unlinked or linking');
}
return this[kWrap].getNamespace();
}
get status() {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
return STATUS_MAP[this[kWrap].getStatus()];
}
get error() {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this[kWrap].getStatus() !== kErrored) {
throw new ERR_VM_MODULE_STATUS('must be errored');
}
return this[kWrap].getError();
}
async link(linker) {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (typeof linker !== 'function') {
throw new ERR_INVALID_ARG_TYPE('linker', 'function', linker);
}
if (this.status === 'linked') {
throw new ERR_VM_MODULE_ALREADY_LINKED();
}
if (this.status !== 'unlinked') {
throw new ERR_VM_MODULE_STATUS('must be unlinked');
}
await this[kLink](linker);
this[kWrap].instantiate();
}
async evaluate(options = {}) {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (typeof options !== 'object' || options === null) {
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
}
let timeout = options.timeout;
if (timeout === undefined) {
timeout = -1;
} else {
validateUint32(timeout, 'options.timeout', true);
}
const { breakOnSigint = false } = options;
if (typeof breakOnSigint !== 'boolean') {
throw new ERR_INVALID_ARG_TYPE('options.breakOnSigint', 'boolean',
breakOnSigint);
}
const status = this[kWrap].getStatus();
if (status !== kInstantiated &&
status !== kEvaluated &&
status !== kErrored) {
throw new ERR_VM_MODULE_STATUS(
'must be one of linked, evaluated, or errored'
);
}
const result = this[kWrap].evaluate(timeout, breakOnSigint);
return { __proto__: null, result };
}
[customInspectSymbol](depth, options) {
let ctor = getConstructorOf(this);
ctor = ctor === null ? Module : ctor;
if (typeof depth === 'number' && depth < 0)
return options.stylize(`[${ctor.name}]`, 'special');
const o = ObjectCreate({ constructor: ctor });
o.status = this.status;
o.identifier = this.identifier;
o.context = this.context;
return require('internal/util/inspect').inspect(o, options);
}
}
const kDependencySpecifiers = Symbol('kDependencySpecifiers');
const kNoError = Symbol('kNoError');
class SourceTextModule extends Module {
#error = kNoError;
#statusOverride;
constructor(sourceText, options = {}) {
validateString(sourceText, 'sourceText');
if (typeof options !== 'object' || options === null) {
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
}
const {
lineOffset = 0,
columnOffset = 0,
initializeImportMeta,
importModuleDynamically,
context,
identifier,
cachedData,
} = options;
validateInt32(lineOffset, 'options.lineOffset');
validateInt32(columnOffset, 'options.columnOffset');
if (initializeImportMeta !== undefined &&
typeof initializeImportMeta !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'options.initializeImportMeta', 'function', initializeImportMeta);
}
if (importModuleDynamically !== undefined &&
typeof importModuleDynamically !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'options.importModuleDynamically', 'function',
importModuleDynamically);
}
if (cachedData !== undefined && !isArrayBufferView(cachedData)) {
throw new ERR_INVALID_ARG_TYPE(
'options.cachedData',
['Buffer', 'TypedArray', 'DataView'],
cachedData
);
}
super({
sourceText,
context,
identifier,
lineOffset,
columnOffset,
cachedData,
initializeImportMeta,
importModuleDynamically,
});
this[kLink] = async (linker) => {
this.#statusOverride = 'linking';
const promises = this[kWrap].link(async (identifier) => {
const module = await linker(identifier, this);
if (module[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (module.context !== this.context) {
throw new ERR_VM_MODULE_DIFFERENT_CONTEXT();
}
if (module.status === 'errored') {
throw new ERR_VM_MODULE_LINKING_ERRORED();
}
if (module.status === 'unlinked') {
await module[kLink](linker);
}
return module[kWrap];
});
try {
if (promises !== undefined) {
await SafePromise.all(promises);
}
} catch (e) {
this.#error = e;
throw e;
} finally {
this.#statusOverride = undefined;
}
};
this[kDependencySpecifiers] = undefined;
}
get dependencySpecifiers() {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this[kDependencySpecifiers] === undefined) {
this[kDependencySpecifiers] = this[kWrap].getStaticDependencySpecifiers();
}
return this[kDependencySpecifiers];
}
get status() {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this.#error !== kNoError) {
return 'errored';
}
if (this.#statusOverride) {
return this.#statusOverride;
}
return super.status;
}
get error() {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this.#error !== kNoError) {
return this.#error;
}
return super.error;
}
createCachedData() {
const { status } = this;
if (status === 'evaluating' ||
status === 'evaluated' ||
status === 'errored') {
throw new ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA();
}
return this[kWrap].createCachedData();
}
}
class SyntheticModule extends Module {
constructor(exportNames, evaluateCallback, options = {}) {
if (!ArrayIsArray(exportNames) ||
exportNames.some((e) => typeof e !== 'string')) {
throw new ERR_INVALID_ARG_TYPE('exportNames',
'Array of unique strings',
exportNames);
} else {
exportNames.forEach((name, i) => {
if (exportNames.indexOf(name, i + 1) !== -1) {
throw new ERR_INVALID_ARG_VALUE(`exportNames.${name}`,
name,
'is duplicated');
}
});
}
if (typeof evaluateCallback !== 'function') {
throw new ERR_INVALID_ARG_TYPE('evaluateCallback', 'function',
evaluateCallback);
}
if (typeof options !== 'object' || options === null) {
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
}
const { context, identifier } = options;
super({
syntheticExportNames: exportNames,
syntheticEvaluationSteps: evaluateCallback,
context,
identifier,
});
this[kLink] = () => this[kWrap].link(() => {
assert.fail('link callback should not be called');
});
}
setExport(name, value) {
if (this[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
validateString(name, 'name');
if (this[kWrap].getStatus() < kInstantiated) {
throw new ERR_VM_MODULE_STATUS('must be linked');
}
this[kWrap].setExport(name, value);
}
}
function importModuleDynamicallyWrap(importModuleDynamically) {
const importModuleDynamicallyWrapper = async (...args) => {
const m = await importModuleDynamically(...args);
if (isModuleNamespaceObject(m)) {
return m;
}
if (!m || m[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (m.status === 'errored') {
throw m.error;
}
return m.namespace;
};
return importModuleDynamicallyWrapper;
}
module.exports = {
Module,
SourceTextModule,
SyntheticModule,
importModuleDynamicallyWrap,
getModuleFromWrap: (wrap) => wrapToModuleMap.get(wrap),
};