Enforce shared WebUI primitive adoption
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m55s

This commit is contained in:
2026-08-18 10:42:55 +02:00
parent 5e9234d4b6
commit 5efb0eea6f
4 changed files with 267 additions and 4 deletions
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""Enforce Core ownership of repeated WebUI toolbar, grid and dialog anatomy."""
from __future__ import annotations
import pathlib
import re
import sys
META_ROOT = pathlib.Path(__file__).resolve().parents[2]
REPOS_ROOT = META_ROOT.parent
CORE_INDEX = pathlib.Path("govoplan-core/webui/src/index.ts")
RAW_ELEMENT = re.compile(
r'<(?P<tag>div|span|header|section|form)\b(?P<attrs>[^>]*?)'
r'\bclassName="(?P<classes>[^"]+)"',
re.DOTALL,
)
LEGACY_LAYOUT_TOKENS = {
"form-grid",
"admin-form-grid",
"dashboard-grid",
"settings-grid",
"admin-dialog",
"admin-dialog-wide",
}
CENTRAL_COMPONENTS = {
"ActionToolbar": pathlib.Path(
"govoplan-core/webui/src/components/ActionToolbar.tsx"
),
"ToolbarGroup": pathlib.Path(
"govoplan-core/webui/src/components/ActionToolbar.tsx"
),
"ToolbarSpacer": pathlib.Path(
"govoplan-core/webui/src/components/ActionToolbar.tsx"
),
"ContentGrid": pathlib.Path(
"govoplan-core/webui/src/components/ContentGrid.tsx"
),
"FormGrid": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"),
"FormLayout": pathlib.Path(
"govoplan-core/webui/src/components/ContentGrid.tsx"
),
"GridItem": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"),
"FormSection": pathlib.Path(
"govoplan-core/webui/src/components/FormSection.tsx"
),
"DialogActions": pathlib.Path(
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
),
"DialogForm": pathlib.Path(
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
),
"DialogSection": pathlib.Path(
"govoplan-core/webui/src/components/DialogAnatomy.tsx"
),
}
REQUIRED_CONSUMERS = {
"ActionToolbar": (
pathlib.Path("govoplan-core/webui/src/components/WysiwygEditor.tsx"),
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarPage.tsx"),
pathlib.Path("govoplan-files/webui/src/features/files/FilesPage.tsx"),
pathlib.Path("govoplan-forms/webui/src/features/forms/FormsPage.tsx"),
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
),
"ContentGrid": (
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx"),
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSettingsPanel.tsx"),
),
"FormGrid": (
pathlib.Path("govoplan-core/webui/src/components/mail/MailServerSettingsPanel.tsx"),
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"),
pathlib.Path("govoplan-forms/webui/src/features/forms/FormDefinitionDialog.tsx"),
pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxAdminPanel.tsx"),
),
"FormSection": (
pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"),
pathlib.Path("govoplan-quick-access/webui/src/features/settings/QuickAccessSettingsPanel.tsx"),
),
"DialogForm": (
pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"),
pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"),
pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"),
),
"DialogSection": (
pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"),
pathlib.Path("govoplan-files/webui/src/features/files/components/FileShareDialog.tsx"),
pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"),
),
}
def source_paths() -> list[pathlib.Path]:
paths: list[pathlib.Path] = []
for repository in sorted(REPOS_ROOT.glob("govoplan*")):
source_root = repository / "webui" / "src"
if source_root.is_dir():
paths.extend(sorted(source_root.rglob("*.tsx")))
return paths
def relative(path: pathlib.Path) -> pathlib.Path:
return path.relative_to(REPOS_ROOT)
def raw_reason(classes: str) -> str | None:
tokens = classes.split()
legacy = sorted(set(tokens) & LEGACY_LAYOUT_TOKENS)
if legacy:
return f"legacy shared layout class {', '.join(legacy)}"
toolbars = [
token
for token in tokens
if token == "admin-toolbar-row" or token.endswith("-toolbar")
]
if toolbars:
return f"raw toolbar class {', '.join(toolbars)}"
dialog_forms = [token for token in tokens if token.endswith("dialog-form")]
if dialog_forms:
return f"raw dialog form class {', '.join(dialog_forms)}"
return None
def main() -> int:
sources = source_paths()
source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources}
errors: list[str] = []
for path, content in source_text.items():
for match in RAW_ELEMENT.finditer(content):
reason = raw_reason(match.group("classes"))
if reason is None:
continue
line = content.count("\n", 0, match.start()) + 1
errors.append(
f"Raw {match.group('tag')} repeats shared anatomy ({reason}): {path}:{line}"
)
for name, owner in CENTRAL_COMPONENTS.items():
definition = re.compile(
rf"\b(?:function|class)\s+{name}\b|\bconst\s+{name}\s*="
)
for path, content in source_text.items():
if path != owner and definition.search(content):
errors.append(f"Module-local {name} definition is not allowed: {path}")
usage_counts: dict[str, int] = {}
for name in CENTRAL_COMPONENTS:
usage = re.compile(rf"<{name}\b")
usage_counts[name] = sum(bool(usage.search(content)) for content in source_text.values())
for name, consumers in REQUIRED_CONSUMERS.items():
for path in consumers:
absolute = REPOS_ROOT / path
if not absolute.exists():
continue
if f"<{name}" not in absolute.read_text(encoding="utf-8"):
errors.append(f"Required shared {name} consumer regressed: {path}")
core_index_path = REPOS_ROOT / CORE_INDEX
if core_index_path.exists():
core_index = core_index_path.read_text(encoding="utf-8")
for name in CENTRAL_COMPONENTS:
if not re.search(rf"\b{name}\b", core_index):
errors.append(f"Core must export {name} from @govoplan/core-webui.")
dialog_path = REPOS_ROOT / "govoplan-core/webui/src/components/Dialog.tsx"
if dialog_path.exists():
dialog_text = dialog_path.read_text(encoding="utf-8")
if "<DialogActions" not in dialog_text:
errors.append("Every Core Dialog footer must compose DialogActions.")
if errors:
print("\n".join(errors), file=sys.stderr)
return 1
migrated = ", ".join(
f"{name}={usage_counts[name]} files"
for name in ("ActionToolbar", "ContentGrid", "FormGrid", "FormSection", "DialogForm", "DialogSection")
)
print(f"Shared WebUI primitive contract passed: {migrated}; no raw legacy anatomy.")
return 0
if __name__ == "__main__":
raise SystemExit(main())