"""Custom hatchling build hook: build the React frontend if it is missing or stale. This makes NVCurve installable with a single command, e.g.:: uv tool install "git+https://gitea.zephyre.one/Pakobbix/nvcurve.git" The hook runs inside the isolated build environment right before the wheel (or sdist) is assembled. If ``frontend/dist`` does not exist yet — or is older than the frontend sources — it compiles the frontend using the host's ``npm`` (PATH is inherited from the environment). """ from __future__ import annotations import os import shutil import subprocess import sys from hatchling.builders.hooks.plugin.interface import BuildHookInterface # Frontend inputs that must be newer than dist/index.html to trigger a rebuild. _FRONTEND_INPUTS = ( "src", "index.html", "vite.config.ts", "package.json", "tsconfig.json", ) class FrontendBuildHook(BuildHookInterface): """Build ``frontend/dist`` with npm when it is missing or stale.""" PLUGIN_NAME = "custom" def initialize(self, version: str, build_data: dict) -> None: frontend = os.path.join(self.root, "frontend") dist_index = os.path.join(frontend, "dist", "index.html") if not self._needs_build(frontend, dist_index): return npm = shutil.which("npm") if npm is None: raise RuntimeError( "npm not found on PATH. Node.js 18+ and npm are required to build " "the NVCurve frontend. Install them and retry, or use install.sh " "which checks prerequisites for you." ) print( "[nvcurve] frontend/dist missing or stale — building frontend with npm ...", file=sys.stderr, ) subprocess.run([npm, "ci", "--no-audit", "--no-fund"], cwd=frontend, check=True) subprocess.run([npm, "run", "build"], cwd=frontend, check=True) @staticmethod def _needs_build(frontend: str, dist_index: str) -> bool: if not os.path.isfile(dist_index): return True dist_mtime = os.path.getmtime(dist_index) for name in _FRONTEND_INPUTS: path = os.path.join(frontend, name) if os.path.isfile(path): if os.path.getmtime(path) > dist_mtime: return True elif os.path.isdir(path): for root, _dirs, files in os.walk(path): for file in files: if os.path.getmtime(os.path.join(root, file)) > dist_mtime: return True return False