Skip to content

Commit 3aa76c8

Browse files
author
phernandez
committed
feat: add component dependency analyzer tool
Introduce a new tool to analyze component dependencies with optional TOML and JSON output. This tool can identify PascalCase component references and supports integrations, special icon cases, and core components. Added a corresponding TOML file for known dependencies.
1 parent d74ea0e commit 3aa76c8

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[dependencies]
2+
accordion = [
3+
"icons/ChevronDown",
4+
"icons/ChevronUp",
5+
]
6+
dialog = [
7+
"icons/X",
8+
]
9+
dropdown_menu = [
10+
"checkbox",
11+
"icons/ChevronRight",
12+
"radio",
13+
]
14+
"integrations/wtform" = [
15+
"form",
16+
]
17+
mode_toggle = [
18+
"button",
19+
"dropdown_menu",
20+
"icons/Moon",
21+
"icons/Sun",
22+
]
23+
select = [
24+
"icons/Check",
25+
"icons/ChevronDown",
26+
"icons/ChevronUp",
27+
]
28+
sheet = [
29+
"icons/X",
30+
]
31+
table = [
32+
"icons/ChevronDown",
33+
"icons/ChevronUp",
34+
"icons/ChevronsUpDown",
35+
]
36+
toast = [
37+
"button",
38+
"icons/X",
39+
]

