added live rebuilding to preview server

This commit is contained in:
Daniel Fichtinger 2025-06-29 23:27:15 -04:00
parent 5cfe684c3b
commit c792a6bb07
4 changed files with 121 additions and 83 deletions

View file

@ -14,11 +14,8 @@ def build(root: Path | None = None, output: Path | None = None):
@app.command()
def serve(dir: Path = Path("public")):
if not dir.exists():
raise typer.BadParameter(f"Directory {dir} does not exist.")
typer.echo(f"Serving files from {dir}")
server.serve(dir)
def serve(root: Path | None = None, output: Path | None = None):
server.serve(root, output)
@app.command()

View file

@ -1,13 +1,100 @@
from starlette.applications import Starlette
from starlette.staticfiles import StaticFiles
import typer
import uvicorn
import signal
import os
import sys
from types import FrameType
from rich import print
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
import threading
from typing import override
from watchdog.observers import Observer
from zona.builder import ZonaBuilder
from watchdog.events import FileSystemEventHandler, FileSystemEvent
from pathlib import Path
def serve(dir: Path, host: str = "localhost", port: int = 8000):
app = Starlette()
app.mount("/", StaticFiles(directory=dir, html=True), name="zona")
class QuietHandler(SimpleHTTPRequestHandler):
@override
def log_message(self, format, *args): # type: ignore
pass
typer.echo(f"Preview server running at http://{host}:{port}")
uvicorn.run(app, host=host, port=port, log_level="warning")
class ZonaServer(ThreadingHTTPServer):
@override
def handle_error(self, request, client_address): # type: ignore
_, exc_value = sys.exc_info()[:2]
if not isinstance(
exc_value, (BrokenPipeError, ConnectionResetError)
):
super().handle_error(request, client_address)
class ZonaReloadHandler(FileSystemEventHandler):
def __init__(self, builder: ZonaBuilder, output: Path):
self.builder: ZonaBuilder = builder
self.output: Path = output.resolve()
def _should_ignore(self, event: FileSystemEvent) -> bool:
path = Path(str(event.src_path)).resolve()
return (
self.output in path.parents
or path == self.output
or event.is_directory
)
@override
def on_modified(self, event: FileSystemEvent):
if not self._should_ignore(event):
print(f"Modified: {event.src_path}, rebuilding...")
self.builder.build()
@override
def on_created(self, event: FileSystemEvent):
if not self._should_ignore(event):
print(f"Modified: {event.src_path}, rebuilding...")
self.builder.build()
def run_http_server(
dir: Path, host: str = "localhost", port: int = 8000
):
os.chdir(dir)
handler = QuietHandler
httpd = ZonaServer(
server_address=(host, port), RequestHandlerClass=handler
)
print(f"Serving {dir} at http://{host}:{port}")
print(f"Exit with <c-c>")
httpd.serve_forever()
def serve(
root: Path | None = None,
output: Path | None = None,
host: str = "localhost",
port: int = 8000,
):
builder = ZonaBuilder(root, output)
builder.build()
if output is None:
output = builder.layout.output
if root is None:
root = builder.layout.root
server_thread = threading.Thread(
target=run_http_server, args=(output, host, port), daemon=True
)
server_thread.start()
event_handler = ZonaReloadHandler(builder, output)
observer = Observer()
observer.schedule(event_handler, path=str(root), recursive=True)
observer.start()
def shutdown_handler(_a: int, _b: FrameType | None):
print("Shutting down...")
observer.stop()
signal.signal(signal.SIGINT, shutdown_handler)
signal.signal(signal.SIGTERM, shutdown_handler)
observer.join()