Skip to main content
Glama

MCP Figma to React Converter

component-generator.ts8.49 kB
import * as prettier from 'prettier'; import { FigmaNode } from './figma-client.js'; import { convertFigmaStylesToTailwind } from './figma-tailwind-converter.js'; import { enhanceWithAccessibility } from './accessibility.js'; interface PropDefinition { name: string; type: string; defaultValue?: string; description?: string; } interface ReactComponentParts { jsx: string; imports: string[]; props: PropDefinition[]; styles?: Record<string, any>; } // Helper function to convert Figma node name to a valid React component name function toComponentName(name: string): string { // Remove invalid characters and convert to PascalCase return name .replace(/[^\w\s-]/g, '') .split(/[-_\s]+/) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(''); } // Helper function to convert Figma node name to a valid prop name function toPropName(name: string): string { // Convert to camelCase const parts = name .replace(/[^\w\s-]/g, '') .split(/[-_\s]+/); return parts[0].toLowerCase() + parts.slice(1) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(''); } // Extract potential props from Figma node function extractProps(node: FigmaNode): PropDefinition[] { const props: PropDefinition[] = []; // Text content could be a prop if (node.type === 'TEXT' && node.characters) { const propName = toPropName(node.name) || 'text'; props.push({ name: propName, type: 'string', defaultValue: JSON.stringify(node.characters), description: `Text content for ${node.name}` }); } // If node has a "variant" property, it could be a prop if (node.name.toLowerCase().includes('variant')) { props.push({ name: 'variant', type: "'primary' | 'secondary' | 'outline' | 'text'", defaultValue: "'primary'", description: 'Visual variant of the component' }); } // If node looks like a button, add onClick prop if (node.name.toLowerCase().includes('button') || node.name.toLowerCase().includes('btn')) { props.push({ name: 'onClick', type: '() => void', description: 'Function called when button is clicked' }); } // If node has children that could be dynamic, add children prop if (node.children && node.children.length > 0) { // Check if it has a container-like name if ( node.name.toLowerCase().includes('container') || node.name.toLowerCase().includes('wrapper') || node.name.toLowerCase().includes('layout') || node.name.toLowerCase().includes('section') ) { props.push({ name: 'children', type: 'React.ReactNode', description: 'Child elements to render inside the component' }); } } // Add className prop for styling customization props.push({ name: 'className', type: 'string', description: 'Additional CSS classes to apply' }); return props; } // Convert a Figma node to JSX function figmaNodeToJSX(node: FigmaNode, level = 0): ReactComponentParts { const imports: string[] = []; const allProps: PropDefinition[] = []; // Default result let result: ReactComponentParts = { jsx: '', imports: [], props: [] }; // Handle different node types switch (node.type) { case 'TEXT': // Extract text content and convert to JSX const textContent = node.characters || ''; const tailwindClasses = convertFigmaStylesToTailwind(node); const textProps = extractProps(node); result = { jsx: `<p className="${tailwindClasses.join(' ')}">{${textProps[0]?.name || 'text'}}</p>`, imports: [], props: textProps }; break; case 'RECTANGLE': case 'ELLIPSE': case 'POLYGON': case 'STAR': case 'VECTOR': case 'LINE': // Convert to a div with appropriate styling const shapeClasses = convertFigmaStylesToTailwind(node); result = { jsx: `<div className="${shapeClasses.join(' ')} ${node.type.toLowerCase()} ${node.name.toLowerCase().replace(/\s+/g, '-')}"></div>`, imports: [], props: [] }; break; case 'COMPONENT': case 'INSTANCE': case 'FRAME': case 'GROUP': // These are container elements that might have children const containerClasses = convertFigmaStylesToTailwind(node); const containerProps = extractProps(node); // Process children if they exist let childrenJSX = ''; if (node.children && node.children.length > 0) { for (const child of node.children) { const childResult = figmaNodeToJSX(child, level + 1); childrenJSX += `\n${' '.repeat(level + 1)}${childResult.jsx}`; // Collect imports and props from children imports.push(...childResult.imports); allProps.push(...childResult.props); } childrenJSX += `\n${' '.repeat(level)}`; } // If this is a component that looks like a button if (node.name.toLowerCase().includes('button') || node.name.toLowerCase().includes('btn')) { result = { jsx: `<button className="${containerClasses.join(' ')} ${node.name.toLowerCase().replace(/\s+/g, '-')}" onClick={onClick} >${childrenJSX}</button>`, imports: imports, props: [...containerProps, ...allProps] }; } else { // Check if we should use children prop const hasChildrenProp = containerProps.some(p => p.name === 'children'); result = { jsx: `<div className="${containerClasses.join(' ')} ${node.name.toLowerCase().replace(/\s+/g, '-')}" >${hasChildrenProp ? '{children}' : childrenJSX}</div>`, imports: imports, props: [...containerProps, ...(hasChildrenProp ? [] : allProps)] }; } break; case 'IMAGE': // Convert to an img tag const imageClasses = convertFigmaStylesToTailwind(node); result = { jsx: `<img src="${node.name.toLowerCase().replace(/\s+/g, '-')}.png" className="${imageClasses.join(' ')}" alt="${node.name}" />`, imports: [], props: [{ name: 'src', type: 'string', description: 'Image source URL' }] }; break; default: // Default to a simple div result = { jsx: `<div className="${node.name.toLowerCase().replace(/\s+/g, '-')}"></div>`, imports: [], props: [] }; } return result; } export class ComponentGenerator { async generateReactComponent(componentName: string, figmaNode: FigmaNode): Promise<string> { // Extract styles, structure, and props from Figma node const componentParts = figmaNodeToJSX(figmaNode); // Enhance with accessibility features const enhancedJSX = enhanceWithAccessibility(componentParts.jsx, figmaNode); // Deduplicate props const uniqueProps = componentParts.props.filter((prop, index, self) => index === self.findIndex(p => p.name === prop.name) ); // Create component template const componentCode = ` import React from 'react'; ${componentParts.imports.join('\n')} interface ${componentName}Props { ${uniqueProps.map(prop => `/** ${prop.description || ''} */ ${prop.name}${prop.type.includes('?') || prop.defaultValue ? '?' : ''}: ${prop.type};` ).join('\n ')} } export const ${componentName} = ({ ${uniqueProps.map(p => p.defaultValue ? `${p.name} = ${p.defaultValue}` : p.name ).join(', ')} }: ${componentName}Props) => { return ( ${enhancedJSX} ); }; `; // Format the code try { return await prettier.format(componentCode, { parser: 'typescript', singleQuote: true, trailingComma: 'es5', tabWidth: 2 }); } catch (error) { console.error('Error formatting component code:', error); return componentCode; } } async generateComponentLibrary(components: Array<{ name: string, node: FigmaNode }>): Promise<Record<string, string>> { const generatedComponents: Record<string, string> = {}; for (const { name, node } of components) { const componentName = toComponentName(name); generatedComponents[componentName] = await this.generateReactComponent(componentName, node); } return generatedComponents; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/StudentOfJS/mcp-figma-to-react'

If you have feedback or need assistance with the MCP directory API, please join our Discord server