Compare commits

..
4 Commits
4 changed files with 33 additions and 32 deletions
+8 -6
View File
@@ -580,7 +580,7 @@ def patch_vite_config(
# Check if content actually changed # Check if content actually changed
if content == original_content: if content == original_content:
print(f" Skipping {path} (no changes needed)") print(f" Skipping {path} (no changes needed)")
return False return False
if dry_run: if dry_run:
@@ -618,7 +618,7 @@ def patch_frontend_health_check(frontend_dir: Path, dry_run: bool = False) -> bo
target_file = app_vue target_file = app_vue
if target_file is None: if target_file is None:
print(" No Vue file found to patch, skipping frontend health check") print(" No Vue file found to patch, skipping frontend health check")
return False return False
original_content = target_file.read_text("UTF-8") original_content = target_file.read_text("UTF-8")
@@ -663,7 +663,7 @@ def patch_frontend_health_check(frontend_dir: Path, dry_run: bool = False) -> bo
else: else:
# Minimal App.vue - only patch if it contains the default welcome message # Minimal App.vue - only patch if it contains the default welcome message
if "<h1>You did it!</h1>" not in content: if "<h1>You did it!</h1>" not in content:
print(f" Skipping {target_file} (not a default Vue template)") print(f" Skipping {target_file} (not a default Vue template)")
return False return False
# Insert before the </p> tag # Insert before the </p> tag
template_end = content.find("</template>") template_end = content.find("</template>")
@@ -680,7 +680,7 @@ def patch_frontend_health_check(frontend_dir: Path, dry_run: bool = False) -> bo
# Check if content actually changed # Check if content actually changed
if content == original_content: if content == original_content:
print(f" Skipping {target_file} (no changes needed)") print(f" Skipping {target_file} (no changes needed)")
return False return False
if dry_run: if dry_run:
@@ -714,7 +714,7 @@ def write_file(
""" """
exists = path.exists() exists = path.exists()
if exists and not overwrite: if exists and not overwrite:
print(f" Skipping {path} (exists)") print(f" Skipping {path} (exists)")
return False return False
# Check if content is the same # Check if content is the same
@@ -1163,7 +1163,9 @@ def cmd_setup(args: argparse.Namespace) -> int:
overwrite=False, overwrite=False,
dry_run=dry_run, dry_run=dry_run,
) )
# else: no file but has existing entrypoint - don't create (user has custom CLI setup) else:
# No file but has existing entrypoint - don't create (user has custom CLI setup)
print(f"️ Skipping __main__.py (package already has CLI: {existing_cli})")
# === Update vite.config.js/ts === # === Update vite.config.js/ts ===
frontend_dir = project_dir / "frontend" frontend_dir = project_dir / "frontend"
+6 -14
View File
@@ -4,13 +4,11 @@
import argparse import argparse
import asyncio import asyncio
import contextlib
import os import os
import sys import sys
from contextlib import suppress
from pathlib import Path from pathlib import Path
from fastapi_vue import server
# Import util.py from scripts/fastapi-vue (not a package, so we adjust sys.path) # 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"))) sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue")))
from devutil import ( # type: ignore from devutil import ( # type: ignore
@@ -34,7 +32,7 @@ async def run_devserver(frontend: str, backend: str) -> None:
raise SystemExit(1) raise SystemExit(1)
viteurl, npm_install, vite = setup_vite(frontend, DEFAULT_VITE_PORT) viteurl, npm_install, vite = setup_vite(frontend, DEFAULT_VITE_PORT)
backurl, module, backend_config = setup_fastapi( backurl, uvicorn = setup_fastapi(
backend, "MODULE_NAME.APP_MODULE:APP_VAR", DEFAULT_DEV_PORT backend, "MODULE_NAME.APP_MODULE:APP_VAR", DEFAULT_DEV_PORT
) )
@@ -45,15 +43,8 @@ async def run_devserver(frontend: str, backend: str) -> None:
async with ProcessGroup() as pg: async with ProcessGroup() as pg:
npm_i = await pg.spawn(*npm_install, cwd=front) npm_i = await pg.spawn(*npm_install, cwd=front)
await check_ports_free(viteurl, backurl) await check_ports_free(viteurl, backurl)
await pg.spawn(*uvicorn)
# 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")) await pg.wait(npm_i, ready(backurl, path="/api/health?from=devserver.py"))
# Start Vite dev server (ProcessGroup waits for any exit, then terminates others)
await pg.spawn(*vite, cwd=front) await pg.spawn(*vite, cwd=front)
@@ -75,7 +66,7 @@ def main():
help=f"FastAPI backend endpoint (default: localhost:{DEFAULT_DEV_PORT})", help=f"FastAPI backend endpoint (default: localhost:{DEFAULT_DEV_PORT})",
) )
args = parser.parse_args() args = parser.parse_args()
with contextlib.suppress(KeyboardInterrupt): with suppress(KeyboardInterrupt):
asyncio.run(run_devserver(args.frontend, args.backend)) asyncio.run(run_devserver(args.frontend, args.backend))
@@ -84,7 +75,8 @@ HELP_EPILOG = """
scripts/devserver.py 3000 # Vite on localhost:3000 scripts/devserver.py 3000 # Vite on localhost:3000
scripts/devserver.py :3000 --backend 8000 # *:3000, localhost:8000 scripts/devserver.py :3000 --backend 8000 # *:3000, localhost:8000
JS_RUNTIME environment variable can be used to select the JS runtime JS_RUNTIME environment variable can be used to select the JS runtime:
npm, deno, bun, or full path to the runtime executable (node maps to npm).
""" """
+1 -1
View File
@@ -179,7 +179,7 @@ def build(folder: str = "frontend") -> None:
raise SystemExit(1) raise SystemExit(1)
def run(cmd): def run(cmd):
display_cmd = [Path(cmd[0]).name, *cmd[1:]] display_cmd = [Path(cmd[0]).stem, *cmd[1:]]
logger.info("### %s", " ".join(display_cmd)) logger.info("### %s", " ".join(display_cmd))
subprocess.run(cmd, check=True, cwd=folder) subprocess.run(cmd, check=True, cwd=folder)
+18 -11
View File
@@ -2,6 +2,7 @@
import asyncio import asyncio
import subprocess import subprocess
import sys
from collections.abc import Coroutine from collections.abc import Coroutine
from contextlib import suppress from contextlib import suppress
from pathlib import Path from pathlib import Path
@@ -23,7 +24,7 @@ class ProcessGroup:
self, *cmd: str, cwd: str | None = None self, *cmd: str, cwd: str | None = None
) -> asyncio.subprocess.Process: ) -> asyncio.subprocess.Process:
"""Spawn a subprocess and track it.""" """Spawn a subprocess and track it."""
cmd_name = Path(cmd[0]).name cmd_name = Path(cmd[0]).stem
logger.info(">>> %s", " ".join([cmd_name, *cmd[1:]])) logger.info(">>> %s", " ".join([cmd_name, *cmd[1:]]))
proc = await asyncio.create_subprocess_exec(*cmd, cwd=cwd) proc = await asyncio.create_subprocess_exec(*cmd, cwd=cwd)
self._procs.append(proc) self._procs.append(proc)
@@ -159,10 +160,10 @@ def setup_vite(
def setup_fastapi( def setup_fastapi(
endpoint: str, module: str, default_port: int = 8000 endpoint: str, module: str, default_port: int = 8000
) -> tuple[str, str, dict]: ) -> tuple[str, list[str]]:
"""Parse backend endpoint and build server.run() config. """Parse backend endpoint and build uvicorn command.
Returns (url, module, config_dict). Returns (url, uvicorn_cmd).
Raises SystemExit(1) on invalid config. Raises SystemExit(1) on invalid config.
""" """
endpoints = parse_endpoint(endpoint, default_port) endpoints = parse_endpoint(endpoint, default_port)
@@ -173,11 +174,17 @@ def setup_fastapi(
host = endpoints[0]["host"] host = endpoints[0]["host"]
port = endpoints[0]["port"] port = endpoints[0]["port"]
reload_dir = module.split(".")[0] # Don't reload on frontend changes
config = { cmd = [
"listen": f"{host}:{port}", sys.executable,
"reload": True, "-m",
"reload_dirs": [module.split(".")[0]], # Don't reload on frontend changes "uvicorn",
"forwarded_allow_ips": "*", module,
} f"--host={host}",
return f"http://{host}:{port}", module, config f"--port={port}",
"--reload",
f"--reload-dir={reload_dir}",
"--forwarded-allow-ips=*",
]
return f"http://{host}:{port}", cmd