Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d9ddf7dc5 | ||
|
|
b189ae66e7 | ||
|
|
4156eb2db3 | ||
|
|
3f6559b2db | ||
|
|
71d4e053a7 | ||
|
|
2dec331afb | ||
|
|
64f43df3e3 | ||
|
|
6ccba2be9f | ||
|
|
2b735528ab | ||
|
|
e6170549f0 |
@@ -1,6 +1,6 @@
|
|||||||
# fastapi-vue-setup
|
# fastapi-vue-setup
|
||||||
|
|
||||||
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 (npm recommended due to bugs in deno and bun).
|
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
|
||||||
|
|
||||||
@@ -61,17 +61,18 @@ Options:
|
|||||||
|
|
||||||
## 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 |
|
|
||||||
| 5180 | FastAPI in dev mode | uvicorn via devserver.py |
|
|
||||||
| 5080 | Production server | `uv run my-app` |
|
|
||||||
|
|
||||||
In development, you access the app at `http://localhost:5173`. Vite proxies `/api/*` requests to FastAPI at port 5180. Configurable with `devserver.py` arguments.
|
## Main CLI
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
In production, FastAPI serves both the API and static files at `http://localhost:5080`. Configurable by `host:port` argument.
|
|
||||||
|
|
||||||
## Vite Plugin Configuration
|
## Vite Plugin Configuration
|
||||||
|
|
||||||
@@ -96,26 +97,27 @@ The plugin reads environment `FASTAPI_VUE_BACKEND_URL` (default: `http://localho
|
|||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
After patching, your project will have:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
my-app/
|
my-app/
|
||||||
├── frontend/ # Vue 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 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
|
||||||
|
|
||||||
@@ -130,6 +132,6 @@ uv build
|
|||||||
uv run my-app
|
uv run my-app
|
||||||
```
|
```
|
||||||
|
|
||||||
# Frontend serving
|
## Frontend serving
|
||||||
|
|
||||||
Your FastAPI app will use [fastapi-vue](https://git.zi.fi/LeoVasanko/fastapi-vue) to serve the frontend file. Refer to that package's documentation for further configuration.
|
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.
|
||||||
|
|||||||
+540
-228
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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]
|
||||||
|
|||||||
@@ -1,30 +1,54 @@
|
|||||||
import sys
|
# auto-upgrade@fastapi-vue-setup - remove this if you modify this file
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
import uvicorn
|
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.")
|
||||||
|
parser.add_argument(
|
||||||
if len(sys.argv) > 1:
|
"endpoint",
|
||||||
endpoint = sys.argv[1]
|
nargs="?",
|
||||||
if ":" in endpoint:
|
help=(
|
||||||
host, port = endpoint.rsplit(":", 1)
|
f"Endpoint (default: localhost:{DEFAULT_PORT}). "
|
||||||
host = host or "localhost"
|
"Forms: host:port | :port | [ipv6]:port | ip | host | unix:/path.sock"
|
||||||
port = int(port)
|
),
|
||||||
else:
|
|
||||||
host = "localhost"
|
|
||||||
port = int(endpoint)
|
|
||||||
else:
|
|
||||||
host = "localhost"
|
|
||||||
port = 5080
|
|
||||||
|
|
||||||
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__":
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/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:
|
||||||
@@ -16,17 +17,15 @@ Environment:
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
import ipaddress
|
|
||||||
import os
|
import os
|
||||||
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 = 5173
|
||||||
DEFAULT_BACKEND_PORT = 5180
|
DEFAULT_BACKEND_PORT = 5180
|
||||||
FRONTEND_PATH = Path(__file__).parent.parent / "frontend"
|
FRONTEND_PATH = Path(__file__).parent.parent / "frontend"
|
||||||
@@ -45,45 +44,8 @@ BUN_BUG = """\
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def parse_endpoint(
|
|
||||||
value: str | None, default_port: int = DEFAULT_VITE_PORT
|
|
||||||
) -> tuple[str | None, int, bool]:
|
|
||||||
"""Parse an endpoint for Vite or backend.
|
|
||||||
|
|
||||||
Returns (host, port, all_ifaces).
|
|
||||||
"""
|
|
||||||
if not value:
|
|
||||||
return DEFAULT_HOST, default_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_port, False
|
|
||||||
|
|
||||||
# Use urllib.parse for everything else
|
|
||||||
parsed = urlparse(f"//{value}")
|
|
||||||
host = parsed.hostname or DEFAULT_HOST
|
|
||||||
port = parsed.port or default_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.
|
||||||
|
|
||||||
@@ -114,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)
|
||||||
@@ -127,13 +92,15 @@ def resolve_frontend_tools(
|
|||||||
return install_cmd, dev_cmd, name
|
return install_cmd, dev_cmd, name
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_backend(backend_host: str, backend_port: int):
|
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://{backend_host}:{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:
|
||||||
@@ -163,23 +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,
|
vite_port: int,
|
||||||
all_ifaces: bool,
|
all_ifaces: bool,
|
||||||
backend_host: str,
|
backend_host: str,
|
||||||
backend_port: int,
|
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["FASTAPI_VUE_BACKEND_URL"] = f"http://{backend_host}:{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)
|
||||||
|
|
||||||
@@ -197,15 +170,6 @@ 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",
|
|
||||||
backend_host,
|
|
||||||
"--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)
|
||||||
|
|
||||||
@@ -287,15 +251,26 @@ def main():
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
vite_host, vite_port, all_ifaces = parse_endpoint(args.frontend, DEFAULT_VITE_PORT)
|
# parse_endpoint returns list of dicts with host/port or uds keys
|
||||||
backend_host, backend_port, _ = parse_endpoint(args.backend, DEFAULT_BACKEND_PORT)
|
# Multiple entries means bind all interfaces (IPv4 + IPv6)
|
||||||
# Backend host defaults to localhost (never None)
|
vite_endpoints = parse_endpoint(args.frontend, DEFAULT_VITE_PORT)
|
||||||
backend_host = backend_host or DEFAULT_HOST
|
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(
|
asyncio.run(run_devserver(vite_port, all_ifaces, backend_host, backend_port))
|
||||||
run_devserver(vite_host, vite_port, all_ifaces, backend_host, backend_port)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -51,10 +51,9 @@ def find_build_tool():
|
|||||||
"npm": ("install",),
|
"npm": ("install",),
|
||||||
"bun": ("--bun", "install"),
|
"bun": ("--bun", "install"),
|
||||||
}
|
}
|
||||||
# Use build-only for deno to avoid npm-run-all2 issues with run-p
|
# Run vite directly for deno to avoid npm-run-all2/run-p issues
|
||||||
# (run-p tries to spawn npm which doesn't exist in deno)
|
|
||||||
build = {
|
build = {
|
||||||
"deno": ("task", "build-only"),
|
"deno": ("run", "-A", "npm:vite", "build"),
|
||||||
"npm": ("run", "build"),
|
"npm": ("run", "build"),
|
||||||
"bun": ("--bun", "run", "build"),
|
"bun": ("--bun", "run", "build"),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user