From 74a77233008494fe425b501e87c5b7cb5adb8a46 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Mon, 1 Dec 2025 20:07:26 +0000 Subject: [PATCH] Updated frontend running dev mode using deno/npm/bun as well. Additional dev mode Caddyfile to go https://localhost/. --- caddy/Caddyfile.dev | 10 +++++ passkey/util/frontend.py | 90 ++++++++++++++++++++++++++++++---------- 2 files changed, 79 insertions(+), 21 deletions(-) create mode 100644 caddy/Caddyfile.dev diff --git a/caddy/Caddyfile.dev b/caddy/Caddyfile.dev new file mode 100644 index 0000000..2c87a6f --- /dev/null +++ b/caddy/Caddyfile.dev @@ -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 + } +} diff --git a/passkey/util/frontend.py b/passkey/util/frontend.py index 13e9a06..b3c2a7d 100644 --- a/passkey/util/frontend.py +++ b/passkey/util/frontend.py @@ -1,8 +1,36 @@ +import atexit +import shutil +import signal +import subprocess from importlib import resources from pathlib import Path +from sys import stderr +from threading import Thread __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: # Try packaged path via importlib.resources (works for wheel/installed). @@ -26,12 +54,7 @@ def file(*parts: str) -> Path: def run_dev(): - """Spawn the frontend dev server (bun or npm) as a background process.""" - import atexit - import shutil - import signal - import subprocess - + """Spawn the frontend dev server (deno, npm, or bunx) as a background process.""" devpath = Path(__file__).parent.parent.parent / "frontend" if not (devpath / "package.json").exists(): raise RuntimeError( @@ -39,22 +62,47 @@ def run_dev(): if "site-packages" in devpath.parts 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(): - if proc.poll() is None: - proc.terminate() + options = [ + ("deno", "run", "dev"), + ("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): - _terminate() - raise SystemExit(0) + def start_vite(): + nonlocal vite_process + 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): - signal.signal(sig, _signal_handler) + stderr.write(f">>> {' '.join([tool_name, *cmd[1:]])}\n") + 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())