import { parse } from "@babel/parser"; import { default as traverse } from "@babel/traverse"; import { default as generate } from "@babel/generator"; import * as t from "@babel/types"; import { StaticArrayProcessor } from "./processors/static-array-processor.js"; import { JSXUtils } from "./jsx-utils.js"; // Helper function to check if JSX element contains dynamic content export function checkIfElementHasDynamicContent(jsxElement) { let hasDynamicContent = false; // Helper function to check if any node contains dynamic patterns function checkNodeForDynamicContent(node) { // JSX expressions like {variable}, {func()}, {obj.prop} if (t.isJSXExpressionContainer(node)) { const expression = node.expression; // Skip empty expressions {} if (t.isJSXEmptyExpression(expression)) { return false; } // Any non-literal expression is considered dynamic if (!t.isLiteral(expression)) { return true; } } // Template literals with expressions `Hello ${name}` if (t.isTemplateLiteral(node) && node.expressions.length > 0) { return true; } // Member expressions like props.title, state.value if (t.isMemberExpression(node)) { return true; } // Function calls like getData(), format() if (t.isCallExpression(node)) { return true; } // Conditional expressions like condition ? "yes" : "no" if (t.isConditionalExpression(node)) { return true; } // Identifier references (could be props, state, variables) if (t.isIdentifier(node)) { // Common dynamic identifiers const dynamicNames = [ "props", "state", "data", "item", "value", "text", "content", ]; if (dynamicNames.some((name) => node.name.includes(name))) { return true; } } return false; } // Recursively traverse all child nodes function traverseNode(node) { if (checkNodeForDynamicContent(node)) { hasDynamicContent = true; return; } // Recursively check child nodes Object.keys(node).forEach((key) => { const value = node[key]; if (Array.isArray(value)) { value.forEach((child) => { if (child && typeof child === "object" && child.type) { traverseNode(child); } }); } else if (value && typeof value === "object" && value.type) { traverseNode(value); } }); } // Check the element's own attributes for dynamic content const attributes = jsxElement.openingElement?.attributes || []; attributes.forEach((attr) => { if (hasDynamicContent) return; // Early exit if already found dynamic content // Spread attributes like {...props} are always dynamic if (t.isJSXSpreadAttribute(attr)) { hasDynamicContent = true; return; } // Check attribute values for dynamic expressions if (t.isJSXAttribute(attr) && attr.value) { traverseNode(attr.value); } }); // Check all children of the JSX element jsxElement.children.forEach((child) => { if (hasDynamicContent) return; // Early exit if already found dynamic content traverseNode(child); }); return hasDynamicContent; } export function visualEditPlugin() { return { name: "visual-edit-transform", apply: (config) => config.mode === "development", enforce: "pre", order: "pre", // Inject Tailwind CDN for visual editing capabilities transformIndexHtml(html) { // Inject the Tailwind CSS CDN script right before the closing tag const tailwindScript = ` \n \n `; return html.replace("", tailwindScript + ""); }, transform(code, id) { // Skip node_modules and visual-edit-agent itself if (id.includes("node_modules") || id.includes("visual-edit-agent")) { return null; } // Process JS/JSX/TS/TSX files if (!id.match(/\.(jsx?|tsx?)$/)) { return null; } // Extract filename from path, preserving pages/ or components/ structure const pathParts = id.split("/"); let filename; // Check if this is a pages or components file if (id.includes("/pages/")) { const pagesIndex = pathParts.findIndex((part) => part === "pages"); if (pagesIndex >= 0 && pagesIndex < pathParts.length - 1) { // Get all parts from 'pages' to the file, preserving nested structure const relevantParts = pathParts.slice(pagesIndex, pathParts.length); const lastPart = relevantParts[relevantParts.length - 1]; // Remove file extension from the last part relevantParts[relevantParts.length - 1] = lastPart.includes(".") ? lastPart.split(".")[0] : lastPart; filename = relevantParts.join("/"); } else { filename = pathParts[pathParts.length - 1]; if (filename.includes(".")) { filename = filename.split(".")[0]; } } } else if (id.includes("/components/")) { const componentsIndex = pathParts.findIndex((part) => part === "components"); if (componentsIndex >= 0 && componentsIndex < pathParts.length - 1) { // Get all parts from 'components' to the file, preserving nested structure const relevantParts = pathParts.slice(componentsIndex, pathParts.length); const lastPart = relevantParts[relevantParts.length - 1]; // Remove file extension from the last part relevantParts[relevantParts.length - 1] = lastPart.includes(".") ? lastPart.split(".")[0] : lastPart; filename = relevantParts.join("/"); } else { filename = pathParts[pathParts.length - 1]; if (filename.includes(".")) { filename = filename.split(".")[0]; } } } else { // For other files (like layout), just use the filename filename = pathParts[pathParts.length - 1]; if (filename.includes(".")) { filename = filename.split(".")[0]; } } try { // Parse the code into an AST const ast = parse(code, { sourceType: "module", plugins: [ "jsx", "typescript", "decorators-legacy", "classProperties", "objectRestSpread", "functionBind", "exportDefaultFrom", "exportNamespaceFrom", "dynamicImport", "nullishCoalescingOperator", "optionalChaining", "asyncGenerators", "bigInt", "optionalCatchBinding", "throwExpressions", ], }); // Traverse the AST and add source location and dynamic content attributes to JSX elements JSXUtils.init(t); const staticArrayProcessor = new StaticArrayProcessor(t); let elementsProcessed = 0; traverse.default(ast, { JSXElement(path) { const jsxElement = path.node; const openingElement = jsxElement.openingElement; // Skip fragments if (t.isJSXFragment(jsxElement)) return; // Skip if already has source location attribute const hasSourceLocation = openingElement.attributes.some((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === "data-source-location"); if (hasSourceLocation) return; // Get line and column from AST node location const { line, column } = openingElement.loc?.start || { line: 1, column: 0, }; // Create the source location attribute const sourceLocationAttr = t.jsxAttribute(t.jsxIdentifier("data-source-location"), t.stringLiteral(`${filename}:${line}:${column}`)); // Check if element has dynamic content const isDynamic = checkIfElementHasDynamicContent(jsxElement); // Create the dynamic content attribute const dynamicContentAttr = t.jsxAttribute(t.jsxIdentifier("data-dynamic-content"), t.stringLiteral(isDynamic ? "true" : "false")); // Add both attributes to the beginning of the attributes array openingElement.attributes.unshift(sourceLocationAttr, dynamicContentAttr); staticArrayProcessor.process(path.get("openingElement")); elementsProcessed++; }, }); // Generate the code back from the AST const result = generate.default(ast, { compact: false, concise: false, retainLines: true, }); return { code: result.code, map: null, }; } catch (error) { console.error("Failed to add source location to JSX:", error); return { code: code, // Return original code on failure map: null, }; } }, }; } //# sourceMappingURL=visual-edit-plugin.js.map