Repository: endojs/endo
Pull request #2380 (2024-07-25 00:16:06)

feat(ses): Opt out of namespace boxes
Refs: #400 

## Description

To reduce mismatched behavior between SES and XS, this change adds an option (to be removed at the next major version) that changes the compartment import method such that it returns a promise for an exports namespace instead of a promise for a box containing the imports namespace. This opts-in to the same thenable behavior as dynamic import in the language.

### Security Considerations

With this option, we must be weary of thenable modules, as we must be for thenable modules across the rest of the JavaScript ecosystem.

### Scaling Considerations

None.

### Documentation Considerations

This option will necessarily appear in all portable code examples on hardenedjs.org once it is available.

### Testing Considerations

Tests have been adjusted to take advantage of the new option, except legacy tests which remain to test the behavior without the option enabled.

### Compatibility Considerations

This is a backward compatible change. Namespace boxes are hereafter deprecated and we hope to remove this option on the next major release, making the new behavior mandatory or at least opt-out.

### Upgrade Considerations

None.


Patch:
diff --git a/packages/ses/NEWS.md b/packages/ses/NEWS.md
index 49604228a..10a93aa9a 100644
--- a/packages/ses/NEWS.md
+++ b/packages/ses/NEWS.md
@@ -25,6 +25,12 @@ User-visible changes in SES:
   the stacktrace line-numbers point back into the original
   source, as they do on Node without SES.
   
+- Adds a `__noNamespaceBox__` option that aligns the behavior of the `import`
+  method on SES `Compartment` with the behavior of XS and the behavior we will
+  champion for compartment standards.
+  All use of `Compartment` should migrate to use this option as the standard
+  behavior will be enabled by default with the next major version of SES.
+
 # v1.5.0 (2024-05-06)
 
 - Adds `importNowHook` to the `Compartment` options. The compartment will invoke the hook whenever it encounters a missing dependency while running `compartmentInstance.importNow(specifier)`, which cannot use an asynchronous `importHook`.
diff --git a/packages/ses/src/compartment.js b/packages/ses/src/compartment.js
index 8728a0b11..270c8203c 100644
--- a/packages/ses/src/compartment.js
+++ b/packages/ses/src/compartment.js
@@ -104,6 +104,8 @@ export const CompartmentPrototype = {
   },
 
   async import(specifier) {
+    const { noNamespaceBox } = weakmapGet(privateFields, this);
+
     if (typeof specifier !== 'string') {
       throw TypeError('first argument of import() must be a string');
     }
@@ -117,6 +119,11 @@ export const CompartmentPrototype = {
           /** @type {Compartment} */ (this),
           specifier,
         );
+        if (noNamespaceBox) {
+          return namespace;
+        }
+        // Legacy behavior: box the namespace object so that thenable modules
+        // do not get coerced into a promise accidentally.
         return { namespace };
       },
     );
