wxt
Version:
⚡ Next-gen Web Extension Framework
150 lines (149 loc) • 5.58 kB
JavaScript
import { parseModule } from "magicast";
//#region src/core/utils/transform.ts
/**
* Removes any code used at runtime related to an entrypoint's main function.
*
* 1. Removes or clears out `main` function from returned object
* 2. Removes any unused functions/variables outside the definition that aren't
* being called/used
* 3. Removes unused imports
* 4. Removes value-less, side-effect only imports (like `import "./styles.css"` or
* `import "polyfill"`)
*/
function removeMainFunctionCode(code) {
const mod = parseModule(code);
emptyMainFunction(mod);
let removedCount = 0;
let depth = 0;
const maxDepth = 10;
do {
removedCount = 0;
removedCount += removeUnusedTopLevelVariables(mod);
removedCount += removeUnusedTopLevelFunctions(mod);
removedCount += removeUnusedImports(mod);
} while (removedCount > 0 && depth++ <= maxDepth);
removeSideEffectImports(mod);
return mod.generate();
}
function emptyMainFunction(mod) {
if (mod.exports?.default?.$type === "function-call") {
if (mod.exports.default.$ast?.arguments?.[0]?.body) delete mod.exports.default.$ast.arguments[0];
else if (mod.exports.default.$ast?.arguments?.[0]?.properties) mod.exports.default.$ast.arguments[0].properties = mod.exports.default.$ast.arguments[0].properties.filter((prop) => prop.key?.name !== "main");
}
}
function removeUnusedTopLevelVariables(mod) {
const usedMap = findUsedIdentifiers(getSimpleAstJson(mod.$ast));
let deletedCount = 0;
const ast = mod.$ast;
const isUsed = (id) => {
return id?.type === "Identifier" && usedMap.get(id.name);
};
const cleanArrayPattern = (pattern) => {
const elements = pattern.elements;
for (let i = elements.length - 1; i >= 0; i--) {
const el = elements[i];
if (el?.type === "Identifier" && !isUsed(el)) {
elements.splice(i, 1);
deletedCount++;
}
}
return elements.length === 0;
};
const cleanObjectPattern = (pattern) => {
const properties = pattern.properties;
for (let i = properties.length - 1; i >= 0; i--) {
const prop = properties[i];
if (prop.type === "Property") {
const value = prop.value;
if (value.type === "ObjectPattern") {
if (cleanObjectPattern(value)) properties.splice(i, 1);
} else if (value.type === "ArrayPattern") {
if (cleanArrayPattern(value)) properties.splice(i, 1);
} else if (value.type === "Identifier" && !isUsed(value)) {
properties.splice(i, 1);
deletedCount++;
}
} else if (prop.type === "RestElement") {
const arg = prop.argument;
if (arg.type === "Identifier" && !isUsed(arg)) {
properties.splice(i, 1);
deletedCount++;
}
}
}
return properties.length === 0;
};
for (let i = ast.body.length - 1; i >= 0; i--) {
if (ast.body[i].type !== "VariableDeclaration") continue;
for (let j = ast.body[i].declarations.length - 1; j >= 0; j--) {
const id = ast.body[i].declarations[j].id;
let shouldRemove = false;
if (id.type === "Identifier") {
shouldRemove = !isUsed(id);
if (shouldRemove) deletedCount++;
} else if (id.type === "ArrayPattern") shouldRemove = cleanArrayPattern(id);
else if (id.type === "ObjectPattern") shouldRemove = cleanObjectPattern(id);
if (shouldRemove) ast.body[i].declarations.splice(j, 1);
}
if (ast.body[i].declarations.length === 0) ast.body.splice(i, 1);
}
return deletedCount;
}
function removeUnusedTopLevelFunctions(mod) {
const usedMap = findUsedIdentifiers(getSimpleAstJson(mod.$ast));
let deletedCount = 0;
const ast = mod.$ast;
for (let i = ast.body.length - 1; i >= 0; i--) if (ast.body[i].type === "FunctionDeclaration" && !usedMap.get(ast.body[i].id.name)) {
ast.body.splice(i, 1);
deletedCount++;
}
return deletedCount;
}
function removeUnusedImports(mod) {
const usedMap = findUsedIdentifiers(getSimpleAstJson(mod.$ast));
const importSymbols = Object.keys(mod.imports);
let deletedCount = 0;
importSymbols.forEach((name) => {
if (usedMap.get(name)) return;
delete mod.imports[name];
deletedCount++;
});
return deletedCount;
}
function findUsedIdentifiers(simpleAst) {
const usedMap = /* @__PURE__ */ new Map();
const queue = [simpleAst];
for (const item of queue) if (!item) {} else if (Array.isArray(item)) queue.push(...item);
else if (item.type === "ImportDeclaration") continue;
else if (item.type === "Identifier") usedMap.set(item.name, true);
else if (typeof item === "object") queue.push(Object.entries(item).filter({
FunctionDeclaration: ([key]) => key !== "id",
ObjectProperty: ([key]) => key !== "key",
VariableDeclarator: ([key]) => key !== "id"
}[item.type] ?? (() => true)).map(([_, value]) => value));
return usedMap;
}
function deleteImportAst(mod, shouldDelete) {
const importIndexesToDelete = [];
mod.$ast.body.forEach((node, index) => {
if (node.type === "ImportDeclaration" && shouldDelete(node)) importIndexesToDelete.push(index);
});
importIndexesToDelete.reverse().forEach((i) => {
delete mod.$ast.body[i];
});
}
function removeSideEffectImports(mod) {
deleteImportAst(mod, (node) => node.specifiers.length === 0);
}
/**
* Util to get the AST as a simple JSON object, stripping out large objects and
* file locations to keep it readable.
*/
function getSimpleAstJson(ast) {
if (!ast) return ast;
else if (Array.isArray(ast)) return ast.map(getSimpleAstJson);
else if (typeof ast === "object") return Object.fromEntries(Object.entries(ast).filter(([key]) => key !== "loc" && key !== "start" && key !== "end").map(([key, value]) => [key, getSimpleAstJson(value)]));
else return ast;
}
//#endregion
export { removeMainFunctionCode };