Skip to content

Commit 84716b7

Browse files
authored
feat(bundle-source,import-bundle): Thread importHook option to endoZipBase64 moduleFormat. (#2753)
## Description This PR allows `bundleSource` and `importBundle` to bundle files and import bundled files respectively in the case that 1. the source file depends on external dependencies (such as 'fs' and 'path') and 2. the bundle is created using the endoZipBase64 moduleFormat option This functionality already existed in `@endo/compartment-mapper`, and this PR simply plumbs the importHook option to the bundleSource and importBundle APIs. ### Security Considerations Any designs which depend upon code bundled with the endoZipBase64 moduleFormat to have no external dependencies will be broken. To my knowledge none exist. ### Scaling Considerations None ### Documentation Considerations The bundleSource documentation does not have a use case showing the importHook option. ### Testing Considerations See included tests. ### Compatibility Considerations None ### Upgrade Considerations Bundling with endoZipBase64 is now more permissive, so existing importBundle calls may fail in a way they couldn't before.
2 parents f27ab4d + 5dc8dc2 commit 84716b7

File tree

9 files changed

+96
-1
lines changed

9 files changed

+96
-1
lines changed

packages/bundle-source/NEWS.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
User-visible changes to `@endo/bundle-source`:
22

3+
# Next release
4+
5+
- The `'endoZipBase64'` moduleFormat now utilizes the `importHook` option to
6+
exit dependencies whose specifiers return a truthy value.
7+
38
# v4.0.0 (2025-03-19)
49

510
- Replaces the implementation for the `nestedEvaluate` and `getExport`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { readFileSync } from './external-fs.js';
2+
3+
assert(typeof readFileSync === 'function');

packages/bundle-source/demo/external-fs.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ import { readFileSync } from 'fs';
33

44
assert(fs[Symbol.toStringTag] === 'Module');
55
assert(typeof readFileSync === 'function');
6+
7+
export { readFileSync };

packages/bundle-source/src/zip-base64.js

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const readPowers = makeReadPowers({ fs, url, crypto });
2525
* @param {boolean} [options.elideComments]
2626
* @param {string[]} [options.conditions]
2727
* @param {Record<string, string>} [options.commonDependencies]
28+
* @param {(specifier: string, packageLocation: string) => Promise<import('@endo/compartment-mapper/src/types').ThirdPartyStaticModuleInterface | undefined>} [options.importHook]
29+
*
2830
* @param {object} [grantedPowers]
2931
* @param {(bytes: string | Uint8Array) => string} [grantedPowers.computeSha512]
3032
* @param {typeof import('path)['resolve']} [grantedPowers.pathResolve]
@@ -44,6 +46,7 @@ export async function bundleZipBase64(
4446
elideComments = false,
4547
conditions = [],
4648
commonDependencies,
49+
importHook,
4750
} = options;
4851
const powers = { ...readPowers, ...grantedPowers };
4952
const {
@@ -96,6 +99,7 @@ export async function bundleZipBase64(
9699
parserForLanguage,
97100
moduleTransforms,
98101
sourceMapHook,
102+
importHook,
99103
},
100104
);
101105
assert(sha512);

packages/bundle-source/test/external-fs.test.js

+26
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,29 @@ test(`external require('fs')`, async t => {
2929
const srcMap1 = `(${src1})`;
3030
nestedEvaluate(srcMap1)();
3131
});
32+
33+
const testFsImportHookEndoZipBase64 = (name, file) => {
34+
test(`bundle ${name} with endoZipBase64`, async t => {
35+
// We expect the provided importHook is called with 'fs' exactly once
36+
t.plan(1);
37+
38+
const testFile = url.fileURLToPath(new URL(file, import.meta.url));
39+
40+
await bundleSource(testFile, {
41+
format: 'endoZipBase64',
42+
importHook: async specifier => {
43+
if (specifier === 'fs') {
44+
t.is(specifier, 'fs', 'imported fs module');
45+
return true;
46+
}
47+
return undefined;
48+
},
49+
});
50+
});
51+
};
52+
53+
testFsImportHookEndoZipBase64('import fs', '../demo/external-fs.js');
54+
testFsImportHookEndoZipBase64(
55+
'transitive import fs',
56+
'../demo/external-fs-transitive.js',
57+
);

packages/import-bundle/NEWS.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
User-visible changes to `@endo/import-bundle`:
22

3+
# Next release
4+
5+
- The `'endoZipBase64'` moduleFormat now utilizes the `importHook` option.
6+
37
# v1.4.0 (2025-03-11)
48

59
- Adds support for `test` format bundles, which simply return a promise for an

packages/import-bundle/src/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export async function importBundle(bundle, options = {}, powers = {}) {
3232
inescapableTransforms = [],
3333
inescapableGlobalProperties = {},
3434
expectedSha512 = undefined,
35+
importHook = undefined,
3536
} = options;
3637
const {
3738
computeSha512 = undefined,
@@ -74,13 +75,15 @@ export async function importBundle(bundle, options = {}, powers = {}) {
7475
expectedSha512,
7576
computeSourceLocation,
7677
computeSourceMapLocation,
78+
importHook,
7779
});
7880
// Call import by property to bypass SES censoring for dynamic import.
7981
// eslint-disable-next-line dot-notation
8082
const { namespace } = await archive['import']({
8183
globals: endowments,
8284
__shimTransforms__: transforms,
8385
Compartment: CompartmentToUse,
86+
importHook,
8487
});
8588
// namespace.default has the default export
8689
return namespace;
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { readFileSync } from 'fs';
2+
3+
const fileContents = readFileSync('self.js', 'utf8');
4+
5+
export default fileContents;

packages/import-bundle/test/import-bundle.test.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ async function testBundle1(t, b1, mode, ew) {
3535
`ns2.f5 ${mode} ok`,
3636
);
3737

38-
const ns3 = await importBundle(b1, { endowments, transforms: [transform1] });
38+
const ns3 = await importBundle(b1, {
39+
endowments,
40+
transforms: [transform1],
41+
});
3942
t.is(ns3.f4('is ok'), 'substitution is ok', `ns3.f4 ${mode} ok`);
4043
t.is(
4144
ns3.f5('the bed'),
@@ -92,6 +95,46 @@ test('test import archive', async t => {
9295
await testBundle1(t, b1EndoZipBase64Bundle, 'endoZipBase64', endowments);
9396
});
9497

98+
test('test fs import with importHook', async t => {
99+
t.plan(3);
100+
const testFilePath = url.fileURLToPath(
101+
new URL('bundle3.js', import.meta.url),
102+
);
103+
const fileContents = await fs.promises.readFile(testFilePath, 'utf8');
104+
105+
const options = {
106+
moduleFormat: 'endoZipBase64',
107+
importHook: async specifier => {
108+
console.log(`importHook(${specifier})`);
109+
if (specifier === 'fs') {
110+
t.is(specifier, 'fs');
111+
return {
112+
imports: [],
113+
exports: ['readFileSync'],
114+
execute: moduleExports => {
115+
moduleExports.readFileSync = path => {
116+
if (path === 'self.js') {
117+
return fileContents;
118+
}
119+
throw new Error(`Unknown path: ${path}`);
120+
};
121+
},
122+
};
123+
}
124+
return undefined; // Let the archive handle other modules
125+
},
126+
};
127+
128+
const bundle = await bundleSource(testFilePath, options);
129+
const ns = await importBundle(bundle, options);
130+
131+
t.is(
132+
ns.default.toString(),
133+
fileContents,
134+
'should read the file contents correctly',
135+
);
136+
});
137+
95138
test('test import script', async t => {
96139
const endowments = { console };
97140
const b1EndoScriptBundle = await bundleSource(

0 commit comments

Comments
 (0)