@@ -172,7 +179,11 @@ export const makeCompartmentConstructor = (
   markVirtualizedNativeFunction,
   parentCompartment = undefined,
 ) => {
-  function Compartment(endowments = {}, moduleMap = {}, options = {}) {
+  function Compartment(
+    endowmentsOption = {},
+    moduleMapOption = {},
+    options = {},
+  ) {
     if (new.target === undefined) {
       throw TypeError(
         "Class constructor Compartment cannot be invoked without 'new'",
@@ -189,8 +200,11 @@ export const makeCompartmentConstructor = (
       importNowHook,
       moduleMapHook,
       importMetaHook,
+      __noNamespaceBox__: noNamespaceBox = false,
     } = options;
     const globalTransforms = [...transforms, ...__shimTransforms__];
+    const endowments = { __proto__: null, ...endowmentsOption };
+    const moduleMap = { __proto__: null, ...moduleMapOption };
 
     // Map<FullSpecifier, ModuleCompartmentRecord>
     const moduleRecords = new Map();
@@ -249,6 +263,7 @@ export const makeCompartmentConstructor = (
       deferredExports,
       instances,
       parentCompartment,
+      noNamespaceBox,
     });
   }
 


Test patch:
diff --git a/packages/ses/test/compartment-transforms.test.js b/packages/ses/test/compartment-transforms.test.js
index 04115b515..2e18ff708 100644
--- a/packages/ses/test/compartment-transforms.test.js
+++ b/packages/ses/test/compartment-transforms.test.js
@@ -52,9 +52,13 @@ test('transforms do not apply to imported modules', async t => {
   const resolveHook = () => '';
   const importHook = () =>
     new ModuleSource('export default "Farewell, World!";');
-  const c = new Compartment({}, {}, { transforms, resolveHook, importHook });
+  const c = new Compartment(
+    {},
+    {},
+    { transforms, resolveHook, importHook, __noNamespaceBox__: true },
+  );
 
-  const { namespace } = await c.import('any-string-here');
+  const namespace = await c.import('any-string-here');
   const { default: greeting } = namespace;
 
   t.is(greeting, 'Farewell, World!');
@@ -82,10 +86,15 @@ test('__shimTransforms__ do apply to imported modules', async t => {
   const c = new Compartment(
     {},
     {},
-    { __shimTransforms__: transforms, resolveHook, importHook },
+    {
+      __shimTransforms__: transforms,
+      __noNamespaceBox__: true,
+      resolveHook,
+      importHook,
+    },
   );
 
-  const { namespace } = await c.import('any-string-here');
+  const namespace = await c.import('any-string-here');
   const { default: greeting } = namespace;
 
   t.is(greeting, 'Hello, World!');
diff --git a/packages/ses/test/import-cjs.test.js b/packages/ses/test/import-cjs.test.js
index efea862a4..b80b47e92 100644
--- a/packages/ses/test/import-cjs.test.js
+++ b/packages/ses/test/import-cjs.test.js
@@ -86,11 +86,13 @@ test('import a CommonJS module with exports assignment', async t => {
     );
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
   const module = compartment.module('.');
-  const {
-    namespace: { meaning },
-  } = await compartment.import('.');
+  const { meaning } = await compartment.import('.');
 
   t.is(meaning, 42, 'exports seen');
   t.is(module.meaning, 42, 'exports seen through deferred proxy');
@@ -109,11 +111,13 @@ test('import a CommonJS module with exports replacement', async t => {
     );
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
   const module = compartment.module('.');
-  const {
-    namespace: { default: meaning },
-  } = await compartment.import('.');
+  const { default: meaning } = await compartment.import('.');
 
   t.is(meaning, 42, 'exports seen');
   t.is(module.default, 42, 'exports seen through deferred proxy');
@@ -144,10 +148,12 @@ test('CommonJS module imports CommonJS module by name', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -178,8 +184,12 @@ test('CommonJS module imports CommonJS module as default', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const { namespace } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const namespace = await compartment.import('./odd');
   const { default: odd } = namespace;
 
   t.is(odd(1), true);
@@ -211,10 +221,12 @@ test('ESM imports CommonJS module as default', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { default: odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { default: odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -245,10 +257,12 @@ test('ESM imports CommonJS module as star', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { default: odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { default: odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -281,10 +295,12 @@ test('ESM imports CommonJS module with replaced exports as star', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { default: odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { default: odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -315,10 +331,12 @@ test('ESM imports CommonJS module by name', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { default: odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { default: odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -349,10 +367,12 @@ test('CommonJS module imports ESM as default', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { default: odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { default: odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -385,10 +405,12 @@ test('CommonJS module imports ESM by name', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -440,7 +462,11 @@ test('cross import ESM and CommonJS modules', async t => {
     throw Error(`Cannot load module for specifier ${specifier}`);
   };
 
-  const compartment = new Compartment({ t }, {}, { resolveHook, importHook });
+  const compartment = new Compartment(
+    { t },
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
   await compartment.import('./src/main.js');
 });
 
diff --git a/packages/ses/test/import-gauntlet.test.js b/packages/ses/test/import-gauntlet.test.js
index 0135fd546..14bdc61da 100644
--- a/packages/ses/test/import-gauntlet.test.js
+++ b/packages/ses/test/import-gauntlet.test.js
@@ -49,10 +49,11 @@ test('import all from module', async t => {
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main.js');
+  const namespace = await compartment.import('./main.js');
 
   t.is(namespace.default.a, 10);
   t.is(namespace.default.b, 20);
@@ -78,10 +79,11 @@ test('import named exports from me', async t => {
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main.js');
+  const namespace = await compartment.import('./main.js');
 
   t.is(namespace.default.fizz, 10);
   t.is(namespace.default.buzz, 20);
@@ -106,10 +108,11 @@ test('import color from module', async t => {
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main.js');
+  const namespace = await compartment.import('./main.js');
 
   t.is(namespace.color, 'blue');
 });
@@ -132,10 +135,11 @@ test('import and reexport', async t => {
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main.js');
+  const namespace = await compartment.import('./main.js');
 
   t.is(namespace.qux, 42);
 });
@@ -159,10 +163,11 @@ test('import and export all', async t => {
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main.js');
+  const namespace = await compartment.import('./main.js');
 
   t.is(namespace.alpha, 0);
   t.is(namespace.omega, 23);
@@ -189,10 +194,11 @@ test('live binding', async t => {
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main.js');
+  const namespace = await compartment.import('./main.js');
 
   t.is(namespace.default, 'Hello, World!');
 });
diff --git a/packages/ses/test/import-hook.test.js b/packages/ses/test/import-hook.test.js
index 6a3cb4c8c..12b587000 100644
--- a/packages/ses/test/import-hook.test.js
+++ b/packages/ses/test/import-hook.test.js
@@ -23,9 +23,10 @@ test('import hook returns module source descriptor with precompiled module sourc
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -52,9 +53,10 @@ test('import hook returns module source descriptor with virtual module source',
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -100,9 +102,10 @@ test('import hook returns parent compartment module source descriptor with strin
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -182,9 +185,10 @@ test('import hook returns parent compartment module source reference with differ
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -205,12 +209,11 @@ test('import hook returns module source descriptor for parent compartment with s
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: parentObject },
-  } = await parent.import('./object.js');
+  const { default: parentObject } = await parent.import('./object.js');
   t.is(parentObject.meaning, 42);
 
   const compartment = new parent.globalThis.Compartment(
@@ -230,12 +233,11 @@ test('import hook returns module source descriptor for parent compartment with s
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: childObject },
-  } = await compartment.import('./index.js');
+  const { default: childObject } = await compartment.import('./index.js');
   t.is(childObject.meaning, 42);
   // Separate instances
   t.not(childObject, parentObject);
@@ -258,12 +260,11 @@ test('import hook returns parent compartment module namespace descriptor', async
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: parentObject },
-  } = await parent.import('./object.js');
+  const { default: parentObject } = await parent.import('./object.js');
   t.is(parentObject.meaning, 42);
 
   const compartment = new parent.globalThis.Compartment(
@@ -283,12 +284,11 @@ test('import hook returns parent compartment module namespace descriptor', async
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: childObject },
-  } = await compartment.import('./index.js');
+  const { default: childObject } = await compartment.import('./index.js');
   t.is(childObject.meaning, 42);
   // Same instances
   t.is(childObject, parentObject);
@@ -311,12 +311,11 @@ test('import hook returns module source descriptor with string reference to pare
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object1 },
-  } = await compartment1.import('./object.js');
+  const { default: object1 } = await compartment1.import('./object.js');
   t.is(object1.meaning, 42);
 
   const compartment2 = new Compartment(
@@ -336,12 +335,11 @@ test('import hook returns module source descriptor with string reference to pare
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object2 },
-  } = await compartment2.import('./index.js');
+  const { default: object2 } = await compartment2.import('./index.js');
   t.is(object2.meaning, 42);
   // Separate instances
   t.not(object1, object2);
@@ -364,12 +362,11 @@ test('import hook returns other compartment module namespace descriptor', async
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object1 },
-  } = await compartment1.import('./object.js');
+  const { default: object1 } = await compartment1.import('./object.js');
   t.is(object1.meaning, 42);
 
   const compartment2 = new Compartment(
@@ -389,12 +386,11 @@ test('import hook returns other compartment module namespace descriptor', async
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object2 },
-  } = await compartment2.import('./index.js');
+  const { default: object2 } = await compartment2.import('./index.js');
   t.is(object2.meaning, 42);
   // Same instances
   t.is(object1, object2);
@@ -413,9 +409,10 @@ test('import hook returns module namespace descriptor and namespace object', asy
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: namespace1 } = await compartment1.import('a');
+  const namespace1 = await compartment1.import('a');
   const compartment2 = new Compartment(
     {},
     {},
@@ -426,9 +423,10 @@ test('import hook returns module namespace descriptor and namespace object', asy
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: namespace2 } = await compartment2.import('z');
+  const namespace2 = await compartment2.import('z');
   t.is(namespace2.default, 42);
   t.is(namespace1, namespace2);
 });
@@ -444,9 +442,10 @@ test('import hook returns module namespace descriptor and non-namespace object',
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace } = await compartment.import('1');
+  const namespace = await compartment.import('1');
   t.is(namespace.meaning, 42);
 });
 
@@ -471,16 +470,13 @@ test('import hook returns module source descriptor for specifier in own compartm
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object1 },
-  } = await compartment.import('./object.js');
+  const { default: object1 } = await compartment.import('./object.js');
   t.is(object1.meaning, 42);
-  const {
-    namespace: { default: object2 },
-  } = await compartment.import('./index.js');
+  const { default: object2 } = await compartment.import('./index.js');
   t.is(object2.meaning, 42);
   // Separate instances
   t.not(object1, object2);
@@ -508,16 +504,13 @@ test('import hook returns module source descriptor for specifier in own compartm
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object1 },
-  } = await compartment.import('./object.js');
+  const { default: object1 } = await compartment.import('./object.js');
   t.is(object1.meaning, 42);
-  const {
-    namespace: { default: object2 },
-  } = await compartment.import('./index.js');
+  const { default: object2 } = await compartment.import('./index.js');
   t.is(object2.meaning, 42);
   // Fails to obtain separate instance due to specifier collison.
   t.is(object1, object2);
@@ -544,16 +537,13 @@ test('import hook returns module namespace descriptor for specifier in own compa
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object1 },
-  } = await compartment.import('./object.js');
+  const { default: object1 } = await compartment.import('./object.js');
   t.is(object1.meaning, 42);
-  const {
-    namespace: { default: object2 },
-  } = await compartment.import('./index.js');
+  const { default: object2 } = await compartment.import('./index.js');
   t.is(object2.meaning, 42);
   // Same instances
   t.is(object1, object2);
@@ -580,11 +570,10 @@ test('module map hook precedes import hook', async t => {
       importHook() {
         throw new Error('not reached');
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: meaning },
-  } = await compartment.import('./index.js');
+  const { default: meaning } = await compartment.import('./index.js');
   t.is(meaning, 42);
 });
diff --git a/packages/ses/test/import-legacy.test.js b/packages/ses/test/import-legacy.test.js
new file mode 100644
index 000000000..fa08cb231
--- /dev/null
+++ b/packages/ses/test/import-legacy.test.js
@@ -0,0 +1,651 @@
+// These tests exercise the Compartment import interface and linkage
+// between compartments, and Compartment endowments.
+
+/* eslint max-lines: 0 */
+
+import test from 'ava';
+import '../index.js';
+import { resolveNode, makeNodeImporter } from './node.js';
+import { makeImporter, makeStaticRetriever } from './import-commons.js';
+
+// This test demonstrates a system of modules in a single Compartment
+// that uses fully qualified URLs as module specifiers and module locations,
+// not distinguishing one from the other.
+test('import within one compartment, web resolution', async t => {
+  t.plan(1);
+
+  const retrieve = makeStaticRetriever({
+    'https://example.com/packages/example/half.js': `
+      export default 21;
+    `,
+    'https://example.com/packages/example/': `
+      import half from 'half.js';
+      export const meaning = double(half);
+    `,
+  });
+  const locate = moduleSpecifier => moduleSpecifier;
+  const resolveHook = (spec, referrer) => new URL(spec, referrer).toString();
+  const importHook = makeImporter(locate, retrieve);
+
+  const compartment = new Compartment(
+    // endowments:
+    {
+      double: n => n * 2,
+    },
+    // module map:
+    {},
+    // options:
+    {
+      resolveHook,
+      importHook,
+    },
+  );
+
+  const { namespace } = await compartment.import(
+    'https://example.com/packages/example/',
+  );
+
+  t.is(namespace.meaning, 42, 'dynamically imports the meaning');
+});
+
+// This case demonstrates the same arrangement except that the Compartment uses
+// Node.js module specifier resolution.
+test('import within one compartment, node resolution', async t => {
+  t.plan(1);
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/packages/example/half.js': `
+      export default 21;
+    `,
+    'https://example.com/packages/example/main.js': `
+      import half from './half.js';
+      export const meaning = double(half);
+    `,
+  });
+
+  const compartment = new Compartment(
+    // endowments:
+    {
+      double: n => n * 2,
+    },
+    // module map:
+    {},
+    // options:
+    {
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com/packages/example'),
+    },
+  );
+
+  const { namespace } = await compartment.import('./main.js');
+
+  t.is(namespace.meaning, 42, 'dynamically imports the meaning');
+});
+
+// This demonstrates a pair of linked Node.js compartments.
+test('two compartments, three modules, one endowment', async t => {
+  t.plan(1);
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/packages/example/half.js': `
+      if (typeof double !== 'undefined') {
+        throw Error('Unexpected leakage of double(n) endowment: ' + typeof double);
+      }
+      export default 21;
+    `,
+    'https://example.com/packages/example/main.js': `
+      import half from './half.js';
+      import double from 'double';
+      export const meaning = double(half);
+    `,
+    'https://example.com/packages/double/main.js': `
+      export default double;
+    `,
+  });
+
+  const doubleCompartment = new Compartment(
+    // endowments:
+    {
+      double: n => n * 2,
+    },
+    // module map:
+    {},
+    // options:
+    {
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com/packages/double'),
+    },
+  );
+
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // module map:
+    {
+      // Notably, this is the first case where we thread a depencency between
+      // two compartments, using the sigil of one's namespace to indicate
+      // linkage before the module has been loaded.
+      double: doubleCompartment.module('./main.js'),
+    },
+    // options:
+    {
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com/packages/example'),
+    },
+  );
+
+  const { namespace } = await compartment.import('./main.js');
+
+  t.is(namespace.meaning, 42, 'dynamically imports the meaning');
+});
+
+test('module exports namespace as an object', async t => {
+  t.plan(7);
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/packages/meaning/main.js': `
+      export const meaning = 42;
+    `,
+  });
+
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // module map:
+    {},
+    // options:
+    {
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com/packages/meaning'),
+    },
+  );
+
+  const { namespace } = await compartment.import('./main.js');
+
+  t.is(
+    namespace.meaning,
+    42,
+    'exported constant must have a namespace property',
+  );
+
+  t.throws(
+    () => {
+      namespace.alternateMeaning = 10;
+    },
+    { message: /^Cannot set property/ },
+  );
+
+  // The first should not throw.
+  t.truthy(
+    Reflect.preventExtensions(namespace),
+    'extensions must be preventable',
+  );
+  // The second should agree.
+  t.truthy(
+    Reflect.preventExtensions(namespace),
+    'preventing extensions must be idempotent',
+  );
+
+  const desc = Object.getOwnPropertyDescriptor(namespace, 'meaning');
+  t.is(
+    typeof desc,
+    'object',
+    'property descriptor for defined export must be an object',
+  );
+  t.is(desc.set, undefined, 'constant export must not be writeable');
+
+  t.is(
+    Object.getPrototypeOf(namespace),
+    null,
+    'module exports namespace prototype must be null',
+  );
+});
+
+test('modules are memoized', async t => {
+  t.plan(1);
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/packages/example/c-s-lewis.js': `
+      export const entity = {};
+    `,
+    'https://example.com/packages/example/clive-hamilton.js': `
+      import { entity } from './c-s-lewis.js';
+      export default entity;
+    `,
+    'https://example.com/packages/example/n-w-clerk.js': `
+      import { entity } from './c-s-lewis.js';
+      export default entity;
+    `,
+    'https://example.com/packages/example/main.js': `
+      import clive from './clive-hamilton.js';
+      import clerk from './n-w-clerk.js';
+      export default { clerk, clive };
+    `,
+  });
+
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // module map:
+    {},
+    // options:
+    {
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com/packages/example'),
+    },
+  );
+
+  const { namespace } = await compartment.import('./main.js');
+  const { clive, clerk } = namespace;
+
+  t.truthy(clive === clerk, 'diamond dependency must refer to the same module');
+});
+
+test('compartments with same sources do not share instances', async t => {
+  t.plan(1);
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/packages/arm/main.js': `
+      export default {};
+    `,
+  });
+
+  const leftCompartment = new Compartment(
+    {}, // endowments
+    {}, // module map
+    {
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com/packages/arm'),
+    },
+  );
+
+  const rightCompartment = new Compartment(
+    {}, // endowments
+    {}, // module map
+    {
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com/packages/arm'),
+    },
+  );
+
+  const [
+    {
+      namespace: { default: leftArm },
+    },
+    {
+      namespace: { default: rightArm },
+    },
+  ] = await Promise.all([
+    leftCompartment.import('./main.js'),
+    rightCompartment.import('./main.js'),
+  ]);
+
+  t.truthy(
+    leftArm !== rightArm,
+    '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.is(dependency, "dependency");
+      t.is(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.truthy(isEven(n), \`\${n} should be even\`);
+        t.truthy(!isOdd(n), \`\${n} should not be odd\`);
+      }
+      for (const n of [1, 3, 5]) {
+        t.truthy(isOdd(n), \`\${n} should be odd\`);
+        t.truthy(!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(
+    {},
+    {},
+    {
+      name: 'https://example.com/even',
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com/even'),
+      moduleMapHook,
+    },
+  );
+
+  const odd = new Compartment(
+    {},
+    {},
+    {
+      name: 'https://example.com/odd',
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com/odd'),
+      moduleMapHook,
+    },
+  );
+
+  const compartment = new Compartment(
+    { t },
+    {},
+    {
+      name: 'https://example.com',
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com'),
+      moduleMapHook,
+    },
+  );
+
+  await compartment.import('./main.js');
+});
+
+test('import redirect shorthand', async t => {
+  // The following use of Math.random() is informative but does not
+  // affect the outcome of the test, just makes the nature of the error
+  // obvious in test output.
+  // The containing objects should be identical.
+  // The contained value should incidentally be identical.
+  // The test depends on the former.
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/main/index.js': `
+      export const unique = {n: Math.random()};
+      export const meaning = 42;
+    `,
+  });
+
+  const wrappedImportHook = makeImportHook('https://example.com');
+
+  const importHook = async specifier => {
+    await null;
+    const candidates = [specifier, `${specifier}.js`, `${specifier}/index.js`];
+    for (const candidate of candidates) {
+      // eslint-disable-next-line no-await-in-loop
+      const record = await wrappedImportHook(candidate).catch(_ => undefined);
+      // return a RedirectStaticModuleInterface with an explicit record
+      if (record !== undefined) {
+        return { record, specifier };
+      }
+    }
+    throw Error(`Cannot find module ${specifier}`);
+  };
+
+  const compartment = new Compartment(
+    {
+      Math,
+    },
+    {},
+    {
+      resolveHook: resolveNode,
+      importHook,
+    },
+  );
+
+  const { namespace } = await compartment.import('./main');
+  t.is(
+    namespace.meaning,
+    42,
+    'dynamically imports the meaning through a redirect',
+  );
+
+  // TODO The following commented test does not pass, and might not be valid.
+  // Web browsers appear to have taken the stance that they will load a static
+  // module record once per *response url* and create unique a unique module
+  // instance per *request url*.
+  //
+  // const { namespace: aliasNamespace } = await compartment.import(
+  //   './main/index.js',
+  // );
+  // t.strictEqual(
+  //   namespace.unique,
+  //   aliasNamespace.unique,
+  //   'alias modules have identical instance',
+  // );
+});
+
+test('import reflexive module alias', async t => {
+  t.plan(1);
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/index.js': `
+      import self from 'self';
+      export default 10;
+      t.is(self, 10);
+    `,
+  });
+
+  const wrappedImportHook = makeImportHook('https://example.com');
+
+  const importHook = async specifier => {
+    await null;
+    const candidates = [specifier, `${specifier}.js`, `${specifier}/index.js`];
+    for (const candidate of candidates) {
+      // eslint-disable-next-line no-await-in-loop
+      const record = await wrappedImportHook(candidate).catch(_ => undefined);
+      if (record !== undefined) {
+        // return a RedirectStaticModuleInterface with an explicit record
+        return { record, specifier };
+      }
+    }
+    throw Error(`Cannot find module ${specifier}`);
+  };
+
+  const moduleMapHook = specifier => {
+    if (specifier === 'self') {
+      // eslint-disable-next-line no-use-before-define
+      return compartment.module('./index.js');
+    }
+    return undefined;
+  };
+
+  const compartment = new Compartment(
+    {
+      t,
+    },
+    {},
+    {
+      resolveHook: resolveNode,
+      importHook,
+      moduleMapHook,
+    },
+  );
+
+  await compartment.import('./index.js');
+});
+
+test('child compartments are modular', async t => {
+  t.plan(1);
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/index.js': `
+      export default 42;
+    `,
+  });
+
+  const parent = new Compartment();
+  const compartment = new parent.globalThis.Compartment(
+    {}, // endowments
+    {}, // module map
+    {
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com/'),
+    },
+  );
+
+  const {
+    namespace: { default: meaning },
+  } = await compartment.import('./index.js');
+
+  t.is(meaning, 42, 'child compartments have module support');
+});
+
+test('import.meta populated from module record', async t => {
+  t.plan(1);
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/index.js': `
+      const myloc = import.meta.url;
+      export default myloc;
+    `,
+  });
+
+  const compartment = new Compartment(
+    { t },
+    {},
+    {
+      name: 'https://example.com',
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com', {
+        meta: { url: 'https://example.com/index.js' },
+      }),
+    },
+  );
+
+  const {
+    namespace: { default: metaurl },
+  } = await compartment.import('./index.js');
+  t.is(metaurl, 'https://example.com/index.js');
+});
+
+test('importMetaHook', async t => {
+  t.plan(1);
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/index.js': `
+      const myloc = import.meta.url;
+      export default myloc;
+    `,
+  });
+
+  const compartment = new Compartment(
+    { t },
+    {},
+    {
+      name: 'https://example.com',
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com'),
+      importMetaHook: (_moduleSpecifier, meta) => {
+        meta.url = 'https://example.com/index.js';
+      },
+    },
+  );
+
+  const {
+    namespace: { default: metaurl },
+  } = await compartment.import('./index.js');
+  t.is(metaurl, 'https://example.com/index.js');
+});
+
+test('importMetaHook and meta from record', async t => {
+  t.plan(1);
+
+  const makeImportHook = makeNodeImporter({
+    'https://example.com/index.js': `
+      const myloc = import.meta.url;
+      export default myloc;
+    `,
+  });
+
+  const compartment = new Compartment(
+    { t },
+    {},
+    {
+      name: 'https://example.com',
+      resolveHook: resolveNode,
+      importHook: makeImportHook('https://example.com', {
+        meta: { url: 'https://example.com/index.js' },
+      }),
+      importMetaHook: (_moduleSpecifier, meta) => {
+        meta.url += '?foo';
+        meta.isStillMutableHopefully = 1;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: metaurl },
+  } = await compartment.import('./index.js');
+  t.is(metaurl, 'https://example.com/index.js?foo');
+});
diff --git a/packages/ses/test/import-non-esm.test.js b/packages/ses/test/import-non-esm.test.js
index 6cf0fe7d1..bb9e5211f 100644
--- a/packages/ses/test/import-non-esm.test.js
+++ b/packages/ses/test/import-non-esm.test.js
@@ -17,11 +17,13 @@ test('import a non-ESM', async t => {
     };
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
   const module = compartment.module('.');
-  const {
-    namespace: { meaning },
-  } = await compartment.import('.');
+  const { meaning } = await compartment.import('.');
 
   t.is(meaning, 42, 'exports seen');
   t.is(module.meaning, 42, 'exports seen through deferred proxy');
@@ -54,10 +56,12 @@ test('non-ESM imports non-ESM by name', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -90,10 +94,12 @@ test('non-ESM imports non-ESM as default', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { default: odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { default: odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -125,10 +131,12 @@ test('ESM imports non-ESM as default', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { default: odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { default: odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -160,10 +168,12 @@ test('ESM imports non-ESM by name', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -195,10 +205,12 @@ test('non-ESM imports ESM as default', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { default: odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { default: odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -230,10 +242,12 @@ test('non-ESM imports ESM by name', async t => {
     throw Error(`Cannot load module ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
-  const {
-    namespace: { odd },
-  } = await compartment.import('./odd');
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
+  const { odd } = await compartment.import('./odd');
 
   t.is(odd(1), true);
   t.is(odd(2), false);
@@ -290,6 +304,10 @@ test('cross import ESM and non-ESMs', async t => {
     throw Error(`Cannot load module for specifier ${specifier}`);
   };
 
-  const compartment = new Compartment({}, {}, { resolveHook, importHook });
+  const compartment = new Compartment(
+    {},
+    {},
+    { resolveHook, importHook, __noNamespaceBox__: true },
+  );
   await compartment.import('./src/main.js');
 });
diff --git a/packages/ses/test/import.test.js b/packages/ses/test/import.test.js
index fa08cb231..d2bc5365a 100644
--- a/packages/ses/test/import.test.js
+++ b/packages/ses/test/import.test.js
@@ -28,20 +28,18 @@ test('import within one compartment, web resolution', async t => {
   const importHook = makeImporter(locate, retrieve);
 
   const compartment = new Compartment(
-    // endowments:
     {
       double: n => n * 2,
     },
-    // module map:
     {},
-    // options:
     {
       resolveHook,
       importHook,
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import(
+  const namespace = await compartment.import(
     'https://example.com/packages/example/',
   );
 
@@ -64,20 +62,18 @@ test('import within one compartment, node resolution', async t => {
   });
 
   const compartment = new Compartment(
-    // endowments:
     {
       double: n => n * 2,
     },
-    // module map:
     {},
-    // options:
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com/packages/example'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main.js');
+  const namespace = await compartment.import('./main.js');
 
   t.is(namespace.meaning, 42, 'dynamically imports the meaning');
 });
@@ -104,13 +100,10 @@ test('two compartments, three modules, one endowment', async t => {
   });
 
   const doubleCompartment = new Compartment(
-    // endowments:
     {
       double: n => n * 2,
     },
-    // module map:
     {},
-    // options:
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com/packages/double'),
@@ -118,23 +111,24 @@ test('two compartments, three modules, one endowment', async t => {
   );
 
   const compartment = new Compartment(
-    // endowments:
     {},
-    // module map:
     {
       // Notably, this is the first case where we thread a depencency between
       // two compartments, using the sigil of one's namespace to indicate
       // linkage before the module has been loaded.
-      double: doubleCompartment.module('./main.js'),
+      double: {
+        namespace: './main.js',
+        compartment: doubleCompartment,
+      },
     },
-    // options:
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com/packages/example'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main.js');
+  const namespace = await compartment.import('./main.js');
 
   t.is(namespace.meaning, 42, 'dynamically imports the meaning');
 });
@@ -149,18 +143,16 @@ test('module exports namespace as an object', async t => {
   });
 
   const compartment = new Compartment(
-    // endowments:
     {},
-    // module map:
     {},
-    // options:
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com/packages/meaning'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main.js');
+  const namespace = await compartment.import('./main.js');
 
   t.is(
     namespace.meaning,
@@ -224,18 +216,16 @@ test('modules are memoized', async t => {
   });
 
   const compartment = new Compartment(
-    // endowments:
     {},
-    // module map:
     {},
-    // options:
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com/packages/example'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main.js');
+  const namespace = await compartment.import('./main.js');
   const { clive, clerk } = namespace;
 
   t.truthy(clive === clerk, 'diamond dependency must refer to the same module');
@@ -251,31 +241,26 @@ test('compartments with same sources do not share instances', async t => {
   });
 
   const leftCompartment = new Compartment(
-    {}, // endowments
-    {}, // module map
+    {},
+    {},
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com/packages/arm'),
+      __noNamespaceBox__: true,
     },
   );
 
   const rightCompartment = new Compartment(
-    {}, // endowments
-    {}, // module map
+    {},
+    {},
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com/packages/arm'),
+      __noNamespaceBox__: true,
     },
   );
 
-  const [
-    {
-      namespace: { default: leftArm },
-    },
-    {
-      namespace: { default: rightArm },
-    },
-  ] = await Promise.all([
+  const [{ default: leftArm }, { default: rightArm }] = await Promise.all([
     leftCompartment.import('./main.js'),
     rightCompartment.import('./main.js'),
   ]);
@@ -321,6 +306,7 @@ test('module map hook', async t => {
     {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com/dependency'),
+      __noNamespaceBox__: true,
     },
   );
 
@@ -336,10 +322,14 @@ test('module map hook', async t => {
           'dependency',
         );
         if (remainder) {
-          return dependency.module(remainder);
+          return {
+            namespace: remainder,
+            compartment: dependency,
+          };
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
@@ -379,7 +369,10 @@ test('mutual dependency between compartments', async t => {
     for (const [prefix, compartment] of Object.entries({ even, odd })) {
       const remainder = trimModuleSpecifierPrefix(moduleSpecifier, prefix);
       if (remainder) {
-        return compartment.module(remainder);
+        return {
+          compartment,
+          namespace: remainder,
+        };
       }
     }
     return undefined;
@@ -393,6 +386,7 @@ test('mutual dependency between compartments', async t => {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com/even'),
       moduleMapHook,
+      __noNamespaceBox__: true,
     },
   );
 
@@ -404,6 +398,8 @@ test('mutual dependency between compartments', async t => {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com/odd'),
       moduleMapHook,
+      [Symbol.for('options')]: true,
+      __noNamespaceBox__: true,
     },
   );
 
@@ -415,6 +411,7 @@ test('mutual dependency between compartments', async t => {
       resolveHook: resolveNode,
       importHook: makeImportHook('https://example.com'),
       moduleMapHook,
+      __noNamespaceBox__: true,
     },
   );
 
@@ -453,17 +450,16 @@ test('import redirect shorthand', async t => {
   };
 
   const compartment = new Compartment(
-    {
-      Math,
-    },
+    { Math },
     {},
     {
       resolveHook: resolveNode,
       importHook,
+      __noNamespaceBox__: true,
     },
   );
 
-  const { namespace } = await compartment.import('./main');
+  const namespace = await compartment.import('./main');
   t.is(
     namespace.meaning,
     42,
@@ -514,21 +510,23 @@ test('import reflexive module alias', async t => {
 
   const moduleMapHook = specifier => {
     if (specifier === 'self') {
-      // eslint-disable-next-line no-use-before-define
-      return compartment.module('./index.js');
+      return {
+        // eslint-disable-next-line no-use-before-define
+        compartment,
+        namespace: './index.js',
+      };
     }
     return undefined;
   };
 
   const compartment = new Compartment(
-    {
-      t,
-    },
+    { t },
     {},
     {
       resolveHook: resolveNode,
       importHook,
       moduleMapHook,
+      __noNamespaceBox__: true,
     },
   );
 
@@ -580,12 +578,11 @@ test('import.meta populated from module record', async t => {
       importHook: makeImportHook('https://example.com', {
         meta: { url: 'https://example.com/index.js' },
       }),
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: metaurl },
-  } = await compartment.import('./index.js');
+  const { default: metaurl } = await compartment.import('./index.js');
   t.is(metaurl, 'https://example.com/index.js');
 });
 
@@ -609,12 +606,11 @@ test('importMetaHook', async t => {
       importMetaHook: (_moduleSpecifier, meta) => {
         meta.url = 'https://example.com/index.js';
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: metaurl },
-  } = await compartment.import('./index.js');
+  const { default: metaurl } = await compartment.import('./index.js');
   t.is(metaurl, 'https://example.com/index.js');
 });
 
@@ -641,11 +637,10 @@ test('importMetaHook and meta from record', async t => {
         meta.url += '?foo';
         meta.isStillMutableHopefully = 1;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: metaurl },
-  } = await compartment.import('./index.js');
+  const { default: metaurl } = await compartment.import('./index.js');
   t.is(metaurl, 'https://example.com/index.js?foo');
 });
diff --git a/packages/ses/test/module-map-hook.test.js b/packages/ses/test/module-map-hook.test.js
index 90eab3e45..38c123819 100644
--- a/packages/ses/test/module-map-hook.test.js
+++ b/packages/ses/test/module-map-hook.test.js
@@ -21,9 +21,10 @@ test('module map hook returns module source descriptor with precompiled module s
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -50,9 +51,10 @@ test('module map hook returns  module source descriptor with virtual module sour
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -98,9 +100,10 @@ test('module map hook returns parent compartment module source descriptor with s
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -180,9 +183,10 @@ test('module map hook returns parent compartment module source reference with di
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -203,12 +207,11 @@ test('module map hook returns module source descriptor for parent compartment wi
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: parentObject },
-  } = await parent.import('./object.js');
+  const { default: parentObject } = await parent.import('./object.js');
   t.is(parentObject.meaning, 42);
 
   const compartment = new parent.globalThis.Compartment(
@@ -228,12 +231,11 @@ test('module map hook returns module source descriptor for parent compartment wi
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: childObject },
-  } = await compartment.import('./index.js');
+  const { default: childObject } = await compartment.import('./index.js');
   t.is(childObject.meaning, 42);
   // Separate instances
   t.not(childObject, parentObject);
@@ -256,12 +258,11 @@ test('module map hook returns parent compartment module namespace descriptor', a
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: parentObject },
-  } = await parent.import('./object.js');
+  const { default: parentObject } = await parent.import('./object.js');
   t.is(parentObject.meaning, 42);
 
   const compartment = new parent.globalThis.Compartment(
@@ -281,12 +282,11 @@ test('module map hook returns parent compartment module namespace descriptor', a
         }
         return undefined;
       },
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: childObject },
-  } = await compartment.import('./index.js');
+  const { default: childObject } = await compartment.import('./index.js');
   t.is(childObject.meaning, 42);
   // Same instances
   t.is(childObject, parentObject);
diff --git a/packages/ses/test/module-map.test.js b/packages/ses/test/module-map.test.js
index fc4de21cb..e69ce3d27 100644
--- a/packages/ses/test/module-map.test.js
+++ b/packages/ses/test/module-map.test.js
@@ -17,9 +17,10 @@ test('module map primed with module source descriptor with precompiled module so
     // options:
     {
       resolveHook: specifier => specifier,
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -42,9 +43,10 @@ test('module map primed with module source descriptor with virtual module source
     // options:
     {
       resolveHook: specifier => specifier,
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -82,9 +84,10 @@ test('module map primed with parent compartment module source descriptor with st
     // options:
     {
       resolveHook: specifier => specifier,
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -154,9 +157,10 @@ test('module map primed with parent compartment module source reference with dif
         t.is(specifier, './lib/meaning.js');
         return './lib/meaningful.js';
       },
+      __noNamespaceBox__: true,
     },
   );
-  const { namespace: index } = await compartment.import('./index.js');
+  const index = await compartment.import('./index.js');
   t.is(index.default, 42);
 });
 
@@ -173,12 +177,11 @@ test('module map primed with module source descriptor for parent compartment wit
     // options:
     {
       name: 'parent',
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: parentObject },
-  } = await parent.import('./object.js');
+  const { default: parentObject } = await parent.import('./object.js');
   t.is(parentObject.meaning, 42);
 
   const compartment = new parent.globalThis.Compartment(
@@ -194,12 +197,11 @@ test('module map primed with module source descriptor for parent compartment wit
     // options:
     {
       name: 'child',
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: childObject },
-  } = await compartment.import('./index.js');
+  const { default: childObject } = await compartment.import('./index.js');
   t.is(childObject.meaning, 42);
   // Separate instances
   t.not(childObject, parentObject);
@@ -218,12 +220,11 @@ test('module map primed with parent compartment module namespace descriptor', as
     // options:
     {
       name: 'parent',
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: parentObject },
-  } = await parent.import('./object.js');
+  const { default: parentObject } = await parent.import('./object.js');
   t.is(parentObject.meaning, 42);
 
   const compartment = new parent.globalThis.Compartment(
@@ -239,12 +240,11 @@ test('module map primed with parent compartment module namespace descriptor', as
     // options:
     {
       name: 'child',
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: childObject },
-  } = await compartment.import('./index.js');
+  const { default: childObject } = await compartment.import('./index.js');
   t.is(childObject.meaning, 42);
   // Same instances
   t.is(childObject, parentObject);
@@ -263,12 +263,11 @@ test('module map primed with module source descriptor with string reference to p
     // options:
     {
       name: 'compartment1',
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object1 },
-  } = await compartment1.import('./object.js');
+  const { default: object1 } = await compartment1.import('./object.js');
   t.is(object1.meaning, 42);
 
   const compartment2 = new Compartment(
@@ -284,12 +283,11 @@ test('module map primed with module source descriptor with string reference to p
     // options:
     {
       name: 'child',
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object2 },
-  } = await compartment2.import('./index.js');
+  const { default: object2 } = await compartment2.import('./index.js');
   t.is(object2.meaning, 42);
   // Separate instances
   t.not(object1, object2);
@@ -308,12 +306,11 @@ test('module map primed with other compartment module namespace descriptor', asy
     // options:
     {
       name: 'compartment1',
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object1 },
-  } = await compartment1.import('./object.js');
+  const { default: object1 } = await compartment1.import('./object.js');
   t.is(object1.meaning, 42);
 
   const compartment2 = new Compartment(
@@ -329,12 +326,11 @@ test('module map primed with other compartment module namespace descriptor', asy
     // options:
     {
       name: 'child',
+      __noNamespaceBox__: true,
     },
   );
 
-  const {
-    namespace: { default: object2 },
-  } = await compartment2.import('./index.js');
+  const { default: object2 } = await compartment2.import('./index.js');
   t.is(object2.meaning, 42);
   // Same instances
   t.is(object1, object2);
@@ -348,17 +344,21 @@ test('module map primed with module namespace descriptor and namespace object',
         source: new ModuleSource(`export default 42`),
       },
     },
-    {},
+    {
+      __noNamespaceBox__: true,
+    },
   );
-  const { namespace: namespace1 } = await compartment1.import('a');
+  const namespace1 = await compartment1.import('a');
   const compartment2 = new Compartment(
     {},
     {
       z: { namespace: namespace1 },
     },
-    {},
+    {
+      __noNamespaceBox__: true,
+    },
   );
-  const { namespace: namespace2 } = await compartment2.import('z');
+  const namespace2 = await compartment2.import('z');
   t.is(namespace2.default, 42);
   t.is(namespace1, namespace2);
 });
@@ -369,9 +369,11 @@ test('module map primed with module namespace descriptor and non-namespace objec
     {
       1: { namespace: { meaning: 42 } },
     },
-    {},
+    {
+      __noNamespaceBox__: true,
+    },
   );
-  const { namespace } = await compartment.import('1');
+  const namespace = await compartment.import('1');
   t.is(namespace.meaning, 42);
 });