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"\n Dependencies 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"\n Dependencies 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