feat(endo): Support mutual dependency among compartments
MODIFY
packages/endo/src/archive.js
packages/endo/src/archive.js
@@ -176,8 +176,6 @@ export const makeArchive = async (read, moduleLocation) => {
packageDescriptor
);
- const { compartments, main } = compartmentMap;
-
const sources = {};
const makeImportHook = makeRecordingImportHookMaker(
read,
@@ -186,14 +184,13 @@ export const makeArchive = async (read, moduleLocation) => {
);
// Induce importHook to record all the necessary modules to import the given module specifier.
- const compartment = assemble({
- name: main,
- compartments,
+ const compartment = assemble(compartmentMap, {
resolve,
makeImportHook
});
await compartment.load(moduleSpecifier);
+ const { compartments, main } = compartmentMap;
const renames = renameCompartments(compartments);
const renamedCompartments = translateCompartmentMap(
compartments,

MODIFY
packages/endo/src/assemble.js
packages/endo/src/assemble.js
import { resolve } from "./node-module-specifier.js";
import { mapParsers } from "./parse.js";
-import { makeModuleMapHook } from "./module-map-hook.js";
const { entries } = Object;
@@ -11,97 +10,171 @@ const defaultCompartment = Compartment;
// q, as in quote, for strings in error messages.
const q = JSON.stringify;
-// Assemble a DAG of compartments as declared in a compartment map starting at
-// the named compartment and building all compartments that it depends upon,
-// recursively threading the modules exported by one compartment into the
-// compartment that imports them.
-// Returns the root of the compartment DAG.
-// Does not load or execute any modules.
-// Uses makeImportHook with the given "location" string of each compartment in
-// the DAG.
-// Passes the given endowments and external modules into the root compartment
-// only.
-export const assemble = ({
- name,
- compartments,
- makeImportHook,
- parents = [],
- loaded = {},
- endowments = {},
- modules = {},
- Compartment = defaultCompartment
-}) => {
- const descriptor = compartments[name];
- if (descriptor === undefined) {
- throw new Error(
- `Cannot assemble compartment graph with missing compartment descriptor named ${q(
- name
- )}, needed by ${parents.map(q).join(", ")}`
- );
+// For a full, absolute module specifier like "dependency",
+// produce the module specifier in the dependency, like ".".
+// For a deeper path like "@org/dep/aux" and a prefix like "@org/dep", produce
+// "./aux".
+const trimModuleSpecifierPrefix = (moduleSpecifier, prefix) => {
+ if (moduleSpecifier === prefix) {
+ return ".";
}
- const result = loaded[name];
- if (result !== undefined) {
- return result;
- }
- if (parents.includes(name)) {
- throw new Error(`Cannot assemble compartment graph that includes a cycle`);
+ if (moduleSpecifier.startsWith(`${prefix}/`)) {
+ return `./${moduleSpecifier.slice(prefix.length + 1)}`;
}
+ return undefined;
+};
- descriptor.modules = descriptor.modules || {};
- for (const [inner, outer] of entries(descriptor.modules)) {
+// `makeModuleMapHook` generates a `moduleMapHook` for the `Compartment`
+// constructor, suitable for Node.js style packages where any module in the
+// package might be imported.
+// Since searching for all of these modules up front is either needlessly
+// costly (on a file system) or impossible (from a web service), we
+// let the import graph guide our search.
+// Any module specifier with an absolute prefix should be captured by
+// the `moduleMap` or `moduleMapHook`.
+const makeModuleMapHook = (
+ compartments,
+ moduleDescriptors,
+ scopeDescriptors,
+ exitModules
+) => {
+ const moduleMapHook = moduleSpecifier => {
+ const moduleDescriptor = moduleDescriptors[moduleSpecifier];
+ if (moduleDescriptor !== undefined) {
const {
compartment: compartmentName,
- module: moduleSpecifier,
+ module: foreignSpecifier,
exit
- } = outer;
+ } = moduleDescriptor;
if (exit !== undefined) {
// TODO Currenly, only the entry package can connect to built-in modules.
// Policies should be able to allow third-party modules to exit to
// built-ins, or have built-ins subverted by modules from specific
// compartments.
- const module = modules[exit];
+ const module = exitModules[exit];
if (module === undefined) {
+ throw new Error(`Cannot import missing external module ${q(exit)}`);
+ }
+ return module;
+ }
+ if (compartmentName !== undefined) {
+ const compartment = compartments[compartmentName];
+ if (compartment === undefined) {
throw new Error(
- `Cannot assemble module graph with missing external module ${q(exit)}`
+ `Cannot import from missing compartment ${q(compartmentName)}`
);
}
- modules[inner] = module;
- } else if (compartmentName !== undefined) {
- const compartment = assemble({
- name: compartmentName,
- compartments,
- makeImportHook,
- parents: [...parents, name],
- loaded,
- Compartment
- });
- modules[inner] = compartment.module(moduleSpecifier);
+ return compartment.module(foreignSpecifier);
}
}
- const scopes = {};
- for (const [prefix, scope] of entries(descriptor.scopes || {})) {
- const { compartment: compartmentName } = scope;
- const compartment = assemble({
- name: compartmentName,
- compartments,
+ // Search for a scope that shares a prefix with the requested module
+ // specifier.
+ // This might be better with a trie, but only a benchmark on real-world
+ // data would tell us whether the additional complexity would translate to
+ // better performance, so this is left readable and presumed slow for now.
+ for (const [scopePrefix, scopeDescriptor] of entries(scopeDescriptors)) {
+ const foreignSpecifier = trimModuleSpecifierPrefix(
+ moduleSpecifier,
+ scopePrefix
+ );
+
+ if (foreignSpecifier !== undefined) {
+ const { compartment: compartmentName } = scopeDescriptor;
+ const compartment = compartments[compartmentName];
+ if (compartment === undefined) {
+ throw new Error(
+ `Cannot import from missing compartment ${q(compartmentName)}`
+ );
+ }
+
+ // The following line is weird.
+ // Information is flowing backward.
+ // This moduleMapHook writes back to the `modules` descriptor, from the
+ // original compartment map.
+ // So the compartment map that was used to create the compartment
+ // assembly, can then be captured in an archive, obviating the need for
+ // a moduleMapHook when we assemble compartments from the resulting
+ // archiev.
+ moduleDescriptors[moduleSpecifier] = {
+ compartment: compartmentName,
+ module: foreignSpecifier
+ };
+ return compartment.module(foreignSpecifier);
+ }
+ }
+
+ // No entry in the module map.
+ // Compartments will fall through to their `importHook`.
+ return undefined;
+ };
+
+ return moduleMapHook;
+};
+
+// Assemble a DAG of compartments as declared in a compartment map starting at
+// the named compartment and building all compartments that it depends upon,
+// recursively threading the modules exported by one compartment into the
+// compartment that imports them.
+// Returns the root of the compartment DAG.
+// Does not load or execute any modules.
+// Uses makeImportHook with the given "location" string of each compartment in
+// the DAG.
+// Passes the given endowments and external modules into the root compartment
+// only.
+export const assemble = (
+ { main, compartments: compartmentDescriptors },
+ {
makeImportHook,
- parents: [...parents, name],
- loaded,
- Compartment
- });
- scopes[prefix] = { compartment, compartmentName };
+ endowments = {},
+ modules: exitModules = {},
+ Compartment = defaultCompartment
}
+) => {
+ const compartments = {};
+ for (const [compartmentName, compartmentDescriptor] of entries(
+ compartmentDescriptors
+ )) {
+ const {
+ location,
+ modules = {},
+ parsers = {},
+ types = {},
+ scopes = {}
+ } = compartmentDescriptor;
+
+ // Capture the default.
+ // The `moduleMapHook` writes back to the compartment map.
+ compartmentDescriptor.modules = modules;
- const parse = mapParsers(descriptor.parsers || {}, descriptor.types || {});
- // TODO makeResolveHook that filters on file patterns
+ const parse = mapParsers(parsers, types);
+ const importHook = makeImportHook(location, parse);
+ const moduleMapHook = makeModuleMapHook(
+ compartments,
+ modules,
+ scopes,
+ exitModules
+ );
+ const resolveHook = resolve;
- const compartment = new Compartment(endowments, modules, {
- resolveHook: resolve,
- importHook: makeImportHook(descriptor.location, parse),
- moduleMapHook: makeModuleMapHook(scopes, descriptor.modules)
+ // TODO also thread endowments selectively.
+ const compartment = new Compartment(endowments, exitModules, {
+ resolveHook,
+ importHook,
+ moduleMapHook
});
- loaded[name] = compartment;
+ compartments[compartmentName] = compartment;
+ }
+
+ const compartment = compartments[main];
+ if (compartment === undefined) {
+ throw new Error(
+ `Cannot assemble compartment graph because the root compartment named ${q(
+ main
+ )} is missing from the compartment map`
+ );
+ }
+
return compartment;
};

MODIFY
packages/endo/src/import-archive.js
packages/endo/src/import-archive.js
@@ -37,14 +37,10 @@ export const parseArchive = async (archiveBytes, archiveLocation) => {
const compartmentMapText = decoder.decode(compartmentMapBytes);
const compartmentMap = JSON.parse(compartmentMapText);
- const { compartments, main, entry: moduleSpecifier } = compartmentMap;
-
- const makeImportHook = makeArchiveImportHookMaker(archive, compartments);
-
const execute = (endowments, modules) => {
- const compartment = assemble({
- name: main,
- compartments,
+ const { compartments, entry: moduleSpecifier } = compartmentMap;
+ const makeImportHook = makeArchiveImportHookMaker(archive, compartments);
+ const compartment = assemble(compartmentMap, {
makeImportHook,
endowments,
modules

MODIFY
packages/endo/src/import.js
packages/endo/src/import.js
@@ -44,12 +44,13 @@ const makeImportHookMaker = (read, baseLocation) => {
if (moduleBytes !== undefined) {
const moduleSource = decoder.decode(moduleBytes);
- return parse(
+ const { record } = parse(
moduleSource,
moduleSpecifier,
moduleLocation,
packageLocation
- ).record;
+ );
+ return record;
}
}
@@ -82,14 +83,9 @@ export const loadLocation = async (read, moduleLocation) => {
packageDescriptor
);
- const { compartments, main } = compartmentMap;
-
+ const execute = async (endowments = {}, modules = {}) => {
const makeImportHook = makeImportHookMaker(read, packageLocation);
-
- const execute = async (endowments, modules) => {
- const compartment = assemble({
- name: main,
- compartments,
+ const compartment = assemble(compartmentMap, {
makeImportHook,
endowments,
modules

DELETE
packages/endo/src/module-map-hook.js
-const { entries } = Object;
-
-// For a full, absolute module specifier like "dependency",
-// produce the module specifier in the dependency, like ".".
-// For a deeper path like "@org/dep/aux" and a prefix like "@org/dep", produce
-// "./aux".
-const trimModuleSpecifierPrefix = (moduleSpecifier, prefix) => {
- if (moduleSpecifier === prefix) {
- return ".";
- }
- if (moduleSpecifier.startsWith(`${prefix}/`)) {
- return `./${moduleSpecifier.slice(prefix.length + 1)}`;
- }
- return undefined;
-};
-
-// `makeModuleMapHook` generates a `moduleMapHook` for the `Compartment`
-// constructor, suitable for Node.js style packages where any module in the
-// package might be imported.
-// Since searching for all of these modules up front is either needlessly
-// costly (on a file system) or impossible (from a web service), we
-// let the import graph guide our search.
-// Any module specifier with an absolute prefix should be captured by
-// the `moduleMap` or `moduleMapHook`.
-export const makeModuleMapHook = (scopes, modules) => {
- const moduleMapHook = moduleSpecifier => {
- // Search for a scope that shares a prefix with the requested module
- // specifier.
- // This might be better with a trie, but only a benchmark on real-world
- // data would tell us whether the additional complexity would translate to
- // better performance, so this is left readable and presumed slow for now.
- for (const [prefix, { compartment, compartmentName }] of entries(scopes)) {
- const remainder = trimModuleSpecifierPrefix(moduleSpecifier, prefix);
- if (remainder) {
- // The following line is weird.
- // Information is flowing backward.
- // This moduleMapHook writes back to the `modules` descriptor, from the
- // original compartment map.
- // So the compartment map that was used to create the compartment
- // assembly, can then be captured in an archive, obviating the need for
- // a moduleMapHook when we assemble compartments from the resulting
- // archiev.
- modules[moduleSpecifier] = {
- compartment: compartmentName,
- module: remainder
- };
-
- return compartment.module(remainder);
- }
- }
-
- // No entry in the module map.
- // Compartments will fall through to their `importHook`.
- return undefined;
- };
-
- return moduleMapHook;
-};

MODIFY
packages/endo/test/main.test.js
packages/endo/test/main.test.js
@@ -77,11 +77,8 @@ test("create builtin", async t => {
const utility = await loadLocation(read, builtinLocation);
const { namespace } = await utility.execute(endowments);
// We pass the builtin module into the module map.
- // We also pass a copy as "avery" to ensure that the real "avery" module
- // overshadows the builtin.
modules = {
- builtin: namespace,
- avery: namespace
+ builtin: namespace
};
t.end();
});

feat(endo): Support mutual dependency among compartments (#433)