diff --git a/fastapi_vue_setup.py b/fastapi_vue_setup.py index 43d9545..1dddcd0 100644 --- a/fastapi_vue_setup.py +++ b/fastapi_vue_setup.py @@ -110,6 +110,9 @@ def uv_add_packages( # If vite == dev, dev is incremented by 100 DEFAULT_PORTS = (3100, 3100, 3200) +# Default health check path for devserver backend readiness check +DEFAULT_HEALTH = "/api/health?from=devserver.py" + # Marker comment indicating file can be auto-upgraded # Users should remove this line to prevent automatic updates UPGRADE_MARKER = "auto-upgrade@fastapi-vue-setup" @@ -300,6 +303,29 @@ def extract_existing_ports( return None +# Sentinel for "not found" in extract_existing_health +_HEALTH_NOT_FOUND = object() + + +def extract_existing_health(project_dir: Path) -> str | object: + """Extract existing health path configuration from devserver.py. + + Returns: + - The path string (may be empty to disable) + - _HEALTH_NOT_FOUND sentinel if not found or file doesn't exist + """ + devserver_file = project_dir / "scripts" / "devserver.py" + if not devserver_file.exists(): + return _HEALTH_NOT_FOUND + + content = devserver_file.read_text("UTF-8") + # Match HEALTH = "/path" or HEALTH = "" + match = re.search(r'^HEALTH\s*=\s*"([^"]*)"', content, re.MULTILINE) + if match: + return match.group(1) + return _HEALTH_NOT_FOUND + + def load_template(path: str) -> str: """Load a template file from the template directory.""" return (TEMPLATE_DIR / path).read_text("UTF-8") @@ -1323,6 +1349,27 @@ def cmd_setup(args: argparse.Namespace) -> int: f"📡 Ports: default={default_port}, vite={vite_port}, dev={dev_port} {ports_note}" ) + # Determine health path configuration + # Priority: --health argument > existing project value > default + if args.health is not None: + health = args.health + health_note = "(--health)" if health else "(disabled via --health)" + else: + existing_health = extract_existing_health(project_dir) + if existing_health is not _HEALTH_NOT_FOUND: + # Explicitly configured (path string, possibly empty to disable) + health = existing_health + health_note = "(kept for upgrade)" + else: + # Not found - use default + health = DEFAULT_HEALTH + health_note = "(--health to override)" + + if health: + print(f"🏥 Health check: {health} {health_note}") + else: + print(f"🏥 Health check: disabled {health_note}") + # Title for templates project_title = module_name.replace("_", " ").title() @@ -1338,6 +1385,7 @@ def cmd_setup(args: argparse.Namespace) -> int: "TEMPLATE_DEFAULT_PORT": str(default_port), "TEMPLATE_VITE_PORT": str(vite_port), "TEMPLATE_DEV_PORT": str(dev_port), + "TEMPLATE_HEALTH": f'"{health}"', "ENVPREFIX": module_name.upper(), "PROJECT_CLI": module_name, "MAIN_MODULE": main_module_path, @@ -1635,6 +1683,11 @@ Examples: metavar="BACKEND,VITE,DEV", help="Port configuration as comma-separated values (default: 3100,3100,3200)", ) + parser.add_argument( + "--health", + metavar="PATH", + help="Health check path for devserver (default: /api/health?from=devserver.py, '' to disable)", + ) parser.add_argument( "--dry", "--dry-run", action="store_true", help="Show what would be done" ) diff --git a/template/scripts/devserver.py b/template/scripts/devserver.py index 6a76b84..51769f4 100755 --- a/template/scripts/devserver.py +++ b/template/scripts/devserver.py @@ -22,6 +22,7 @@ from devutil import ( # type: ignore DEFAULT_VITE_PORT = TEMPLATE_VITE_PORT DEFAULT_DEV_PORT = TEMPLATE_DEV_PORT +HEALTH = TEMPLATE_HEALTH async def run_devserver( @@ -45,7 +46,7 @@ async def run_devserver( npm_i = await pg.spawn(*npm_install, cwd=front) await check_ports_free(viteurl, backurl) await pg.spawn(*MODULE_NAME, *(extra_args or [])) - await pg.wait(npm_i, ready(backurl, path="/api/health?from=devserver.py")) + await pg.wait(npm_i, ready(backurl, path=HEALTH)) await pg.spawn(*vite, cwd=front) diff --git a/template/scripts/fastapi-vue/devutil.py b/template/scripts/fastapi-vue/devutil.py index 66c4744..f4cf749 100644 --- a/template/scripts/fastapi-vue/devutil.py +++ b/template/scripts/fastapi-vue/devutil.py @@ -111,18 +111,19 @@ async def check_ports_free(*urls: str) -> None: await asyncio.gather(*[check(client, url) for url in urls]) -async def ready(url: str, path: str = "") -> None: +async def ready(url: str, path: str = "", max_attempts=50) -> None: """Wait for the server to be ready by polling an endpoint. + Use empty path to disable the check and make this return immediately. Raises SystemExit(1) if server doesn't start in time. """ - max_attempts = 50 - full_url = f"{url}{path}" + if not path: + return async with httpx.AsyncClient() as client: for attempt in range(max_attempts): try: - await client.get(full_url, timeout=1.0) + await client.get(f"{url}{path}", timeout=1.0) logger.info("✓ Backend ready!") return except httpx.RequestError: