Files
govoplan/tools/checks/check-webui-package-facades.py
T

102 lines
4.1 KiB
Python

#!/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())