@endo/static-module-record
Version:
Shim for the SES StaticModuleRecord and module-to-program transformer
193 lines (177 loc) • 6.47 kB
JavaScript
// @ts-nocheck XXX Babel types
import { makeTransformSource } from './transformSource.js';
import makeModulePlugins from './babelPlugin.js';
import * as h from './hidden.js';
const { freeze } = Object;
const makeCreateStaticRecord = transformSource =>
/**
*
* @param {string} moduleSource
* @param {import('./static-module-record.js').Options} options
*/
function createStaticRecord(
moduleSource,
{ sourceUrl, sourceMapUrl, sourceMap, sourceMapHook } = {},
) {
// Transform the Module source code.
const sourceOptions = {
sourceUrl,
sourceMap,
sourceMapUrl,
sourceMapHook,
sourceType: 'module',
// exportNames of variables that are only initialized and used, but
// never assigned to.
fixedExportMap: Object.create(null),
// Record of imported module specifier names to list of importNames.
// The importName '*' is that module's module namespace object.
imports: Object.create(null),
// List of module specifiers that we export all from.
exportAlls: [],
// exportNames of variables that are assigned to, or reexported and
// therefore assumed live. A reexported variable might not have any
// localName.
reexportMap: Object.create(null),
liveExportMap: Object.create(null),
hoistedDecls: [],
importSources: Object.create(null),
importDecls: [],
// enables passing import.meta usage hints up.
importMeta: { present: false },
};
if (moduleSource.startsWith('#!')) {
// Comment out the shebang lines.
moduleSource = `//${moduleSource}`;
}
let scriptSource;
try {
scriptSource = transformSource(moduleSource, sourceOptions);
} catch (err) {
const moduleLocation = sourceUrl
? JSON.stringify(sourceUrl)
: '<unknown>';
throw SyntaxError(
`Error transforming source in ${moduleLocation}: ${err.message}`,
{ cause: err },
);
}
let preamble = sourceOptions.importDecls.join(',');
if (preamble !== '') {
preamble = `let ${preamble};`;
}
const js = JSON.stringify;
const isrc = sourceOptions.importSources;
preamble += `${h.HIDDEN_IMPORTS}([${Object.keys(isrc)
.map(
src =>
`[${js(src)}, [${Object.entries(isrc[src])
.map(([exp, upds]) => `[${js(exp)}, [${upds.join(',')}]]`)
.join(',')}]]`,
)
.join(',')}]);`;
preamble += sourceOptions.hoistedDecls
.map(([vname, isOnce, cvname]) => {
let src = '';
if (cvname) {
// It's a function assigned to, so set its name property.
src = `Object.defineProperty(${cvname}, 'name', {value: ${js(
vname,
)}});`;
}
const hDeclId = isOnce ? h.HIDDEN_ONCE : h.HIDDEN_LIVE;
src += `${hDeclId}.${vname}(${cvname || ''});`;
return src;
})
.join('');
// The outer function destructures the module calling convention's internal
// variables into hidden lexical variables.
// The inner function binds `this` to `undefined` and overshadows the
// evaluator's `arguments` with a completely empty `arguments` object.
// There is no avoiding the overshadowing of `globalThis.arguments` if it
// exists in this emulation of ESM since the evaluator binds `arguments` as
// well.
// Relies on the evaluator to ensure these functions are strict.
let functorSource = `\
({ \
imports: ${h.HIDDEN_IMPORTS}, \
liveVar: ${h.HIDDEN_LIVE}, \
onceVar: ${h.HIDDEN_ONCE}, \
importMeta: ${h.HIDDEN_META}, \
}) => (function () { 'use strict'; \
${preamble} \
${scriptSource}
})()
`;
if (sourceUrl) {
functorSource += `//# sourceURL=${sourceUrl}\n`;
}
const moduleAnalysis = freeze({
exportAlls: freeze(sourceOptions.exportAlls),
imports: freeze(sourceOptions.imports),
liveExportMap: freeze(sourceOptions.liveExportMap),
fixedExportMap: freeze(sourceOptions.fixedExportMap),
reexportMap: freeze(sourceOptions.reexportMap),
needsImportMeta: sourceOptions.importMeta.present,
functorSource,
});
return moduleAnalysis;
};
export const makeModuleAnalyzer = babel => {
const transformSource = makeTransformSource(makeModulePlugins, babel);
return makeCreateStaticRecord(transformSource);
};
export const makeModuleTransformer = (babel, importer) => {
const transformSource = makeTransformSource(makeModulePlugins, babel);
const createStaticRecord = makeCreateStaticRecord(transformSource);
return {
rewrite(ss) {
// Transform the source into evaluable form.
const { allowHidden, endowments, src: source, url } = ss;
// Make an importer that uses our transform for its submodules.
function curryImporter(srcSpec) {
return importer(srcSpec, endowments);
}
// Create an import expression for the given URL.
function makeImportExpr() {
// TODO: Provide a way to allow hardening of the import expression.
const importExpr = spec => curryImporter({ url, spec });
importExpr.meta = Object.create(null);
importExpr.meta.url = url;
return importExpr;
}
// Add the $h_import hidden endowment for import expressions.
Object.assign(endowments, {
[h.HIDDEN_IMPORT]: makeImportExpr(),
});
if (ss.sourceType === 'module') {
// Do the rewrite of our own sources.
const staticRecord = createStaticRecord(source, url);
Object.assign(endowments, {
// Import our own source directly, returning a promise.
[h.HIDDEN_IMPORT_SELF]: () => curryImporter({ url, staticRecord }),
});
return {
...ss,
endowments,
allowHidden: true,
staticRecord,
sourceType: 'script',
src: `${h.HIDDEN_IMPORT_SELF}();`,
};
}
// Transform the Script or Expression source code with import expression.
const maybeSource = transformSource(source, {
allowHidden,
sourceType: 'script',
});
// Work around Babel appending semicolons.
const actualSource =
ss.sourceType === 'expression' &&
maybeSource.endsWith(';') &&
!source.endsWith(';')
? maybeSource.slice(0, -1)
: maybeSource;
return harden({ ...ss, endowments, src: actualSource });
},
};
};