link.js: implement the linker API
MODIFY
src/index.js
src/index.js
export { makeRootedResolver } from './resolve';
export { makeSuffixLocator } from './locate';
export { makeProtocolRetriever } from './retrieve';
+export { makeEvaluateLinker } from './link';
export { makeImporter as default } from './pipeline';

ADD
src/link.js
+import { makeModuleInstance } from './moduleInstance';
+
+const { keys } = Object;
+
+const makeMap = (...args) => new Map(...args);
+
+export const makeEvaluateLinker = (evaluator, instanceCache = new Map()) => {
+ const linker = {
+ // Instantiation phase produces a linked module instance. A module
+ // instance is linked when its importNS is populated with linked
+ // module instances whose exports satify this module's imports.
+ link(linkageRecord, recursiveLink, preEndowments) {
+ const linkedImportNS = makeMap();
+ const linkedInstance = makeModuleInstance(
+ linkageRecord,
+ linkedImportNS,
+ evaluator,
+ preEndowments,
+ );
+ instanceCache.set(linkageRecord.moduleId, linkedInstance);
+
+ for (const specifier of keys(linkageRecord.imports)) {
+ const moduleId = linkageRecord.moduleIds[specifier];
+ const importedInstance = recursiveLink(moduleId, linker, preEndowments);
+ linkedImportNS.set(moduleId, importedInstance);
+ }
+
+ return linkedInstance;
+ },
+ instanceCache,
+ };
+ return linker;
+};

