Better handling of upgrades, an option to avoid overwriting main and devserver scripts, writing .new.py instead. Error out on unix socket in devserver.
This commit is contained in:
+96
-35
@@ -23,6 +23,10 @@ import tomlkit
|
|||||||
# Template directory
|
# Template directory
|
||||||
TEMPLATE_DIR = Path(__file__).parent / "template"
|
TEMPLATE_DIR = Path(__file__).parent / "template"
|
||||||
|
|
||||||
|
# Marker comment indicating file can be auto-upgraded
|
||||||
|
# Users should remove this line to prevent automatic updates
|
||||||
|
UPGRADE_MARKER = "auto-upgrade@fastapi-vue-setup"
|
||||||
|
|
||||||
# pyproject.toml additions for patched projects
|
# pyproject.toml additions for patched projects
|
||||||
PYPROJECT_ADDITIONS = {
|
PYPROJECT_ADDITIONS = {
|
||||||
"tool": {
|
"tool": {
|
||||||
@@ -471,14 +475,23 @@ def patch_frontend_health_check(frontend_dir: Path, dry_run: bool = False) -> bo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Track .new.py files written during setup (for merge notification)
|
||||||
|
_new_files_written: list[tuple[Path, Path]] = []
|
||||||
|
|
||||||
|
|
||||||
def write_file(
|
def write_file(
|
||||||
path: Path,
|
path: Path,
|
||||||
content: str,
|
content: str,
|
||||||
overwrite: bool = True,
|
overwrite: bool = True,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
executable: bool = False,
|
executable: bool = False,
|
||||||
|
fallback_path: Path | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Write content to a file, handling existing files and dry-run."""
|
"""Write content to a file, handling existing files and dry-run.
|
||||||
|
|
||||||
|
If fallback_path is provided and the file exists without the upgrade marker,
|
||||||
|
the content will be written to fallback_path instead of being skipped.
|
||||||
|
"""
|
||||||
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)")
|
||||||
@@ -491,6 +504,16 @@ def write_file(
|
|||||||
print(f"✔️ {path} (already up to date)")
|
print(f"✔️ {path} (already up to date)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# If overwrite requested but file doesn't have upgrade marker
|
||||||
|
if overwrite and UPGRADE_MARKER not in existing_content:
|
||||||
|
if fallback_path is not None:
|
||||||
|
# Write to fallback path instead
|
||||||
|
return _write_fallback_file(
|
||||||
|
path, fallback_path, content, dry_run, executable
|
||||||
|
)
|
||||||
|
print(f"ℹ️ Skipping {path} (customized by user)")
|
||||||
|
return False
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
action = "overwrite" if exists else "create"
|
action = "overwrite" if exists else "create"
|
||||||
print(f"[DRY RUN] Would {action} {path}")
|
print(f"[DRY RUN] Would {action} {path}")
|
||||||
@@ -505,6 +528,34 @@ def write_file(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _write_fallback_file(
|
||||||
|
original_path: Path,
|
||||||
|
fallback_path: Path,
|
||||||
|
content: str,
|
||||||
|
dry_run: bool,
|
||||||
|
executable: bool,
|
||||||
|
) -> bool:
|
||||||
|
"""Write content to a fallback .new.py file when original can't be overwritten."""
|
||||||
|
# Check if fallback already has same content
|
||||||
|
if fallback_path.exists():
|
||||||
|
if fallback_path.read_text() == content:
|
||||||
|
print(f"✔️ {fallback_path} (already up to date)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print(f"[DRY RUN] Would create {fallback_path} (original customized by user)")
|
||||||
|
_new_files_written.append((fallback_path, original_path))
|
||||||
|
return True
|
||||||
|
|
||||||
|
fallback_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fallback_path.write_text(content)
|
||||||
|
if executable and sys.platform != "win32":
|
||||||
|
fallback_path.chmod(fallback_path.stat().st_mode | 0o111)
|
||||||
|
print(f"✅ Created {fallback_path} (original customized by user)")
|
||||||
|
_new_files_written.append((fallback_path, original_path))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def merge_pyproject(
|
def merge_pyproject(
|
||||||
data: tomlkit.TOMLDocument, additions: dict, module_name: str
|
data: tomlkit.TOMLDocument, additions: dict, module_name: str
|
||||||
) -> tomlkit.TOMLDocument:
|
) -> tomlkit.TOMLDocument:
|
||||||
@@ -768,27 +819,40 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|||||||
module_dir.mkdir(parents=True)
|
module_dir.mkdir(parents=True)
|
||||||
|
|
||||||
# === Install scripts (always update our own scripts) ===
|
# === Install scripts (always update our own scripts) ===
|
||||||
script_files = [
|
# util.py and build-frontend.py are internal and always overwritten
|
||||||
|
# devserver.py can be customized, so use fallback if needed
|
||||||
|
internal_script_files = [
|
||||||
(fastapi_vue_scripts / "util.py", "scripts/fastapi-vue/util.py"),
|
(fastapi_vue_scripts / "util.py", "scripts/fastapi-vue/util.py"),
|
||||||
(
|
(
|
||||||
fastapi_vue_scripts / "build-frontend.py",
|
fastapi_vue_scripts / "build-frontend.py",
|
||||||
"scripts/fastapi-vue/build-frontend.py",
|
"scripts/fastapi-vue/build-frontend.py",
|
||||||
),
|
),
|
||||||
(scripts_dir / "devserver.py", "scripts/devserver.py"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for dest_path, template_path in script_files:
|
for dest_path, template_path in internal_script_files:
|
||||||
template = load_template(template_path)
|
template = load_template(template_path)
|
||||||
content = render_template(template, **tpl_vars)
|
content = render_template(template, **tpl_vars)
|
||||||
is_executable = dest_path.name == "devserver.py"
|
|
||||||
write_file(
|
write_file(
|
||||||
dest_path,
|
dest_path,
|
||||||
content,
|
content,
|
||||||
overwrite=True,
|
overwrite=True,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
executable=is_executable,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# devserver.py - use fallback if customized by user
|
||||||
|
devserver_path = scripts_dir / "devserver.py"
|
||||||
|
devserver_fallback = scripts_dir / "devserver.new.py"
|
||||||
|
template = load_template("scripts/devserver.py")
|
||||||
|
content = render_template(template, **tpl_vars)
|
||||||
|
write_file(
|
||||||
|
devserver_path,
|
||||||
|
content,
|
||||||
|
overwrite=True,
|
||||||
|
dry_run=dry_run,
|
||||||
|
executable=True,
|
||||||
|
fallback_path=devserver_fallback,
|
||||||
|
)
|
||||||
|
|
||||||
# === Handle app module ===
|
# === Handle app module ===
|
||||||
if app_file:
|
if app_file:
|
||||||
# Existing app: patch with import, route, and try to patch lifespan
|
# Existing app: patch with import, route, and try to patch lifespan
|
||||||
@@ -810,28 +874,21 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|||||||
|
|
||||||
# === Handle __main__.py ===
|
# === Handle __main__.py ===
|
||||||
main_file = module_dir / "__main__.py"
|
main_file = module_dir / "__main__.py"
|
||||||
alt_main_file = module_dir / "__main__.fastapi-vue.py"
|
main_fallback = module_dir / "__main__.new.py"
|
||||||
template = load_template("backend/__main__.py")
|
template = load_template("backend/__main__.py")
|
||||||
main_content = render_template(template, **tpl_vars)
|
main_content = render_template(template, **tpl_vars)
|
||||||
used_alt_main = False
|
|
||||||
|
|
||||||
if not main_file.exists():
|
# Use write_file with fallback - it handles all cases:
|
||||||
# No __main__.py, create it
|
# - File doesn't exist: create it
|
||||||
write_file(main_file, main_content, overwrite=False, dry_run=dry_run)
|
# - File exists with marker: update it
|
||||||
else:
|
# - File exists without marker: write to fallback
|
||||||
# Check if existing __main__.py is ours or compatible
|
write_file(
|
||||||
existing_content = main_file.read_text()
|
main_file,
|
||||||
if "fastapi_vue" in existing_content or existing_content == main_content:
|
main_content,
|
||||||
# Already ours or matches - skip or update
|
overwrite=True,
|
||||||
if existing_content != main_content:
|
dry_run=dry_run,
|
||||||
write_file(main_file, main_content, overwrite=True, dry_run=dry_run)
|
fallback_path=main_fallback,
|
||||||
else:
|
)
|
||||||
print(f"✔️ {main_file} (already up to date)")
|
|
||||||
else:
|
|
||||||
# User has their own __main__.py, install as alternate filename
|
|
||||||
print(f"⚠️ {main_file} exists and is not ours, using alternate name")
|
|
||||||
write_file(alt_main_file, main_content, overwrite=True, dry_run=dry_run)
|
|
||||||
used_alt_main = True
|
|
||||||
|
|
||||||
# === Update vite.config.js/ts ===
|
# === Update vite.config.js/ts ===
|
||||||
frontend_dir = project_dir / "frontend"
|
frontend_dir = project_dir / "frontend"
|
||||||
@@ -869,16 +926,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|||||||
if "scripts" not in updated["project"] or not updated["project"]["scripts"]:
|
if "scripts" not in updated["project"] or not updated["project"]["scripts"]:
|
||||||
updated["project"]["scripts"] = tomlkit.table()
|
updated["project"]["scripts"] = tomlkit.table()
|
||||||
script_name = module_name.replace("_", "-")
|
script_name = module_name.replace("_", "-")
|
||||||
if used_alt_main:
|
updated["project"]["scripts"][script_name] = f"{module_name}.__main__:main"
|
||||||
# Use the alternate module name with dot notation
|
|
||||||
main_module = "__main__.fastapi-vue"
|
|
||||||
updated["project"]["scripts"][script_name] = (
|
|
||||||
f"{module_name}.{main_module}:main"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
updated["project"]["scripts"][script_name] = (
|
|
||||||
f"{module_name}.__main__:main"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if content actually changed
|
# Check if content actually changed
|
||||||
new_content = tomlkit.dumps(updated)
|
new_content = tomlkit.dumps(updated)
|
||||||
@@ -946,6 +994,19 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|||||||
)
|
)
|
||||||
print(message)
|
print(message)
|
||||||
|
|
||||||
|
# Show merge note if any .new.py files were written
|
||||||
|
if _new_files_written:
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
"⚠️ Note: Some files could not be auto-upgraded because you customized them."
|
||||||
|
)
|
||||||
|
print(" Please manually merge the following files:")
|
||||||
|
for new_file, original_file in _new_files_written:
|
||||||
|
print(f" • {new_file.name} → {original_file.name}")
|
||||||
|
print()
|
||||||
|
# Clear the list for potential subsequent runs
|
||||||
|
_new_files_written.clear()
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# auto-upgrade@fastapi-vue-setup - remove this if you modify this file
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -91,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:
|
||||||
@@ -129,18 +132,27 @@ async def _terminate_process(proc: asyncio.subprocess.Process, name: str) -> Non
|
|||||||
async def run_devserver(
|
async def run_devserver(
|
||||||
vite_port: int,
|
vite_port: int,
|
||||||
all_ifaces: bool,
|
all_ifaces: bool,
|
||||||
backend_ep: dict,
|
backend_host: str,
|
||||||
|
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(vite_port, all_ifaces)
|
install_cmd, dev_cmd, tool_name = resolve_frontend_tools(vite_port, all_ifaces)
|
||||||
|
|
||||||
backend_host = backend_ep["host"]
|
|
||||||
backend_port = backend_ep["port"]
|
|
||||||
|
|
||||||
# Tell the backend where the Vite dev server is
|
# Tell the backend where the Vite dev server is
|
||||||
os.environ["FASTAPI_VUE_FRONTEND_URL"] = f"http://localhost:{vite_port}"
|
os.environ["FASTAPI_VUE_FRONTEND_URL"] = f"http://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)
|
||||||
|
|
||||||
@@ -158,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)
|
||||||
|
|
||||||
@@ -255,14 +258,19 @@ def main():
|
|||||||
|
|
||||||
# Vite doesn't support unix sockets
|
# Vite doesn't support unix sockets
|
||||||
if "uds" in vite_endpoints[0]:
|
if "uds" in vite_endpoints[0]:
|
||||||
stderr.write("┃ ⚠️ Vite does not support unix sockets\n")
|
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)
|
raise SystemExit(1)
|
||||||
|
|
||||||
vite_port = vite_endpoints[0]["port"]
|
vite_port = vite_endpoints[0]["port"]
|
||||||
all_ifaces = len(vite_endpoints) > 1
|
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(run_devserver(vite_port, all_ifaces, backend_endpoints[0]))
|
asyncio.run(run_devserver(vite_port, all_ifaces, backend_host, backend_port))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user