Skip to content

Commit 8b5ac3e

Browse files
committed
feat(compartment-mapper): allow force-loading of compartments
This adds option `forceLoad` to `captureFromMap()`. This is an optional `string[]` of keys of the `CompartmentMapDescriptor` also provided to `captureFromMap()`. After loading the entry Compartment and attenuators Compartment, the Compartments having names (practically speaking, these will be _locations_) mentioned in `forceLoad` will then be loaded _if they were not already_. This option can be used to support dynamic requires and imports which would otherwise be omitted from the captured `CompartmentMapDescriptor` via digestion. * * * - Refactored `capture-lite.js`; stuffed all of the Compartment-loading business into a function. - Added test and fixture that shows how a `Compartment` which would otherwise be omitted from the captured Compartment Map is included when used with `forceLoad`.
1 parent d62fdf8 commit 8b5ac3e

File tree

9 files changed

+350
-22
lines changed

9 files changed

+350
-22
lines changed

packages/compartment-mapper/src/capture-lite.js

Lines changed: 197 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
/**
23
*
34
* This module provides {@link captureFromMap}, which only "captures" the
@@ -30,31 +31,39 @@
3031
*/
3132

3233
/* eslint no-shadow: 0 */
34+
/* global globalThis */
3335

3436
/**
3537
* @import {
3638
* CaptureLiteOptions,
3739
* CaptureResult,
3840
* CompartmentMapDescriptor,
41+
* PreloadOption,
42+
* LogFn,
43+
* LogOptions,
44+
* PolicyOption,
3945
* ReadFn,
4046
* ReadPowers,
4147
* Sources,
4248
* } from './types.js'
4349
*/
4450

51+
import { digestCompartmentMap } from './digest.js';
4552
import {
4653
exitModuleImportHookMaker,
4754
makeImportHookMaker,
4855
} from './import-hook.js';
4956
import { link } from './link.js';
5057
import { resolve } from './node-module-specifier.js';
58+
import { ATTENUATORS_COMPARTMENT } from './policy-format.js';
5159
import { detectAttenuators } from './policy.js';
5260
import { unpackReadPowers } from './powers.js';
53-
import { digestCompartmentMap } from './digest.js';
5461

55-
const { freeze, assign, create } = Object;
62+
const { freeze, assign, create, keys } = Object;
63+
const { stringify: q } = JSON;
5664

57-
const defaultCompartment = Compartment;
65+
// @ts-expect-error globalThis.Compartment is definitely on globalThis.
66+
const DefaultCompartment = /** @type {typeof Compartment} */(globalThis.Compartment);
5867

