Patch existing projects better.
This commit is contained in:
+98
-5
@@ -95,7 +95,16 @@ PYPROJECT_ADDITIONS = {
|
|||||||
# Frontend instantiation block for patching existing apps
|
# Frontend instantiation block for patching existing apps
|
||||||
FRONTEND_BLOCK = """
|
FRONTEND_BLOCK = """
|
||||||
# Vue Frontend static files
|
# Vue Frontend static files
|
||||||
frontend = Frontend(Path(__file__).with_name("frontend-build"), cached=["/assets/"])
|
frontend = Frontend(Path(__file__).with_name("frontend-build"))
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Lifespan block for patching apps that don't have one
|
||||||
|
LIFESPAN_BLOCK = """
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
\"\"\"Manage app startup and shutdown resources.\"\"\"
|
||||||
|
await frontend.load()
|
||||||
|
yield
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TypeScript health check script for Vue components
|
# TypeScript health check script for Vue components
|
||||||
@@ -493,8 +502,9 @@ def patch_app_file(
|
|||||||
has_frontend = "from fastapi_vue import Frontend" in content
|
has_frontend = "from fastapi_vue import Frontend" in content
|
||||||
has_devmode = f"from {module_name}.__main__ import DEVMODE" in content
|
has_devmode = f"from {module_name}.__main__ import DEVMODE" in content
|
||||||
has_debug_arg = re.search(r"FastAPI\s*\([^)]*debug\s*=", content) is not None
|
has_debug_arg = re.search(r"FastAPI\s*\([^)]*debug\s*=", content) is not None
|
||||||
|
has_lifespan = "await frontend.load()" in content
|
||||||
|
|
||||||
if has_frontend and has_devmode and has_debug_arg:
|
if has_frontend and has_devmode and has_debug_arg and has_lifespan:
|
||||||
print(f"✔️ {path} (already patched)")
|
print(f"✔️ {path} (already patched)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -532,8 +542,11 @@ def patch_app_file(
|
|||||||
elif stripped and not stripped.startswith("#") and last_import_idx > 0:
|
elif stripped and not stripped.startswith("#") and last_import_idx > 0:
|
||||||
break
|
break
|
||||||
lines.insert(last_import_idx + 1, FRONTEND_BLOCK)
|
lines.insert(last_import_idx + 1, FRONTEND_BLOCK)
|
||||||
|
content = "\n".join(lines)
|
||||||
|
|
||||||
# Append route at end
|
# Append route at end (only if not already present)
|
||||||
|
if route_line not in content:
|
||||||
|
lines = content.split("\n")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(
|
lines.append(
|
||||||
"# Serve the Vue frontend (needs to be last if SPA catch-all is used)"
|
"# Serve the Vue frontend (needs to be last if SPA catch-all is used)"
|
||||||
@@ -547,9 +560,9 @@ def patch_app_file(
|
|||||||
for match in re.finditer(fastapi_pattern, content, re.DOTALL):
|
for match in re.finditer(fastapi_pattern, content, re.DOTALL):
|
||||||
args = match.group(2)
|
args = match.group(2)
|
||||||
if "debug" not in args:
|
if "debug" not in args:
|
||||||
# Add debug=DEVMODE as first argument
|
# Add debug=DEVMODE as last argument
|
||||||
if args.strip():
|
if args.strip():
|
||||||
new_args = f"debug=DEVMODE, {args}"
|
new_args = f"{args}, debug=DEVMODE"
|
||||||
else:
|
else:
|
||||||
new_args = "debug=DEVMODE"
|
new_args = "debug=DEVMODE"
|
||||||
content = (
|
content = (
|
||||||
@@ -576,6 +589,52 @@ def patch_app_file(
|
|||||||
content = content[:insert_pos] + load_code + content[insert_pos:]
|
content = content[:insert_pos] + load_code + content[insert_pos:]
|
||||||
lifespan_patched = True
|
lifespan_patched = True
|
||||||
|
|
||||||
|
# No lifespan at all: create one and wire it into FastAPI()
|
||||||
|
if not lifespan_patched and f"@{app_var}.on_event" not in content:
|
||||||
|
# Add contextlib import
|
||||||
|
if "from contextlib import asynccontextmanager" not in content:
|
||||||
|
insert_line = find_import_insertion_line(content)
|
||||||
|
lines = content.splitlines(keepends=True)
|
||||||
|
insert_idx = insert_line - 1
|
||||||
|
import_text = "from contextlib import asynccontextmanager\n"
|
||||||
|
if insert_idx >= len(lines):
|
||||||
|
content = content.rstrip("\n") + "\n" + import_text
|
||||||
|
else:
|
||||||
|
content = (
|
||||||
|
"".join(lines[:insert_idx])
|
||||||
|
+ import_text
|
||||||
|
+ "".join(lines[insert_idx:])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Insert lifespan block before the FastAPI() call
|
||||||
|
fastapi_line_pattern = r"^(\w+\s*=\s*FastAPI\s*\()"
|
||||||
|
fastapi_match = re.search(fastapi_line_pattern, content, re.MULTILINE)
|
||||||
|
if fastapi_match:
|
||||||
|
content = (
|
||||||
|
content[: fastapi_match.start()]
|
||||||
|
+ LIFESPAN_BLOCK.lstrip("\n")
|
||||||
|
+ "\n"
|
||||||
|
+ content[fastapi_match.start() :]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add lifespan=lifespan to FastAPI() call
|
||||||
|
fastapi_pattern = r"(\w+\s*=\s*FastAPI\s*\()([^)]*)\)"
|
||||||
|
fastapi_match = re.search(fastapi_pattern, content, re.DOTALL)
|
||||||
|
if fastapi_match and "lifespan" not in fastapi_match.group(2):
|
||||||
|
args = fastapi_match.group(2)
|
||||||
|
if args.strip():
|
||||||
|
new_args = f"{args}, lifespan=lifespan"
|
||||||
|
else:
|
||||||
|
new_args = "lifespan=lifespan"
|
||||||
|
content = (
|
||||||
|
content[: fastapi_match.start()]
|
||||||
|
+ fastapi_match.group(1)
|
||||||
|
+ new_args
|
||||||
|
+ ")"
|
||||||
|
+ content[fastapi_match.end() :]
|
||||||
|
)
|
||||||
|
lifespan_patched = True
|
||||||
|
|
||||||
# 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)")
|
||||||
@@ -781,6 +840,38 @@ def patch_frontend_health_check(frontend_dir: Path, dry_run: bool = False) -> bo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# SHA-256 of old vite-plugin-fastapi.js (before auto-upgrade marker was added)
|
||||||
|
# with module name replaced by MODULE_NAME in the outDir path
|
||||||
|
_OLD_VITE_PLUGIN_SHA256 = "93713e879c15a25c750a70ce1de684adeaf11b0c723c38da56e5e7ba207f6632"
|
||||||
|
|
||||||
|
|
||||||
|
def _upgrade_old_vite_plugin(
|
||||||
|
path: Path, module_name: str, dry_run: bool = False
|
||||||
|
) -> None:
|
||||||
|
"""Remove old vite-plugin-fastapi.js that lacks auto-upgrade marker.
|
||||||
|
|
||||||
|
Old versions didn't have the upgrade marker, so write_file skips them as
|
||||||
|
'customized by user'. We recognize the old version by normalizing the module
|
||||||
|
name in outDir and comparing the SHA-256 hash.
|
||||||
|
"""
|
||||||
|
if not path.exists():
|
||||||
|
return
|
||||||
|
content = path.read_text("UTF-8")
|
||||||
|
if UPGRADE_MARKER in content:
|
||||||
|
return # Already new format, write_file handles it
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
normalized = content.replace(f"../{module_name}/frontend-build", "../MODULE_NAME/frontend-build")
|
||||||
|
digest = hashlib.sha256(normalized.encode()).hexdigest()
|
||||||
|
if digest != _OLD_VITE_PLUGIN_SHA256:
|
||||||
|
return # Modified by user, don't touch
|
||||||
|
if dry_run:
|
||||||
|
print(f"🔄 Would upgrade old {path}")
|
||||||
|
return
|
||||||
|
path.unlink()
|
||||||
|
print(f"🔄 Removing old {path} (will be replaced)")
|
||||||
|
|
||||||
|
|
||||||
# Track .new.py files written during setup (for merge notification)
|
# Track .new.py files written during setup (for merge notification)
|
||||||
_new_files_written: list[tuple[Path, Path]] = []
|
_new_files_written: list[tuple[Path, Path]] = []
|
||||||
|
|
||||||
@@ -1267,6 +1358,8 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|||||||
if frontend_dir.exists():
|
if frontend_dir.exists():
|
||||||
# Install the vite plugin file (always update)
|
# Install the vite plugin file (always update)
|
||||||
plugin_file = frontend_dir / "vite-plugin-fastapi.js"
|
plugin_file = frontend_dir / "vite-plugin-fastapi.js"
|
||||||
|
# Upgrade old plugin versions that lack the auto-upgrade marker
|
||||||
|
_upgrade_old_vite_plugin(plugin_file, module_name, dry_run)
|
||||||
template = load_template("frontend/vite-plugin-fastapi.js")
|
template = load_template("frontend/vite-plugin-fastapi.js")
|
||||||
content = render_template(template, **tpl_vars)
|
content = render_template(template, **tpl_vars)
|
||||||
write_file(plugin_file, content, overwrite=True, dry_run=dry_run)
|
write_file(plugin_file, content, overwrite=True, dry_run=dry_run)
|
||||||
|
|||||||
Reference in New Issue
Block a user