Repository: endojs/endo
Pull request #2345 (2024-07-11 20:29:28)

feat(ses): Module descriptors for XS parity
Refs: https://github.com/endojs/endo/issues/400 https://github.com/endojs/endo/issues/2251

## Description

This change introduces support for XS-compatible module descriptors with the discriminating properties `source` and `namespace`, including support for loading a module from the parent compartment.

### Security Considerations

None.

### Scaling Considerations

None.

### Documentation Considerations

This change is additive.

### Testing Considerations

Covered.

### Compatibility Considerations

Some usage patterns are now deprecated but continue to be supported without complaint.

### Upgrade Considerations

None.

kriskowal: > LGTM
> 
> The README.md is an awesome guide for stating this stuff precisely. Should already be much of the work towards spec text!

Credit where due https://github.com/Moddable-OpenSource/moddable/blob/public/documentation/xs/XS%20Compartment.md#-module-descriptor

Patch:
diff --git a/packages/compartment-mapper/README.md b/packages/compartment-mapper/README.md
index eeb9297a3..5d2a88d24 100644
--- a/packages/compartment-mapper/README.md
+++ b/packages/compartment-mapper/README.md
@@ -485,7 +485,7 @@ type InternalModuleSpecifier = string;
 // in the namespace of the foreign compartment.
 type ExternalModuleSpecifier = string;
 
-// ParserMap indicates which parser to use to construct static module records
+// ParserMap indicates which parser to use to construct module sources
 // from sources, for each supported file extension.
 // For parity with Node.js, a package with `"type": "module"` in its
 // `package.json` would have a parser map of `{"js": "mjs", "cjs": "cjs",
@@ -500,7 +500,7 @@ type ParserMap = Record<Extension, Parser>;
 // Extension is a file extension such as "js" for "main.js" or "" for "README".
 type Extension = string;
 
-// Parser is a union of built-in parsers for static module records.
+// Parser is a union of built-in parsers for module sources.
 // "mjs" corresponds to ECMAScript modules.
 // "cjs" corresponds to CommonJS modules.
 // "json" corresponds to JSON.
diff --git a/packages/compartment-mapper/package.json b/packages/compartment-mapper/package.json
index 91f01adf2..497dfbd8c 100644
--- a/packages/compartment-mapper/package.json
+++ b/packages/compartment-mapper/package.json
@@ -56,7 +56,7 @@
   },
   "dependencies": {
     "@endo/cjs-module-analyzer": "^1.0.5",
-    "@endo/static-module-record": "^1.1.2",
+    "@endo/module-source": "^0.0.0",
     "@endo/zip": "^1.0.5",
     "ses": "^1.5.0"
   },
diff --git a/packages/compartment-mapper/src/parse-archive-mjs.js b/packages/compartment-mapper/src/parse-archive-mjs.js
index c5a2a0ff0..189402138 100644
--- a/packages/compartment-mapper/src/parse-archive-mjs.js
+++ b/packages/compartment-mapper/src/parse-archive-mjs.js
@@ -3,7 +3,7 @@
  */
 // @ts-check
 
-import { StaticModuleRecord } from '@endo/static-module-record';
+import { ModuleSource } from '@endo/module-source';
 
 const textEncoder = new TextEncoder();
 const textDecoder = new TextDecoder();
