295 lines
9.2 KiB
Python
Executable File
295 lines
9.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Map Figma components to codebase components using Code Connect data and fuzzy matching.
|
|
"""
|
|
|
|
import json
|
|
import argparse
|
|
import os
|
|
from typing import Dict, List, Any
|
|
from difflib import SequenceMatcher
|
|
|
|
|
|
def calculate_similarity(str1: str, str2: str) -> float:
|
|
"""Calculate similarity ratio between two strings."""
|
|
return SequenceMatcher(None, str1.lower(), str2.lower()).ratio()
|
|
|
|
|
|
def find_component_files(project_root: str, extensions: List[str] = None) -> List[Dict[str, str]]:
|
|
"""
|
|
Find all component files in project.
|
|
|
|
Args:
|
|
project_root: Project root directory
|
|
extensions: File extensions to search (default: ['tsx', 'jsx', 'vue'])
|
|
|
|
Returns:
|
|
List of component file info (path, name)
|
|
"""
|
|
if extensions is None:
|
|
extensions = ['tsx', 'jsx', 'vue', 'svelte']
|
|
|
|
components = []
|
|
|
|
for root, dirs, files in os.walk(project_root):
|
|
# Skip node_modules, dist, build directories
|
|
dirs[:] = [d for d in dirs if d not in ['node_modules', 'dist', 'build', '.git', '.next']]
|
|
|
|
for file in files:
|
|
if any(file.endswith(f'.{ext}') for ext in extensions):
|
|
full_path = os.path.join(root, file)
|
|
rel_path = os.path.relpath(full_path, project_root)
|
|
|
|
# Extract component name (filename without extension)
|
|
comp_name = os.path.splitext(file)[0]
|
|
|
|
# Skip test files, stories, etc.
|
|
if any(suffix in comp_name.lower() for suffix in ['.test', '.spec', '.stories', '.story']):
|
|
continue
|
|
|
|
components.append({
|
|
'name': comp_name,
|
|
'path': rel_path,
|
|
'full_path': full_path
|
|
})
|
|
|
|
return components
|
|
|
|
|
|
def fuzzy_match_component(figma_name: str, codebase_components: List[Dict[str, str]],
|
|
threshold: float = 0.6) -> List[Dict[str, Any]]:
|
|
"""
|
|
Fuzzy match Figma component name to codebase components.
|
|
|
|
Args:
|
|
figma_name: Figma component name
|
|
codebase_components: List of codebase component info
|
|
threshold: Minimum similarity threshold
|
|
|
|
Returns:
|
|
List of matches with confidence scores
|
|
"""
|
|
matches = []
|
|
|
|
# Clean Figma name (remove variant info)
|
|
# "Button/Primary/Large" → "Button"
|
|
base_name = figma_name.split('/')[0].strip()
|
|
|
|
for comp in codebase_components:
|
|
comp_name = comp['name']
|
|
similarity = calculate_similarity(base_name, comp_name)
|
|
|
|
if similarity >= threshold:
|
|
matches.append({
|
|
'figma_name': figma_name,
|
|
'code_component': comp_name,
|
|
'code_path': comp['path'],
|
|
'confidence': round(similarity, 3),
|
|
'match_type': 'fuzzy'
|
|
})
|
|
|
|
# Sort by confidence
|
|
matches.sort(key=lambda x: x['confidence'], reverse=True)
|
|
|
|
return matches
|
|
|
|
|
|
def extract_variant_mapping(figma_name: str) -> Dict[str, str]:
|
|
"""
|
|
Extract variant information from Figma component name.
|
|
|
|
Examples:
|
|
"Button/Primary/Large" → {"variant": "primary", "size": "lg"}
|
|
"Card/Elevated" → {"variant": "elevated"}
|
|
|
|
Args:
|
|
figma_name: Figma component name with variants
|
|
|
|
Returns:
|
|
Dictionary of variant properties
|
|
"""
|
|
parts = [p.strip() for p in figma_name.split('/')]
|
|
|
|
if len(parts) == 1:
|
|
return {}
|
|
|
|
# Base component is first part
|
|
variants = parts[1:]
|
|
|
|
# Map common variant patterns
|
|
mapping = {}
|
|
|
|
for variant in variants:
|
|
variant_lower = variant.lower()
|
|
|
|
# Size variants
|
|
if variant_lower in ['small', 'sm', 'xs', 'tiny']:
|
|
mapping['size'] = 'sm'
|
|
elif variant_lower in ['medium', 'md', 'base']:
|
|
mapping['size'] = 'md'
|
|
elif variant_lower in ['large', 'lg']:
|
|
mapping['size'] = 'lg'
|
|
elif variant_lower in ['xl', 'xlarge', 'extra-large']:
|
|
mapping['size'] = 'xl'
|
|
|
|
# Style variants
|
|
elif variant_lower in ['primary', 'main']:
|
|
mapping['variant'] = 'primary'
|
|
elif variant_lower in ['secondary', 'outline', 'outlined']:
|
|
mapping['variant'] = 'secondary'
|
|
elif variant_lower in ['tertiary', 'ghost', 'link', 'text']:
|
|
mapping['variant'] = 'ghost'
|
|
|
|
# State variants
|
|
elif variant_lower in ['disabled', 'inactive']:
|
|
mapping['state'] = 'disabled'
|
|
elif variant_lower in ['loading', 'busy']:
|
|
mapping['state'] = 'loading'
|
|
|
|
# Type variants
|
|
elif variant_lower in ['solid', 'filled']:
|
|
mapping['type'] = 'solid'
|
|
elif variant_lower in ['elevated', 'raised']:
|
|
mapping['type'] = 'elevated'
|
|
elif variant_lower in ['flat', 'plain']:
|
|
mapping['type'] = 'flat'
|
|
|
|
# If no pattern matches, use as generic variant
|
|
else:
|
|
if 'variant' not in mapping:
|
|
mapping['variant'] = variant_lower
|
|
|
|
return mapping
|
|
|
|
|
|
def map_components(figma_components: List[Dict[str, Any]],
|
|
code_connect_map: Dict[str, Any],
|
|
project_root: str) -> Dict[str, Any]:
|
|
"""
|
|
Main mapping function: map Figma components to codebase components.
|
|
|
|
Args:
|
|
figma_components: List of Figma components from design_analyzer
|
|
code_connect_map: Figma Code Connect mappings
|
|
project_root: Project root directory for component search
|
|
|
|
Returns:
|
|
Component mappings with confidence scores
|
|
"""
|
|
# Find all component files in codebase
|
|
codebase_components = find_component_files(project_root)
|
|
|
|
mappings = {
|
|
'mapped': [],
|
|
'unmapped': [],
|
|
'low_confidence': [],
|
|
'summary': {}
|
|
}
|
|
|
|
for figma_comp in figma_components:
|
|
comp_id = figma_comp.get('id')
|
|
comp_name = figma_comp.get('name')
|
|
|
|
# Check Code Connect first (highest confidence)
|
|
if comp_id and comp_id in code_connect_map:
|
|
code_connect_data = code_connect_map[comp_id]
|
|
mappings['mapped'].append({
|
|
'figma_id': comp_id,
|
|
'figma_name': comp_name,
|
|
'code_component': code_connect_data.get('codeConnectName'),
|
|
'code_path': code_connect_data.get('codeConnectSrc'),
|
|
'confidence': 1.0,
|
|
'match_type': 'code_connect',
|
|
'props_mapping': extract_variant_mapping(comp_name)
|
|
})
|
|
else:
|
|
# Fallback to fuzzy matching
|
|
matches = fuzzy_match_component(comp_name, codebase_components, threshold=0.6)
|
|
|
|
if matches and matches[0]['confidence'] >= 0.8:
|
|
# High confidence match
|
|
best_match = matches[0]
|
|
best_match['figma_id'] = comp_id
|
|
best_match['props_mapping'] = extract_variant_mapping(comp_name)
|
|
mappings['mapped'].append(best_match)
|
|
|
|
elif matches:
|
|
# Low confidence match (manual review needed)
|
|
for match in matches[:3]: # Top 3 matches
|
|
match['figma_id'] = comp_id
|
|
match['props_mapping'] = extract_variant_mapping(comp_name)
|
|
mappings['low_confidence'].append(match)
|
|
|
|
else:
|
|
# No match found
|
|
mappings['unmapped'].append({
|
|
'figma_id': comp_id,
|
|
'figma_name': comp_name,
|
|
'recommendation': 'Create new component',
|
|
'props_mapping': extract_variant_mapping(comp_name)
|
|
})
|
|
|
|
# Generate summary
|
|
total = len(figma_components)
|
|
mappings['summary'] = {
|
|
'total_figma_components': total,
|
|
'mapped_count': len(mappings['mapped']),
|
|
'low_confidence_count': len(mappings['low_confidence']),
|
|
'unmapped_count': len(mappings['unmapped']),
|
|
'mapping_coverage': f"{(len(mappings['mapped']) / max(total, 1)) * 100:.1f}%"
|
|
}
|
|
|
|
return mappings
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Map Figma components to codebase components'
|
|
)
|
|
parser.add_argument(
|
|
'--figma-components',
|
|
required=True,
|
|
help='Path to JSON file with Figma components (from design_analyzer)'
|
|
)
|
|
parser.add_argument(
|
|
'--code-connect-map',
|
|
help='Path to Code Connect map JSON (optional)'
|
|
)
|
|
parser.add_argument(
|
|
'--project-root',
|
|
required=True,
|
|
help='Project root directory'
|
|
)
|
|
parser.add_argument(
|
|
'--output',
|
|
help='Output file path (default: stdout)'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Load Figma components
|
|
with open(args.figma_components, 'r') as f:
|
|
figma_components = json.load(f)
|
|
|
|
# Load Code Connect map if provided
|
|
code_connect_map = {}
|
|
if args.code_connect_map:
|
|
with open(args.code_connect_map, 'r') as f:
|
|
code_connect_map = json.load(f)
|
|
|
|
# Run mapping
|
|
mappings = map_components(figma_components, code_connect_map, args.project_root)
|
|
|
|
# Output results
|
|
output_json = json.dumps(mappings, indent=2)
|
|
|
|
if args.output:
|
|
with open(args.output, 'w') as f:
|
|
f.write(output_json)
|
|
else:
|
|
print(output_json)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|