Architecture Diagram Generator
Generates architecture diagrams from project structure in multiple formats:
- component: Shows modules and their relationships
- layer: Shows architectural layers
- deployment: Shows deployment topology
from typing import Dict, List, Set, Tuple, Optional
from collections import defaultdict
"""Scans project structure to detect components and relationships."""
# Common architectural layer patterns
'presentation': ['controller', 'handler', 'view', 'page', 'component', 'ui'],
'api': ['api', 'route', 'endpoint', 'rest', 'graphql'],
'business': ['service', 'usecase', 'domain', 'logic', 'core'],
'data': ['repository', 'dao', 'model', 'entity', 'schema', 'migration'],
'infrastructure': ['config', 'util', 'helper', 'middleware', 'plugin'],
# File patterns for different technologies
'react': ['jsx', 'tsx', 'package.json'],
'vue': ['vue', 'nuxt.config'],
'angular': ['component.ts', 'module.ts', 'angular.json'],
'node': ['package.json', 'express', 'fastify'],
'python': ['requirements.txt', 'pyproject.toml', 'setup.py'],
'go': ['go.mod', 'go.sum'],
'java': ['pom.xml', 'build.gradle'],
'docker': ['Dockerfile', 'docker-compose'],
'kubernetes': ['deployment.yaml', 'service.yaml', 'k8s'],
def __init__(self, project_path: Path):
self.project_path = project_path
self.components: Dict[str, Dict] = {}
self.relationships: List[Tuple[str, str, str]] = [] # (from, to, type)
self.layers: Dict[str, List[str]] = defaultdict(list)
self.technologies: Set[str] = set()
self.external_deps: Set[str] = set()
"""Scan the project and return structure information."""
self._detect_technologies()
self._detect_relationships()
'components': self.components,
'relationships': self.relationships,
'layers': dict(self.layers),
'technologies': list(self.technologies),
'external_deps': list(self.external_deps),
def _scan_directories(self):
"""Scan directory structure for components."""
ignore_dirs = {'.git', 'node_modules', '__pycache__', '.venv', 'venv',
'dist', 'build', '.next', '.nuxt', 'coverage', '.pytest_cache'}
for item in self.project_path.iterdir():
if item.is_dir() and item.name not in ignore_dirs and not item.name.startswith('.'):
component_info = self._analyze_directory(item)
if component_info['files'] > 0:
self.components[item.name] = component_info
def _analyze_directory(self, dir_path: Path) -> Dict:
"""Analyze a directory to understand its role."""
files = list(dir_path.rglob('*'))
code_files = [f for f in files if f.is_file() and f.suffix in
['.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs', '.java', '.vue']]
# Count imports/dependencies within the directory
for f in code_files[:50]: # Limit to avoid large projects
imports.update(self._extract_imports(f))
'path': str(dir_path.relative_to(self.project_path)),
'files': len(code_files),
'imports': list(imports)[:20], # Top 20 imports
'type': self._guess_component_type(dir_path.name),
def _extract_imports(self, file_path: Path) -> Set[str]:
"""Extract import statements from a file."""
content = file_path.read_text(encoding='utf-8', errors='ignore')
py_imports = re.findall(r'^(?:from|import)\s+([\w.]+)', content, re.MULTILINE)
imports.update(py_imports)
js_imports = re.findall(r'(?:import|require)\s*\(?[\'"]([^\'"\s]+)[\'"]', content)
imports.update(js_imports)
go_imports = re.findall(r'import\s+(?:\(\s*)?["\']([^"\']+)["\']', content)
imports.update(go_imports)
def _guess_component_type(self, name: str) -> str:
"""Guess component type from directory name."""
name_lower = name.lower()
for layer, patterns in self.LAYER_PATTERNS.items():
if pattern in name_lower:
def _detect_technologies(self):
"""Detect technologies used in the project."""
for tech, patterns in self.TECH_PATTERNS.items():
matches = list(self.project_path.rglob(f'*{pattern}*'))
self.technologies.add(tech)
# Detect external dependencies from package files
self._parse_package_json()
self._parse_requirements_txt()
def _parse_package_json(self):
"""Parse package.json for dependencies."""
pkg_path = self.project_path / 'package.json'
data = json.loads(pkg_path.read_text())
deps = list(data.get('dependencies', {}).keys())[:10]
self.external_deps.update(deps)
def _parse_requirements_txt(self):
"""Parse requirements.txt for dependencies."""
req_path = self.project_path / 'requirements.txt'
content = req_path.read_text()
deps = re.findall(r'^([a-zA-Z0-9_-]+)', content, re.MULTILINE)[:10]
self.external_deps.update(deps)
"""Parse go.mod for dependencies."""
mod_path = self.project_path / 'go.mod'
content = mod_path.read_text()
deps = re.findall(r'^\s+([^\s]+)\s+v', content, re.MULTILINE)[:10]
self.external_deps.update([d.split('/')[-1] for d in deps])
def _detect_relationships(self):
"""Detect relationships between components."""
component_names = set(self.components.keys())
for comp_name, comp_info in self.components.items():
for imp in comp_info.get('imports', []):
# Check if import references another component
for other_comp in component_names:
if other_comp != comp_name and other_comp.lower() in imp.lower():
self.relationships.append((comp_name, other_comp, 'uses'))
def _classify_layers(self):
"""Classify components into architectural layers."""
for comp_name, comp_info in self.components.items():
layer = comp_info.get('type', 'unknown')
self.layers[layer].append(comp_name)
self.layers['other'].append(comp_name)
"""Base class for diagram generators."""
def __init__(self, scan_result: Dict):
self.components = scan_result['components']
self.relationships = scan_result['relationships']
self.layers = scan_result['layers']
self.technologies = scan_result['technologies']
self.external_deps = scan_result['external_deps']
def generate(self, diagram_type: str) -> str:
"""Generate diagram based on type."""
if diagram_type == 'component':
return self._generate_component_diagram()
elif diagram_type == 'layer':
return self._generate_layer_diagram()
elif diagram_type == 'deployment':
return self._generate_deployment_diagram()
return self._generate_component_diagram()
def _generate_component_diagram(self) -> str:
raise NotImplementedError
def _generate_layer_diagram(self) -> str:
raise NotImplementedError
def _generate_deployment_diagram(self) -> str:
raise NotImplementedError
class MermaidGenerator(DiagramGenerator):
"""Generate Mermaid diagrams."""
def _generate_component_diagram(self) -> str:
for name, info in self.components.items():
safe_name = self._safe_id(name)
file_count = info.get('files', 0)
lines.append(f' {safe_name}["{name}<br/>{file_count} files"]')
for src, dst, rel_type in self.relationships:
lines.append(f' {self._safe_id(src)} --> {self._safe_id(dst)}')
# Add external dependencies if any
lines.append(' subgraph External')
for dep in list(self.external_deps)[:5]:
safe_dep = self._safe_id(dep)
lines.append(f' {safe_dep}(("{dep}"))')
def _generate_layer_diagram(self) -> str:
layer_order = ['presentation', 'api', 'business', 'data', 'infrastructure', 'other']
for layer in layer_order:
components = self.layers.get(layer, [])
lines.append(f' subgraph {layer.title()} Layer')
safe_comp = self._safe_id(comp)
lines.append(f' {safe_comp}["{comp}"]')
# Add layer relationships (top-down)
for layer in layer_order:
if self.layers.get(layer):
if prev_layer and self.layers.get(prev_layer):
first_prev = self._safe_id(self.layers[prev_layer][0])
first_curr = self._safe_id(self.layers[layer][0])
lines.append(f' {first_prev} -.-> {first_curr}')
def _generate_deployment_diagram(self) -> str:
lines.append(' subgraph Client')
lines.append(' browser["Browser/Mobile"]')
# Determine if we have typical deployment components
has_api = any('api' in t for t in self.technologies)
has_docker = 'docker' in self.technologies
has_k8s = 'kubernetes' in self.technologies
lines.append(' subgraph Application')
lines.append(' k8s["Kubernetes Cluster"]')
lines.append(' docker["Docker Container"]')
lines.append(' app["Application Server"]')
lines.append(' subgraph Data')
lines.append(' db[("Database")]')
lines.append(' cache[("Cache")]')
lines.append(' browser --> k8s')
lines.append(' k8s --> db')
lines.append(' browser --> docker')
lines.append(' docker --> db')
lines.append(' browser --> app')
lines.append(' app --> db')
def _safe_id(self, name: str) -> str:
"""Convert name to safe Mermaid ID."""
return re.sub(r'[^a-zA-Z0-9]', '_', name)
class PlantUMLGenerator(DiagramGenerator):
"""Generate PlantUML diagrams."""
def _generate_component_diagram(self) -> str:
lines = ['@startuml', 'skinparam componentStyle rectangle', '']
for name, info in self.components.items():
file_count = info.get('files', 0)
lines.append(f'component "{name}\\n({file_count} files)" as {self._safe_id(name)}')
for src, dst, rel_type in self.relationships:
lines.append(f'{self._safe_id(src)} --> {self._safe_id(dst)}')
lines.append('package "External Dependencies" {')
for dep in list(self.external_deps)[:5]:
lines.append(f' [{dep}]')
def _generate_layer_diagram(self) -> str:
lines = ['@startuml', 'skinparam packageStyle rectangle', '']
layer_order = ['presentation', 'api', 'business', 'data', 'infrastructure', 'other']
for layer in layer_order:
components = self.layers.get(layer, [])
lines.append(f'package "{layer.title()} Layer" {{')
lines.append(f' [{comp}]')
def _generate_deployment_diagram(self) -> str:
lines = ['@startuml', '']
lines.append('node "Client" {')
lines.append(' [Browser/Mobile] as browser')
has_docker = 'docker' in self.technologies
has_k8s = 'kubernetes' in self.technologies
lines.append('node "Application Server" {')
lines.append(' [Kubernetes Cluster] as app')
lines.append(' [Docker Container] as app')
lines.append(' [Application] as app')
lines.append('database "Data Store" {')
lines.append(' [Database] as db')
lines.append('browser --> app')
lines.append('app --> db')
def _safe_id(self, name: str) -> str:
"""Convert name to safe PlantUML ID."""
return re.sub(r'[^a-zA-Z0-9]', '_', name)
class ASCIIGenerator(DiagramGenerator):
"""Generate ASCII diagrams."""
def _generate_component_diagram(self) -> str:
lines.append('COMPONENT DIAGRAM')
lines.append('Components:')
for name, info in self.components.items():
file_count = info.get('files', 0)
comp_type = info.get('type', 'unknown')
lines.append(f' [{name}]')
lines.append(f' Files: {file_count}')
lines.append(f' Type: {comp_type}')
lines.append('Relationships:')
for src, dst, rel_type in self.relationships:
lines.append(f' {src} --> {dst}')
lines.append('External Dependencies:')
for dep in list(self.external_deps)[:10]:
lines.append(f' - {dep}')
def _generate_layer_diagram(self) -> str:
lines.append('LAYERED ARCHITECTURE')
layer_order = ['presentation', 'api', 'business', 'data', 'infrastructure', 'other']
for layer in layer_order:
components = self.layers.get(layer, [])
lines.append(f'+{"-" * 56}+')
lines.append(f'| {layer.upper():^54} |')
lines.append(f'+{"-" * 56}+')
lines.append(f'| [{comp:^48}] |')
lines.append(f'+{"-" * 56}+')
if lines[-2:] == [' |', ' v']:
def _generate_deployment_diagram(self) -> str:
lines.append('DEPLOYMENT DIAGRAM')
has_docker = 'docker' in self.technologies
has_k8s = 'kubernetes' in self.technologies
lines.append('+----------------------+')
lines.append('| CLIENT |')
lines.append('| [Browser/Mobile] |')
lines.append('+----------+-----------+')
lines.append('+----------------------+')
lines.append('| APPLICATION |')
lines.append('| [Kubernetes Cluster] |')
lines.append('| [Docker Container] |')
lines.append('| [App Server] |')
lines.append('+----------+-----------+')
lines.append('+----------------------+')
lines.append('| [(Database)] |')
lines.append('+----------------------+')
lines.append('Technologies detected:')
for tech in sorted(self.technologies):
lines.append(f' - {tech}')
parser = argparse.ArgumentParser(
description='Generate architecture diagrams from project structure',
formatter_class=argparse.RawDescriptionHelpFormatter,
%(prog)s ./my-project --format mermaid
%(prog)s ./my-project --format plantuml --type layer
%(prog)s ./my-project --format ascii -o architecture.txt
component - Shows modules and their relationships (default)
layer - Shows architectural layers
deployment - Shows deployment topology
mermaid - Mermaid.js format (default)
plantuml - PlantUML format
help='Path to the project directory'
choices=['mermaid', 'plantuml', 'ascii'],
help='Output format (default: mermaid)'
choices=['component', 'layer', 'deployment'],
help='Diagram type (default: component)'
help='Output file path (prints to stdout if not specified)'
help='Enable verbose output'
help='Output raw scan results as JSON'
args = parser.parse_args()
project_path = Path(args.project_path).resolve()
if not project_path.exists():
print(f"Error: Project path does not exist: {project_path}", file=sys.stderr)
if not project_path.is_dir():
print(f"Error: Project path is not a directory: {project_path}", file=sys.stderr)
print(f"Scanning project: {project_path}")
scanner = ProjectScanner(project_path)
scan_result = scanner.scan()
print(f"Found {len(scan_result['components'])} components")
print(f"Found {len(scan_result['relationships'])} relationships")
print(f"Technologies: {', '.join(scan_result['technologies']) or 'none detected'}")
# Output raw JSON if requested
output = json.dumps(scan_result, indent=2)
Path(args.output).write_text(output)
print(f"Results written to {args.output}")
'mermaid': MermaidGenerator,
'plantuml': PlantUMLGenerator,
generator = generators[args.format](scan_result)
diagram = generator.generate(args.type)
Path(args.output).write_text(diagram)
print(f"Diagram written to {args.output}")
if __name__ == '__main__':