Talk to Figma MCP

by sonnylazuardi
Verified
// This is the main code file for the Cursor MCP Figma plugin // It handles Figma API commands // Plugin state const state = { serverPort: 3055, // Default port }; // Show UI figma.showUI(__html__, { width: 350, height: 450 }); // Plugin commands from UI figma.ui.onmessage = async (msg) => { switch (msg.type) { case "update-settings": updateSettings(msg); break; case "notify": figma.notify(msg.message); break; case "close-plugin": figma.closePlugin(); break; case "execute-command": // Execute commands received from UI (which gets them from WebSocket) try { const result = await handleCommand(msg.command, msg.params); // Send result back to UI figma.ui.postMessage({ type: "command-result", id: msg.id, result, }); } catch (error) { figma.ui.postMessage({ type: "command-error", id: msg.id, error: error.message || "Error executing command", }); } break; } }; // Listen for plugin commands from menu figma.on("run", ({ command }) => { figma.ui.postMessage({ type: "auto-connect" }); }); // Update plugin settings function updateSettings(settings) { if (settings.serverPort) { state.serverPort = settings.serverPort; } figma.clientStorage.setAsync("settings", { serverPort: state.serverPort, }); } // Handle commands from UI async function handleCommand(command, params) { switch (command) { case "get_document_info": return await getDocumentInfo(); case "get_selection": return await getSelection(); case "get_node_info": if (!params || !params.nodeId) { throw new Error("Missing nodeId parameter"); } return await getNodeInfo(params.nodeId); case "get_nodes_info": if (!params || !params.nodeIds || !Array.isArray(params.nodeIds)) { throw new Error("Missing or invalid nodeIds parameter"); } return await getNodesInfo(params.nodeIds); case "create_rectangle": return await createRectangle(params); case "create_frame": return await createFrame(params); case "create_text": return await createText(params); case "set_fill_color": return await setFillColor(params); case "set_stroke_color": return await setStrokeColor(params); case "move_node": return await moveNode(params); case "resize_node": return await resizeNode(params); case "delete_node": return await deleteNode(params); case "get_styles": return await getStyles(); case "get_local_components": return await getLocalComponents(); // case "get_team_components": // return await getTeamComponents(); case "create_component_instance": return await createComponentInstance(params); case "export_node_as_image": return await exportNodeAsImage(params); case "set_corner_radius": return await setCornerRadius(params); case "set_text_content": return await setTextContent(params); case "clone_node": return await cloneNode(params); default: throw new Error(`Unknown command: ${command}`); } } // Command implementations async function getDocumentInfo() { await figma.currentPage.loadAsync(); const page = figma.currentPage; return { name: page.name, id: page.id, type: page.type, children: page.children.map((node) => ({ id: node.id, name: node.name, type: node.type, })), currentPage: { id: page.id, name: page.name, childCount: page.children.length, }, pages: [ { id: page.id, name: page.name, childCount: page.children.length, }, ], }; } async function getSelection() { return { selectionCount: figma.currentPage.selection.length, selection: figma.currentPage.selection.map((node) => ({ id: node.id, name: node.name, type: node.type, visible: node.visible, })), }; } async function getNodeInfo(nodeId) { const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } const response = await node.exportAsync({ format: "JSON_REST_V1", }); return response.document; } async function getNodesInfo(nodeIds) { try { // Load all nodes in parallel const nodes = await Promise.all( nodeIds.map((id) => figma.getNodeByIdAsync(id)) ); // Filter out any null values (nodes that weren't found) const validNodes = nodes.filter((node) => node !== null); // Export all valid nodes in parallel const responses = await Promise.all( validNodes.map(async (node) => { const response = await node.exportAsync({ format: "JSON_REST_V1", }); return { nodeId: node.id, document: response.document, }; }) ); return responses; } catch (error) { throw new Error(`Error getting nodes info: ${error.message}`); } } async function createRectangle(params) { const { x = 0, y = 0, width = 100, height = 100, name = "Rectangle", parentId, } = params || {}; const rect = figma.createRectangle(); rect.x = x; rect.y = y; rect.resize(width, height); rect.name = name; // If parentId is provided, append to that node, otherwise append to current page if (parentId) { const parentNode = await figma.getNodeByIdAsync(parentId); if (!parentNode) { throw new Error(`Parent node not found with ID: ${parentId}`); } if (!("appendChild" in parentNode)) { throw new Error(`Parent node does not support children: ${parentId}`); } parentNode.appendChild(rect); } else { figma.currentPage.appendChild(rect); } return { id: rect.id, name: rect.name, x: rect.x, y: rect.y, width: rect.width, height: rect.height, parentId: rect.parent ? rect.parent.id : undefined, }; } async function createFrame(params) { const { x = 0, y = 0, width = 100, height = 100, name = "Frame", parentId, fillColor, strokeColor, strokeWeight, } = params || {}; const frame = figma.createFrame(); frame.x = x; frame.y = y; frame.resize(width, height); frame.name = name; // Set fill color if provided if (fillColor) { const paintStyle = { type: "SOLID", color: { r: parseFloat(fillColor.r) || 0, g: parseFloat(fillColor.g) || 0, b: parseFloat(fillColor.b) || 0, }, opacity: parseFloat(fillColor.a) || 1, }; frame.fills = [paintStyle]; } // Set stroke color and weight if provided if (strokeColor) { const strokeStyle = { type: "SOLID", color: { r: parseFloat(strokeColor.r) || 0, g: parseFloat(strokeColor.g) || 0, b: parseFloat(strokeColor.b) || 0, }, opacity: parseFloat(strokeColor.a) || 1, }; frame.strokes = [strokeStyle]; } // Set stroke weight if provided if (strokeWeight !== undefined) { frame.strokeWeight = strokeWeight; } // If parentId is provided, append to that node, otherwise append to current page if (parentId) { const parentNode = await figma.getNodeByIdAsync(parentId); if (!parentNode) { throw new Error(`Parent node not found with ID: ${parentId}`); } if (!("appendChild" in parentNode)) { throw new Error(`Parent node does not support children: ${parentId}`); } parentNode.appendChild(frame); } else { figma.currentPage.appendChild(frame); } return { id: frame.id, name: frame.name, x: frame.x, y: frame.y, width: frame.width, height: frame.height, fills: frame.fills, strokes: frame.strokes, strokeWeight: frame.strokeWeight, parentId: frame.parent ? frame.parent.id : undefined, }; } async function createText(params) { const { x = 0, y = 0, text = "Text", fontSize = 14, fontWeight = 400, fontColor = { r: 0, g: 0, b: 0, a: 1 }, // Default to black name = "Text", parentId, } = params || {}; // Map common font weights to Figma font styles const getFontStyle = (weight) => { switch (weight) { case 100: return "Thin"; case 200: return "Extra Light"; case 300: return "Light"; case 400: return "Regular"; case 500: return "Medium"; case 600: return "Semi Bold"; case 700: return "Bold"; case 800: return "Extra Bold"; case 900: return "Black"; default: return "Regular"; } }; const textNode = figma.createText(); textNode.x = x; textNode.y = y; textNode.name = name; try { await figma.loadFontAsync({ family: "Inter", style: getFontStyle(fontWeight), }); textNode.fontName = { family: "Inter", style: getFontStyle(fontWeight) }; textNode.fontSize = parseInt(fontSize); } catch (error) { console.error("Error setting font size", error); } setCharacters(textNode, text); // Set text color const paintStyle = { type: "SOLID", color: { r: parseFloat(fontColor.r) || 0, g: parseFloat(fontColor.g) || 0, b: parseFloat(fontColor.b) || 0, }, opacity: parseFloat(fontColor.a) || 1, }; textNode.fills = [paintStyle]; // If parentId is provided, append to that node, otherwise append to current page if (parentId) { const parentNode = await figma.getNodeByIdAsync(parentId); if (!parentNode) { throw new Error(`Parent node not found with ID: ${parentId}`); } if (!("appendChild" in parentNode)) { throw new Error(`Parent node does not support children: ${parentId}`); } parentNode.appendChild(textNode); } else { figma.currentPage.appendChild(textNode); } return { id: textNode.id, name: textNode.name, x: textNode.x, y: textNode.y, width: textNode.width, height: textNode.height, characters: textNode.characters, fontSize: textNode.fontSize, fontWeight: fontWeight, fontColor: fontColor, fontName: textNode.fontName, fills: textNode.fills, parentId: textNode.parent ? textNode.parent.id : undefined, }; } async function setFillColor(params) { console.log("setFillColor", params); const { nodeId, color: { r, g, b, a }, } = params || {}; if (!nodeId) { throw new Error("Missing nodeId parameter"); } const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } if (!("fills" in node)) { throw new Error(`Node does not support fills: ${nodeId}`); } // Create RGBA color const rgbColor = { r: parseFloat(r) || 0, g: parseFloat(g) || 0, b: parseFloat(b) || 0, a: parseFloat(a) || 1, }; // Set fill const paintStyle = { type: "SOLID", color: { r: parseFloat(rgbColor.r), g: parseFloat(rgbColor.g), b: parseFloat(rgbColor.b), }, opacity: parseFloat(rgbColor.a), }; console.log("paintStyle", paintStyle); node.fills = [paintStyle]; return { id: node.id, name: node.name, fills: [paintStyle], }; } async function setStrokeColor(params) { const { nodeId, color: { r, g, b, a }, weight = 1, } = params || {}; if (!nodeId) { throw new Error("Missing nodeId parameter"); } const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } if (!("strokes" in node)) { throw new Error(`Node does not support strokes: ${nodeId}`); } // Create RGBA color const rgbColor = { r: r !== undefined ? r : 0, g: g !== undefined ? g : 0, b: b !== undefined ? b : 0, a: a !== undefined ? a : 1, }; // Set stroke const paintStyle = { type: "SOLID", color: { r: rgbColor.r, g: rgbColor.g, b: rgbColor.b, }, opacity: rgbColor.a, }; node.strokes = [paintStyle]; // Set stroke weight if available if ("strokeWeight" in node) { node.strokeWeight = weight; } return { id: node.id, name: node.name, strokes: node.strokes, strokeWeight: "strokeWeight" in node ? node.strokeWeight : undefined, }; } async function moveNode(params) { const { nodeId, x, y } = params || {}; if (!nodeId) { throw new Error("Missing nodeId parameter"); } if (x === undefined || y === undefined) { throw new Error("Missing x or y parameters"); } const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } if (!("x" in node) || !("y" in node)) { throw new Error(`Node does not support position: ${nodeId}`); } node.x = x; node.y = y; return { id: node.id, name: node.name, x: node.x, y: node.y, }; } async function resizeNode(params) { const { nodeId, width, height } = params || {}; if (!nodeId) { throw new Error("Missing nodeId parameter"); } if (width === undefined || height === undefined) { throw new Error("Missing width or height parameters"); } const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } if (!("resize" in node)) { throw new Error(`Node does not support resizing: ${nodeId}`); } node.resize(width, height); return { id: node.id, name: node.name, width: node.width, height: node.height, }; } async function deleteNode(params) { const { nodeId } = params || {}; if (!nodeId) { throw new Error("Missing nodeId parameter"); } const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } // Save node info before deleting const nodeInfo = { id: node.id, name: node.name, type: node.type, }; node.remove(); return nodeInfo; } async function getStyles() { const styles = { colors: await figma.getLocalPaintStylesAsync(), texts: await figma.getLocalTextStylesAsync(), effects: await figma.getLocalEffectStylesAsync(), grids: await figma.getLocalGridStylesAsync(), }; return { colors: styles.colors.map((style) => ({ id: style.id, name: style.name, key: style.key, paint: style.paints[0], })), texts: styles.texts.map((style) => ({ id: style.id, name: style.name, key: style.key, fontSize: style.fontSize, fontName: style.fontName, })), effects: styles.effects.map((style) => ({ id: style.id, name: style.name, key: style.key, })), grids: styles.grids.map((style) => ({ id: style.id, name: style.name, key: style.key, })), }; } async function getLocalComponents() { await figma.loadAllPagesAsync(); const components = figma.root.findAllWithCriteria({ types: ["COMPONENT"], }); return { count: components.length, components: components.map((component) => ({ id: component.id, name: component.name, key: "key" in component ? component.key : null, })), }; } // async function getTeamComponents() { // try { // const teamComponents = // await figma.teamLibrary.getAvailableComponentsAsync(); // return { // count: teamComponents.length, // components: teamComponents.map((component) => ({ // key: component.key, // name: component.name, // description: component.description, // libraryName: component.libraryName, // })), // }; // } catch (error) { // throw new Error(`Error getting team components: ${error.message}`); // } // } async function createComponentInstance(params) { const { componentKey, x = 0, y = 0 } = params || {}; if (!componentKey) { throw new Error("Missing componentKey parameter"); } try { const component = await figma.importComponentByKeyAsync(componentKey); const instance = component.createInstance(); instance.x = x; instance.y = y; figma.currentPage.appendChild(instance); return { id: instance.id, name: instance.name, x: instance.x, y: instance.y, width: instance.width, height: instance.height, componentId: instance.componentId, }; } catch (error) { throw new Error(`Error creating component instance: ${error.message}`); } } async function exportNodeAsImage(params) { const { nodeId, scale = 1 } = params || {}; const format = "PNG"; if (!nodeId) { throw new Error("Missing nodeId parameter"); } const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } if (!("exportAsync" in node)) { throw new Error(`Node does not support exporting: ${nodeId}`); } try { const settings = { format: format, constraint: { type: "SCALE", value: scale }, }; const bytes = await node.exportAsync(settings); let mimeType; switch (format) { case "PNG": mimeType = "image/png"; break; case "JPG": mimeType = "image/jpeg"; break; case "SVG": mimeType = "image/svg+xml"; break; case "PDF": mimeType = "application/pdf"; break; default: mimeType = "application/octet-stream"; } // Proper way to convert Uint8Array to base64 const base64 = customBase64Encode(bytes); // const imageData = `data:${mimeType};base64,${base64}`; return { nodeId, format, scale, mimeType, imageData: base64, }; } catch (error) { throw new Error(`Error exporting node as image: ${error.message}`); } } function customBase64Encode(bytes) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let base64 = ""; const byteLength = bytes.byteLength; const byteRemainder = byteLength % 3; const mainLength = byteLength - byteRemainder; let a, b, c, d; let chunk; // Main loop deals with bytes in chunks of 3 for (let i = 0; i < mainLength; i = i + 3) { // Combine the three bytes into a single integer chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; // Use bitmasks to extract 6-bit segments from the triplet a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 d = chunk & 63; // 63 = 2^6 - 1 // Convert the raw binary segments to the appropriate ASCII encoding base64 += chars[a] + chars[b] + chars[c] + chars[d]; } // Deal with the remaining bytes and padding if (byteRemainder === 1) { chunk = bytes[mainLength]; a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 // Set the 4 least significant bits to zero b = (chunk & 3) << 4; // 3 = 2^2 - 1 base64 += chars[a] + chars[b] + "=="; } else if (byteRemainder === 2) { chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 // Set the 2 least significant bits to zero c = (chunk & 15) << 2; // 15 = 2^4 - 1 base64 += chars[a] + chars[b] + chars[c] + "="; } return base64; } async function setCornerRadius(params) { const { nodeId, radius, corners } = params || {}; if (!nodeId) { throw new Error("Missing nodeId parameter"); } if (radius === undefined) { throw new Error("Missing radius parameter"); } const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } // Check if node supports corner radius if (!("cornerRadius" in node)) { throw new Error(`Node does not support corner radius: ${nodeId}`); } // If corners array is provided, set individual corner radii if (corners && Array.isArray(corners) && corners.length === 4) { if ("topLeftRadius" in node) { // Node supports individual corner radii if (corners[0]) node.topLeftRadius = radius; if (corners[1]) node.topRightRadius = radius; if (corners[2]) node.bottomRightRadius = radius; if (corners[3]) node.bottomLeftRadius = radius; } else { // Node only supports uniform corner radius node.cornerRadius = radius; } } else { // Set uniform corner radius node.cornerRadius = radius; } return { id: node.id, name: node.name, cornerRadius: "cornerRadius" in node ? node.cornerRadius : undefined, topLeftRadius: "topLeftRadius" in node ? node.topLeftRadius : undefined, topRightRadius: "topRightRadius" in node ? node.topRightRadius : undefined, bottomRightRadius: "bottomRightRadius" in node ? node.bottomRightRadius : undefined, bottomLeftRadius: "bottomLeftRadius" in node ? node.bottomLeftRadius : undefined, }; } async function setTextContent(params) { const { nodeId, text } = params || {}; if (!nodeId) { throw new Error("Missing nodeId parameter"); } if (text === undefined) { throw new Error("Missing text parameter"); } const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } if (node.type !== "TEXT") { throw new Error(`Node is not a text node: ${nodeId}`); } try { await figma.loadFontAsync(node.fontName); await setCharacters(node, text); return { id: node.id, name: node.name, characters: node.characters, fontName: node.fontName, }; } catch (error) { throw new Error(`Error setting text content: ${error.message}`); } } // Initialize settings on load (async function initializePlugin() { try { const savedSettings = await figma.clientStorage.getAsync("settings"); if (savedSettings) { if (savedSettings.serverPort) { state.serverPort = savedSettings.serverPort; } } // Send initial settings to UI figma.ui.postMessage({ type: "init-settings", settings: { serverPort: state.serverPort, }, }); } catch (error) { console.error("Error loading settings:", error); } })(); function uniqBy(arr, predicate) { const cb = typeof predicate === "function" ? predicate : (o) => o[predicate]; return [ ...arr .reduce((map, item) => { const key = item === null || item === undefined ? item : cb(item); map.has(key) || map.set(key, item); return map; }, new Map()) .values(), ]; } const setCharacters = async (node, characters, options) => { const fallbackFont = (options && options.fallbackFont) || { family: "Inter", style: "Regular", }; try { if (node.fontName === figma.mixed) { if (options && options.smartStrategy === "prevail") { const fontHashTree = {}; for (let i = 1; i < node.characters.length; i++) { const charFont = node.getRangeFontName(i - 1, i); const key = `${charFont.family}::${charFont.style}`; fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1; } const prevailedTreeItem = Object.entries(fontHashTree).sort( (a, b) => b[1] - a[1] )[0]; const [family, style] = prevailedTreeItem[0].split("::"); const prevailedFont = { family, style, }; await figma.loadFontAsync(prevailedFont); node.fontName = prevailedFont; } else if (options && options.smartStrategy === "strict") { return setCharactersWithStrictMatchFont(node, characters, fallbackFont); } else if (options && options.smartStrategy === "experimental") { return setCharactersWithSmartMatchFont(node, characters, fallbackFont); } else { const firstCharFont = node.getRangeFontName(0, 1); await figma.loadFontAsync(firstCharFont); node.fontName = firstCharFont; } } else { await figma.loadFontAsync({ family: node.fontName.family, style: node.fontName.style, }); } } catch (err) { console.warn( `Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`, err ); await figma.loadFontAsync(fallbackFont); node.fontName = fallbackFont; } try { node.characters = characters; return true; } catch (err) { console.warn(`Failed to set characters. Skipped.`, err); return false; } }; const setCharactersWithStrictMatchFont = async ( node, characters, fallbackFont ) => { const fontHashTree = {}; for (let i = 1; i < node.characters.length; i++) { const startIdx = i - 1; const startCharFont = node.getRangeFontName(startIdx, i); const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`; while (i < node.characters.length) { i++; const charFont = node.getRangeFontName(i - 1, i); if (startCharFontVal !== `${charFont.family}::${charFont.style}`) { break; } } fontHashTree[`${startIdx}_${i}`] = startCharFontVal; } await figma.loadFontAsync(fallbackFont); node.fontName = fallbackFont; node.characters = characters; console.log(fontHashTree); await Promise.all( Object.keys(fontHashTree).map(async (range) => { console.log(range, fontHashTree[range]); const [start, end] = range.split("_"); const [family, style] = fontHashTree[range].split("::"); const matchedFont = { family, style, }; await figma.loadFontAsync(matchedFont); return node.setRangeFontName(Number(start), Number(end), matchedFont); }) ); return true; }; const getDelimiterPos = (str, delimiter, startIdx = 0, endIdx = str.length) => { const indices = []; let temp = startIdx; for (let i = startIdx; i < endIdx; i++) { if ( str[i] === delimiter && i + startIdx !== endIdx && temp !== i + startIdx ) { indices.push([temp, i + startIdx]); temp = i + startIdx + 1; } } temp !== endIdx && indices.push([temp, endIdx]); return indices.filter(Boolean); }; const buildLinearOrder = (node) => { const fontTree = []; const newLinesPos = getDelimiterPos(node.characters, "\n"); newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => { const newLinesRangeFont = node.getRangeFontName( newLinesRangeStart, newLinesRangeEnd ); if (newLinesRangeFont === figma.mixed) { const spacesPos = getDelimiterPos( node.characters, " ", newLinesRangeStart, newLinesRangeEnd ); spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => { const spacesRangeFont = node.getRangeFontName( spacesRangeStart, spacesRangeEnd ); if (spacesRangeFont === figma.mixed) { const spacesRangeFont = node.getRangeFontName( spacesRangeStart, spacesRangeStart[0] ); fontTree.push({ start: spacesRangeStart, delimiter: " ", family: spacesRangeFont.family, style: spacesRangeFont.style, }); } else { fontTree.push({ start: spacesRangeStart, delimiter: " ", family: spacesRangeFont.family, style: spacesRangeFont.style, }); } }); } else { fontTree.push({ start: newLinesRangeStart, delimiter: "\n", family: newLinesRangeFont.family, style: newLinesRangeFont.style, }); } }); return fontTree .sort((a, b) => +a.start - +b.start) .map(({ family, style, delimiter }) => ({ family, style, delimiter })); }; const setCharactersWithSmartMatchFont = async ( node, characters, fallbackFont ) => { const rangeTree = buildLinearOrder(node); const fontsToLoad = uniqBy( rangeTree, ({ family, style }) => `${family}::${style}` ).map(({ family, style }) => ({ family, style, })); await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync)); node.fontName = fallbackFont; node.characters = characters; let prevPos = 0; rangeTree.forEach(({ family, style, delimiter }) => { if (prevPos < node.characters.length) { const delimeterPos = node.characters.indexOf(delimiter, prevPos); const endPos = delimeterPos > prevPos ? delimeterPos : node.characters.length; const matchedFont = { family, style, }; node.setRangeFontName(prevPos, endPos, matchedFont); prevPos = endPos + 1; } }); return true; }; // Add the cloneNode function implementation async function cloneNode(params) { const { nodeId, x, y } = params || {}; if (!nodeId) { throw new Error("Missing nodeId parameter"); } const node = await figma.getNodeByIdAsync(nodeId); if (!node) { throw new Error(`Node not found with ID: ${nodeId}`); } // Clone the node const clone = node.clone(); // If x and y are provided, move the clone to that position if (x !== undefined && y !== undefined) { if (!("x" in clone) || !("y" in clone)) { throw new Error(`Cloned node does not support position: ${nodeId}`); } clone.x = x; clone.y = y; } // Add the clone to the same parent as the original node if (node.parent) { node.parent.appendChild(clone); } else { figma.currentPage.appendChild(clone); } return { id: clone.id, name: clone.name, x: "x" in clone ? clone.x : undefined, y: "y" in clone ? clone.y : undefined, width: "width" in clone ? clone.width : undefined, height: "height" in clone ? clone.height : undefined, }; }