101 lines
4.5 KiB
Python
101 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Trace Plugins — Find plugin→manifest→frontend verkabelungsfehler."""
|
|
from __future__ import annotations
|
|
import ast, re, sys, json
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
|
APP = ROOT / "app"
|
|
FRONTEND = ROOT / "frontend" / "src"
|
|
|
|
def find_plugin_manifests() -> list[dict]:
|
|
"""Find plugin manifests.
|
|
|
|
Project convention: manifests are defined inline in plugin.py via
|
|
``PluginManifest(...)`` — no separate manifest.py file exists.
|
|
"""
|
|
plugins = []
|
|
builtins = APP / "plugins" / "builtins"
|
|
if not builtins.exists(): return plugins
|
|
for d in builtins.iterdir():
|
|
if not d.is_dir() or d.name.startswith("_"): continue
|
|
plugin_file = d / "plugin.py"
|
|
if not plugin_file.exists():
|
|
# Not a plugin directory (e.g. migrations/, tests/) — skip silently
|
|
continue
|
|
has_manifest = False
|
|
plugin_info = {"name": d.name, "path": str(d.relative_to(ROOT)), "has_manifest": False}
|
|
if plugin_file.exists():
|
|
try: content = plugin_file.read_text()
|
|
except: continue
|
|
has_manifest = bool(re.search(r'PluginManifest\s*\(', content))
|
|
plugin_info["has_manifest"] = has_manifest
|
|
for m in re.finditer(r'menu_items\s*[=:]\s*\[', content): plugin_info["has_menu_items"] = True
|
|
for m in re.finditer(r'page_routes\s*[=:]\s*\[', content): plugin_info["has_page_routes"] = True
|
|
for m in re.finditer(r'settings_pages\s*[=:]\s*\[', content): plugin_info["has_settings_pages"] = True
|
|
for m in re.finditer(r'detail_tabs\s*[=:]\s*\[', content): plugin_info["has_detail_tabs"] = True
|
|
plugins.append(plugin_info)
|
|
return plugins
|
|
|
|
def find_frontend_plugin_refs() -> dict[str, list[str]]:
|
|
refs = {}
|
|
for ts in FRONTEND.rglob("*.ts"):
|
|
try: content = ts.read_text()
|
|
except: continue
|
|
for m in re.finditer(r'@/plugins/([\w-]+)', content):
|
|
refs.setdefault(m.group(1), []).append(str(ts.relative_to(ROOT)))
|
|
return refs
|
|
|
|
def find_plugin_routes() -> list[dict]:
|
|
routes = []
|
|
builtins = APP / "plugins" / "builtins"
|
|
if not builtins.exists(): return routes
|
|
for py in builtins.rglob("*.py"):
|
|
try: content = py.read_text()
|
|
except: continue
|
|
rel = str(py.relative_to(ROOT))
|
|
for m in re.finditer(r'@router\.(get|post|put|patch|delete)\s*\(\s*["\']([^"\']+)["\']', content):
|
|
routes.append({"method": m.group(1).upper(), "path": m.group(2), "file": rel})
|
|
return routes
|
|
|
|
|
|
def _has_dynamic_consumption() -> bool:
|
|
"""True if the frontend consumes plugin manifests dynamically.
|
|
|
|
The frontend fetches menu_items/page_routes via the active-manifests API
|
|
(frontend/src/api/pluginManifests.ts) and renders them through pluginStore —
|
|
static '@/plugins/<name>' imports are therefore not required for wiring.
|
|
"""
|
|
return (FRONTEND / "api" / "pluginManifests.ts").exists()
|
|
|
|
def main():
|
|
print("=" * 70)
|
|
print("TRACE PLUGINS — Plugin→Manifest→Frontend Verkabelungsfehler")
|
|
print("=" * 70)
|
|
plugins = find_plugin_manifests()
|
|
frontend_refs = find_frontend_plugin_refs()
|
|
plugin_routes = find_plugin_routes()
|
|
issues = []
|
|
for p in plugins:
|
|
if not p["has_manifest"]:
|
|
issues.append({"plugin": p["name"], "issue": "No manifest (PluginManifest in plugin.py expected)", "severity": "HIGH"})
|
|
# NOTE: menu_items are consumed dynamically by the frontend via the
|
|
# active-manifests API (frontend/src/api/pluginManifests.ts + pluginStore),
|
|
# NOT via static '@/plugins/<name>' imports — a missing static import is
|
|
# therefore not a wiring error.
|
|
if p.get("has_menu_items") and p["name"] not in frontend_refs and not _has_dynamic_consumption():
|
|
issues.append({"plugin": p["name"], "issue": "Has menu_items but no frontend reference", "severity": "MEDIUM"})
|
|
print(f"\nPlugins found: {len(plugins)}")
|
|
print(f"Plugin routes: {len(plugin_routes)}")
|
|
print(f"Frontend plugin refs: {len(frontend_refs)}")
|
|
print(f"\nISSUES: {len(issues)}")
|
|
if issues:
|
|
print("\n--- Plugin issues ---")
|
|
for i in issues: print(f" [{i['severity']}] {i['plugin']}: {i['issue']}")
|
|
results = {"plugins": plugins, "frontend_refs": frontend_refs, "plugin_routes": plugin_routes, "issues": issues}
|
|
with open(ROOT / "scripts" / "test_suite" / "results_trace_plugins.json", "w") as f:
|
|
json.dump(results, f, indent=2, default=str)
|
|
return len(issues)
|
|
|
|
if __name__ == "__main__": sys.exit(main())
|