Compare commits

...
1 Commits
2 changed files with 102 additions and 25 deletions
+101 -24
View File
@@ -427,10 +427,43 @@ def _find_app_in_subpackage(subpkg_dir: Path) -> tuple[Path, str] | None:
return None return None
def _find_existing_cli_entrypoint(project_dir: Path, module_name: str) -> str | None: def _add_devmode_to_main(content: str) -> str:
"""Add DEVMODE variable to an existing main module."""
lines = content.splitlines()
# Check if os is imported
has_os_import = any("import os" in line for line in lines)
# Find the first import line
insert_idx = 0
for i, line in enumerate(lines):
stripped = line.strip()
if stripped.startswith("import ") or stripped.startswith("from "):
insert_idx = i + 1
elif stripped and not stripped.startswith("#"):
break
# Insert imports and DEVMODE after existing imports
new_lines = []
if not has_os_import:
new_lines.append("import os")
new_lines.extend(
[
"",
"# Added by fastapi-vue-setup",
'DEVMODE = os.getenv("ENVPREFIX_DEV") == "1"',
"",
]
)
lines[insert_idx:insert_idx] = new_lines
return "\n".join(lines)
def _find_existing_cli_module_path(project_dir: Path, module_name: str) -> str | None:
"""Check if pyproject.toml already has a CLI entrypoint for this module. """Check if pyproject.toml already has a CLI entrypoint for this module.
Returns the entrypoint string if found, None otherwise. Returns the module path (e.g., 'module.subpkg.__main__') if found, None otherwise.
""" """
pyproject = project_dir / "pyproject.toml" pyproject = project_dir / "pyproject.toml"
if not pyproject.exists(): if not pyproject.exists():
@@ -448,7 +481,10 @@ def _find_existing_cli_entrypoint(project_dir: Path, module_name: str) -> str |
# Look for any script that references our module # Look for any script that references our module
for script_name, entry in scripts.items(): for script_name, entry in scripts.items():
if isinstance(entry, str) and entry.startswith(f"{module_name}."): if isinstance(entry, str) and entry.startswith(f"{module_name}."):
return f'{script_name} = "{entry}"' # Extract module path from "module.subpkg.__main__:main"
if ":" in entry:
module_path, _ = entry.rsplit(":", 1)
return module_path
return None return None
@@ -517,7 +553,7 @@ def render_template(template: str, **kwargs) -> str:
def patch_app_file( def patch_app_file(
path: Path, module_name: str, app_var: str, dry: bool = False path: Path, main_module_path: str, app_var: str, dry: bool = False
) -> bool: ) -> bool:
"""Patch an existing app.py with frontend integration. """Patch an existing app.py with frontend integration.
@@ -535,7 +571,7 @@ def patch_app_file(
# Check what's already patched # Check what's already patched
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 {main_module_path} 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 has_lifespan = "await frontend.load()" in content
@@ -550,7 +586,7 @@ def patch_app_file(
if not has_frontend: if not has_frontend:
imports.extend(["from pathlib import Path", "from fastapi_vue import Frontend"]) imports.extend(["from pathlib import Path", "from fastapi_vue import Frontend"])
if not has_devmode: if not has_devmode:
imports.append(f"from {module_name}.__main__ import DEVMODE") imports.append(f"from {main_module_path} import DEVMODE")
if imports: if imports:
insert_line = find_import_insertion_line(content) insert_line = find_import_insertion_line(content)
lines = content.splitlines(keepends=True) lines = content.splitlines(keepends=True)
@@ -987,6 +1023,7 @@ def _write_fallback_file(
existing_content = fallback_path.read_text("UTF-8") existing_content = fallback_path.read_text("UTF-8")
if existing_content == content: if existing_content == content:
print(f"✔️ {fallback_path} (already up to date)") print(f"✔️ {fallback_path} (already up to date)")
_new_files_written.append((fallback_path, original_path))
return False return False
if dry: if dry:
@@ -1268,6 +1305,21 @@ def cmd_setup(args: argparse.Namespace) -> int:
# Title for templates # Title for templates
project_title = module_name.replace("_", " ").title() project_title = module_name.replace("_", " ").title()
module_dir = project_dir / module_name
scripts_dir = project_dir / "scripts"
fastapi_vue_scripts = scripts_dir / "fastapi-vue"
# Find existing FastAPI app
app_info = (
find_fastapi_app(module_dir, project_dir) if module_dir.exists() else None
)
# Check if project already has a CLI entrypoint in pyproject.toml
existing_cli_module = _find_existing_cli_module_path(project_dir, module_name)
# Determine main module path for DEVMODE import
main_module_path = existing_cli_module or f"{module_name}.__main__"
# Template variables # Template variables
tpl_vars = { tpl_vars = {
"MODULE_NAME": module_name, "MODULE_NAME": module_name,
@@ -1277,17 +1329,9 @@ def cmd_setup(args: argparse.Namespace) -> int:
"TEMPLATE_DEV_PORT": str(dev_port), "TEMPLATE_DEV_PORT": str(dev_port),
"ENVPREFIX": module_name.upper(), "ENVPREFIX": module_name.upper(),
"PROJECT_CLI": module_name, "PROJECT_CLI": module_name,
"MAIN_MODULE": main_module_path,
} }
module_dir = project_dir / module_name
scripts_dir = project_dir / "scripts"
fastapi_vue_scripts = scripts_dir / "fastapi-vue"
# Find existing FastAPI app
app_info = (
find_fastapi_app(module_dir, project_dir) if module_dir.exists() else None
)
if app_info: if app_info:
app_file, app_var = app_info app_file, app_var = app_info
print(f"📍 Found FastAPI app: {app_var} in {app_file.name}") print(f"📍 Found FastAPI app: {app_var} in {app_file.name}")
@@ -1351,7 +1395,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
# === 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
patch_app_file(app_file, module_name, app_var, dry=dry) patch_app_file(app_file, main_module_path, app_var, dry=dry)
else: else:
# No app: create full app.py # No app: create full app.py
# Create __init__.py if missing # Create __init__.py if missing
@@ -1368,14 +1412,19 @@ def cmd_setup(args: argparse.Namespace) -> int:
write_file(app_file_path, content, overwrite=False, dry=dry) write_file(app_file_path, content, overwrite=False, dry=dry)
# === Handle __main__.py === # === Handle __main__.py ===
if existing_cli_module:
# Existing CLI entrypoint - write .new.py beside the existing main module
main_module_parts = existing_cli_module.split(".")
main_module_file = project_dir / ("/".join(main_module_parts) + ".py")
main_fallback = main_module_file.with_suffix(".new.py")
else:
main_module_file = None
main_fallback = module_dir / "__main__.new.py"
main_file = module_dir / "__main__.py" main_file = module_dir / "__main__.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)
# Check if project already has a CLI entrypoint in pyproject.toml
existing_cli = _find_existing_cli_entrypoint(project_dir, module_name)
if main_file.exists(): if main_file.exists():
# File exists: update if it has the auto-upgrade marker, otherwise use fallback # File exists: update if it has the auto-upgrade marker, otherwise use fallback
write_file( write_file(
@@ -1385,7 +1434,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
dry=dry, dry=dry,
fallback_path=main_fallback, fallback_path=main_fallback,
) )
elif not existing_cli: elif not existing_cli_module:
# No file and no existing entrypoint: create new __main__.py # No file and no existing entrypoint: create new __main__.py
write_file( write_file(
main_file, main_file,
@@ -1394,8 +1443,30 @@ def cmd_setup(args: argparse.Namespace) -> int:
dry=dry, dry=dry,
) )
else: else:
# No file but has existing entrypoint - don't create (user has custom CLI setup) # Existing CLI entrypoint: write our template as .new.py beside the existing module
print(f"️ Skipping __main__.py (package already has CLI: {existing_cli})") # and also patch the existing module with DEVMODE if needed
print(f"️ Using existing CLI: {existing_cli_module}")
_write_fallback_file(
main_module_file or main_file,
main_fallback,
main_content,
dry=dry,
executable=False,
)
if main_module_file and main_module_file.exists():
content = main_module_file.read_text("UTF-8")
if "DEVMODE" not in content:
new_content = _add_devmode_to_main(content)
new_file = main_module_file.with_suffix(".new.py")
_write_fallback_file(
main_module_file,
new_file,
new_content,
dry=dry,
executable=False,
)
elif main_module_file:
print(f"⚠️ Existing CLI main module {main_module_file} not found")
# === Update vite.config.js/ts === # === Update vite.config.js/ts ===
frontend_dir = project_dir / "frontend" frontend_dir = project_dir / "frontend"
@@ -1498,7 +1569,13 @@ def cmd_setup(args: argparse.Namespace) -> int:
) )
print(" Please manually merge the following files:") print(" Please manually merge the following files:")
for new_file, original_file in _new_files_written: for new_file, original_file in _new_files_written:
print(f"{new_file.name}{original_file.name}") try:
new_rel = new_file.relative_to(project_dir)
orig_rel = original_file.relative_to(project_dir)
except ValueError:
new_rel = new_file.name
orig_rel = original_file.name
print(f"{new_rel}{orig_rel}")
print() print()
# Clear the list for potential subsequent runs # Clear the list for potential subsequent runs
_new_files_written.clear() _new_files_written.clear()
+1 -1
View File
@@ -3,7 +3,7 @@ from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
from fastapi_vue import Frontend from fastapi_vue import Frontend
from MODULE_NAME.__main__ import DEVMODE from MAIN_MODULE import DEVMODE
# Vue Frontend static files # Vue Frontend static files
frontend = Frontend(Path(__file__).with_name("frontend-build")) frontend = Frontend(Path(__file__).with_name("frontend-build"))