Skip to content

Latest commit

 

History

History
911 lines (681 loc) · 20.6 KB

File metadata and controls

911 lines (681 loc) · 20.6 KB

⚙️ AST Transformations Reference

Complete guide to all AST transforms in the processing pipeline

Table of Contents


Overview

The transform pipeline uses Babel AST (Abstract Syntax Tree) to parse, modify, and optimize React/JSX code generated by Figma's MCP server.

Why AST Transforms?

  • Precision - Modify code structure, not strings
  • Safety - Type-aware transformations
  • Performance - Single-pass traversal
  • Composability - Independent, reusable transforms

Transform Execution

All transforms run in a single pass through the AST:

  1. Parse code → AST
  2. Sort transforms by priority (10 → 100)
  3. Execute all transforms in one traversal
  4. Generate optimized code
  5. Extract CSS

Transform Pipeline

Priority System

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

Execution Flow

// 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)

Transform Reference

Priority 10: Font Detection

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:

  1. Detect pattern: font-['FontFamily:Style']
  2. Extract font family and style
  3. Map style to font-weight
  4. Convert to inline style
  5. 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

Priority 20: Auto Layout

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;
}

Priority 30: AST Cleaning

File: scripts/transformations/ast-cleaning.js

Purpose: Remove invalid Tailwind classes and fix common issues

Detected Issues:

  1. Invalid Class Names

    • Remove classes with special characters
    • Remove empty classes
    • Remove duplicate classes
  2. Malformed Arbitrary Values

    • Fix: w-[100px]]w-[100px]
    • Fix: bg-[#fffbg-[#fff]
  3. Conflicting Classes

    • Keep last value: w-full w-[500px]w-[500px]

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 }
}

Priority 40: SVG Icon Fixes

File: scripts/transformations/svg-icon-fixes.js

Purpose: Fix SVG structure and attributes

Fixes Applied:

  1. Remove Invalid Attributes

    • xmlns (not needed in JSX)
    • xml:space
    • Invalid style strings
  2. Convert Attributes to JSX

    • fill-opacityfillOpacity
    • stroke-widthstrokeWidth
    • clip-pathclipPath
  3. Fix viewBox

    • Ensure viewBox is present
    • Fix malformed values
  4. Namespace Fixes

    • Remove xmlns:xlink
    • Convert xlink:hrefxlinkHref

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>

Priority 45: SVG Consolidation

File: scripts/transformations/svg-consolidation.js

Purpose: Merge nested SVG elements

Problem: Figma sometimes generates nested <svg> elements which cause rendering issues.

Solution:

  1. Detect nested SVGs
  2. Extract inner content
  3. Merge into parent SVG
  4. 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>

Priority 50: Post Fixes

File: scripts/transformations/post-fixes.js

Purpose: Fix gradients, shapes, and advanced visual effects

Fixes:

  1. Gradient Fixes

    • Linear gradients
    • Radial gradients
    • Gradient stops
  2. Shape Fixes

    • Border radius with individual corners
    • Box shadows
    • Text shadows
  3. 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' }}>

Priority 60: Position Fixes

File: scripts/transformations/position-fixes.js

Purpose: Fix positioning and layout issues

Fixes:

  1. Absolute Positioning

    • Fix negative values
    • Fix percentage values
    • Ensure parent is relative
    • Parent Context Detection (Enhanced Jan 2025) - Check parent for overlay attributes
  2. Flex Layout

    • Fix flex-grow/shrink
    • Fix flex-basis
  3. 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' }}>

Parent Context Detection (Bug Fix - Jan 2025)

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 absolute positioning to work correctly
  • Previous logic: Only checked element itself, not parent context
  • Fixed logic: Checks parent for aria-hidden or pointer-events-none attributes

Detection Logic:

  1. Check if element has pointer-events-none or aria-hidden → skip fix
  2. Check if parent has aria-hidden or pointer-events-none → skip fix
  3. Otherwise, apply position fix (remove absolute if parent is grid/flex)

Stats Impact:

// Before fix: insetFixed: 1 (absolute removed)
// After fix: insetFixed: 0 (absolute preserved)

Priority 70: Stroke Alignment

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:

  1. Detect stroke alignment from Figma metadata
  2. Use box-shadow for inner strokes
  3. Use outline for outer strokes
  4. Use border for 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'
}}>

Priority 80: CSS Variables

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:

  1. Load variables from variables.json
  2. Detect var(--variable-name) in styles
  3. Replace with actual value
  4. 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 }
}

Priority 90: Tailwind Optimizer

File: scripts/transformations/tailwind-optimizer.js

Purpose: Convert arbitrary values to standard Tailwind classes

Optimizations:

  1. Common Values

    • w-[100%]w-full
    • h-[100vh]h-screen
    • flex-[1]flex-1
  2. Spacing

    • p-[16px]p-4 (4 × 4px = 16px)
    • m-[32px]m-8
  3. Colors

    • bg-[#ffffff]bg-white
    • text-[#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">

Priority 100: Production Cleaner

File: scripts/transformations/production-cleaner.js

Purpose: Remove debug attributes for production builds

Removes:

  • data-name - Figma layer names
  • data-node-id - Figma node IDs
  • data-debug - Debug flags
  • Empty attributes

When to Use:

  • Enable with --clean flag
  • 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;
}

Creating Custom Transforms

Step 1: Create Transform File

// 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')
    )
  )
}

Step 2: Register Transform

// scripts/pipeline.js
import * as myCustomTransform from './transformations/my-custom-transform.js'

const ALL_TRANSFORMS = [
  fontDetection,
  autoLayout,
  myCustomTransform,  // Add here
  // ... other transforms
]

Step 3: Configure

// scripts/config.js
export const defaultConfig = {
  'my-custom-transform': { enabled: true },
  // ... other configs
}

Step 4: Test

# 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

Best Practices

✅ DO

  • 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)

❌ DON'T

  • 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)

Debugging Transforms

Enable Logging

export function execute(ast, context) {
  if (context.verbose) {
    console.log('Running my-transform...')
  }

  // ... transform logic

  if (context.verbose) {
    console.log(`Processed ${itemsProcessed} items`)
  }
}

Inspect AST

// Print AST structure
traverse.default(ast, {
  JSXElement(path) {
    console.log('Node type:', path.node.type)
    console.log('Attributes:', path.node.openingElement.attributes)
  }
})

Test Individual Transform

// 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)

Next Steps