Compare commits

...
5 Commits
5 changed files with 156 additions and 122 deletions
+11 -2
View File
@@ -63,7 +63,16 @@ Options:
In development, you access the Vite dev server at `http://localhost:5173`. Vite proxies `/api/*` requests to FastAPI at port 5180. Ports and hosts of Vite and FastAPI are configurable by `devserver.py` arguments. In development, you access the Vite dev server at `http://localhost:5173`. Vite proxies `/api/*` requests to FastAPI at port 5180. Ports and hosts of Vite and FastAPI are configurable by `devserver.py` arguments.
In production, FastAPI serves both the API and static files at `http://localhost:5080`. Configurable by `host:port` argument. In production, FastAPI serves both the API and static files at `http://localhost:5080`. Configurable by `host:port` argument with defaults set in `__main__.py`
## Main CLI
If your project didn't already have `__main__.py`, we create one that runs the FastAPI app with richer configuration than what the FastAPI CLI offers. Running your module starts it in production mode, and optionally host:port may be given as argument to specify where it listens.
If you are running behind a reverse proxy like [Caddy](https://caddyserver.com/) on localhost, your app will trust the proxy headers it sends. However, if you need to configure another proxy host or IP, set `FORWARDED_ALLOW_IPS` env variable before running the server.
The devserver script depends on this CLI entry for running the backend. You will have to modify the `devserver.py` script if your app has its own incompatible main module. Note that we set FastAPI debug mode and Uvicorn reload when configured via `FASTAPI_VUE_BACKEND_URL` env variable (set by `devserver.py`), while for normal production use these stay disabled. The same variable also controls static files serving (disabled in dev mode).
## Vite Plugin Configuration ## Vite Plugin Configuration
@@ -108,7 +117,7 @@ my-app/
└── pyproject.toml └── pyproject.toml
``` ```
The project directory tree looks roughly like this after project creation or patching. The script finds your existing fastpi app module and other files and patches them with minimal changes to enable the Vue-FastAPI interconnection. New Python and Vue projects are created automatically if none exist. The project directory tree looks roughly like this after project creation or patching. The script finds your existing app module and other files and patches them with minimal changes to enable the Vue-FastAPI interconnection. New Python and Vue projects are created automatically if none exist.
## Development Workflow ## Development Workflow
+91 -93
View File
@@ -17,6 +17,7 @@ import subprocess
import sys import sys
import tomllib import tomllib
from pathlib import Path from pathlib import Path
from textwrap import indent
import tomli_w import tomli_w
@@ -44,6 +45,71 @@ PYPROJECT_ADDITIONS = {
} }
# Frontend instantiation block for patching existing apps
FRONTEND_BLOCK = """
# Vue Frontend static files
frontend = Frontend(
Path(__file__).with_name("frontend-build"), spa=True, cached=["/assets/"]
)
"""
# TypeScript health check script for Vue components
TS_HEALTH_CHECK_SCRIPT = """\
import { ref, onMounted } from 'vue'
const backendStatus = ref<'checking' | 'connected' | 'error'>('checking')
onMounted(async () => {
try {
const res = await fetch('/api/health')
backendStatus.value = res.ok ? 'connected' : 'error'
} catch {
backendStatus.value = 'error'
}
})
"""
# JavaScript health check script for Vue components
JS_HEALTH_CHECK_SCRIPT = """\
import { ref, onMounted } from 'vue'
const backendStatus = ref('checking')
onMounted(async () => {
try {
const res = await fetch('/api/health')
backendStatus.value = res.ok ? 'connected' : 'error'
} catch {
backendStatus.value = 'error'
}
})
"""
# Status indicator template for Vue components
STATUS_SPAN_TEMPLATE = """\
<span style="white-space: nowrap">
— FastAPI:
<span v-if="backendStatus === 'checking'">⏳</span>
<span v-else-if="backendStatus === 'connected'">✅</span>
<span v-else>❌ not reachable</span>
</span>
"""
# Setup complete message template
SETUP_COMPLETE_MESSAGE = """
Next steps:
1. Build for production:
CD_CMDuv build
2. Start development server:
CD_CMDuv run scripts/devserver.py
3. Run production server:
CD_CMDuv run SCRIPT_NAME
"""
# ============================================================================= # =============================================================================
# Utility functions # Utility functions
# ============================================================================= # =============================================================================
@@ -149,16 +215,6 @@ def patch_app_file(
lines = content.split("\n") lines = content.split("\n")
import_line = "from fastapi_vue import Frontend" import_line = "from fastapi_vue import Frontend"
# Frontend instantiation block - use module_name for the frontend-build path
frontend_block = """
# Frontend static file server - configure options here
frontend = Frontend(
Path(__file__).parent / "frontend-build",
spa=True,
favicon="/assets/favicon",
cached=["/assets/"],
)
"""
route_line = f'frontend.route({app_var}, "/")' route_line = f'frontend.route({app_var}, "/")'
# Find last import line and check if pathlib is imported # Find last import line and check if pathlib is imported
@@ -179,7 +235,7 @@ frontend = Frontend(
lines.insert(last_import_idx + 1, "from pathlib import Path") lines.insert(last_import_idx + 1, "from pathlib import Path")
last_import_idx += 1 last_import_idx += 1
lines.insert(last_import_idx + 1, import_line) lines.insert(last_import_idx + 1, import_line)
lines.insert(last_import_idx + 2, frontend_block) lines.insert(last_import_idx + 2, FRONTEND_BLOCK)
# Append route at end # Append route at end
lines.append("") lines.append("")
@@ -343,44 +399,9 @@ def patch_frontend_health_check(frontend_dir: Path, dry_run: bool = False) -> bo
is_typescript = 'lang="ts"' in content is_typescript = 'lang="ts"' in content
# Build the script content based on JS/TS # Build the script content based on JS/TS
if is_typescript: script_addition = (
script_addition = """ TS_HEALTH_CHECK_SCRIPT if is_typescript else JS_HEALTH_CHECK_SCRIPT
import { ref, onMounted } from 'vue' )
const backendStatus = ref<'checking' | 'connected' | 'error'>('checking')
onMounted(async () => {
try {
const res = await fetch('/api/health')
backendStatus.value = res.ok ? 'connected' : 'error'
} catch {
backendStatus.value = 'error'
}
})
"""
else:
script_addition = """
import { ref, onMounted } from 'vue'
const backendStatus = ref('checking')
onMounted(async () => {
try {
const res = await fetch('/api/health')
backendStatus.value = res.ok ? 'connected' : 'error'
} catch {
backendStatus.value = 'error'
}
})
"""
# Template addition - inline status indicator using emoji for colors
status_span = """<span style="white-space: nowrap">
— FastAPI:
<span v-if="backendStatus === 'checking'">⏳</span>
<span v-else-if="backendStatus === 'connected'">✅</span>
<span v-else>❌ not reachable</span>
</span>"""
# Insert script addition before </script> # Insert script addition before </script>
script_end_match = re.search(r"</script>", content) script_end_match = re.search(r"</script>", content)
@@ -393,19 +414,21 @@ onMounted(async () => {
# For App.vue (minimal): insert before the last </p> in template # For App.vue (minimal): insert before the last </p> in template
if "HelloWorld" in str(target_file): if "HelloWorld" in str(target_file):
# Insert before closing </h3> # Insert before closing </h3>
h3_close = content.find("</h3>") h3_close = content.find(" </h3>")
if h3_close != -1: if h3_close == -1:
content = ( return False
f"{content[:h3_close]}\n {status_span}\n {content[h3_close:]}" before, after = content[:h3_close], content[h3_close:]
) content = f"{before}{indent(STATUS_SPAN_TEMPLATE, ' ')}{after}"
else: else:
# Minimal App.vue - insert before the last </p> before </template> # Minimal App.vue - insert before the last </p> before </template>
template_end = content.find("</template>") template_end = content.find("</template>")
if template_end != -1: if template_end != -1:
# Find last </p> before </template> # Find last </p> before </template>
last_p = content.rfind("</p>", 0, template_end) last_p = content.rfind(" </p>", 0, template_end)
if last_p != -1: if last_p == -1:
content = f"{content[:last_p]}\n {status_span}\n {content[last_p:]}" return False
before, after = content[:last_p], content[last_p:]
content = f"{before}{STATUS_SPAN_TEMPLATE}{after}"
target_file.write_text(content) target_file.write_text(content)
print(f"✅ Patched {target_file}") print(f"✅ Patched {target_file}")
@@ -625,12 +648,12 @@ def cmd_setup(args: argparse.Namespace) -> int:
if dry_run: if dry_run:
print("\n🏃 DRY RUN MODE - no changes will be made\n") print("\n🏃 DRY RUN MODE - no changes will be made\n")
# Step 1: Ensure Python project exists # Step 1: Ensure frontend exists (do this first so cancellation doesn't leave partial setup)
if not ensure_python_project(project_dir, dry_run): if not ensure_frontend(project_dir, dry_run):
return 1 return 1
# Step 2: Ensure frontend exists # Step 2: Ensure Python project exists
if not ensure_frontend(project_dir, dry_run): if not ensure_python_project(project_dir, dry_run):
return 1 return 1
# Detect module name # Detect module name
@@ -819,18 +842,10 @@ def cmd_setup(args: argparse.Namespace) -> int:
cd_cmd = "" if project_dir == Path.cwd() else f"cd {project_dir}\n " cd_cmd = "" if project_dir == Path.cwd() else f"cd {project_dir}\n "
script_name = module_name.replace("_", "-") script_name = module_name.replace("_", "-")
print(f""" message = SETUP_COMPLETE_MESSAGE.replace("CD_CMD", cd_cmd).replace(
Next steps: "SCRIPT_NAME", script_name
)
1. Build for production: print(message)
{cd_cmd}uv build
2. Start development server:
{cd_cmd}uv run scripts/devserver.py
3. Run production server:
{cd_cmd}uv run {script_name}
""")
return 0 return 0
@@ -875,7 +890,7 @@ Examples:
"project_dir", "project_dir",
nargs="?", nargs="?",
default=None, default=None,
help="Project directory (default: current directory)", help="Project directory (use . for current directory)",
) )
parser.add_argument("--module-name", help="Python module name (auto-detected)") parser.add_argument("--module-name", help="Python module name (auto-detected)")
parser.add_argument( parser.add_argument(
@@ -884,26 +899,9 @@ Examples:
args = parser.parse_args() args = parser.parse_args()
# Handle default project directory with safety check
if args.project_dir is None: if args.project_dir is None:
cwd = Path.cwd() parser.print_help()
if not is_uninitialized_folder(cwd) and not is_already_patched(cwd): return 0
print(
"⚠️ Current directory contains an existing project that hasn't been patched yet."
)
print()
print(
" If you want to set up FastAPI+Vue integration in the current directory, run:"
)
print(" fastapi-vue-setup .")
print()
print(" Or specify a new project directory:")
print(" fastapi-vue-setup my-new-project")
print()
print(" Python project will be at root, while Vue lives in frontend/.")
print()
return 1
args.project_dir = "."
return cmd_setup(args) return cmd_setup(args)
+44 -21
View File
@@ -1,30 +1,53 @@
import sys import argparse
import asyncio
import os
import uvicorn import uvicorn
from fastapi_vue.hostutil import parse_endpoint
from uvicorn import Config, Server
from .APP_MODULE import APP_VAR
DEFAULT_PORT = 5080
def run_server(endpoints: list[dict], *, proxy="", devmode=False):
conf: dict[str, object] = {"app": "MODULE_NAME.APP_MODULE:APP_VAR"}
if proxy:
conf["proxy_headers"] = True
conf["forwarded_allow_ips"] = proxy
if devmode:
conf["reload"] = True
conf["reload_dirs"] = ["MODULE_NAME"]
APP_VAR.debug = True
if len(endpoints) > 1:
# Run separate servers for multiple endpoints
async def serve_all():
async with asyncio.TaskGroup() as tg:
for ep in endpoints:
tg.create_task(Server(Config(**conf, **ep)).serve())
asyncio.run(serve_all())
else:
uvicorn.run(**conf, **endpoints[0])
def main(): def main():
"""Run the FastAPI application using uvicorn.""" parser = argparse.ArgumentParser(description="Run the MODULE_NAME server.")
parser.add_argument(
if len(sys.argv) > 1: "endpoint",
endpoint = sys.argv[1] nargs="?",
if ":" in endpoint: help=(
host, port = endpoint.rsplit(":", 1) f"Endpoint (default: localhost:{DEFAULT_PORT}). "
host = host or "localhost" "Forms: host:port | :port | [ipv6]:port | ip | host | unix:/path.sock"
port = int(port) ),
else:
host = "localhost"
port = int(endpoint)
else:
host = "localhost"
port = 5080
uvicorn.run(
"MODULE_NAME.app:app",
host=host,
port=port,
log_level="info",
) )
args = parser.parse_args()
proxy = os.getenv("FORWARDED_ALLOW_IPS", "127.0.0.1,::1")
devmode = bool(os.getenv("FASTAPI_VUE_FRONTEND_URL"))
endpoints = parse_endpoint(args.endpoint, DEFAULT_PORT)
run_server(endpoints, proxy=proxy, devmode=devmode)
if __name__ == "__main__": if __name__ == "__main__":
+8 -3
View File
@@ -114,12 +114,17 @@ def resolve_frontend_tools(
} }
install_cmd = [tool, *install_args[name]] install_cmd = [tool, *install_args[name]]
dev_cmd = [tool, *dev_args[name], "--port", str(vite_port)] dev_cmd = [
tool,
*dev_args[name],
"--clearScreen=false",
f"--port={vite_port}",
]
if all_ifaces: if all_ifaces:
dev_cmd.append("--host") dev_cmd.append("--host")
elif vite_host: elif vite_host and vite_host != "localhost":
dev_cmd.extend(["--host", vite_host]) dev_cmd.append(f"--host={vite_host}")
if name == "bun": if name == "bun":
stderr.write(BUN_BUG) stderr.write(BUN_BUG)
+2 -3
View File
@@ -51,10 +51,9 @@ def find_build_tool():
"npm": ("install",), "npm": ("install",),
"bun": ("--bun", "install"), "bun": ("--bun", "install"),
} }
# Use build-only for deno to avoid npm-run-all2 issues with run-p # Run vite directly for deno to avoid npm-run-all2/run-p issues
# (run-p tries to spawn npm which doesn't exist in deno)
build = { build = {
"deno": ("task", "build-only"), "deno": ("run", "-A", "npm:vite", "build"),
"npm": ("run", "build"), "npm": ("run", "build"),
"bun": ("--bun", "run", "build"), "bun": ("--bun", "run", "build"),
} }