Skip to content

Commit b4820dc

Browse files
committed
feat(compartment-mapper): expose experimental _redundantPreloadHook on captureFromMap
This adds a new hook option, `_redundantPreloadHook`, which is fired if and only if: 1. A canonical name w/ optional entry is present in the `_preload` array, _and_ 2. The corresponding `Compartment` has already had its module sources loaded indirectly via loading the entry `Compartment`. This better enables the caller to report the information to the end-user. ## Fixes This change also fixes two bugs: - The presence of `ModuleSource`s for a given `Compartment` does not imply _all_ preloaded modules have been loaded, as `_preload` may contain multiple entries for the same canonical name. It now checks individual entries. - The type of `_preload` was incorrect; it demanded either an `Array<string>` _or_ an `Array<{name: string, entry: string}>`, when it should be possible to mix-and-match `string` and `{name: string, entry: string}` in the same array. ## Notes - We could remove the `log()` call in `capture-lite.js`'s L187-L189 in lieu of the hook. - Interested in any suggestions to avoid the assumption in `capture-lite.js`'s L181-L191; module resolution seems like a non-starter at this point, but maybe the mapping from `.` to a relative path exists in scope here already?
1 parent 6b84fb2 commit b4820dc

File tree

7 files changed

+166
-9
lines changed

7 files changed

+166
-9
lines changed

