jsx-generator.ts•10.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 };
};