803 lines
24 KiB
TypeScript
803 lines
24 KiB
TypeScript
|
|
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;
|
|||
|
|
}
|