341 lines
11 KiB
JavaScript
341 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
module.exports = ({ types: t, traverse }) => ({
|
|
name: "inline-requires",
|
|
visitor: {
|
|
Program: {
|
|
enter() {},
|
|
exit(path, state) {
|
|
const ignoredRequires = new Set();
|
|
const inlineableCalls = new Set(["require"]);
|
|
const nonMemoizedModules = new Set();
|
|
let memoizeCalls = false;
|
|
const opts = state.opts;
|
|
if (opts != null) {
|
|
opts.ignoredRequires?.forEach((name) => ignoredRequires.add(name));
|
|
opts.inlineableCalls?.forEach((name) => inlineableCalls.add(name));
|
|
opts.nonMemoizedModules?.forEach((name) =>
|
|
nonMemoizedModules.add(name)
|
|
);
|
|
memoizeCalls = opts.memoizeCalls ?? false;
|
|
}
|
|
const programNode = path.scope.block;
|
|
if (programNode.type !== "Program") {
|
|
return;
|
|
}
|
|
path.scope.crawl();
|
|
path.traverse(
|
|
{
|
|
CallExpression(path, state) {
|
|
const parseResult =
|
|
parseInlineableAlias(path, state) ||
|
|
parseInlineableMemberAlias(path, state);
|
|
if (parseResult == null) {
|
|
return;
|
|
}
|
|
const { declarationPath, moduleName, requireFnName } =
|
|
parseResult;
|
|
const maybeInit = declarationPath.node.init;
|
|
const name = declarationPath.node.id
|
|
? declarationPath.node.id.name
|
|
: null;
|
|
const binding =
|
|
name == null ? null : declarationPath.scope.getBinding(name);
|
|
if (
|
|
maybeInit == null ||
|
|
!t.isExpression(maybeInit) ||
|
|
binding == null ||
|
|
binding.constantViolations.length > 0
|
|
) {
|
|
return;
|
|
}
|
|
const init = maybeInit;
|
|
const initPath = declarationPath.get("init");
|
|
if (Array.isArray(initPath)) {
|
|
return;
|
|
}
|
|
const initLoc = getNearestLocFromPath(initPath);
|
|
deleteLocation(init);
|
|
traverse(init, {
|
|
noScope: true,
|
|
enter: (path) => deleteLocation(path.node),
|
|
});
|
|
let thrown = false;
|
|
const memoVarName = parseResult.identifierName;
|
|
let hasMemoVar = false;
|
|
if (
|
|
memoizeCalls &&
|
|
binding.referencePaths.length > 0 &&
|
|
!nonMemoizedModules.has(moduleName)
|
|
) {
|
|
const varInitStmt = t.variableDeclaration("var", [
|
|
t.variableDeclarator(t.identifier(memoVarName)),
|
|
]);
|
|
declarationPath.remove();
|
|
hasMemoVar = addStmtToBlock(programNode, varInitStmt, 0);
|
|
}
|
|
function getMemoOrCallExpr() {
|
|
const refExpr = t.cloneDeep(init);
|
|
refExpr.METRO_INLINE_REQUIRES_INIT_LOC = initLoc;
|
|
return t.logicalExpression(
|
|
"||",
|
|
t.identifier(memoVarName),
|
|
t.assignmentExpression(
|
|
"=",
|
|
t.identifier(memoVarName),
|
|
refExpr
|
|
)
|
|
);
|
|
}
|
|
const scopesWithInlinedRequire = new Set();
|
|
for (const referencePath of binding.referencePaths) {
|
|
excludeMemberAssignment(moduleName, referencePath, state);
|
|
try {
|
|
referencePath.scope.rename(requireFnName);
|
|
if (hasMemoVar) {
|
|
referencePath.scope.rename(memoVarName);
|
|
if (!isDirectlyEnclosedByBlock(t, referencePath)) {
|
|
referencePath.replaceWith(getMemoOrCallExpr());
|
|
continue;
|
|
}
|
|
if (scopesWithInlinedRequire.has(referencePath.scope)) {
|
|
referencePath.replaceWith(t.identifier(memoVarName));
|
|
} else {
|
|
referencePath.replaceWith(getMemoOrCallExpr());
|
|
scopesWithInlinedRequire.add(referencePath.scope);
|
|
}
|
|
} else {
|
|
const refExpr = t.cloneDeep(init);
|
|
refExpr.METRO_INLINE_REQUIRES_INIT_LOC = initLoc;
|
|
referencePath.replaceWith(refExpr);
|
|
}
|
|
} catch (error) {
|
|
thrown = true;
|
|
}
|
|
}
|
|
if (!thrown && declarationPath.node != null) {
|
|
declarationPath.remove();
|
|
}
|
|
},
|
|
},
|
|
{
|
|
ignoredRequires,
|
|
inlineableCalls,
|
|
membersAssigned: new Map(),
|
|
}
|
|
);
|
|
},
|
|
},
|
|
},
|
|
});
|
|
function excludeMemberAssignment(moduleName, referencePath, state) {
|
|
const assignment = referencePath.parentPath?.parent;
|
|
if (assignment?.type !== "AssignmentExpression") {
|
|
return;
|
|
}
|
|
const left = assignment.left;
|
|
if (left.type !== "MemberExpression" || left.object !== referencePath.node) {
|
|
return;
|
|
}
|
|
const memberPropertyName = getMemberPropertyName(left);
|
|
if (memberPropertyName == null) {
|
|
return;
|
|
}
|
|
let membersAssigned = state.membersAssigned.get(moduleName);
|
|
if (membersAssigned == null) {
|
|
membersAssigned = new Set();
|
|
state.membersAssigned.set(moduleName, membersAssigned);
|
|
}
|
|
membersAssigned.add(memberPropertyName);
|
|
}
|
|
function isExcludedMemberAssignment(moduleName, memberPropertyName, state) {
|
|
const excludedAliases = state.membersAssigned.get(moduleName);
|
|
return excludedAliases != null && excludedAliases.has(memberPropertyName);
|
|
}
|
|
function getMemberPropertyName(node) {
|
|
if (node.property.type === "Identifier") {
|
|
return node.property.name;
|
|
}
|
|
if (node.property.type === "StringLiteral") {
|
|
return node.property.value;
|
|
}
|
|
return null;
|
|
}
|
|
function deleteLocation(node) {
|
|
delete node.start;
|
|
delete node.end;
|
|
delete node.loc;
|
|
}
|
|
function parseInlineableAlias(path, state) {
|
|
const module = getInlineableModule(path, state);
|
|
if (module == null) {
|
|
return null;
|
|
}
|
|
const { moduleName, requireFnName } = module;
|
|
const parentPath = path.parentPath;
|
|
if (parentPath == null) {
|
|
return null;
|
|
}
|
|
const grandParentPath = parentPath.parentPath;
|
|
if (grandParentPath == null) {
|
|
return null;
|
|
}
|
|
if (path.parent.type !== "VariableDeclarator") {
|
|
return null;
|
|
}
|
|
const variableDeclarator = path.parent;
|
|
if (variableDeclarator.id.type !== "Identifier") {
|
|
return null;
|
|
}
|
|
const identifier = variableDeclarator.id;
|
|
const isValid =
|
|
parentPath.parent.type === "VariableDeclaration" &&
|
|
grandParentPath.parent.type === "Program";
|
|
return !isValid || parentPath.node == null
|
|
? null
|
|
: {
|
|
declarationPath: parentPath,
|
|
moduleName,
|
|
requireFnName,
|
|
identifierName: identifier.name,
|
|
};
|
|
}
|
|
function parseInlineableMemberAlias(path, state) {
|
|
const module = getInlineableModule(path, state);
|
|
if (module == null) {
|
|
return null;
|
|
}
|
|
const { moduleName, requireFnName } = module;
|
|
const parent = path.parent;
|
|
const parentPath = path.parentPath;
|
|
if (parentPath == null) {
|
|
return null;
|
|
}
|
|
const grandParentPath = parentPath.parentPath;
|
|
if (grandParentPath == null) {
|
|
return null;
|
|
}
|
|
if (parent.type !== "MemberExpression") {
|
|
return null;
|
|
}
|
|
const memberExpression = parent;
|
|
if (parentPath.parent.type !== "VariableDeclarator") {
|
|
return null;
|
|
}
|
|
const variableDeclarator = parentPath.parent;
|
|
if (variableDeclarator.id.type !== "Identifier") {
|
|
return null;
|
|
}
|
|
const identifier = variableDeclarator.id;
|
|
if (
|
|
grandParentPath.parent.type !== "VariableDeclaration" ||
|
|
grandParentPath.parentPath?.parent.type !== "Program" ||
|
|
grandParentPath.node == null
|
|
) {
|
|
return null;
|
|
}
|
|
const memberPropertyName = getMemberPropertyName(memberExpression);
|
|
return memberPropertyName == null ||
|
|
isExcludedMemberAssignment(moduleName, memberPropertyName, state)
|
|
? null
|
|
: {
|
|
declarationPath: grandParentPath,
|
|
moduleName,
|
|
requireFnName,
|
|
identifierName: identifier.name,
|
|
};
|
|
}
|
|
function getInlineableModule(path, state) {
|
|
const node = path.node;
|
|
const isInlineable =
|
|
node.type === "CallExpression" &&
|
|
node.callee.type === "Identifier" &&
|
|
state.inlineableCalls.has(node.callee.name) &&
|
|
node["arguments"].length >= 1;
|
|
if (!isInlineable) {
|
|
return null;
|
|
}
|
|
let moduleName =
|
|
node["arguments"][0].type === "StringLiteral"
|
|
? node["arguments"][0].value
|
|
: null;
|
|
if (moduleName == null) {
|
|
const callNode = node["arguments"][0];
|
|
if (
|
|
callNode.type === "CallExpression" &&
|
|
callNode.callee.type === "MemberExpression" &&
|
|
callNode.callee.object.type === "Identifier"
|
|
) {
|
|
const callee = callNode.callee;
|
|
moduleName =
|
|
callee.object.type === "Identifier" &&
|
|
state.inlineableCalls.has(callee.object.name) &&
|
|
callee.property.type === "Identifier" &&
|
|
callee.property.name === "resolve" &&
|
|
callNode["arguments"].length >= 1 &&
|
|
callNode["arguments"][0].type === "StringLiteral"
|
|
? callNode["arguments"][0].value
|
|
: null;
|
|
}
|
|
}
|
|
const fnName = node.callee.name;
|
|
if (fnName == null) {
|
|
return null;
|
|
}
|
|
const isRequireInScope = path.scope.getBinding(fnName) != null;
|
|
return moduleName == null ||
|
|
state.ignoredRequires.has(moduleName) ||
|
|
moduleName.startsWith("@babel/runtime/") ||
|
|
isRequireInScope
|
|
? null
|
|
: {
|
|
moduleName,
|
|
requireFnName: fnName,
|
|
};
|
|
}
|
|
function getNearestLocFromPath(path) {
|
|
let current = path;
|
|
while (current && !current.node.loc) {
|
|
current = current.parentPath;
|
|
}
|
|
return current?.node.loc;
|
|
}
|
|
function isBranch(t, node) {
|
|
return (
|
|
t.isIfStatement(node) ||
|
|
t.isLogicalExpression(node) ||
|
|
t.isConditionalExpression(node) ||
|
|
t.isSwitchStatement(node) ||
|
|
t.isSwitchCase(node) ||
|
|
t.isForStatement(node) ||
|
|
t.isForInStatement(node) ||
|
|
t.isForOfStatement(node) ||
|
|
t.isWhileStatement(node)
|
|
);
|
|
}
|
|
function isDirectlyEnclosedByBlock(t, path) {
|
|
let curPath = path;
|
|
while (curPath) {
|
|
if (isBranch(t, curPath.node)) {
|
|
return false;
|
|
}
|
|
if (t.isBlockStatement(curPath.node)) {
|
|
return true;
|
|
}
|
|
curPath = curPath.parentPath;
|
|
}
|
|
return true;
|
|
}
|
|
function addStmtToBlock(block, stmt, idx) {
|
|
const scopeBody = block.body;
|
|
if (Array.isArray(scopeBody)) {
|
|
scopeBody.splice(idx, 0, stmt);
|
|
return true;
|
|
} else if (scopeBody && Array.isArray(scopeBody.body)) {
|
|
scopeBody.body.splice(idx, 0, stmt);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|