bunext/dist/functions/bundler/strip-server-side-logic.js

93 lines
4.7 KiB
JavaScript

import path from "path";
import ts from "typescript";
export default function stripServerSideLogic({ txt_code, file_path }) {
const file_dir_name = path.dirname(file_path);
// 1. Initial Parse of the source
const sourceFile = ts.createSourceFile("file.tsx", txt_code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const printer = ts.createPrinter();
/**
* PASS 1: Remove 'server' export and resolve absolute paths
*/
const stripTransformer = (context) => (rootNode) => {
const visitor = (node) => {
// Remove 'export const server'
if (ts.isVariableStatement(node) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
if (node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) &&
d.name.text === "server")) {
return undefined;
}
}
// Convert relative imports to absolute
if (ts.isImportDeclaration(node)) {
const specifier = node.moduleSpecifier;
if (ts.isStringLiteral(specifier) &&
specifier.text.startsWith(".")) {
const absolutePath = path.resolve(file_dir_name, specifier.text);
return ts.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts.factory.createStringLiteral(absolutePath), node.attributes);
}
}
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(rootNode, visitor);
};
const step1Result = ts.transform(sourceFile, [stripTransformer]);
const intermediateCode = printer.printFile(step1Result.transformed[0]);
// 2. Re-parse to get a "Clean Slate" AST
const cleanSource = ts.createSourceFile("clean.tsx", intermediateCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
/**
* PASS 2: Collect all used Identifiers and Prune Imports
*/
const usedIdentifiers = new Set();
function walkAndFindUsages(node) {
if (ts.isIdentifier(node)) {
// We only care about identifiers that AREN'T the names in the import statements themselves
const parent = node.parent;
const isImportName = ts.isImportSpecifier(parent) ||
ts.isImportClause(parent) ||
ts.isNamespaceImport(parent);
if (!isImportName) {
usedIdentifiers.add(node.text);
}
}
ts.forEachChild(node, walkAndFindUsages);
}
walkAndFindUsages(cleanSource);
const cleanupTransformer = (context) => (rootNode) => {
const visitor = (node) => {
if (ts.isImportDeclaration(node)) {
const clause = node.importClause;
if (!clause)
return node;
// 1. Clean Named Imports: { A, B }
if (clause.namedBindings &&
ts.isNamedImports(clause.namedBindings)) {
const activeElements = clause.namedBindings.elements.filter((el) => usedIdentifiers.has(el.name.text));
// If no named imports are used and there is no default import, nix the whole line
if (activeElements.length === 0 && !clause.name)
return undefined;
// If we have some named imports left, update the node
return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly,
// Only keep default import if it's used
clause.name &&
usedIdentifiers.has(clause.name.text)
? clause.name
: undefined, ts.factory.createNamedImports(activeElements)), node.moduleSpecifier, node.attributes);
}
// 2. Clean Default Imports: import X from '...'
if (clause.name && !usedIdentifiers.has(clause.name.text)) {
// If there are no named bindings attached, nix the whole line
if (!clause.namedBindings)
return undefined;
// Otherwise, just strip the default name and keep the named bindings
return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly, undefined, clause.namedBindings), node.moduleSpecifier, node.attributes);
}
}
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(rootNode, visitor);
};
const finalResult = ts.transform(cleanSource, [cleanupTransformer]);
return printer.printFile(finalResult.transformed[0]);
}