#!/usr/bin/env python3 """Check that release Git dependencies expose their owning WebUI package.""" from __future__ import annotations import argparse import json from pathlib import Path import re META_ROOT = Path(__file__).resolve().parents[2] GIT_REPOSITORY = re.compile(r"/(govoplan-[a-z0-9-]+)\.git#v[^/]+$") PARITY_FIELDS = ( "name", "version", "type", "dependencies", "optionalDependencies", "peerDependencies", "peerDependenciesMeta", ) ENTRY_FIELDS = ("main", "module", "types", "exports") def prefixed_entries(value: object) -> object: if isinstance(value, str): return "./webui/" + value[2:] if value.startswith("./") else "webui/" + value if isinstance(value, dict): return {key: prefixed_entries(item) for key, item in value.items()} if isinstance(value, list): return [prefixed_entries(item) for item in value] return value def entry_paths(value: object) -> list[str]: if isinstance(value, str): return [value] if isinstance(value, dict): return [path for item in value.values() for path in entry_paths(item)] if isinstance(value, list): return [path for item in value for path in entry_paths(item)] return [] def facade_issues(repository: Path, *, package_name: str) -> list[str]: issues: list[str] = [] try: root = json.loads((repository / "package.json").read_text(encoding="utf-8")) webui = json.loads((repository / "webui/package.json").read_text(encoding="utf-8")) except (OSError, ValueError) as exc: return [f"{repository.name}: cannot read package facades: {exc}"] if not isinstance(root, dict) or not isinstance(webui, dict): return [f"{repository.name}: package manifests must be JSON objects"] if webui.get("name") != package_name: issues.append(f"{repository.name}: WebUI name does not match {package_name}") for field in PARITY_FIELDS: if root.get(field) != webui.get(field): issues.append(f"{repository.name}: root {field} differs from owning WebUI package") for field in ENTRY_FIELDS: expected = prefixed_entries(webui.get(field)) if root.get(field) != expected: issues.append(f"{repository.name}: root {field} must target the corresponding webui/ entry") for entry in entry_paths(root.get(field)): path = repository / entry if not path.resolve().is_relative_to((repository / "webui").resolve()): issues.append(f"{repository.name}: {field} entry escapes webui/: {entry}") elif "*" not in entry and not path.is_file(): issues.append(f"{repository.name}: missing {field} entry: {entry}") if not root.get("exports") and not root.get("main"): issues.append(f"{repository.name}: root package has no WebUI entry point") return issues def check_composition(workspace: Path, *, core_root: Path | None = None) -> tuple[int, list[str]]: core = core_root or workspace / "govoplan-core" release = json.loads((core / "webui/package.release.json").read_text(encoding="utf-8")) checked = 0 issues: list[str] = [] for name, reference in release.get("dependencies", {}).items(): if not name.startswith("@govoplan/"): continue match = GIT_REPOSITORY.search(reference) if isinstance(reference, str) else None if match is None: issues.append(f"{name}: release dependency is not a versioned GovOPlaN Git source") continue checked += 1 issues.extend(facade_issues(workspace / match.group(1), package_name=name)) return checked, issues def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--workspace-root", type=Path, default=META_ROOT.parent) parser.add_argument("--core-root", type=Path) args = parser.parse_args() checked, issues = check_composition(args.workspace_root, core_root=args.core_root) if issues: print("\n".join(issues)) return 1 print(f"Release WebUI package facade checks passed for {checked} Git dependencies") return 0 if __name__ == "__main__": raise SystemExit(main())