5968
/**
6069
* @param {CompartmentMapDescriptor} compartmentMap
@@ -79,12 +88,172 @@ const captureCompartmentMap = (compartmentMap, sources) => {
7988
};
8089

8190
/**
82-
* @param {ReadFn | ReadPowers} powers
91+
* @type {LogFn}
92+
*/
93+
const noop = () => {};
94+
95+
/**
96+
* The name of the module to preload if no entry is provided in a {@link PreloadOption} array
97+
*/
98+
const DEFAULT_PRELOAD_ENTRY = '.';
99+
100+
/**
101+
* Factory for a function that loads compartments.
102+
*
103+
* @param {CompartmentMapDescriptor} compartmentMap Compartment map
104+
* @param {Sources} sources Sources
105+
* @param {LogOptions & PolicyOption & PreloadOption} [options]
106+
* @returns {(linkedCompartments: Record<string, Compartment>, entryCompartment: Compartment, attenuatorsCompartment: Compartment) => Promise<void>}
107+
*/
108+
const makePreloader = (
109+
compartmentMap,
110+
sources,
111+
{ log = noop, policy, preload = [] } = {},
112+
) => {
113+
const {
114+
entry: { module: entryModuleSpecifier },
115+
} = compartmentMap;
116+
117+
/**
118+
* Given {@link CompartmentDescriptor CompartmentDescriptors}, loads any which
119+
* a) are present in the {@link preload forceLoad array}, and b) have not
120+
* yet been loaded.
121+
*
122+
* Will not load the "attenuators" `Compartment`, nor will it load any
123+
* `Compartment` having a non-empty value in `sources` (since it is presumed
124+
* it has already been loaded).
125+
*
126+
* @param {Record<string, Compartment>} compartments
127+
* @returns {Promise<void>} Resolves when all appropriate compartments are
128+
* loaded.
129+
*/
130+
const preloader = async compartments => {
131+
/** @type {[compartmentName: string, compartment: Compartment, moduleSpecifier: string][]} */
132+
const compartmentsToLoad = [];
133+
for (const preloadValue of preload) {
134+
/** @type {string} */
135+
let compartmentName;
136+
/** @type {string} */
137+
let entry;
138+
if (typeof preloadValue === 'string') {
139+
compartmentName = preloadValue;
140+
entry = DEFAULT_PRELOAD_ENTRY;
141+
} else {
142+
compartmentName = preloadValue.compartment;
143+
entry = preloadValue.entry;
144+
}
145+
146+
// skip; should already be loaded
147+
if (
148+
compartmentName === ATTENUATORS_COMPARTMENT ||
149+
compartmentName === compartmentMap.entry.compartment
150+
) {
151+
// skip
152+
} else {
153+
const compartmentDescriptor =
154+
compartmentMap.compartments[compartmentName];
155+
156+
if (!compartmentDescriptor) {
157+
throw new ReferenceError(
158+
`Failed attempting to force-load unknown compartment ${q(compartmentName)}`,
159+
);
160+
}
161+
162+
const compartmentSources = sources[compartmentName];
163+
164+
if (keys(compartmentSources).length) {
165+
log(
166+
`Refusing to force-load Compartment ${q(compartmentName)}; already loaded`,
167+
);
168+
} else {
169+
const compartment = compartments[compartmentName];
170+
if (!compartment) {
171+
throw new ReferenceError(
172+
`No compartment found for ${q(compartmentName)}`,
173+
);
174+
}
175+
176+
compartmentsToLoad.push([
177+
compartmentName,
178+
compartment,
179+
entry,
180+
]);
181+
}
182+
}
183+
}
184+
185+
const { length: compartmentsToLoadCount } = compartmentsToLoad;
186+
187+
/**
188+
* This index increments in the order in which compartments finish
189+
* loading—_not_ the order in which they began loading.
190+
*/
191+
let loadedCompartmentIndex = 0;
192+
await Promise.all(
193+
compartmentsToLoad.map(
194+
async ([compartmentName, compartment, moduleSpecifier]) => {
195+
await compartment.load(moduleSpecifier);
196+
loadedCompartmentIndex += 1
197+
log(
198+
`Force-loaded Compartment: ${q(compartmentName)} (${(loadedCompartmentIndex)}/${compartmentsToLoadCount})`,
199+
);
200+
},
201+
),
202+
);
203+
};
204+
205+
/**
206+
* Loads, in order:
207+
*
208+
* 1. The entry compartment
209+
* 2. The attenuators compartment (_if and only if_ `policy` was provided)
210+
* 3. All modules scheduled for preloading
211+
*
212+
* @param {Record<string, Compartment>} linkedCompartments
213+
* @param {Compartment} entryCompartment
214+
* @param {Compartment} attenuatorsCompartment
215+
* @returns {Promise<void>} Resolves when all compartments are loaded.
216+
*/
217+
const loadCompartments = async (
218+
linkedCompartments,
219+
entryCompartment,
220+
attenuatorsCompartment,
221+
) => {
222+
await entryCompartment.load(entryModuleSpecifier);
223+
224+
if (policy) {
225+
// retain all attenuators.
226+
await Promise.all(
227+
detectAttenuators(policy).map(attenuatorSpecifier =>
228+
attenuatorsCompartment.load(attenuatorSpecifier),
229+
),
230+
);
231+
}
232+
233+
await preloader(linkedCompartments);
234+
};
235+
236+
return loadCompartments;
237+
};
238+
239+
/**
240+
* "Captures" the compartment map descriptors and sources from a partially
241+
* completed compartment map—_without_ creating an archive.
242+
*
243+
* The resulting compartment map represents a well-formed dependency graph,
244+
* laden with useful metadata. This, for example, could be used for automatic
245+
* policy generation.
246+
*
247+
* @param {ReadFn | ReadPowers} readPowers Powers
83248
* @param {CompartmentMapDescriptor} compartmentMap
84249
* @param {CaptureLiteOptions} [options]
85250
* @returns {Promise<CaptureResult>}
86251
*/
87-
export const captureFromMap = async (powers, compartmentMap, options = {}) => {
252+
export const captureFromMap = async (
253+
readPowers,
254+
compartmentMap,
255+
options = {},
256+
) => {
88257
const {
89258
moduleTransforms,
90259
syncModuleTransforms,
@@ -94,14 +263,15 @@ export const captureFromMap = async (powers, compartmentMap, options = {}) => {
94263
policy = undefined,
95264
sourceMapHook = undefined,
96265
parserForLanguage: parserForLanguageOption = {},
97-
Compartment = defaultCompartment,
266+
Compartment: CompartmentOption = DefaultCompartment,
267+
log = noop,
268+
preload = [],
98269
} = options;
99-
100270
const parserForLanguage = freeze(
101271
assign(create(null), parserForLanguageOption),
102272
);
103273

104-
const { read, computeSha512 } = unpackReadPowers(powers);
274+
const { read, computeSha512 } = unpackReadPowers(readPowers);
105275

106276
const {
107277
compartments,
@@ -111,6 +281,12 @@ export const captureFromMap = async (powers, compartmentMap, options = {}) => {
111281
/** @type {Sources} */
112282
const sources = Object.create(null);
113283

284+
const loadCompartments = makePreloader(compartmentMap, sources, {
285+
log,
286+
policy,
287+
preload,
288+
});
289+
114290
const consolidatedExitModuleImportHook = exitModuleImportHookMaker({
115291
modules: exitModules,
116292
exitModuleImportHook,
@@ -128,25 +304,27 @@ export const captureFromMap = async (powers, compartmentMap, options = {}) => {
128304
importHook: consolidatedExitModuleImportHook,
129305
sourceMapHook,
130306
});
307+
131308
// Induce importHook to record all the necessary modules to import the given module specifier.
132-
const { compartment, attenuatorsCompartment } = link(compartmentMap, {
309+
const {
310+
compartment: entryCompartment,
311+
compartments: linkedCompartments,
312+
attenuatorsCompartment,
313+
} = link(compartmentMap, {
133314
resolve,
134315
makeImportHook,
135316
moduleTransforms,
136317
syncModuleTransforms,
137318
parserForLanguage,
138319
archiveOnly: true,
139-
Compartment,
320+
Compartment: CompartmentOption,
140321
});
141-
await compartment.load(entryModuleSpecifier);
142-
if (policy) {
143-
// retain all attenuators.
144-
await Promise.all(
145-
detectAttenuators(policy).map(attenuatorSpecifier =>
146-
attenuatorsCompartment.load(attenuatorSpecifier),
147-
),
148-
);
149-
}
322+
323+
await loadCompartments(
324+
linkedCompartments,
325+
entryCompartment,
326+
attenuatorsCompartment,
327+
);
150328

151329
return captureCompartmentMap(compartmentMap, sources);
152330
};

packages/compartment-mapper/src/types/external.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,28 @@ export type CompartmentMapForNodeModulesOptions = Omit<
135135
'conditions' | 'tags'
136136
>;
137137

138+
/**
139+
* Options for `captureFromMap()`
140+
*/
138141
export type CaptureLiteOptions = ImportingOptions &
139142
LinkingOptions &
140143
PolicyOption &
141-
LogOptions;
144+
LogOptions &
145+
PreloadOption;
146+
147+
/**
148+
* Options bag containing a `preload` array.
149+
*/
150+
export interface PreloadOption {
151+
/**
152+
* List of compartment names (the keys of
153+
* {@link CompartmentMapDescriptor.compartments}) and entries (`ModuleDescriptorConfiguration` names) to force-load _after_ the
154+
* entry compartment and any attenuators.
155+
*
156+
* If an array of strings is provided, the entry is assumed to be `.`.
157+
*/
158+
preload?: Array<string> | Array<{ compartment: string; entry: string }>;
159+
}
142160

143161
export type ArchiveLiteOptions = SyncOrAsyncArchiveOptions &
144162
ModuleTransformsOption &

0 commit comments

Comments
 (0)