feat(compartment-mapper)!: Required exits on archives
Upon creating a new archive, the user will now be required to provide a
modules record (with meaningless values) to denote what dependencies are
expected to be provided by the importer.
MODIFY
packages/compartment-mapper/NEWS.md
packages/compartment-mapper/NEWS.md
@@ -2,6 +2,10 @@ User-visible changes to the compartment mapper:
# Next release
+- *BREAKING*: When constructing an archive, the creator must provide a record
+ of exit modules. Unlike import functions, the values of the exit module
+ record are ignored.
+ Any omitted exit module will cause an exception during archive creation.
- Fixes a missing file in the published assets for
`@endo/compartment-mapper/node-powers.js`.

MODIFY
packages/compartment-mapper/src/archive.js
packages/compartment-mapper/src/archive.js
@@ -140,12 +140,11 @@ const addSourcesToArchive = async (archive, sources) => {
/**
* @param {ReadFn | ReadPowers} powers
* @param {string} moduleLocation
- * @param {Object} [options]
- * @param {ModuleTransforms} [options.moduleTransforms]
+ * @param {ArchiveOptions} [options]
* @returns {Promise<Uint8Array>}
*/
export const makeArchive = async (powers, moduleLocation, options) => {
- const { moduleTransforms } = options || {};
+ const { moduleTransforms, modules = {} } = options || {};
const { read } = unpackReadPowers(powers);
const {
packageLocation,
@@ -186,6 +185,7 @@ export const makeArchive = async (powers, moduleLocation, options) => {
// Induce importHook to record all the necessary modules to import the given module specifier.
const compartment = assemble(compartmentMap, {
resolve,
+ modules,
makeImportHook,
moduleTransforms,
parserForLanguage,

MODIFY
packages/compartment-mapper/src/assemble.js
packages/compartment-mapper/src/assemble.js
@@ -303,7 +303,7 @@ export const link = (
parserForLanguage,
moduleTransforms,
);
- const importHook = makeImportHook(location, parse);
+ const importHook = makeImportHook(location, parse, exitModules);
const moduleMapHook = makeModuleMapHook(
compartments,
compartmentName,

MODIFY
packages/compartment-mapper/src/import-hook.js
packages/compartment-mapper/src/import-hook.js
@@ -4,7 +4,14 @@ import { parseExtension } from './extension.js';
// q, as in quote, for quoting strings in error messages.
const q = JSON.stringify;
+const { apply } = Reflect;
const { freeze } = Object;
+const { hasOwnProperty } = Object.prototype;
+/**
+ * @param {Record<string, never>} haystack
+ * @param {string} needle
+ */
+const has = (haystack, needle) => apply(hasOwnProperty, haystack, [needle]);
/**
* @param {string} rel - a relative URL
@@ -18,6 +25,7 @@ const resolveLocation = (rel, abs) => new URL(rel, abs).toString();
* @param {string} baseLocation
* @param {Sources} sources
* @param {Record<string, CompartmentDescriptor>} compartments
+ * @param {Record<string, never>} exitModules
* @returns {ImportHookMaker}
*/
export const makeImportHookMaker = (
@@ -25,6 +33,7 @@ export const makeImportHookMaker = (
baseLocation,
sources = {},
compartments = {},
+ exitModules = {},
) => {
// per-assembly:
/** @type {ImportHookMaker} */
@@ -43,12 +52,19 @@ export const makeImportHookMaker = (
// third-party dependency.
// The `moduleMapHook` captures all third-party dependencies.
if (moduleSpecifier !== '.' && !moduleSpecifier.startsWith('./')) {
+ if (has(exitModules, moduleSpecifier)) {
packageSources[moduleSpecifier] = {
exit: moduleSpecifier,
};
// Return a place-holder.
// Archived compartments are not executed.
- return freeze({ imports: [], execute() {} });
+ return freeze({ imports: [], exports: [], execute() {} });
+ }
+ throw new Error(
+ `Cannot find external module ${q(
+ moduleSpecifier,
+ )} in package ${packageLocation}`,
+ );
}
// Collate candidate locations for the moduleSpecifier per Node.js

MODIFY
packages/compartment-mapper/src/types.js
packages/compartment-mapper/src/types.js
* @callback ImportHookMaker
* @param {string} packageLocation
* @param {ParseFn} parse
+ * @param {Record<string, Object>} exitModules
* @returns {ImportHook}
*/
* @property {Object} [globalLexicals]
* @property {Array<Transform>} [transforms]
* @property {Array<Transform>} [__shimTransforms__]
- * @property {Record<string, string>} [modules]
+ * @property {Record<string, Object>} [modules]
* @property {typeof Compartment.prototype.constructor} [Compartment]
*/
/**
* @typedef {Object} ArchiveOptions
* @property {ModuleTransforms} [moduleTransforms]
+ * @property {Record<string, any>} [modules]
*/

MODIFY
packages/compartment-mapper/test/test-main.js
packages/compartment-mapper/test/test-main.js
@@ -132,7 +132,9 @@ test('makeArchive / parseArchive', async t => {
t.plan(fixtureAssertionCount);
await setup();
- const archive = await makeArchive(readPowers, fixture);
+ const archive = await makeArchive(readPowers, fixture, {
+ modules,
+ });
const application = await parseArchive(archive);
const { namespace } = await application.import({
globals,
@@ -148,7 +150,9 @@ test('makeArchive / parseArchive with a prefix', async t => {
await setup();
// Zip files support an arbitrary length prefix.
- const archive = await makeArchive(readPowers, fixture);
+ const archive = await makeArchive(readPowers, fixture, {
+ modules,
+ });
const prefixArchive = new Uint8Array(archive.length + 10);
prefixArchive.set(archive, 10);
@@ -177,7 +181,9 @@ test('writeArchive / loadArchive', async t => {
archive = content;
};
- await writeArchive(fakeWrite, readPowers, 'app.agar', fixture);
+ await writeArchive(fakeWrite, readPowers, 'app.agar', fixture, {
+ modules,
+ });
const application = await loadArchive(fakeRead, 'app.agar');
const { namespace } = await application.import({
globals,
@@ -203,7 +209,9 @@ test('writeArchive / importArchive', async t => {
archive = content;
};
- await writeArchive(fakeWrite, readPowers, 'app.agar', fixture);
+ await writeArchive(fakeWrite, readPowers, 'app.agar', fixture, {
+ modules,
+ });
const { namespace } = await importArchive(fakeRead, 'app.agar', {
globals,
globalLexicals,

feat(compartment-mapper)!: Required exits on archives

Upon creating a new archive, the user will now be required to provide a
modules record (with meaningless values) to denote what dependencies are
expected to be provided by the importer.