Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee4e682cfc | ||
|
|
7fc549e2f3 | ||
|
|
1c3dfd4545 | ||
|
|
9208718b3f | ||
|
|
cf1c536f5c | ||
|
|
ed2013c515 | ||
|
|
9d681853e6 | ||
|
|
0c05551d33 | ||
|
|
67b804eaf2 | ||
|
|
863eed8460 | ||
|
|
1690b051c4 | ||
|
|
c6378e6abd | ||
|
|
a28c4ec149 |
@@ -3,3 +3,4 @@
|
|||||||
*.lock
|
*.lock
|
||||||
__pycache__/
|
__pycache__/
|
||||||
dist/
|
dist/
|
||||||
|
/_version.py
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+101
-93
@@ -17,9 +17,15 @@ 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
|
||||||
|
|
||||||
|
try:
|
||||||
|
from _version import __version__
|
||||||
|
except ImportError:
|
||||||
|
__version__ = "0.0.0+unknown"
|
||||||
|
|
||||||
# Template directory
|
# Template directory
|
||||||
TEMPLATE_DIR = Path(__file__).parent / "template"
|
TEMPLATE_DIR = Path(__file__).parent / "template"
|
||||||
|
|
||||||
@@ -44,6 +50,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 +220,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 +240,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 +404,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 +419,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 +653,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 +847,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,35 +895,23 @@ 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(
|
||||||
"--dry-run", action="store_true", help="Show what would be done"
|
"--dry-run", action="store_true", help="Show what would be done"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--version",
|
||||||
|
action="version",
|
||||||
|
version=f"%(prog)s {__version__}",
|
||||||
|
)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ fastapi-vue-setup = "fastapi_vue_setup:main"
|
|||||||
[tool.hatch.version]
|
[tool.hatch.version]
|
||||||
source = "vcs"
|
source = "vcs"
|
||||||
|
|
||||||
|
[tool.hatch.build.hooks.vcs]
|
||||||
|
version-file = "_version.py"
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
include = ["fastapi_vue_setup.py", "template/**/*", "_version.py"]
|
include = ["fastapi_vue_setup.py", "template/**/*", "_version.py"]
|
||||||
|
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user