ADD
src/moduleInstance.js
+import { link } from "fs";
+
+// This file needs to provide the API that the transforms
+// target.
+
+const {
+ create,
+ entries,
+ defineProperty: defProp,
+ freeze: harden,
+ getOwnPropertyDescriptors: getProps,
+} = Object;
+
+export function makeModuleInstance(
+ linkageRecord,
+ importNS,
+ evaluator,
+ preEndowments,
+) {
+ // {_exportName_: getter} module namespace object
+ const moduleNS = create(null);
+
+ // {_localName_: accessor} added to endowments for proxy traps
+ const trappers = create(null);
+
+ // {_fixedExportName_: init(initValue) -> initValue} used by the
+ // rewritten code to initialize exported fixed bindings.
+ const constVal = create(null);
+
+ // {_liveExportName_: update(newValue)} used by the rewritten code to
+ // both initiailize and update live bindings.
+ const letVal = create(null);
+
+ // {_importName_: notify(update(newValue))} Used by code that imports
+ // one of this module's exports, so that their update function will
+ // be notified when this binding is initialized or updated.
+ const notifiers = create(null);
+
+ for (const fixedExportName of linkageRecord.fixedExports) {
+ const qname = JSON.stringify(fixedExportName);
+
+ // fixed binding state
+ let value;
+ let tdz = true;
+ let optUpdaters = []; // optUpdaters === null iff tdz === false
+
+ // tdz sensitive getter
+ const get = () => {
+ if (tdz) {
+ throw new ReferenceError(`binding ${qname} not yet initialized`);
+ }
+ return value;
+ };
+
+ // leave tdz once
+ const init = initValue => {
+ // init with initValue of a declared const binding, and return
+ // it.
+ if (!tdz) {
+ throw new Error(`Internal: binding ${qname} already initialized`);
+ }
+ value = initValue;
+ const updaters = optUpdaters;
+ optUpdaters = null;
+ tdz = false;
+ for (const updater of updaters) {
+ updater(initValue);
+ }
+ return initValue;
+ };
+
+ // If still tdz, register update for notification later.
+ // Otherwise, update now.
+ const notify = updater => {
+ if (tdz) {
+ optUpdaters.push(updater);
+ } else {
+ updater(value);
+ }
+ };
+
+ defProp(moduleNS, fixedExportName, {
+ get,
+ set: undefined,
+ enumerable: true,
+ configurable: false,
+ });
+
+ constVal[fixedExportName] = init;
+ notifiers[fixedExportName] = notify;
+ }
+
+ for (const [liveExportName, vars] of entries(linkageRecord.liveExportMap)) {
+ const qname = JSON.stringify(liveExportName);
+
+ // live binding state
+ let value;
+ let tdz = true;
+ const updaters = [];
+
+ // tdz sensitive getter
+ const get = () => {
+ if (tdz) {
+ throw new ReferenceError(`binding ${qname} not yet initialized`);
+ }
+ return value;
+ };
+
+ // This must be usable locally for the translation of initializing
+ // a declared local live binding variable.
+ //
+ // For reexported variable, this is also an update function to
+ // register for notification with the downstream import, which we
+ // must assume to be live. Thus, it can be called independent of
+ // tdz but always leaves tdz. Such reexporting creates a tree of
+ // bindings. This lets the tree be hooked up even if the imported
+ // module instance isn't initialized yet, as may happen in cycles.
+ const update = newValue => {
+ value = newValue;
+ tdz = false;
+ for (const updater of updaters) {
+ updater(newValue);
+ }
+ };
+
+ // tdz sensitive setter
+ const set = newValue => {
+ if (tdz) {
+ throw new ReferenceError(`binding ${qname} not yet initialized`);
+ }
+ value = newValue;
+ for (const updater of updaters) {
+ updater(newValue);
+ }
+ };
+
+ // Always register the updater function.
+ // If not in tdz, also update now.
+ const notify = updater => {
+ updaters.push(updater);
+ if (!tdz) {
+ updater(value);
+ }
+ };
+
+ defProp(moduleNS, liveExportName, {
+ get,
+ set: undefined,
+ enumerable: true,
+ configurable: false,
+ });
+
+ for (const localName of vars) {
+ defProp(trappers, localName, {
+ get,
+ set,
+ enumerable: true,
+ configurable: false,
+ });
+ }
+
+ letVal[liveExportName] = update;
+ notifiers[liveExportName] = notify;
+ }
+
+ const notifyStar = update => {
+ update(moduleNS);
+ };
+ notifiers['*'] = notifyStar;
+
+ // The updateRecord must conform to linkageRecord.imports
+ // updateRecord = { _specifier_: importUpdaters }
+ // importUpdaters = { _importName_: [update(newValue)*] }}
+ function imports(updateRecord) {
+ // By the time imports is called, the importNS should already be
+ // initialized with module instances that satisfy
+ // linkageRecord.imports.
+ // importNS = Map[_specifier_, { initialize, notifiers }]
+ // notifiers = { _importName_: notify(update(newValue))}
+ for (const [specifier, importUpdaters] of entries(updateRecord)) {
+ const moduleId = linkageRecord.moduleIds[specifier];
+ const instance = importNS.get(moduleId);
+ instance.initialize(); // bottom up cycle tolerant
+ const { notifiers: modNotifiers } = instance;
+ for (const [importName, updaters] of entries(importUpdaters)) {
+ const notify = modNotifiers[importName];
+ for (const update of updaters) {
+ notify(update);
+ }
+ }
+ }
+ }
+
+ const endowments = create(null, {
+ // TODO should check for collisions.
+ // TODO should check that preEndowments has no $h_stuff names.
+ // Neither is a security hole since trappers replace conflicting
+ // preEndowments
+ ...getProps(preEndowments),
+ ...getProps(trappers),
+ });
+
+ const { functorSource } = linkageRecord;
+ let optFunctor = evaluator(functorSource, endowments);
+ function initialize() {
+ if (optFunctor) {
+ // uninitialized
+ const functor = optFunctor;
+ optFunctor = null;
+ // initializing - call with `this` of `undefined`.
+ functor(harden({ imports, constVal, letVal }));
+ // initialized
+ }
+ }
+
+ return harden({
+ linkageRecord,
+ moduleNS,
+ notifiers,
+ initialize,
+ });
+}

