Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
458d798022 | ||
|
|
f5fcd626e8 | ||
|
|
1d9ddf7dc5 | ||
|
|
b189ae66e7 | ||
|
|
4156eb2db3 | ||
|
|
3f6559b2db | ||
|
|
71d4e053a7 | ||
|
|
2dec331afb | ||
|
|
64f43df3e3 | ||
|
|
6ccba2be9f | ||
|
|
2b735528ab |
@@ -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
|
||||||
|
|
||||||
|
|||||||
+564
-232
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -9,7 +9,7 @@ description = "Tool to create or patch FastAPI+Vue projects with integrated buil
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tomli-w>=1.0.0",
|
"tomlkit>=0.12.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
@@ -1,30 +1,44 @@
|
|||||||
import sys
|
# auto-upgrade@fastapi-vue-setup - remove this if you modify this file
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
import uvicorn
|
from fastapi_vue.hostutil import parse_endpoint
|
||||||
|
from uvicorn import Config, Server
|
||||||
|
|
||||||
|
DEFAULT_PORT = 5080
|
||||||
|
|
||||||
|
|
||||||
|
def run_server(endpoints: list[dict], *, proxy=""):
|
||||||
|
conf: dict[str, object] = {"app": "MODULE_NAME.APP_MODULE:APP_VAR"}
|
||||||
|
if proxy:
|
||||||
|
conf["proxy_headers"] = True
|
||||||
|
conf["forwarded_allow_ips"] = proxy
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
|
try:
|
||||||
|
run_server(parse_endpoint(args.endpoint, DEFAULT_PORT), proxy=proxy)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ from fastapi import FastAPI
|
|||||||
from fastapi_vue import Frontend
|
from fastapi_vue import Frontend
|
||||||
|
|
||||||
# Vue Frontend static files
|
# Vue Frontend static files
|
||||||
frontend = Frontend(
|
frontend = Frontend(Path(__file__).with_name("frontend-build"), cached=["/assets/"])
|
||||||
Path(__file__).with_name("frontend-build"), spa=True, cached=["/assets/"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -29,5 +27,5 @@ async def health_check():
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
# Final catch-all route for frontend files (keep at end of file)
|
# Serve the Vue frontend (needs to be last if SPA catch-all is used)
|
||||||
frontend.route(app, "/")
|
frontend.route(app, "/")
|
||||||
|
|||||||
+47
-260
@@ -1,278 +1,64 @@
|
|||||||
#!/usr/bin/env -S uv run
|
#!/usr/bin/env -S uv run
|
||||||
"""Run Vite development server for frontend and FastAPI backend with auto-reload.
|
# auto-upgrade@fastapi-vue-setup - remove this if you modify this file
|
||||||
|
"""Run Vite development server for frontend and FastAPI backend with auto-reload."""
|
||||||
Usage:
|
|
||||||
uv run scripts/devserver.py [host:port] [--backend host:port]
|
|
||||||
|
|
||||||
The optional host:port argument sets where the Vite frontend listens.
|
|
||||||
Supported forms: host[:port], :port (all interfaces), or just port.
|
|
||||||
The --backend option sets where the FastAPI backend listens (default: localhost:5180).
|
|
||||||
|
|
||||||
Environment:
|
|
||||||
JS_RUNTIME Path or name of JS runtime to use (deno, npm/node or bun).
|
|
||||||
FASTAPI_VUE_FRONTEND_URL Set by this script for the backend to know where Vite is.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
import ipaddress
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sys import stderr
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import httpx
|
# Import util.py from scripts/fastapi-vue (not a package, so we adjust sys.path)
|
||||||
|
sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue")))
|
||||||
exec((Path(__file__).parent / "fastapi-vue/util.py").read_text("UTF-8")) # noqa: S102
|
from devutil import ( # type: ignore
|
||||||
|
ProcessGroup,
|
||||||
DEFAULT_HOST = "localhost"
|
logger,
|
||||||
DEFAULT_VITE_PORT = 5173
|
ready,
|
||||||
DEFAULT_BACKEND_PORT = 5180
|
setup_fastapi,
|
||||||
FRONTEND_PATH = Path(__file__).parent.parent / "frontend"
|
setup_vite,
|
||||||
|
)
|
||||||
EPILOG = """
|
|
||||||
scripts/devserver.py # Default ports on localhost
|
|
||||||
scripts/devserver.py 3000 # Vite on localhost:3000
|
|
||||||
scripts/devserver.py :3000 --backend 8000 # *:3000, localhost:8000
|
|
||||||
"""
|
|
||||||
|
|
||||||
BUN_BUG = """\
|
|
||||||
┃ ⚠️ Bun cannot correctly proxy API requests to the backend.
|
|
||||||
┃ Bug report: https://github.com/oven-sh/bun/issues/9882
|
|
||||||
┃
|
|
||||||
┃ Consider using deno or npm instead for development.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def parse_endpoint(
|
async def run_devserver(frontend: str, backend: str) -> None:
|
||||||
value: str | None, default_port: int = DEFAULT_VITE_PORT
|
reporoot = Path(__file__).parent.parent
|
||||||
) -> tuple[str | None, int, bool]:
|
front = reporoot / "frontend"
|
||||||
"""Parse an endpoint for Vite or backend.
|
if not (front / "package.json").exists():
|
||||||
|
logger.warning("Frontend source not found at %s", front)
|
||||||
Returns (host, port, all_ifaces).
|
|
||||||
"""
|
|
||||||
if not value:
|
|
||||||
return DEFAULT_HOST, default_port, False
|
|
||||||
|
|
||||||
# Port only (numeric) -> localhost:port
|
|
||||||
if value.isdigit():
|
|
||||||
return DEFAULT_HOST, int(value), False
|
|
||||||
|
|
||||||
# Leading colon :port -> bind all interfaces
|
|
||||||
if value.startswith(":") and value != ":":
|
|
||||||
port_part = value[1:]
|
|
||||||
if not port_part.isdigit():
|
|
||||||
raise SystemExit(f"Invalid port in '{value}'")
|
|
||||||
return None, int(port_part), True
|
|
||||||
|
|
||||||
# Unbracketed IPv6 (cannot safely contain a port)
|
|
||||||
if value.count(":") > 1 and not value.startswith("["):
|
|
||||||
try:
|
|
||||||
ipaddress.IPv6Address(value)
|
|
||||||
except ValueError as e:
|
|
||||||
raise SystemExit(f"Invalid IPv6 address '{value}': {e}") from e
|
|
||||||
return value, default_port, False
|
|
||||||
|
|
||||||
# Use urllib.parse for everything else
|
|
||||||
parsed = urlparse(f"//{value}")
|
|
||||||
host = parsed.hostname or DEFAULT_HOST
|
|
||||||
port = parsed.port or default_port
|
|
||||||
|
|
||||||
return host, port, False
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_frontend_tools(
|
|
||||||
vite_host: str | None, vite_port: int, all_ifaces: bool
|
|
||||||
) -> tuple[list[str], list[str], str]:
|
|
||||||
"""Resolve frontend install and dev commands.
|
|
||||||
|
|
||||||
Returns (install_cmd, dev_cmd, tool_name).
|
|
||||||
Raises SystemExit if tools are not available.
|
|
||||||
"""
|
|
||||||
if not (FRONTEND_PATH / "package.json").exists():
|
|
||||||
stderr.write(f"┃ ⚠️ Frontend source not found at {FRONTEND_PATH}\n")
|
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
result = find_js_runtime() # noqa # type: ignore
|
frontend_url, npm_install, vite = setup_vite(frontend)
|
||||||
if result is None:
|
backend_url, fastapi = setup_fastapi(backend, "MODULE_NAME.APP_MODULE:APP_VAR")
|
||||||
if not os.environ.get("JS_RUNTIME"):
|
|
||||||
stderr.write("┃ ⚠️ deno, npm or bun needed to run the frontend server.\n")
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
tool, name = result
|
# Tell the everyone where the frontend and backend are (vite proxy, etc)
|
||||||
|
os.environ["FASTAPI_VUE_FRONTEND_URL"] = frontend_url
|
||||||
|
os.environ["FASTAPI_VUE_BACKEND_URL"] = backend_url
|
||||||
|
|
||||||
install_args = {
|
async with ProcessGroup() as pg:
|
||||||
"deno": ("install", "--quiet", "--allow-scripts=npm:vue-demi"),
|
install_proc = await pg.spawn(*npm_install, cwd=front)
|
||||||
"npm": ("install", "--silent"),
|
await asyncio.sleep(0.2) # reduce message overlap
|
||||||
"bun": ("install", "--silent"),
|
await pg.spawn(
|
||||||
}
|
*fastapi,
|
||||||
dev_args = {
|
|
||||||
"deno": ("run", "dev", "--"),
|
|
||||||
"npm": ("--silent", "run", "dev", "--"),
|
|
||||||
"bun": ("run", "dev", "--"),
|
|
||||||
}
|
|
||||||
|
|
||||||
install_cmd = [tool, *install_args[name]]
|
|
||||||
dev_cmd = [tool, *dev_args[name], "--port", str(vite_port)]
|
|
||||||
|
|
||||||
if all_ifaces:
|
|
||||||
dev_cmd.append("--host")
|
|
||||||
elif vite_host:
|
|
||||||
dev_cmd.extend(["--host", vite_host])
|
|
||||||
|
|
||||||
if name == "bun":
|
|
||||||
stderr.write(BUN_BUG)
|
|
||||||
|
|
||||||
return install_cmd, dev_cmd, name
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_backend(backend_host: str, backend_port: int):
|
|
||||||
"""Wait for the backend to be ready by polling the health endpoint."""
|
|
||||||
max_attempts = 50
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
for attempt in range(max_attempts):
|
|
||||||
try:
|
|
||||||
await client.get(f"http://{backend_host}:{backend_port}", timeout=1.0)
|
|
||||||
stderr.write("✓ Backend ready!\n")
|
|
||||||
return True
|
|
||||||
except httpx.RequestError:
|
|
||||||
if attempt == max_attempts - 1:
|
|
||||||
stderr.write("┃ ⚠️ Backend didn't start in time\n")
|
|
||||||
return False
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def _terminate_process(proc: asyncio.subprocess.Process, name: str) -> None:
|
|
||||||
"""Gracefully terminate a subprocess."""
|
|
||||||
if proc.returncode is not None:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
proc.terminate()
|
|
||||||
except ProcessLookupError:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(proc.wait(), timeout=2)
|
|
||||||
except TimeoutError:
|
|
||||||
try:
|
|
||||||
proc.kill()
|
|
||||||
except ProcessLookupError:
|
|
||||||
return
|
|
||||||
await proc.wait()
|
|
||||||
|
|
||||||
|
|
||||||
async def run_devserver(
|
|
||||||
vite_host: str | None,
|
|
||||||
vite_port: int,
|
|
||||||
all_ifaces: bool,
|
|
||||||
backend_host: str,
|
|
||||||
backend_port: int,
|
|
||||||
) -> None:
|
|
||||||
"""Run the development server with install, backend, and frontend."""
|
|
||||||
install_cmd, dev_cmd, tool_name = resolve_frontend_tools(
|
|
||||||
vite_host, vite_port, all_ifaces
|
|
||||||
)
|
|
||||||
|
|
||||||
# Tell the backend where the Vite dev server is
|
|
||||||
os.environ["FASTAPI_VUE_FRONTEND_URL"] = (
|
|
||||||
f"http://{vite_host or 'localhost'}:{vite_port}"
|
|
||||||
)
|
|
||||||
# Tell Vite where the backend is (for proxying /api requests)
|
|
||||||
os.environ["FASTAPI_VUE_BACKEND_URL"] = f"http://{backend_host}:{backend_port}"
|
|
||||||
cwd = str(Path(__file__).parent.parent)
|
|
||||||
frontend_cwd = str(FRONTEND_PATH)
|
|
||||||
|
|
||||||
backend_proc: asyncio.subprocess.Process | None = None
|
|
||||||
install_proc: asyncio.subprocess.Process | None = None
|
|
||||||
frontend_proc: asyncio.subprocess.Process | None = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Start install (concurrent with backend)
|
|
||||||
stderr.write(f">>> {tool_name} {' '.join(install_cmd[1:])}\n")
|
|
||||||
install_proc = await asyncio.create_subprocess_exec(
|
|
||||||
*install_cmd, cwd=frontend_cwd
|
|
||||||
)
|
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
# Start backend (concurrent with install)
|
|
||||||
backend_cmd = [
|
|
||||||
"uvicorn",
|
|
||||||
"MODULE_NAME.app:app",
|
|
||||||
"--host",
|
|
||||||
backend_host,
|
|
||||||
"--port",
|
|
||||||
str(backend_port),
|
|
||||||
"--reload",
|
"--reload",
|
||||||
]
|
"--reload-dir=MODULE_NAME", # Don't reload on frontend changes
|
||||||
stderr.write(f">>> {' '.join(backend_cmd)}\n")
|
"--forwarded-allow-ips=*",
|
||||||
backend_proc = await asyncio.create_subprocess_exec(*backend_cmd, cwd=cwd)
|
cwd=reporoot,
|
||||||
|
|
||||||
# Wait for install to complete and backend to be ready
|
|
||||||
install_task = asyncio.create_task(install_proc.wait(), name="install")
|
|
||||||
backend_ready_task = asyncio.create_task(
|
|
||||||
wait_for_backend(backend_host, backend_port), name="backend_ready"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
done, pending = await asyncio.wait(
|
# Wait for both install and backend to be ready
|
||||||
{install_task, backend_ready_task},
|
async with asyncio.TaskGroup() as tg:
|
||||||
return_when=asyncio.FIRST_COMPLETED,
|
tg.create_task(pg.wait(install_proc))
|
||||||
)
|
tg.create_task(ready(backend_url, path="/api/health?from=devserver.py"))
|
||||||
|
|
||||||
for task in done:
|
# Start Vite dev server (ProcessGroup waits for any exit, then terminates others)
|
||||||
if task.get_name() == "install":
|
await pg.spawn(*vite, cwd=front)
|
||||||
if task.result() != 0:
|
|
||||||
stderr.write("┃ ⚠️ Install failed\n")
|
|
||||||
raise SystemExit(1)
|
|
||||||
elif task.get_name() == "backend_ready" and not task.result():
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
if pending:
|
|
||||||
done2, _ = await asyncio.wait(pending)
|
|
||||||
for task in done2:
|
|
||||||
if task.get_name() == "install":
|
|
||||||
if task.result() != 0:
|
|
||||||
stderr.write("┃ ⚠️ Install failed\n")
|
|
||||||
raise SystemExit(1)
|
|
||||||
elif task.get_name() == "backend_ready" and not task.result():
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
install_proc = None
|
|
||||||
|
|
||||||
# Start Vite dev server
|
|
||||||
stderr.write(f">>> {tool_name} {' '.join(dev_cmd[1:])}\n")
|
|
||||||
frontend_proc = await asyncio.create_subprocess_exec(*dev_cmd, cwd=frontend_cwd)
|
|
||||||
|
|
||||||
# Wait for either process to exit
|
|
||||||
done, pending = await asyncio.wait(
|
|
||||||
{
|
|
||||||
asyncio.create_task(backend_proc.wait(), name="backend"),
|
|
||||||
asyncio.create_task(frontend_proc.wait(), name="frontend"),
|
|
||||||
},
|
|
||||||
return_when=asyncio.FIRST_COMPLETED,
|
|
||||||
)
|
|
||||||
for t in done:
|
|
||||||
t.result()
|
|
||||||
for t in pending:
|
|
||||||
t.cancel()
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
stderr.write("\n✓ Shutting down...\n")
|
|
||||||
finally:
|
|
||||||
if frontend_proc is not None:
|
|
||||||
await _terminate_process(frontend_proc, "frontend")
|
|
||||||
if install_proc is not None:
|
|
||||||
await _terminate_process(install_proc, "install")
|
|
||||||
if backend_proc is not None:
|
|
||||||
await _terminate_process(backend_proc, "backend")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Run Vite and FastAPI development servers",
|
description="Run Vite and FastAPI development servers",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog=EPILOG,
|
epilog=HELP_EPILOG,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"frontend",
|
"frontend",
|
||||||
@@ -286,16 +72,17 @@ def main():
|
|||||||
help="FastAPI backend endpoint (default: localhost:5180)",
|
help="FastAPI backend endpoint (default: localhost:5180)",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
vite_host, vite_port, all_ifaces = parse_endpoint(args.frontend, DEFAULT_VITE_PORT)
|
|
||||||
backend_host, backend_port, _ = parse_endpoint(args.backend, DEFAULT_BACKEND_PORT)
|
|
||||||
# Backend host defaults to localhost (never None)
|
|
||||||
backend_host = backend_host or DEFAULT_HOST
|
|
||||||
|
|
||||||
with contextlib.suppress(KeyboardInterrupt):
|
with contextlib.suppress(KeyboardInterrupt):
|
||||||
asyncio.run(
|
asyncio.run(run_devserver(args.frontend, args.backend))
|
||||||
run_devserver(vite_host, vite_port, all_ifaces, backend_host, backend_port)
|
|
||||||
)
|
|
||||||
|
HELP_EPILOG = """
|
||||||
|
scripts/devserver.py # Default ports on localhost
|
||||||
|
scripts/devserver.py 3000 # Vite on localhost:3000
|
||||||
|
scripts/devserver.py :3000 --backend 8000 # *:3000, localhost:8000
|
||||||
|
|
||||||
|
JS_RUNTIME environment variable can be used to select the JS runtime
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,34 +1,15 @@
|
|||||||
"""Hatch build hook for building Vue frontend during package build."""
|
"""Hatch build hook for building Vue frontend during package build."""
|
||||||
|
|
||||||
import subprocess
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sys import stderr
|
|
||||||
|
|
||||||
from hatchling.builders.hooks.plugin.interface import BuildHookInterface # type: ignore
|
from hatchling.builders.hooks.plugin.interface import BuildHookInterface # type: ignore
|
||||||
|
|
||||||
exec(Path(__file__).with_name("util.py").read_text("UTF-8")) # noqa: S102
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from buildutil import build
|
||||||
|
|
||||||
def run(cmd, **kwargs):
|
|
||||||
"""Run a command and display it."""
|
|
||||||
display_cmd = [Path(cmd[0]).name, *cmd[1:]]
|
|
||||||
stderr.write(f"### {' '.join(display_cmd)}\n")
|
|
||||||
subprocess.run(cmd, check=True, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomBuildHook(BuildHookInterface):
|
class CustomBuildHook(BuildHookInterface):
|
||||||
"""Build hook that compiles Vue frontend before packaging."""
|
|
||||||
|
|
||||||
def initialize(self, version, build_data):
|
def initialize(self, version, build_data):
|
||||||
super().initialize(version, build_data)
|
super().initialize(version, build_data)
|
||||||
stderr.write(">>> Building the frontend\n")
|
build("frontend")
|
||||||
|
|
||||||
install_cmd, build_cmd = find_build_tool() # noqa # type: ignore
|
|
||||||
|
|
||||||
try:
|
|
||||||
run(install_cmd, cwd="frontend")
|
|
||||||
stderr.write("\n")
|
|
||||||
run(build_cmd, cwd="frontend")
|
|
||||||
except Exception as e:
|
|
||||||
stderr.write(f"Error occurred while building frontend: {e}\n")
|
|
||||||
raise
|
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
"""Shared utilities for build and dev scripts."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
|
||||||
from sys import stderr
|
|
||||||
|
|
||||||
|
|
||||||
def find_js_runtime() -> tuple[str, str] | None:
|
|
||||||
"""Find a JavaScript runtime from JS_RUNTIME env or auto-detect.
|
|
||||||
|
|
||||||
Returns (tool_path, tool_name) where tool_name is "deno", "npm", or "bun".
|
|
||||||
Returns None if no runtime is found.
|
|
||||||
"""
|
|
||||||
options = ["deno", "npm", "bun"]
|
|
||||||
|
|
||||||
# Check for JS_RUNTIME environment variable
|
|
||||||
if js_runtime_env := os.environ.get("JS_RUNTIME"):
|
|
||||||
js_runtime = js_runtime_env
|
|
||||||
js_path = Path(js_runtime)
|
|
||||||
runtime_name = js_path.name
|
|
||||||
# Map node to npm
|
|
||||||
if runtime_name == "node":
|
|
||||||
runtime_name = "npm"
|
|
||||||
js_runtime = str(js_path.parent / "npm") if js_path.parent.name else "npm"
|
|
||||||
for option in options:
|
|
||||||
if option == runtime_name or runtime_name.startswith(option):
|
|
||||||
tool = shutil.which(js_runtime)
|
|
||||||
if tool is None:
|
|
||||||
stderr.write(f"┃ ⚠️ JS_RUNTIME={js_runtime_env} not found\n")
|
|
||||||
return None
|
|
||||||
return tool, option
|
|
||||||
stderr.write(f"┃ ⚠️ JS_RUNTIME={js_runtime_env} not recognized\n")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Auto-detect
|
|
||||||
for option in options:
|
|
||||||
if tool := shutil.which(option):
|
|
||||||
return tool, option
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def find_build_tool():
|
|
||||||
"""Find JavaScript runtime and construct install/build commands.
|
|
||||||
|
|
||||||
Returns (install_cmd, build_cmd) tuples of command lists.
|
|
||||||
Raises RuntimeError if no runtime is found.
|
|
||||||
"""
|
|
||||||
install = {
|
|
||||||
"deno": ("install", "--allow-scripts=npm:vue-demi"),
|
|
||||||
"npm": ("install",),
|
|
||||||
"bun": ("--bun", "install"),
|
|
||||||
}
|
|
||||||
# Use build-only for deno to avoid npm-run-all2 issues with run-p
|
|
||||||
# (run-p tries to spawn npm which doesn't exist in deno)
|
|
||||||
build = {
|
|
||||||
"deno": ("task", "build-only"),
|
|
||||||
"npm": ("run", "build"),
|
|
||||||
"bun": ("--bun", "run", "build"),
|
|
||||||
}
|
|
||||||
|
|
||||||
result = find_js_runtime()
|
|
||||||
if result is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Deno, npm or Bun is required for building but none was found"
|
|
||||||
)
|
|
||||||
|
|
||||||
tool, name = result
|
|
||||||
return [tool, *install[name]], [tool, *build[name]]
|
|
||||||
|
|
||||||
|
|
||||||
def find_dev_tool():
|
|
||||||
"""Find JavaScript runtime and construct dev command.
|
|
||||||
|
|
||||||
Returns (dev_cmd, tool_name) or (None, None) if not found.
|
|
||||||
"""
|
|
||||||
dev_args = {
|
|
||||||
"deno": ("run", "dev", "--"),
|
|
||||||
"npm": ("--silent", "run", "dev", "--"),
|
|
||||||
"bun": ("run", "dev", "--"),
|
|
||||||
}
|
|
||||||
|
|
||||||
result = find_js_runtime()
|
|
||||||
if result is None:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
tool, name = result
|
|
||||||
return [tool, *dev_args[name]], name
|
|
||||||
Reference in New Issue
Block a user