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:
+115
-1
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user