feat(static-assets): cache-bust JS/CSS via ?v=<commit-sha>, immutable in prod
All checks were successful
CI / ruff (push) Successful in 18s
CI / pytest (push) Successful in 2h50m43s
CI / validate (push) Successful in 33s
CI / dependency-scanning (push) Successful in 33s
CI / docs (push) Successful in 50s
CI / deploy (push) Successful in 1m15s

Adds a `static_v(request, name, path=...)` Jinja helper that appends
?v=<commit-sha> from app.core.build_info, plus a CachedStaticFiles
subclass that serves Cache-Control: public, max-age=31536000, immutable
in production and no-cache in development. Browsers refetch JS/CSS
automatically on every deploy without the user having to hard-reload.

- New: app/core/static_files.py (CachedStaticFiles)
- Updated: app/templates_config.py (static_v helper)
- Updated: main.py (use CachedStaticFiles for *_static mounts)
- Codemod: 143 url_for('*_static', path='*.js'|'*.css') → static_v(...)
  across 123 templates. Images/fonts/JSON locales intentionally
  unchanged (out of scope).
- Arch rule: FE-024 (warning) flags raw url_for on JS/CSS to prevent
  drift. Note: FE-008 was already taken by the number_stepper rule.
- docs/proposals/static-asset-cache-busting.md marked Done.

Closes plan from docs/proposals/static-asset-cache-busting.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 19:35:59 +02:00
parent 236fee015e
commit 54247ca4f0
129 changed files with 281 additions and 146 deletions

View File

@@ -867,6 +867,10 @@ class ArchitectureValidator:
if not is_components_page and not is_macro:
self._check_number_stepper_macro_usage(file_path, content, lines)
# FE-024: Check for raw url_for(...) on .js/.css (should use static_v)
if not is_macro:
self._check_static_v_usage(file_path, content, lines)
# TPL-004: Check x-text usage for dynamic content
self._check_xtext_usage(file_path, content, lines)
@@ -1793,6 +1797,40 @@ class ArchitectureValidator:
)
return # Only report once per file
_STATIC_V_PATTERN = re.compile(
r"""url_for\(\s*['"]\w+_static['"]\s*,\s*path=['"][^'"]+\.(?:js|css)['"]\s*\)""",
re.IGNORECASE,
)
def _check_static_v_usage(
self, file_path: Path, content: str, lines: list[str]
):
"""FE-024: Flag raw url_for() on .js/.css URLs — must use static_v() instead.
The static_v() Jinja helper appends ?v=<commit-sha> from build_info so
browsers refetch JS/CSS automatically after each deploy.
Skip lines with `noqa: FE-024` or `noqa: FE024`.
"""
for i, line in enumerate(lines, 1):
if not self._STATIC_V_PATTERN.search(line):
continue
if "noqa" in line.lower() and (
"fe-024" in line.lower() or "fe024" in line.lower()
):
continue
stripped = line.strip()
self._add_violation(
rule_id="FE-024",
rule_name="Use static_v() for JS/CSS, not raw url_for()",
severity=Severity.WARNING,
file_path=file_path,
line_number=i,
message="Raw url_for() on JS/CSS — browsers won't refetch after deploy",
context=stripped[:80],
suggestion="Replace url_for(...) with static_v(request, ...) so the URL carries ?v=<commit>",
)
def _validate_api_endpoints(self, target_path: Path):
"""Validate API endpoint rules (API-001, API-002, API-003, API-004)"""
print("📡 Validating API endpoints...")
@@ -3508,6 +3546,10 @@ class ArchitectureValidator:
if not is_base_or_partial and not is_macro and not is_components_page:
self._check_number_stepper_macro_usage(file_path, content, lines)
# FE-024: Check for raw url_for(...) on .js/.css (should use static_v)
if not is_base_or_partial and not is_macro:
self._check_static_v_usage(file_path, content, lines)
# TPL-008: Check for call table_header() pattern
self._check_table_header_call_pattern(file_path, content, lines)