Skip to content

Commit 2412537

Browse files
authored
feat(compartment-mapper): add "preload" option to captureFromMap() (#2915)
1 parent a1bc9ee commit 2412537

File tree

10 files changed

+356
-22
lines changed

10 files changed

+356
-22
lines changed

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

Lines changed: 194 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,41 @@
3030
*/
3131

3232
/* eslint no-shadow: 0 */
33+
/* global globalThis */
3334

3435
/**
3536
* @import {
3637
* CaptureLiteOptions,
3738
* CaptureResult,
3839
* CompartmentMapDescriptor,
40+
* PreloadOption,
41+
* LogFn,
42+
* LogOptions,
43+
* PolicyOption,
3944
* ReadFn,
4045
* ReadPowers,
4146
* Sources,
4247
* } from './types.js'
4348
*/
4449

50+
import { digestCompartmentMap } from './digest.js';
4551
import {
4652
exitModuleImportHookMaker,
4753
makeImportHookMaker,
4854
} from './import-hook.js';
4955
import { link } from './link.js';
5056
import { resolve } from './node-module-specifier.js';
57+
import { ATTENUATORS_COMPARTMENT } from './policy-format.js';
5158
import { detectAttenuators } from './policy.js';
5259
import { unpackReadPowers } from './powers.js';
53-
import { digestCompartmentMap } from './digest.js';
5460

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

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

5969
/**
6070
* @param {CompartmentMapDescriptor} compartmentMap
@@ -79,12 +89,168 @@ const captureCompartmentMap = (compartmentMap, sources) => {
7989
};
8090

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

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

106273
const {
107274
compartments,
@@ -111,6 +278,12 @@ export const captureFromMap = async (powers, compartmentMap, options = {}) => {
111278
/** @type {Sources} */
112279
const sources = Object.create(null);
113280

281+
const loadCompartments = makePreloader(compartmentMap, sources, {
282+
log,
283+
policy,
284+
_preload: preload,
285+
});
286+
114287
const consolidatedExitModuleImportHook = exitModuleImportHookMaker({
115288
modules: exitModules,
116289
exitModuleImportHook,
@@ -128,25 +301,27 @@ export const captureFromMap = async (powers, compartmentMap, options = {}) => {
128301
importHook: consolidatedExitModuleImportHook,
129302
sourceMapHook,
130303
});
304+
131305
// Induce importHook to record all the necessary modules to import the given module specifier.
132-
const { compartment, attenuatorsCompartment } = link(compartmentMap, {
306+
const {
307+
compartment: entryCompartment,
308+
compartments: linkedCompartments,
309+
attenuatorsCompartment,
310+
} = link(compartmentMap, {
133311
resolve,
134312
makeImportHook,
135313
moduleTransforms,
136314
syncModuleTransforms,
137315
parserForLanguage,
138316
archiveOnly: true,
139-
Compartment,
317+
Compartment: CompartmentOption,
140318
});
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-
}
319+
320+
await loadCompartments(
321+
linkedCompartments,
322+
entryCompartment,
323+
attenuatorsCompartment,
324+
);
150325

151326
return captureCompartmentMap(compartmentMap, sources);
152327
};

packages/compartment-mapper/src/policy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* Attenuator,
1616
* SomePolicy,
1717
* PolicyEnforcementField,
18+
* SomePackagePolicy,
1819
* } from './types.js'
1920
* @import {ThirdPartyStaticModuleInterface} from 'ses'
2021
*/

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)