Enforce semantic page layout contracts
This commit is contained in:
@@ -16,6 +16,10 @@ WORKSPACE_BASELINE_PATH = pathlib.Path(__file__).with_name(
|
||||
)
|
||||
RAW_PAGE_FRAME = 'className="content-pad workspace-data-page'
|
||||
RAW_WORKSPACE = re.compile(r'<div\s+className="workspace(?:\s|\")')
|
||||
PAGE_LAYOUT_USAGE = re.compile(r"<PageLayout\b")
|
||||
SEMANTIC_PAGE_LAYOUT_USAGE = re.compile(
|
||||
r"<PageLayout\s+(?:\n\s*)?archetype="
|
||||
)
|
||||
LOCAL_PAGE_LAYOUT = re.compile(r"\b(?:function|class|const)\s+PageLayout\b")
|
||||
LOCAL_WORKSPACE_LAYOUT = re.compile(
|
||||
r"\b(?:function|class|const)\s+WorkspaceLayout\b"
|
||||
@@ -61,6 +65,23 @@ REQUIRED_WORKSPACE_CONSUMERS = (
|
||||
pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"),
|
||||
pathlib.Path("govoplan-organizations/webui/src/features/organizations/OrganizationsPage.tsx"),
|
||||
)
|
||||
HEADERLESS_PAGE_LAYOUT_CONSUMERS = {
|
||||
pathlib.Path("govoplan-access/webui/src/features/admin/AdminPage.tsx"),
|
||||
}
|
||||
EDITOR_PAGE_CONSUMERS = (
|
||||
pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-dashboard/webui/src/features/dashboard/DashboardPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/AttachmentsDataPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignFieldsPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignOverviewPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/MailSettingsPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/TemplateDataPage.tsx"),
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx"),
|
||||
)
|
||||
DESTRUCTIVE_PAGE_CONSUMERS = (
|
||||
pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignOverviewPage.tsx"),
|
||||
)
|
||||
|
||||
|
||||
def baseline_paths(path: pathlib.Path) -> set[pathlib.Path]:
|
||||
@@ -129,6 +150,28 @@ def main() -> int:
|
||||
if path != CENTRAL_WORKSPACE_LAYOUT and LOCAL_WORKSPACE_LAYOUT.search(text):
|
||||
errors.append(f"Module-local WorkspaceLayout definition is not allowed: {path}")
|
||||
|
||||
page_layout_count = len(PAGE_LAYOUT_USAGE.findall(text))
|
||||
semantic_layout_count = len(SEMANTIC_PAGE_LAYOUT_USAGE.findall(text))
|
||||
if page_layout_count and semantic_layout_count != page_layout_count:
|
||||
errors.append(
|
||||
"Every PageLayout must declare its semantic archetype immediately "
|
||||
f"after the component name: {path} ({semantic_layout_count}/{page_layout_count})"
|
||||
)
|
||||
if (
|
||||
page_layout_count
|
||||
and "actions=" in text
|
||||
and path not in HEADERLESS_PAGE_LAYOUT_CONSUMERS
|
||||
and path != pathlib.Path("govoplan-core/webui/src/components/admin/AdminPageLayout.tsx")
|
||||
and "<PageActionBar" not in text
|
||||
):
|
||||
errors.append(
|
||||
f"Headed page actions must use the semantic PageActionBar: {path}"
|
||||
)
|
||||
if "<PageActionBar" in text and "consequentialActions=" in text:
|
||||
errors.append(
|
||||
f"Ambiguous consequential action slots are forbidden; use destructiveActions: {path}"
|
||||
)
|
||||
|
||||
for path in REQUIRED_CONSUMERS:
|
||||
absolute_path = REPOS_ROOT / path
|
||||
if not absolute_path.exists():
|
||||
@@ -149,11 +192,39 @@ def main() -> int:
|
||||
if RAW_WORKSPACE.search(text):
|
||||
errors.append(f"Required shared-workspace consumer restored the raw workspace: {path}")
|
||||
|
||||
for path in EDITOR_PAGE_CONSUMERS:
|
||||
absolute_path = REPOS_ROOT / path
|
||||
if not absolute_path.exists():
|
||||
continue
|
||||
text = absolute_path.read_text(encoding="utf-8")
|
||||
for required in ('variant="editor"', "dirty=", "discardAction=", "saveAction="):
|
||||
if required not in text:
|
||||
errors.append(f"Editor page is missing {required}: {path}")
|
||||
if path.name != "CampaignDraftPageScaffold.tsx" and not any(
|
||||
guard in text
|
||||
for guard in (
|
||||
"useUnsavedDraftGuard",
|
||||
"useCampaignDraftEditor",
|
||||
"useRegisterUnsavedChanges",
|
||||
)
|
||||
):
|
||||
errors.append(f"Editor page is missing an unsaved-change guard: {path}")
|
||||
|
||||
for path in DESTRUCTIVE_PAGE_CONSUMERS:
|
||||
absolute_path = REPOS_ROOT / path
|
||||
if not absolute_path.exists():
|
||||
continue
|
||||
text = absolute_path.read_text(encoding="utf-8")
|
||||
if "destructiveActions=" not in text or 'variant="danger"' not in text:
|
||||
errors.append(f"Destructive page actions lost their separated slot: {path}")
|
||||
|
||||
core_index = REPOS_ROOT / "govoplan-core/webui/src/index.ts"
|
||||
if core_index.exists() and "PageLayout, PageHeader" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export PageLayout and PageHeader from @govoplan/core-webui.")
|
||||
if core_index.exists() and "WorkspaceLayout" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export WorkspaceLayout from @govoplan/core-webui.")
|
||||
if core_index.exists() and "PageActionBar" not in core_index.read_text(encoding="utf-8"):
|
||||
errors.append("Core must export PageActionBar from @govoplan/core-webui.")
|
||||
|
||||
if errors:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
@@ -162,6 +233,8 @@ def main() -> int:
|
||||
print(
|
||||
"Shared WebUI layout contract passed: "
|
||||
f"{len(REQUIRED_CONSUMERS)} central consumers, "
|
||||
f"{sum(len(PAGE_LAYOUT_USAGE.findall(text)) for text in source_text.values())} semantic pages, "
|
||||
f"{len(EDITOR_PAGE_CONSUMERS)} guarded editor consumers, "
|
||||
f"{len(raw_frames)} registered legacy page-frame files; "
|
||||
f"{len(REQUIRED_WORKSPACE_CONSUMERS)} workspace consumers, "
|
||||
f"{len(raw_workspaces)} registered legacy workspace files."
|
||||
|
||||
Reference in New Issue
Block a user