tools/component_analyzer.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# component_analyzer.py
2+
3+
from pathlib import Path
4+
import json
5+
from typing import Dict, List, Set, Tuple
6+
import re
7+
from collections import defaultdict
8+
9+
import tomli_w
10+
import typer
11+
from rich.console import Console
12+
from rich.table import Table
13+
14+
app = typer.Typer(
15+
help="Analyze component dependencies and optionally output to TOML and JSON",
16+
add_completion=False,
17+
)
18+
console = Console()
19+
20+
21+
def find_component_references(content: str) -> Set[str]:
22+
"""Find all PascalCase component references in template content."""
23+
pattern = r'<([A-Z][a-zA-Z0-9]*)[^>]*/?>'
24+
return set(re.findall(pattern, content))
25+
26+
27+
def get_components(components_dir: Path) -> Tuple[Set[str], Dict[str, str]]:
28+
"""Get all component names and create a mapping for integration components."""
29+
core_components = set()
30+
integration_mapping = {}
31+
32+
for d in components_dir.iterdir():
33+
if d.is_dir():
34+
if d.name == 'integrations':
35+
for integration_dir in d.iterdir():
36+
if integration_dir.is_dir():
37+
integration_name = f"integrations/{integration_dir.name}"
38+
integration_mapping[integration_dir.name] = integration_name
39+
else:
40+
core_components.add(d.name)
41+
42+
return core_components, integration_mapping
43+
44+
45+
def normalize_component_name(
46+
name: str,
47+
core_components: Set[str],
48+
integration_mapping: Dict[str, str]
49+
) -> Tuple[str, bool]:
50+
"""Convert component name to its normalized form. Returns (name, is_valid)."""
51+
# Handle icon components - keep PascalCase
52+
if name.endswith('Icon'):
53+
# Just strip "Icon" suffix but maintain PascalCase
54+
base_name = name[:-4]
55+
icon_ref = f"icons/{base_name}"
56+
is_valid = True # We assume all Icon references are valid since they're direct class names
57+
return icon_ref, is_valid
58+
59+
# Handle special cases for moon/sun icons - now in PascalCase
60+
if name.lower() in {'moon', 'sun'}:
61+
return f"icons/{name.title()}", True
62+
63+
name_lower = name.lower()
64+
65+
# Check integration components
66+
for integration_key, integration_name in integration_mapping.items():
67+
if name_lower.startswith(integration_key.lower()):
68+
return integration_name, True
69+
70+
# Special case mappings based on your dependencies
71+
if name_lower in {'checkbox', 'radio', 'form', 'button'}:
72+
return name_lower, True
73+
74+
# Find component in core components
75+
for component in core_components:
76+
if name_lower.startswith(component.replace('_', '')):
77+
return component, component in core_components
78+
79+
return name_lower, False
80+
81+
82+
def analyze_components(
83+
components_dir: Path,
84+
) -> Tuple[Dict[str, List[str]], List[str]]:
85+
"""Analyze components and return (dependencies, warnings)."""
86+
core_components, integration_mapping = get_components(components_dir)
87+
raw_dependencies = defaultdict(set)
88+
warnings = []
89+
90+
for template_file in components_dir.rglob("*.jinja"):
91+
rel_path = template_file.relative_to(components_dir)
92+
parts = rel_path.parts
93+
94+
if parts[0] == 'integrations':
95+
if len(parts) > 1:
96+
component_name = f"integrations/{parts[1]}"
97+
else:
98+
continue
99+
else:
100+
component_name = parts[0]
101+
102+
if component_name in {'ui', 'integrations'}:
103+
continue
104+
105+
content = template_file.read_text()
106+
refs = find_component_references(content)
107+
108+
for ref in refs:
109+
normalized_ref, is_valid = normalize_component_name(
110+
ref, core_components, integration_mapping
111+
)
112+
if not is_valid and not normalized_ref.startswith('icons/'):
113+
warnings.append(
114+
f"Unresolved component reference: '{ref}' in {template_file}"
115+
)
116+
if normalized_ref != component_name:
117+
raw_dependencies[component_name].add(normalized_ref)
118+
119+
dependencies = {
120+
k: sorted(list(v))
121+
for k, v in raw_dependencies.items()
122+
if v
123+
}
124+
125+
return dict(sorted(dependencies.items())), warnings
126+
127+
128+
def display_results(
129+
dependencies: Dict[str, List[str]],
130+
warnings: List[str],
131+
known_dependencies: Dict[str, List[str]],
132+
) -> None:
133+
"""Display analysis results in a formatted table."""
134+
if warnings:
135+
console.print("\n[yellow]Warnings:[/yellow]")
136+
for warning in warnings:
137+
console.print(f"[yellow]⚠ {warning}[/yellow]")
138+
139+
table = Table(title="Component Dependencies")
140+
table.add_column("Component", style="cyan")
141+
table.add_column("Dependencies", style="green")
142+
table.add_column("Status", style="yellow")
143+
144+
for component, deps in dependencies.items():
145+
known_deps = set(known_dependencies.get(component, []))
146+
found_deps = set(deps)
147+
148+
if component not in known_dependencies:
149+
status = "⚠ New component"
150+
elif known_deps != found_deps:
151+
status = "⚠ Dependencies changed"
152+
else:
153+
status = "✓ No changes"
154+
155+
table.add_row(component, "\n".join(deps), status)
156+
157+
console.print("\n", table)
158+
159+
160+
@app.command()
161+
def analyze(
162+
components_dir: Path = typer.Option(
163+
Path("components/ui"),
164+
"--components-dir",
165+
"-d",
166+
help="Directory containing the components",
167+
),
168+
output_toml: bool = typer.Option(
169+
True,
170+
"--toml/--no-toml",
171+
"-t/-nt",
172+
help="Output dependencies to TOML file",
173+
),
174+
output_json: bool = typer.Option(
175+
False,
176+
"--json/--no-json",
177+
"-j/-nj",
178+
help="Output dependencies to JSON file",
179+
),
180+
) -> None:
181+
"""Analyze component dependencies and generate dependency map."""
182+
if not components_dir.exists():
183+
console.print(f"[red]Error: Components directory not found at {components_dir}[/red]")
184+
raise typer.Exit(1)
185+
186+
try:
187+
# Load existing dependencies from component_dependencies.toml if it exists
188+
known_dependencies = {}
189+
toml_path = Path("basic_components/component_dependencies.toml")
190+
if toml_path.exists():
191+
import tomli
192+
with toml_path.open('rb') as f:
193+
toml_data = tomli.load(f)
194+
known_dependencies = toml_data.get('dependencies', {})
195+
196+
# Analyze components
197+
dependencies, warnings = analyze_components(components_dir)
198+
199+
# Display results
200+
display_results(dependencies, warnings, known_dependencies)
201+
202+
# Write TOML output if requested
203+
if output_toml:
204+
output_file = Path("basic_components/component_dependencies.toml")
205+
output_file.parent.mkdir(parents=True, exist_ok=True)
206+
with output_file.open('wb') as f:
207+
tomli_w.dump({'dependencies': dependencies}, f)
208+
console.print(f"\nDependencies written to {output_file}")
209+
210+
# Write JSON output if requested
211+
if output_json:
212+
output_file = Path("component_dependencies.json")
213+
with output_file.open('w') as f:
214+
json.dump({'dependencies': dependencies}, f, indent=2)
215+
console.print(f"\nDependencies written to {output_file}")
216+
217+
# Exit with warning status if there were warnings
218+
if warnings:
219+
raise typer.Exit(1)
220+
221+
except Exception as e:
222+
console.print(f"[red]Error: {str(e)}[/red]")
223+
raise typer.Exit(1)
224+
225+
226+
if __name__ == "__main__":
227+
app()

0 commit comments

Comments
 (0)