refactor(static-module-record): Convert transform-module
This change converts the transform-module into a static module record
shim that can be used with SES at runtime or at build time.
This pivot afforded an opportunity to refactor and reenable the test
suite, using SES as the evaluator foundation instead of the dead
make-simple-evaluator package.
MODIFY
packages/ses/README.md
packages/ses/README.md
@@ -243,6 +243,7 @@ The `importHook` accepts a full specifier and asynchronously returns a
```js
import 'ses';
+import { StaticModuleRecord } from '@endo/static-module-record`;
const c1 = new Compartment({}, {}, {
name: "first compartment",

ADD
packages/transform-module/DESIGN.md
+# Module transforms
+
+This package uses a transformation from a JS module source to a corresponding
+JS program that, when evaluated, produces a function that can execute a module
+and orchestrate its imports and exports.
+The static module record includes the static analysis of the module: what it
+imports and exports and how it refers to imported modules.
+
+The workflow for executing a module, as conducted by a Compartment,
+is to analyze the module source, link with other modules based on the static
+record's metadata, evaluate the functor, call the functor with linkage.
+
+```js
+import { makeModuleAnalyzer } from './src/transform-analyze.js';
+import * as babel from '@agoric/babel-standalone';
+const analyze = makeModuleAnalyzer(babel.default);
+const moduleAnalysis = analyze(moduleSource);
+const moduleFunctor = evaluateModuleFunctor(moduleAnalysis.functorSource, /* ... */);
+moduleFunctor({
+ imports(importedVariableUpdaters, exportAlls) { /* ... */ },
+ liveVar: exportedVariableUpdaters,
+ onceVar: exportedConstantEmitters,
+});
+```
+
+So, for example, the following module uses import and export quite thoroughly.
+
+```js
+import foo from 'import-default-export-from-me.js';
+import * as bar from 'import-all-from-me.js';
+import { fizz, buzz } from 'import-named-exports-from-me.js';
+import { color as colour } from 'import-named-export-and-rename.js';
+
+export let quuux = null;
+
+export { qux } from 'import-and-reexport-name-from-me.js';
+export * from 'import-and-export-all.js';
+export default 42;
+export const quux = 'Hello, World!';
+
+// Late binding of an exported variable.
+quuux = 'Hello, World!';
+```
+
+From this, the analyzer produces a static module record that shows what modules
+the module needs to be linked to and how to link their imports and exports.
+
+```js
+{
+ "exportAlls": [ "import-and-export-all.js" ],
+ "imports": {
+ "import-default-export-from-me.js": [ "default" ],
+ "import-all-from-me.js": [ "*" ],
+ "import-named-exports-from-me.js": [ "fizz", "buzz" ],
+ "import-named-export-and-rename.js": [ "color" ],
+ "import-and-reexport-name-from-me.js": [ "qux" ],
+ "import-and-export-all.js": []
+ },
+ "liveExportMap": {
+ "qux": [ "qux", false ],
+ "quuux": [ "quuux", true ],
+ },
+ "fixedExportMap": {
+ "default": [ "default" ],
+ "quux": [ "quux" ],
+ }
+}
+```
+
+The functor source, the module transformed into a program, has the following
+shape.
+The names are additionally obscured with Unicode zero-width-joiners to avoid
+collisions with sensibly constructed modules, and the transformation preserves
+line numbers.
+
+```js
+(({
+ imports: $h_imports,
+ liveVar: $h_live,
+ onceVar: $h_once,
+}) => {
+ let foo, bar, fizz, buzz, colour;
+ $h_imports(
+ new Map([
+ ["import-default-export-from-me.js", new Map([
+ ["default", [$h_a => (foo = $h_a)]],
+ ])],
+ ["import-all-from-me.js", new Map([
+ ["*", [$h_a => (bar = $h_a)]]
+ ])],
+ ["import-named-exports-from-me.js", new Map([
+ ["fizz", [$h_a => (fizz = $h_a)]],
+ ["buzz", [$h_a => (buzz = $h_a)]],
+ ])],
+ ["import-named-export-and-rename.js", new Map([
+ ["color", [$h_a => (colour = $h_a)]],
+ ])],
+ ["import-and-reexport-name-from-me.js", new Map([
+ ["qux", [$h_live["qux"]]]
+ ])],
+ ["import-and-export-all.js", new Map([])]
+ ]),
+ ["import-and-export-all.js"]
+ );
+
+ let $c_quuux = null;
+ $h_live.quuux($c_quuux);
+
+ const { default: $c_default } = { default: 42 };
+
+ $h_once.default($c_default);
+
+ const quux = 'Hello, World!';
+ $h_once.quux(quux);
+
+ quuux = 'Sorry for binding late!';
+})
+```
+
+For the final live binding to `quuux`, we depend on the evaluator to put a
+Proxy on the scope chain to intercept the assignment and effect an update
+to all modules that import the value.
+
+```ts
+// This is the signature of the analyze function, after composing it
+// with Babel core.
+type Analyzer = ({string: ModuleSource}) => ModuleAnalysis
+
+type ModuleSource = string
+
+type ModuleAnalysis = {
+ exportAlls: ExportAlls,
+ imports: Imports,
+ liveExportMap: LiveExportMap,
+ fixedExportMap: FixedExportMap,
+ functorSource: string,
+};
+
+// ExportAlls are the relative module specifiers found in directives like:
+// export * from 'import-and-reexport-all-from-me.js';
+// These are both on the static module record and passed to the import function
+// injected into a module functor.
+// TODO Consider removing the import argument.
+// It does not appear to be used by module instances.
+type ExportAlls = Array<RelativeModuleSpecifier>
+
+// Imports includes a key for every relative module specifier in
+// any static import declaration, including those implied by
+// export/from clauses.
+// The import names are the names from the dependency module
+// that this module will import.
+// If this module reexports names from the dependency module
+// but doesn't capture them in its own scope, the imports map
+// has an entry for the module but the array of import names is empty.
+type Imports = Object<RelativeModuleSpecifier, Array<ImportNames>)
+
+// ImportName is the name of a property of a module namespace object.
+type ImportName = string
+
+// LiveExportMap indicates which variables in this module's scope
+// need to emit updates when they change.
+type LiveExportMap = Object<ExportName, [ExportName, SetProxyTrap]>
+
+// FixedExportMap indicates which constants in this module's scope
+// need to emit updates when they are initialized.
+// FixedExportMap is an aesthetic subtype of LiveExportMap.
+// The single box around ImportName is not meaningful.
+type FixedExportMap = Object<ExportName, [ExportName]>
+
+// ExportName is the name of a property of a module namespace object.
+type ExportName = string
+
+// SetProxyTrap indicates that the variable has a temporal
+// dead-zone and the module namespace should throw a ReferenceError
+// before its first update.
+type SetProxyTrap = bool
+
+type ModuleFunctor = (UpdaterArgument):void
+type UpdaterArgument = {
+ imports(Updaters, ExportAlls) => void,
+ liveVar(Exporters) => void,
+ onceVar(Exporters) => void,
+};
+
+// Update functions communicate values both out of one module's scope and into
+// another module's scope.
+type UpdateFunction = (value:any) => void
+
+// Modules use updaters to receive their imports
+// as the exporting modules update them.
+type Updaters = Map<RelativeModuleSpecifier, ModuleUpdaters>>
+type ModuleUpdaters = Map<ImportName, Array<UpdateFunction>>
+type ImportName = string
+type RelativeModuleSpecifier = string
+
+// Modules use update functions to ship values out.
+type Exporters = Object<ExportName, UpdateFunction>
+```

MODIFY
packages/transform-module/README.md
packages/transform-module/README.md
-# Transform Module
+# StaticModuleRecord
-Transform-module is a transformation from a JS module source to a corresponding
-JS program that, when evaluated, produces a function that can execute a module
-and orchestrate its imports and exports.
-The static module record includes the static analysis of the module: what it
-imports and exports and how it refers to imported modules.
-
-The workflow for executing a module, as conducted by a Compartment,
-is to analyze the module source, link with other modules based on the static
-record's metadata, evaluate the functor, call the functor with linkage.
+This package provides a shim for the `StaticModuleRecord` constructor, suitable
+for use with the SES shim `importHook`.
+The static module record accepts a JavaScript module and converts it into
+a form that SES can use to securely emulate JavaScript modules (ESMs, the `mjs`
+file format) with compartments.
```js
-import { makeModuleAnalyzer } from './src/main.js';
-import * as babel from '@agoric/babel-standalone';
-const analyze = makeModuleAnalyzer(babel.default);
-const moduleAnalysis = analyze(moduleSource);
-const moduleFunctor = evaluateModuleFunctor(moduleAnalysis.functorSource, /* ... */);
-moduleFunctor({
- imports(importedVariableUpdaters, exportAlls) { /* ... */ },
- liveVar: exportedVariableUpdaters,
- onceVar: exportedConstantEmitters,
-});
-```
-
-So, for example, the following module uses import and export quite thoroughly.
-
-```js
-import foo from 'import-default-export-from-me.js';
-import * as bar from 'import-all-from-me.js';
-import { fizz, buzz } from 'import-named-exports-from-me.js';
-import { color as colour } from 'import-named-export-and-rename.js';
-
-export let quuux = null;
-
-export { qux } from 'import-and-reexport-name-from-me.js';
-export * from 'import-and-export-all.js';
-export default 42;
-export const quux = 'Hello, World!';
-
-// Late binding of an exported variable.
-quuux = 'Hello, World!';
-```
-
-From this, the analyzer produces a static module record that shows what modules
-the module needs to be linked to and how to link their imports and exports.
+import 'ses';
+import { StaticModuleRecord } from '@endo/static-module-record`;
-```js
-{
- "exportAlls": [ "import-and-export-all.js" ],
- "imports": {
- "import-default-export-from-me.js": [ "default" ],
- "import-all-from-me.js": [ "*" ],
- "import-named-exports-from-me.js": [ "fizz", "buzz" ],
- "import-named-export-and-rename.js": [ "color" ],
- "import-and-reexport-name-from-me.js": [ "qux" ],
- "import-and-export-all.js": []
+const c1 = new Compartment({}, {}, {
+ name: "first compartment",
+ resolveHook: (moduleSpecifier, moduleReferrer) => {
+ return resolve(moduleSpecifier, moduleReferrer);
},
- "liveExportMap": {
- "qux": [ "qux", false ],
- "quuux": [ "quuux", true ],
+ importHook: async moduleSpecifier => {
+ const moduleLocation = locate(moduleSpecifier);
+ const moduleText = await retrieve(moduleLocation);
+ return new StaticModuleRecord(moduleText, moduleLocation);
},
- "fixedExportMap": {
- "default": [ "default" ],
- "quux": [ "quux" ],
- }
-}
-```
-
-The functor source, the module transformed into a program, has the following
-shape.
-The names are additionally obscured with Unicode zero-width-joiners to avoid
-collisions with sensibly constructed modules, and the transformation preserves
-line numbers.
-
-```js
-(({
- imports: $h_imports,
- liveVar: $h_live,
- onceVar: $h_once,
-}) => {
- let foo, bar, fizz, buzz, colour;
- $h_imports(
- new Map([
- ["import-default-export-from-me.js", new Map([
- ["default", [$h_a => (foo = $h_a)]],
- ])],
- ["import-all-from-me.js", new Map([
- ["*", [$h_a => (bar = $h_a)]]
- ])],
- ["import-named-exports-from-me.js", new Map([
- ["fizz", [$h_a => (fizz = $h_a)]],
- ["buzz", [$h_a => (buzz = $h_a)]],
- ])],
- ["import-named-export-and-rename.js", new Map([
- ["color", [$h_a => (colour = $h_a)]],
- ])],
- ["import-and-reexport-name-from-me.js", new Map([
- ["qux", [$h_live["qux"]]]
- ])],
- ["import-and-export-all.js", new Map([])]
- ]),
- ["import-and-export-all.js"]
- );
-
- let $c_quuux = null;
- $h_live.quuux($c_quuux);
-
- const { default: $c_default } = { default: 42 };
-
- $h_once.default($c_default);
-
- const quux = 'Hello, World!';
- $h_once.quux(quux);
-
- quuux = 'Sorry for binding late!';
-})
+});
```
-For the final live binding to `quuux`, we depend on the evaluator to put a
-Proxy on the scope chain to intercept the assignment and effect an update
-to all modules that import the value.
-
-```ts
-// This is the signature of the analyze function, after composing it
-// with Babel core.
-type Analyzer = ({string: ModuleSource}) => ModuleAnalysis
-
-type ModuleSource = string
-
-type ModuleAnalysis = {
- exportAlls: ExportAlls,
- imports: Imports,
- liveExportMap: LiveExportMap,
- fixedExportMap: FixedExportMap,
- functorSource: string,
-};
-
-// ExportAlls are the relative module specifiers found in directives like:
-// export * from 'import-and-reexport-all-from-me.js';
-// These are both on the static module record and passed to the import function
-// injected into a module functor.
-// TODO Consider removing the import argument.
-// It does not appear to be used by module instances.
-type ExportAlls = Array<RelativeModuleSpecifier>
-
-// Imports includes a key for every relative module specifier in
-// any static import declaration, including those implied by
-// export/from clauses.
-// The import names are the names from the dependency module
-// that this module will import.
-// If this module reexports names from the dependency module
-// but doesn't capture them in its own scope, the imports map
-// has an entry for the module but the array of import names is empty.
-type Imports = Object<RelativeModuleSpecifier, Array<ImportNames>)
+## Bug Disclosure
-// ImportName is the name of a property of a module namespace object.
-type ImportName = string
+Please help us practice coordinated security bug disclosure, by using the
+instructions in
+[SECURITY.md](https://github.com/Agoric/ses-shim/blob/master/SECURITY.md)
+to report security-sensitive bugs privately.
-// LiveExportMap indicates which variables in this module's scope
-// need to emit updates when they change.
-type LiveExportMap = Object<ExportName, [ExportName, SetProxyTrap]>
-
-// FixedExportMap indicates which constants in this module's scope
-// need to emit updates when they are initialized.
-// FixedExportMap is an aesthetic subtype of LiveExportMap.
-// The single box around ImportName is not meaningful.
-type FixedExportMap = Object<ExportName, [ExportName]>
-
-// ExportName is the name of a property of a module namespace object.
-type ExportName = string
-
-// SetProxyTrap indicates that the variable has a temporal
-// dead-zone and the module namespace should throw a ReferenceError
-// before its first update.
-type SetProxyTrap = bool
-
-type ModuleFunctor = (UpdaterArgument):void
-type UpdaterArgument = {
- imports(Updaters, ExportAlls) => void,
- liveVar(Exporters) => void,
- onceVar(Exporters) => void,
-};
-
-// Update functions communicate values both out of one module's scope and into
-// another module's scope.
-type UpdateFunction = (value:any) => void
-
-// Modules use updaters to receive their imports
-// as the exporting modules update them.
-type Updaters = Map<RelativeModuleSpecifier, ModuleUpdaters>>
-type ModuleUpdaters = Map<ImportName, Array<UpdateFunction>>
-type ImportName = string
-type RelativeModuleSpecifier = string
-
-// Modules use update functions to ship values out.
-type Exporters = Object<ExportName, UpdateFunction>
-```
+For non-security bugs, please use the [regular Issues
+page](https://github.com/Agoric/ses-shim/issues).

MODIFY
packages/transform-module/index.d.ts
packages/transform-module/index.d.ts
-export type { StaticModuleRecord } from './src/static-module-record.js';
+export { StaticModuleRecord } from './src/static-module-record.js';

MODIFY
packages/transform-module/package.json
packages/transform-module/package.json
{
- "name": "@agoric/transform-module",
- "version": "0.4.1+1-dev",
- "description": "Transform for evaluating ES modules as Javascript programs",
+ "name": "@endo/static-module-record",
+ "version": "0.1.0+1-dev",
+ "description": "Shim for the SES StaticModuleRecord and module-to-program transformer",
"keywords": ["ses", "StaticModuleRecord", "modules", "ESM"],
"author": "Agoric",
"license": "Apache-2.0",
- "homepage": "https://github.com/Agoric/SES-shim/tree/master/packages/transform-module#readme",
+ "homepage": "https://github.com/endojs/endo/tree/master/packages/transform-module#readme",
"repository": {
"type": "git",
- "url": "git+https://github.com/Agoric/SES-shim.git"
+ "url": "git+https://github.com/endojs/endo.git"
},
"bugs": {
- "url": "https://github.com/Agoric/SES-shim/issues"
+ "url": "https://github.com/endojs/endo/issues"
},
"type": "module",
"parsers": {
"js": "mjs"
},
"main": "./dist/transform-module.cjs",
- "module": "./src/main.js",
+ "module": "./index.js",
"browser": "./dist/transform-module.umd.js",
"unpkg": "./dist/transform-module.umd.js",
"types": "./index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
- "import": "./src/main.js",
+ "import": "./index.js",
"require": "./dist/transform-module.cjs",
"browser": "./dist/transform-module.umd.js"
}
"@agoric/babel-standalone": "^7.9.5"
},
"devDependencies": {
- "@agoric/make-simple-evaluate": "0.1.0",
"@ava/babel": "^1.0.1",
"@rollup/plugin-commonjs": "^13.0.0",
"@rollup/plugin-node-resolve": "^6.1.0",
"ava": "^3.12.1",
"babel-eslint": "^10.0.3",
- "eslint": "^6.8.0",
+ "eslint": "^7.23.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.9.0",
"eslint-plugin-eslint-comments": "^3.1.2",
"eslint-plugin-prettier": "^3.1.2",
"nyc": "^15.1.0",
"prettier": "^1.19.1",
- "ses": "0.12.0",
+ "ses": "0.12.7",
"rollup": "1.31.0",
"rollup-plugin-node-resolve": "5.2.0",
"rollup-plugin-terser": "^5.1.3",

MODIFY
packages/transform-module/rollup.config.js
packages/transform-module/rollup.config.js
@@ -10,7 +10,7 @@ const umd = meta.umd || name;
export default [
{
- input: 'src/main.js',
+ input: 'index.js',
output: [
{
file: `dist/${name}.mjs`,
@@ -24,7 +24,7 @@ export default [
plugins: [resolve(), commonjs()],
},
{
- input: 'src/main.js',
+ input: 'index.js',
output: {
file: `dist/${name}.umd.js`,
format: 'umd',
@@ -33,7 +33,7 @@ export default [
plugins: [resolve(), commonjs()],
},
{
- input: 'src/main.js',
+ input: 'index.js',
output: {
file: `dist/${name}.umd.min.js`,
format: 'umd',

MODIFY
packages/transform-module/test/test-static-module-record.js
packages/transform-module/test/test-static-module-record.js
@@ -39,7 +39,7 @@ test('export default', t => {
functor({
imports: () => {
- t.is(1, 1);
+ t.assert(true);
},
onceVar: {
/** @type {(value: string) => void} */
@@ -74,7 +74,7 @@ function initialize(t, source, options = {}) {
Object.keys(record.__fixedExportMap__).forEach(name => {
/** @param {any} value */
onceUpdaters[name] = value => {
- t.is(namespace[name], undefined);
+ t.assert(!(name in namespace));
namespace[name] = value;
log.push(`${name}: ${JSON.stringify(value)}`);
};
@@ -162,15 +162,15 @@ test('export default arguments (not technically valid but must be handled)', t =
t.is(namespace.default.length, 1);
});
-test('export default this (illustrates scope proxy leak)', t => {
+test.failing('export default this', t => {
const { record, namespace } = initialize(t, `export default this`, {
endowments: { leak: 'leaks' },
});
assertDefaultExport(t, record);
- t.is(namespace.default.leak, 'leaks');
+ t.is(namespace.default, undefined);
});
-test.skip('export named', t => {
+test.failing('export named', t => {
const { log } = initialize(
t,
`\
@@ -267,7 +267,7 @@ export let abc = 123;
);
});
-test.skip('var exports with hoisting', t => {
+test.failing('var exports with hoisting', t => {
const { log } = initialize(
t,
`\
@@ -279,7 +279,7 @@ export const abc3 = abc;
t.deepEqual(log, ['abc2: undefined', 'abc: 123', 'abc3: 123']);
});
-test.skip('function exports with hoisting', t => {
+test.failing('function exports with hoisting', t => {
const { namespace } = initialize(
t,
`\
@@ -296,7 +296,7 @@ export const fn3 = fn;
t.is(fn(), 'foo', 'fn evaluates');
});
-test.skip('export class and let', t => {
+test.failing('export class and let', t => {
const { namespace } = initialize(
t,
`\
@@ -346,7 +346,7 @@ export default (class {});
t.is(C.name, 'default', 'C class name');
});
-test.skip('hoist export function', t => {
+test.failing('hoist export function', t => {
const { namespace } = initialize(
t,
`\
@@ -358,7 +358,7 @@ export function F(arg) { return arg; }
t.is(F.name, 'F', 'F function name');
});
-test.skip('hoist default async export named function', async t => {
+test.failing('hoist default async export named function', async t => {
const { namespace } = initialize(
t,
`\
@@ -373,7 +373,7 @@ export default async function F(arg) { return arg; }
t.is(await ret, 'foo', 'F returns correctly');
});
-test.skip('hoist default async export anonymous function', async t => {
+test.failing('hoist default async export anonymous function', async t => {
const { namespace } = initialize(
t,
`\
@@ -396,7 +396,7 @@ test('zero width joiner is reserved', t => {
});
});
-test('zero width joinger in constified variable is reserved', t => {
+test('zero width joiner in constified variable is reserved', t => {
t.throws(() => {
const _ = new StaticModuleRecord(
`const $c\u200d_myVar = 123; $c\u200d_myVar`,

refactor(static-module-record): Convert transform-module

This change converts the transform-module into a static module record
shim that can be used with SES at runtime or at build time.

This pivot afforded an opportunity to refactor and reenable the test
suite, using SES as the evaluator foundation instead of the dead
make-simple-evaluator package.