Updated frontend running dev mode using deno/npm/bun as well. Additional dev mode Caddyfile to go https://localhost/.

This commit is contained in:
2025-12-01 20:07:26 +00:00
parent 4f50974222
commit 2dca6b1eec
2 changed files with 79 additions and 21 deletions
+10
View File
@@ -0,0 +1,10 @@
localhost {
# Forwards API by caddy, bypassing the Vite dev proxy
# Avoids bug https://github.com/oven-sh/bun/issues/9882
handle /api/* {
reverse_proxy :4402 # directly to backend
}
handle {
reverse_proxy :4403 # vite dev server
}
}
+69 -21
View File
@@ -1,8 +1,36 @@
import atexit
import shutil
import signal
import subprocess
from importlib import resources from importlib import resources
from pathlib import Path from pathlib import Path
from sys import stderr
from threading import Thread
__all__ = ["path", "file", "run_dev"] __all__ = ["path", "file", "run_dev"]
NO_FRONTEND_TOOL = """\
┃ ⚠️ deno, npm or bunx needed to run the frontend server.
"""
BUN_BUG = """\
┃ ⚠️ Bun cannot correctly proxy API requests to the backend.
┃ Bug report: https://github.com/oven-sh/bun/issues/9882
┃ Options:
┃ - sudo caddy run --config caddy/Caddyfile.dev
┃ - Install deno or npm instead
┃ Caddy will skip the Vite for API calls and serve everything at port 443.
┃ Otherwise Vite serves at port 8077 and proxies to backend (broken with bun).
"""
NO_FRONTEND = """\
┃ Note: only static build of the frontend is served at port 8078.
┃ The page will not update with frontend code changes.
"""
def _resolve_static_dir() -> Path: def _resolve_static_dir() -> Path:
# Try packaged path via importlib.resources (works for wheel/installed). # Try packaged path via importlib.resources (works for wheel/installed).
@@ -26,12 +54,7 @@ def file(*parts: str) -> Path:
def run_dev(): def run_dev():
"""Spawn the frontend dev server (bun or npm) as a background process.""" """Spawn the frontend dev server (deno, npm, or bunx) as a background process."""
import atexit
import shutil
import signal
import subprocess
devpath = Path(__file__).parent.parent.parent / "frontend" devpath = Path(__file__).parent.parent.parent / "frontend"
if not (devpath / "package.json").exists(): if not (devpath / "package.json").exists():
raise RuntimeError( raise RuntimeError(
@@ -39,22 +62,47 @@ def run_dev():
if "site-packages" in devpath.parts if "site-packages" in devpath.parts
else f"Frontend source code not found at {devpath}" else f"Frontend source code not found at {devpath}"
) )
bun = shutil.which("bun")
npm = shutil.which("npm") if bun is None else None
if not bun and not npm:
raise RuntimeError("Neither bun nor npm found on PATH for dev server")
cmd: list[str] = [bun, "--bun", "run", "dev"] if bun else [npm, "run", "dev"] # type: ignore[list-item]
proc = subprocess.Popen(cmd, cwd=str(devpath))
def _terminate(): options = [
if proc.poll() is None: ("deno", "run", "dev"),
proc.terminate() ("npm", "run", "dev", "--"),
("bunx", "--bun", "vite"),
]
cmd = None
tool_name = None
for option in options:
if tool := shutil.which(option[0]):
cmd = [tool, *option[1:]]
tool_name = option[0]
break
atexit.register(_terminate) vite_process = None
def _signal_handler(signum, frame): def start_vite():
_terminate() nonlocal vite_process
raise SystemExit(0) if cmd is None:
stderr.write(NO_FRONTEND_TOOL)
stderr.write(NO_FRONTEND)
return
assert tool_name is not None
try:
if tool_name == "bunx":
stderr.write(BUN_BUG)
for sig in (signal.SIGINT, signal.SIGTERM): stderr.write(f">>> {' '.join([tool_name, *cmd[1:]])}\n")
signal.signal(sig, _signal_handler) vite_process = subprocess.Popen(cmd, cwd=str(devpath), shell=False)
except Exception as e:
stderr.write(f"┃ ⚠️ Vite couldn't start: {e}\n")
stderr.write(NO_FRONTEND)
def cleanup():
vite_process.terminate()
vite_process.wait()
# Start Vite in a separate thread
vite_thread = Thread(target=start_vite, daemon=True)
vite_thread.start()
atexit.register(cleanup)
signal.signal(signal.SIGTERM, lambda *_: cleanup())
signal.signal(signal.SIGINT, lambda *_: cleanup())