Upgrade fastapi-vue-setup.
This commit is contained in:
@@ -1,17 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* FastAPI-Vue Vite Plugin
|
* FastAPI-Vue Vite Plugin
|
||||||
|
* auto-upgrade@fastapi-vue-setup -- remove this if you edit the plugin
|
||||||
*
|
*
|
||||||
* Configures Vite for FastAPI backend integration:
|
* Configures Vite for FastAPI backend integration:
|
||||||
* - Proxies /api/* requests to the FastAPI backend
|
* - Proxies /api/* requests to the FastAPI backend
|
||||||
* - Builds to the Python module's frontend-build directory
|
* - Builds to the Python module's frontend-build directory
|
||||||
*
|
*
|
||||||
* Environment variables (with defaults):
|
* Options:
|
||||||
* FASTAPI_VUE_BACKEND_URL=http://localhost:5180 - Backend API URL for proxying
|
* paths - Array of paths to proxy (default: ["/api"])
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const backendUrl = process.env.FASTAPI_VUE_BACKEND_URL || "http://localhost:5180"
|
|
||||||
|
|
||||||
export default function fastapiVue({ paths = ["/api"] } = {}) {
|
export default function fastapiVue({ paths = ["/api"] } = {}) {
|
||||||
|
const backendUrl = process.env.PASKIA_BACKEND_URL || "http://localhost:4402"
|
||||||
|
|
||||||
// Build proxy configuration for each path
|
// Build proxy configuration for each path
|
||||||
const proxy = {}
|
const proxy = {}
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
@@ -23,7 +24,7 @@ export default function fastapiVue({ paths = ["/api"] } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "fastapi-vite",
|
name: "vite-plugin-fastapi-paskia",
|
||||||
config: () => ({
|
config: () => ({
|
||||||
server: { proxy },
|
server: { proxy },
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from paskia.util import startupbox
|
|||||||
from paskia.util.hostutil import normalize_origin
|
from paskia.util.hostutil import normalize_origin
|
||||||
|
|
||||||
DEFAULT_PORT = 4401
|
DEFAULT_PORT = 4401
|
||||||
|
DEVMODE = bool(os.getenv("PASKIA_FRONTEND_URL"))
|
||||||
|
|
||||||
EPILOG = """\
|
EPILOG = """\
|
||||||
Example:
|
Example:
|
||||||
@@ -167,14 +168,12 @@ def main():
|
|||||||
|
|
||||||
startupbox.print_startup_config(config)
|
startupbox.print_startup_config(config)
|
||||||
|
|
||||||
devmode = bool(os.environ.get("FASTAPI_VUE_FRONTEND_URL"))
|
|
||||||
|
|
||||||
run_kwargs: dict = {
|
run_kwargs: dict = {
|
||||||
"log_level": "warning", # Suppress startup messages; we use custom logging
|
"log_level": "warning", # Suppress startup messages; we use custom logging
|
||||||
"access_log": False, # We use custom AccessLogMiddleware instead
|
"access_log": False, # We use custom AccessLogMiddleware instead
|
||||||
}
|
}
|
||||||
|
|
||||||
if devmode:
|
if DEVMODE:
|
||||||
# Security: dev mode must run on localhost:4402 to prevent
|
# Security: dev mode must run on localhost:4402 to prevent
|
||||||
# accidental public exposure of the Vite dev server
|
# accidental public exposure of the Vite dev server
|
||||||
if host != "localhost" or port != 4402:
|
if host != "localhost" or port != 4402:
|
||||||
@@ -200,7 +199,7 @@ def main():
|
|||||||
Config(app="paskia.fastapi:app", **run_kwargs, **ep)
|
Config(app="paskia.fastapi:app", **run_kwargs, **ep)
|
||||||
).serve()
|
).serve()
|
||||||
)
|
)
|
||||||
elif devmode:
|
elif DEVMODE:
|
||||||
# Use uvicorn.run for proper reload support (it handles subprocess spawning)
|
# Use uvicorn.run for proper reload support (it handles subprocess spawning)
|
||||||
ep = endpoints[0]
|
ep = endpoints[0]
|
||||||
uvicorn_run("paskia.fastapi:app", **run_kwargs, **ep)
|
uvicorn_run("paskia.fastapi:app", **run_kwargs, **ep)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from paskia import globals
|
|||||||
from paskia.db import start_background, stop_background
|
from paskia.db import start_background, stop_background
|
||||||
from paskia.db.logging import configure_db_logging
|
from paskia.db.logging import configure_db_logging
|
||||||
from paskia.fastapi import admin, api, auth_host, ws
|
from paskia.fastapi import admin, api, auth_host, ws
|
||||||
|
from paskia.fastapi.__main__ import DEVMODE
|
||||||
from paskia.fastapi.logging import AccessLogMiddleware, configure_access_logging
|
from paskia.fastapi.logging import AccessLogMiddleware, configure_access_logging
|
||||||
from paskia.fastapi.session import AUTH_COOKIE
|
from paskia.fastapi.session import AUTH_COOKIE
|
||||||
from paskia.util import hostutil, passphrase, vitedev
|
from paskia.util import hostutil, passphrase, vitedev
|
||||||
@@ -59,7 +60,7 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path
|
|||||||
|
|
||||||
# Restore uvicorn info logging (suppressed during startup in dev mode)
|
# Restore uvicorn info logging (suppressed during startup in dev mode)
|
||||||
# Keep uvicorn.error at WARNING to suppress WebSocket "connection open/closed" messages
|
# Keep uvicorn.error at WARNING to suppress WebSocket "connection open/closed" messages
|
||||||
if frontend.devmode:
|
if app.debug:
|
||||||
logging.getLogger("uvicorn").setLevel(logging.INFO)
|
logging.getLogger("uvicorn").setLevel(logging.INFO)
|
||||||
logging.getLogger("uvicorn.error").setLevel(logging.WARNING)
|
logging.getLogger("uvicorn.error").setLevel(logging.WARNING)
|
||||||
await frontend.load()
|
await frontend.load()
|
||||||
@@ -74,6 +75,7 @@ app = FastAPI(
|
|||||||
docs_url=None,
|
docs_url=None,
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
openapi_url=None,
|
openapi_url=None,
|
||||||
|
debug=DEVMODE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Custom access logging (uvicorn's access_log is disabled)
|
# Custom access logging (uvicorn's access_log is disabled)
|
||||||
|
|||||||
+35
-91
@@ -13,8 +13,8 @@ All other options are forwarded to `paskia`.
|
|||||||
Backend always listens on localhost:4402.
|
Backend always listens on localhost:4402.
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
FASTAPI_VUE_FRONTEND_URL Set by this script for the backend to know where Vite is.
|
PASKIA_FRONTEND_URL Set by this script for the backend to know where Vite is.
|
||||||
FASTAPI_VUE_BACKEND_URL Set by this script for Vite to know where to proxy API calls.
|
PASKIA_BACKEND_URL Set by this script for Vite to know where to proxy API calls.
|
||||||
PASKIA_SITE_URL User-facing URL for reset links (Caddy HTTPS or Vite HTTP).
|
PASKIA_SITE_URL User-facing URL for reset links (Caddy HTTPS or Vite HTTP).
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@@ -34,12 +34,16 @@ from contextlib import suppress
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from fastapi_vue.hostutil import parse_endpoint
|
|
||||||
|
|
||||||
# Import utilities from scripts/fastapi-vue (not a package, so we adjust sys.path)
|
# Import utilities from scripts/fastapi-vue (not a package, so we adjust sys.path)
|
||||||
sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue")))
|
sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue")))
|
||||||
from buildutil import find_dev_tool, find_install_tool, logger # noqa: E402
|
from devutil import ( # noqa: E402
|
||||||
from devutil import ProcessGroup, check_ports_free # noqa: E402
|
ProcessGroup,
|
||||||
|
check_ports_free,
|
||||||
|
logger,
|
||||||
|
ready,
|
||||||
|
setup_cli,
|
||||||
|
setup_vite,
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_VITE_PORT = 4403 # overrides by CLI option
|
DEFAULT_VITE_PORT = 4403 # overrides by CLI option
|
||||||
BACKEND_PORT = 4402 # hardcoded, also in vite.config.ts
|
BACKEND_PORT = 4402 # hardcoded, also in vite.config.ts
|
||||||
@@ -60,35 +64,6 @@ SITE_ADDR {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def build_vite_cmd(vite_host: str, vite_port: int) -> list[str] | None:
|
|
||||||
"""Build the Vite dev command, or None if not available."""
|
|
||||||
devpath = Path(__file__).parent.parent / "frontend"
|
|
||||||
if not (devpath / "package.json").exists():
|
|
||||||
logger.warning("Frontend source not found at %s", devpath)
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
cmd = find_dev_tool()
|
|
||||||
except RuntimeError as e:
|
|
||||||
logger.warning(str(e))
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Add Vite CLI args for host/port
|
|
||||||
cmd.extend([f"--port={vite_port}", "--logLevel=silent"])
|
|
||||||
if vite_host and vite_host != "localhost":
|
|
||||||
cmd.append("--host" if vite_host == "0.0.0.0" else f"--host={vite_host}")
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
|
|
||||||
def build_npm_install_cmd() -> list[str] | None:
|
|
||||||
"""Build the npm install command, or None if not available."""
|
|
||||||
try:
|
|
||||||
return find_install_tool()
|
|
||||||
except RuntimeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def build_caddyfile(origins: list[str], vite_port: int) -> str:
|
def build_caddyfile(origins: list[str], vite_port: int) -> str:
|
||||||
"""Build a Caddyfile for the given origins."""
|
"""Build a Caddyfile for the given origins."""
|
||||||
caddyfile_parts = []
|
caddyfile_parts = []
|
||||||
@@ -181,22 +156,26 @@ async def run_caddy(origins: list[str], vite_port: int) -> asyncio.subprocess.Pr
|
|||||||
|
|
||||||
async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None:
|
async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None:
|
||||||
"""Run the development server with all components."""
|
"""Run the development server with all components."""
|
||||||
# Parse Vite endpoint
|
reporoot = Path(__file__).parent.parent
|
||||||
endpoints = parse_endpoint(args.listen, DEFAULT_VITE_PORT)
|
frontend_path = reporoot / "frontend"
|
||||||
ep = endpoints[0]
|
if not (frontend_path / "package.json").exists():
|
||||||
|
logger.warning("Frontend source not found at %s", frontend_path)
|
||||||
if "uds" in ep:
|
|
||||||
logger.warning("Unix sockets are not supported for Vite frontend")
|
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
vite_host = ep["host"]
|
viteurl, npm_install, vite = setup_vite(args.listen, DEFAULT_VITE_PORT)
|
||||||
vite_port = ep["port"]
|
backurl, paskia = setup_cli("paskia", f"localhost:{BACKEND_PORT}", BACKEND_PORT)
|
||||||
# Multiple endpoints means all-interfaces (:port syntax)
|
|
||||||
if len(endpoints) > 1:
|
|
||||||
vite_host = "0.0.0.0"
|
|
||||||
|
|
||||||
vite_url = f"http://localhost:{vite_port}"
|
# Extract vite port for Caddy config
|
||||||
backend_url = f"http://localhost:{BACKEND_PORT}"
|
vite_port = int(viteurl.rsplit(":", 1)[1])
|
||||||
|
|
||||||
|
# Build paskia command with options
|
||||||
|
paskia.extend(["--rp-id", args.rp_id])
|
||||||
|
if args.auth_host:
|
||||||
|
paskia.extend(["--auth-host", args.auth_host])
|
||||||
|
if args.origins:
|
||||||
|
for origin in args.origins:
|
||||||
|
paskia.extend(["--origin", origin])
|
||||||
|
paskia.extend(remaining)
|
||||||
|
|
||||||
# Compute origins for Caddy
|
# Compute origins for Caddy
|
||||||
caddy_origins = []
|
caddy_origins = []
|
||||||
@@ -217,30 +196,13 @@ async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None:
|
|||||||
seen = set()
|
seen = set()
|
||||||
caddy_origins = [x for x in caddy_origins if not (x in seen or seen.add(x))]
|
caddy_origins = [x for x in caddy_origins if not (x in seen or seen.add(x))]
|
||||||
|
|
||||||
# Check ports are free before starting
|
|
||||||
await check_ports_free(vite_url, backend_url)
|
|
||||||
|
|
||||||
# Set environment for subprocesses
|
# Set environment for subprocesses
|
||||||
os.environ["FASTAPI_VUE_FRONTEND_URL"] = vite_url
|
os.environ["PASKIA_FRONTEND_URL"] = viteurl
|
||||||
os.environ["FASTAPI_VUE_BACKEND_URL"] = backend_url
|
os.environ["PASKIA_BACKEND_URL"] = backurl
|
||||||
os.environ["PASKIA_SITE_URL"] = caddy_origins[0] if args.caddy else vite_url
|
os.environ["PASKIA_SITE_URL"] = caddy_origins[0] if args.caddy else viteurl
|
||||||
if args.auth_host:
|
if args.auth_host:
|
||||||
os.environ["PASKIA_AUTH_HOST"] = args.auth_host
|
os.environ["PASKIA_AUTH_HOST"] = args.auth_host
|
||||||
|
|
||||||
# Build commands
|
|
||||||
frontend_path = Path(__file__).parent.parent / "frontend"
|
|
||||||
vite_cmd = build_vite_cmd(vite_host, vite_port)
|
|
||||||
install_cmd = build_npm_install_cmd()
|
|
||||||
|
|
||||||
paskia_cmd = ["paskia", "-l", f"localhost:{BACKEND_PORT}"]
|
|
||||||
paskia_cmd.extend(["--rp-id", args.rp_id])
|
|
||||||
if args.auth_host:
|
|
||||||
paskia_cmd.extend(["--auth-host", args.auth_host])
|
|
||||||
if args.origins:
|
|
||||||
for origin in args.origins:
|
|
||||||
paskia_cmd.extend(["--origin", origin])
|
|
||||||
paskia_cmd.extend(remaining)
|
|
||||||
|
|
||||||
async with ProcessGroup() as pg:
|
async with ProcessGroup() as pg:
|
||||||
# Start Caddy first if requested (needs to bind ports)
|
# Start Caddy first if requested (needs to bind ports)
|
||||||
if args.caddy:
|
if args.caddy:
|
||||||
@@ -248,29 +210,11 @@ async def run_devserver(args: argparse.Namespace, remaining: list[str]) -> None:
|
|||||||
pg._procs.append(caddy_proc)
|
pg._procs.append(caddy_proc)
|
||||||
pg._cmds[caddy_proc.pid] = "caddy"
|
pg._cmds[caddy_proc.pid] = "caddy"
|
||||||
|
|
||||||
# Run npm install concurrently with backend startup
|
npm_proc = await pg.spawn(*npm_install, cwd=frontend_path)
|
||||||
if install_cmd and (frontend_path / "package.json").exists():
|
await check_ports_free(viteurl, backurl)
|
||||||
npm_proc = await pg.spawn(*install_cmd, cwd=str(frontend_path))
|
await pg.spawn(*paskia)
|
||||||
else:
|
await pg.wait(npm_proc, ready(backurl, path="/api/health?from=devserver.py"))
|
||||||
npm_proc = None
|
await pg.spawn(*vite, cwd=frontend_path)
|
||||||
|
|
||||||
# Start paskia backend
|
|
||||||
logger.info(">>> (devmode) %s", " ".join(paskia_cmd))
|
|
||||||
paskia_proc = await asyncio.create_subprocess_exec(*paskia_cmd)
|
|
||||||
pg._procs.append(paskia_proc)
|
|
||||||
pg._cmds[paskia_proc.pid] = "paskia"
|
|
||||||
|
|
||||||
# Wait for npm install to complete before starting Vite
|
|
||||||
if npm_proc:
|
|
||||||
await pg.wait(npm_proc)
|
|
||||||
|
|
||||||
# Start Vite dev server
|
|
||||||
if vite_cmd:
|
|
||||||
await pg.spawn(*vite_cmd, cwd=str(frontend_path))
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
"Backend expects Vite at %s - start it manually if needed", vite_url
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ def find_dev_tool() -> list[str]:
|
|||||||
Raises RuntimeError if no runtime is found.
|
Raises RuntimeError if no runtime is found.
|
||||||
"""
|
"""
|
||||||
dev_args = {
|
dev_args = {
|
||||||
"deno": ("run", "dev", "--"),
|
"deno": ("run", "-A", "npm:vite"),
|
||||||
"npm": ("--silent", "run", "dev", "--"),
|
"npm": ("--silent", "run", "dev", "--"),
|
||||||
"bun": ("run", "dev", "--"),
|
"bun": ("run", "dev", "--"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ class ProcessGroup:
|
|||||||
self._cmds: dict[int, str] = {} # pid -> command name
|
self._cmds: dict[int, str] = {} # pid -> command name
|
||||||
|
|
||||||
async def spawn(
|
async def spawn(
|
||||||
self, *cmd: str, cwd: str | None = None, env: dict | None = None
|
self, *cmd: str, cwd: str | None = None
|
||||||
) -> asyncio.subprocess.Process:
|
) -> asyncio.subprocess.Process:
|
||||||
"""Spawn a subprocess and track it."""
|
"""Spawn a subprocess and track it."""
|
||||||
cmd_name = Path(cmd[0]).stem
|
cmd_name = Path(cmd[0]).stem
|
||||||
logger.info(">>> %s", " ".join([cmd_name, *cmd[1:]]))
|
logger.info(">>> %s", " ".join([cmd_name, *cmd[1:]]))
|
||||||
proc = await asyncio.create_subprocess_exec(*cmd, cwd=cwd, env=env)
|
proc = await asyncio.create_subprocess_exec(*cmd, cwd=cwd)
|
||||||
self._procs.append(proc)
|
self._procs.append(proc)
|
||||||
self._cmds[proc.pid] = cmd_name
|
self._cmds[proc.pid] = cmd_name
|
||||||
return proc
|
return proc
|
||||||
@@ -188,3 +188,24 @@ def setup_fastapi(
|
|||||||
"--forwarded-allow-ips=*",
|
"--forwarded-allow-ips=*",
|
||||||
]
|
]
|
||||||
return f"http://{host}:{port}", cmd
|
return f"http://{host}:{port}", cmd
|
||||||
|
|
||||||
|
|
||||||
|
def setup_cli(
|
||||||
|
cli: str, endpoint: str, default_port: int = 8000
|
||||||
|
) -> tuple[str, list[str]]:
|
||||||
|
"""Parse backend endpoint and build CLI command.
|
||||||
|
|
||||||
|
Returns (url, cli_cmd).
|
||||||
|
Raises SystemExit(1) on invalid config.
|
||||||
|
"""
|
||||||
|
endpoints = parse_endpoint(endpoint, default_port)
|
||||||
|
|
||||||
|
if "uds" in endpoints[0]:
|
||||||
|
logger.warning("Unix sockets not supported with vite devserver")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
host = endpoints[0]["host"]
|
||||||
|
port = endpoints[0]["port"]
|
||||||
|
|
||||||
|
cmd = [cli, f"--listen={host}:{port}"]
|
||||||
|
return f"http://{host}:{port}", cmd
|
||||||
|
|||||||
Reference in New Issue
Block a user