feat: add frontend architecture rules FE-001 to FE-004
Add rules to enforce consistent frontend component usage: - FE-001 (warning): Use pagination macro instead of inline HTML - FE-002 (warning): Use $icon() helper instead of inline SVGs - FE-003 (info): Use table macros for consistent styling - FE-004 (info): Use form macros for consistent styling Update validate_architecture.py to check FE-001 and FE-002 anti-patterns in templates, with exceptions for macro definitions and the components showcase page. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -600,6 +600,92 @@ template_rules:
|
||||
file_pattern: "app/templates/**/*.html"
|
||||
recommended_pattern: '<template x-if="items.length === 0">No items</template>'
|
||||
|
||||
# ============================================================================
|
||||
# FRONTEND COMPONENT RULES
|
||||
# ============================================================================
|
||||
|
||||
frontend_component_rules:
|
||||
|
||||
- id: "FE-001"
|
||||
name: "Use pagination macro instead of inline HTML"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Use the shared pagination macro instead of duplicating pagination HTML.
|
||||
Import from shared/macros/pagination.html.
|
||||
|
||||
WRONG (inline pagination):
|
||||
<div class="grid px-4 py-3 text-xs font-semibold...">
|
||||
<span class="flex items-center col-span-3">
|
||||
Showing ...
|
||||
</span>
|
||||
<nav aria-label="Table navigation">...
|
||||
|
||||
RIGHT (use macro):
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
{{ pagination() }}
|
||||
pattern:
|
||||
file_pattern: "app/templates/**/*.html"
|
||||
anti_patterns:
|
||||
- 'aria-label="Table navigation"'
|
||||
- "previousPage\\(\\).*nextPage\\(\\)"
|
||||
exceptions:
|
||||
- "shared/macros/pagination.html"
|
||||
- "components.html" # Showcase page
|
||||
|
||||
- id: "FE-002"
|
||||
name: "Use $icon() helper instead of inline SVGs"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Use the Alpine.js $icon() helper for consistent iconography.
|
||||
Do not use inline <svg> elements.
|
||||
|
||||
WRONG (inline SVG):
|
||||
<svg class="w-4 h-4" viewBox="0 0 20 20">...</svg>
|
||||
|
||||
RIGHT (icon helper):
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4')"></span>
|
||||
pattern:
|
||||
file_pattern: "app/templates/**/*.html"
|
||||
anti_patterns:
|
||||
- "<svg.*viewBox.*>.*</svg>"
|
||||
exceptions:
|
||||
- "base.html" # Base templates may define SVG sprites
|
||||
- "components.html" # Showcase page
|
||||
- "shared/macros/" # Macros may contain SVGs for compatibility
|
||||
|
||||
- id: "FE-003"
|
||||
name: "Use table macros for consistent table styling"
|
||||
severity: "info"
|
||||
description: |
|
||||
Use the shared table macros for consistent table styling.
|
||||
Import from shared/macros/tables.html.
|
||||
|
||||
Recommended macros:
|
||||
- table_wrapper() - Wraps table with overflow and shadow
|
||||
- table_header(['Column1', 'Column2']) - Consistent header styling
|
||||
- table_empty_state(colspan, 'icon', 'Title', 'Message') - Empty states
|
||||
pattern:
|
||||
file_pattern: "app/templates/**/*.html"
|
||||
encouraged_patterns:
|
||||
- "{% from 'shared/macros/tables.html' import"
|
||||
|
||||
- id: "FE-004"
|
||||
name: "Use form macros for consistent form styling"
|
||||
severity: "info"
|
||||
description: |
|
||||
Use the shared form macros for consistent input styling and validation.
|
||||
Import from shared/macros/forms.html.
|
||||
|
||||
Recommended macros:
|
||||
- form_input(label, name, x_model, ...) - Text inputs with validation
|
||||
- form_select(label, x_model, options) - Select dropdowns
|
||||
- form_textarea(label, x_model, rows) - Textareas
|
||||
- form_checkbox(label, x_model) - Checkboxes
|
||||
pattern:
|
||||
file_pattern: "app/templates/**/*.html"
|
||||
encouraged_patterns:
|
||||
- "{% from 'shared/macros/forms.html' import"
|
||||
|
||||
# ============================================================================
|
||||
# FRONTEND STYLING RULES
|
||||
# ============================================================================
|
||||
|
||||
@@ -298,6 +298,7 @@ class ArchitectureValidator:
|
||||
self._check_no_http_exception_in_services(file_path, content, lines)
|
||||
self._check_service_exceptions(file_path, content, lines)
|
||||
self._check_db_session_parameter(file_path, content, lines)
|
||||
self._check_no_commit_in_services(file_path, content, lines)
|
||||
|
||||
# Models
|
||||
elif "/app/models/" in file_path_str or "\\app\\models\\" in file_path_str:
|
||||
@@ -372,15 +373,35 @@ class ArchitectureValidator:
|
||||
"""Validate a single HTML template file"""
|
||||
print("📄 Validating template...")
|
||||
|
||||
# Skip base template and partials
|
||||
if "base.html" in file_path.name or "partials" in str(file_path):
|
||||
file_path_str = str(file_path)
|
||||
|
||||
# Skip base template and partials for extends check
|
||||
is_base_or_partial = "base.html" in file_path.name or "partials" in file_path_str
|
||||
|
||||
# Skip macros directory for FE rules
|
||||
is_macro = "shared/macros/" in file_path_str or "shared\\macros\\" in file_path_str
|
||||
|
||||
# Skip components showcase page
|
||||
is_components_page = "components.html" in file_path.name
|
||||
|
||||
if is_base_or_partial:
|
||||
print("⏭️ Skipping base/partial template")
|
||||
elif is_macro:
|
||||
print("⏭️ Skipping macro file")
|
||||
else:
|
||||
# FE-001: Check for inline pagination (should use macro)
|
||||
if not is_components_page:
|
||||
self._check_pagination_macro_usage(file_path, content, lines)
|
||||
|
||||
# FE-002: Check for inline SVGs (should use $icon())
|
||||
if not is_components_page and not is_macro:
|
||||
self._check_icon_helper_usage(file_path, content, lines)
|
||||
|
||||
# Only check admin templates for extends
|
||||
if "/admin/" not in file_path_str and "\\admin\\" not in file_path_str:
|
||||
return
|
||||
|
||||
# Only check admin templates
|
||||
file_path_str = str(file_path)
|
||||
if "/admin/" not in file_path_str and "\\admin\\" not in file_path_str:
|
||||
print("⏭️ Not an admin template, skipping extends check")
|
||||
if is_base_or_partial:
|
||||
return
|
||||
|
||||
# Check for standalone marker in template (first 5 lines)
|
||||
@@ -419,6 +440,67 @@ class ArchitectureValidator:
|
||||
suggestion="Add {% extends 'admin/base.html' %} at the top, or add {# standalone #} if intentional",
|
||||
)
|
||||
|
||||
def _check_pagination_macro_usage(self, file_path: Path, content: str, lines: list[str]):
|
||||
"""FE-001: Check for inline pagination that should use macro"""
|
||||
# Look for signs of inline pagination
|
||||
pagination_indicators = [
|
||||
('aria-label="Table navigation"', "Inline table navigation found"),
|
||||
("previousPage()" , "Inline pagination controls found"),
|
||||
("nextPage()" , "Inline pagination controls found"),
|
||||
("goToPage(" , "Inline pagination controls found"),
|
||||
]
|
||||
|
||||
# Check if already using the pagination macro
|
||||
uses_macro = any("from 'shared/macros/pagination.html'" in line for line in lines)
|
||||
if uses_macro:
|
||||
return
|
||||
|
||||
for i, line in enumerate(lines, 1):
|
||||
for pattern, message in pagination_indicators:
|
||||
if pattern in line:
|
||||
# Skip if it's in a comment
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("{#") or stripped.startswith("<!--"):
|
||||
continue
|
||||
|
||||
self._add_violation(
|
||||
rule_id="FE-001",
|
||||
rule_name="Use pagination macro",
|
||||
severity=Severity.WARNING,
|
||||
file_path=file_path,
|
||||
line_number=i,
|
||||
message=message + " - use shared macro instead",
|
||||
context=stripped[:60],
|
||||
suggestion="{% from 'shared/macros/pagination.html' import pagination %}\n{{ pagination() }}",
|
||||
)
|
||||
return # Only report once per file
|
||||
|
||||
def _check_icon_helper_usage(self, file_path: Path, content: str, lines: list[str]):
|
||||
"""FE-002: Check for inline SVGs that should use $icon() helper"""
|
||||
# Pattern to find inline SVGs
|
||||
svg_pattern = re.compile(r'<svg[^>]*viewBox[^>]*>.*?</svg>', re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# Find all SVG occurrences
|
||||
for match in svg_pattern.finditer(content):
|
||||
# Find line number
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
|
||||
# Skip if this is likely in a code example (inside <pre> or <code>)
|
||||
context_before = content[max(0, match.start()-200):match.start()]
|
||||
if '<pre' in context_before or '<code' in context_before:
|
||||
continue
|
||||
|
||||
self._add_violation(
|
||||
rule_id="FE-002",
|
||||
rule_name="Use $icon() helper",
|
||||
severity=Severity.WARNING,
|
||||
file_path=file_path,
|
||||
line_number=line_num,
|
||||
message="Inline SVG found - use $icon() helper for consistency",
|
||||
context="<svg...>",
|
||||
suggestion='<span x-html="$icon(\'icon-name\', \'w-4 h-4\')"></span>',
|
||||
)
|
||||
|
||||
def _validate_api_endpoints(self, target_path: Path):
|
||||
"""Validate API endpoint rules (API-001, API-002, API-003, API-004)"""
|
||||
print("📡 Validating API endpoints...")
|
||||
@@ -974,6 +1056,7 @@ class ArchitectureValidator:
|
||||
"exception_rules",
|
||||
"javascript_rules",
|
||||
"template_rules",
|
||||
"frontend_component_rules",
|
||||
]:
|
||||
rules = self.config.get(category, [])
|
||||
for rule in rules:
|
||||
|
||||
Reference in New Issue
Block a user