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' ;
4551import {
4652 exitModuleImportHookMaker ,
4753 makeImportHookMaker ,
4854} from './import-hook.js' ;
4955import { link } from './link.js' ;
5056import { resolve } from './node-module-specifier.js' ;
57+ import { ATTENUATORS_COMPARTMENT } from './policy-format.js' ;
5158import { detectAttenuators } from './policy.js' ;
5259import { 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} ;
0 commit comments