erp-platform/ui/src/utils/codeParser.ts

803 lines
24 KiB
TypeScript
Raw Normal View History

2025-08-11 06:34:44 +00:00
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import * as t from "@babel/types";
import generate from "@babel/generator";
import { ComponentInfo } from "../@types/componentInfo";
export interface ParsedComponent {
components: ComponentInfo[];
imports: string[];
hooks: string[];
}
export const generateUniqueId = (): string => {
// Include timestamp for better uniqueness
return (
"c_" +
Date.now().toString(36) +
"_" +
Math.random().toString(36).substring(2, 8)
);
};
export const generateSingleComponentJSX = (
type: string,
props: Record<string, { type: string; value: any }>
): string => {
const attributes = Object.entries(props)
.filter(([key]) => key !== "children")
.map(([key, propInfo]) => {
const { type, value } = propInfo;
// null ve boş değerleri ekleme
if (value === null || value === "") return "";
// object tipindeki boş nesneleri atla
if (
type === "object" &&
(value === "" ||
(typeof value === "object" &&
value !== null &&
Object.keys(value).length === 0))
) {
return "";
}
// false boolean'ları atla
if (type === "boolean" && value === false) return "";
// true boolean'ları yalnızca anahtar olarak ekle
if (type === "boolean" && value === true) return `${key}`;
// number her zaman eklenir
if (type === "number") return `${key}={${value}}`;
// Diğer her şey
return `${key}=${JSON.stringify(value)}`;
})
.filter(Boolean)
.join(" ");
const children = props.children?.value ?? "";
return `<${type}${attributes ? " " + attributes : ""}>${children}</${type}>`;
};
export const parseReactCode = (code: string): ParsedComponent => {
try {
// Clean up duplicate imports before parsing
const cleanCode = cleanupDuplicateImports(code);
const ast = parser.parse(cleanCode, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
const components: ComponentInfo[] = [];
const imports: string[] = [];
const hooks: string[] = [];
traverse(ast, {
ImportDeclaration(path) {
if (path.node.source.value === "react") {
path.node.specifiers.forEach((spec) => {
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
imports.push(spec.imported.name);
}
});
}
},
CallExpression(path) {
if (t.isIdentifier(path.node.callee)) {
const functionName = path.node.callee.name;
if (functionName.startsWith("use")) {
hooks.push(functionName);
}
}
},
JSXElement(path) {
const element = path.node;
if (t.isJSXIdentifier(element.openingElement.name)) {
const componentName = element.openingElement.name.name;
const props: Record<string, any> = {};
// Extract props
element.openingElement.attributes.forEach((attr) => {
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
const propName = attr.name.name;
let propValue: any = "";
if (attr.value) {
if (t.isStringLiteral(attr.value)) {
propValue = attr.value.value;
} else if (t.isJSXExpressionContainer(attr.value)) {
if (t.isStringLiteral(attr.value.expression)) {
propValue = attr.value.expression.value;
} else if (t.isBooleanLiteral(attr.value.expression)) {
propValue = attr.value.expression.value;
} else if (t.isNumericLiteral(attr.value.expression)) {
propValue = attr.value.expression.value;
} else {
// For complex expressions, store as string
propValue = cleanCode.slice(
attr.value.expression.start!,
attr.value.expression.end!
);
}
}
} else {
propValue = true; // Boolean prop without value
}
props[propName] = propValue;
}
});
// Add unique ID if not present
if (!props.id || !props.id.startsWith("c_")) {
props.id = generateUniqueId();
}
// Extract children content - only if it's simple text/expression content
const childrenContent = extractChildrenContent(element, cleanCode);
const hasNestedElements =
element.children &&
element.children.some(
(child) => t.isJSXElement(child) || t.isJSXFragment(child)
);
const component: ComponentInfo = {
id: props.id,
name: componentName,
type: componentName.toLowerCase(),
props,
children: hasNestedElements ? undefined : childrenContent,
startLine: element.loc?.start.line || 0,
endLine: element.loc?.end.line || 0,
startColumn: element.loc?.start.column || 0,
endColumn: element.loc?.end.column || 0,
};
components.push(component);
}
},
});
return { components, imports, hooks };
} catch (error) {
console.error("Error parsing React code:", error);
return { components: [], imports: [], hooks: [] };
}
};
const extractChildrenContent = (element: any, code: string): string => {
if (!element.children || element.children.length === 0) {
return "";
}
// Get the content between opening and closing tags
const start = element.openingElement.end;
const end = element.closingElement
? element.closingElement.start
: element.end;
if (start && end && start < end) {
return code.slice(start, end).trim();
}
return "";
};
export const generateHookVariableName = (
hookType: string,
componentId: string
): string => {
// Use the full componentId without shortening
const cleanId = componentId.replace("c_", ""); // Remove c_ prefix but keep the rest
switch (hookType) {
case "useState":
return `state_c_${cleanId}`;
case "useRef":
return `ref_c_${cleanId}`;
case "useEffect":
return `effect_c_${cleanId}`;
case "useCallback":
return `callback_c_${cleanId}`;
case "useMemo":
return `memo_c_${cleanId}`;
default:
return `hook_c_${cleanId}`;
}
};
export const removeHookFromCode = (
code: string,
hookType: string,
componentId: string
): string => {
const varName = generateHookVariableName(hookType, componentId);
// Sadece hook tanım satırını sil
// ^\s*const\s+\[?.*?varName.*?\]?\s*=\s*hookType\(.*?\);\s*$
// Bu satırı, başında ve sonunda yalnızca bir satırı siler şekilde tasarla
const hookLineRegex = new RegExp(
`^\\s*const\\s+(?:\\[.*?${varName}.*?\\]|${varName})\\s*=\\s*${hookType}\\([^)]*\\);?\\s*$`,
"gm"
);
code = code.replace(hookLineRegex, "");
// Import'tan kaldırma mantığı aynı kalabilir
const remainingHooks = code.match(new RegExp(`${hookType}\\(`, "g"));
if (!remainingHooks || remainingHooks.length === 0) {
const reactImportRegex =
// eslint-disable-next-line no-useless-escape
/import\s+(React\s*,\s*)?\{([^}]*)\}\s+from\s+['"]react['\"];?\s*/g;
let match;
let newCode = code;
while ((match = reactImportRegex.exec(code)) !== null) {
const importLine = match[0];
const hooks = match[2]
.split(",")
.map((h) => h.trim())
.filter((h) => h && h !== hookType);
let newImport = "";
if (hooks.length > 0) {
newImport = match[1]
? `import React, { ${hooks.join(", ")} } from 'react';\n`
: `import { ${hooks.join(", ")} } from 'react';\n`;
} else {
newImport = match[1] ? `import React from 'react';\n` : "";
}
newCode = newCode.replace(importLine, newImport);
}
code = newCode;
}
// Duplicate import cleanup aynı kalabilir
code = cleanupDuplicateImports(code);
return code;
};
export const insertJSXAtPosition = (
code: string,
jsx: string,
position: { line: number; column: number }
): string => {
const lines = code.split("\n");
// JSX kodunu istenen satıra ekle
lines.splice(position.line, 0, jsx);
return lines.join("\n");
};
export const generateComponentJSX = (component: ComponentInfo): string => {
const { type, props } = component;
const propsString = Object.entries(props)
.filter(([key, val]) => {
if (
key === "children" ||
!val ||
typeof val !== "object" ||
!("value" in val)
)
return false;
const isFunction = val.type === "function";
const isObject = val.type === "object";
const isBoolean = typeof val.value === "boolean";
return (
val.value !== null &&
val.value !== undefined &&
(isFunction || isBoolean || val.value !== "") &&
(isObject || Object.keys(val.value).length > 0)
);
})
.map(([key, val]) => {
const propVal =
val && typeof val === "object" && "value" in val ? val.value : val;
if (typeof propVal === "string") return `${key}="${propVal}"`;
if (typeof propVal === "number") return `${key}={${propVal}}`;
if (typeof propVal === "boolean")
return propVal ? `${key}` : `${key}={false}`;
return `${key}={${JSON.stringify(propVal)}}`;
})
.join(" ");
console.log("📝 App: Generated props string:", propsString);
const children =
props.children &&
typeof props.children === "object" &&
"value" in props.children
? props.children.value
: "";
const hasProps = propsString.length > 0;
const hasChildren = children && children.length > 0;
if (hasChildren) {
return `<${type} id="${component.id}"${
hasProps ? " " + propsString : ""
}>${children}</${type}>`;
} else {
return `<${type} id="${component.id}"${
hasProps ? " " + propsString : ""
} />`;
}
};
export const generateHookCode = (
hookType: string,
componentId: string,
componentType: string,
initialValue?: any
): string => {
const varName = generateHookVariableName(hookType, componentId);
switch (hookType) {
case "useState":
const defaultValue = getDefaultValueForComponent(
componentType,
initialValue
);
// Generate proper camelCase setter name from state variable
const setterName = `set${
varName.charAt(0).toUpperCase() + varName.slice(1)
}`;
return `const [${varName}, ${setterName}] = useState(${defaultValue});`;
case "useRef":
return `const ${varName} = useRef(null);`;
case "useCallback":
return `const ${varName} = useCallback(() => {\n // Callback logic here\n }, []);`;
case "useMemo":
return `const ${varName} = useMemo(() => {\n // Memo logic here\n }, []);`;
default:
return `const ${varName} = ${hookType}();`;
}
};
const getDefaultValueForComponent = (
componentType: string,
initialValue?: any
): string => {
if (initialValue !== undefined) {
return typeof initialValue === "string"
? `'${initialValue}'`
: String(initialValue);
}
switch (componentType) {
case "input":
case "textarea":
return "''";
case "checkbox":
return "false";
case "select":
return "''";
case "button":
return "false";
default:
return "''";
}
};
export const updateComponentProp = (
code: string,
componentId: string,
propName: string,
propValue: any
): string => {
// Special handling for children
if (propName === "children") {
return updateComponentChildren(code, componentId, propValue);
}
console.log("🔧 updateComponentProp called:", {
componentId,
propName,
propValue,
propType: typeof propValue,
});
// Clean up duplicate imports before parsing
const cleanCode = cleanupDuplicateImports(code);
try {
const ast = parser.parse(cleanCode, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
let updatedCode = cleanCode;
let offset = 0;
traverse(ast, {
JSXElement(path) {
const element = path.node;
if (t.isJSXIdentifier(element.openingElement.name)) {
// Find the component with matching ID
const idAttr = element.openingElement.attributes.find(
(attr) =>
t.isJSXAttribute(attr) &&
t.isJSXIdentifier(attr.name) &&
attr.name.name === "id" &&
attr.value &&
t.isStringLiteral(attr.value) &&
attr.value.value === componentId
);
if (idAttr) {
console.log("🎯 Found component with ID:", componentId);
// Find existing prop or add new one
const existingPropIndex =
element.openingElement.attributes.findIndex(
(attr) =>
t.isJSXAttribute(attr) &&
t.isJSXIdentifier(attr.name) &&
attr.name.name === propName
);
console.log("📍 Existing prop index:", existingPropIndex);
// Generate proper JSX attribute string
let newPropString: string | null = null;
let shouldRemoveAttribute = false;
if (
propValue === null ||
propValue === undefined ||
propValue === ""
) {
shouldRemoveAttribute = true;
console.log("🗑️ Will remove attribute - value is:", propValue);
} else if (propValue === true) {
newPropString = propName; // Boolean true: just the attribute name
console.log("✅ Boolean true prop:", newPropString);
} else if (typeof propValue === "string") {
// Handle event props and JSX expressions
if (propName.startsWith("on") || propValue.startsWith("{")) {
// Event props or JSX expressions - ensure proper braces
if (propValue.startsWith("{") && propValue.endsWith("}")) {
newPropString = `${propName}=${propValue}`;
} else {
newPropString = `${propName}={${propValue}}`;
}
console.log("🎯 Event/JSX prop:", newPropString);
} else {
newPropString = `${propName}="${propValue}"`; // String literal
console.log("📝 String prop:", newPropString);
}
} else if (typeof propValue === "boolean") {
newPropString = `${propName}={${propValue}}`;
console.log("🔘 Boolean prop:", newPropString);
} else {
newPropString = `${propName}={${propValue}}`; // Other values as JSX expression
console.log("🔢 Other prop:", newPropString);
}
if (existingPropIndex !== -1) {
// Update existing prop
const existingProp =
element.openingElement.attributes[existingPropIndex];
if (t.isJSXAttribute(existingProp)) {
const attrStart = existingProp.start! + offset;
const attrEnd = existingProp.end! + offset;
console.log(
"🔄 Updating existing prop at position:",
attrStart,
"-",
attrEnd
);
console.log(
"🔄 Old attribute:",
updatedCode.slice(attrStart, attrEnd)
);
if (shouldRemoveAttribute) {
// Remove the entire attribute with proper whitespace handling
let removeStart = attrStart;
const removeEnd = attrEnd;
// Look for whitespace/newline before the attribute
while (removeStart > 0) {
const char = updatedCode[removeStart - 1];
if (
char === " " ||
char === "\t" ||
char === "\n" ||
char === "\r"
) {
removeStart--;
} else {
break;
}
}
// Make sure we don't remove too much - keep at least one space if needed
const beforeChar =
removeStart > 0 ? updatedCode[removeStart - 1] : "";
const afterChar =
removeEnd < updatedCode.length
? updatedCode[removeEnd]
: "";
// If we're between two attributes or after tag name, ensure proper spacing
if (
beforeChar &&
beforeChar !== " " &&
beforeChar !== "\n" &&
beforeChar !== "\t" &&
afterChar &&
afterChar !== " " &&
afterChar !== "\n" &&
afterChar !== "\t" &&
afterChar !== ">" &&
afterChar !== "/"
) {
// Insert a space to prevent attributes from merging
updatedCode =
updatedCode.slice(0, removeStart) +
" " +
updatedCode.slice(removeEnd);
offset -= removeEnd - removeStart - 1; // -1 because we added a space
} else {
updatedCode =
updatedCode.slice(0, removeStart) +
updatedCode.slice(removeEnd);
offset -= removeEnd - removeStart;
}
console.log(
"🗑️ Removed attribute from",
removeStart,
"to",
removeEnd
);
} else {
// Replace the entire attribute
if (newPropString) {
console.log("🔄 Replacing with:", newPropString);
updatedCode =
updatedCode.slice(0, attrStart) +
newPropString +
updatedCode.slice(attrEnd);
offset += newPropString.length - (attrEnd - attrStart);
}
}
}
// Stop traversal after modifying the target component
path.stop();
} else if (!shouldRemoveAttribute && newPropString) {
// Add new prop
const insertPos = element.openingElement.name.end! + offset;
const propToInsert = ` ${newPropString}`;
console.log(
" Adding new prop at position",
insertPos,
":",
propToInsert
);
updatedCode =
updatedCode.slice(0, insertPos) +
propToInsert +
updatedCode.slice(insertPos);
offset += propToInsert.length;
}
// Stop traversal after processing the target component
path.stop();
}
}
},
});
console.log(
"✅ updateComponentProp completed. Code changed:",
code !== updatedCode
);
return updatedCode;
} catch (error) {
console.error("Error updating component prop:", error);
return code;
}
};
const cleanupDuplicateImports = (code: string): string => {
// Remove duplicate React imports
const reactImportRegex =
/import\s+React(?:\s*,\s*\{[^}]*\})?\s+from\s+['"]react['"];\s*/g;
const reactImports = code.match(reactImportRegex);
if (reactImports && reactImports.length > 1) {
console.log("🔧 Found duplicate React imports:", reactImports.length);
// Collect all hooks from all imports
const allHooks = new Set<string>();
reactImports.forEach((importLine) => {
const hooksMatch = importLine.match(/\{([^}]*)\}/);
if (hooksMatch) {
const hooks = hooksMatch[1]
.split(",")
.map((h) => h.trim())
.filter((h) => h);
hooks.forEach((hook) => allHooks.add(hook));
}
});
// Remove all React imports
let cleanedCode = code.replace(reactImportRegex, "");
// Add single consolidated import
if (allHooks.size > 0) {
const consolidatedImport = `import React, { ${Array.from(allHooks).join(
", "
)} } from 'react';\n`;
cleanedCode = consolidatedImport + cleanedCode;
} // else: Hiç import ekleme
console.log("✅ Consolidated imports:", Array.from(allHooks));
return cleanedCode;
}
return code;
};
export const updateComponentProps = (
code: string,
componentId: string,
updates: Record<string, any>
): string => {
console.log("🔄 updateComponentProps called:", { componentId, updates });
// Apply each update individually to avoid offset calculation issues
let updatedCode = code;
Object.entries(updates).forEach(([propName, propValue]) => {
console.log(`<EFBFBD> Processing update: ${propName} = ${propValue}`);
updatedCode = updateComponentProp(
updatedCode,
componentId,
propName,
propValue
);
});
console.log(
"✅ updateComponentProps completed. Code changed:",
code !== updatedCode
);
return updatedCode;
};
const updateComponentChildren = (
code: string,
componentId: string,
newChildren: string
): string => {
try {
const ast = parser.parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
let updatedCode = code;
let offset = 0;
traverse(ast, {
JSXElement(path) {
const element = path.node;
if (t.isJSXIdentifier(element.openingElement.name)) {
// Find the component with matching ID
const idAttr = element.openingElement.attributes.find(
(attr) =>
t.isJSXAttribute(attr) &&
t.isJSXIdentifier(attr.name) &&
attr.name.name === "id" &&
attr.value &&
t.isStringLiteral(attr.value) &&
attr.value.value === componentId
);
if (idAttr && element.closingElement) {
// Update children content
const start = element.openingElement.end! + offset;
const end = element.closingElement.start! + offset;
updatedCode =
updatedCode.slice(0, start) +
newChildren +
updatedCode.slice(end);
offset += newChildren.length - (end - start);
// Stop traversal after modifying the target component
path.stop();
}
}
},
});
return updatedCode;
} catch (error) {
console.error("Error updating component children:", error);
return code;
}
};
export function removeComponentAndHooksFromCode(
code: string,
componentId: string
): string {
let ast;
try {
ast = parser.parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
} catch (e) {
console.error("Parse error:", e);
return code;
}
// 1. JSX'i sil
traverse(ast, {
JSXElement(path) {
const el = path.node;
if (t.isJSXIdentifier(el.openingElement.name)) {
const idAttr = el.openingElement.attributes.find(
(attr) =>
t.isJSXAttribute(attr) &&
t.isJSXIdentifier(attr.name) &&
attr.name.name === "id" &&
attr.value &&
t.isStringLiteral(attr.value) &&
attr.value.value === componentId
);
if (idAttr) {
path.remove();
}
}
},
});
// 2. useState ve useRef hooklarını sil
traverse(ast, {
VariableDeclaration(path) {
const decl = path.node.declarations[0];
// useState
if (
t.isVariableDeclarator(decl) &&
t.isArrayPattern(decl.id) &&
t.isCallExpression(decl.init) &&
t.isIdentifier(decl.init.callee, { name: "useState" }) &&
decl.id.elements[0] &&
t.isIdentifier(decl.id.elements[0]) &&
decl.id.elements[0].name.includes(componentId)
) {
path.remove();
}
// useRef
if (
t.isVariableDeclarator(decl) &&
t.isIdentifier(decl.id) &&
t.isCallExpression(decl.init) &&
t.isIdentifier(decl.init.callee, { name: "useRef" }) &&
decl.id.name.includes(componentId)
) {
path.remove();
}
},
});
// 3. Boş satırları temizle
let output = generate(ast, { retainLines: true }).code;
output = output.replace(/\n{3,}/g, "\n\n");
return output;
}