Complete guide to all AST transforms in the processing pipeline
- Overview
- Transform Pipeline
- Transform Reference
- Priority 10: Font Detection
- Priority 20: Auto Layout
- Priority 30: AST Cleaning
- Priority 40: SVG Icon Fixes
- Priority 45: SVG Consolidation
- Priority 50: Post Fixes
- Priority 60: Position Fixes
- Priority 70: Stroke Alignment
- Priority 80: CSS Variables
- Priority 90: Tailwind Optimizer
- Priority 100: Production Cleaner
- Creating Custom Transforms
The transform pipeline uses Babel AST (Abstract Syntax Tree) to parse, modify, and optimize React/JSX code generated by Figma's MCP server.
- Precision - Modify code structure, not strings
- Safety - Type-aware transformations
- Performance - Single-pass traversal
- Composability - Independent, reusable transforms
All transforms run in a single pass through the AST:
- Parse code → AST
- Sort transforms by priority (10 → 100)
- Execute all transforms in one traversal
- Generate optimized code
- Extract CSS
| Priority | Transform | Phase | Purpose |
|---|---|---|---|
| 10 | Font Detection | Early | Convert font classes to inline styles |
| 20 | Auto Layout | Early | Fix Figma auto-layout classes |
| 30 | AST Cleaning | Core | Remove invalid Tailwind |
| 40 | SVG Icon Fixes | SVG | Fix SVG structure/attributes |
| 45 | SVG Consolidation | SVG | Merge nested SVGs |
| 50 | Post Fixes | Core | Gradients, shapes, blur |
| 60 | Position Fixes | Layout | Fix positioning issues |
| 70 | Stroke Alignment | Graphics | Stroke alignment fixes |
| 80 | CSS Variables | Late | Convert CSS vars to values |
| 90 | Tailwind Optimizer | Optimization | Arbitrary → standard |
| 100 | Production Cleaner | Production | Remove debug attributes |
// Parse code to AST
const ast = parser.parse(sourceCode)
// Load and sort transforms
const transforms = [
fontDetection, // Priority 10
autoLayout, // Priority 20
astCleaning, // Priority 30
// ... etc
].sort((a, b) => a.meta.priority - b.meta.priority)
// Execute all transforms
for (const transform of transforms) {
transform.execute(ast, context)
}
// Generate code
const { code } = generate(ast)File: scripts/transformations/font-detection.js
Purpose: Convert Figma font classes to inline styles
Problem:
Figma generates font classes like font-['Inter:Bold'] which are not valid Tailwind.
Solution:
- Detect pattern:
font-['FontFamily:Style'] - Extract font family and style
- Map style to font-weight
- Convert to inline style
- Remove from className
Example:
// Before
<div className="font-['Inter:Bold'] text-lg">Text</div>
// After
<div className="text-lg" style={{ fontFamily: 'Inter', fontWeight: 700 }}>Text</div>Implementation:
export function execute(ast, context) {
let fontsDetected = 0
traverse.default(ast, {
JSXAttribute(path) {
if (path.node.name.name === 'className') {
const value = getClassNameValue(path)
// Regex: font-['FontFamily:Style']
const fontMatch = /font-\['([^:]+):([^']+)'\]/.exec(value)
if (fontMatch) {
const [fullMatch, family, style] = fontMatch
// Add inline style
addStyleAttribute(path.parentPath, {
fontFamily: family,
fontWeight: mapStyleToWeight(style) // Bold→700, Regular→400
})
// Remove from className
removeFromClassName(path, fullMatch)
fontsDetected++
}
}
}
})
return { fontsDetected }
}Style Mapping:
| Figma Style | CSS Weight |
|---|---|
| Thin | 100 |
| ExtraLight | 200 |
| Light | 300 |
| Regular | 400 |
| Medium | 500 |
| SemiBold | 600 |
| Bold | 700 |
| ExtraBold | 800 |
| Black | 900 |
File: scripts/transformations/auto-layout.js
Purpose: Fix Figma auto-layout CSS classes
Problem: Figma generates non-standard classes for auto-layout properties.
Solution: Convert Figma-specific classes to standard CSS/Tailwind equivalents.
Transformations:
const autoLayoutFixes = {
// Alignment
'items-start': 'items-start', // Already valid
'items-center': 'items-center',
'justify-start': 'justify-start',
'justify-center': 'justify-center',
// Figma-specific → Standard
'content-start': 'content-start', // Keep as utility class
'min-content': 'w-min',
'max-content': 'w-max',
// Gap classes (already valid in Tailwind)
'gap-4': 'gap-4',
'gap-8': 'gap-8',
}Example:
// Before
<div className="flex content-start gap-4">
// After (unchanged - already valid)
<div className="flex content-start gap-4">CSS Output:
/* Add utility class if not in Tailwind */
.content-start {
align-content: flex-start;
}File: scripts/transformations/ast-cleaning.js
Purpose: Remove invalid Tailwind classes and fix common issues
Detected Issues:
-
Invalid Class Names
- Remove classes with special characters
- Remove empty classes
- Remove duplicate classes
-
Malformed Arbitrary Values
- Fix:
w-[100px]]→w-[100px] - Fix:
bg-[#fff→bg-[#fff]
- Fix:
-
Conflicting Classes
- Keep last value:
w-full w-[500px]→w-[500px]
- Keep last value:
Example:
// Before
<div className="flex w-full w-[500px] invalid@class ">
// After
<div className="flex w-[500px]">Implementation:
export function execute(ast, context) {
let classesRemoved = 0
traverse.default(ast, {
JSXAttribute(path) {
if (path.node.name.name === 'className') {
const classes = getClassNameValue(path).split(' ')
const cleaned = classes
.map(cls => cls.trim())
.filter(cls => cls.length > 0)
.filter(cls => isValidClassName(cls))
.filter((cls, idx, arr) => {
// Remove duplicates (keep last)
return arr.lastIndexOf(cls) === idx
})
classesRemoved += classes.length - cleaned.length
setClassNameValue(path, cleaned.join(' '))
}
}
})
return { classesRemoved }
}File: scripts/transformations/svg-icon-fixes.js
Purpose: Fix SVG structure and attributes
Fixes Applied:
-
Remove Invalid Attributes
xmlns(not needed in JSX)xml:space- Invalid
stylestrings
-
Convert Attributes to JSX
fill-opacity→fillOpacitystroke-width→strokeWidthclip-path→clipPath
-
Fix viewBox
- Ensure viewBox is present
- Fix malformed values
-
Namespace Fixes
- Remove
xmlns:xlink - Convert
xlink:href→xlinkHref
- Remove
Example:
// Before
<svg xmlns="http://www.w3.org/2000/svg" fill-opacity="0.5">
<path d="M 0 0 L 100 100" />
</svg>
// After
<svg fillOpacity={0.5}>
<path d="M 0 0 L 100 100" />
</svg>File: scripts/transformations/svg-consolidation.js
Purpose: Merge nested SVG elements
Problem:
Figma sometimes generates nested <svg> elements which cause rendering issues.
Solution:
- Detect nested SVGs
- Extract inner content
- Merge into parent SVG
- Preserve viewBox and dimensions
Example:
// Before
<svg width="100" height="100">
<svg width="50" height="50">
<circle cx="25" cy="25" r="20" />
</svg>
</svg>
// After
<svg width="100" height="100">
<circle cx="25" cy="25" r="20" />
</svg>File: scripts/transformations/post-fixes.js
Purpose: Fix gradients, shapes, and advanced visual effects
Fixes:
-
Gradient Fixes
- Linear gradients
- Radial gradients
- Gradient stops
-
Shape Fixes
- Border radius with individual corners
- Box shadows
- Text shadows
-
Blur Effects
- Backdrop blur
- Filter blur
Example - Gradient:
// Before
<div style={{ background: 'linear-gradient(90deg, var(--color-1) 0%, var(--color-2) 100%)' }}>
// After (if CSS vars transform runs)
<div style={{ background: 'linear-gradient(90deg, #FF0000 0%, #00FF00 100%)' }}>Example - Border Radius:
// Before
<div className="rounded-tl-lg rounded-tr-lg">
// After (consolidated to style)
<div style={{ borderRadius: '8px 8px 0 0' }}>File: scripts/transformations/position-fixes.js
Purpose: Fix positioning and layout issues
Fixes:
-
Absolute Positioning
- Fix negative values
- Fix percentage values
- Ensure parent is relative
- Parent Context Detection (Enhanced Jan 2025) - Check parent for overlay attributes
-
Flex Layout
- Fix flex-grow/shrink
- Fix flex-basis
-
Grid Layout
- Fix grid-template
- Fix grid-area
Example:
// Before
<div className="absolute top-[-10px]">
// After (convert to style for negative values)
<div className="absolute" style={{ top: '-10px' }}>Problem: Overlay elements inside aria-hidden parent containers were incorrectly having absolute removed.
Pattern 1 Enhancement (lines 296-330):
The transform now checks both the element itself AND its parent for overlay attributes before removing absolute positioning.
Old Logic (BUGGY):
// ❌ Only checked element itself
const isOverlay = className.includes('pointer-events-none') ||
attributes.some(attr => attr.name?.name === 'aria-hidden')
if (!isOverlay) {
// Remove absolute if parent is grid/flex
}New Logic (FIXED):
// ✅ Also checks parent for overlay attributes
const isOverlay = className.includes('pointer-events-none') ||
attributes.some(attr => attr.name?.name === 'aria-hidden')
// Check if parent has aria-hidden or pointer-events-none (overlay container)
const parent = path.parent
let parentIsOverlay = false
if (parent && t.isJSXElement(parent)) {
const parentAttributes = parent.openingElement.attributes
const parentClassAttr = parentAttributes.find(attr => attr.name && attr.name.name === 'className')
parentIsOverlay = parentAttributes.some(attr => attr.name?.name === 'aria-hidden') ||
(parentClassAttr && t.isStringLiteral(parentClassAttr.value) &&
parentClassAttr.value.value.includes('pointer-events-none'))
}
if (!isOverlay && !parentIsOverlay) {
// Only remove absolute if neither element nor parent is an overlay
if (parent && t.isJSXElement(parent)) {
const parentClassAttr = parent.openingElement.attributes.find(
attr => attr.name && attr.name.name === 'className'
)
if (parentClassAttr && t.isStringLiteral(parentClassAttr.value)) {
const parentClasses = parentClassAttr.value.value
// If parent is grid or flex, child doesn't need absolute
if (parentClasses.includes('grid') || parentClasses.includes('flex')) {
className = className.replace('absolute', '').trim()
modified = true
stats.insetFixed++
}
}
}
}Example Scenario:
// Figma Design: Header with overlay background
<div aria-hidden="true" className="absolute inset-0 pointer-events-none">
<img className="absolute max-w-none object-cover size-full" src={bg} />
<div className="absolute bg-opacity-30 inset-0" />
↑ This overlay child needs absolute positioning
</div>Before Fix: ❌
// Bug: absolute removed from child because parent is an overlay
<div aria-hidden="true" className="absolute inset-0 pointer-events-none">
<img className="max-w-none object-cover size-full" src={bg} />
<div className="bg-opacity-30 inset-0" />
↑ absolute removed - overlay broken!
</div>After Fix: ✅
// Fixed: absolute preserved because parent has aria-hidden
<div aria-hidden="true" className="absolute inset-0 pointer-events-none">
<img className="absolute max-w-none object-cover size-full" src={bg} />
<div className="absolute bg-opacity-30 inset-0" />
↑ absolute preserved - overlay works correctly!
</div>Why This Matters:
- Figma pattern: Figma often creates overlay containers with
aria-hidden="true"parent - Visual fidelity: Child overlay elements need
absolutepositioning to work correctly - Previous logic: Only checked element itself, not parent context
- Fixed logic: Checks parent for
aria-hiddenorpointer-events-noneattributes
Detection Logic:
- Check if element has
pointer-events-noneoraria-hidden→ skip fix - Check if parent has
aria-hiddenorpointer-events-none→ skip fix - Otherwise, apply position fix (remove
absoluteif parent is grid/flex)
Stats Impact:
// Before fix: insetFixed: 1 (absolute removed)
// After fix: insetFixed: 0 (absolute preserved)File: scripts/transformations/stroke-alignment.js
Purpose: Fix stroke alignment for borders
Problem: Figma supports inner/outer/center stroke alignment. CSS only supports center.
Solution:
- Detect stroke alignment from Figma metadata
- Use
box-shadowfor inner strokes - Use
outlinefor outer strokes - Use
borderfor center strokes (default)
Example:
// Figma: Inner stroke (2px, red)
// CSS equivalent:
<div style={{
boxShadow: 'inset 0 0 0 2px #FF0000'
}}>
// Figma: Outer stroke (2px, red)
// CSS equivalent:
<div style={{
outline: '2px solid #FF0000',
outlineOffset: '0'
}}>File: scripts/transformations/css-vars.js
Purpose: Convert CSS variables to actual values
Why?
- Ensures components work without external CSS
- Improves portability
- Reduces dependencies
Process:
- Load variables from
variables.json - Detect
var(--variable-name)in styles - Replace with actual value
- Fallback to computed value if not found
Example:
// Before
<div style={{ color: 'var(--colors-primary, #000)' }}>
// After (if --colors-primary = #7C3AED)
<div style={{ color: '#7C3AED' }}>Implementation:
export function execute(ast, context) {
const variables = loadVariables(context.testDir)
let varsReplaced = 0
traverse.default(ast, {
JSXAttribute(path) {
if (path.node.name.name === 'style') {
const styleObj = path.node.value.expression
if (t.isObjectExpression(styleObj)) {
styleObj.properties.forEach(prop => {
const value = getPropertyValue(prop)
// Detect: var(--name, fallback)
const varMatch = /var\(--([^,)]+)(?:,\s*([^)]+))?\)/.exec(value)
if (varMatch) {
const [, varName, fallback] = varMatch
const actualValue = variables[varName] || fallback
if (actualValue) {
setPropertyValue(prop, actualValue)
varsReplaced++
}
}
})
}
}
}
})
return { varsReplaced }
}File: scripts/transformations/tailwind-optimizer.js
Purpose: Convert arbitrary values to standard Tailwind classes
Optimizations:
-
Common Values
w-[100%]→w-fullh-[100vh]→h-screenflex-[1]→flex-1
-
Spacing
p-[16px]→p-4(4 × 4px = 16px)m-[32px]→m-8
-
Colors
bg-[#ffffff]→bg-whitetext-[#000000]→text-black
Mapping Tables:
const spacingMap = {
'0px': '0',
'4px': '1',
'8px': '2',
'12px': '3',
'16px': '4',
'20px': '5',
'24px': '6',
'32px': '8',
'64px': '16',
}
const colorMap = {
'#ffffff': 'white',
'#000000': 'black',
'#f3f4f6': 'gray-100',
'#e5e7eb': 'gray-200',
// ... more colors
}Example:
// Before
<div className="w-[100%] h-[100vh] p-[16px] bg-[#ffffff]">
// After
<div className="w-full h-screen p-4 bg-white">File: scripts/transformations/production-cleaner.js
Purpose: Remove debug attributes for production builds
Removes:
data-name- Figma layer namesdata-node-id- Figma node IDsdata-debug- Debug flags- Empty attributes
When to Use:
- Enable with
--cleanflag - Generates
Component-clean.tsx - Production-ready output
Example:
// Before
<div data-name="Header" data-node-id="9:2654" className="flex">
// After
<div className="flex">CSS Conversion:
Also converts Tailwind classes to pure CSS:
// Before (Component-fixed.tsx)
<div className="flex items-center justify-between bg-white">
// After (Component-clean.tsx)
<div className="header-container">
// Component-clean.css
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #ffffff;
}// scripts/transformations/my-custom-transform.js
import traverse from '@babel/traverse'
import * as t from '@babel/types'
export const meta = {
name: 'my-custom-transform',
priority: 55 // Choose appropriate priority
}
export function execute(ast, context) {
const startTime = Date.now()
let itemsProcessed = 0
traverse.default(ast, {
JSXElement(path) {
// Your transformation logic
if (shouldTransform(path.node)) {
transformNode(path)
itemsProcessed++
}
}
})
return {
itemsProcessed,
executionTime: Date.now() - startTime
}
}
function shouldTransform(node) {
// Condition logic
return true
}
function transformNode(path) {
// Modify AST
// Example: Add attribute
path.node.openingElement.attributes.push(
t.jsxAttribute(
t.jsxIdentifier('data-transformed'),
t.stringLiteral('true')
)
)
}// scripts/pipeline.js
import * as myCustomTransform from './transformations/my-custom-transform.js'
const ALL_TRANSFORMS = [
fontDetection,
autoLayout,
myCustomTransform, // Add here
// ... other transforms
]// scripts/config.js
export const defaultConfig = {
'my-custom-transform': { enabled: true },
// ... other configs
}# Test with real design
./cli/figma-analyze "https://www.figma.com/design/FILE_ID?node-id=X-Y"
# Check transform stats in analysis.md
cat src/generated/export_figma/node-*/analysis.md- Test extensively with various Figma designs
- Return detailed stats for monitoring
- Handle edge cases gracefully
- Use TypeScript types where possible
- Document complex logic with comments
- Keep transforms focused (single responsibility)
- Modify AST outside visitors (use traverse)
- Assume node structure (check with guards)
- Perform I/O operations (transforms should be pure)
- Create circular dependencies between transforms
- Mutate context object (read-only)
export function execute(ast, context) {
if (context.verbose) {
console.log('Running my-transform...')
}
// ... transform logic
if (context.verbose) {
console.log(`Processed ${itemsProcessed} items`)
}
}// Print AST structure
traverse.default(ast, {
JSXElement(path) {
console.log('Node type:', path.node.type)
console.log('Attributes:', path.node.openingElement.attributes)
}
})// test-transform.js
import { execute } from './transformations/my-transform.js'
import parser from '@babel/parser'
const code = '<div className="flex">Test</div>'
const ast = parser.parse(code, { plugins: ['jsx'] })
const stats = execute(ast, {})
console.log(stats)- See DEVELOPMENT.md for development workflow
- See ARCHITECTURE.md for system architecture
- See API.md for API documentation