Compare commits

..
5 Commits
6 changed files with 119 additions and 30 deletions
+1 -1
View File
@@ -276,6 +276,6 @@ def _devmode_respond(request: Request, name=""):
return JSONResponse( return JSONResponse(
status_code=409, status_code=409,
content={ content={
"detail": "Frontend assets served by Vite in debug mode. You are on backend, connect to frontend instead." "detail": "[devmode] Not serving frontend files here. Should you connect to Vite instead?"
}, },
) )
+113 -25
View File
@@ -50,7 +50,18 @@ def ruff_format_content(content: str, target_path: Path) -> str:
temp_file.write_text(content, "UTF-8", newline="\n") temp_file.write_text(content, "UTF-8", newline="\n")
# Sort imports first # Sort imports first
subprocess.run( subprocess.run(
["uv", "run", "--with", "ruff", "ruff", "check", "--select", "I", "--fix", str(temp_file)], [
"uv",
"run",
"--with",
"ruff",
"ruff",
"check",
"--select",
"I",
"--fix",
str(temp_file),
],
cwd=target_path.parent, cwd=target_path.parent,
capture_output=True, capture_output=True,
) )
@@ -416,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():
@@ -437,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
@@ -506,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.
@@ -524,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
@@ -539,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)
@@ -976,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:
@@ -1257,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,
@@ -1266,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}")
@@ -1340,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
@@ -1357,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(
@@ -1374,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,
@@ -1383,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"
@@ -1487,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
@@ -5,7 +5,7 @@ import os
from fastapi_vue import server from fastapi_vue import server
DEFAULT_PORT = TEMPLATE_DEFAULT_PORT DEFAULT_PORT = TEMPLATE_DEFAULT_PORT
DEVMODE = bool(os.getenv("ENVPREFIX_DEV") == "1") DEVMODE = os.getenv("ENVPREFIX_DEV") == "1"
def main(): def main():
+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"))
+1 -1
View File
@@ -11,7 +11,7 @@
*/ */
export default function fastapiVue({ paths = ["/api"] } = {}) { export default function fastapiVue({ paths = ["/api"] } = {}) {
const backendUrl = process.env.ENVPREFIX_BACKEND_URL || "http://localhost:TEMPLATE_DEFAULT_PORT" const backendUrl = process.env.ENVPREFIX_BACKEND_URL || "http://localhost:TEMPLATE_DEV_PORT"
// Build proxy configuration for each path // Build proxy configuration for each path
const proxy = {} const proxy = {}
+2 -1
View File
@@ -56,7 +56,8 @@ def main():
epilog=HELP_EPILOG, epilog=HELP_EPILOG,
) )
parser.add_argument( parser.add_argument(
"-l", "--listen", "-l",
"--listen",
metavar="host:port", metavar="host:port",
help=f"Vite (default: localhost:{DEFAULT_VITE_PORT})", help=f"Vite (default: localhost:{DEFAULT_VITE_PORT})",
) )