Skip to main content
Glama
jsx-generator.ts10.4 kB
import type { FlatElement, PseudoCodeOptions, ComponentContext, } from "../types/generator.js"; import { compact } from "../utils/figma-utils.js"; import { generateClassName } from "./css-generator.js"; import { cleanComponentName, inferSemanticComponent, inferTextComponent, } from "./component-inference.js"; /** * Generates JSX component structure from processed elements. * Creates a React component hierarchy with appropriate components and props. */ export const generateJSX = ({ elements, options, }: { elements: FlatElement[]; options: PseudoCodeOptions; }): string => { const jsxBlocks: string[] = []; if (options.includeComments) { jsxBlocks.push("/* Following is Pseudo JSX code of given design: */"); } const rootElements = elements.filter((el) => el.level === 0); rootElements.forEach((rootElement) => { const jsx = generateElementJSX({ element: rootElement, allElements: elements, options, context: { level: 0 }, }); jsxBlocks.push(jsx); }); return jsxBlocks.join("\n\n"); }; /** * Checks if an element is used as a prop for a component. * Determines if an element should be rendered as a component property rather than a child. */ export const isUsedAsPropForComponent = ({ element, allElements, }: { element: FlatElement; allElements: FlatElement[]; }): boolean => { if ( !element.parentComponentReferences || element.parentComponentReferences.length === 0 ) { return false; } return element.parentComponentReferences.some((ref) => { return allElements.some((el) => { return el.component?.variantProperties?.some( (prop) => prop.name === ref.value && ref.name !== "visible" ); }); }); }; /** * Generates JSX code for a single element and recursively processes its children. * This is the core function for transforming the flat element structure into nested JSX. */ export const generateElementJSX = ({ element, allElements, options, context, }: { element: FlatElement; allElements: FlatElement[]; options: PseudoCodeOptions; context: ComponentContext; }): string => { const isPropForElement = isUsedAsPropForComponent({ element, allElements }); if (isPropForElement) { // console.error( // `Skipping element ${element.name} (${element.id}) as it is used as a prop for a component` // ); return ""; // Skip elements that are used as props for components } const componentName = selectComponent({ element, context, options }); const className = generateClassName(element); const children = getChildElements({ element, allElements }); // Handle icon props - check if any children should be rendered as icon props instead const nonIconChildren: FlatElement[] = []; children.forEach((child) => { if (isIconComponent(child)) { return; } nonIconChildren.push(child); }); const { propsString } = generateComponentProps({ element, options, allElements, }); const allProps = propsString; const indent = " ".repeat(context.level); let openingTag = `<${componentName}`; if (className && !element.component?.isInstance) { openingTag += ` className="${className}"`; } if (allProps && allProps.trim()) { openingTag += ` ${allProps}`; } if (nonIconChildren.length === 0 && !element.text?.content) { return `${indent}${openingTag} />`; } openingTag += ">"; const content: string[] = []; if (element.text?.content) { if (options.semanticHTML && element.type === "Text") { content.push(`${indent} ${element.text.content}`); } else { content.push(`${indent} ${element.text.content}`); } } nonIconChildren.forEach((child) => { const childContext: ComponentContext = { parentName: element.name, parentType: componentName, level: context.level + 1, }; const childJSX = generateElementJSX({ element: child, allElements, options, context: childContext, }); content.push(childJSX); }); const closingTag = `${indent}</${componentName}>`; if (compact(content).length === 0) { if (["Container", "Box", "Wrapper", "Flex", "Stack"].includes(element.type)) return ""; return `${indent}${openingTag.slice(0, -1)} />`; } return [`${indent}${openingTag}`, ...content, closingTag].join("\n\n"); }; /** * Selects the appropriate component name based on element properties and context. * Uses heuristics, component type mapping, and semantic inference to choose the best component. */ export const selectComponent = ({ element, context, options, }: { element: FlatElement; context: ComponentContext; options?: PseudoCodeOptions; }): string => { if (element.component?.isInstance && element.component.componentName) { const componentName = element.component.componentName; // If component name looks like variant properties (contains = and ,), skip it if (componentName.includes("=") && componentName.includes(",")) { // Fall through to other methods } else { // Clean the component name from Figma format to React format const cleanName = cleanComponentName(componentName); return cleanName; } } // 3. Check semantic patterns with parent context const semanticComponent = inferSemanticComponent({ element, context, ...(options ? { options } : {}), }); if (semanticComponent) return semanticComponent; if (element.layout) { if (element.layout.direction === "row") return "Flex"; if (element.layout.direction === "column") return "Stack"; } if (element.type === "Text") { return inferTextComponent(element); } return "div"; }; /** * Gets child elements for a given element. * Retrieves and filters child elements based on the element's childIds. */ export const getChildElements = ({ element, allElements, }: { element: FlatElement; allElements: FlatElement[]; }): FlatElement[] => { return allElements.filter((el) => element.childIds.includes(el.id)); }; /** * Checks if an element should be treated as an icon component. * Determines if an element represents an icon based on type and properties. */ function isIconComponent(element: FlatElement): boolean { const iconPatterns = ["filled", "outlined", "colored", "brand", "3D", "duotone", "illustration"]; // Check by component name first (most reliable) if (element.component?.isInstance && element.component.componentName) { const name = element.component.componentName.toLowerCase(); // Check for common icon naming patterns if (iconPatterns.some((pattern) => name.includes(pattern))) { return true; } } // Check by element name patterns const elementName = element.name.toLowerCase(); if (iconPatterns.some((pattern) => elementName.includes(pattern))) { return true; } // Check by element type (vectors are often icons) if (element.type === "IMAGE-SVG" || element.type === "VECTOR") { // Additional check: if it's small and square-ish, likely an icon // This is a heuristic, but can help catch vector icons return true; } return false; } /** * Validates if a property name is suitable for React components. * Checks if the property name is non-empty and does not contain invalid characters. */ const isValidReactPropName = (propName: string): boolean => { if (!propName || propName.trim() === "") return false; return true; }; /** * Checks if a property value is valid for React components. * Validates if the property value is not empty and does not contain invalid characters. */ const isDefaultValue = (value: string): boolean => { const defaultValues = ["default", "none", "false", "", "0"]; return defaultValues.includes(value.toLowerCase()); }; const isValidPropValue = ( value: string, allElements: FlatElement[] ): boolean => { if (value.includes(":")) { return allElements.some((el) => el.component?.componentId === value); } return true; }; const formatPropValue = (value: string, allElements: FlatElement[]): string => { if (value === "true") return "{true}"; if (value === "false") return "{false}"; if (/^\d+$/.test(value)) return `{${value}}`; if (value.includes(":")) { const element = allElements.find( (el) => el.component?.componentId === value ); if (element) { // Generate JSX for this element const componentName = selectComponent({ element, context: { level: 0 }, }); const className = generateClassName(element); const { propsString } = generateComponentProps({ element, options: { minifyProps: true, semanticHTML: false, includeComments: false, groupStyles: true, }, allElements, }); return `{<${componentName} className="${className}" ${propsString} />}`; } } // String values return `"${value}"`; }; /** * Generates component props from element properties * Creates appropriate React props based on component variants and attributes. */ export const generateComponentProps = ({ element, options, allElements, }: { element: FlatElement; options: PseudoCodeOptions; allElements: FlatElement[]; }): { propsString: string; props: string[] } => { const props: string[] = []; if (element.component?.variantProperties) { element.component.variantProperties.forEach((prop, index) => { // Filter out invalid prop names that contain # or other special characters // These are likely Figma internal references, not actual component variant properties if (!isValidReactPropName(prop.name)) { return; } if (options.minifyProps && isDefaultValue(prop.value)) { return; // Skip default values } // If prop name has "#random-id", then remove it, and convert to camelCase const propName = prop.name .toLowerCase() .replace(/#.*$/, "") .trim() .split(" ") .map((word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1) ) .join(""); if (isValidPropValue(prop.value, allElements)) { const propValue = formatPropValue(prop.value, allElements); props.push(`${index === 0 ? "\n" : ""}\t${propName}=${propValue}`); } }); } return { propsString: props.join(`\n`), props }; };

Latest Blog Posts

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/toddle-edu/figma-mcp-server'

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