@endo/static-module-record
Version:
Shim for the SES StaticModuleRecord and module-to-program transformer
633 lines (597 loc) • 21.5 kB
JavaScript
/* eslint max-lines: 0 */
import * as h from './hidden.js';
/*
* Collects all of the identifiers on the left-hand-side of an exported
* assignment expression, deeply exploring complex destructuring assignment.
* In an export assignment, every one of these identifiers is an exported name.
*
* ```
* export const pattern = ...;
* export let pattern = ...;
* export var pattern = ...;
* ```
*/
const collectPatternIdentifiers = (path, pattern) => {
switch (pattern.type) {
case 'Identifier':
return [pattern];
case 'RestElement':
return collectPatternIdentifiers(path, pattern.argument);
case 'ObjectProperty':
return collectPatternIdentifiers(path, pattern.value);
case 'ObjectPattern':
return pattern.properties.flatMap(prop =>
collectPatternIdentifiers(path, prop),
);
case 'ArrayPattern':
return pattern.elements.flatMap(pat => {
if (pat === null) return [];
// Non-elided pattern.
return collectPatternIdentifiers(path, pat);
});
default:
throw path.buildCodeFrameError(
`Pattern type ${pattern.type} is not recognized`,
);
}
};
function makeModulePlugins(options) {
const {
sourceType,
exportAlls,
fixedExportMap,
imports,
importDecls,
importSources,
reexportMap,
liveExportMap,
importMeta,
} = options;
if (sourceType !== 'module') {
throw Error(`Module sourceType must be 'module'`);
}
const updaterSources = Object.create(null);
/**
* Indicates that a name is declared at the top level and is never
* reassigned.
* All of these declarations are discovered in the analysis pass by visiting
* every function, class, and declaration.
*
* @type {Record<string, boolean>}
*/
const topLevelIsOnce = Object.create(null);
/**
* Indicates that a local name is declared at the top level and exported, and
* lists all of the corresponding exported names that should be updated if it
* changes.
* All of these declarations are discovered in the analysis pass by visiting
* every export declaration.
*
* @type {Record<string, Array<string>>}
*/
const topLevelExported = Object.create(null);
const rewriteModules =
pass =>
({ types: t }) => {
const replace = (
src,
node = t.expressionStatement(t.identifier('null')),
) => {
node.loc = src.loc;
node.comments = [...(src.leadingComments || [])];
t.inheritsComments(node, src);
return node;
};
const allowedHiddens = new WeakSet();
const rewrittenDecls = new WeakSet();
const hiddenIdentifier = hi => {
const ident = t.identifier(hi);
allowedHiddens.add(ident);
return ident;
};
const hOnceId = hiddenIdentifier(h.HIDDEN_ONCE);
const hLiveId = hiddenIdentifier(h.HIDDEN_LIVE);
const soften = id => {
// Remap the name to $c_name.
const { name } = id;
id.name = `${h.HIDDEN_CONST_VAR_PREFIX}${name}`;
allowedHiddens.add(id);
};
/**
* Adds an exported name to the module static record private metadata,
* indicating that it is updated live as opposted to a constant
* or variable that is only initialized once and never reassigned.
*
* Any top-level exported `function`, `let`, or `var` declaration is a
* "live" binding unless it's initialized and never reassigned anywhere
* in the module.
* As are any other top-level exported `var` declarations because they
* require hoisting.
*
* This method gets called in the transform phase.
* The returned hidden variable name may be used to transform
* a declaration, particularly for an export class statement.
*
* @param {string} name - the local name of the exported variable.
*/
const markLiveExport = name => {
for (const importTo of topLevelExported[name]) {
liveExportMap[importTo] = [name, true];
}
return hLiveId;
};
/**
* Adds an exported name to the module static record private metadata,
* indicating that it is updated fixed, either because it is a constant
* or because it is initialized and never reassigned.
*
* This method gets called in the transform phase.
* The returned hidden variable name may be used to transform
* a declaration, particularly for an export class statement.
*
* @param {string} name - the local name of the exported variable.
*/
const markFixedExport = name => {
for (const importTo of topLevelExported[name]) {
fixedExportMap[importTo] = [name];
}
return hOnceId;
};
/**
* Adds an exported name to the module static record private metadata,
* indicating whether it is fixed or live depending on whether
* there are any assignments to the bound variable except for
* its declaration.
*
* This function gets called in the cases where whether the export is
* live or fixed depends only on whether the export gets assigned
* anywhere outside its declaration: exported function declarations and
* exported variables initialized to function declarations.
*
* This method gets called in the transform phase.
* The returned hidden variable name may be used to transform
* a declaration, particularly for an export class statement.
*
* @param {string} name - the local name of the exported variable.
*/
const markExport = name => {
if (topLevelIsOnce[name]) {
return markFixedExport(name);
} else {
return markLiveExport(name);
}
};
const rewriteVars = (vids, isConst, needsHoisting) => {
const replacements = [];
for (const id of vids) {
const { name } = id;
if (!isConst && !topLevelIsOnce[name]) {
if (topLevelExported[name]) {
// Just add $h_live.name($c_name);
soften(id);
replacements.push(
t.expressionStatement(
t.callExpression(
t.memberExpression(hLiveId, t.identifier(name)),
[t.identifier(id.name)],
),
),
);
markLiveExport(name);
}
} else if (topLevelExported[name]) {
if (needsHoisting) {
// Hoist the declaration and soften.
if (needsHoisting === 'function') {
if (!topLevelIsOnce[name]) {
soften(id);
}
options.hoistedDecls.push([
name,
topLevelIsOnce[name],
id.name,
]);
markExport(name);
} else {
// Rewrite to be just name = value.
soften(id);
options.hoistedDecls.push([name]);
replacements.push(
t.expressionStatement(
t.assignmentExpression(
'=',
t.identifier(name),
t.identifier(id.name),
),
),
);
markLiveExport(name);
}
} else {
// Just add $h_once.name(name);
replacements.push(
t.expressionStatement(
t.callExpression(
t.memberExpression(hOnceId, t.identifier(id.name)),
[t.identifier(id.name)],
),
),
);
markFixedExport(name);
}
}
}
return replacements;
};
const rewriteExportDeclaration = path => {
// Find all the declared identifiers.
if (rewrittenDecls.has(path.node)) {
return;
}
const decl = path.node;
const declarations = decl.declarations || [decl];
const vids = declarations.flatMap(({ id }) =>
collectPatternIdentifiers(path, id),
);
// Create the export calls.
const isConst = decl.kind === 'const';
const additions = rewriteVars(
vids,
isConst,
decl.type === 'FunctionDeclaration'
? 'function'
: !isConst && decl.kind !== 'let',
);
if (additions.length > 0) {
if (decl.type === 'VariableDeclaration') {
rewrittenDecls.add(decl);
}
path.insertAfter(additions);
}
};
const visitor = {
Identifier(path) {
if (options.allowHidden || allowedHiddens.has(path.node)) {
return;
}
// Ensure the parse doesn't already include our required hidden identifiers.
// console.log(`have identifier`, path.node);
const i = h.HIDDEN_IDENTIFIERS.indexOf(path.node.name);
if (i >= 0) {
throw path.buildCodeFrameError(
`The ${h.HIDDEN_IDENTIFIERS[i]} identifier is reserved`,
);
}
if (path.node.name.startsWith(h.HIDDEN_CONST_VAR_PREFIX)) {
throw path.buildCodeFrameError(
`The ${path.node.name} constant variable is reserved`,
);
}
},
CallExpression(path) {
// import(FOO) -> $h_import(FOO)
if (path.node.callee.type === 'Import') {
path.node.callee = hiddenIdentifier(h.HIDDEN_IMPORT);
}
},
};
const importMetaVisitor = {
MetaProperty(path) {
if (
path.node.meta &&
path.node.meta.name === 'import' &&
path.node.property.name === 'meta'
) {
importMeta.present = true;
path.replaceWithMultiple([
replace(path.node, hiddenIdentifier(h.HIDDEN_META)),
]);
}
},
};
const moduleVisitor = (doAnalyze, doTransform) => ({
// We handle all the import and export productions.
ImportDeclaration(path) {
if (doAnalyze) {
const specs = path.node.specifiers;
const specifier = path.node.source.value;
let myImportSources = importSources[specifier];
if (!myImportSources) {
myImportSources = Object.create(null);
importSources[specifier] = myImportSources;
}
/** @type {Array} */
let myImports = imports[specifier];
if (!myImports) {
myImports = [];
imports[specifier] = myImports;
}
if (!specs) {
return;
}
for (const spec of specs) {
const importTo = spec.local.name;
importDecls.push(importTo);
let importFrom;
switch (spec.type) {
// import importTo from 'module';
case 'ImportDefaultSpecifier':
importFrom = 'default';
break;
// import * as importTo from 'module';
case 'ImportNamespaceSpecifier':
importFrom = '*';
break;
// import { importFrom as importTo } from 'module';
case 'ImportSpecifier':
importFrom = spec.imported.name;
break;
default:
throw path.buildCodeFrameError(
`Unrecognized import specifier type ${spec.type}`,
);
}
if (myImports && myImports.indexOf(importFrom) < 0) {
myImports.push(importFrom);
}
if (myImportSources) {
let myUpdaterSources = myImportSources[importFrom];
if (!myUpdaterSources) {
myUpdaterSources = [];
myImportSources[importFrom] = myUpdaterSources;
}
myUpdaterSources.push(
`${h.HIDDEN_A} => (${importTo} = ${h.HIDDEN_A})`,
);
updaterSources[importTo] = myUpdaterSources;
}
}
}
if (doTransform) {
// Nullify the import declaration.
path.replaceWithMultiple([]);
}
},
ExportDefaultDeclaration(path) {
// export default FOO -> $h_once.default(FOO)
if (doAnalyze) {
fixedExportMap.default = ['default'];
}
if (doTransform) {
const id = t.identifier('default');
const cid = t.identifier('default');
soften(cid);
const callee = t.memberExpression(
hiddenIdentifier(h.HIDDEN_ONCE),
id,
);
let expr = path.node.declaration;
const decl = path.node.declaration;
if (expr.type === 'ClassDeclaration') {
expr = t.classExpression(expr.id, expr.superClass, expr.body);
} else if (expr.type === 'FunctionDeclaration') {
expr = t.functionExpression(
expr.id,
expr.params,
expr.body,
expr.generator,
expr.async,
);
}
if (decl.id) {
// Just keep the same declaration and mark it as the default.
path.replaceWithMultiple([
replace(path.node, decl),
t.expressionStatement(t.callExpression(callee, [decl.id])),
]);
return;
}
// const {default: $c_default} = {default: (XXX)}; $h_once.default($c_default);
path.replaceWithMultiple([
replace(
path.node,
t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern([t.objectProperty(id, cid)]),
t.objectExpression([t.objectProperty(id, expr)]),
),
]),
),
t.expressionStatement(t.callExpression(callee, [cid])),
]);
}
},
ClassDeclaration(path) {
const ptype = path.parent.type;
if (ptype !== 'Program' && ptype !== 'ExportNamedDeclaration') {
return;
}
const { name } = path.node.id;
if (doAnalyze) {
topLevelIsOnce[name] = path.scope.getBinding(name).constant;
}
if (doTransform) {
if (topLevelExported[name]) {
const callee = t.memberExpression(markExport(name), path.node.id);
path.replaceWithMultiple([
path.node,
t.expressionStatement(t.callExpression(callee, [path.node.id])),
]);
}
}
},
FunctionDeclaration(path) {
const ptype = path.parent.type;
if (ptype !== 'Program' && ptype !== 'ExportNamedDeclaration') {
return;
}
const { name } = path.node.id;
if (doAnalyze) {
topLevelIsOnce[name] = path.scope.getBinding(name).constant;
// console.error('have function', name, 'is', topLevelIsOnce[name]);
}
if (doTransform) {
if (topLevelExported[name]) {
rewriteExportDeclaration(path);
markExport(name);
}
}
},
VariableDeclaration(path) {
const ptype = path.parent.type;
if (ptype !== 'Program' && ptype !== 'ExportNamedDeclaration') {
return;
}
// We may need to rewrite this topLevelDecl later.
const vids = path.node.declarations.flatMap(({ id }) =>
collectPatternIdentifiers(path, id),
);
if (doAnalyze) {
for (const { name } of vids) {
topLevelIsOnce[name] = path.scope.getBinding(name).constant;
}
}
if (doTransform) {
for (const { name } of vids) {
if (topLevelExported[name]) {
rewriteExportDeclaration(path);
break;
}
}
}
},
ExportAllDeclaration(path) {
const { source } = path.node;
if (doAnalyze) {
const specifier = source.value;
let myImportSources = importSources[specifier];
if (!myImportSources) {
myImportSources = Object.create(null);
importSources[specifier] = myImportSources;
}
let myImports = imports[specifier];
if (!myImports) {
// Ensure that the specifier is imported.
myImports = [];
imports[specifier] = myImports;
}
exportAlls.push(specifier);
}
if (doTransform) {
path.replaceWithMultiple([]);
}
},
ExportNamedDeclaration(path) {
const { declaration: decl, specifiers: specs, source } = path.node;
if (doAnalyze) {
let myImportSources;
let myImports;
if (source) {
const specifier = source.value;
myImportSources = importSources[specifier];
if (!myImportSources) {
myImportSources = Object.create(null);
importSources[specifier] = myImportSources;
}
myImports = imports[specifier];
if (!myImports) {
myImports = [];
imports[specifier] = myImports;
}
}
if (decl) {
const declarations = decl.declarations || [decl];
const vids = declarations.flatMap(({ id }) =>
collectPatternIdentifiers(path, id),
);
for (const { name } of vids) {
let tle = topLevelExported[name];
if (!tle) {
tle = [];
topLevelExported[name] = tle;
}
tle.push(name);
}
}
for (const spec of specs) {
const { local, exported } = spec;
const importFrom =
spec.type === 'ExportNamespaceSpecifier' ? '*' : local.name;
let myUpdaterSources;
// If local.name is reexported we omit it.
const importTo = exported.name;
if (source) {
// If source is defined, it's a reexport from a different module.
// In this case we want to collect the local and reexported name
// If there's no `as newName`, the ExportNamedDeclaration will have the `exported` field anyway,
// representing the same identifier as `local`
if (!reexportMap[source.value]) {
reexportMap[source.value] = [];
}
reexportMap[source.value].push([local.name, exported.name]);
// Don't populate importSources here, so the live binding won't get
// generated by the transform
} else {
myUpdaterSources = updaterSources[importFrom];
if (myImportSources) {
myUpdaterSources = myImportSources[importFrom];
if (!myUpdaterSources) {
myUpdaterSources = [];
myImportSources[importFrom] = myUpdaterSources;
}
updaterSources[importTo] = myUpdaterSources;
myImports.push(importFrom);
}
}
if (myUpdaterSources) {
// If there are updaters, we must have a local
// name, so update it with this export.
const ident = topLevelIsOnce[importFrom]
? h.HIDDEN_ONCE
: h.HIDDEN_LIVE;
myUpdaterSources.push(
`${ident}[${JSON.stringify(importFrom)}]`,
);
liveExportMap[importTo] = [importFrom, false];
}
if (!(source || myUpdaterSources)) {
let tle = topLevelExported[importFrom];
if (!tle) {
tle = [];
topLevelExported[importFrom] = tle;
}
tle.push(importTo);
}
}
}
if (doTransform) {
path.replaceWithMultiple(decl ? [replace(path.node, decl)] : []);
}
},
});
// Add the module visitor.
switch (pass) {
case 0:
return {
visitor: {
...visitor,
...moduleVisitor(true, false),
},
};
case 1:
return {
visitor: {
...moduleVisitor(false, true),
...importMetaVisitor,
},
};
default:
throw TypeError(`Unrecognized module pass ${pass}`);
}
};
return {
analyzePlugin: rewriteModules(0),
transformPlugin: rewriteModules(1),
};
}
export default makeModulePlugins;