node/lib/internal/vm/source_text_module.js
Gus Caplan 5e7946fe79 vm: refactor SourceTextModule
- Removes redundant `instantiate` method
- Refactors `link` to match the spec linking steps more accurately
- Removes URL validation from SourceTextModule specifiers
- DRYs some dynamic import logic

Closes: https://github.com/nodejs/node/issues/29030

Co-Authored-By: Michaël Zasso <targos@protonmail.com>

PR-URL: https://github.com/nodejs/node/pull/29776
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Minwoo Jung <minwoo@nodesource.com>
2019-10-02 15:54:35 -07:00

334 lines
8.4 KiB
JavaScript

'use strict';
const { Object, SafePromise } = primordials;
const { isModuleNamespaceObject } = require('internal/util/types');
const { isContext } = internalBinding('contextify');
const {
ERR_INVALID_ARG_TYPE,
ERR_VM_MODULE_ALREADY_LINKED,
ERR_VM_MODULE_DIFFERENT_CONTEXT,
ERR_VM_MODULE_LINKING_ERRORED,
ERR_VM_MODULE_NOT_MODULE,
ERR_VM_MODULE_STATUS,
} = require('internal/errors').codes;
const {
getConstructorOf,
customInspectSymbol,
emitExperimentalWarning
} = require('internal/util');
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 perContextModuleId = new WeakMap();
const wrapToModuleMap = new WeakMap();
const kNoError = Symbol('kNoError');
class SourceTextModule {
#wrap;
#identifier;
#context;
#dependencySpecifiers;
#statusOverride;
#error = kNoError;
constructor(source, options = {}) {
emitExperimentalWarning('vm.SourceTextModule');
validateString(source, 'source');
if (typeof options !== 'object' || options === null)
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
const {
context,
lineOffset = 0,
columnOffset = 0,
initializeImportMeta,
importModuleDynamically,
} = 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);
}
}
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);
}
let { identifier } = options;
if (identifier !== undefined) {
validateString(identifier, 'options.identifier');
} else if (context === undefined) {
identifier = `${defaultModuleName}(${globalModuleId++})`;
} else if (perContextModuleId.has(context)) {
const curId = perContextModuleId.get(context);
identifier = `${defaultModuleName}(${curId})`;
perContextModuleId.set(context, curId + 1);
} else {
identifier = `${defaultModuleName}(0)`;
perContextModuleId.set(context, 1);
}
this.#wrap = new ModuleWrap(
source, identifier, context,
lineOffset, columnOffset,
);
wrapToModuleMap.set(this.#wrap, this);
this.#identifier = identifier;
this.#context = context;
binding.callbackMap.set(this.#wrap, {
initializeImportMeta,
importModuleDynamically: importModuleDynamically ?
importModuleDynamicallyWrap(importModuleDynamically) :
undefined,
});
}
get status() {
try {
this.#error;
} catch {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this.#error !== kNoError) {
return 'errored';
}
if (this.#statusOverride) {
return this.#statusOverride;
}
return STATUS_MAP[this.#wrap.getStatus()];
}
get identifier() {
try {
return this.#identifier;
} catch {
throw new ERR_VM_MODULE_NOT_MODULE();
}
}
get context() {
try {
return this.#context;
} catch {
throw new ERR_VM_MODULE_NOT_MODULE();
}
}
get namespace() {
try {
this.#wrap;
} catch {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this.#wrap.getStatus() < kInstantiated) {
throw new ERR_VM_MODULE_STATUS('must not be unlinked or linking');
}
return this.#wrap.getNamespace();
}
get dependencySpecifiers() {
try {
this.#dependencySpecifiers;
} catch {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this.#dependencySpecifiers === undefined) {
this.#dependencySpecifiers = this.#wrap.getStaticDependencySpecifiers();
}
return this.#dependencySpecifiers;
}
get error() {
try {
this.#error;
} catch {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this.#error !== kNoError) {
return this.#error;
}
if (this.#wrap.getStatus() !== kErrored) {
throw new ERR_VM_MODULE_STATUS('must be errored');
}
return this.#wrap.getError();
}
async link(linker) {
try {
this.#link;
} catch {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (typeof linker !== 'function') {
throw new ERR_INVALID_ARG_TYPE('linker', 'function', linker);
}
if (this.status !== 'unlinked') {
throw new ERR_VM_MODULE_ALREADY_LINKED();
}
await this.#link(linker);
this.#wrap.instantiate();
}
#link = async function(linker) {
this.#statusOverride = 'linking';
const promises = this.#wrap.link(async (identifier) => {
const module = await linker(identifier, this);
try {
module.#wrap;
} catch {
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.#link(linker);
}
return module.#wrap;
});
try {
if (promises !== undefined) {
await SafePromise.all(promises);
}
} catch (e) {
this.#error = e;
throw e;
} finally {
this.#statusOverride = undefined;
}
};
async evaluate(options = {}) {
try {
this.#wrap;
} catch {
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.#wrap.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.#wrap.evaluate(timeout, breakOnSigint);
return { __proto__: null, result };
}
static importModuleDynamicallyWrap(importModuleDynamically) {
// Named declaration for function name
const importModuleDynamicallyWrapper = async (...args) => {
const m = await importModuleDynamically(...args);
if (isModuleNamespaceObject(m)) {
return m;
}
try {
m.#wrap;
} catch {
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (m.status === 'errored') {
throw m.error;
}
return m.namespace;
};
return importModuleDynamicallyWrapper;
}
[customInspectSymbol](depth, options) {
let ctor = getConstructorOf(this);
ctor = ctor === null ? SourceTextModule : ctor;
if (typeof depth === 'number' && depth < 0)
return options.stylize(`[${ctor.name}]`, 'special');
const o = Object.create({ constructor: ctor });
o.status = this.status;
o.identifier = this.identifier;
o.context = this.context;
return require('internal/util/inspect').inspect(o, options);
}
}
// Declared as static to allow access to #wrap
const importModuleDynamicallyWrap =
SourceTextModule.importModuleDynamicallyWrap;
delete SourceTextModule.importModuleDynamicallyWrap;
module.exports = {
SourceTextModule,
wrapToModuleMap,
importModuleDynamicallyWrap,
};