Compare commits

..
15 Commits
Author SHA1 Message Date
LeoVasanko f5fcd626e8 Update templates for fastapi_vue 0.5.0 catch-all toggle. 2026-01-25 01:07:56 +00:00
LeoVasanko 1d9ddf7dc5 Update target project python version requirement if needed. 2026-01-24 21:31:07 +00:00
LeoVasanko b189ae66e7 Smarter .gitignore patching. 2026-01-24 21:16:05 +00:00
LeoVasanko 4156eb2db3 Better handling of upgrades, an option to avoid overwriting main and devserver scripts, writing .new.py instead. Error out on unix socket in devserver. 2026-01-21 21:13:23 +00:00
LeoVasanko 3f6559b2db Simplify devserver script and remove its duplicated parse_endpoint function. 2026-01-18 01:15:34 +00:00
LeoVasanko 71d4e053a7 Strictly correct indent and formatting on Vue demo patches. 2026-01-17 23:21:01 +00:00
LeoVasanko 2dec331afb Setup script cleanup. Use discovered app module and var names when creating __main__.py from template. README updates. 2026-01-17 23:04:07 +00:00
LeoVasanko 64f43df3e3 Completely rewritten CLI main, support unix sockets and wildcard bindings. 2026-01-17 22:48:13 +00:00
LeoVasanko 6ccba2be9f Adjust vite args (cleanup, don't clear screen). 2026-01-17 22:20:27 +00:00
LeoVasanko 2b735528ab Fix another Deno issue running Vue without typescript. 2026-01-17 22:19:28 +00:00
LeoVasanko e6170549f0 README 2026-01-17 20:21:31 +00:00
LeoVasanko a2983a62a0 Simplified templating, backend port config on devserver.py, simplified Vue app patches. 2026-01-17 19:51:47 +00:00
LeoVasanko d9323f34e1 Silence type warnings for symbols that actually are available at build time. 2026-01-17 17:08:50 +00:00
LeoVasanko a4379094db README 2026-01-17 16:42:02 +00:00
LeoVasanko 52ec232ce4 Fix deno npm run-p problem that was preventing TypeScript builds on Deno, by using build-only instead. 2026-01-17 16:40:43 +00:00
10 changed files with 784 additions and 497 deletions
+63 -50
View File
@@ -1,15 +1,13 @@
# fastapi-vue-setup # fastapi-vue-setup
Tool to create or patch FastAPI project with a Vue frontend, with integrated build and development systems. Only building the package needs JS runtime and Vue. Additionally, a devmode setup using Vite dev server with hot auto reloads is available via the scripts/devserver.py script (only intended to be used on the source repo, not included in installed package). Tool to create or patch FastAPI project with a Vue frontend, with integrated build and development systems. The Python package will not need any JS runtime because it includes a prebuilt Vue frontend in it. For development (Vite and FastAPI auto reloads) and building the package one of npm, deno or bun is required (node is recommended due to bugs in deno and bun).
## Features ## Features
- **No JavaScript**: Your Python package can be installed and used without any JS runtime - **No JavaScript**: Your Python package can be installed and used without any JS runtime
- **Create new projects** with `uv init` + `create-vue` (interactive)
- **Patch existing projects** with build hooks and dev server scripts
- **Integrated build system**: Vue frontend builds into Python package during `uv build` - **Integrated build system**: Vue frontend builds into Python package during `uv build`
- **Development server**: Single command runs Vite + FastAPI with hot-reload - **Development server**: Single command runs Vite + FastAPI with hot-reload
- **Optimized static serving**: zstd compression, ETag caching, SPA support - **Optimized static serving**: Caching, zstd compression and SPA support
## Installation ## Installation
@@ -28,77 +26,98 @@ uvx fastapi-vue-setup my-app
## Usage ## Usage
The script may be used to create an all new project folder or to patch or update an existing project to use this framework. It autodetects the project folder given and performs the appropriate actions.
### Create a new project ### Create a new project
```sh ```sh
fastapi-vue-setup new my-app fastapi-vue-setup my-app
``` ```
This will: This will:
1. Run `uv init my-app` 1. Run `uv init my-app`
2. Run `npm create vue@latest frontend` (interactive) 2. Run `create-vue frontend` (interactive - choose your Vue options)
3. Patch the project with FastAPI integration 3. Patch the project with FastAPI integration
4. Install dependencies via `uv add`
### Patch an existing project ### Patch an existing project
```bash You should have your pyproject.toml at the current working directory, and Vue with its package.json under `frontend/`. If either one is missing, new applications will be initialised. Otherwise we only patch what can be patched without breaking your existing projects.
fastapi-vue-setup patch /path/to/project
```sh
fastapi-vue-setup .
``` ```
### CLI Options
```
fastapi-vue-setup [project-dir] [options]
Options: Options:
--module-name NAME Python module name (auto-detected from pyproject.toml)
- `--module-name NAME`: Python module name (auto-detected from pyproject.toml) --dry-run Preview changes without modifying files
- `--vite-port PORT`: Vite dev server port (default: 5173)
- `--backend-port PORT`: Backend API port in dev mode (default: 5174)
- `--prod-port PORT`: Production server port (default: 8000)
- `--force`: Overwrite existing files
- `--dry-run`: Preview changes without modifying files
### Update an existing project
```bash
fastapi-vue-setup update /path/to/project
``` ```
Same as `patch --force` - overwrites template files with latest versions.
## Port Configuration ## Port Configuration
The tool uses three distinct ports: In development, you access the Vite dev server at `http://localhost:5173`. Vite proxies `/api/*` requests to FastAPI at port 5180. Ports and hosts of Vite and FastAPI are configurable by `devserver.py` arguments.
| Port | Purpose | Used by | In production, FastAPI serves both the API and static files at `http://localhost:5080`. Configurable by `host:port` argument with defaults set in `__main__.py`
| ---- | --------------------- | ------------------------------ |
| 5173 | Vite dev server (HMR) | `npm run dev` via devserver.py |
| 5174 | FastAPI in dev mode | uvicorn via devserver.py |
| 8000 | Production server | `uv run my-app` |
In development, you access the app at `http://localhost:5173`. Vite proxies `/api/*` requests to FastAPI at port 5174. ## Main CLI
In production, FastAPI serves both the API and static files from the same port (8000). If your project didn't already have `__main__.py`, we create one that runs the FastAPI app with richer configuration than what the FastAPI CLI offers. Running your module starts it in production mode, and optionally host:port may be given as argument to specify where it listens.
If you are running behind a reverse proxy like [Caddy](https://caddyserver.com/) on localhost, your app will trust the proxy headers it sends. However, if you need to configure another proxy host or IP, set `FORWARDED_ALLOW_IPS` env variable before running the server.
The devserver script depends on this CLI entry for running the backend. You will have to modify the `devserver.py` script if your app has its own incompatible main module. Note that we set FastAPI debug mode and Uvicorn reload when configured via `FASTAPI_VUE_BACKEND_URL` env variable (set by `devserver.py`), while for normal production use these stay disabled. The same variable also controls static files serving (disabled in dev mode).
## Vite Plugin Configuration
The `fastapiVue()` plugin in `vite.config.ts` accepts options to customize proxy behavior:
```js
import fastapiVue from "./vite-plugin-fastapi.js";
export default defineConfig({
plugins: [
vue(),
// Default: proxies only /api
fastapiVue(),
// Or specify custom paths to proxy to backend
fastapiVue({ paths: ["/api", "/auth", "/ws"] }),
],
});
```
The plugin reads environment `FASTAPI_VUE_BACKEND_URL` (default: `http://localhost:5180`) to determine where to proxy requests. This is set automatically by `devserver.py`.
## Project Structure ## Project Structure
After patching, your project will have:
``` ```
my-app/ my-app/
├── frontend/ # Vue.js application ├── frontend/ # Vue application
│ ├── src/ │ ├── src/
│ ├── vite.config.ts # Builds to ../my_app/frontend-build │ ├── vite-plugin-fastapi.js
│ ├── vite.config.js
│ └── package.json │ └── package.json
├── my_app/ # Python package ├── my_app/ # Python module (files included in sdist)
│ ├── __init__.py │ ├── __init__.py
│ ├── __main__.py # CLI entry point │ ├── __main__.py # CLI entrypoint
│ ├── app.py # FastAPI application │ ├── app.py # FastAPI application
│ └── frontend-build/ # Built frontend (gitignored) │ └── frontend-build/ # Built frontend (gitignored)
├── scripts/ ├── scripts/
│ ├── devserver.py # Development server │ ├── devserver.py # CLI dev server (only in source tree)
│ └── fastapi-vue/ # Build utilities │ └── fastapi-vue/
└── pyproject.toml # Project configuration │ ├── build-frontend.py
│ └── util.py
└── pyproject.toml
``` ```
The script finds your existing fastpi app module (even if not named app.py) and other files and patches them with minimal changes to enable the Vue-FastAPI interconnection. The project directory tree looks roughly like this after project creation or patching. The script finds your existing app module and other files and patches them with minimal changes to enable the Vue-FastAPI interconnection. New Python and Vue projects are created automatically if none exist.
## Development Workflow ## Development Workflow
@@ -113,12 +132,6 @@ uv build
uv run my-app uv run my-app
``` ```
## Static File Serving ## Frontend serving
The `Frontend` class provides: Your FastAPI app will use [fastapi-vue](https://git.zi.fi/LeoVasanko/fastapi-vue) to serve the frontend files. Refer to that package's documentation for further configuration.
- **Automatic zstd compression** at level 18
- **Smart caching**: Content-hashed assets get `immutable` cache headers
- **ETag support**: 304 Not Modified responses for cached content
- **SPA routing**: Falls back to index.html for client-side routes
- **Favicon handling**: Automatic /favicon.ico from hashed assets
+566 -309
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@ description = "Tool to create or patch FastAPI+Vue projects with integrated buil
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
"tomli-w>=1.0.0", "tomlkit>=0.12.0",
] ]
[project.urls] [project.urls]
-3
View File
@@ -1,3 +0,0 @@
"""{{PROJECT_TITLE}} package."""
__version__ = "0.1.0"
+46 -23
View File
@@ -1,31 +1,54 @@
"""Entry point for the application.""" # auto-upgrade@fastapi-vue-setup - remove this if you modify this file
import argparse
import asyncio
import os
import sys import uvicorn
from fastapi_vue.hostutil import parse_endpoint
from uvicorn import Config, Server
from .APP_MODULE import APP_VAR
DEFAULT_PORT = 5080
def run_server(endpoints: list[dict], *, proxy="", devmode=False):
conf: dict[str, object] = {"app": "MODULE_NAME.APP_MODULE:APP_VAR"}
if proxy:
conf["proxy_headers"] = True
conf["forwarded_allow_ips"] = proxy
if devmode:
conf["reload"] = True
conf["reload_dirs"] = ["MODULE_NAME"]
APP_VAR.debug = True
if len(endpoints) > 1:
# Run separate servers for multiple endpoints
async def serve_all():
async with asyncio.TaskGroup() as tg:
for ep in endpoints:
tg.create_task(Server(Config(**conf, **ep)).serve())
asyncio.run(serve_all())
else:
uvicorn.run(**conf, **endpoints[0])
def main(): def main():
"""Run the FastAPI application using uvicorn.""" parser = argparse.ArgumentParser(description="Run the MODULE_NAME server.")
import uvicorn parser.add_argument(
"endpoint",
if len(sys.argv) > 1: nargs="?",
endpoint = sys.argv[1] help=(
if ":" in endpoint: f"Endpoint (default: localhost:{DEFAULT_PORT}). "
host, port = endpoint.rsplit(":", 1) "Forms: host:port | :port | [ipv6]:port | ip | host | unix:/path.sock"
host = host or "localhost" ),
port = int(port)
else:
host = "localhost"
port = int(endpoint)
else:
host = "localhost"
port = {{PROD_PORT}}
uvicorn.run(
"{{MODULE_NAME}}.app:app",
host=host,
port=port,
log_level="info",
) )
args = parser.parse_args()
proxy = os.getenv("FORWARDED_ALLOW_IPS", "127.0.0.1,::1")
devmode = bool(os.getenv("FASTAPI_VUE_FRONTEND_URL"))
endpoints = parse_endpoint(args.endpoint, DEFAULT_PORT)
run_server(endpoints, proxy=proxy, devmode=devmode)
if __name__ == "__main__": if __name__ == "__main__":
+8 -17
View File
@@ -1,18 +1,11 @@
"""Main FastAPI application."""
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
from fastapi_vue import Frontend from fastapi_vue import Frontend
# Frontend static file server - configure options here # Vue Frontend static files
frontend = Frontend( frontend = Frontend(Path(__file__).with_name("frontend-build"), cached=["/assets/"])
Path(__file__).parent / "frontend-build",
spa=True,
favicon="/assets/favicon",
cached=["/assets/"],
)
@asynccontextmanager @asynccontextmanager
@@ -22,19 +15,17 @@ async def lifespan(app: FastAPI):
yield yield
app = FastAPI(title="{{PROJECT_TITLE}}", lifespan=lifespan) app = FastAPI(title="PROJECT_TITLE", lifespan=lifespan)
# Add API routes here...
# Health check endpoint for the Vue demo app to verify the backend is running
@app.get("/api/health") @app.get("/api/health")
async def health_check(): async def health_check():
"""Health check endpoint for the dev server."""
return {"status": "ok"} return {"status": "ok"}
# Add your API routes here # Serve the Vue frontend (needs to be last if SPA catch-all is used)
# @app.get("/api/example")
# async def example():
# return {"message": "Hello World"}
frontend.route(app, "/") frontend.route(app, "/")
+16 -20
View File
@@ -6,34 +6,30 @@
* - Builds to the Python module's frontend-build directory * - Builds to the Python module's frontend-build directory
* *
* Environment variables (with defaults): * Environment variables (with defaults):
* VITE_PORT=5173 - Vite dev server port * FASTAPI_VUE_BACKEND_URL=http://localhost:5180 - Backend API URL for proxying
* VITE_BACKEND_URL=http://localhost:5180 - Backend API URL for proxying
*/ */
const backendUrl = const backendUrl = process.env.FASTAPI_VUE_BACKEND_URL || "http://localhost:5180"
process.env.VITE_BACKEND_URL || "http://localhost:{{BACKEND_PORT}}";
const vitePort = parseInt(process.env.VITE_PORT || "{{VITE_PORT}}"); export default function fastapiVue({ paths = ["/api"] } = {}) {
// Build proxy configuration for each path
const proxy = {}
for (const path of paths) {
proxy[path] = {
target: backendUrl,
changeOrigin: false,
ws: true,
}
}
export default function fastapiVue() {
return { return {
name: "fastapi-vite", name: "fastapi-vite",
config: () => ({ config: () => ({
server: { server: { proxy },
host: "localhost",
port: vitePort,
strictPort: true,
proxy: {
"/api": {
target: backendUrl,
changeOrigin: false,
ws: true,
},
},
},
build: { build: {
outDir: "../{{MODULE_NAME}}/frontend-build", outDir: "../MODULE_NAME/frontend-build",
emptyOutDir: true, emptyOutDir: true,
}, },
}), }),
}; }
} }
Regular → Executable
+80 -71
View File
@@ -1,36 +1,41 @@
#!/usr/bin/env -S uv run #!/usr/bin/env -S uv run
# auto-upgrade@fastapi-vue-setup - remove this if you modify this file
"""Run Vite development server for frontend and FastAPI backend with auto-reload. """Run Vite development server for frontend and FastAPI backend with auto-reload.
Usage: Usage:
uv run scripts/devserver.py [host:port] uv run scripts/devserver.py [host:port] [--backend host:port]
The optional host:port argument sets where the Vite frontend listens. The optional host:port argument sets where the Vite frontend listens.
Supported forms: host[:port], :port (all interfaces), or just port. Supported forms: host[:port], :port (all interfaces), or just port.
Backend always listens on localhost:{{BACKEND_PORT}}. The --backend option sets where the FastAPI backend listens (default: localhost:5180).
Environment: Environment:
JS_RUNTIME Path or name of JS runtime to use (deno, npm/node or bun). JS_RUNTIME Path or name of JS runtime to use (deno, npm/node or bun).
FASTAPI_VUE_FRONTEND_URL Set by this script for the backend to know where Vite is. FASTAPI_VUE_FRONTEND_URL Set by this script for the backend to know where Vite is.
""" """
import argparse
import asyncio import asyncio
import contextlib import contextlib
import ipaddress
import os import os
import sys
from pathlib import Path from pathlib import Path
from sys import stderr from sys import stderr
from urllib.parse import urlparse
import httpx import httpx
from fastapi_vue.hostutil import parse_endpoint
exec((Path(__file__).parent / "fastapi-vue/util.py").read_text("UTF-8")) # noqa: S102 exec((Path(__file__).parent / "fastapi-vue/util.py").read_text("UTF-8")) # noqa: S102
DEFAULT_HOST = "localhost" DEFAULT_VITE_PORT = 5173
DEFAULT_VITE_PORT = {{VITE_PORT}} DEFAULT_BACKEND_PORT = 5180
BACKEND_PORT = {{BACKEND_PORT}}
FRONTEND_PATH = Path(__file__).parent.parent / "frontend" FRONTEND_PATH = Path(__file__).parent.parent / "frontend"
EPILOG = """
scripts/devserver.py # Default ports on localhost
scripts/devserver.py 3000 # Vite on localhost:3000
scripts/devserver.py :3000 --backend 8000 # *:3000, localhost:8000
"""
BUN_BUG = """\ BUN_BUG = """\
┃ ⚠️ Bun cannot correctly proxy API requests to the backend. ┃ ⚠️ Bun cannot correctly proxy API requests to the backend.
┃ Bug report: https://github.com/oven-sh/bun/issues/9882 ┃ Bug report: https://github.com/oven-sh/bun/issues/9882
@@ -39,43 +44,8 @@ BUN_BUG = """\
""" """
def parse_endpoint(value: str | None) -> tuple[str | None, int, bool]:
"""Parse an endpoint for Vite (no unix socket support).
Returns (host, port, all_ifaces).
"""
if not value:
return DEFAULT_HOST, DEFAULT_VITE_PORT, False
# Port only (numeric) -> localhost:port
if value.isdigit():
return DEFAULT_HOST, int(value), False
# Leading colon :port -> bind all interfaces
if value.startswith(":") and value != ":":
port_part = value[1:]
if not port_part.isdigit():
raise SystemExit(f"Invalid port in '{value}'")
return None, int(port_part), True
# Unbracketed IPv6 (cannot safely contain a port)
if value.count(":") > 1 and not value.startswith("["):
try:
ipaddress.IPv6Address(value)
except ValueError as e:
raise SystemExit(f"Invalid IPv6 address '{value}': {e}") from e
return value, DEFAULT_VITE_PORT, False
# Use urllib.parse for everything else
parsed = urlparse(f"//{value}")
host = parsed.hostname or DEFAULT_HOST
port = parsed.port or DEFAULT_VITE_PORT
return host, port, False
def resolve_frontend_tools( def resolve_frontend_tools(
vite_host: str | None, vite_port: int, all_ifaces: bool vite_port: int, all_ifaces: bool
) -> tuple[list[str], list[str], str]: ) -> tuple[list[str], list[str], str]:
"""Resolve frontend install and dev commands. """Resolve frontend install and dev commands.
@@ -86,7 +56,7 @@ def resolve_frontend_tools(
stderr.write(f"┃ ⚠️ Frontend source not found at {FRONTEND_PATH}\n") stderr.write(f"┃ ⚠️ Frontend source not found at {FRONTEND_PATH}\n")
raise SystemExit(1) raise SystemExit(1)
result = find_js_runtime() result = find_js_runtime() # noqa # type: ignore
if result is None: if result is None:
if not os.environ.get("JS_RUNTIME"): if not os.environ.get("JS_RUNTIME"):
stderr.write("┃ ⚠️ deno, npm or bun needed to run the frontend server.\n") stderr.write("┃ ⚠️ deno, npm or bun needed to run the frontend server.\n")
@@ -106,12 +76,15 @@ def resolve_frontend_tools(
} }
install_cmd = [tool, *install_args[name]] install_cmd = [tool, *install_args[name]]
dev_cmd = [tool, *dev_args[name], "--port", str(vite_port)] dev_cmd = [
tool,
*dev_args[name],
"--clearScreen=false",
f"--port={vite_port}",
]
if all_ifaces: if all_ifaces:
dev_cmd.append("--host") dev_cmd.append("--host")
elif vite_host:
dev_cmd.extend(["--host", vite_host])
if name == "bun": if name == "bun":
stderr.write(BUN_BUG) stderr.write(BUN_BUG)
@@ -119,13 +92,15 @@ def resolve_frontend_tools(
return install_cmd, dev_cmd, name return install_cmd, dev_cmd, name
async def wait_for_backend(): async def wait_for_backend(host: str, port: int):
"""Wait for the backend to be ready by polling the health endpoint.""" """Wait for the backend to be ready by polling the health endpoint."""
max_attempts = 50 max_attempts = 50
url = f"http://{host}:{port}"
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
for attempt in range(max_attempts): for attempt in range(max_attempts):
try: try:
await client.get(f"http://localhost:{BACKEND_PORT}", timeout=1.0) await client.get(url, timeout=1.0)
stderr.write("✓ Backend ready!\n") stderr.write("✓ Backend ready!\n")
return True return True
except httpx.RequestError: except httpx.RequestError:
@@ -155,19 +130,29 @@ async def _terminate_process(proc: asyncio.subprocess.Process, name: str) -> Non
async def run_devserver( async def run_devserver(
vite_host: str | None, vite_port: int, all_ifaces: bool vite_port: int,
all_ifaces: bool,
backend_host: str,
backend_port: int,
) -> None: ) -> None:
"""Run the development server with install, backend, and frontend.""" """Run the development server with install, backend, and frontend."""
install_cmd, dev_cmd, tool_name = resolve_frontend_tools( install_cmd, dev_cmd, tool_name = resolve_frontend_tools(vite_port, all_ifaces)
vite_host, vite_port, all_ifaces
)
# Tell the backend where the Vite dev server is # Tell the backend where the Vite dev server is
os.environ["FASTAPI_VUE_FRONTEND_URL"] = ( os.environ["FASTAPI_VUE_FRONTEND_URL"] = f"http://localhost:{vite_port}"
f"http://{vite_host or 'localhost'}:{vite_port}"
)
# Tell Vite where the backend is (for proxying /api requests) # Tell Vite where the backend is (for proxying /api requests)
os.environ["VITE_BACKEND_URL"] = f"http://localhost:{BACKEND_PORT}" os.environ["FASTAPI_VUE_BACKEND_URL"] = f"http://{backend_host}:{backend_port}"
backend_cmd = [
"uvicorn",
"MODULE_NAME.app:app",
"--host",
backend_host,
"--port",
str(backend_port),
"--reload",
]
cwd = str(Path(__file__).parent.parent) cwd = str(Path(__file__).parent.parent)
frontend_cwd = str(FRONTEND_PATH) frontend_cwd = str(FRONTEND_PATH)
@@ -185,22 +170,13 @@ async def run_devserver(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
# Start backend (concurrent with install) # Start backend (concurrent with install)
backend_cmd = [
"uvicorn",
"{{MODULE_NAME}}.app:app",
"--host",
"localhost",
"--port",
str(BACKEND_PORT),
"--reload",
]
stderr.write(f">>> {' '.join(backend_cmd)}\n") stderr.write(f">>> {' '.join(backend_cmd)}\n")
backend_proc = await asyncio.create_subprocess_exec(*backend_cmd, cwd=cwd) backend_proc = await asyncio.create_subprocess_exec(*backend_cmd, cwd=cwd)
# Wait for install to complete and backend to be ready # Wait for install to complete and backend to be ready
install_task = asyncio.create_task(install_proc.wait(), name="install") install_task = asyncio.create_task(install_proc.wait(), name="install")
backend_ready_task = asyncio.create_task( backend_ready_task = asyncio.create_task(
wait_for_backend(), name="backend_ready" wait_for_backend(backend_host, backend_port), name="backend_ready"
) )
done, pending = await asyncio.wait( done, pending = await asyncio.wait(
@@ -257,11 +233,44 @@ async def run_devserver(
def main(): def main():
hostport = sys.argv[1] if len(sys.argv) > 1 else None parser = argparse.ArgumentParser(
vite_host, vite_port, all_ifaces = parse_endpoint(hostport) description="Run Vite and FastAPI development servers",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=EPILOG,
)
parser.add_argument(
"frontend",
nargs="?",
metavar="host:port",
help="Vite frontend endpoint (default: localhost:5173)",
)
parser.add_argument(
"--backend",
metavar="host:port",
help="FastAPI backend endpoint (default: localhost:5180)",
)
args = parser.parse_args()
# parse_endpoint returns list of dicts with host/port or uds keys
# Multiple entries means bind all interfaces (IPv4 + IPv6)
vite_endpoints = parse_endpoint(args.frontend, DEFAULT_VITE_PORT)
backend_endpoints = parse_endpoint(args.backend, DEFAULT_BACKEND_PORT)
# Vite doesn't support unix sockets
if "uds" in vite_endpoints[0]:
stderr.write("┃ ⚠️ Unix sockets not supported for frontend\n")
raise SystemExit(1)
if "uds" in backend_endpoints[0]:
stderr.write("┃ ⚠️ Unix sockets not supported for backend\n")
raise SystemExit(1)
vite_port = vite_endpoints[0]["port"]
all_ifaces = len(vite_endpoints) > 1
backend_host = backend_endpoints[0]["host"]
backend_port = backend_endpoints[0]["port"]
with contextlib.suppress(KeyboardInterrupt): with contextlib.suppress(KeyboardInterrupt):
asyncio.run(run_devserver(vite_host, vite_port, all_ifaces)) asyncio.run(run_devserver(vite_port, all_ifaces, backend_host, backend_port))
if __name__ == "__main__": if __name__ == "__main__":
@@ -4,7 +4,7 @@ import subprocess
from pathlib import Path from pathlib import Path
from sys import stderr from sys import stderr
from hatchling.builders.hooks.plugin.interface import BuildHookInterface from hatchling.builders.hooks.plugin.interface import BuildHookInterface # type: ignore
exec(Path(__file__).with_name("util.py").read_text("UTF-8")) # noqa: S102 exec(Path(__file__).with_name("util.py").read_text("UTF-8")) # noqa: S102
@@ -23,7 +23,7 @@ class CustomBuildHook(BuildHookInterface):
super().initialize(version, build_data) super().initialize(version, build_data)
stderr.write(">>> Building the frontend\n") stderr.write(">>> Building the frontend\n")
install_cmd, build_cmd = find_build_tool() install_cmd, build_cmd = find_build_tool() # noqa # type: ignore
try: try:
run(install_cmd, cwd="frontend") run(install_cmd, cwd="frontend")
+2 -1
View File
@@ -51,8 +51,9 @@ def find_build_tool():
"npm": ("install",), "npm": ("install",),
"bun": ("--bun", "install"), "bun": ("--bun", "install"),
} }
# Run vite directly for deno to avoid npm-run-all2/run-p issues
build = { build = {
"deno": ("task", "build"), "deno": ("run", "-A", "npm:vite", "build"),
"npm": ("run", "build"), "npm": ("run", "build"),
"bun": ("--bun", "run", "build"), "bun": ("--bun", "run", "build"),
} }