.changeset/good-jeans-fetch.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@endo/compartment-mapper': minor
3+
---
4+
5+
Expose `_redundantPreloadHook` option in `captureFromMap()`, which will be called for each item in the `_preload` array that was already indirectly loaded via the entry `Compartment`.
6+
7+
Fixes a bug in the type of `_preload` option, which now allows for mixed arrays.
8+
9+
Fixes a bug in the preloader, which was not exhaustively checking if a non-entry module was already loaded via the entry `Compartment`.

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ const captureCompartmentMap = (
112112
const makePreloader = (
113113
compartmentMap,
114114
sources,
115-
{ log = noop, policy, _preload: preload = [] } = {},
115+
{
116+
log = noop,
117+
policy,
118+
_preload: preload = [],
119+
_redundantPreloadHook: redundantPreloadHook = undefined,
120+
} = {},
116121
) => {
117122
const {
118123
entry: { module: entryModuleSpecifier },
@@ -173,10 +178,29 @@ const makePreloader = (
173178

174179
const compartmentSources = sources[compartmentName];
175180

176-
if (keys(compartmentSources).length) {
181+
// The default preload entry is the entry module as defined by the
182+
// package itself. This corresponds to the `ModuleConfiguration` of `.`
183+
// in the `CompartmentDescriptor`'s `modules` object. Since the key in
184+
// `compartmentSources` is presumably a resolved relative path, we don't
185+
// actually know which key to look for! Thus, we are assuming that the
186+
// `Compartment`'s entry module has been loaded if _any_ sources are
187+
// present.
188+
const entryIsLoaded =
189+
entry === DEFAULT_PRELOAD_ENTRY
190+
? keys(compartmentSources).length > 0
191+
: entry in compartmentSources;
192+
193+
if (entryIsLoaded) {
177194
log(
178-
`Refusing to preload Compartment ${q(canonicalName)}; already loaded`,
195+
`Refusing to preload Compartment ${q(canonicalName)} entry ${q(entry)}; already loaded`,
179196
);
197+
if (redundantPreloadHook) {
198+
redundantPreloadHook({
199+
canonicalName,
200+
entry,
201+
log,
202+
});
203+
}
180204
} else {
181205
const compartment = compartments[compartmentName];
182206
if (!compartment) {
@@ -274,6 +298,7 @@ export const captureFromMap = async (
274298
Compartment: CompartmentOption = DefaultCompartment,
275299
log = noop,
276300
_preload: preload = [],
301+
_redundantPreloadHook: redundantPreloadHook = undefined,
277302
packageConnectionsHook,
278303
moduleSourceHook,
279304
} = options;
@@ -295,6 +320,7 @@ export const captureFromMap = async (
295320
log,
296321
policy,
297322
_preload: preload,
323+
_redundantPreloadHook: redundantPreloadHook,
298324
});
299325

300326
const consolidatedExitModuleImportHook = exitModuleImportHookMaker({

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,16 @@ export type PackageConnectionsHook = (params: {
141141
log: LogFn;
142142
}) => void;
143143

144+
/**
145+
* Hook executed during preloading when a compartment designated to be preloaded
146+
* is already loaded.
147+
*/
148+
export type RedundantPreloadHook = (params: {
149+
canonicalName: CanonicalName;
150+
entry: string;
151+
log: LogFn;
152+
}) => void;
153+
144154
/**
145155
* Set of options available in the context of code execution.
146156
*
@@ -291,7 +301,13 @@ export interface PreloadOption {
291301
*
292302
* If an array of strings is provided, the entry is assumed to be `.`.
293303
*/
294-
_preload?: Array<string> | Array<{ compartment: string; entry: string }>;
304+
_preload?: Array<string | { compartment: string; entry: string }>;
305+
306+
/**
307+
* Hook executed during preloading when a compartment designated to be preloaded
308+
* has already been loaded (via entry Compartment).
309+
*/
310+
_redundantPreloadHook?: RedundantPreloadHook | undefined;
295311
}
296312

297313
export type ArchiveLiteOptions = SyncOrAsyncArchiveOptions &

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import type {
4141
Sources,
4242
SyncModuleTransforms,
4343
CompartmentsRenameFn,
44+
RedundantPreloadHook,
4445
} from './external.js';
4546
import type { PackageDescriptor } from './node-modules.js';
4647
import type { DeferredAttenuatorsProvider } from './policy.js';

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

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import { ENTRY_COMPARTMENT } from '../src/policy-format.js';
1616

1717
const { keys } = Object;
1818

19+
const readPowers = makeReadPowers({ fs, url });
20+
1921
test('captureFromMap() - should resolve with a CaptureResult', async t => {
2022
t.plan(5);
2123

22-
const readPowers = makeReadPowers({ fs, url });
2324
const moduleLocation = `${new URL(
2425
'fixtures-0/node_modules/bundle/main.js',
2526
import.meta.url,
@@ -66,8 +67,90 @@ test('captureFromMap() - should resolve with a CaptureResult', async t => {
6667
);
6768
});
6869

70+
test('captureFromMap() - should call _redundantPreloadHook for already-loaded compartments', async t => {
71+
t.plan(2);
72+
const moduleLocation = `${new URL(
73+
'fixtures-0/node_modules/bundle/main.js',
74+
import.meta.url,
75+
)}`;
76+
77+
const nodeCompartmentMap = await mapNodeModules(readPowers, moduleLocation);
78+
79+
/** @type {{ canonicalName: string, entry: string }[]} */
80+
const hookCalls = [];
81+
82+
await captureFromMap(readPowers, nodeCompartmentMap, {
83+
_preload: ['bundle-dep'],
84+
_redundantPreloadHook: ({ canonicalName, entry }) => {
85+
hookCalls.push({ canonicalName, entry });
86+
},
87+
parserForLanguage: defaultParserForLanguage,
88+
});
89+
90+
t.is(
91+
hookCalls.length,
92+
1,
93+
'_redundantPreloadHook should have been called once',
94+
);
95+
t.deepEqual(
96+
hookCalls[0],
97+
{ canonicalName: 'bundle-dep', entry: '.' },
98+
'hook should have been called with the correct parameters',
99+
);
100+
});
101+
102+
test('captureFromMap() - should only call _redundantPreloadHook for the entry already loaded', async t => {
103+
t.plan(3);
104+
const moduleLocation = `${new URL(
105+
'fixtures-digest/node_modules/app2/index.js',
106+
import.meta.url,
107+
)}`;
108+
109+
const nodeCompartmentMap = await mapNodeModules(readPowers, moduleLocation);
110+
111+
const fjordCompartment = Object.values(nodeCompartmentMap.compartments).find(
112+
c => c.name === 'fjord',
113+
);
114+
if (!fjordCompartment) {
115+
t.fail('Expected "fjord" compartment to be present in nodeCompartmentMap');
116+
return;
117+
}
118+
119+
/** @type {{ canonicalName: string, entry: string }[]} */
120+
const hookCalls = [];
121+
122+
const { captureCompartmentMap } = await captureFromMap(
123+
readPowers,
124+
nodeCompartmentMap,
125+
{
126+
_preload: [
127+
'fjord',
128+
{ compartment: 'fjord', entry: './some-other-entry.js' },
129+
],
130+
_redundantPreloadHook: ({ canonicalName, entry }) => {
131+
hookCalls.push({ canonicalName, entry });
132+
},
133+
parserForLanguage: defaultParserForLanguage,
134+
},
135+
);
136+
137+
t.true(
138+
'fjord' in captureCompartmentMap.compartments,
139+
'"fjord" should be retained in captureCompartmentMap',
140+
);
141+
t.is(
142+
hookCalls.length,
143+
1,
144+
'_redundantPreloadHook should have been called exactly once',
145+
);
146+
t.deepEqual(
147+
hookCalls[0],
148+
{ canonicalName: 'fjord', entry: '.' },
149+
'hook should have fired for the default entry which was already loaded',
150+
);
151+
});
152+
69153
test('captureFromMap() - should preload with canonical name', async t => {
70-
const readPowers = makeReadPowers({ fs, url });
71154
const moduleLocation = `${new URL(
72155
'fixtures-digest/node_modules/app/index.js',
73156
import.meta.url,
@@ -83,11 +166,17 @@ test('captureFromMap() - should preload with canonical name', async t => {
83166
return;
84167
}
85168

169+
/** @type {{ canonicalName: string, entry: string }[]} */
170+
const hookCalls = [];
171+
86172
const { captureCompartmentMap } = await captureFromMap(
87173
readPowers,
88174
nodeCompartmentMap,
89175
{
90176
_preload: [fjordCompartment.location],
177+
_redundantPreloadHook: ({ canonicalName, entry }) => {
178+
hookCalls.push({ canonicalName, entry });
179+
},
91180
parserForLanguage: defaultParserForLanguage,
92181
},
93182
);
@@ -96,10 +185,14 @@ test('captureFromMap() - should preload with canonical name', async t => {
96185
'fjord' in captureCompartmentMap.compartments,
97186
'"fjord" should be retained in captureCompartmentMap',
98187
);
188+
t.is(
189+
hookCalls.length,
190+
0,
191+
'_redundantPreloadHook should not have been called for a non-redundant preload',
192+
);
99193
});
100194

101195
test('captureFromMap() - should discard unretained CompartmentDescriptors', async t => {
102-
const readPowers = makeReadPowers({ fs, url });
103196
const moduleLocation = `${new URL(
104197
'fixtures-digest/node_modules/app/index.js',
105198
import.meta.url,
@@ -141,7 +234,6 @@ test('captureFromMap() - should discard unretained CompartmentDescriptors', asyn
141234
});
142235

143236
test('captureFromMap() - should preload custom entry', async t => {
144-
const readPowers = makeReadPowers({ fs, url });
145237
const moduleLocation = `${new URL(
146238
'fixtures-digest/node_modules/app/index.js',
147239
import.meta.url,
@@ -183,7 +275,6 @@ test('captureFromMap() - should preload custom entry', async t => {
183275
});
184276

185277
test('captureFromMap() - should round-trip sources based on parsers', async t => {
186-
const readPowers = makeReadPowers({ fs, url });
187278
const moduleLocation = `${new URL(
188279
'fixtures-0/node_modules/bundle/main.js',
189280
import.meta.url,

packages/compartment-mapper/test/fixtures-digest/node_modules/app2/index.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compartment-mapper/test/fixtures-digest/node_modules/app2/package.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)