@@ -18,7 +18,7 @@ export const parseArchiveMjs = async (
 ) => {
   const { sourceMap, sourceMapHook } = options;
   const source = textDecoder.decode(bytes);
-  const record = new StaticModuleRecord(source, {
+  const record = new ModuleSource(source, {
     sourceMap,
     sourceMapUrl: sourceUrl,
     sourceMapHook,
diff --git a/packages/compartment-mapper/src/parse-mjs.js b/packages/compartment-mapper/src/parse-mjs.js
index 95f663e9b..684a494d4 100644
--- a/packages/compartment-mapper/src/parse-mjs.js
+++ b/packages/compartment-mapper/src/parse-mjs.js
@@ -2,7 +2,7 @@
 
 // @ts-check
 
-import { StaticModuleRecord } from '@endo/static-module-record';
+import { ModuleSource } from '@endo/module-source';
 
 const textDecoder = new TextDecoder();
 
@@ -16,7 +16,7 @@ export const parseMjs = async (
 ) => {
   const { sourceMap, sourceMapHook } = options;
   const source = textDecoder.decode(bytes);
-  const record = new StaticModuleRecord(source, {
+  const record = new ModuleSource(source, {
     sourceUrl,
     sourceMap,
     sourceMapUrl: sourceUrl,
diff --git a/packages/eslint-plugin/lib/configs/recommended.js b/packages/eslint-plugin/lib/configs/recommended.js
index 8733b0ab3..6e0da94c8 100644
--- a/packages/eslint-plugin/lib/configs/recommended.js
+++ b/packages/eslint-plugin/lib/configs/recommended.js
@@ -11,7 +11,7 @@ module.exports = {
     assert: 'readonly',
     console: 'readonly',
     Compartment: 'readonly',
-    StaticModuleRecord: 'readonly',
+    ModuleSource: 'readonly',
     TextDecoder: 'readonly',
     TextEncoder: 'readonly',
     URL: 'readonly',
diff --git a/packages/static-module-record/CHANGELOG.md b/packages/module-source/CHANGELOG.md
similarity index 100%
rename from packages/static-module-record/CHANGELOG.md
rename to packages/module-source/CHANGELOG.md
diff --git a/packages/static-module-record/DESIGN.md b/packages/module-source/DESIGN.md
similarity index 96%
rename from packages/static-module-record/DESIGN.md
rename to packages/module-source/DESIGN.md
index 54c426dce..1df2ec7d8 100644
--- a/packages/static-module-record/DESIGN.md
+++ b/packages/module-source/DESIGN.md
@@ -3,7 +3,7 @@
 This package uses a transformation from a JS module source to a corresponding
 JS program that, when evaluated, produces a function that can execute a module
 and orchestrate its imports and exports.
-The static module record includes the static analysis of the module: what it
+The module source record includes the static analysis of the module: what it
 imports and exports and how it refers to imported modules.
 
 The workflow for executing a module, as conducted by a Compartment,
@@ -47,7 +47,7 @@ export { grey as gray } from './reexport-name-and-rename.js';
 quuux = 'Hello, World!';
 ```
 
-From this, the analyzer produces a static module record that shows what modules
+From this, the analyzer produces a module source record that shows what modules
 the module needs to be linked to and how to link their imports and exports.
 
 ```js
@@ -147,7 +147,7 @@ type ModuleAnalysis = {
 
 // ExportAlls are the relative module specifiers found in directives like:
 //   export * from 'import-and-reexport-all-from-me.js';
-// These are both on the static module record and passed to the import function
+// These are both on the module source and passed to the import function
 // injected into a module functor.
 // TODO Consider removing the import argument.
 // It does not appear to be used by module instances.
diff --git a/packages/static-module-record/LICENSE b/packages/module-source/LICENSE
similarity index 100%
rename from packages/static-module-record/LICENSE
rename to packages/module-source/LICENSE
diff --git a/packages/static-module-record/NEWS.md b/packages/module-source/NEWS.md
similarity index 97%
rename from packages/static-module-record/NEWS.md
rename to packages/module-source/NEWS.md
index be8fcf785..274513f9e 100644
--- a/packages/static-module-record/NEWS.md
+++ b/packages/module-source/NEWS.md
@@ -1,5 +1,9 @@
 User-visible changes in Static Module Record, née Transform Module:
 
+# Next version
+
+- Renamed to `@endo/module-source` exporting `ModuleSource`.
+
 # 1.0.0 (2023-12-12)
 
 - Ensures that `this` is bound to `undefined` in module scope and that the
diff --git a/packages/static-module-record/README.md b/packages/module-source/README.md
similarity index 77%
rename from packages/static-module-record/README.md
rename to packages/module-source/README.md
index 142368369..077c632ae 100644
--- a/packages/static-module-record/README.md
+++ b/packages/module-source/README.md
@@ -1,14 +1,14 @@
-# StaticModuleRecord
+# ModuleSource
 
-This package provides a shim for the `StaticModuleRecord` constructor, suitable
-for use with the SES shim `importHook`.
-The static module record accepts a JavaScript module and converts it into
-a form that SES can use to securely emulate JavaScript modules (ESMs, the `mjs`
-file format) with compartments.
+This package provides a ponyfill for the `ModuleSource` constructor, suitable
+for use in the SES shim's module descriptors.
+The module source accepts a JavaScript module and converts it into
+a form that SES can use to emulate and confine JavaScript modules (ESMs, the
+`mjs` file format) with compartments.
 
 ```js
 import 'ses';
-import { StaticModuleRecord } from '@endo/static-module-record`;
+import { ModuleSource } from '@endo/module-source`;
 
 const c1 = new Compartment({}, {}, {
   name: "first compartment",
@@ -18,14 +18,14 @@ const c1 = new Compartment({}, {}, {
   importHook: async moduleSpecifier => {
     const moduleLocation = locate(moduleSpecifier);
     const moduleText = await retrieve(moduleLocation);
-    return new StaticModuleRecord(moduleText, moduleLocation);
+    return new ModuleSource(moduleText, moduleLocation);
   },
 });
 ```
 
 ## Source maps
 
-The `StaticModuleRecord` is a shim for what we hope to eventually call a native
+The `ModuleSource` is a shim for what we hope to eventually call a native
 `ModuleSource` constructor.
 However, in the absence of a native `ModuleSource`, this produces a
 serializable object that emulates the behavior of `ModuleSource` in conjunction
@@ -39,7 +39,7 @@ changes due to (hopefully temporary) limitations to the underlying code
 generator.
 In the interim, generating a source map can help.
 
-The `StaticModuleRecord` constructor accepts non-standards-track
+The `ModuleSource` constructor accepts non-standards-track
 `sourceMapHook` and `sourceMapUrl` options.
 
 Previously, the sole option was a `string` argument for the `sourceUrl`, such
@@ -47,9 +47,9 @@ that this would be appended to the generated source.
 This change allows for the old or new usage:
 
 ```js
-new StaticModuleRecord(source, sourceUrl);
+new ModuleSource(source, sourceUrl);
 // or
-new StaticModuleRecord(source, { sourceUrl, sourceMapUrl, sourceMapHook });
+new ModuleSource(source, { sourceUrl, sourceMapUrl, sourceMapHook });
 ```
 
 The `sourceMapUrl` is necessary for generating a source map.
diff --git a/packages/static-module-record/REWRITE.md b/packages/module-source/REWRITE.md
similarity index 100%
rename from packages/static-module-record/REWRITE.md
rename to packages/module-source/REWRITE.md
diff --git a/packages/static-module-record/SECURITY.md b/packages/module-source/SECURITY.md
similarity index 100%
rename from packages/static-module-record/SECURITY.md
rename to packages/module-source/SECURITY.md
diff --git a/packages/static-module-record/TESTS.md b/packages/module-source/TESTS.md
similarity index 100%
rename from packages/static-module-record/TESTS.md
rename to packages/module-source/TESTS.md
diff --git a/packages/module-source/index.js b/packages/module-source/index.js
new file mode 100644
index 000000000..9e2fff13b
--- /dev/null
+++ b/packages/module-source/index.js
@@ -0,0 +1,1 @@
+export { ModuleSource } from './src/module-source.js';
diff --git a/packages/static-module-record/package.json b/packages/module-source/package.json
similarity index 86%
rename from packages/static-module-record/package.json
rename to packages/module-source/package.json
index 397863cc0..bb3b90e98 100644
--- a/packages/static-module-record/package.json
+++ b/packages/module-source/package.json
@@ -1,20 +1,20 @@
 {
-  "name": "@endo/static-module-record",
-  "version": "1.1.2",
-  "description": "Shim for the SES StaticModuleRecord and module-to-program transformer",
+  "name": "@endo/module-source",
+  "version": "0.0.0",
+  "description": "Ponyfill for the SES ModuleSource and module-to-program transformer",
   "keywords": [
     "ses",
-    "StaticModuleRecord",
+    "ModuleSource",
     "modules",
     "ESM"
   ],
   "author": "Agoric",
   "license": "Apache-2.0",
-  "homepage": "https://github.com/endojs/endo/tree/master/packages/static-module-record#readme",
+  "homepage": "https://github.com/endojs/endo/tree/master/packages/module-source#readme",
   "repository": {
     "type": "git",
     "url": "git+https://github.com/endojs/endo.git",
-    "directory": "packages/static-module-record"
+    "directory": "packages/module-source"
   },
   "bugs": {
     "url": "https://github.com/endojs/endo/issues"
diff --git a/packages/static-module-record/src/babelPlugin.js b/packages/module-source/src/babelPlugin.js
similarity index 100%
rename from packages/static-module-record/src/babelPlugin.js
rename to packages/module-source/src/babelPlugin.js
diff --git a/packages/static-module-record/src/hidden.js b/packages/module-source/src/hidden.js
similarity index 100%
rename from packages/static-module-record/src/hidden.js
rename to packages/module-source/src/hidden.js
diff --git a/packages/static-module-record/src/static-module-record.js b/packages/module-source/src/module-source.js
similarity index 87%
rename from packages/static-module-record/src/static-module-record.js
rename to packages/module-source/src/module-source.js
index f13ebd12b..d220cb23d 100644
--- a/packages/static-module-record/src/static-module-record.js
+++ b/packages/module-source/src/module-source.js
@@ -55,21 +55,21 @@ const analyzeModule = makeModuleAnalyzer();
  * @property {SourceMapHook} [sourceMapHook]
  */
 
-// XXX implements import('ses').PrecompiledStaticModuleInterface but adding
+// XXX implements import('ses').PrecompiledModuleSource but adding
 // `@implements` errors that this isn't a class and `@returns` errors that
 // there's no value returned.
 /**
- * StaticModuleRecord captures the effort of parsing and analyzing module text
- * so a cache of StaticModuleRecords may be shared by multiple Compartments.
+ * ModuleSource captures the effort of parsing and analyzing module text
+ * so a cache of ModuleSources may be shared by multiple Compartments.
  *
  * @class
  * @param {string} source
  * @param {string | Options} [opts]
  */
-export function StaticModuleRecord(source, opts = {}) {
+export function ModuleSource(source, opts = {}) {
   if (new.target === undefined) {
     throw TypeError(
-      "Class constructor StaticModuleRecord cannot be invoked without 'new'",
+      "Class constructor ModuleSource cannot be invoked without 'new'",
     );
   }
   if (typeof opts === 'string') {
diff --git a/packages/static-module-record/src/transform-analyze.js b/packages/module-source/src/transform-analyze.js
similarity index 98%
rename from packages/static-module-record/src/transform-analyze.js
rename to packages/module-source/src/transform-analyze.js
index d32a465c3..588d918b1 100644
--- a/packages/static-module-record/src/transform-analyze.js
+++ b/packages/module-source/src/transform-analyze.js
@@ -6,11 +6,13 @@ import * as h from './hidden.js';
 
 const { freeze } = Object;
 
+/** @import {Options} from './module-source.js' */
+
 const makeCreateStaticRecord = transformSource =>
   /**
    *
    * @param {string} moduleSource
-   * @param {import('./static-module-record.js').Options} options
+   * @param {Options} options
    */
   function createStaticRecord(
     moduleSource,
diff --git a/packages/static-module-record/tsconfig.build.json b/packages/module-source/tsconfig.build.json
similarity index 100%
rename from packages/static-module-record/tsconfig.build.json
rename to packages/module-source/tsconfig.build.json
diff --git a/packages/static-module-record/tsconfig.json b/packages/module-source/tsconfig.json
similarity index 100%
rename from packages/static-module-record/tsconfig.json
rename to packages/module-source/tsconfig.json
diff --git a/packages/ses/NEWS.md b/packages/ses/NEWS.md
index 2b63c1d0a..a787bbe48 100644
--- a/packages/ses/NEWS.md
+++ b/packages/ses/NEWS.md
@@ -1,5 +1,9 @@
 User-visible changes in SES:
 
+# Next version
+
+- Adds support for module descriptors better aligned with XS.
+
 # 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/README.md b/packages/ses/README.md
index 3970dead7..10a2cdc8d 100644
--- a/packages/ses/README.md
+++ b/packages/ses/README.md
@@ -262,20 +262,29 @@ This can and should be frozen before running any dynamic code in that
 compartment, yet is not strictly necessary if the compartment only
 runs code from a single party.
 
-
 ### Modules
 
 Any code executed within a compartment shares a set of module instances.
 For modules to work within a compartment, the creator must provide
-a `resolveHook` and an `importHook`.
+module descriptors.
+A compartment can be configured with module descriptors, from highest to lowest
+precedence:
+
+- the `modules` map provided to the `Compartment` constructor,
+- returned by a `moduleMapHook(specifier)` passed as an option to the
+  `Compartment` constructor.
+- returned by either the `importHook(specifier)` or `importNowHook(specifier)`
+  option passed to the `Compartment` constructor. Calling
+  `compartment.import(specifier)` falls through to the `importHook` which may
+  return a promises, whereas `compartment.importNow(specifier)` falls through
+  to the synchronous `importNowHook`.
+
 The `resolveHook` determines how the compartment will infer the full module
 specifier for another module from a referrer module and the import specifier.
-The `importHook` accepts a full specifier and asynchronously returns a
-`StaticModuleRecord` for that module.
 
 ```js
 import 'ses';
-import { StaticModuleRecord } from '@endo/static-module-record';
+import { ModuleSource } from '@endo/module-source';
 
 const c1 = new Compartment({}, {}, {
   name: "first compartment",
@@ -285,29 +294,27 @@ const c1 = new Compartment({}, {}, {
   importHook: async moduleSpecifier => {
     const moduleLocation = locate(moduleSpecifier);
     const moduleText = await retrieve(moduleLocation);
-    return new StaticModuleRecord(moduleText, moduleLocation);
+    return {
+        source: new ModuleSource(moduleText, moduleLocation);
+    };
   },
 });
 ```
 
-> The SES language specifies a global `StaticModuleRecord`, but this is not
-> provided by the shim because it entrains a full JavaScript parser that is an
-> unnecessary performance penalty for the SES runtime.
-> Instead, the SES shim accepts a compiled static module record duck-type that
+> The Hardened JavaScript language specifies a global `ModuleSource`, but this
+> is not provided by the shim because it entrains a full JavaScript parser that
+> is an unnecessary performance penalty for the SES runtime.
+> Instead, the SES shim accepts a pre-compiled module source duck-type that
 > is tightly coupled to the shim implementation.
-> Third party modules can provide suitable implementations and even move the
-> compile step to build time instead of runtime.
 
 A compartment can also link a module in another compartment.
-Each compartment has a `module` function that accepts a module specifier
-and returns the module exports namespace for that module.
-The module exports namespace is not useful for inspecting the exports of the
-module until that module has been imported, but it can be passed into the
-module map of another Compartment, creating a link.
 
 ```js
 const c2 = new Compartment({}, {
-  'c1': c1.module('./main.js'),
+  'c1': {
+    source: './main.js',
+    compartment: c1,
+  },
 }, {
   name: "second compartment",
   resolveHook,
@@ -315,20 +322,67 @@ const c2 = new Compartment({}, {
 });
 ```
 
-### importHook aliases
+#### Module Descriptors
+
+Comparments can load and initialize module namespaces from module descriptors.
+Like property descriptors, module descriptors are ordinary objects with various
+forms.
+
+##### Descriptors with `source` property
+
+- If fhe value of the `source` property is a string, the parent compartment
+  loads the module but the compartment itself initializes the module.
+
+- Otherwise, if the value of the `source` property is the module source, the
+  module is initialized from the module source.
+
+- Otherwise, the value of the `source` property must be an object. The module
+  is loaded and initialized from the object according to the [virtual module
+  source](#VirtualModuleSource) pattern.
+
+If the `importMeta` property is present, its value must be an object. The
+default `importMeta` object is an empty object.
+
+Compartments copy the `importMeta` object properties into the module
+`import.meta` object like `Object.assign`.
+
+If the `specifier` property is present, its value is coerced into a string and
+becomes the referrer specifier of the module, on which all import specifiers
+are resolved using the `resolveHook`.
+
+##### Descriptors with `namespace` property
+
+- If fhe value of the `namespace` property is a string, the descriptor shares a
+  module to be loaded and initialized by the compartment referred by the
+  `compartment` property.
+
+    - If the `compartment` property is present, its value must be a
+      compartment.
+    - If absent, the `compartment` property defaults to the compartment being
+      constructed in the `modules` option, or being hooked in the `loadHook`
+      and `loadNowHook` options.
+
+- Otherwise, if the value of the `namespace` property is a module namepace, the
+  descriptor shares a module that is already available.
+
+- Otherwise, the value of `namespace` property must be an object. The module is
+  loaded and initialized from the object according to the [virtual module
+  namespace](#VirtualModuleNamespace) pattern.
+
+#### Redirects
 
 If a compartment imports a module specified as `"./utility"` but actually
 implemented by an alias like `"./utility/index.js"`, the `importHook` may
 follow redirects, symbolic links, or search for candidates using its own logic
 and return a module that has a different "response specifier" than the original
 "request specifier".
-The `importHook` may return an "alias" object with `record`, `compartment`,
-and `module` properties.
+The `importHook` may return an "alias" object with `source`, `compartment`,
+and `specifier` properties.
 
-- `record` must be a static module record, either a third-party module record
-  or a compiled static module record.
+- `source` must be a module source, either a virtual module source
+  or a compiled module source.
 - `compartment` is optional, to be specified if the alias transits to a
-  different compartment, and
+  the specified different compartment, and
 - `specifier` is the full module specifier of the module in its compartment.
   This defaults to the request specifier, which is only useful if the
   compartment is different.
@@ -340,9 +394,13 @@ alias.
 const importHook = async specifier => {
   const candidates = [specifier, `${specifier}.js`, `${specifier}/index.js`];
   for (const candidate of candidates) {
-    const record = await wrappedImportHook(candidate).catch(_ => undefined);
-    if (record !== undefined) {
-      return { record, specifier };
+    const source = await maybeImportSource(candidate);
+    if (source !== undefined) {
+      return {
+        source,
+        specifier: candidate,
+        compartment,
+      };
     }
   }
   throw new Error(`Cannot find module ${specifier}`);
@@ -354,7 +412,7 @@ const compartment = new Compartment({}, {}, {
 });
 ```
 
-### moduleMapHook
+#### moduleMapHook
 
 The module map above allows modules to be introduced to a compartment up-front.
 Some modules cannot be known that early.
@@ -373,9 +431,15 @@ If the `moduleMapHook` returns `undefined`, the compartment proceeds to the
 ```js
 const moduleMapHook = moduleSpecifier => {
   if (moduleSpecifier === 'even') {
-    return even.module('./index.js');
+    return {
+      source: './index.js',
+      compartment: even,
+    };
   } else if (moduleSpecifier === 'odd') {
-    return odd.module('./index.js');
+    return {
+      source: './index.js',
+      compartment: odd,
+    };
   }
 };
 
@@ -392,17 +456,25 @@ const odd = new Compartment({}, {}, {
 });
 ```
 
-### importNowHook
+#### importNowHook
 
-Additionally, an `importNowHook` may be provided that the compartment will use as means to synchronously load modules not seen before in situations where calling out to asynchronous `importHook` is not possible.
-Specifically, when `compartmentInstance.importNow('specifier')` is called, the compartment will first look up module records it's already aware of and call `moduleMapHook` and if none of that is successful in finding a module record matching the specifier, it will call `importNowHook` expecting to synchronously receive the same record type as from `importHook` or throw if it cannot.
+Additionally, an `importNowHook` may be provided that the compartment will use
+as means to synchronously load modules not seen before in situations where
+calling out to asynchronous `importHook` is not possible.
+Specifically, when `compartmentInstance.importNow('specifier')` is called, the
+compartment will first look up module records it's already aware of and call
+`moduleMapHook` and if none of that is successful in finding a module record
+matching the specifier, it will call `importNowHook` expecting to synchronously
+receive the same record type as from `importHook` or throw if it cannot.
 
 ```js
 import 'ses';
-import { StaticModuleRecord } from '@endo/static-module-record';
+import { ModuleSource } from '@endo/module-source';
 
-const c1 = new Compartment({}, {
-  'c2': c2.module('./main.js'),
+const compartment = new Compartment({}, {
+  c: {
+    source: new ModuleSource(''),
+  },
 }, {
   name: "first compartment",
   resolveHook: (moduleSpecifier, moduleReferrer) => {
@@ -411,27 +483,31 @@ const c1 = new Compartment({}, {
   importHook: async moduleSpecifier => {
     const moduleLocation = locate(moduleSpecifier);
     const moduleText = await retrieve(moduleLocation);
-    return new StaticModuleRecord(moduleText, moduleLocation);
+    return {
+      source: new ModuleSource(moduleText, moduleLocation),
+    };
   },
   importNowHook: moduleSpecifier => {
     const moduleLocation = locate(moduleSpecifier);
-    // Platform specific synchronous read API can be used
+    // Platform-specific synchronous read API can be used
     const moduleText = fs.readFileSync(moduleLocation);
-    return new StaticModuleRecord(moduleText, moduleLocation);
+    return {
+      source: new ModuleSource(moduleText, moduleLocation),
+    };
   },
 });
 //...                   | importHook | importNowHook
-await c1.import('a'); //| called     | not called
-c1.importNow('b');    //| not called | called
-c1.importNow('a');    //| not called | not called
-c1.importNow('c2');   //| not called | not called
+await compartment.import('a'); //| called     | not called
+compartment.importNow('b');    //| not called | called
+compartment.importNow('a');    //| not called | not called
+compartment.importNow('c');    //| not called | not called
 ```
 
-### Third-party modules
+### <a name="VirtualModuleSource"></a> Virtual Module Source
 
 To incorporate modules not implemented as JavaScript modules, third-parties may
-implement a `StaticModuleRecord` interface.
-The record must have an `imports` array and an `execute` method.
+implement a `VirtualModuleSource` interface.
+The object must have an `imports` array and an `execute` method.
 The compartment will call `execute` with:
 
 1. the proxied `exports` namespace object,
@@ -442,23 +518,27 @@ The compartment will call `execute` with:
    specified `imports`.
 
 :warning: A future breaking version may allow the `importNow` and the `execute`
-method of third-party static module records to return promises, to support
+method of virtual module sources to return promises, to support
 top-level await.
 
+:warning: The virtual module source interface does not yet agree with the
+[XS](https://www.moddable.com/hardening-xs) implementation of [Hardened
+JavaScript](https://hardenedjs.org/).
+
 ### Compiled modules
 
-Instead of the `StaticModuleRecord` constructor specified for the SES language,
-the SES shim uses compiled static module records as a stand-in.
-These can be created with a `StaticModuleRecord` constructor from a package
-like `@endo/static-module-record`.
-We omitted `StaticModuleRecord` from the SES shim because it entrains a heavy
+Instead of the `ModuleSource` constructor specified for the SES language,
+the SES shim uses compiled module source records as a stand-in.
+These can be created with a `ModuleSource` constructor from a package
+like `@endo/module-source`.
+We omitted `ModuleSource` from the SES shim because it entrains a heavy
 dependency on a JavaScript parser.
-The shim depends upon a `StaticModuleRecord` constructor to analyze and
+The shim depends upon a `ModuleSource` constructor to analyze and
 transform the source of a JavaScript module (known as an ESM or a `.mjs` file)
 into a JavaScript program suitable for evaluation with `compartment.evaluate`
 using a particular calling convention to initialize a module instance.
 
-A compiled static module record has the following shape:
+A compiled module source record has the following shape:
 
 - `imports` is a record that maps partial module specifiers to a list of
   names imported from the corresponding module.
@@ -490,7 +570,7 @@ A compiled static module record has the following shape:
   instead of the evaluation of the `__syncModuleProgram__` string. It will be
   called with the initialization record described above. It is intended to be
   used in environments where eval is not available. Sandboxing of the functor is
-  the responsibility of the author of the StaticModuleRecord.
+  the responsibility of the author of the ModuleSource.
 - `__liveExportsMap__` is a record that maps import names or names in the lexical
   scope of the module to export names, for variables that may change after
   initialization. Any reexported name is assumed to possibly change.
@@ -532,7 +612,7 @@ c.evaluate('console.log("Farewell, World!")', { transforms });
 These transforms do not apply to modules.
 To transform the source of a JavaScript module, the `importHook` must
 intercept the source and transform it before passing it to the
-`StaticModuleRecord` constructor.
+`ModuleSource` constructor.
 These are distinct because programs and modules have distinct grammar
 productions.
 
diff --git a/packages/ses/package.json b/packages/ses/package.json
index 34de978bb..e562a123e 100644
--- a/packages/ses/package.json
+++ b/packages/ses/package.json
@@ -80,7 +80,7 @@
   },
   "devDependencies": {
     "@endo/compartment-mapper": "^1.1.5",
-    "@endo/static-module-record": "^1.1.2",
+    "@endo/module-source": "^0.0.0",
     "@endo/test262-runner": "^0.1.37",
     "ava": "^6.1.3",
     "babel-eslint": "^10.1.0",
diff --git a/packages/ses/src/compartment.js b/packages/ses/src/compartment.js
index 97113f138..8728a0b11 100644
--- a/packages/ses/src/compartment.js
+++ b/packages/ses/src/compartment.js
@@ -161,6 +161,7 @@ defineProperties(InertCompartment, {
  * @param {MakeCompartmentConstructor} targetMakeCompartmentConstructor
  * @param {Record<string, any>} intrinsics
  * @param {(object: object) => void} markVirtualizedNativeFunction
+ * @param {Compartment} [parentCompartment]
  * @returns {Compartment['constructor']}
  */
 
@@ -169,6 +170,7 @@ export const makeCompartmentConstructor = (
   targetMakeCompartmentConstructor,
   intrinsics,
   markVirtualizedNativeFunction,
+  parentCompartment = undefined,
 ) => {
   function Compartment(endowments = {}, moduleMap = {}, options = {}) {
     if (new.target === undefined) {
@@ -218,6 +220,7 @@ export const makeCompartmentConstructor = (
       intrinsics,
       newGlobalPropertyNames: sharedGlobalPropertyNames,
       makeCompartmentConstructor: targetMakeCompartmentConstructor,
+      parentCompartment: this,
       markVirtualizedNativeFunction,
     });
 
@@ -245,6 +248,7 @@ export const makeCompartmentConstructor = (
       __shimTransforms__,
       deferredExports,
       instances,
+      parentCompartment,
     });
   }
 
diff --git a/packages/ses/src/global-object.js b/packages/ses/src/global-object.js
index 8107fac25..468d3296c 100644
--- a/packages/ses/src/global-object.js
+++ b/packages/ses/src/global-object.js
@@ -69,11 +69,12 @@ export const setGlobalObjectConstantProperties = globalObject => {
  * `sharedGlobalPropertyNames`.
  *
  * @param {object} globalObject
- * @param {object} param1
- * @param {object} param1.intrinsics
- * @param {object} param1.newGlobalPropertyNames
- * @param {Function} param1.makeCompartmentConstructor
- * @param {(object) => void} param1.markVirtualizedNativeFunction
+ * @param {object} args
+ * @param {object} args.intrinsics
+ * @param {object} args.newGlobalPropertyNames
+ * @param {Function} args.makeCompartmentConstructor
+ * @param {(object) => void} args.markVirtualizedNativeFunction
+ * @param {Compartment} [args.parentCompartment]
  */
 export const setGlobalObjectMutableProperties = (
   globalObject,
@@ -82,6 +83,7 @@ export const setGlobalObjectMutableProperties = (
     newGlobalPropertyNames,
     makeCompartmentConstructor,
     markVirtualizedNativeFunction,
+    parentCompartment,
   },
 ) => {
   for (const [name, intrinsicName] of entries(universalPropertyNames)) {
@@ -115,6 +117,7 @@ export const setGlobalObjectMutableProperties = (
       makeCompartmentConstructor,
       intrinsics,
       markVirtualizedNativeFunction,
+      parentCompartment,
     ),
   );
 
diff --git a/packages/ses/src/module-load.js b/packages/ses/src/module-load.js
index 5ba805f6e..dff6c35bc 100644
--- a/packages/ses/src/module-load.js
+++ b/packages/ses/src/module-load.js
@@ -10,6 +10,7 @@ import {
   freeze,
   generatorNext,
   generatorThrow,
+  getOwnPropertyNames,
   isObject,
   mapGet,
   mapHas,
@@ -60,10 +61,7 @@ const syncTrampoline = (generatorFunc, args) => {
 // aliases.
 // Both are facilitated by the moduleMap Compartment constructor option.
 export const makeAlias = (compartment, specifier) =>
-  freeze({
-    compartment,
-    specifier,
-  });
+  freeze({ compartment, specifier });
 
 // `resolveAll` pre-computes resolutions of all imports within the compartment
 // in which a module was loaded.
@@ -87,10 +85,7 @@ const loadModuleSource = (
   moduleLoads,
   importMeta,
 ) => {
-  const { resolveHook, moduleRecords } = weakmapGet(
-    compartmentPrivateFields,
-    compartment,
-  );
+  const { resolveHook } = weakmapGet(compartmentPrivateFields, compartment);
 
   // resolve all imports relative to this referrer module.
   const resolvedImports = resolveAll(
@@ -121,8 +116,6 @@ const loadModuleSource = (
     ]);
   }
 
-  // Memoize.
-  mapSet(moduleRecords, moduleSpecifier, moduleRecord);
   return moduleRecord;
 };
 
@@ -135,8 +128,14 @@ function* loadWithoutErrorAnnotation(
   selectImplementation,
   moduleLoads,
 ) {
-  const { importHook, importNowHook, moduleMap, moduleMapHook, moduleRecords } =
-    weakmapGet(compartmentPrivateFields, compartment);
+  const {
+    importHook,
+    importNowHook,
+    moduleMap,
+    moduleMapHook,
+    moduleRecords,
+    parentCompartment,
+  } = weakmapGet(compartmentPrivateFields, compartment);
 
   if (mapHas(moduleRecords, moduleSpecifier)) {
     return mapGet(moduleRecords, moduleSpecifier);
@@ -173,7 +172,7 @@ function* loadWithoutErrorAnnotation(
     throw makeError(
       X`Cannot map module ${q(moduleSpecifier)} to ${q(
         moduleDescriptor,
-      )} in parent compartment, not yet implemented`,
+      )} in parent compartment, use {source} module descriptor`,
       TypeError,
     );
   } else if (isObject(moduleDescriptor)) {
@@ -181,11 +180,183 @@ function* loadWithoutErrorAnnotation(
     // allow a module namespace object to stand in for a module descriptor that
     // describes its original {compartment, specifier} so that it can be used
     // to create a link.
-    const aliasDescriptor = weakmapGet(moduleAliases, moduleDescriptor);
+    let aliasDescriptor = weakmapGet(moduleAliases, moduleDescriptor);
     if (aliasDescriptor !== undefined) {
       moduleDescriptor = aliasDescriptor;
     }
 
+    if (moduleDescriptor.namespace !== undefined) {
+      // { namespace: string, compartment?: Compartment }
+      // Namespace module descriptors link to a module instance.
+
+      if (typeof moduleDescriptor.namespace === 'string') {
+        // The default compartment is the *parent*, not this child compartment.
+        // This is a difference from the legacy {specifier, compartment} module
+        // descriptor.
+        const {
+          compartment: aliasCompartment = parentCompartment,
+          namespace: aliasSpecifier,
+        } = moduleDescriptor;
+        if (
+          !isObject(aliasCompartment) ||
+          !weakmapHas(compartmentPrivateFields, aliasCompartment)
+        ) {
+          throw makeError(
+            X`Invalid compartment in module descriptor for specifier ${q(moduleSpecifier)} in compartment ${q(compartment.name)}`,
+          );
+        }
+        // Behold: recursion.
+        // eslint-disable-next-line no-use-before-define
+        const aliasRecord = yield memoizedLoadWithErrorAnnotation(
+          compartmentPrivateFields,
+          moduleAliases,
+          aliasCompartment,
+          aliasSpecifier,
+          enqueueJob,
+          selectImplementation,
+          moduleLoads,
+        );
+        mapSet(moduleRecords, moduleSpecifier, aliasRecord);
+        return aliasRecord;
+      }
+
+      // All remaining objects must either be a module namespace, or be
+      // promoted into a module namespace with a virtual module source.
+      if (isObject(moduleDescriptor.namespace)) {
+        const { namespace } = moduleDescriptor;
+        // Brand-check SES shim module exports namespaces:
+        aliasDescriptor = weakmapGet(moduleAliases, namespace);
+        if (aliasDescriptor !== undefined) {
+          moduleDescriptor = aliasDescriptor;
+          // Fall through to processing the resulting {compartment, specifier}
+          // alias.
+        } else {
+          // Promote an arbitrary object to a module namespace with a virtual
+          // module source.
+          // { namespace: Object }
+          const exports = getOwnPropertyNames(namespace);
+          /** @type {import('../types.js').VirtualModuleSource} */
+          const moduleSource = {
+            imports: [],
+            exports,
+            execute(env) {
+              for (const name of exports) {
+                env[name] = namespace[name];
+              }
+            },
+          };
+          const importMeta = undefined;
+          const moduleRecord = loadModuleSource(
+            compartmentPrivateFields,
+            moduleAliases,
+            compartment,
+            moduleSpecifier,
+            moduleSource,
+            enqueueJob,
+            selectImplementation,
+            moduleLoads,
+            importMeta,
+          );
+          mapSet(moduleRecords, moduleSpecifier, moduleRecord);
+          return moduleRecord;
+        }
+      } else {
+        throw makeError(
+          X`Invalid compartment in module descriptor for specifier ${q(moduleSpecifier)} in compartment ${q(compartment.name)}`,
+        );
+      }
+    }
+
+    if (moduleDescriptor.source !== undefined) {
+      // Module source descriptors create an instance from a module source.
+      // The descriptor may contain the module source, or refer to a source
+      // loaded in a particular compartment.
+
+      if (typeof moduleDescriptor.source === 'string') {
+        // { source: string, importMeta?, specifier?: string, compartment? }
+        // A string source is the specifier for a different module source.
+        // That source may come from this compartment's parent (default), or
+        // from a specified compartment, and the specified compartment may be
+        // this compartment to make a duplicate.
+
+        const {
+          source: loaderSpecifier,
+          specifier: instanceSpecifier = moduleSpecifier,
+          compartment: loaderCompartment = parentCompartment,
+          importMeta = undefined,
+        } = moduleDescriptor;
+
+        // Induce the compartment, possibly a different compartment
+        // to load a module source.
+
+        // Behold: recursion.
+        // eslint-disable-next-line no-use-before-define
+        const loaderRecord = yield memoizedLoadWithErrorAnnotation(
+          compartmentPrivateFields,
+          moduleAliases,
+          loaderCompartment,
+          loaderSpecifier,
+          enqueueJob,
+          selectImplementation,
+          moduleLoads,
+        );
+
+        // Extract the source of the module from the loader compartment's
+        // record.
+        const { moduleSource } = loaderRecord;
+
+        // Instantiate that source in our own compartment, possibly with a
+        // different specifier for resolving its own imports.
+        const moduleRecord = loadModuleSource(
+          compartmentPrivateFields,
+          moduleAliases,
+          compartment,
+          instanceSpecifier,
+          moduleSource,
+          enqueueJob,
+          selectImplementation,
+          moduleLoads,
+          importMeta,
+        );
+        mapSet(moduleRecords, moduleSpecifier, moduleRecord);
+        return moduleRecord;
+      } else {
+        // { source: ModuleSource, importMeta?, specifier?: string }
+        // We assume all non-string module sources are any of the supported
+        // kinds of module source: PrecompiledModuleSource,
+        // VirtualModuleSource, or a native ModuleSource.
+
+        const {
+          source: moduleSource,
+          specifier: aliasSpecifier = moduleSpecifier,
+          importMeta,
+        } = moduleDescriptor;
+
+        const aliasRecord = loadModuleSource(
+          compartmentPrivateFields,
+          moduleAliases,
+          compartment,
+          aliasSpecifier,
+          moduleSource,
+          enqueueJob,
+          selectImplementation,
+          moduleLoads,
+          importMeta,
+        );
+        mapSet(moduleRecords, moduleSpecifier, aliasRecord);
+        return aliasRecord;
+      }
+    }
+
+    if (moduleDescriptor.archive !== undefined) {
+      // { archive: Archive, path: string }
+      // We do not support this XS-native module descriptor.
+      throw makeError(
+        X`Unsupported archive module descriptor for specifier ${q(moduleSpecifier)} in compartment ${q(compartment.name)}`,
+      );
+    }
+
+    // { record, specifier?, compartment?, importMeta? }
     // A (legacy) module descriptor for when we find the module source (record)
     // but at a different specifier than requested.
     // Providing this {specifier, record} descriptor serves as an ergonomic
@@ -212,9 +383,11 @@ function* loadWithoutErrorAnnotation(
         importMeta,
       );
       mapSet(moduleRecords, moduleSpecifier, aliasRecord);
+      mapSet(moduleRecords, aliasSpecifier, aliasRecord);
       return aliasRecord;
     }
 
+    // { specifier: string, compartment: Compartment }
     // A (legacy) module descriptor that describes a link to a module instance
     // in a specified compartment.
     if (
@@ -248,7 +421,7 @@ function* loadWithoutErrorAnnotation(
     // A (legacy) behavior: If we do not recognize the module descriptor as a
     // module descriptor, we assume that it is a module source (record):
     const moduleSource = moduleDescriptor;
-    return loadModuleSource(
+    const moduleRecord = loadModuleSource(
       compartmentPrivateFields,
       moduleAliases,
       compartment,
@@ -258,6 +431,9 @@ function* loadWithoutErrorAnnotation(
       selectImplementation,
       moduleLoads,
     );
+    // Memoize.
+    mapSet(moduleRecords, moduleSpecifier, moduleRecord);
+    return moduleRecord;
   } else {
     throw makeError(
       X`module descriptor must be a string or object for specifier ${q(
diff --git a/packages/ses/types.d.ts b/packages/ses/types.d.ts
index d95ae4c7b..6ccbab0b1 100644
--- a/packages/ses/types.d.ts
+++ b/packages/ses/types.d.ts
@@ -72,27 +72,32 @@ export interface VirtualModuleSource {
 
 export type ModuleSource = PrecompiledModuleSource | VirtualModuleSource;
 
-export interface RedirectModuleDescriptor {
-  specifier: string;
-  record?: ModuleSource;
+export interface SourceModuleDescriptor {
+  source: string | ModuleSource;
+  specifier?: string;
   importMeta?: any;
+  compartment?: Compartment; // defaults to parent
+}
+
+export interface NamespaceModuleDescriptor {
+  namespace: string | ModuleExportsNamespace;
   compartment?: Compartment;
 }
 
-export interface ReferenceModuleDescriptor {
+// Deprecated in favor of SourceModuleDescriptor,
+// but beware the change in default compartment.
+export interface RecordModuleDescriptor {
   specifier: string;
   record?: ModuleSource;
   importMeta?: any;
-  compartment?: Compartment;
+  compartment?: Compartment; // defaults to self
 }
 
 export type ModuleDescriptor =
-  // These descriptor shapes are needed for XS parity:
-  // | SourceModuleDescriptor
-  // | NamespaceModuleDescriptor
+  | SourceModuleDescriptor
+  | NamespaceModuleDescriptor
   // To be deprecated:
-  | RedirectModuleDescriptor
-  | ReferenceModuleDescriptor
+  | RecordModuleDescriptor
   | ModuleExportsNamespace
   | VirtualModuleSource
   | PrecompiledModuleSource;
@@ -100,7 +105,7 @@ export type ModuleDescriptor =
 // Deprecated type aliases:
 export type PrecompiledStaticModuleInterface = PrecompiledModuleSource;
 export type ThirdPartyStaticModuleInterface = VirtualModuleSource;
-export type RedirectStaticModuleInterface = RedirectModuleDescriptor;
+export type RedirectStaticModuleInterface = RecordModuleDescriptor;
 export type FinalStaticModuleType = ModuleSource;
 export type StaticModuleType = RedirectStaticModuleInterface | ModuleSource;
 
diff --git a/packages/static-module-record/index.js b/packages/static-module-record/index.js
deleted file mode 100644
index 8cb6e65e4..000000000
--- a/packages/static-module-record/index.js
+++ /dev/null
@@ -1,1 +0,0 @@
-export { StaticModuleRecord } from './src/static-module-record.js';
diff --git a/tsconfig.json b/tsconfig.json
index 67d4f935c..340994615 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,7 @@
     "exclude": [
         "packages/eslint-plugin",
         "packages/ses-integration-test",
-        "packages/static-module-record",
+        "packages/module-source",
         "packages/test262-runner",
         "packages/ses/test262",
         "packages/ses/types.test-d.ts",
diff --git a/typedoc.json b/typedoc.json
index 3707eeb67..5515034bf 100644
--- a/typedoc.json
+++ b/typedoc.json
@@ -4,19 +4,19 @@
     ],
     "exclude": [
         "packages/cjs-module-analyzer",
-        "packages/compartment-mapper",
         "packages/cli",
+        "packages/compartment-mapper",
         "packages/daemon",
         "packages/evasive-transform",
         "packages/lp32",
         "packages/memoize",
+        "packages/module-source",
         "packages/netstring",
-        "packages/syrup",
         "packages/skel",
-        "packages/static-module-record",
         "packages/stream-types-test",
-        "packages/where",
-        "packages/test262-runner/"
+        "packages/syrup",
+        "packages/test262-runner/",
+        "packages/where"
     ],
     "name": "Endo API documentation",
     "entryPointStrategy": "packages",
diff --git a/yarn.lock b/yarn.lock
index 8e077e17f..e4671a361 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -317,7 +317,7 @@ __metadata:
   resolution: "@endo/compartment-mapper@workspace:packages/compartment-mapper"
   dependencies:
     "@endo/cjs-module-analyzer": "npm:^1.0.5"
-    "@endo/static-module-record": "npm:^1.1.2"
+    "@endo/module-source": "npm:^0.0.0"
     "@endo/zip": "npm:^1.0.5"
     ava: "npm:^6.1.3"
     babel-eslint: "npm:^10.1.0"
@@ -592,6 +592,29 @@ __metadata:
   languageName: unknown
   linkType: soft
 
+"@endo/module-source@npm:^0.0.0, @endo/module-source@workspace:packages/module-source":
+  version: 0.0.0-use.local
+  resolution: "@endo/module-source@workspace:packages/module-source"
+  dependencies:
+    "@agoric/babel-generator": "npm:^7.17.6"
+    "@babel/parser": "npm:^7.23.6"
+    "@babel/traverse": "npm:^7.23.6"
+    "@babel/types": "npm:^7.24.0"
+    ava: "npm:^6.1.3"
+    babel-eslint: "npm:^10.1.0"
+    benchmark: "npm:^2.1.4"
+    c8: "npm:^7.14.0"
+    eslint: "npm:^8.57.0"
+    eslint-config-airbnb-base: "npm:^15.0.0"
+    eslint-config-prettier: "npm:^9.1.0"
+    eslint-plugin-eslint-comments: "npm:^3.2.0"
+    eslint-plugin-import: "npm:^2.29.1"
+    prettier: "npm:^3.2.5"
+    ses: "npm:^1.5.0"
+    typescript: "npm:5.5.2"
+  languageName: unknown
+  linkType: soft
+
 "@endo/nat@npm:^5.0.7, @endo/nat@workspace:packages/nat":
   version: 0.0.0-use.local
   resolution: "@endo/nat@workspace:packages/nat"
@@ -728,29 +751,6 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@endo/static-module-record@npm:^1.1.2, @endo/static-module-record@workspace:packages/static-module-record":
-  version: 0.0.0-use.local
-  resolution: "@endo/static-module-record@workspace:packages/static-module-record"
-  dependencies:
-    "@agoric/babel-generator": "npm:^7.17.6"
-    "@babel/parser": "npm:^7.23.6"
-    "@babel/traverse": "npm:^7.23.6"
-    "@babel/types": "npm:^7.24.0"
-    ava: "npm:^6.1.3"
-    babel-eslint: "npm:^10.1.0"
-    benchmark: "npm:^2.1.4"
-    c8: "npm:^7.14.0"
-    eslint: "npm:^8.57.0"
-    eslint-config-airbnb-base: "npm:^15.0.0"
-    eslint-config-prettier: "npm:^9.1.0"
-    eslint-plugin-eslint-comments: "npm:^3.2.0"
-    eslint-plugin-import: "npm:^2.29.1"
-    prettier: "npm:^3.2.5"
-    ses: "npm:^1.5.0"
-    typescript: "npm:5.5.2"
-  languageName: unknown
-  linkType: soft
-
 "@endo/stream-node@npm:^1.1.2, @endo/stream-node@workspace:packages/stream-node":
   version: 0.0.0-use.local
   resolution: "@endo/stream-node@workspace:packages/stream-node"
@@ -10357,7 +10357,7 @@ __metadata:
   dependencies:
     "@endo/compartment-mapper": "npm:^1.1.5"
     "@endo/env-options": "npm:^1.1.4"
-    "@endo/static-module-record": "npm:^1.1.2"
+    "@endo/module-source": "npm:^0.0.0"
     "@endo/test262-runner": "npm:^0.1.37"
     ava: "npm:^6.1.3"
     babel-eslint: "npm:^10.1.0"


Test patch:
diff --git a/packages/static-module-record/src/transformSource.js b/packages/module-source/src/transformSource.js
similarity index 100%
rename from packages/static-module-record/src/transformSource.js
rename to packages/module-source/src/transformSource.js
diff --git a/packages/static-module-record/test/benchmark-babel-plugin.js b/packages/module-source/test/benchmark-babel-plugin.js
similarity index 100%
rename from packages/static-module-record/test/benchmark-babel-plugin.js
rename to packages/module-source/test/benchmark-babel-plugin.js
diff --git a/packages/static-module-record/test/fixtures/exportheavy.js b/packages/module-source/test/fixtures/exportheavy.js
similarity index 100%
rename from packages/static-module-record/test/fixtures/exportheavy.js
rename to packages/module-source/test/fixtures/exportheavy.js
diff --git a/packages/static-module-record/test/fixtures/immer.js b/packages/module-source/test/fixtures/immer.js
similarity index 100%
rename from packages/static-module-record/test/fixtures/immer.js
rename to packages/module-source/test/fixtures/immer.js
diff --git a/packages/static-module-record/test/fixtures/large.js b/packages/module-source/test/fixtures/large.js
similarity index 100%
rename from packages/static-module-record/test/fixtures/large.js
rename to packages/module-source/test/fixtures/large.js
diff --git a/packages/static-module-record/test/fixtures/small.js b/packages/module-source/test/fixtures/small.js
similarity index 100%
rename from packages/static-module-record/test/fixtures/small.js
rename to packages/module-source/test/fixtures/small.js
diff --git a/packages/static-module-record/test/lockdown.js b/packages/module-source/test/lockdown.js
similarity index 100%
rename from packages/static-module-record/test/lockdown.js
rename to packages/module-source/test/lockdown.js
diff --git a/packages/static-module-record/test/static-module-record.test.js b/packages/module-source/test/module-source.test.js
similarity index 92%
rename from packages/static-module-record/test/static-module-record.test.js
rename to packages/module-source/test/module-source.test.js
index 1acb0c28c..833ec7887 100644
--- a/packages/static-module-record/test/static-module-record.test.js
+++ b/packages/module-source/test/module-source.test.js
@@ -3,7 +3,7 @@
 /* eslint no-underscore-dangle: ["off"] */
 
 // eslint-disable-next-line import/order
-import { StaticModuleRecord } from '../src/static-module-record.js';
+import { ModuleSource } from '../src/module-source.js';
 import './lockdown.js';
 import url from 'url';
 import fs from 'fs';
@@ -18,8 +18,8 @@ import test from 'ava';
 /** @typedef {Map<string, Map<string, Array<Updater>>>} ImportUpdaters */
 
 /**
- * @param {TestContext} t
- * @param {StaticModuleRecord} record
+ * @param {ExecutionContext} t
+ * @param {ModuleSource} record
  */
 function assertDefaultExport(t, record) {
   t.deepEqual(record.imports, []);
@@ -38,7 +38,7 @@ function readFixture(filename) {
 
 test('export default', t => {
   t.plan(8);
-  const record = new StaticModuleRecord('export default bb;');
+  const record = new ModuleSource('export default bb;');
   assertDefaultExport(t, record);
 
   const compartment = new Compartment({
@@ -61,7 +61,7 @@ test('export default', t => {
 });
 
 /**
- * @param {TestContext} t
+ * @param {ExecutionContext} t
  * @param {string} source
  * @param {object} [options]
  * @param {object} [options.endowments]
@@ -69,7 +69,7 @@ test('export default', t => {
  */
 function initialize(t, source, options = {}) {
   const { endowments, imports = new Map() } = options;
-  const record = new StaticModuleRecord(source);
+  const record = new ModuleSource(source);
   // t.log(record.__syncModuleProgram__);
   const liveUpdaters = {};
   const onceUpdaters = {};
@@ -444,17 +444,13 @@ export default async function (arg) { return arg; }
 
 test('zero width joiner is reserved', t => {
   t.throws(() => {
-    const _ = new StaticModuleRecord(
-      `const $h\u200d_import = 123; $h\u200d_import`,
-    );
+    const _ = new ModuleSource(`const $h\u200d_import = 123; $h\u200d_import`);
   });
 });
 
 test('zero width joiner in constified variable is reserved', t => {
   t.throws(() => {
-    const _ = new StaticModuleRecord(
-      `const $c\u200d_myVar = 123; $c\u200d_myVar`,
-    );
+    const _ = new ModuleSource(`const $c\u200d_myVar = 123; $c\u200d_myVar`);
   });
 });
 
@@ -484,7 +480,7 @@ export default new outer().f();
 });
 
 test('nested export fails as syntax', t => {
-  t.throws(() => new StaticModuleRecord(`{ void 0; export default null; }`), {
+  t.throws(() => new ModuleSource(`{ void 0; export default null; }`), {
     instanceOf: SyntaxError,
   });
 });
@@ -586,11 +582,11 @@ test('import meta in export', t => {
   t.is(namespace.a, 'ok file://meta.url');
 });
 test('import meta member present', t => {
-  const record = new StaticModuleRecord(`const a = import.meta.url`);
+  const record = new ModuleSource(`const a = import.meta.url`);
   t.is(record.__needsImportMeta__, true);
 });
 test('import meta present', t => {
-  const record = new StaticModuleRecord(`const a = import.meta`);
+  const record = new ModuleSource(`const a = import.meta`);
   t.is(record.__needsImportMeta__, true);
 });
 
@@ -660,27 +656,27 @@ test('export all', t => {
 // TODO cross product let, class, maybe var:
 
 test('Object.hasOwnProperty override mistake should not crash transform', t => {
-  const { __fixedExportMap__ } = new StaticModuleRecord(`
+  const { __fixedExportMap__ } = new ModuleSource(`
     const { hasOwnProperty } = Object;
     export { hasOwnProperty };
   `);
   t.deepEqual(__fixedExportMap__, { hasOwnProperty: ['hasOwnProperty'] });
 
-  const { __liveExportMap__ } = new StaticModuleRecord(`
+  const { __liveExportMap__ } = new ModuleSource(`
     let hop = 1;
     ({ hasOwnProperty: hop } = Object);
     export { hop as hasOwnProperty };
   `);
   t.deepEqual(__liveExportMap__, { hasOwnProperty: ['hop', true] });
 
-  const { imports } = new StaticModuleRecord(`
+  const { imports } = new ModuleSource(`
     import { hasOwnProperty } from 'hasOwnProperty';
   `);
   t.deepEqual(imports, ['hasOwnProperty']);
 });
 
 test('export function should be fixed when not assigned', t => {
-  const { __fixedExportMap__, __liveExportMap__ } = new StaticModuleRecord(`
+  const { __fixedExportMap__, __liveExportMap__ } = new ModuleSource(`
     export function work() {}
   `);
   t.deepEqual(__fixedExportMap__, {
@@ -690,7 +686,7 @@ test('export function should be fixed when not assigned', t => {
 });
 
 test('export function should be live when assigned', t => {
-  const { __fixedExportMap__, __liveExportMap__ } = new StaticModuleRecord(`
+  const { __fixedExportMap__, __liveExportMap__ } = new ModuleSource(`
     export function work() {}
     work = () => {};
   `);
@@ -701,7 +697,7 @@ test('export function should be live when assigned', t => {
 });
 
 test('export const name as default', t => {
-  const { __fixedExportMap__, __liveExportMap__ } = new StaticModuleRecord(`
+  const { __fixedExportMap__, __liveExportMap__ } = new ModuleSource(`
     const meaning = 42;
     export { meaning as default };
   `);
@@ -717,7 +713,7 @@ test('export name as default from', t => {
     __fixedExportMap__,
     __liveExportMap__,
     __reexportMap__,
-  } = new StaticModuleRecord(`
+  } = new ModuleSource(`
     export { meaning as default } from './meaning.js';
   `);
   // t.log(__syncModuleProgram__);
@@ -730,7 +726,7 @@ test('export name as default from', t => {
 
 test('source map generation', t => {
   t.plan(5);
-  const { __syncModuleProgram__ } = new StaticModuleRecord(`'Hello, World!'`, {
+  const { __syncModuleProgram__ } = new ModuleSource(`'Hello, World!'`, {
     sourceUrl: 'must-appear-in-source.js',
     sourceMapUrl: 'must-not-appear-in-source.js',
     sourceMapHook(sourceMap, { sourceUrl, sourceMapUrl, source }) {
@@ -745,13 +741,13 @@ test('source map generation', t => {
 });
 
 // Regression test for #823
-test('static module records can name Map in scope', t => {
+test('module sources can name Map in scope', t => {
   t.notThrows(() => initialize(t, `const { Map } = globalThis;`));
 });
 
 // Regression test for comment duplication
-test('static module records do not duplicate comments', t => {
-  const { __syncModuleProgram__: program } = new StaticModuleRecord(`
+test('module sources do not duplicate comments', t => {
+  const { __syncModuleProgram__: program } = new ModuleSource(`
     let hi = 'hi';
     /* I both lead and follow */
     let bye = 'bye';
@@ -761,7 +757,7 @@ test('static module records do not duplicate comments', t => {
 
 // Regression test for immer@9.0.6
 test('should handle package "immer" source', t => {
-  const { __fixedExportMap__, __liveExportMap__ } = new StaticModuleRecord(
+  const { __fixedExportMap__, __liveExportMap__ } = new ModuleSource(
     readFixture('fixtures/immer.js'),
   );
   t.deepEqual(__fixedExportMap__, {
@@ -794,7 +790,7 @@ test('should handle package "immer" source', t => {
 
 // https://github.com/endojs/endo/issues/2094
 test.failing('should support export of defaulted extraction', t => {
-  const _ = new StaticModuleRecord(`
+  const _ = new ModuleSource(`
     const { x, y = x } = globalThis;
     export { x, y };
   `);
diff --git a/packages/ses/test/compartment-transforms.test.js b/packages/ses/test/compartment-transforms.test.js
index 38fc5bd23..04115b515 100644
--- a/packages/ses/test/compartment-transforms.test.js
+++ b/packages/ses/test/compartment-transforms.test.js
@@ -1,9 +1,6 @@
-// Importing StaticModuleRecord before lockdown is necessary because of an
-// incompatibility in the Babel dependency: There is a conflict with the
-// prototype override mistake.
-import { StaticModuleRecord } from '@endo/static-module-record';
 import '../index.js';
 import './lockdown-safe.js';
+import { ModuleSource } from '@endo/module-source';
 // Placing the ava import last demonstrates that ava itself is compatible with SES
 import test from 'ava';
 
@@ -54,7 +51,7 @@ test('transforms do not apply to imported modules', async t => {
   const transforms = [transform];
   const resolveHook = () => '';
   const importHook = () =>
-    new StaticModuleRecord('export default "Farewell, World!";');
+    new ModuleSource('export default "Farewell, World!";');
   const c = new Compartment({}, {}, { transforms, resolveHook, importHook });
 
   const { namespace } = await c.import('any-string-here');
@@ -81,7 +78,7 @@ test('__shimTransforms__ do apply to imported modules', async t => {
   const transforms = [transform];
   const resolveHook = () => '';
   const importHook = () =>
-    new StaticModuleRecord('export default "Farewell, World!";');
+    new ModuleSource('export default "Farewell, World!";');
   const c = new Compartment(
     {},
     {},
diff --git a/packages/ses/test/import-cjs.test.js b/packages/ses/test/import-cjs.test.js
index f43f37b67..efea862a4 100644
--- a/packages/ses/test/import-cjs.test.js
+++ b/packages/ses/test/import-cjs.test.js
@@ -1,5 +1,5 @@
 import test from 'ava';
-import { StaticModuleRecord } from '@endo/static-module-record';
+import { ModuleSource } from '@endo/module-source';
 import { resolveNode } from './node.js';
 import '../index.js';
 import { freeze, keys } from '../src/commons.js';
@@ -25,15 +25,15 @@ function heuristicAnalysis(moduleSource) {
   };
 }
 
-const CjsStaticModuleRecord = (moduleSource, moduleLocation) => {
+const CjsModuleSource = (moduleSource, moduleLocation) => {
   if (typeof moduleSource !== 'string') {
     throw TypeError(
-      `Cannot create CommonJS static module record, module source must be a string, got ${moduleSource}`,
+      `Cannot create CommonJS virtual module source, module source must be a string, got ${moduleSource}`,
     );
   }
   if (typeof moduleLocation !== 'string') {
     throw TypeError(
-      `Cannot create CommonJS static module record, module location must be a string, got ${moduleLocation}`,
+      `Cannot create CommonJS virtual module source, module location must be a string, got ${moduleLocation}`,
     );
   }
 
@@ -78,7 +78,7 @@ test('import a CommonJS module with exports assignment', async t => {
 
   const resolveHook = resolveNode;
   const importHook = async () => {
-    return CjsStaticModuleRecord(
+    return CjsModuleSource(
       `
       exports.meaning = 42;
     `,
@@ -101,7 +101,7 @@ test('import a CommonJS module with exports replacement', async t => {
 
   const resolveHook = resolveNode;
   const importHook = async () => {
-    return CjsStaticModuleRecord(
+    return CjsModuleSource(
       `
       module.exports = 42;
     `,
@@ -125,7 +125,7 @@ test('CommonJS module imports CommonJS module by name', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './even') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         exports.even = n => n % 2 === 0;
       `,
@@ -133,7 +133,7 @@ test('CommonJS module imports CommonJS module by name', async t => {
       );
     }
     if (specifier === './odd') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         const { even } = require('./even');
         exports.odd = n => !even(n);
@@ -159,7 +159,7 @@ test('CommonJS module imports CommonJS module as default', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './even') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         module.exports = n => n % 2 === 0;
       `,
@@ -167,7 +167,7 @@ test('CommonJS module imports CommonJS module as default', async t => {
       );
     }
     if (specifier === './odd') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         const even = require('./even');
         module.exports = n => !even(n);
@@ -192,7 +192,7 @@ test('ESM imports CommonJS module as default', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './even') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         module.exports = n => n % 2 === 0;
       `,
@@ -200,7 +200,7 @@ test('ESM imports CommonJS module as default', async t => {
       );
     }
     if (specifier === './odd') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         import even from './even';
         export default n => !even(n);
@@ -226,7 +226,7 @@ test('ESM imports CommonJS module as star', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './even') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         exports.even = n => n % 2 === 0;
       `,
@@ -234,7 +234,7 @@ test('ESM imports CommonJS module as star', async t => {
       );
     }
     if (specifier === './odd') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         import * as evens from './even';
         export default n => !evens.even(n);
@@ -260,7 +260,7 @@ test('ESM imports CommonJS module with replaced exports as star', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './even') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         module.exports = { even: n => n % 2 === 0 };
       `,
@@ -269,8 +269,8 @@ test('ESM imports CommonJS module with replaced exports as star', async t => {
     }
     if (specifier === './odd') {
       // It should be evens.even(n) not evens.default.even(n)
-      // but CjsStaticModuleRecord is not enough to handle that
-      return new StaticModuleRecord(
+      // but CjsModuleSource is not enough to handle that
+      return new ModuleSource(
         `
         import * as evens from './even';
         export default n => !evens.default.even(n);
@@ -296,7 +296,7 @@ test('ESM imports CommonJS module by name', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './even') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         exports.even = n => n % 2 === 0;
       `,
@@ -304,7 +304,7 @@ test('ESM imports CommonJS module by name', async t => {
       );
     }
     if (specifier === './odd') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         import { even } from './even';
         export default n => !even(n);
@@ -330,7 +330,7 @@ test('CommonJS module imports ESM as default', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './even') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         export default n => n % 2 === 0;
       `,
@@ -338,7 +338,7 @@ test('CommonJS module imports ESM as default', async t => {
       );
     }
     if (specifier === './odd') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
           const even = require('./even');
           module.exports = n => !even(n);
@@ -364,7 +364,7 @@ test('CommonJS module imports ESM by name', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './even') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         export function even(n) {
          return n % 2 === 0;
@@ -374,7 +374,7 @@ test('CommonJS module imports ESM by name', async t => {
       );
     }
     if (specifier === './odd') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
           const { even } = require('./even');
           exports.odd = n => !even(n);
@@ -400,7 +400,7 @@ test('cross import ESM and CommonJS modules', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './src/main.js') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         const direct = require('./other.js');
         const indirect = require("./helper.mjs");
@@ -414,14 +414,14 @@ test('cross import ESM and CommonJS modules', async t => {
       );
     }
     if (specifier === './src/helper.mjs') {
-      return new StaticModuleRecord(`
+      return new ModuleSource(`
         export * from './other.js';
         import d from './default.js';
         export const c = d;
       `);
     }
     if (specifier === './src/other.js') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         exports.a = 10;
         exports.b = 20;
@@ -430,7 +430,7 @@ test('cross import ESM and CommonJS modules', async t => {
       );
     }
     if (specifier === './src/default.js') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         exports.default = 30;
       `,
@@ -448,7 +448,7 @@ 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(
+      return CjsModuleSource(
         `
         const dep = require("./intermediate.mjs");
         t.is(dep.value, undefined);
@@ -460,7 +460,7 @@ test('live bindings through through an ESM between CommonJS modules', async t =>
       );
     }
     if (specifier === './src/intermediate.mjs') {
-      return new StaticModuleRecord(`
+      return new ModuleSource(`
         export * from './value.js';
         import { value } from './value.js';
         export function check() {
@@ -469,7 +469,7 @@ test('live bindings through through an ESM between CommonJS modules', async t =>
       `);
     }
     if (specifier === './src/value.js') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         exports.reanimate = () => {
           exports.value = 30;
@@ -490,7 +490,7 @@ test('export name as default from CommonJS module', async t => {
 
   const importHook = async specifier => {
     if (specifier === './meaning.cjs') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         exports.meaning = 42;
       `,
@@ -498,12 +498,12 @@ test('export name as default from CommonJS module', async t => {
       );
     }
     if (specifier === './meaning.mjs') {
-      return new StaticModuleRecord(`
+      return new ModuleSource(`
         export { meaning as default } from './meaning.cjs';
       `);
     }
     if (specifier === './main.js') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         import meaning from './meaning.mjs';
         t.is(meaning, 42);
@@ -531,7 +531,7 @@ test('synchronous loading via importNowHook', async t => {
 
   const importNowHook = specifier => {
     if (specifier === './meaning.cjs') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         exports.meaning = 42;
       `,
@@ -539,12 +539,12 @@ test('synchronous loading via importNowHook', async t => {
       );
     }
     if (specifier === './meaning.mjs') {
-      return new StaticModuleRecord(`
+      return new ModuleSource(`
         export { meaning as default } from './meaning.cjs';
       `);
     }
     if (specifier === './main.js') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         import meaning from './meaning.mjs';
         t.is(meaning, 42);
@@ -576,12 +576,12 @@ test('importNowHook only called if specifier was not imported before', async t =
       throw Error(`importNowHook should not be called to get ./meaning.cjs`);
     }
     if (specifier === './meaning.mjs') {
-      return new StaticModuleRecord(`
+      return new ModuleSource(`
         export { meaning as default } from './meaning.cjs';
       `);
     }
     if (specifier === './main.js') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         import meaning from './meaning.mjs';
         t.is(meaning, 42);
@@ -593,7 +593,7 @@ test('importNowHook only called if specifier was not imported before', async t =
   };
   const importHook = async specifier => {
     if (specifier === './meaning.cjs') {
-      return CjsStaticModuleRecord(
+      return CjsModuleSource(
         `
         exports.meaning = 42;
       `,
diff --git a/packages/ses/test/import-commons.js b/packages/ses/test/import-commons.js
index 8f586f116..bd4b03663 100644
--- a/packages/ses/test/import-commons.js
+++ b/packages/ses/test/import-commons.js
@@ -1,4 +1,4 @@
-import { StaticModuleRecord } from '@endo/static-module-record';
+import { ModuleSource } from '@endo/module-source';
 
 // q, to quote strings in error messages.
 const q = JSON.stringify;
@@ -23,5 +23,5 @@ export const makeStaticRetriever = sources => {
 export const makeImporter = (locate, retrieve) => async moduleSpecifier => {
   const moduleLocation = locate(moduleSpecifier);
   const string = await retrieve(moduleLocation);
-  return new StaticModuleRecord(string, moduleLocation);
+  return new ModuleSource(string, moduleLocation);
 };
diff --git a/packages/ses/test/import-hook-legacy.test.js b/packages/ses/test/import-hook-legacy.test.js
new file mode 100644
index 000000000..0fbf72890
--- /dev/null
+++ b/packages/ses/test/import-hook-legacy.test.js
@@ -0,0 +1,239 @@
+// These tests exercise the Compartment importHook.
+
+/* eslint max-lines: 0 */
+
+import test from 'ava';
+import { ModuleSource } from '@endo/module-source';
+import '../index.js';
+
+test('import hook returns module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return new ModuleSource('export default 42');
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import hook returns module record descriptor with module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return new ModuleSource('export default 42');
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import hook returns virtual module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            imports: [],
+            exports: ['default'],
+            execute(env) {
+              env.default = 42;
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import hook returns virtual module record descriptor', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            record: {
+              imports: [],
+              exports: ['default'],
+              execute(env) {
+                env.default = 42;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+// This case requires the module loader to take care not to attempt to
+// coerce the module namespace object to a promise.
+test('import hook returns module namespace using module method', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === '.') {
+          return compartment.module('./index.js');
+        }
+        if (specifier === './index.js') {
+          return new ModuleSource('export default 42');
+        }
+        return undefined;
+      },
+    },
+  );
+  // Unlike import, importNow does not box the namespace.
+  const { namespace: index } = await compartment.import('.');
+  t.is(index.default, 42);
+});
+
+test('import hook returns module namespace using import method', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === '.') {
+          return compartment.import('./index.js');
+        }
+        if (specifier === './index.js') {
+          return new ModuleSource('export default 42');
+        }
+        return undefined;
+      },
+    },
+  );
+  // Unlike import, importNow does not box the namespace.
+  const { namespace: index } = await compartment.import('.');
+  t.is(index.default, 42);
+});
+
+test('import hook returns compartment and specifier module descriptor', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === '.') {
+          return { compartment, specifier: './index.js' };
+        }
+        if (specifier === './index.js') {
+          return new ModuleSource('export default 42');
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('.');
+  t.is(index.default, 42);
+});
+
+test('import hook returns record and specifier module descriptor', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === '.') {
+          return {
+            record: new ModuleSource('export default 42'),
+            specifier: './index.js',
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('.');
+  t.is(index.default, 42);
+});
+
+test('import hook returns record and specifier module descriptor and import specifiers resolve from response specifier', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './src/peer.js': new ModuleSource('export const number = 42'),
+    },
+    // options:
+    {
+      resolveHook(importSpecifier, moduleSpecifier) {
+        if (
+          moduleSpecifier === './src/index.js' &&
+          importSpecifier === './peer.js'
+        ) {
+          return './src/peer.js';
+        }
+        return importSpecifier;
+      },
+      importHook(specifier) {
+        if (specifier === '.') {
+          return {
+            record: new ModuleSource('export * from "./peer.js"'),
+            specifier: './src/index.js',
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('.');
+  t.is(index.number, 42);
+});
diff --git a/packages/ses/test/import-hook.test.js b/packages/ses/test/import-hook.test.js
new file mode 100644
index 000000000..6a3cb4c8c
--- /dev/null
+++ b/packages/ses/test/import-hook.test.js
@@ -0,0 +1,590 @@
+// These tests exercise the Compartment importHook.
+
+/* eslint max-lines: 0 */
+
+import test from 'ava';
+import { ModuleSource } from '@endo/module-source';
+import '../index.js';
+
+test('import hook returns module source descriptor with precompiled module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: new ModuleSource('export default 42'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import hook returns module source descriptor with virtual module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['default'],
+              execute(env) {
+                env.default = 42;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import hook returns parent compartment module source descriptor with string reference to parent compartment', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === './meaning.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['default'],
+              execute(env) {
+                env.default = 42;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './meaning.js',
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import hook returns parent compartment module source reference with different specifier', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'parent',
+      resolveHook(relative, specifier) {
+        t.is(relative, './meaningful.js');
+        t.is(specifier, './meaning.js');
+        return relative;
+      },
+      importHook(specifier) {
+        if (specifier === './meaningful.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['meaning'],
+              execute() {
+                throw new Error('should not execute');
+              },
+            },
+          };
+        } else if (specifier === './meaning.js') {
+          return {
+            source: {
+              imports: ['./meaningful.js'],
+              exports: ['default'],
+              execute(env, c, resolutions) {
+                // eslint-disable-next-line no-use-before-define
+                t.is(c, compartment);
+                const { meaning } = c.importNow(resolutions['./meaningful.js']);
+                env.default = meaning;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      resolveHook(relative, specifier) {
+        t.is(relative, './meaningful.js');
+        t.is(specifier, './lib/meaning.js');
+        return './lib/meaningful.js';
+      },
+      importHook(specifier) {
+        if (specifier === './lib/meaningful.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['meaning'],
+              execute(env) {
+                env.meaning = 42;
+              },
+            },
+          };
+        } else if (specifier === './index.js') {
+          return {
+            source: './meaning.js',
+            specifier: './lib/meaning.js',
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import hook returns module source descriptor for parent compartment with string reference', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'parent',
+      importHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: parentObject },
+  } = await parent.import('./object.js');
+  t.is(parentObject.meaning, 42);
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            // implies parent compartment
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: childObject },
+  } = await compartment.import('./index.js');
+  t.is(childObject.meaning, 42);
+  // Separate instances
+  t.not(childObject, parentObject);
+});
+
+test('import hook returns parent compartment module namespace descriptor', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'parent',
+      importHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: parentObject },
+  } = await parent.import('./object.js');
+  t.is(parentObject.meaning, 42);
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            namespace: './object.js',
+            // implies parent compartment
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: childObject },
+  } = await compartment.import('./index.js');
+  t.is(childObject.meaning, 42);
+  // Same instances
+  t.is(childObject, parentObject);
+});
+
+test('import hook returns module source descriptor with string reference to parent compartment', async t => {
+  const compartment1 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'compartment1',
+      importHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment1.import('./object.js');
+  t.is(object1.meaning, 42);
+
+  const compartment2 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            compartment: compartment1,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object2 },
+  } = await compartment2.import('./index.js');
+  t.is(object2.meaning, 42);
+  // Separate instances
+  t.not(object1, object2);
+});
+
+test('import hook returns other compartment module namespace descriptor', async t => {
+  const compartment1 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'compartment1',
+      importHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment1.import('./object.js');
+  t.is(object1.meaning, 42);
+
+  const compartment2 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            namespace: './object.js',
+            compartment: compartment1,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object2 },
+  } = await compartment2.import('./index.js');
+  t.is(object2.meaning, 42);
+  // Same instances
+  t.is(object1, object2);
+});
+
+test('import hook returns module namespace descriptor and namespace object', async t => {
+  const compartment1 = new Compartment(
+    {},
+    {},
+    {
+      importHook(specifier) {
+        if (specifier === 'a') {
+          return {
+            source: new ModuleSource(`export default 42`),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: namespace1 } = await compartment1.import('a');
+  const compartment2 = new Compartment(
+    {},
+    {},
+    {
+      importHook(specifier) {
+        if (specifier === 'z') {
+          return { namespace: namespace1 };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: namespace2 } = await compartment2.import('z');
+  t.is(namespace2.default, 42);
+  t.is(namespace1, namespace2);
+});
+
+test('import hook returns module namespace descriptor and non-namespace object', async t => {
+  const compartment = new Compartment(
+    {},
+    {},
+    {
+      importHook(specifier) {
+        if (specifier === '1') {
+          return { namespace: { meaning: 42 } };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace } = await compartment.import('1');
+  t.is(namespace.meaning, 42);
+});
+
+test('import hook returns module source descriptor for specifier in own compartment', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            compartment,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment.import('./object.js');
+  t.is(object1.meaning, 42);
+  const {
+    namespace: { default: object2 },
+  } = await compartment.import('./index.js');
+  t.is(object2.meaning, 42);
+  // Separate instances
+  t.not(object1, object2);
+});
+
+test('import hook returns module source descriptor for specifier in own compartment and overridden base specifier that collides', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            specifier: './object.js',
+            compartment,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment.import('./object.js');
+  t.is(object1.meaning, 42);
+  const {
+    namespace: { 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);
+});
+
+test('import hook returns module namespace descriptor for specifier in own compartment', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      importHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            namespace: './object.js',
+            compartment,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment.import('./object.js');
+  t.is(object1.meaning, 42);
+  const {
+    namespace: { default: object2 },
+  } = await compartment.import('./index.js');
+  t.is(object2.meaning, 42);
+  // Same instances
+  t.is(object1, object2);
+});
+
+test('module map hook precedes import hook', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: new ModuleSource(`
+              export default 42;
+            `),
+          };
+        }
+        return undefined;
+      },
+      importHook() {
+        throw new Error('not reached');
+      },
+    },
+  );
+
+  const {
+    namespace: { default: meaning },
+  } = await compartment.import('./index.js');
+  t.is(meaning, 42);
+});
diff --git a/packages/ses/test/import-non-esm.test.js b/packages/ses/test/import-non-esm.test.js
index 1318c71e8..6cf0fe7d1 100644
--- a/packages/ses/test/import-non-esm.test.js
+++ b/packages/ses/test/import-non-esm.test.js
@@ -1,5 +1,5 @@
 import test from 'ava';
-import { StaticModuleRecord } from '@endo/static-module-record';
+import { ModuleSource } from '@endo/module-source';
 import { resolveNode } from './node.js';
 import '../index.js';
 
@@ -114,7 +114,7 @@ test('ESM imports non-ESM as default', async t => {
       };
     }
     if (specifier === './odd') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         import even from './even';
         export default n => !even(n);
@@ -149,7 +149,7 @@ test('ESM imports non-ESM by name', async t => {
       };
     }
     if (specifier === './odd') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         import { even } from './even';
         export const odd = n => !even(n);
@@ -175,7 +175,7 @@ test('non-ESM imports ESM as default', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './even') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         export default n => n % 2 === 0;
       `,
@@ -210,7 +210,7 @@ test('non-ESM imports ESM by name', async t => {
   const resolveHook = resolveNode;
   const importHook = async specifier => {
     if (specifier === './even') {
-      return new StaticModuleRecord(
+      return new ModuleSource(
         `
         export const even = n => n % 2 === 0;
       `,
@@ -262,7 +262,7 @@ test('cross import ESM and non-ESMs', async t => {
       };
     }
     if (specifier === './src/helper.mjs') {
-      return new StaticModuleRecord(`
+      return new ModuleSource(`
         export * from './other.js';
         import d from './default.js';
         export const c = d;
diff --git a/packages/ses/test/import-now-hook-legacy.test.js b/packages/ses/test/import-now-hook-legacy.test.js
new file mode 100644
index 000000000..a8549cf9b
--- /dev/null
+++ b/packages/ses/test/import-now-hook-legacy.test.js
@@ -0,0 +1,110 @@
+// These tests exercise legacy module descriptor shapes for the Compartment
+// importNowHook.
+
+/* eslint max-lines: 0 */
+
+import test from 'ava';
+import { ModuleSource } from '@endo/module-source';
+import '../index.js';
+
+test('import now hook returns precompiled module source', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return new ModuleSource('export default 42');
+        }
+        return undefined;
+      },
+    },
+  );
+  const index = compartment.importNow('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import now hook returns virtual module source', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            imports: [],
+            exports: ['default'],
+            execute(env) {
+              env.default = 42;
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const index = compartment.importNow('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import now hook returns virtual module record descriptor', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            record: {
+              imports: [],
+              exports: ['default'],
+              execute(env) {
+                env.default = 42;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const index = compartment.importNow('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import now hook returns namespace using module method', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importNowHook(specifier) {
+        if (specifier === '.') {
+          return compartment.module('./index.js');
+        }
+        if (specifier === './index.js') {
+          return new ModuleSource('export default 42');
+        }
+        return undefined;
+      },
+    },
+  );
+  // Unlike import, importNow does not box the namespace.
+  const index = compartment.importNow('.');
+  t.is(index.default, 42);
+});
diff --git a/packages/ses/test/import-now-hook.test.js b/packages/ses/test/import-now-hook.test.js
new file mode 100644
index 000000000..4f5755a71
--- /dev/null
+++ b/packages/ses/test/import-now-hook.test.js
@@ -0,0 +1,558 @@
+// These tests exercise the Compartment importNowHook.
+
+/* eslint max-lines: 0 */
+
+import test from 'ava';
+import { ModuleSource } from '@endo/module-source';
+import '../index.js';
+
+test('import now hook returns module source descriptor with precompiled module source', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return { source: new ModuleSource('export default 42') };
+        }
+        return undefined;
+      },
+    },
+  );
+  const index = compartment.importNow('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import now hook returns module source descriptor with virtual module source', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['default'],
+              execute(env) {
+                env.default = 42;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const index = compartment.importNow('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import now hook returns parent compartment module source descriptor with string reference to parent compartment', t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importNowHook(specifier) {
+        if (specifier === './meaning.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['default'],
+              execute(env) {
+                env.default = 42;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './meaning.js',
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const index = compartment.importNow('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import now hook returns parent compartment module source reference with different specifier', t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'parent',
+      resolveHook(relative, specifier) {
+        t.is(relative, './meaningful.js');
+        t.is(specifier, './meaning.js');
+        return relative;
+      },
+      importNowHook(specifier) {
+        if (specifier === './meaningful.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['meaning'],
+              execute() {
+                throw new Error('should not execute');
+              },
+            },
+          };
+        } else if (specifier === './meaning.js') {
+          return {
+            source: {
+              imports: ['./meaningful.js'],
+              exports: ['default'],
+              execute(env, c, resolutions) {
+                // eslint-disable-next-line no-use-before-define
+                t.is(c, compartment);
+                const { meaning } = c.importNow(resolutions['./meaningful.js']);
+                env.default = meaning;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      resolveHook(relative, specifier) {
+        t.is(relative, './meaningful.js');
+        t.is(specifier, './lib/meaning.js');
+        return './lib/meaningful.js';
+      },
+      importNowHook(specifier) {
+        if (specifier === './lib/meaningful.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['meaning'],
+              execute(env) {
+                env.meaning = 42;
+              },
+            },
+          };
+        } else if (specifier === './index.js') {
+          return {
+            source: './meaning.js',
+            specifier: './lib/meaning.js',
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const index = compartment.importNow('./index.js');
+  t.is(index.default, 42);
+});
+
+test('import now hook returns module source descriptor for parent compartment with string reference', t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'parent',
+      importNowHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: parentObject } = parent.importNow('./object.js');
+  t.is(parentObject.meaning, 42);
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            // implies parent compartment
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: childObject } = compartment.importNow('./index.js');
+  t.is(childObject.meaning, 42);
+  // Separate instances
+  t.not(childObject, parentObject);
+});
+
+test('import now hook returns parent compartment module namespace descriptor', t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'parent',
+      importNowHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: parentObject } = parent.importNow('./object.js');
+  t.is(parentObject.meaning, 42);
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            namespace: './object.js',
+            // implies parent compartment
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: childObject } = compartment.importNow('./index.js');
+  t.is(childObject.meaning, 42);
+  // Same instances
+  t.is(childObject, parentObject);
+});
+
+test('import now hook returns module source descriptor with string reference to parent compartment', t => {
+  const compartment1 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'compartment1',
+      importNowHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: object1 } = compartment1.importNow('./object.js');
+  t.is(object1.meaning, 42);
+
+  const compartment2 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            compartment: compartment1,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: object2 } = compartment2.importNow('./index.js');
+  t.is(object2.meaning, 42);
+  // Separate instances
+  t.not(object1, object2);
+});
+
+test('import now hook returns other compartment module namespace descriptor', t => {
+  const compartment1 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'compartment1',
+      importNowHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: object1 } = compartment1.importNow('./object.js');
+  t.is(object1.meaning, 42);
+
+  const compartment2 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            namespace: './object.js',
+            compartment: compartment1,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: object2 } = compartment2.importNow('./index.js');
+  t.is(object2.meaning, 42);
+  // Same instances
+  t.is(object1, object2);
+});
+
+test('import now hook returns module namespace descriptor and namespace object', t => {
+  const compartment1 = new Compartment(
+    {},
+    {},
+    {
+      importNowHook(specifier) {
+        if (specifier === 'a') {
+          return {
+            source: new ModuleSource(`export default 42`),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const namespace1 = compartment1.importNow('a');
+  const compartment2 = new Compartment(
+    {},
+    {},
+    {
+      importNowHook(specifier) {
+        if (specifier === 'z') {
+          return { namespace: namespace1 };
+        }
+        return undefined;
+      },
+    },
+  );
+  const namespace2 = compartment2.importNow('z');
+  t.is(namespace2.default, 42);
+  t.is(namespace1, namespace2);
+});
+
+test('import now hook returns module namespace descriptor and non-namespace object', t => {
+  const compartment = new Compartment(
+    {},
+    {},
+    {
+      importNowHook(specifier) {
+        if (specifier === '1') {
+          return { namespace: { meaning: 42 } };
+        }
+        return undefined;
+      },
+    },
+  );
+  const namespace = compartment.importNow('1');
+  t.is(namespace.meaning, 42);
+});
+
+test('import now hook returns module source descriptor for specifier in own compartment', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            compartment,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: object1 } = compartment.importNow('./object.js');
+  t.is(object1.meaning, 42);
+  const { default: object2 } = compartment.importNow('./index.js');
+  t.is(object2.meaning, 42);
+  // Separate instances
+  t.not(object1, object2);
+});
+
+test('import now hook returns module source descriptor for specifier in own compartment and overridden base specifier that collides', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            specifier: './object.js',
+            compartment,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: object1 } = compartment.importNow('./object.js');
+  t.is(object1.meaning, 42);
+  const { default: object2 } = compartment.importNow('./index.js');
+  t.is(object2.meaning, 42);
+  // Fails to obtain separate instance due to specifier collison.
+  t.is(object1, object2);
+});
+
+test('import now hook returns module namespace descriptor for specifier in own compartment', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      importNowHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            namespace: './object.js',
+            compartment,
+          };
+        } else if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const { default: object1 } = compartment.importNow('./object.js');
+  t.is(object1.meaning, 42);
+  const { default: object2 } = compartment.importNow('./index.js');
+  t.is(object2.meaning, 42);
+  // Same instances
+  t.is(object1, object2);
+});
+
+test('module map hook precedes import now hook', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: new ModuleSource(`
+              export default 42;
+            `),
+          };
+        }
+        return undefined;
+      },
+      importNowHook() {
+        throw new Error('not reached');
+      },
+    },
+  );
+
+  const { default: meaning } = compartment.importNow('./index.js');
+  t.is(meaning, 42);
+});
diff --git a/packages/ses/test/import.test.js b/packages/ses/test/import.test.js
index 5180f2d1d..fa08cb231 100644
--- a/packages/ses/test/import.test.js
+++ b/packages/ses/test/import.test.js
@@ -4,271 +4,10 @@
 /* eslint max-lines: 0 */
 
 import test from 'ava';
-import { StaticModuleRecord } from '@endo/static-module-record';
 import '../index.js';
 import { resolveNode, makeNodeImporter } from './node.js';
 import { makeImporter, makeStaticRetriever } from './import-commons.js';
 
-test('module map primed with module source', async t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {
-      './index.js': new StaticModuleRecord('export default 42'),
-    },
-    // options:
-    {
-      resolveHook: specifier => specifier,
-    },
-  );
-  const { namespace: index } = await compartment.import('./index.js');
-  t.is(index.default, 42);
-});
-
-test('module map primed with module record descriptor', async t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {
-      './index.js': {
-        record: new StaticModuleRecord('export default 42'),
-      },
-    },
-    // options:
-    {
-      resolveHook: specifier => specifier,
-    },
-  );
-  const { namespace: index } = await compartment.import('./index.js');
-  t.is(index.default, 42);
-});
-
-test('module map primed with virtual module source', async t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {
-      './index.js': {
-        imports: [],
-        exports: ['default'],
-        execute(env) {
-          env.default = 42;
-        },
-      },
-    },
-    // options:
-    {
-      resolveHook: specifier => specifier,
-    },
-  );
-  const { namespace: index } = await compartment.import('./index.js');
-  t.is(index.default, 42);
-});
-
-test('module map primed with virtual module record descriptor', async t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {
-      './index.js': {
-        record: {
-          imports: [],
-          exports: ['default'],
-          execute(env) {
-            env.default = 42;
-          },
-        },
-      },
-    },
-    // options:
-    {
-      resolveHook: specifier => specifier,
-    },
-  );
-  const { namespace: index } = await compartment.import('./index.js');
-  t.is(index.default, 42);
-});
-
-test('module map hook returns module source', async t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {},
-    // options:
-    {
-      resolveHook: specifier => specifier,
-      moduleMapHook(specifier) {
-        if (specifier === './index.js') {
-          return new StaticModuleRecord('export default 42');
-        }
-        return undefined;
-      },
-    },
-  );
-  const { namespace: index } = await compartment.import('./index.js');
-  t.is(index.default, 42);
-});
-
-test('importHook returns module source', async t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {},
-    // options:
-    {
-      resolveHook: specifier => specifier,
-      importHook(specifier) {
-        if (specifier === './index.js') {
-          return new StaticModuleRecord('export default 42');
-        }
-        return undefined;
-      },
-    },
-  );
-  const { namespace: index } = await compartment.import('./index.js');
-  t.is(index.default, 42);
-});
-
-// This case requires the module loader to take care not to attempt to
-// coerce the module namespace object to a promise.
-test('importHook returns module namespace', async t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {},
-    // options:
-    {
-      resolveHook: specifier => specifier,
-      importHook(specifier) {
-        if (specifier === '.') {
-          return compartment.module('./index.js');
-        }
-        if (specifier === './index.js') {
-          return new StaticModuleRecord('export default 42');
-        }
-        return undefined;
-      },
-    },
-  );
-  // Unlike import, importNow does not box the namespace.
-  const { namespace: index } = await compartment.import('.');
-  t.is(index.default, 42);
-});
-
-test('importNowHook returns namespace', t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {},
-    // options:
-    {
-      resolveHook: specifier => specifier,
-      importNowHook(specifier) {
-        if (specifier === '.') {
-          return compartment.module('./index.js');
-        }
-        if (specifier === './index.js') {
-          return new StaticModuleRecord('export default 42');
-        }
-        return undefined;
-      },
-    },
-  );
-  // Unlike import, importNow does not box the namespace.
-  const index = compartment.importNow('.');
-  t.is(index.default, 42);
-});
-
-test('importHook returns compartment and specifier module descriptor', async t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {},
-    // options:
-    {
-      resolveHook: specifier => specifier,
-      importHook(specifier) {
-        if (specifier === '.') {
-          return { compartment, specifier: './index.js' };
-        }
-        if (specifier === './index.js') {
-          return new StaticModuleRecord('export default 42');
-        }
-        return undefined;
-      },
-    },
-  );
-  const { namespace: index } = await compartment.import('.');
-  t.is(index.default, 42);
-});
-
-test('importHook returns record and specifier module descriptor', async t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {},
-    // options:
-    {
-      resolveHook: specifier => specifier,
-      importHook(specifier) {
-        if (specifier === '.') {
-          return {
-            record: new StaticModuleRecord('export default 42'),
-            specifier: './index.js',
-          };
-        }
-        return undefined;
-      },
-    },
-  );
-  const { namespace: index } = await compartment.import('.');
-  t.is(index.default, 42);
-});
-
-test('importHook returns record and specifier module descriptor and import specifiers resolve from response specifier', async t => {
-  const compartment = new Compartment(
-    // endowments:
-    {},
-    // modules:
-    {
-      './src/peer.js': new StaticModuleRecord('export const number = 42'),
-    },
-    // options:
-    {
-      resolveHook(importSpecifier, moduleSpecifier) {
-        if (
-          moduleSpecifier === './src/index.js' &&
-          importSpecifier === './peer.js'
-        ) {
-          return './src/peer.js';
-        }
-        return importSpecifier;
-      },
-      importHook(specifier) {
-        if (specifier === '.') {
-          return {
-            record: new StaticModuleRecord('export * from "./peer.js"'),
-            specifier: './src/index.js',
-          };
-        }
-        return undefined;
-      },
-    },
-  );
-  const { namespace: index } = await compartment.import('.');
-  t.is(index.number, 42);
-});
-
 // 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.
diff --git a/packages/ses/test/module-map-hook-legacy.test.js b/packages/ses/test/module-map-hook-legacy.test.js
new file mode 100644
index 000000000..650132f33
--- /dev/null
+++ b/packages/ses/test/module-map-hook-legacy.test.js
@@ -0,0 +1,106 @@
+// These tests exercise the Compartment moduleMapHook for legacy module
+// descriptor shapes.
+
+/* eslint max-lines: 0 */
+
+import test from 'ava';
+import { ModuleSource } from '@endo/module-source';
+import '../index.js';
+
+test('module map hook returns module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return new ModuleSource('export default 42');
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map hook returns module record descriptor', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return new ModuleSource('export default 42');
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map hook returns virtual module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            imports: [],
+            exports: ['default'],
+            execute(env) {
+              env.default = 42;
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map hook returns virtual module record descriptor', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            record: {
+              imports: [],
+              exports: ['default'],
+              execute(env) {
+                env.default = 42;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
diff --git a/packages/ses/test/module-map-hook.test.js b/packages/ses/test/module-map-hook.test.js
new file mode 100644
index 000000000..90eab3e45
--- /dev/null
+++ b/packages/ses/test/module-map-hook.test.js
@@ -0,0 +1,582 @@
+// These tests exercise the Compartment moduleMapHook.
+
+/* eslint max-lines: 0 */
+
+import test from 'ava';
+import { ModuleSource } from '@endo/module-source';
+import '../index.js';
+
+test('module map hook returns module source descriptor with precompiled module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return { source: new ModuleSource('export default 42') };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map hook returns  module source descriptor with virtual module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['default'],
+              execute(env) {
+                env.default = 42;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map hook returns parent compartment module source descriptor with string reference to parent compartment', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      moduleMapHook(specifier) {
+        if (specifier === './meaning.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['default'],
+              execute(env) {
+                env.default = 42;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      resolveHook: specifier => specifier,
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './meaning.js',
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map hook returns parent compartment module source reference with different specifier', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'parent',
+      resolveHook(relative, specifier) {
+        t.is(relative, './meaningful.js');
+        t.is(specifier, './meaning.js');
+        return relative;
+      },
+      moduleMapHook(specifier) {
+        if (specifier === './meaningful.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['meaning'],
+              execute() {
+                throw new Error('should not execute');
+              },
+            },
+          };
+        } else if (specifier === './meaning.js') {
+          return {
+            source: {
+              imports: ['./meaningful.js'],
+              exports: ['default'],
+              execute(env, c, resolutions) {
+                // eslint-disable-next-line no-use-before-define
+                t.is(c, compartment);
+                const { meaning } = c.importNow(resolutions['./meaningful.js']);
+                env.default = meaning;
+              },
+            },
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      resolveHook(relative, specifier) {
+        t.is(relative, './meaningful.js');
+        t.is(specifier, './lib/meaning.js');
+        return './lib/meaningful.js';
+      },
+      moduleMapHook(specifier) {
+        if (specifier === './lib/meaningful.js') {
+          return {
+            source: {
+              imports: [],
+              exports: ['meaning'],
+              execute(env) {
+                env.meaning = 42;
+              },
+            },
+          };
+        } else if (specifier === './index.js') {
+          return {
+            source: './meaning.js',
+            specifier: './lib/meaning.js',
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map hook returns module source descriptor for parent compartment with string reference', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'parent',
+      moduleMapHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: parentObject },
+  } = await parent.import('./object.js');
+  t.is(parentObject.meaning, 42);
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            // implies parent compartment
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: childObject },
+  } = await compartment.import('./index.js');
+  t.is(childObject.meaning, 42);
+  // Separate instances
+  t.not(childObject, parentObject);
+});
+
+test('module map hook returns parent compartment module namespace descriptor', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'parent',
+      moduleMapHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: parentObject },
+  } = await parent.import('./object.js');
+  t.is(parentObject.meaning, 42);
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            namespace: './object.js',
+            // implies parent compartment
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: childObject },
+  } = await compartment.import('./index.js');
+  t.is(childObject.meaning, 42);
+  // Same instances
+  t.is(childObject, parentObject);
+});
+
+test('module map hook returns module source descriptor with string reference to parent compartment', async t => {
+  const compartment1 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'compartment1',
+      moduleMapHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment1.import('./object.js');
+  t.is(object1.meaning, 42);
+
+  const compartment2 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            compartment: compartment1,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object2 },
+  } = await compartment2.import('./index.js');
+  t.is(object2.meaning, 42);
+  // Separate instances
+  t.not(object1, object2);
+});
+
+test('module map hook returns other compartment module namespace descriptor', async t => {
+  const compartment1 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'compartment1',
+      moduleMapHook(specifier) {
+        if (specifier === './object.js') {
+          return {
+            source: new ModuleSource('export default { meaning: 42 }'),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment1.import('./object.js');
+  t.is(object1.meaning, 42);
+
+  const compartment2 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {},
+    // options:
+    {
+      name: 'child',
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            namespace: './object.js',
+            compartment: compartment1,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object2 },
+  } = await compartment2.import('./index.js');
+  t.is(object2.meaning, 42);
+  // Same instances
+  t.is(object1, object2);
+});
+
+test('module map hook returns module namespace descriptor and namespace object', async t => {
+  const compartment1 = new Compartment(
+    {},
+    {},
+    {
+      moduleMapHook(specifier) {
+        if (specifier === 'a') {
+          return {
+            source: new ModuleSource(`export default 42`),
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: namespace1 } = await compartment1.import('a');
+  const compartment2 = new Compartment(
+    {},
+    {},
+    {
+      moduleMapHook(specifier) {
+        if (specifier === 'z') {
+          return { namespace: namespace1 };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace: namespace2 } = await compartment2.import('z');
+  t.is(namespace2.default, 42);
+  t.is(namespace1, namespace2);
+});
+
+test('module map hook returns module namespace descriptor and non-namespace object', async t => {
+  const compartment = new Compartment(
+    {},
+    {},
+    {
+      moduleMapHook(specifier) {
+        if (specifier === '1') {
+          return { namespace: { meaning: 42 } };
+        }
+        return undefined;
+      },
+    },
+  );
+  const { namespace } = await compartment.import('1');
+  t.is(namespace.meaning, 42);
+});
+
+test('module map hook returns module source descriptor for specifier in own compartment', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            compartment,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment.import('./object.js');
+  t.is(object1.meaning, 42);
+  const {
+    namespace: { default: object2 },
+  } = await compartment.import('./index.js');
+  t.is(object2.meaning, 42);
+  // Separate instances
+  t.not(object1, object2);
+});
+
+test('module map hook returns module source descriptor for specifier in own compartment and overridden base specifier that collides', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            source: './object.js',
+            specifier: './object.js',
+            compartment,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment.import('./object.js');
+  t.is(object1.meaning, 42);
+  const {
+    namespace: { 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);
+});
+
+test('module map hook returns module namespace descriptor for specifier in own compartment', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      moduleMapHook(specifier) {
+        if (specifier === './index.js') {
+          return {
+            namespace: './object.js',
+            compartment,
+          };
+        }
+        return undefined;
+      },
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment.import('./object.js');
+  t.is(object1.meaning, 42);
+  const {
+    namespace: { default: object2 },
+  } = await compartment.import('./index.js');
+  t.is(object2.meaning, 42);
+  // Same instances
+  t.is(object1, object2);
+});
+
+test('module map precedes module map hook', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        source: new ModuleSource(`
+          export default 42;
+        `),
+      },
+    },
+    // options:
+    {
+      moduleMapHook() {
+        throw new Error('not reached');
+      },
+    },
+  );
+
+  const { default: meaning } = compartment.importNow('./index.js');
+  t.is(meaning, 42);
+});
diff --git a/packages/ses/test/module-map-legacy.test.js b/packages/ses/test/module-map-legacy.test.js
new file mode 100644
index 000000000..83296a160
--- /dev/null
+++ b/packages/ses/test/module-map-legacy.test.js
@@ -0,0 +1,90 @@
+// These tests exercise the Compartment module map for legacy module
+// descriptor shapes.
+
+import test from 'ava';
+import { ModuleSource } from '@endo/module-source';
+import '../index.js';
+
+test('module map primed with module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': new ModuleSource('export default 42'),
+    },
+    // options:
+    {
+      resolveHook: specifier => specifier,
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map primed with module record descriptor', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        record: new ModuleSource('export default 42'),
+      },
+    },
+    // options:
+    {
+      resolveHook: specifier => specifier,
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map primed with virtual module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        imports: [],
+        exports: ['default'],
+        execute(env) {
+          env.default = 42;
+        },
+      },
+    },
+    // options:
+    {
+      resolveHook: specifier => specifier,
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map primed with virtual module record descriptor', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        record: {
+          imports: [],
+          exports: ['default'],
+          execute(env) {
+            env.default = 42;
+          },
+        },
+      },
+    },
+    // options:
+    {
+      resolveHook: specifier => specifier,
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
diff --git a/packages/ses/test/module-map.test.js b/packages/ses/test/module-map.test.js
new file mode 100644
index 000000000..fc4de21cb
--- /dev/null
+++ b/packages/ses/test/module-map.test.js
@@ -0,0 +1,400 @@
+// These tests exercise the Compartment module map.
+
+import test from 'ava';
+import { ModuleSource } from '@endo/module-source';
+import '../index.js';
+
+test('module map primed with module source descriptor with precompiled module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        source: new ModuleSource('export default 42'),
+      },
+    },
+    // options:
+    {
+      resolveHook: specifier => specifier,
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map primed with module source descriptor with virtual module source', async t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        source: {
+          imports: [],
+          exports: ['default'],
+          execute(env) {
+            env.default = 42;
+          },
+        },
+      },
+    },
+    // options:
+    {
+      resolveHook: specifier => specifier,
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map primed with parent compartment module source descriptor with string reference to parent compartment', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './meaning.js': {
+        source: {
+          imports: [],
+          exports: ['default'],
+          execute(env) {
+            env.default = 42;
+          },
+        },
+      },
+    },
+    // options:
+    {
+      resolveHook: specifier => specifier,
+    },
+  );
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        source: './meaning.js',
+      },
+    },
+    // options:
+    {
+      resolveHook: specifier => specifier,
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map primed with parent compartment module source reference with different specifier', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './meaningful.js': {
+        source: {
+          imports: [],
+          exports: ['meaning'],
+          execute() {
+            throw new Error('should not execute');
+          },
+        },
+      },
+      './meaning.js': {
+        source: {
+          imports: ['./meaningful.js'],
+          exports: ['default'],
+          execute(env, c, resolutions) {
+            // eslint-disable-next-line no-use-before-define
+            t.is(c, compartment);
+            const { meaning } = c.importNow(resolutions['./meaningful.js']);
+            env.default = meaning;
+          },
+        },
+      },
+    },
+    // options:
+    {
+      name: 'parent',
+      resolveHook(relative, specifier) {
+        t.is(relative, './meaningful.js');
+        t.is(specifier, './meaning.js');
+        return relative;
+      },
+    },
+  );
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './lib/meaningful.js': {
+        source: {
+          imports: [],
+          exports: ['meaning'],
+          execute(env) {
+            env.meaning = 42;
+          },
+        },
+      },
+      './index.js': {
+        source: './meaning.js',
+        specifier: './lib/meaning.js',
+      },
+    },
+    // options:
+    {
+      name: 'child',
+      resolveHook(relative, specifier) {
+        t.is(relative, './meaningful.js');
+        t.is(specifier, './lib/meaning.js');
+        return './lib/meaningful.js';
+      },
+    },
+  );
+  const { namespace: index } = await compartment.import('./index.js');
+  t.is(index.default, 42);
+});
+
+test('module map primed with module source descriptor for parent compartment with string reference', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      name: 'parent',
+    },
+  );
+
+  const {
+    namespace: { default: parentObject },
+  } = await parent.import('./object.js');
+  t.is(parentObject.meaning, 42);
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        source: './object.js',
+        // implies parent compartment
+      },
+    },
+    // options:
+    {
+      name: 'child',
+    },
+  );
+
+  const {
+    namespace: { default: childObject },
+  } = await compartment.import('./index.js');
+  t.is(childObject.meaning, 42);
+  // Separate instances
+  t.not(childObject, parentObject);
+});
+
+test('module map primed with parent compartment module namespace descriptor', async t => {
+  const parent = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      name: 'parent',
+    },
+  );
+
+  const {
+    namespace: { default: parentObject },
+  } = await parent.import('./object.js');
+  t.is(parentObject.meaning, 42);
+
+  const compartment = new parent.globalThis.Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        namespace: './object.js',
+        // implies parent compartment
+      },
+    },
+    // options:
+    {
+      name: 'child',
+    },
+  );
+
+  const {
+    namespace: { default: childObject },
+  } = await compartment.import('./index.js');
+  t.is(childObject.meaning, 42);
+  // Same instances
+  t.is(childObject, parentObject);
+});
+
+test('module map primed with module source descriptor with string reference to parent compartment', async t => {
+  const compartment1 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      name: 'compartment1',
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment1.import('./object.js');
+  t.is(object1.meaning, 42);
+
+  const compartment2 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        source: './object.js',
+        compartment: compartment1,
+      },
+    },
+    // options:
+    {
+      name: 'child',
+    },
+  );
+
+  const {
+    namespace: { default: object2 },
+  } = await compartment2.import('./index.js');
+  t.is(object2.meaning, 42);
+  // Separate instances
+  t.not(object1, object2);
+});
+
+test('module map primed with other compartment module namespace descriptor', async t => {
+  const compartment1 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './object.js': {
+        source: new ModuleSource('export default { meaning: 42 }'),
+      },
+    },
+    // options:
+    {
+      name: 'compartment1',
+    },
+  );
+
+  const {
+    namespace: { default: object1 },
+  } = await compartment1.import('./object.js');
+  t.is(object1.meaning, 42);
+
+  const compartment2 = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        namespace: './object.js',
+        compartment: compartment1,
+      },
+    },
+    // options:
+    {
+      name: 'child',
+    },
+  );
+
+  const {
+    namespace: { default: object2 },
+  } = await compartment2.import('./index.js');
+  t.is(object2.meaning, 42);
+  // Same instances
+  t.is(object1, object2);
+});
+
+test('module map primed with module namespace descriptor and namespace object', async t => {
+  const compartment1 = new Compartment(
+    {},
+    {
+      a: {
+        source: new ModuleSource(`export default 42`),
+      },
+    },
+    {},
+  );
+  const { namespace: namespace1 } = await compartment1.import('a');
+  const compartment2 = new Compartment(
+    {},
+    {
+      z: { namespace: namespace1 },
+    },
+    {},
+  );
+  const { namespace: namespace2 } = await compartment2.import('z');
+  t.is(namespace2.default, 42);
+  t.is(namespace1, namespace2);
+});
+
+test('module map primed with module namespace descriptor and non-namespace object', async t => {
+  const compartment = new Compartment(
+    {},
+    {
+      1: { namespace: { meaning: 42 } },
+    },
+    {},
+  );
+  const { namespace } = await compartment.import('1');
+  t.is(namespace.meaning, 42);
+});
+
+test('module map precedes module map hook', t => {
+  const compartment = new Compartment(
+    // endowments:
+    {},
+    // modules:
+    {
+      './index.js': {
+        source: new ModuleSource(`
+          export default 42;
+        `),
+      },
+    },
+    // options:
+    {
+      moduleMapHook() {
+        throw new Error('not reached');
+      },
+    },
+  );
+
+  const { default: meaning } = compartment.importNow('./index.js');
+  t.is(meaning, 42);
+});
diff --git a/packages/ses/test/module-static-record.test.js b/packages/ses/test/module-source.test.js
similarity index 78%
rename from packages/ses/test/module-static-record.test.js
rename to packages/ses/test/module-source.test.js
index e38df4136..4f9458439 100644
--- a/packages/ses/test/module-static-record.test.js
+++ b/packages/ses/test/module-source.test.js
@@ -1,11 +1,11 @@
 import test from 'ava';
 import '../index.js';
-import { StaticModuleRecord } from '@endo/static-module-record';
+import { ModuleSource } from '@endo/module-source';
 
 lockdown();
 
-test('static module record constructor', t => {
-  const msr = new StaticModuleRecord(`
+test('module source constructor', t => {
+  const msr = new ModuleSource(`
     import foo from 'import-default-export-from-me.js';
     import oof from 'import-default-export-from-me.js';
     import * as bar from 'import-all-from-me.js';
@@ -36,9 +36,9 @@ test('static module record constructor', t => {
     'should capture sorted unique imports',
   );
 
-  t.truthy(Object.isFrozen(msr), 'StaticModuleRecords should be frozen');
+  t.truthy(Object.isFrozen(msr), 'ModuleSources should be frozen');
   t.truthy(
     Object.isFrozen(msr.imports),
-    'StaticModuleRecord imports should be frozen',
+    'ModuleSource imports should be frozen',
   );
 });