1+
12/**
23 *
34 * This module provides {@link captureFromMap}, which only "captures" the
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' ;
4552import {
4653 exitModuleImportHookMaker ,
4754 makeImportHookMaker ,
4855} from './import-hook.js' ;
4956import { link } from './link.js' ;
5057import { resolve } from './node-module-specifier.js' ;
58+ import { ATTENUATORS_COMPARTMENT } from './policy-format.js' ;
5159import { detectAttenuators } from './policy.js' ;
5260import { 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} ;
0 commit comments