125 lines
5.0 KiB
Python
125 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Endpoints returning binary (blob) instead of JSON. These cannot use handleApiResponse
|
|
# or parseApiJsonResponse. Add path segment or exact path that matches api.fetchApi URL.
|
|
# - "download": matches /meld/api/download/zip, /meld/api/download/raw (ZIP/image binary)
|
|
BLOB_ENDPOINTS = ("download",)
|
|
|
|
|
|
def get_files_to_check() -> list[str]:
|
|
"""Search for .ts and .tsx files using api.fetchApi or .json()"""
|
|
base = Path("ui/src")
|
|
files = []
|
|
for ext in ("*.ts", "*.tsx"):
|
|
for p in base.rglob(ext):
|
|
if p.name.endswith(".d.ts"):
|
|
continue
|
|
# Skip test files and mocks
|
|
posix = str(p).replace("\\", "/")
|
|
if (
|
|
"/test/" in posix
|
|
or "/__mocks__/" in posix
|
|
or p.name.endswith(".test.ts")
|
|
or p.name.endswith(".test.tsx")
|
|
):
|
|
continue
|
|
text = p.read_text(encoding="utf-8", errors="replace")
|
|
if "api.fetchApi" in text or (".json()" in text and "api" in text):
|
|
files.append(posix)
|
|
return sorted(set(files))
|
|
|
|
|
|
def check_frontend_api_usage() -> int:
|
|
files = get_files_to_check()
|
|
errors = []
|
|
|
|
for file_path in files:
|
|
if not os.path.isfile(file_path):
|
|
continue
|
|
|
|
with open(file_path, encoding="utf-8") as f:
|
|
lines = f.read().splitlines()
|
|
|
|
for i, line in enumerate(lines):
|
|
# Inline ignore
|
|
if "frontend-api-check-ignore" in line:
|
|
continue
|
|
|
|
# Exclude handleResponse/parseJsonResponse/handleApiResponse/parseApiJsonResponse definition itself
|
|
if "export" in line:
|
|
import re
|
|
|
|
if re.search(r"\bexport\s+(const|let|var|function|default|type)\b.*\bhandleResponse\b", line):
|
|
continue
|
|
if re.search(r"\bexport\s+(const|let|var|function|default|type)\b.*\bparseJsonResponse\b", line):
|
|
continue
|
|
if re.search(r"\bexport\s+(const|let|var|function|default|type)\b.*\bhandleApiResponse\b", line):
|
|
continue
|
|
if re.search(r"\bexport\s+(const|let|var|function|default|type)\b.*\bparseApiJsonResponse\b", line):
|
|
continue
|
|
|
|
# Rule 1: Check .json() usage
|
|
if ".json()" in line:
|
|
# In ui/src/api.ts, .json() is allowed (implementation of handleResponse/parseJsonResponse)
|
|
if file_path.endswith("ui/src/api.ts"):
|
|
continue
|
|
|
|
errors.append(
|
|
f"{file_path}:{i + 1}: Forbidden direct .json() call. Use handleApiResponse(res) or parseApiJsonResponse(res) instead."
|
|
)
|
|
|
|
# Rule 2: If api.fetchApi is used, one of the response handlers should be used
|
|
if "api.fetchApi" in line:
|
|
found_handle = False
|
|
for j in range(i, min(i + 15, len(lines))):
|
|
if (
|
|
"handleResponse" in lines[j]
|
|
or "parseJsonResponse" in lines[j]
|
|
or "handleApiResponse" in lines[j]
|
|
or "parseApiJsonResponse" in lines[j]
|
|
):
|
|
found_handle = True
|
|
break
|
|
|
|
if not found_handle:
|
|
import re
|
|
|
|
# Extract the URL from api.fetchApi(URL, ...) - handle multiline
|
|
buffer = " ".join(lines[i : min(i + 5, len(lines))])
|
|
url_match = re.search(r'api\.fetchApi\(\s*["\'`]([^"\'`]+)["\'`]', buffer)
|
|
url = url_match.group(1) if url_match else None
|
|
|
|
if url:
|
|
# Check against BLOB_ENDPOINTS strictly (exact match or path segment)
|
|
if any(url == b or url.startswith(f"{b}/") or f"/{b}/" in f"/{url}/" for b in BLOB_ENDPOINTS):
|
|
continue
|
|
|
|
# Determine error message: use extracted URL if available, otherwise check line
|
|
is_meld = (url and "/meld/" in f"/{url}/") or any(
|
|
q in line for q in ['"/meld/', "'/meld/", "`/meld/"]
|
|
)
|
|
if is_meld:
|
|
errors.append(
|
|
f"{file_path}:{i + 1}: api.fetchApi call to /meld/ should use handleApiResponse(res) or parseApiJsonResponse(res)."
|
|
)
|
|
else:
|
|
errors.append(
|
|
f"{file_path}:{i + 1}: api.fetchApi call should use handleApiResponse or parseApiJsonResponse, or add to BLOB_ENDPOINTS if binary."
|
|
)
|
|
|
|
if errors:
|
|
print("Frontend API Usage Checks Failed:")
|
|
for error in errors:
|
|
print(f" {error}")
|
|
return 1
|
|
|
|
print("Frontend API Usage Checks Passed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(check_frontend_api_usage())
|