feat(ses): Add moduleMapHook
Adds a `moduleMapHook` option to the `Compartment` constructor options.  The module map hook can return values for the `moduleMap` for any given module specifier, or return `undefined` to fall back to the `importHook`.  This allows for wildcard linkage to other compartments.
MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
@@ -2,7 +2,11 @@ User-visible changes in SES:
## Next release
-* No changes yet.
+* Adds a `moduleMapHook` option to the `Compartment` constructor options.
+ The module map hook can return values for the `moduleMap` for
+ any given module specifier, or return `undefined` to fall back to the
+ `importHook`.
+ This allows for wildcard linkage to other compartments.
## Release 0.10.1 (13-August-2020)

MODIFY
packages/ses/src/compartment-shim.js
packages/ses/src/compartment-shim.js
@@ -208,6 +208,7 @@ export const makeCompartmentConstructor = (intrinsics, nativeBrander) => {
globalLexicals = {},
resolveHook,
importHook,
+ moduleMapHook,
} = options;
const globalTransforms = [...transforms];
@@ -266,6 +267,7 @@ export const makeCompartmentConstructor = (intrinsics, nativeBrander) => {
resolveHook,
importHook,
moduleMap,
+ moduleMapHook,
moduleRecords,
deferredExports,
instances,

MODIFY
packages/ses/src/module-load.js
packages/ses/src/module-load.js
@@ -49,11 +49,15 @@ export const load = async (
resolveHook,
importHook,
moduleMap,
+ moduleMapHook,
moduleRecords,
} = compartmentPrivateFields.get(compartment);
- // Follow moduleMap.
- const aliasNamespace = moduleMap[moduleSpecifier];
+ // Follow moduleMap, or moduleMapHook if present.
+ let aliasNamespace = moduleMap[moduleSpecifier];
+ if (aliasNamespace === undefined && moduleMapHook !== undefined) {
+ aliasNamespace = moduleMapHook(moduleSpecifier);
+ }
if (typeof aliasNamespace === 'string') {
throw new TypeError(
`Cannot map module ${q(moduleSpecifier)} to ${q(

MODIFY
packages/ses/test/import.test.js
packages/ses/test/import.test.js
@@ -281,3 +281,135 @@ test('compartments with same sources do not share instances', async t => {
'different compartments with same sources do not share instances',
);
});
+
+const trimModuleSpecifierPrefix = (moduleSpecifier, prefix) => {
+ if (moduleSpecifier === prefix) {
+ return './index.js';
+ }
+ if (moduleSpecifier.startsWith(`${prefix}/`)) {
+ return `./${moduleSpecifier.slice(prefix.length + 1)}`;
+ }
+ return undefined;
+};
+
+test('module map hook', async t => {
+ t.plan(2);
+
+ const makeImportHook = makeNodeImporter({
+ 'https://example.com/main.js': `
+ import dependency from 'dependency';
+ import utility from 'dependency/utility.js';
+
+ t.equal(dependency, "dependency");
+ t.equal(utility, "utility");
+ `,
+ 'https://example.com/dependency/index.js': `
+ export default "dependency";
+ `,
+ 'https://example.com/dependency/utility.js': `
+ export default "utility";
+ `,
+ });
+
+ const dependency = new Compartment(
+ {},
+ {},
+ {
+ resolveHook: resolveNode,
+ importHook: makeImportHook('https://example.com/dependency'),
+ },
+ );
+
+ const compartment = new Compartment(
+ { t },
+ {},
+ {
+ resolveHook: resolveNode,
+ importHook: makeImportHook('https://example.com'),
+ moduleMapHook: moduleSpecifier => {
+ const remainder = trimModuleSpecifierPrefix(
+ moduleSpecifier,
+ 'dependency',
+ );
+ if (remainder) {
+ return dependency.module(remainder);
+ }
+ return undefined;
+ },
+ },
+ );
+
+ await compartment.import('./main.js');
+});
+
+test('mutual dependency between compartments', async t => {
+ t.plan(12);
+
+ const makeImportHook = makeNodeImporter({
+ 'https://example.com/main.js': `
+ import isEven from "even";
+ import isOdd from "odd";
+
+ for (const n of [0, 2, 4]) {
+ t.ok(isEven(n), \`\${n} should be even\`);
+ t.ok(!isOdd(n), \`\${n} should not be odd\`);
+ }
+ for (const n of [1, 3, 5]) {
+ t.ok(isOdd(n), \`\${n} should be odd\`);
+ t.ok(!isEven(n), \`\${n} should not be even\`);
+ }
+ `,
+ 'https://example.com/even/index.js': `
+ import isOdd from "odd";
+ export default n => n === 0 || isOdd(n - 1);
+ `,
+ 'https://example.com/odd/index.js': `
+ import isEven from "even";
+ export default n => n !== 0 && isEven(n - 1);
+ `,
+ });
+
+ const moduleMapHook = moduleSpecifier => {
+ // Mutual dependency ahead:
+ // eslint-disable-next-line no-use-before-define
+ for (const [prefix, compartment] of Object.entries({ even, odd })) {
+ const remainder = trimModuleSpecifierPrefix(moduleSpecifier, prefix);
+ if (remainder) {
+ return compartment.module(remainder);
+ }
+ }
+ return undefined;
+ };
+
+ const even = new Compartment(
+ {},
+ {},
+ {
+ resolveHook: resolveNode,
+ importHook: makeImportHook('https://example.com/even'),
+ moduleMapHook,
+ },
+ );
+
+ const odd = new Compartment(
+ {},
+ {},
+ {
+ resolveHook: resolveNode,
+ importHook: makeImportHook('https://example.com/odd'),
+ moduleMapHook,
+ },
+ );
+
+ const compartment = new Compartment(
+ { t },
+ {},
+ {
+ resolveHook: resolveNode,
+ importHook: makeImportHook('https://example.com'),
+ moduleMapHook,
+ },
+ );
+
+ await compartment.import('./main.js');
+});

feat(ses): Add moduleMapHook (#419)

Adds a `moduleMapHook` option to the `Compartment` constructor options.  The module map hook can return values for the `moduleMap` for any given module specifier, or return `undefined` to fall back to the `importHook`.  This allows for wildcard linkage to other compartments.