Refactor with new fastapi-vue utility module to run the server, used in CLI main and devserver to run the main app. Default ports made part of templating, so that --ports can be easily specified to fastapi-vue-setup to customize (respects earlier choices when no --ports is specified). New overall default ports 3100, 3200.

This commit is contained in:
2026-02-02 19:56:31 +00:00
parent 9a9a00380f
commit a84041a923
4 changed files with 157 additions and 64 deletions
+115 -1
View File
@@ -7,6 +7,7 @@ Usage:
Options:
--module-name NAME Python module name (auto-detected from pyproject.toml)
--ports DEFAULT,VITE,DEV Port configuration (default: 3100,3100,3200)
--dry-run Show what would be done without making changes
"""
@@ -24,6 +25,10 @@ import tomlkit
# Template directory
TEMPLATE_DIR = Path(__file__).parent / "template"
# Default ports: (default, vite, dev)
# If vite == dev, dev is incremented by 100
DEFAULT_PORTS = (3100, 3100, 3200)
# Marker comment indicating file can be auto-upgraded
# Users should remove this line to prevent automatic updates
UPGRADE_MARKER = "auto-upgrade@fastapi-vue-setup"
@@ -117,6 +122,80 @@ Next steps:
# =============================================================================
def parse_ports(ports_str: str | None) -> tuple[int, int, int]:
"""Parse comma-separated port string into (default, vite, dev) tuple.
If dev == vite, dev is incremented by 100 to avoid conflicts.
"""
if not ports_str:
return DEFAULT_PORTS
parts = ports_str.split(",")
if len(parts) == 1:
default = int(parts[0])
vite = default
dev = default + 100
elif len(parts) == 2:
default = int(parts[0])
vite = int(parts[1])
dev = vite + 100 if vite == default else default + 100
elif len(parts) == 3:
default = int(parts[0])
vite = int(parts[1])
dev = int(parts[2])
else:
raise ValueError(f"Invalid ports format: {ports_str}")
# Auto-adjust dev if it conflicts with vite
if dev == vite:
dev = vite + 100
return default, vite, dev
def extract_existing_ports(project_dir: Path) -> tuple[int, int, int] | None:
"""Extract existing port configuration from project files.
Returns (default, vite, dev) or None if not found.
"""
module_name = find_module_name(project_dir)
if not module_name:
return None
default_port = None
vite_port = None
dev_port = None
# Try to extract DEFAULT_PORT from __main__.py
main_file = project_dir / module_name / "__main__.py"
if main_file.exists():
content = main_file.read_text("UTF-8")
match = re.search(r"DEFAULT_PORT\s*=\s*(\d+)", content)
if match:
default_port = int(match.group(1))
# Try to extract ports from devserver.py
devserver_file = project_dir / "scripts" / "devserver.py"
if devserver_file.exists():
content = devserver_file.read_text("UTF-8")
match = re.search(r"DEFAULT_VITE_PORT\s*=\s*(\d+)", content)
if match:
vite_port = int(match.group(1))
match = re.search(r"DEFAULT_DEV_PORT\s*=\s*(\d+)", content)
if match:
dev_port = int(match.group(1))
# Return only if we found at least one port
if default_port is not None or vite_port is not None or dev_port is not None:
return (
default_port or DEFAULT_PORTS[0],
vite_port or DEFAULT_PORTS[1],
dev_port or DEFAULT_PORTS[2],
)
return None
def load_template(path: str) -> str:
"""Load a template file from the template directory."""
return (TEMPLATE_DIR / path).read_text("UTF-8")
@@ -943,10 +1022,31 @@ def cmd_setup(args: argparse.Namespace) -> int:
print(f"📦 Module: {module_name}")
# Determine port configuration
# Priority: --ports argument > existing project values > defaults
if args.ports:
default_port, vite_port, dev_port = parse_ports(args.ports)
ports_note = "(--ports)"
else:
existing_ports = extract_existing_ports(project_dir)
if existing_ports:
default_port, vite_port, dev_port = existing_ports
ports_note = "(kept for upgrade)"
else:
default_port, vite_port, dev_port = DEFAULT_PORTS
ports_note = "(--ports to override)"
print(
f"📡 Ports: default={default_port}, vite={vite_port}, dev={dev_port} {ports_note}"
)
# Template variables
tpl_vars = {
"MODULE_NAME": module_name,
"PROJECT_TITLE": project_title,
"TEMPLATE_DEFAULT_PORT": str(default_port),
"TEMPLATE_VITE_PORT": str(vite_port),
"TEMPLATE_DEV_PORT": str(dev_port),
}
module_dir = project_dir / module_name
@@ -1137,7 +1237,15 @@ def cmd_setup(args: argparse.Namespace) -> int:
print("✅ Created .gitignore")
# === Add dependencies using uv ===
uv_add_main = ["uv", "add", "-q", "-U", "--no-sync", "fastapi[standard]", "fastapi-vue"]
uv_add_main = [
"uv",
"add",
"-q",
"-U",
"--no-sync",
"fastapi[standard]",
"fastapi-vue",
]
uv_add_dev = ["uv", "add", "-q", "-U", "--group", "dev", "httpx"]
if dry_run:
print(f"[DRY RUN] Would run: {' '.join(uv_add_main)}")
@@ -1215,6 +1323,7 @@ Examples:
fastapi-vue-setup my-new-project Create a new project from scratch
fastapi-vue-setup . Set up integration in current directory
fastapi-vue-setup . --dry-run Preview what would be done
fastapi-vue-setup . --ports 8000,5173,8080 Custom ports (default,vite,dev)
""",
)
parser.add_argument(
@@ -1224,6 +1333,11 @@ Examples:
help="Project directory (use . for current directory)",
)
parser.add_argument("--module-name", help="Python module name (auto-detected)")
parser.add_argument(
"--ports",
metavar="DEFAULT,VITE,DEV",
help="Port configuration as comma-separated values (default: 3100,3100,3200)",
)
parser.add_argument(
"--dry-run", action="store_true", help="Show what would be done"
)
+11 -30
View File
@@ -1,44 +1,25 @@
# auto-upgrade@fastapi-vue-setup - remove this if you modify this file
import argparse
import asyncio
import os
from fastapi_vue.hostutil import parse_endpoint
from uvicorn import Config, Server
from fastapi_vue import server
DEFAULT_PORT = 5080
def run_server(endpoints: list[dict], *, proxy=""):
conf: dict[str, object] = {"app": "MODULE_NAME.APP_MODULE:APP_VAR"}
if proxy:
conf["proxy_headers"] = True
conf["forwarded_allow_ips"] = proxy
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())
DEFAULT_PORT = TEMPLATE_DEFAULT_PORT
def main():
parser = argparse.ArgumentParser(description="Run the MODULE_NAME server.")
parser.add_argument(
"endpoint",
nargs="?",
help=(
f"Endpoint (default: localhost:{DEFAULT_PORT}). "
"Forms: host:port | :port | [ipv6]:port | ip | host | unix:/path.sock"
),
"-l",
"--listen",
action="append",
help=(f"Endpoint (default: localhost:{DEFAULT_PORT})."),
)
args = parser.parse_args()
proxy = os.getenv("FORWARDED_ALLOW_IPS", "127.0.0.1,::1")
try:
run_server(parse_endpoint(args.endpoint, DEFAULT_PORT), proxy=proxy)
except KeyboardInterrupt:
pass
server.run(
"MODULE_NAME.APP_MODULE:APP_VAR",
listen=args.listen,
default_port=DEFAULT_PORT,
)
if __name__ == "__main__":
+15 -11
View File
@@ -9,6 +9,8 @@ import os
import sys
from pathlib import Path
from fastapi_vue import server
# Import util.py from scripts/fastapi-vue (not a package, so we adjust sys.path)
sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue")))
from devutil import ( # type: ignore
@@ -20,6 +22,9 @@ from devutil import ( # type: ignore
setup_vite,
)
DEFAULT_VITE_PORT = TEMPLATE_VITE_PORT
DEFAULT_DEV_PORT = TEMPLATE_DEV_PORT
async def run_devserver(frontend: str, backend: str) -> None:
reporoot = Path(__file__).parent.parent
@@ -28,8 +33,10 @@ async def run_devserver(frontend: str, backend: str) -> None:
logger.warning("Frontend source not found at %s", front)
raise SystemExit(1)
viteurl, npm_install, vite = setup_vite(frontend)
backurl, fastapi = setup_fastapi(backend, "MODULE_NAME.APP_MODULE:APP_VAR")
viteurl, npm_install, vite = setup_vite(frontend, DEFAULT_VITE_PORT)
backurl, module, backend_config = setup_fastapi(
backend, "MODULE_NAME.APP_MODULE:APP_VAR", DEFAULT_DEV_PORT
)
# Tell the everyone where the frontend and backend are (vite proxy, etc)
os.environ["FASTAPI_VUE_FRONTEND_URL"] = viteurl
@@ -38,13 +45,10 @@ async def run_devserver(frontend: str, backend: str) -> None:
async with ProcessGroup() as pg:
npm_i = await pg.spawn(*npm_install, cwd=front)
await check_ports_free(viteurl, backurl)
await pg.spawn(
*fastapi,
"--reload",
"--reload-dir=MODULE_NAME", # Don't reload on frontend changes
"--forwarded-allow-ips=*",
cwd=reporoot,
)
# Run backend in a thread (server.run is blocking)
loop = asyncio.get_event_loop()
loop.run_in_executor(None, lambda: server.run(module, **backend_config))
# Wait for both install and backend to be ready
await pg.wait(npm_i, ready(backurl, path="/api/health?from=devserver.py"))
@@ -63,12 +67,12 @@ def main():
"frontend",
nargs="?",
metavar="host:port",
help="Vite frontend endpoint (default: localhost:5173)",
help=f"Vite frontend endpoint (default: localhost:{DEFAULT_VITE_PORT})",
)
parser.add_argument(
"--backend",
metavar="host:port",
help="FastAPI backend endpoint (default: localhost:5180)",
help=f"FastAPI backend endpoint (default: localhost:{DEFAULT_DEV_PORT})",
)
args = parser.parse_args()
with contextlib.suppress(KeyboardInterrupt):
+16 -22
View File
@@ -11,9 +11,6 @@ import httpx
from buildutil import find_dev_tool, find_install_tool, logger
from fastapi_vue.hostutil import parse_endpoint
DEFAULT_VITE_PORT = 5173
DEFAULT_BACKEND_PORT = 5180
class ProcessGroup:
"""Manage async subprocesses with automatic cleanup, like TaskGroup for processes."""
@@ -134,13 +131,15 @@ async def ready(url: str, path: str = "") -> None:
await asyncio.sleep(0.1)
def setup_vite(endpoint: str) -> tuple[str, list[str], list[str]]:
def setup_vite(
endpoint: str, default_port: int = 5173
) -> tuple[str, list[str], list[str]]:
"""Parse frontend endpoint and build commands.
Returns (url, install_cmd, dev_cmd).
Raises SystemExit(1) on invalid config.
"""
endpoints = parse_endpoint(endpoint, DEFAULT_VITE_PORT)
endpoints = parse_endpoint(endpoint, default_port)
if "uds" in endpoints[0]:
logger.warning("Unix sockets not supported with vite devserver")
@@ -153,18 +152,17 @@ def setup_vite(endpoint: str) -> tuple[str, list[str], list[str]]:
dev_cmd = find_dev_tool()
if host != "localhost":
dev_cmd.append("--host" if len(endpoints) > 1 else f"--host={host}")
if port != 5173:
dev_cmd.append(f"--port={port}")
dev_cmd.append(f"--port={port}")
return f"http://{host}:{port}", install_cmd, dev_cmd
def setup_fastapi(
endpoint: str, module: str, default_port: int = DEFAULT_BACKEND_PORT
) -> tuple[str, list[str]]:
"""Parse backend endpoint and build fastapi dev command.
endpoint: str, module: str, default_port: int = 8000
) -> tuple[str, str, dict]:
"""Parse backend endpoint and build server.run() config.
Returns (url, cmd).
Returns (url, module, config_dict).
Raises SystemExit(1) on invalid config.
"""
endpoints = parse_endpoint(endpoint, default_port)
@@ -176,14 +174,10 @@ def setup_fastapi(
host = endpoints[0]["host"]
port = endpoints[0]["port"]
cmd = [
"fastapi",
"dev",
"--entrypoint",
module,
"--host",
host,
"--port",
str(port),
]
return f"http://{host}:{port}", cmd
config = {
"listen": f"{host}:{port}",
"reload": True,
"reload_dirs": [module.split(".")[0]], # Don't reload on frontend changes
"forwarded_allow_ips": "*",
}
return f"http://{host}:{port}", module, config