#!/usr/bin/env python3 """Serveur de previsualisation locale (sert src/), avec rechargement automatique, sauvegarde et export depuis l'editeur visuel (editor.html). Usage: python3 scripts/preview.py [port] """ import http.server import json import shutil import sys from pathlib import Path PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8000 SCRIPT_DIR = Path(__file__).resolve().parent PROJECT_ROOT = SCRIPT_DIR.parent SRC_DIR = PROJECT_ROOT / "src" OUTPUT_DIR = PROJECT_ROOT / "output" WATCHED_FILES = ["index.html", "style.css", "app.js", "config.json"] EXPORTED_FILES = ["index.html", "style.css", "app.js", "config.json"] RELOAD_SNIPPET = b""" """ def watched_mtime(): mtimes = [] for name in WATCHED_FILES: try: mtimes.append((SRC_DIR / name).stat().st_mtime) except OSError: pass return str(max(mtimes)) if mtimes else "0" class ReloadingHandler(http.server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=str(SRC_DIR), **kwargs) def do_GET(self): if self.path == "/__mtime": body = watched_mtime().encode() self.send_response(200) self.send_header("Content-Type", "text/plain") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) return if self.path in ("/", "/index.html"): try: html = (SRC_DIR / "index.html").read_bytes() except OSError: return super().do_GET() html = html.replace(b"", RELOAD_SNIPPET) self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(html))) self.end_headers() self.wfile.write(html) return super().do_GET() def do_POST(self): if self.path == "/__save": length = int(self.headers.get("Content-Length", 0)) raw = self.rfile.read(length) try: data = json.loads(raw) except json.JSONDecodeError as exc: self.send_error(400, f"JSON invalide: {exc}") return with open(SRC_DIR / "config.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) f.write("\n") self._send_json({"ok": True}) return if self.path == "/__export": OUTPUT_DIR.mkdir(parents=True, exist_ok=True) copied = [] for name in EXPORTED_FILES: src = SRC_DIR / name if src.exists(): shutil.copy(src, OUTPUT_DIR / name) copied.append(name) self._send_json({"ok": True, "files": copied, "dir": str(OUTPUT_DIR)}) return self.send_error(404) def _send_json(self, payload): body = json.dumps(payload).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def end_headers(self): # Empeche le navigateur de mettre en cache src/*, sinon un simple # rechargement (ou le reload automatique) peut afficher une version # perimee et forcer a faire Ctrl+F5. self.send_header("Cache-Control", "no-store, must-revalidate") super().end_headers() def log_message(self, format, *args): if args and "__mtime" in str(args[0]): return super().log_message(format, *args) if __name__ == "__main__": with http.server.ThreadingHTTPServer(("127.0.0.1", PORT), ReloadingHandler) as httpd: print(f"Apercu : http://localhost:{PORT}/") print(f"Editeur : http://localhost:{PORT}/editor.html") print("Ctrl+C pour arreter.") try: httpd.serve_forever() except KeyboardInterrupt: pass