Skip to content

Commit bc34d40

Browse files
authored
feat(compartment-mapper): Make script or functor bundles (#2707)
Refs: #2444 ## Description Toward better preserving the format of original source through the `ModuleSource` and censorship evasion transforms, we upgraded Babel and use a new code generator. The new code generator does not compose with Rollup. However, in the intervening years, we have reïmplemented every part of Rollup we find desirable and better in keeping with our integrity requirements. So, we are poised to reïmplement the `nestedEvaluate` and `getExport` formats using our own implementation. To that end, this change closes the feature parity gap needed to undertake that refactor. We refactor `makeBundle` into `makeScript` and `makeFuctor`. We effectively rename `makeBundle` to `makeScript` for clarity. The `script` form surfaces as the `endoScript` bundler format and we expect `functor` to surface in an `endoFunctor` format, in addition to replacing the implementation of `nestedEvaluate` and `getExport`. Both of these functions are extended to accept compile time `useEvaluate`, `sourceUrlPrefix`, and `format`, to exit to CommonJS `require`. We leave open the possibility of an `esm` or `mjs` format that would exit to `import` for host modules. The new `makeFunctor` is analogous but isn’t suitable for `<script>` tags and instead allows the user to supply runtime options, including `require` for CommonJS format, `evaluate` if compiled with `useEvaluate` to override indirect `eval`, and `sourceUrlPrefix` which also in combination with `useEvaluate` overrides the compile time option by the same name. Captures `sourceDirname` for each packaged compartment for improved `sourceURL` generation. So, if the directory name does not match the package name, the source URL will be more likely to united with the original sources if they are open in the developer’s IDE. Moves bundle `use strict` pragma into evaluated function expression bodies for better composition with `eval`. Improve error message for misconfigured `require` option. ### Security Considerations No impact. ### Scaling Considerations No impact. ### Documentation Considerations The new bundling features necessitate new API reference documentation, both those generated from TypeScript, and our hand-crafted README. ### Testing Considerations This change includes coverage for all essential combinations of the new bundler options, including compile time and runtime options and cases where the latter overrides the former. ### Compatibility Considerations Documentation included for was that the new features do not have parity with Rollup. This change is purely additive, but the upcoming changes to `@endo/bundle-source` will require a major version bump for not being comfortably equivalent to former behavior. We have confirmed that the new behavior produces a passing CI run in composition with usage patterns in Agoric SDK, with a couple preparatory changes to make the transition. ### Upgrade Considerations These changes will impact the formation of the Agoric SwingSet XSnap Supervisor Lockdown (bootstrap) script. They should not produce observable differences in behavior.
2 parents bb2b899 + ee87476 commit bc34d40

31 files changed

+1804
-232
lines changed

packages/compartment-mapper/NEWS.md

+22
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@ User-visible changes to `@endo/compartment-mapper`:
22

33
# Next release
44

5+
- Divides the role of `makeBundle` into `makeScript` and `makeFunctor`.
6+
The new `makeScript` replaces `makeBundle` without breaking changes,
7+
producing a JavaScript string that is suitable as a `<script>` tag in a web
8+
page.
9+
- The new `makeFunctor` produces a JavaScript string that, when evaluated,
10+
produces a partially applied function, so the caller can provide runtime
11+
options.
12+
- Both `makeScript` and `makeFunctor` now accept `format`, `useEvaluate` and
13+
`sourceUrlPrefix` options.
14+
- The functor produced by `makeFunctor` now accepts `evaluate`, `require`, and
15+
`sourceUrlPrefix` runtime options.
16+
- Both `makeScript` and `makeFunctor` now accept a `format` option.
17+
Specifiying the `"cjs"` format allows the bundle to exit to the host's
18+
CommonJS `require` for host modules.
19+
- Adds `sourceDirname` to compartment descriptors in the compartment maps
20+
generated by `mapNodeModules` and uses these to provide better source URL
21+
comments for bundles generated by `makeScript` and `makeFunctor`, by default.
22+
23+
These changes collectively allow us to replace the implementation of
24+
`nestedEvaluate` and `getExports` formats in `@endo/bundle-source`, including
25+
the preservation of useful line numbers and file names in stack traces.
26+
527
- `mapNodeModules`, `importLocation` and `loadLocation` now accept a `log`
628
option for users to define a custom logging function. As of this writing,
729
_only `mapNodeModules`_ will potentially call this function if provided.

packages/compartment-mapper/README.md

+167-1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,173 @@ Use `parseArchive` to construct a runner from the bytes of an archive.
137137
`loadArchive` and `parseArchive` do not run the archived application,
138138
so they can be used to safely check its hash.
139139

140+
# Script bundles
141+
142+
From `@endo/compartment-mapper/script.js`, the `makeScript` function is similar
143+
to `makeArchive` but generates a string of JavaScript suitable for `eval` or
144+
embedding in a web page with a `<script>`.
145+
Endo uses this "bundle" format to bootstrap an environment up to the point it
146+
can call `importArchive`, so bundles are at least suitable for creating a
147+
script that subsumes `ses`, `@endo/compartment-mapper/import-archive.js`, and
148+
other parts of Endo, but is not as feature-complete as `importArchive`.
149+
150+
```js
151+
import url from "url";
152+
import fs from "fs";
153+
import { makeScript } from "@endo/compartment-mapper/script.js";
154+
import { makeReadPowers } from "@endo/compartment-mapper/node-powers.js";
155+
const readPowers = makeReadPowers({ fs, url });
156+
const options = {}; // if any
157+
const script = await makeScript(readPowers, moduleSpecifier, options);
158+
```
159+
160+
The script is suitable for evaluating as a script in a web environment.
161+
The script is in UTF-8 format and uses non-ASCII characters, so may require
162+
headers or tags to specify the encoding.
163+
164+
```html
165+
<meta charset="utf-8">
166+
<script src="script.js"></script>
167+
```
168+
169+
Evaluation of `script` returns the emulated exports namespace of the entry
170+
module.
171+
172+
```js
173+
const script = await makeScript(readPowers, moduleSpecifier, options);
174+
175+
// This one weird trick evaluates your script in global scope instead of
176+
// lexical scope.
177+
const globalEval = eval;
178+
const moduleExports = globalEval(script);
179+
```
180+
181+
Scripts can include ESM, CJS, and JSON modules, but no other module languages
182+
like bytes or text.
183+
184+
> [!WARNING]
185+
> Scripts do not support [live
186+
> bindings](https://developer.mozilla.org/en-US/docs/Glossary/Binding), dynamic
187+
> `import`, or `import.meta`.
188+
> Scripts do not isolate modules to a compartment.
189+
190+
`makeScript` accepts all the options of `makeArchive` and:
191+
192+
- `sourceUrlPrefix` (string, default `""`):
193+
Specifies a prefix to occur on each module's `sourceURL` comment, as injected
194+
at runtime.
195+
Should generally end with `/` if non-empty.
196+
This can improve stack traces.
197+
- `format` (`"cjs"` or `undefined`, default `undefined`):
198+
By default, `makeBundle` generates a bundle that can be evaluated in any
199+
context.
200+
By specifying `"cjs"`, the bundle can assume there is a host CommonJS
201+
`require` function available for resolving modules that exit the bundle.
202+
The default is `require` on `globalThis`.
203+
The `require` function can be overridden with a curried runtime option.
204+
- `useEvaluate` (boolean, default `false`):
205+
Disabled by default, for bundles that may be embedded on a web page with a
206+
`no-unsafe-eval` Content Security Policy.
207+
Enable for any environment that can use `eval` or other suitable evaluator
208+
(like a Hardened JavaScript `Compartment`).
209+
210+
By default and when `useEvaluate` is explicitly `false`, the text of a module
211+
includes an array of module evaluator functions.
212+
213+
> [!WARNING]
214+
> Example is illustrative and neither a compatibility guarantee nor even
215+
> precise.
216+
217+
```js
218+
(modules => options => {
219+
/* ...linker runtime... */
220+
for (const module of modules) {
221+
module(/* linking convention */);
222+
}
223+
)([
224+
// 1. bundle ./dependency.js
225+
function () { /* ... */ },
226+
// 2. bundle ./dependent.js
227+
function () { /* ... */ },
228+
])(/* runtime options */)
229+
```
230+
231+
Each of these functions is generated by [Endo's emulation of a JavaScript
232+
`ModuleSource`
233+
constructor](https://github.com/endojs/endo/blob/master/packages/module-source/DESIGN.md),
234+
which we use elsewhere in the Compartment Mapper to emulate Compartment
235+
module systems at runtime, as in the Compartment Mapper's own `importArchive`.
236+
237+
With `useEvaluate`, the script instead embeds the text for each module as a
238+
string, along with a package-relative source URL, and uses an `eval` function
239+
to produce the corresponding `function`.
240+
241+
```js
242+
(modules => options => {
243+
/* ...linker runtime... */
244+
for (const [module, sourceURL] of modules) {
245+
evalWithSourceURL(module, sourceURL)(/* linking convention */);
246+
}
247+
)([
248+
// 1. bundle ./dependency.js
249+
["(function () { /* ... */ })", "bundle/dependency.js"],
250+
// 2. bundle ./dependent.js
251+
["(function () { /* ... */ })", "bundle/dependent.js"],
252+
])(/* runtime options */)
253+
```
254+
255+
With `useEvaluate`, the bundle will instead capture a string for
256+
each module function and use an indirect `eval` to revive them.
257+
This can make the file locations and line numbers in stack traces more
258+
useful.
259+
260+
From `@endo/compartment-mapper/script-lite.js`, the `makeScriptFromMap` takes
261+
a compartment map, like that generated by `mapNodeModules` in
262+
`@endo/compartment-mapper/node-modules.js` instead of the entry module's
263+
location.
264+
The `-lite.js` modules, in general, do not entrain a specific compartment
265+
mapper.
266+
267+
# Functor bundles
268+
269+
From `@endo/compartment-mapper/functor.js`, the `makeFunctor` function is similar
270+
to `makeScript` but generates a string of JavaScript suitable for `eval` but *not*
271+
suitable for embedding as a script. But, the completion value of the script
272+
is a function that accepts runtime options and returns the entry module's emulated
273+
module exports namespace, adding a level of indirection.
274+
275+
In this example, we use a Hardened JavaScript `Compartment` to confine the
276+
execution of the functor and its modules.
277+
278+
```js
279+
const functorScript = await makeFunctor(readPowers, moduleSpecifier, options);
280+
const compartment = new Compartment();
281+
const moduleExports = compartment.evaluate(functorScript)({
282+
require,
283+
evaluate: compartment.evaluate,
284+
sourceUrlPrefix: 'file:///Users/you/project/',
285+
});
286+
```
287+
288+
The functor runtime options include:
289+
290+
- `evaluate`: for functors made with `useEvaluate`,
291+
specifies a function to use to evaluate each module.
292+
The default evaluator is indirect `eval`.
293+
- `require`: for functors made with `format` of `"cjs"`, provides the behavior
294+
for `require` calls that exit the bundle to the host environment.
295+
Defaults to the `require` in lexical scope.
296+
- `sourceUrlPrefix`: specifies a prefix to occur on each module's `sourceURL` comment,
297+
as injected at runtime.
298+
Overrides the `sourceUrlPrefix` provided to `makeFunctor`, if any.
299+
300+
From `@endo/compartment-mapper/functor-lite.js`, the `makeFunctorFromMap` takes
301+
a compartment map, like that generated by `mapNodeModules` in
302+
`@endo/compartment-mapper/node-modules.js` instead of the entry module's
303+
location.
304+
The `-lite.js` modules, in general, do not entrain a specific compartment
305+
mapper.
306+
140307
# Package Descriptors
141308
142309
The compartment mapper uses [Compartments], one for each Node.js package your
@@ -598,7 +765,6 @@ The shape of the `policy` object is based on `policy.json` from LavaMoat. MetaMa
598765
> policy.json.
599766
> Policy generation may be ported to Endo.
600767
601-
602768
[LavaMoat]: https://github.com/LavaMoat/lavamoat
603769
[Compartments]: ../ses/README.md#compartment
604770
[Policy Demo]: ./demo/policy/README.md

packages/compartment-mapper/bundle.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
// eslint-disable-next-line import/export -- just types
22
export * from './src/types-external.js';
33

4-
export { makeBundle, writeBundle } from './src/bundle.js';
4+
export {
5+
makeScript as makeBundle,
6+
writeScript as writeBundle,
7+
} from './src/bundle.js';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// eslint-disable-next-line import/export -- just types
2+
export * from './src/types-external.js';
3+
4+
export { makeFunctorFromMap } from './src/bundle-lite.js';
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// eslint-disable-next-line import/export -- just types
2+
export * from './src/types-external.js';
3+
4+
export { makeFunctor } from './src/bundle.js';

packages/compartment-mapper/index.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ export {
1616
} from './src/import-archive.js';
1717
export { search } from './src/search.js';
1818
export { compartmentMapForNodeModules } from './src/node-modules.js';
19-
export { makeBundle, writeBundle } from './src/bundle.js';
19+
export {
20+
makeScript as makeBundle,
21+
writeScript as writeBundle,
22+
} from './src/bundle.js';

packages/compartment-mapper/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
},
4040
"./import-archive-all-parsers.js": "./import-archive-all-parsers.js",
4141
"./bundle.js": "./bundle.js",
42+
"./functor.js": "./functor.js",
43+
"./functor-lite.js": "./functor-lite.js",
44+
"./script.js": "./script.js",
45+
"./script-lite.js": "./script-lite.js",
4246
"./node-powers.js": "./node-powers.js",
4347
"./node-modules.js": "./node-modules.js",
4448
"./package.json": "./package.json"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// eslint-disable-next-line import/export -- just types
2+
export * from './src/types-external.js';
3+
4+
export { makeScriptFromMap } from './src/bundle-lite.js';

packages/compartment-mapper/script.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// eslint-disable-next-line import/export -- just types
2+
export * from './src/types-external.js';
3+
4+
export { makeScript } from './src/bundle.js';

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ const digestFromMap = async (powers, compartmentMap, options = {}) => {
176176
searchSuffixes,
177177
entryCompartmentName,
178178
entryModuleSpecifier,
179-
exitModuleImportHook: consolidatedExitModuleImportHook,
179+
importHook: consolidatedExitModuleImportHook,
180180
sourceMapHook,
181181
});
182182
// Induce importHook to record all the necessary modules to import the given module specifier.

0 commit comments

Comments
 (0)