MODIFY
src/pipeline.js
src/pipeline.js
@@ -5,27 +5,26 @@ export const makeImporter = (importHooks, moduleCache = new Map()) => {
locate,
retrieve,
rewrite,
- rootContainer,
+ rootLinker,
} = importHooks;
- let importer;
- function recursiveLink(moduleId, container, preEndowments) {
- const subContainer =
- (container.containerFor && container.containerFor(moduleId)) || container;
+ function recursiveLink(moduleId, linker, preEndowments) {
+ const subLinker =
+ (linker.linkerFor && linker.linkerFor(moduleId)) || linker;
- let moduleInstance = subContainer.instanceCache.get(moduleId);
+ let moduleInstance = subLinker.instanceCache.get(moduleId);
if (moduleInstance) {
return moduleInstance;
}
// Translate the linkage record into a module instance, and cache it.
const linkageRecord = moduleCache.get(moduleId);
- moduleInstance = subContainer.link(
- linkageRecord,
- makeImporter,
+ moduleInstance = subLinker.link(
+ linkageRecord, // has url
+ recursiveLink,
preEndowments,
);
- subContainer.instanceCache.set(moduleId, moduleInstance);
+ subLinker.instanceCache.set(moduleId, moduleInstance);
return moduleInstance;
}
@@ -38,13 +37,17 @@ export const makeImporter = (importHooks, moduleCache = new Map()) => {
if (!loadedP) {
// Begin the recursive load.
loadedP = retrieve(moduleId)
- .then(body => rewrite({ sourceType: 'module', src: body }))
- .then(({ linkageRecord }) => {
+ .then(body => rewrite(body, moduleId))
+ .then(({ staticRecord: sr }) => {
// Prevent circularity.
+ const linkageRecord = { ...sr, moduleIds: {}, moduleId };
+ // console.log(`linkageRecord`, linkageRecord);
moduleCache.set(moduleId, linkageRecord);
return Promise.all(
Object.keys(linkageRecord.imports).map(spec =>
- loadOne(spec, moduleId),
+ loadOne(spec, moduleId).then(
+ subModuleId => (linkageRecord.moduleIds[spec] = subModuleId),
+ ),
),
);
});
@@ -56,7 +59,7 @@ export const makeImporter = (importHooks, moduleCache = new Map()) => {
return Promise.resolve(loadedP).then(() => moduleId);
}
- importer = async (srcSpec, preEndowments) => {
+ const importer = async (srcSpec, preEndowments) => {
const { spec, linkageRecord, url } = srcSpec;
let moduleId;
if (linkageRecord !== undefined) {
@@ -73,12 +76,9 @@ export const makeImporter = (importHooks, moduleCache = new Map()) => {
}
// Begin initialization of the linked modules.
- const moduleInstance = recursiveLink(
- moduleId,
- rootContainer,
- preEndowments,
- );
- return moduleInstance.initialize();
+ const moduleInstance = recursiveLink(moduleId, rootLinker, preEndowments);
+ await moduleInstance.initialize();
+ return moduleInstance.moduleNS;
};
return importer;

ADD
test/test-link.js
+import { test } from 'tape-promise/tape';
+
+import { makeEvaluateLinker } from '../src';
+
+test('evaluate linker', async t => {
+ try {
+ // eslint-disable-next-line no-new-func
+ const evaluate = new Function(`
+with (arguments[1]) {
+ return eval(arguments[0]);
+}`);
+
+ const rootLinker = makeEvaluateLinker(evaluate);
+ const linkageMap = new Map([
+ [
+ 'https://www.example.com/foo/abc',
+ {
+ functorSource: `\
+({ letVal, imports }) => {
+ let def;
+ imports({
+ './def': {
+ def: [$h_a => (def = $h_a)],
+ },
+ });
+
+ letVal.abc(def);
+}`,
+ imports: { './def': ['def'] },
+ fixedExports: [],
+ liveExportMap: { abc: [] },
+ moduleIds: { './def': 'https://www.example.com/foo/def' },
+ moduleId: 'https://www.example.com/foo/abc',
+ },
+ ],
+ [
+ 'https://www.example.com/foo/def',
+ {
+ functorSource: `({ letVal }) => { letVal.def(456); lo ++; }`,
+ imports: {},
+ fixedExports: [],
+ liveExportMap: { def: ['lo'] },
+ moduleIds: {},
+ moduleId: 'https://www.example.com/foo/def',
+ },
+ ],
+ ]);
+ const recursiveLink = (moduleId, linker, preEndowments) => {
+ const linkageRecord = linkageMap.get(moduleId);
+ return linker.link(linkageRecord, recursiveLink, preEndowments);
+ };
+ const mi = recursiveLink('https://www.example.com/foo/abc', rootLinker, {});
+ await mi.initialize();
+ const { moduleNS } = mi;
+ t.deepEqual(moduleNS, { abc: 457 }, 'linkage success');
+ } catch (e) {
+ t.isNot(e, e, 'unexpected exception');
+ } finally {
+ t.end();
+ }
+});

MODIFY
test/test-pipeline.js
test/test-pipeline.js
@@ -2,6 +2,110 @@ import { test } from 'tape-promise/tape';
import makeImporter from '../src';
+test('end-to-end import', async t => {
+ try {
+ let transformModule;
+ const indexSource = `\
+// index.js
+export { default as abc } from './abc';
+`;
+ const importer = makeImporter({
+ resolve(specifier, referrer) {
+ return new URL(specifier, referrer).href;
+ },
+ async locate(scopedRef) {
+ if (scopedRef === 'https://example.com/hello/') {
+ return 'https://example.com/hello/index.js';
+ }
+ if (scopedRef === 'https://example.com/hello/abc') {
+ return 'https://example.com/hello/abc.js';
+ }
+ throw TypeError(`Unrecognized scopedRef`);
+ },
+ async retrieve(moduleId) {
+ if (moduleId === 'https://example.com/hello/index.js') {
+ return indexSource;
+ }
+ if (moduleId === 'https://example.com/hello/abc.js') {
+ return `\
+// abc.js
+export default 123;
+`;
+ }
+ throw TypeError(`Unrecognized retrieve ${moduleId}`);
+ },
+ rewrite(moduleSource, moduleId) {
+ const rs = transformModule.rewrite({
+ endowments: {},
+ sourceType: 'module',
+ src: moduleSource,
+ url: moduleId,
+ });
+ return rs;
+ },
+ rootLinker: {
+ link(linkageRecord, _recursiveLink, _preEndowments) {
+ // eslint-disable-next-line no-eval
+ let functor = (1, eval)(linkageRecord.functorSource);
+ const moduleNS = {};
+ const functorArg = {
+ constVar: {
+ default(val) {
+ moduleNS.default = val;
+ },
+ },
+ };
+ return {
+ async initialize() {
+ if (functor) {
+ // console.log(`have functor`, String(functor));
+ const f = functor;
+ functor = null;
+ f(functorArg);
+ }
+ },
+ moduleNS,
+ };
+ },
+ instanceCache: new Map(),
+ },
+ });
+
+ transformModule = {
+ rewrite(rs) {
+ if (rs.sourceType === 'module') {
+ const staticRecord = {
+ functorSource: `({ constVar }) => constVar.default(${JSON.stringify(
+ rs.src,
+ )});`,
+ imports: {},
+ moduleId: rs.url,
+ };
+ return {
+ staticRecord,
+ endowments: { hImport: 'something' },
+ sourceType: 'script',
+ };
+ }
+ return {
+ src: '987',
+ sourceType: 'script',
+ };
+ },
+ };
+
+ t.deepEquals(
+ await importer({ spec: './hello/', url: 'https://example.com/' }),
+ { default: indexSource },
+ 'importer works',
+ );
+ } catch (e) {
+ t.isNot(e, e, 'unexpected exception');
+ } finally {
+ t.end();
+ }
+});
+
test('import cached specifier', async t => {
try {
const moduleCache = new Map([
@@ -26,17 +130,18 @@ test('import cached specifier', async t => {
}
throw TypeError(`Don't know how to locate ${scopedRef}`);
},
- rootContainer: {
- link(mlr, _mimp) {
- console.log(`linking`, mlr);
+ rootLinker: {
+ link(mlr, _recursiveLink, _preEndowments) {
+ // console.log(`linking`, mlr);
if (!mlr.source) {
throw TypeError(`Don't know how to link non-source mlr`);
}
return {
initialize() {
// eslint-disable-next-line no-eval
- return (1, eval)(mlr.source);
+ this.moduleNS = (1, eval)(mlr.source);
},
+ moduleNS: {},
};
},
instanceCache: new Map(),

link.js: implement the linker API