feat(ses): Support explicit exports of third-party modules
MODIFY
packages/ses/src/module-instance.js
packages/ses/src/module-instance.js
@@ -4,6 +4,67 @@ import { create, entries, keys, freeze, defineProperty } from './commons.js';
// q, for enquoting strings in error messages.
const q = JSON.stringify;
+export function makeThirdPartyModuleInstance(
+ compartmentPrivateFields,
+ staticModuleRecord,
+ compartment,
+ moduleAliases,
+ moduleSpecifier,
+ resolvedImports,
+) {
+ const { exportsProxy, proxiedExports, activate } = getDeferredExports(
+ compartment,
+ compartmentPrivateFields.get(compartment),
+ moduleAliases,
+ moduleSpecifier,
+ );
+
+ const notifiers = create(null);
+
+ staticModuleRecord.exports.forEach(name => {
+ let value = proxiedExports[name];
+ const updaters = [];
+
+ const get = () => value;
+
+ const set = newValue => {
+ value = newValue;
+ for (const updater of updaters) {
+ updater(newValue);
+ }
+ };
+
+ defineProperty(proxiedExports, name, {
+ get,
+ set,
+ enumerable: true,
+ configurable: false,
+ });
+
+ notifiers[name] = update => {
+ updaters.push(update);
+ update(value);
+ };
+ });
+
+ let activated = false;
+ return freeze({
+ notifiers,
+ exportsProxy,
+ execute() {
+ if (!activated) {
+ activate();
+ activated = true;
+ staticModuleRecord.execute(
+ proxiedExports,
+ compartment,
+ resolvedImports,
+ );
+ }
+ },
+ });
+}
+
// `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
@@ -228,6 +289,11 @@ export const makeModuleInstance = (
};
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)*]>
@@ -289,7 +355,8 @@ export const makeModuleInstance = (
// exported live binding state
let value;
- notify(v => (value = v));
+ const update = newValue => (value = newValue);
+ notify(update);
exportsProps[importName] = {
get() {
return value;

MODIFY
packages/ses/src/module-link.js
packages/ses/src/module-link.js
// module's "imports" with the more specific "resolvedImports" as inferred from
// the particular compartment's "resolveHook".
-import { makeModuleInstance } from './module-instance.js';
-import { getDeferredExports } from './module-proxy.js';
-import { entries, freeze } from './commons.js';
+import {
+ makeModuleInstance,
+ makeThirdPartyModuleInstance,
+} from './module-instance.js';
+import { entries } from './commons.js';
// q, as in quote, for quoting strings in error messages.
const q = JSON.stringify;
@@ -113,28 +115,15 @@ export const instantiate = (
globalObject,
);
} else {
- const { exportsProxy, proxiedExports, activate } = getDeferredExports(
+ moduleInstance = makeThirdPartyModuleInstance(
+ compartmentPrivateFields,
+ staticModuleRecord,
compartment,
- compartmentPrivateFields.get(compartment),
moduleAliases,
moduleSpecifier,
- );
- let activated = false;
- moduleInstance = freeze({
- exportsProxy,
- execute() {
- if (!activated) {
- activate();
- activated = true;
- staticModuleRecord.execute(
- proxiedExports,
- compartment,
resolvedImports,
);
}
- },
- });
- }
// Memoize.
instances.set(moduleSpecifier, moduleInstance);

MODIFY
packages/ses/test/test-import-cjs.js
packages/ses/test/test-import-cjs.js
@@ -3,15 +3,25 @@ import { resolveNode } from './node.js';
import '../ses.js';
import { freeze, keys } from '../src/commons.js';
-function heuristicRequires(moduleSource) {
+function heuristicAnalysis(moduleSource) {
const dependsUpon = {};
+ const exports = {};
moduleSource.replace(
/(?:^|[^\w$_.])require\s*\(\s*["']([^"']*)["']\s*\)/g,
(_, id) => {
dependsUpon[id] = true;
},
);
- return keys(dependsUpon);
+ moduleSource.replace(/(?:^|[^\w$_.])exports\.(\w[\w\d]*)\s*=/g, (_, name) => {
+ exports[name] = true;
+ });
+ moduleSource.replace(/(?:^|[^\w$_.])module\.exports\s*=/g, () => {
+ exports.default = true;
+ });
+ return {
+ imports: keys(dependsUpon),
+ exports: keys(exports),
+ };
}
const CjsStaticModuleRecord = (moduleSource, moduleLocation) => {
@@ -26,21 +36,19 @@ const CjsStaticModuleRecord = (moduleSource, moduleLocation) => {
);
}
- const imports = heuristicRequires(moduleSource);
- const execute = (exports, compartment, resolvedImports) => {
+ const { imports, exports } = heuristicAnalysis(moduleSource);
+
+ const execute = (moduleExports, compartment, resolvedImports) => {
const functor = compartment.evaluate(
`(function (require, exports, module, __filename, __dirname) { ${moduleSource} //*/\n})\n//# sourceURL=${moduleLocation}`,
);
- let moduleExports = exports;
-
const module = {
get exports() {
return moduleExports;
},
- set exports(namespace) {
- moduleExports = namespace;
- exports.default = namespace;
+ set exports(newModuleExports) {
+ moduleExports.default = newModuleExports;
},
};
@@ -54,13 +62,14 @@ const CjsStaticModuleRecord = (moduleSource, moduleLocation) => {
functor(
require,
- exports,
+ moduleExports,
module,
moduleLocation, // __filename
new URL('./', moduleLocation).toString(), // __dirname
);
};
- return freeze({ imports, execute });
+
+ return freeze({ imports, exports, execute });
};
test('import a CommonJS module with exports assignment', async t => {
@@ -169,9 +178,8 @@ test('CommonJS module imports CommonJS module as default', async t => {
};
const compartment = new Compartment({}, {}, { resolveHook, importHook });
- const {
- namespace: { default: odd },
- } = await compartment.import('./odd');
+ const { namespace } = await compartment.import('./odd');
+ const { default: odd } = namespace;
t.is(odd(1), true);
t.is(odd(2), false);
@@ -364,3 +372,44 @@ test('cross import ESM and CommonJS modules', async t => {
const compartment = new Compartment({ t }, {}, { resolveHook, importHook });
await compartment.import('./src/main.js');
});
+
+test('live bindings through through an ESM between CommonJS modules', async t => {
+ const resolveHook = resolveNode;
+ const importHook = async specifier => {
+ if (specifier === './src/main.js') {
+ return CjsStaticModuleRecord(
+ `
+ const dep = require("./intermediate.mjs");
+ t.is(dep.value, undefined);
+ dep.reanimate();
+ t.is(dep.check(), 30);
+ t.is(dep.value, 30);
+ `,
+ 'https://example.com/src/main.js',
+ );
+ }
+ if (specifier === './src/intermediate.mjs') {
+ return new StaticModuleRecord(`
+ export * from './value.js';
+ import { value } from './value.js';
+ export function check() {
+ return value;
+ }
+ `);
+ }
+ if (specifier === './src/value.js') {
+ return CjsStaticModuleRecord(
+ `
+ exports.reanimate = () => {
+ exports.value = 30;
+ };
+ `,
+ 'https://example.com/src/value.js',
+ );
+ }
+ throw new Error(`Cannot load module for specifier ${specifier}`);
+ };
+
+ const compartment = new Compartment({ t }, {}, { resolveHook, importHook });
+ await compartment.import('./src/main.js');
+});

MODIFY
packages/ses/test/test-import-gauntlet.js
packages/ses/test/test-import-gauntlet.js
@@ -196,3 +196,37 @@ test('live binding', async t => {
t.is(namespace.default, 'Hello, World!');
});
+
+test('live binding through reexporting intermediary', async t => {
+ t.plan(2);
+
+ const makeImportHook = makeNodeImporter({
+ 'https://example.com/import-live-export.js': `
+ export let quuux = null;
+ export function live() {
+ // Live binding of an exported variable.
+ quuux = 'Hello, World!';
+ }
+ `,
+ 'https://example.com/reexport-live-export.js': `
+ export * from './import-live-export.js';
+ `,
+ 'https://example.com/main.js': `
+ import { quuux, live } from './reexport-live-export.js';
+ t.is(quuux, null);
+ live();
+ t.is(quuux, 'Hello, World!');
+ `,
+ });
+
+ const compartment = new Compartment(
+ { t },
+ {},
+ {
+ resolveHook: resolveNode,
+ importHook: makeImportHook('https://example.com'),
+ },
+ );
+
+ await compartment.import('./main.js');
+});

feat(ses): Support explicit exports of third-party modules