ses
Version:
Hardened JavaScript for Fearless Cooperation
498 lines (450 loc) • 14.9 kB
JavaScript
/** @import {ModuleExportsNamespace} from '../types.js' */
import { assert } from './error/assert.js';
import { getDeferredExports } from './module-proxy.js';
import {
ReferenceError,
SyntaxError,
TypeError,
arrayForEach,
arrayIncludes,
arrayPush,
arraySome,
arraySort,
create,
defineProperty,
entries,
freeze,
isArray,
keys,
mapGet,
weakmapGet,
reflectHas,
assign,
} from './commons.js';
import { compartmentEvaluate } from './compartment-evaluate.js';
const { quote: q } = assert;
export const makeVirtualModuleInstance = (
compartmentPrivateFields,
moduleSource,
compartment,
moduleAliases,
moduleSpecifier,
resolvedImports,
) => {
const { exportsProxy, exportsTarget, activate } = getDeferredExports(
compartment,
weakmapGet(compartmentPrivateFields, compartment),
moduleAliases,
moduleSpecifier,
);
const notifiers = create(null);
if (moduleSource.exports) {
if (
!isArray(moduleSource.exports) ||
arraySome(moduleSource.exports, name => typeof name !== 'string')
) {
throw TypeError(
`SES virtual module source "exports" property must be an array of strings for module ${moduleSpecifier}`,
);
}
arrayForEach(moduleSource.exports, name => {
let value = exportsTarget[name];
const updaters = [];
const get = () => value;
const set = newValue => {
value = newValue;
for (const updater of updaters) {
updater(newValue);
}
};
defineProperty(exportsTarget, name, {
get,
set,
enumerable: true,
configurable: false,
});
notifiers[name] = update => {
arrayPush(updaters, update);
update(value);
};
});
// This is enough to support import * from cjs - the '*' field doesn't need to be in exports nor exportsTarget because import will only ever access it via notifiers
notifiers['*'] = update => {
update(exportsTarget);
};
}
const localState = {
activated: false,
};
return freeze({
notifiers,
exportsProxy,
execute() {
if (reflectHas(localState, 'errorFromExecute')) {
throw localState.errorFromExecute;
}
if (!localState.activated) {
activate();
localState.activated = true;
try {
// eslint-disable-next-line @endo/no-polymorphic-call
moduleSource.execute(exportsTarget, compartment, resolvedImports);
} catch (err) {
localState.errorFromExecute = err;
throw err;
}
}
},
});
};
// `makeModuleInstance` takes a module's compartment record, the live import
// namespace, and a global object; and produces a module instance.
// The module instance carries the proxied module exports namespace (the
// "exports"), notifiers to update the module's internal import namespace, and
// an idempotent execute function.
// The module exports namespace is a proxy to the proxied exports namespace
// that the execution of the module instance populates.
export const makeModuleInstance = (
privateFields,
moduleAliases,
moduleRecord,
importedInstances,
) => {
const {
compartment,
moduleSpecifier,
moduleSource,
importMeta: moduleRecordMeta,
} = moduleRecord;
const {
reexports: exportAlls = [],
__syncModuleProgram__: functorSource,
__fixedExportMap__: fixedExportMap = {},
__liveExportMap__: liveExportMap = {},
__reexportMap__: reexportMap = {},
__needsImport__: needsImport = false,
__needsImportMeta__: needsImportMeta = false,
__syncModuleFunctor__,
} = moduleSource;
const compartmentFields = weakmapGet(privateFields, compartment);
const { __shimTransforms__, resolveHook, importMetaHook, compartmentImport } =
compartmentFields;
const { exportsProxy, exportsTarget, activate } = getDeferredExports(
compartment,
compartmentFields,
moduleAliases,
moduleSpecifier,
);
// {_exportName_: getter} module exports namespace
// object (eventually proxied).
const exportsProps = create(null);
// {_localName_: accessor} proxy traps for moduleLexicals and live bindings.
// The moduleLexicals object is frozen and the corresponding properties of
// moduleLexicals must be immutable, so we copy the descriptors.
const moduleLexicals = create(null);
// {_localName_: init(initValue) -> initValue} used by the
// rewritten code to initialize exported fixed bindings.
const onceVar = create(null);
// {_localName_: update(newValue)} used by the rewritten code to
// both initialize and update live bindings.
const liveVar = create(null);
const importMeta = create(null);
if (moduleRecordMeta) {
assign(importMeta, moduleRecordMeta);
}
if (needsImportMeta && importMetaHook) {
importMetaHook(moduleSpecifier, importMeta);
}
/** @type {(fullSpecifier: string) => Promise<ModuleExportsNamespace>} */
let dynamicImport;
if (needsImport) {
/** @param {string} importSpecifier */
dynamicImport = async importSpecifier =>
compartmentImport(resolveHook(importSpecifier, moduleSpecifier));
}
// {_localName_: [{get, set, notify}]} used to merge all the export updaters.
const localGetNotify = create(null);
// {[importName: string]: notify(update(newValue))} Used by code that imports
// one of this module's exports, so that their update function will
// be notified when this binding is initialized or updated.
const notifiers = create(null);
arrayForEach(entries(fixedExportMap), ([fixedExportName, [localName]]) => {
let fixedGetNotify = localGetNotify[localName];
if (!fixedGetNotify) {
// fixed binding state
let value;
let tdz = true;
/** @type {null | Array<(value: any) => void>} */
let optUpdaters = [];
// tdz sensitive getter
const get = () => {
if (tdz) {
throw ReferenceError(`binding ${q(localName)} not yet initialized`);
}
return value;
};
// leave tdz once
const init = freeze(initValue => {
// init with initValue of a declared const binding, and return
// it.
if (!tdz) {
throw TypeError(
`Internal: binding ${q(localName)} already initialized`,
);
}
value = initValue;
const updaters = optUpdaters;
optUpdaters = null;
tdz = false;
for (const updater of updaters || []) {
updater(initValue);
}
return initValue;
});
// If still tdz, register update for notification later.
// Otherwise, update now.
const notify = updater => {
if (updater === init) {
// Prevent recursion.
return;
}
if (tdz) {
arrayPush(optUpdaters || [], updater);
} else {
updater(value);
}
};
// Need these for additional exports of the local variable.
fixedGetNotify = {
get,
notify,
};
localGetNotify[localName] = fixedGetNotify;
onceVar[localName] = init;
}
exportsProps[fixedExportName] = {
get: fixedGetNotify.get,
set: undefined,
enumerable: true,
configurable: false,
};
notifiers[fixedExportName] = fixedGetNotify.notify;
});
arrayForEach(
entries(liveExportMap),
([liveExportName, [localName, setProxyTrap]]) => {
let liveGetNotify = localGetNotify[localName];
if (!liveGetNotify) {
// live binding state
let value;
let tdz = true;
const updaters = [];
// tdz sensitive getter
const get = () => {
if (tdz) {
throw ReferenceError(
`binding ${q(liveExportName)} not yet initialized`,
);
}
return value;
};
// This must be usable locally for the translation of initializing
// a declared local live binding variable.
//
// For reexported variable, this is also an update function to
// register for notification with the downstream import, which we
// must assume to be live. Thus, it can be called independent of
// tdz but always leaves tdz. Such reexporting creates a tree of
// bindings. This lets the tree be hooked up even if the imported
// module instance isn't initialized yet, as may happen in cycles.
const update = freeze(newValue => {
value = newValue;
tdz = false;
for (const updater of updaters) {
updater(newValue);
}
});
// tdz sensitive setter
const set = newValue => {
if (tdz) {
throw ReferenceError(`binding ${q(localName)} not yet initialized`);
}
value = newValue;
for (const updater of updaters) {
updater(newValue);
}
};
// Always register the updater function.
// If not in tdz, also update now.
const notify = updater => {
if (updater === update) {
// Prevent recursion.
return;
}
arrayPush(updaters, updater);
if (!tdz) {
updater(value);
}
};
liveGetNotify = {
get,
notify,
};
localGetNotify[localName] = liveGetNotify;
if (setProxyTrap) {
defineProperty(moduleLexicals, localName, {
get,
set,
enumerable: true,
configurable: false,
});
}
liveVar[localName] = update;
}
exportsProps[liveExportName] = {
get: liveGetNotify.get,
set: undefined,
enumerable: true,
configurable: false,
};
notifiers[liveExportName] = liveGetNotify.notify;
},
);
const notifyStar = update => {
update(exportsTarget);
};
notifiers['*'] = notifyStar;
// Per the calling convention for the moduleFunctor generated from
// an ESM, the `imports` function gets called once up front
// to populate or arrange the population of imports and reexports.
// The generated code produces an `updateRecord`: the means for
// the linker to update the imports and exports of the module.
// The updateRecord must conform to moduleAnalysis.imports
// updateRecord = Map<specifier, importUpdaters>
// importUpdaters = Map<importName, [update(newValue)*]>
function imports(updateRecord) {
// By the time imports is called, the importedInstances should already be
// initialized with module instances that satisfy
// imports.
// importedInstances = Map[_specifier_, { notifiers, module, execute }]
// notifiers = { [importName: string]: notify(update(newValue))}
// export * cannot export default.
const candidateAll = create(null);
candidateAll.default = false;
for (const [specifier, importUpdaters] of updateRecord) {
const instance = mapGet(importedInstances, specifier);
// The module instance object is an internal literal, does not bind this,
// and never revealed outside the SES shim.
// There are two instantiation sites for instances and they are both in
// this module.
// eslint-disable-next-line @endo/no-polymorphic-call
instance.execute(); // bottom up cycle tolerant
const { notifiers: importNotifiers } = instance;
for (const [importName, updaters] of importUpdaters) {
const importNotify = importNotifiers[importName];
if (!importNotify) {
throw SyntaxError(
`The requested module '${specifier}' does not provide an export named '${importName}'`,
);
}
for (const updater of updaters) {
importNotify(updater);
}
}
if (arrayIncludes(exportAlls, specifier)) {
// Make all these imports candidates.
// Note names don't change in reexporting all
for (const [importAndExportName, importNotify] of entries(
importNotifiers,
)) {
if (candidateAll[importAndExportName] === undefined) {
candidateAll[importAndExportName] = importNotify;
} else {
// Already a candidate: remove ambiguity.
candidateAll[importAndExportName] = false;
}
}
}
if (reexportMap[specifier]) {
// Make named reexports candidates too.
for (const [localName, exportedName] of reexportMap[specifier]) {
candidateAll[exportedName] = importNotifiers[localName];
}
}
}
for (const [exportName, notify] of entries(candidateAll)) {
if (!notifiers[exportName] && notify !== false) {
notifiers[exportName] = notify;
// exported live binding state
let value;
const update = newValue => (value = newValue);
notify(update);
exportsProps[exportName] = {
get() {
return value;
},
set: undefined,
enumerable: true,
configurable: false,
};
}
}
// Sort the module exports namespace as per spec.
// The module exports namespace will be wrapped in a module namespace
// exports proxy which will serve as a "module exports namespace exotic
// object".
// Sorting properties is not generally reliable because some properties may
// be symbols, and symbols do not have an inherent relative order, but
// since all properties of the exports namespace must be keyed by a string
// and the string must correspond to a valid identifier, sorting these
// properties works for this specific case.
arrayForEach(arraySort(keys(exportsProps)), k =>
defineProperty(exportsTarget, k, exportsProps[k]),
);
freeze(exportsTarget);
activate();
}
let optFunctor;
if (__syncModuleFunctor__ !== undefined) {
optFunctor = __syncModuleFunctor__;
} else {
optFunctor = compartmentEvaluate(compartmentFields, functorSource, {
globalObject: compartment.globalThis,
transforms: __shimTransforms__,
__moduleShimLexicals__: moduleLexicals,
});
}
let didThrow = false;
let thrownError;
function execute() {
if (optFunctor) {
// uninitialized
const functor = optFunctor;
optFunctor = null;
// initializing - call with `this` of `undefined`.
try {
functor(
freeze({
imports: freeze(imports),
onceVar: freeze(onceVar),
liveVar: freeze(liveVar),
import: dynamicImport,
importMeta,
}),
);
} catch (e) {
didThrow = true;
thrownError = e;
}
// initialized
}
if (didThrow) {
throw thrownError;
}
}
return freeze({
notifiers,
exportsProxy,
execute,
});
};