Files
gh-alekspetrov-navigator/skills/product-design/functions/component_mapper.py
2025-11-29 17:51:59 +08:00

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