Initial Commit
This commit is contained in:
115
dev_server.py
Normal file
115
dev_server.py
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import errno
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
WATCH_EXTENSIONS = {".html", ".css", ".js"}
|
||||
RELOAD_SNIPPET = """
|
||||
<script>
|
||||
(() => {
|
||||
const events = new EventSource("/__hotreload");
|
||||
events.addEventListener("reload", () => window.location.reload());
|
||||
})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
||||
class HotReloadHandler(SimpleHTTPRequestHandler):
|
||||
def end_headers(self):
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
super().end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/__hotreload":
|
||||
self.handle_hotreload()
|
||||
return
|
||||
|
||||
super().do_GET()
|
||||
|
||||
def translate_path(self, path):
|
||||
clean_path = path.split("?", 1)[0].split("#", 1)[0].lstrip("/")
|
||||
return str(ROOT / clean_path)
|
||||
|
||||
def send_head(self):
|
||||
path = Path(self.translate_path(self.path))
|
||||
if path.is_dir():
|
||||
path = path / "index.html"
|
||||
|
||||
if path.suffix == ".html" and path.exists():
|
||||
html = path.read_text(encoding="utf-8")
|
||||
html = html.replace("</body>", f"{RELOAD_SNIPPET}</body>")
|
||||
body = html.encode("utf-8")
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
return BytesIO(body)
|
||||
|
||||
return super().send_head()
|
||||
|
||||
def handle_hotreload(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Connection", "keep-alive")
|
||||
self.end_headers()
|
||||
|
||||
last_seen = latest_mtime()
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
current = latest_mtime()
|
||||
if current > last_seen:
|
||||
last_seen = current
|
||||
try:
|
||||
self.wfile.write(b"event: reload\ndata: changed\n\n")
|
||||
self.wfile.flush()
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
break
|
||||
|
||||
|
||||
def latest_mtime():
|
||||
latest = 0.0
|
||||
for directory, names, files in os.walk(ROOT):
|
||||
if ".git" in names:
|
||||
names.remove(".git")
|
||||
|
||||
for filename in files:
|
||||
path = Path(directory) / filename
|
||||
if path.suffix in WATCH_EXTENSIONS:
|
||||
latest = max(latest, path.stat().st_mtime)
|
||||
|
||||
return latest
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Run a tiny p5 dev server with hot reload.")
|
||||
parser.add_argument("--host", default="0.0.0.0")
|
||||
parser.add_argument("--port", type=int, default=8000)
|
||||
args = parser.parse_args()
|
||||
|
||||
server, port = create_server(args.host, args.port)
|
||||
print(f"Serving http://{args.host}:{port}", flush=True)
|
||||
print("Watching .html, .css, and .js files for changes", flush=True)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
def create_server(host, preferred_port):
|
||||
for port in range(preferred_port, preferred_port + 20):
|
||||
try:
|
||||
return ThreadingHTTPServer((host, port), HotReloadHandler), port
|
||||
except OSError as error:
|
||||
if error.errno != errno.EADDRINUSE:
|
||||
raise
|
||||
|
||||
raise OSError(f"No available port found from {preferred_port} to {preferred_port + 19}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user