Add deferred module exports namespace proxy
MODIFY
packages/ses/src/module-instance.js
packages/ses/src/module-instance.js
import { performEval } from './evaluate.js';
import { getCurrentRealmRec } from './realm-rec.js';
+import { getDeferredExports } from './module-proxy.js';
-const { create, entries, keys, freeze, seal, defineProperty: defProp } = Object;
+const { create, entries, keys, freeze, defineProperty: defProp } = Object;
// q, for enquoting strings in error messages.
const q = JSON.stringify;
-// `makeModuleInstance` takes a module's analysis (the private data of a module
-// static record), the live import namespace, and a global object, and produces
-// a module instance.
-// The module analysis is a subset of a module's compartment record.
-// The module instance carries the module exports namespace (the "module"),
-// notifiers to update the module's internal import namespace, and an
-// idempotent execute function.
-// The actual module namespace is a proxy that will delegate to the internal
-// module import namespace before the module executes.
+// `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 = (
- moduleAnalysis,
+ privateFields,
+ moduleAliases,
+ moduleRecord,
importedInstances,
globalObject,
) => {
const {
+ compartment,
+ moduleSpecifier,
functorSource,
fixedExportMap,
liveExportMap,
exportAlls,
- } = moduleAnalysis;
+ } = moduleRecord;
- // {_exportName_: getter} module exports namespace object
- const module = create(null);
- const moduleProps = create(null);
+ const { exportsProxy, proxiedExports, activate } = getDeferredExports(
+ compartment,
+ privateFields.get(compartment),
+ moduleAliases,
+ moduleSpecifier,
+ );
+
+ // {_exportName_: getter} module exports namespace
+ // object (eventually proxied).
+ const exportsProps = create(null);
// {_localName_: accessor} added to endowments for proxy traps
const trappers = create(null);
@@ -110,7 +120,7 @@ export const makeModuleInstance = (
onceVar[localName] = init;
}
- moduleProps[fixedExportName] = {
+ exportsProps[fixedExportName] = {
get: fixedGetNotify.get,
set: undefined,
enumerable: true,
@@ -199,7 +209,7 @@ export const makeModuleInstance = (
liveVar[localName] = update;
}
- moduleProps[liveExportName] = {
+ exportsProps[liveExportName] = {
get: liveGetNotify.get,
set: undefined,
enumerable: true,
@@ -211,7 +221,7 @@ export const makeModuleInstance = (
);
const notifyStar = update => {
- update(module);
+ update(proxiedExports);
};
notifiers['*'] = notifyStar;
@@ -263,7 +273,7 @@ export const makeModuleInstance = (
// exported live binding state
let value;
notify(v => (value = v));
- moduleProps[importName] = {
+ exportsProps[importName] = {
get() {
return value;
},
@@ -278,14 +288,17 @@ export const makeModuleInstance = (
// The module exports namespace will be wrapped in a module namespace
// exports proxy which will serve as a "module exports namespace exotic
// object".
- keys(moduleProps)
+ // 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.
+ keys(exportsProps)
.sort()
- .forEach(k => defProp(module, k, moduleProps[k]));
+ .forEach(k => defProp(proxiedExports, k, exportsProps[k]));
- // Finally, a module exports namespace is not extensible.
- // The module must be sealed before its namespace object can be used to
- // resolve the promise returned by a dynamic import.
- seal(module);
+ freeze(proxiedExports);
+ activate();
}
const realmRec = getCurrentRealmRec();
@@ -329,7 +342,7 @@ export const makeModuleInstance = (
return freeze({
notifiers,
- module,
+ exportsProxy,
execute,
});
};

MODIFY
packages/ses/src/module-link.js
packages/ses/src/module-link.js
+// For brevity, in this file, as in module-load.js, the term "moduleRecord"
+// without qualification means "module compartment record".
+// This is a super-set of the "module static record", that is reusable between
+// compartments with different hooks.
+// The "module compartment record" captures the compartment and overlays the
+// module's "imports" with the more specific "resolvedImports" as inferred from
+// the particular compartment's "resolveHook".
+
import { makeModuleInstance } from './module-instance.js';
const { entries } = Object;
@@ -12,6 +20,7 @@ const q = JSON.stringify;
// the actual `ModuleNamespace`.
export const link = (
compartmentPrivateFields,
+ moduleAliases,
compartment,
moduleSpecifier,
) => {
@@ -26,10 +35,14 @@ export const link = (
// compartment is in context: the module record may be in another
// compartment, denoted by moduleRecord.compartment.
// eslint-disable-next-line no-use-before-define
- return instantiate(compartmentPrivateFields, moduleRecord);
+ return instantiate(compartmentPrivateFields, moduleAliases, moduleRecord);
};
-export const instantiate = (compartmentPrivateFields, moduleRecord) => {
+export const instantiate = (
+ compartmentPrivateFields,
+ moduleAliases,
+ moduleRecord,
+) => {
const { compartment, moduleSpecifier, resolvedImports } = moduleRecord;
const { globalObject, instances } = compartmentPrivateFields.get(compartment);
@@ -40,6 +53,8 @@ export const instantiate = (compartmentPrivateFields, moduleRecord) => {
const importedInstances = new Map();
const moduleInstance = makeModuleInstance(
+ compartmentPrivateFields,
+ moduleAliases,
moduleRecord,
importedInstances,
globalObject,
@@ -52,6 +67,7 @@ export const instantiate = (compartmentPrivateFields, moduleRecord) => {
for (const [importSpecifier, resolvedSpecifier] of entries(resolvedImports)) {
const importedInstance = link(
compartmentPrivateFields,
+ moduleAliases,
compartment,
resolvedSpecifier,
);

MODIFY
packages/ses/src/module-load.js
packages/ses/src/module-load.js
+// For brevity, in this file, as in module-link.js, the term "moduleRecord"
+// without qualification means "module compartment record".
+// This is a super-set of the "module static record", that is reusable between
+// compartments with different hooks.
+// The "module compartment record" captures the compartment and overlays the
+// module's "imports" with the more specific "resolvedImports" as inferred from
+// the particular compartment's "resolveHook".
+
const { create, keys, values, freeze } = Object;
// `makeAlias` constructs compartment specifier tuples for the `aliases`

ADD
packages/ses/src/module-proxy.js
+// Compartments need a mechanism to link a module from one compartment
+// to another.
+// The procedure is to use `compartment.module(specifier)` to obtain the module
+// exports namespace from one compartment, possibly before importing or even
+// merely loading module, and threading it into the module map of another
+// compartment.
+// For this to be possible, it is necessary to model the module exports
+// namespace as a proxy that will treat all access to those exported properties
+// as reference errors until the properties of the actual module are known.
+// This provides the mechanism for modeling the public exports proxy
+// and eventually connecting it to to the proxied exports.
+
+import { makeAlias } from './module-load.js';
+
+const { create, freeze } = Object;
+// q, as in quote, for error messages.
+const q = JSON.stringify;
+
+// `deferExports` creates a module's exports proxy, proxied exports, and
+// activator.
+// A `Compartment` can create a module for any module specifier, regardless of
+// whether it is loadable or executable, and use that object as a token that
+// can be fed into another compartment's module map.
+// Only after the specified module has been analyzed is it possible for the
+// module namespace proxy to behave properly, so it throws exceptions until
+// after the compartment has begun executing the module.
+// The module instance must freeze the proxied exports and activate the exports
+// proxy before executing the module.
+//
+// The module exports proxy's behavior differs from the ECMAScript 262
+// specification for "module namespace exotic objects" only in that according
+// to the specification value property descriptors have a non-writable "value"
+// and this implementation models all properties with accessors.
+//
+// https://tc39.es/ecma262/#sec-module-namespace-exotic-objects
+//
+export const deferExports = () => {
+ let active = false;
+ const proxiedExports = create(null);
+ return freeze({
+ activate() {
+ active = true;
+ },
+ proxiedExports,
+ exportsProxy: new Proxy(proxiedExports, {
+ get(_target, name, receiver) {
+ if (!active) {
+ throw new TypeError(
+ `Cannot get property ${q(
+ name,
+ )} of module exports namespace, the module has not yet begun to execute`,
+ );
+ }
+ return Reflect.get(proxiedExports, name, receiver);
+ },
+ set(_target, name, _value) {
+ throw new TypeError(
+ `Cannot set property ${q(name)} of module exports namespace`,
+ );
+ },
+ has(_target, name) {
+ if (!active) {
+ throw new TypeError(
+ `Cannot check property ${q(
+ name,
+ )}, the module has not yet begun to execute`,
+ );
+ }
+ return Reflect.has(proxiedExports, name);
+ },
+ deleteProperty(_target, name) {
+ throw new TypeError(
+ `Cannot delete property ${q(name)}s of module exports namespace`,
+ );
+ },
+ ownKeys(_target) {
+ if (!active) {
+ throw new TypeError(
+ `Cannot enumerate keys, the module has not yet begun to execute`,
+ );
+ }
+ // return Object.keys(proxiedExports);
+ return Reflect.ownKeys(proxiedExports);
+ },
+ getOwnPropertyDescriptor(_target, name) {
+ if (!active) {
+ throw new TypeError(
+ `Cannot get own property descriptor ${q(
+ name,
+ )}, the module has not yet begun to execute`,
+ );
+ }
+ return Reflect.getOwnPropertyDescriptor(proxiedExports, name);
+ },
+ preventExtensions(_target) {
+ if (!active) {
+ throw new TypeError(
+ `Cannot prevent extensions of module exports namespace, the module has not yet begun to execute`,
+ );
+ }
+ return Reflect.preventExtensions(proxiedExports);
+ },
+ isExtensible() {
+ if (!active) {
+ throw new TypeError(
+ `Cannot check extensibility of module exports namespace, the module has not yet begun to execute`,
+ );
+ }
+ return Reflect.isExtensible(proxiedExports);
+ },
+ getPrototypeOf(_target) {
+ return null;
+ },
+ setPrototypeOf(_target, _proto) {
+ throw new TypeError(`Cannot set prototype of module exports namespace`);
+ },
+ defineProperty(_target, name, _descriptor) {
+ throw new TypeError(
+ `Cannot define property ${q(name)} of module exports namespace`,
+ );
+ },
+ apply(_target, _thisArg, _args) {
+ throw new TypeError(
+ `Cannot call module exports namespace, it is not a function`,
+ );
+ },
+ construct(_target, _args) {
+ throw new TypeError(
+ `Cannot construct module exports namespace, it is not a constructor`,
+ );
+ },
+ }),
+ });
+};
+
+// `getDeferredExports` memoizes the creation of a deferred module exports
+// namespace proxy for any abritrary full specifier in a compartment.
+// It also records the compartment and specifier affiliated with that module
+// exports namespace proxy so it can be used as an alias into another
+// compartment when threaded through a compartment's `moduleMap` argument.
+export const getDeferredExports = (
+ compartment,
+ compartmentPrivateFields,
+ moduleAliases,
+ specifier,
+) => {
+ const { deferredExports } = compartmentPrivateFields;
+ if (!deferredExports.has(specifier)) {
+ const deferred = deferExports();
+ moduleAliases.set(deferred.exportsProxy, makeAlias(compartment, specifier));
+ deferredExports.set(specifier, deferred);
+ }
+ return deferredExports.get(specifier);
+};

ADD
packages/ses/test/module-proxy.test.js
+import tap from 'tap';
+import { deferExports } from '../src/module-proxy.js';
+
+const { test } = tap;
+const { keys, seal, isExtensible } = Object;
+
+test('proxied exports keys are readable', t => {
+ t.plan(2);
+ const { proxiedExports, exportsProxy, activate } = deferExports();
+ t.throws(
+ () => {
+ keys(exportsProxy);
+ },
+ /^Cannot enumerate keys/,
+ 'keys fails for inactive module',
+ );
+ proxiedExports.a = 10;
+ proxiedExports.b = 20;
+ activate();
+ t.deepEqual(keys(exportsProxy), ['a', 'b']);
+});
+
+test('proxied exports is not extensible', t => {
+ t.plan(1);
+ const { proxiedExports, exportsProxy, activate } = deferExports();
+ seal(proxiedExports);
+ activate();
+ t.ok(
+ !isExtensible(exportsProxy),
+ 'sealed module means sealed proxied exports',
+ );
+});
+
+test('proxied exports has own keys', t => {
+ t.plan(3);
+ const { proxiedExports, exportsProxy, activate } = deferExports();
+ t.throws(
+ () => {
+ 'irrelevant' in exportsProxy;
+ },
+ /^Cannot check property/,
+ 'module must throw error for owns trap before it begins executing',
+ );
+ proxiedExports.present = 'here';
+ activate(seal());
+ t.ok('present' in exportsProxy, 'module has key');
+ t.ok(!('absent' in exportsProxy), 'module does not have key');
+});
+
+test('proxied exports set/get round-trip', t => {
+ t.plan(3);
+ const { proxiedExports, exportsProxy, activate } = deferExports();
+ t.throws(
+ () => {
+ exportsProxy.ceciNEstPasUnePipe;
+ },
+ /^Cannot get property/,
+ 'properties must not be known until execution begins',
+ );
+ t.throws(
+ () => {
+ exportsProxy.ceciNEstPasUnePipe = 'pipe';
+ },
+ /^Cannot set property/,
+ 'properties must not be mutable',
+ );
+
+ proxiedExports.ceciNEstPasUnePipe = 'pipe';
+ seal(proxiedExports);
+ activate();
+
+ t.throws(
+ () => {
+ exportsProxy.ceciNEstPasUnePipe = 'not a pipe';
+ },
+ /^Cannot set property/,
+ 'properties must not be mutable, even after activation',
+ );
+});
+
+test('proxied exports delete', t => {
+ t.plan(2);
+ const { exportsProxy, activate } = deferExports();
+ t.throws(
+ () => {
+ delete exportsProxy.existentialDread;
+ },
+ /^Cannot delete property/,
+ 'deleting before existing throws',
+ );
+ activate();
+ t.throws(
+ () => {
+ delete exportsProxy.cogitoErgoSum;
+ },
+ /^Cannot delete property/,
+ 'deleting from a sealed proxy',
+ );
+});
+
+test('proxied exports prototype', t => {
+ t.plan(1);
+ const { exportsProxy } = deferExports();
+ t.equals(
+ Object.getPrototypeOf(exportsProxy),
+ null,
+ 'prototype of module exports namespace must be null',
+ );
+});
+
+test('proxied exports is not a function', t => {
+ t.plan(1);
+ const { exportsProxy } = deferExports();
+ t.throws(
+ () => {
+ exportsProxy();
+ },
+ /is not a function$/,
+ 'proxied exports must not be callable',
+ );
+});
+
+test('proxied exports is not a constructor', t => {
+ t.plan(1);
+ const { exportsProxy } = deferExports();
+ t.throws(
+ () => {
+ const Constructor = exportsProxy;
+ return new Constructor();
+ },
+ /is not a constructor$/,
+ 'proxied exports must not be constructable',
+ );
+});

Add deferred module exports namespace proxy (#287)