Add browser asset quality gate
This commit is contained in:
parent
2841042b9c
commit
5ccb336e84
14 changed files with 2898 additions and 16 deletions
131
tools/check_web_assets.py
Normal file
131
tools/check_web_assets.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Validate the exact embedded graph-browser HTML, CSS, and JavaScript."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from docforge.index import ProjectIndex
|
||||
from docforge.project import Project
|
||||
from docforge.rendering import RenderService
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "src" / "docforge" / "visualization.py"
|
||||
|
||||
|
||||
def _graph_browser_html() -> str:
|
||||
tree = ast.parse(SOURCE.read_text(encoding="utf-8"), filename=str(SOURCE))
|
||||
for statement in tree.body:
|
||||
if not isinstance(statement, ast.Assign):
|
||||
continue
|
||||
if any(
|
||||
isinstance(target, ast.Name) and target.id == "_GRAPH_BROWSER_HTML"
|
||||
for target in statement.targets
|
||||
):
|
||||
value = ast.literal_eval(statement.value)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
raise RuntimeError("Could not find the literal graph-browser HTML asset")
|
||||
|
||||
|
||||
def _embedded(html: str, tag: str) -> str:
|
||||
opening = f"<{tag}>"
|
||||
closing = f"</{tag}>"
|
||||
if html.count(opening) != 1 or html.count(closing) != 1:
|
||||
raise RuntimeError(f"Expected exactly one embedded {tag} asset")
|
||||
return html.split(opening, 1)[1].split(closing, 1)[0]
|
||||
|
||||
|
||||
def _rendered_manual_html() -> str:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = ROOT / "tests" / "fixtures" / "alpha"
|
||||
project_root = Path(directory) / "alpha"
|
||||
shutil.copytree(fixture, project_root)
|
||||
project = Project.open(project_root)
|
||||
ProjectIndex(project).build()
|
||||
result = RenderService(project).render("manual")
|
||||
output = result.get("output")
|
||||
if not isinstance(output, dict):
|
||||
raise RuntimeError("Manual render did not return output identity")
|
||||
relative_path = cast(dict[str, object], output).get("path")
|
||||
if not isinstance(relative_path, str):
|
||||
raise RuntimeError("Manual render did not return an output path")
|
||||
return (project_root / relative_path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _tool(name: str) -> str:
|
||||
suffix = ".cmd" if os.name == "nt" else ""
|
||||
path = ROOT / "node_modules" / ".bin" / f"{name}{suffix}"
|
||||
if not path.is_file():
|
||||
raise RuntimeError(f"Missing web validator {name}; run npm ci")
|
||||
return str(path)
|
||||
|
||||
|
||||
def _run(label: str, command: list[str], content: str) -> bool:
|
||||
print(f"Checking {label}...")
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=ROOT,
|
||||
input=content,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
html = _graph_browser_html()
|
||||
checks = (
|
||||
(
|
||||
"HTML",
|
||||
[
|
||||
_tool("html-validate"),
|
||||
"--stdin",
|
||||
"--stdin-filename=graph-browser.html",
|
||||
"--max-warnings=0",
|
||||
],
|
||||
html,
|
||||
),
|
||||
(
|
||||
"rendered manual HTML",
|
||||
[
|
||||
_tool("html-validate"),
|
||||
"--stdin",
|
||||
"--stdin-filename=rendered-manual.html",
|
||||
"--max-warnings=0",
|
||||
],
|
||||
_rendered_manual_html(),
|
||||
),
|
||||
(
|
||||
"CSS",
|
||||
[
|
||||
_tool("stylelint"),
|
||||
"--stdin",
|
||||
"--stdin-filename=graph-browser.css",
|
||||
"--max-warnings=0",
|
||||
],
|
||||
_embedded(html, "style"),
|
||||
),
|
||||
(
|
||||
"JavaScript",
|
||||
[
|
||||
_tool("eslint"),
|
||||
"--stdin",
|
||||
"--stdin-filename=graph-browser.js",
|
||||
"--max-warnings=0",
|
||||
],
|
||||
_embedded(html, "script"),
|
||||
),
|
||||
)
|
||||
results = [_run(label, command, content) for label, command, content in checks]
|
||||
return 0 if all(results) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue