#!/usr/bin/env python3 """ porting-tally.py — VSR suite-frontend porting status report. Produces a Markdown summary of: - Pages/components using vs. missing suite-components - Raw HTML elements that have a suite-component equivalent - Apollo usage patterns (raw @apollo/client vs @theorchard/suite-apollo) - Route patterns (react-router-dom vs suite-frontend routing) - Dev gaps: stub server query coverage, GQL schema/resolver alignment Deliverable: Markdown to stdout. Pipe or redirect as needed. """ import ast import json import os import re import subprocess import sys from collections import defaultdict from pathlib import Path ROOT = Path(__file__).resolve().parents[1] VSR_SRC = ROOT / "apps" / "vsr" / "src" PAGES_DIR = VSR_SRC / "pages" COMPONENTS_DIR = VSR_SRC / "components" STUB_SRC = ROOT / "tools" / "vsr-graphql-stub" / "src" GQL_SCHEMA = ROOT / "packages" / "vsr-graphql-server" / "src" / "schema" # Suite-components available (discovered from node_modules) SUITE_COMPONENTS_PATH = ( ROOT / "apps" / "vsr" / "node_modules" / "@theorchard" / "suite-components" / "dist" / "esm" / "src" / "components" ) # HTML element → suite-component equivalent SUITE_EQUIVALENTS: dict[str, str] = { " list[str]: if not SUITE_COMPONENTS_PATH.exists(): return [] return sorted( p.name for p in SUITE_COMPONENTS_PATH.iterdir() if p.is_dir() and not p.name.startswith("index") ) def read_file(path: Path) -> str: try: return path.read_text(encoding="utf-8") except Exception: return "" def tsx_files(directory: Path) -> list[Path]: return sorted(directory.glob("*.tsx")) + sorted(directory.glob("*.ts")) def extract_imports(content: str) -> list[tuple[str, list[str]]]: """Returns list of (from_module, [named_imports]) from import statements.""" results = [] pattern = re.compile( r'import\s+(?:\{([^}]*)\}|(\w+))\s+from\s+["\']([^"\']+)["\']', re.MULTILINE ) for m in pattern.finditer(content): named = m.group(1) or "" default = m.group(2) or "" mod = m.group(3) names = [n.strip() for n in named.split(",") if n.strip()] + ( [default] if default else [] ) results.append((mod, names)) return results def analyze_file(path: Path) -> dict: content = read_file(path) imports = extract_imports(content) suite_imports = [ (mod, names) for mod, names in imports if "@theorchard" in mod ] apollo_imports = [ (mod, names) for mod, names in imports if "@apollo/client" in mod ] router_imports = [ (mod, names) for mod, names in imports if "react-router-dom" in mod ] # Raw HTML elements that have suite equivalents raw_elements: list[str] = [] for pattern, suite in SUITE_EQUIVALENTS.items(): if pattern in content: raw_elements.append(f"`{pattern}` → use {suite}") # Count raw dict: """Check stub server for query handler coverage.""" stub_index = STUB_SRC / "index.ts" content = read_file(stub_index) # Extract resolver function names / handler patterns resolver_pattern = re.compile(r"(\w+)\s*\(", re.MULTILINE) query_types = re.findall(r'viewType:\s*["\'](\w+)["\']', content) query_handlers = re.findall(r"case ['\"](\w+)['\"]", content) resolvers = re.findall(r"(\w+):\s*(?:async\s*)?\(", content) return { "viewType_handlers": sorted(set(query_types)), "case_handlers": sorted(set(query_handlers)), "resolver_names": sorted(set(resolvers[:30])), } def schema_types() -> list[str]: """Extract type names from .graphql files.""" types = [] if not GQL_SCHEMA.exists(): return types for f in sorted(GQL_SCHEMA.glob("*.graphql")): content = read_file(f) types += re.findall(r"^type\s+(\w+)", content, re.MULTILINE) return sorted(set(types)) def format_markdown( pages: dict, components: dict, stub: dict, gql_types: list[str], suite_comps: list[str], ) -> str: lines = [ "# VSR Porting Tally — suite-frontend Migration Status", "", f"> Generated: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M')}", "", ] # ── Summary ────────────────────────────────────────────────────────────── total_files = len(pages) + len(components) suite_adopted = sum( 1 for d in list(pages.values()) + list(components.values()) if d["uses_suite_components"] ) raw_only = total_files - suite_adopted lines += [ "## Summary", "", f"| Metric | Value |", f"|--------|-------|", f"| Pages analysed | {len(pages)} |", f"| Components analysed | {len(components)} |", f"| Files using suite-components | {suite_adopted} / {total_files} |", f"| Files with 0 suite imports | {raw_only} |", f"| suite-apollo adopted | {sum(1 for d in list(pages.values()) + list(components.values()) if d['uses_suite_apollo'])} / {total_files} |", "", ] # ── Pages ──────────────────────────────────────────────────────────────── lines += ["## Pages", "", "| Page | suite-components | suite-apollo | Raw HTML elements | Notes |", "|------|-----------------|-------------|-------------------|-------|"] for name, d in sorted(pages.items()): suite_names = ", ".join( n for _, names in d["suite_imports"] for n in names ) or "—" raw = "; ".join( f"{tag}×{cnt}" for tag, cnt in sorted(d["raw_counts"].items()) ) or "—" apollo = "✅ suite-apollo" if d["uses_suite_apollo"] else ( "⚠️ raw @apollo/client" if d["apollo_imports"] else "—" ) suite_badge = "✅" if d["uses_suite_components"] else "❌" lines.append( f"| **{name}** | {suite_badge} {suite_names} | {apollo} | {raw} | |" ) lines.append("") # ── Components ─────────────────────────────────────────────────────────── lines += ["## Local Components", "", "| Component | suite-components | Raw HTML | Notes |", "|-----------|-----------------|----------|-------|"] for name, d in sorted(components.items()): suite_names = ", ".join( n for _, names in d["suite_imports"] for n in names ) or "—" raw = "; ".join( f"{tag}×{cnt}" for tag, cnt in sorted(d["raw_counts"].items()) ) or "—" badge = "✅" if d["uses_suite_components"] else "❌" lines.append(f"| **{name}** | {badge} {suite_names} | {raw} | |") lines.append("") # ── Raw-element hotspots ───────────────────────────────────────────────── all_raws: dict[str, list[str]] = defaultdict(list) for name, d in list(pages.items()) + list(components.items()): for pattern in d["raw_elements"]: all_raws[pattern].append(name) if all_raws: lines += ["## Raw HTML → suite-component Opportunities", ""] for pattern, files in sorted(all_raws.items()): lines.append(f"- {pattern}") for f in files: lines.append(f" - `{f}`") lines.append("") # ── High-priority suite-components not yet used ────────────────────────── used_suite: set[str] = set() for d in list(pages.values()) + list(components.values()): for _, names in d["suite_imports"]: used_suite.update(names) not_yet_used = [ c for c in HIGH_PRIORITY_SUITE_COMPONENTS if c not in used_suite ] if not_yet_used: lines += [ "## High-Priority suite-components Not Yet Adopted", "", "These exist in `@theorchard/suite-components` and are likely needed:", "", ] for c in not_yet_used: lines.append(f"- `{c}`") lines.append("") # ── Stub server coverage ───────────────────────────────────────────────── EXPECTED_VIEW_TYPES = [ "NEW_RELEASES", "BEST_SELLERS", "ON_DEAL", "GENRE", "CONFIGURATION", "EXCLUSIVE", ] missing_view_types = [ vt for vt in EXPECTED_VIEW_TYPES if vt not in stub["viewType_handlers"] and vt not in stub["case_handlers"] ] lines += [ "## Stub Server Coverage", "", f"**viewType handlers found:** {', '.join(stub['viewType_handlers']) or '(none detected)'}", f"**case handlers found:** {', '.join(stub['case_handlers']) or '(none)'}", "", ] if missing_view_types: lines.append( f"⚠️ **Missing viewType handlers:** {', '.join(missing_view_types)}" ) else: lines.append("✅ All expected viewType values appear handled.") lines.append("") # ── GQL Schema types ───────────────────────────────────────────────────── if gql_types: lines += [ "## GraphQL Schema Types", "", ", ".join(f"`{t}`" for t in gql_types), "", ] # ── Available suite-components (for reference) ─────────────────────────── if suite_comps: lines += [ "## Available suite-components (reference)", "", ", ".join(f"`{c}`" for c in suite_comps), "", ] return "\n".join(lines) def main() -> None: suite_comps = available_suite_components() pages: dict[str, dict] = {} for f in tsx_files(PAGES_DIR): pages[f.stem] = analyze_file(f) components: dict[str, dict] = {} for f in tsx_files(COMPONENTS_DIR): if f.suffix in (".tsx", ".ts") and not f.name.startswith("."): components[f.stem] = analyze_file(f) stub = analyze_stub() gql_types = schema_types() print(format_markdown(pages, components, stub, gql_types, suite_comps)) if __name__ == "__main__": main()