Compare commits

...
12 Commits
10 changed files with 597 additions and 461 deletions
+63 -50
View File
@@ -1,15 +1,13 @@
# fastapi-vue-setup
Tool to create or patch FastAPI project with a Vue frontend, with integrated build and development systems. Only building the package needs JS runtime and Vue. Additionally, a devmode setup using Vite dev server with hot auto reloads is available via the scripts/devserver.py script (only intended to be used on the source repo, not included in installed package).
Tool to create or patch FastAPI project with a Vue frontend, with integrated build and development systems. The Python package will not need any JS runtime because it includes a prebuilt Vue frontend in it. For development (Vite and FastAPI auto reloads) and building the package one of npm, deno or bun is required (node is recommended due to bugs in deno and bun).
## Features
- **No JavaScript**: Your Python package can be installed and used without any JS runtime
- **Create new projects** with `uv init` + `create-vue` (interactive)
- **Patch existing projects** with build hooks and dev server scripts
- **Integrated build system**: Vue frontend builds into Python package during `uv build`
- **Development server**: Single command runs Vite + FastAPI with hot-reload
- **Optimized static serving**: zstd compression, ETag caching, SPA support
- **Optimized static serving**: Caching, zstd compression and SPA support
## Installation
@@ -28,77 +26,98 @@ uvx fastapi-vue-setup my-app
## Usage
The script may be used to create an all new project folder or to patch or update an existing project to use this framework. It autodetects the project folder given and performs the appropriate actions.
### Create a new project
```sh
fastapi-vue-setup new my-app
fastapi-vue-setup my-app
```
This will:
1. Run `uv init my-app`
2. Run `npm create vue@latest frontend` (interactive)
2. Run `create-vue frontend` (interactive - choose your Vue options)
3. Patch the project with FastAPI integration
4. Install dependencies via `uv add`
### Patch an existing project
```bash
fastapi-vue-setup patch /path/to/project
You should have your pyproject.toml at the current working directory, and Vue with its package.json under `frontend/`. If either one is missing, new applications will be initialised. Otherwise we only patch what can be patched without breaking your existing projects.
```sh
fastapi-vue-setup .
```
### CLI Options
```
fastapi-vue-setup [project-dir] [options]
Options:
- `--module-name NAME`: Python module name (auto-detected from pyproject.toml)
- `--vite-port PORT`: Vite dev server port (default: 5173)
- `--backend-port PORT`: Backend API port in dev mode (default: 5174)
- `--prod-port PORT`: Production server port (default: 8000)
- `--force`: Overwrite existing files
- `--dry-run`: Preview changes without modifying files
### Update an existing project
```bash
fastapi-vue-setup update /path/to/project
--module-name NAME Python module name (auto-detected from pyproject.toml)
--dry-run Preview changes without modifying files
```
Same as `patch --force` - overwrites template files with latest versions.
## Port Configuration
The tool uses three distinct ports:
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.
| Port | Purpose | Used by |
| ---- | --------------------- | ------------------------------ |
| 5173 | Vite dev server (HMR) | `npm run dev` via devserver.py |
| 5174 | FastAPI in dev mode | uvicorn via devserver.py |
| 8000 | Production server | `uv run my-app` |
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`
In development, you access the app at `http://localhost:5173`. Vite proxies `/api/*` requests to FastAPI at port 5174.
## Main CLI
In production, FastAPI serves both the API and static files from the same port (8000).
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
The `fastapiVue()` plugin in `vite.config.ts` accepts options to customize proxy behavior:
```js
import fastapiVue from "./vite-plugin-fastapi.js";
export default defineConfig({
plugins: [
vue(),
// Default: proxies only /api
fastapiVue(),
// Or specify custom paths to proxy to backend
fastapiVue({ paths: ["/api", "/auth", "/ws"] }),
],
});
```
The plugin reads environment `FASTAPI_VUE_BACKEND_URL` (default: `http://localhost:5180`) to determine where to proxy requests. This is set automatically by `devserver.py`.
## Project Structure
After patching, your project will have:
```
my-app/
├── frontend/ # Vue.js application
├── frontend/ # Vue application
│ ├── src/
│ ├── vite.config.ts # Builds to ../my_app/frontend-build
│ ├── vite-plugin-fastapi.js
│ ├── vite.config.js
│ └── package.json
├── my_app/ # Python package
├── my_app/ # Python module (files included in sdist)
│ ├── __init__.py
│ ├── __main__.py # CLI entry point
│ ├── app.py # FastAPI application
│ └── frontend-build/ # Built frontend (gitignored)
│ ├── __main__.py # CLI entrypoint
│ ├── app.py # FastAPI application
│ └── frontend-build/ # Built frontend (gitignored)
├── scripts/
│ ├── devserver.py # Development server
│ └── fastapi-vue/ # Build utilities
└── pyproject.toml # Project configuration
│ ├── devserver.py # CLI dev server (only in source tree)
│ └── fastapi-vue/
│ ├── build-frontend.py
│ └── util.py
└── pyproject.toml
```
The script finds your existing fastpi app module (even if not named app.py) and other files and patches them with minimal changes to enable the Vue-FastAPI interconnection.
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
@@ -113,12 +132,6 @@ uv build
uv run my-app
```
## Static File Serving
## Frontend serving
The `Frontend` class provides:
- **Automatic zstd compression** at level 18
- **Smart caching**: Content-hashed assets get `immutable` cache headers
- **ETag support**: 304 Not Modified responses for cached content
- **SPA routing**: Falls back to index.html for client-side routes
- **Favicon handling**: Automatic /favicon.ico from hashed assets
Your FastAPI app will use [fastapi-vue](https://git.zi.fi/LeoVasanko/fastapi-vue) to serve the frontend files. Refer to that package's documentation for further configuration.
+377 -273
View File
@@ -7,12 +7,7 @@ Usage:
Options:
--module-name NAME Python module name (auto-detected from pyproject.toml)
--vite-port PORT Vite dev server port (default: 5173)
--backend-port PORT Backend API port in dev mode (default: 5180)
--prod-port PORT Production server port (default: 5080)
--dry-run Show what would be done without making changes
Port options update existing files when specified, allowing reconfiguration.
"""
import argparse
@@ -20,26 +15,25 @@ import os
import re
import subprocess
import sys
import tomllib
from pathlib import Path
from textwrap import indent
import tomli_w
# Port configuration (keeping them distinct)
DEFAULT_VITE_PORT = 5173 # Vite dev server (frontend HMR)
DEFAULT_BACKEND_PORT = 5180 # FastAPI in dev mode (Vite proxies /api here)
DEFAULT_PROD_PORT = 5080 # Production server (static files served by FastAPI)
import tomlkit
# Template directory
TEMPLATE_DIR = Path(__file__).parent / "template"
# Marker comment indicating file can be auto-upgraded
# Users should remove this line to prevent automatic updates
UPGRADE_MARKER = "auto-upgrade@fastapi-vue-setup"
# pyproject.toml additions for patched projects
PYPROJECT_ADDITIONS = {
"tool": {
"hatch": {
"build": {
"packages": ["{{MODULE_NAME}}"],
"artifacts": ["{{MODULE_NAME}}/frontend-build"],
"packages": ["MODULE_NAME"],
"artifacts": ["MODULE_NAME/frontend-build"],
"targets": {
"sdist": {
"hooks": {
@@ -54,6 +48,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
# =============================================================================
@@ -70,8 +129,7 @@ def find_module_name(project_dir: Path) -> str | None:
if not pyproject.exists():
return None
with open(pyproject, "rb") as f:
data = tomllib.load(f)
data = tomlkit.parse(pyproject.read_text())
if "project" in data and "name" in data["project"]:
name = data["project"]["name"]
@@ -123,10 +181,10 @@ def _find_app_in_file(path: Path) -> str | None:
def render_template(template: str, **kwargs) -> str:
"""Simple template rendering with {{KEY}} placeholders."""
"""Simple template rendering replacing KEY with value."""
result = template
for key, value in kwargs.items():
result = result.replace(f"{{{{{key}}}}}", str(value))
result = result.replace(key, value)
return result
@@ -144,31 +202,17 @@ def patch_app_file(
print(f"❌ Cannot patch {path} - file not found")
return False
content = path.read_text()
original_content = path.read_text()
marker = "from fastapi_vue import Frontend"
if marker in content:
print(f" Skipping {path} (already patched)")
if marker in original_content:
print(f" {path} (already patched)")
return False
if dry_run:
print(f"[DRY RUN] Would patch {path}")
return True
# Find where to insert the import (after other imports)
lines = content.split("\n")
lines = original_content.split("\n")
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}, "/")'
# Find last import line and check if pathlib is imported
@@ -184,12 +228,17 @@ frontend = Frontend(
# Stop at first non-import, non-comment, non-empty line after imports
break
# Check if we found imports to insert after
if last_import_idx == 0 and not lines[0].strip().startswith(("import ", "from ")):
print(f"⚠️ Skipping {path} (no imports found to patch)")
return False
# Insert imports after last import, then frontend instantiation
if not has_pathlib:
lines.insert(last_import_idx + 1, "from pathlib import Path")
last_import_idx += 1
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
lines.append("")
@@ -222,12 +271,21 @@ frontend = Frontend(
content = content[:insert_pos] + load_code + content[insert_pos:]
lifespan_patched = True
# Check if content actually changed
if content == original_content:
print(f"⚠️ Skipping {path} (no changes needed)")
return False
if dry_run:
print(f"[DRY RUN] Would patch {path}")
return True
path.write_text(content)
print(f"✅ Patched {path}")
if not lifespan_patched:
# Check if they're using deprecated on_event
if "@" + app_var + ".on_event" in content or f"@{app_var}.on_event" in content:
if f"@{app_var}.on_event" in content:
print()
print("⚠️ Your app uses the deprecated @app.on_event decorator.")
print(" Please migrate to the lifespan pattern and add:")
@@ -246,8 +304,6 @@ frontend = Frontend(
def patch_vite_config(
path: Path,
module_name: str,
backend_port: int,
vite_port: int,
dry_run: bool = False,
) -> bool:
"""Patch an existing vite.config.js/ts by adding fastapi-vue plugin.
@@ -259,21 +315,17 @@ def patch_vite_config(
print(f"❌ Cannot patch {path} - file not found")
return False
content = path.read_text()
original_content = path.read_text()
marker = "vite-plugin-fastapi"
if marker in content:
print(f" Skipping {path} (already patched)")
if marker in original_content:
print(f" {path} (already patched)")
return False
if dry_run:
print(f"[DRY RUN] Would patch {path}")
return True
# Add import for the plugin at the top (after other imports)
import_line = f"import fastapiVue from './{marker}.js'"
lines = content.split("\n")
lines = original_content.split("\n")
new_lines = []
import_inserted = False
@@ -305,6 +357,18 @@ def patch_vite_config(
if match:
insert_pos = match.end()
content = content[:insert_pos] + "\n fastapiVue()," + content[insert_pos:]
else:
print(f"⚠️ Skipping {path} (no plugins array found)")
return False
# Check if content actually changed
if content == original_content:
print(f"⚠️ Skipping {path} (no changes needed)")
return False
if dry_run:
print(f"[DRY RUN] Would patch {path}")
return True
path.write_text(content)
print(f"✅ Patched {path}")
@@ -340,116 +404,116 @@ def patch_frontend_health_check(frontend_dir: Path, dry_run: bool = False) -> bo
print("⚠️ No Vue file found to patch, skipping frontend health check")
return False
content = target_file.read_text()
original_content = target_file.read_text()
# Check if already patched
if "/api/health" in content:
print(f" Skipping {target_file} (already patched)")
if "/api/health" in original_content:
print(f" {target_file} (already patched)")
return False
content = original_content
# Detect if TypeScript (has lang="ts" in script tag)
is_typescript = 'lang="ts"' in content
# Build the script content based on JS/TS
script_addition = (
TS_HEALTH_CHECK_SCRIPT if is_typescript else JS_HEALTH_CHECK_SCRIPT
)
# Insert script addition before </script>
script_end_match = re.search(r"</script>", content)
if not script_end_match:
print(f"⚠️ Skipping {target_file} (no </script> tag found)")
return False
insert_pos = script_end_match.start()
content = content[:insert_pos] + script_addition + content[insert_pos:]
# Insert status inline - find the best place based on file type
# For HelloWorld.vue: insert before </h3>
# For App.vue (minimal): only patch if it's the default "You did it!" template
if "HelloWorld" in str(target_file):
# Insert before closing </h3>
h3_close = content.find(" </h3>")
if h3_close == -1:
print(
f"⚠️ Skipping {target_file} (no </h3> tag found for status insertion)"
)
return False
before, after = content[:h3_close], content[h3_close:]
content = f"{before}{indent(STATUS_SPAN_TEMPLATE, ' ')}{after}"
else:
# Minimal App.vue - only patch if it contains the default welcome message
if "<h1>You did it!</h1>" not in content:
print(f"⚠️ Skipping {target_file} (not a default Vue template)")
return False
# Insert before the </p> tag
template_end = content.find("</template>")
if template_end == -1:
print(f"⚠️ Skipping {target_file} (no </template> tag found)")
return False
# Find </p> before </template>
last_p = content.rfind("</p>", 0, template_end)
if last_p == -1:
print(f"⚠️ Skipping {target_file} (no </p> tag found for status insertion)")
return False
before, after = content[:last_p], content[last_p:]
content = f"{before}{STATUS_SPAN_TEMPLATE}{after}"
# Check if content actually changed
if content == original_content:
print(f"⚠️ Skipping {target_file} (no changes needed)")
return False
if dry_run:
print(f"[DRY RUN] Would patch {target_file}")
return True
# Detect if TypeScript (has lang="ts" in script tag)
is_typescript = 'lang="ts"' in content
# Build the script content based on JS/TS
if is_typescript:
script_addition = '''
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 (no wrapper, fits in existing text)
status_span = '''<span class="backend-status">
— FastAPI backend:
<span v-if="backendStatus === 'checking'">⏳</span>
<span v-else-if="backendStatus === 'connected'" class="connected">✓ connected</span>
<span v-else class="error">✗ not reachable</span>
</span>'''
style_addition = '''
.backend-status {
white-space: nowrap;
}
.backend-status .connected {
color: hsla(160, 100%, 37%, 1);
}
.backend-status .error {
color: hsla(0, 100%, 50%, 1);
}
'''
# Insert script addition before </script>
script_end_match = re.search(r"</script>", content)
if script_end_match:
insert_pos = script_end_match.start()
content = content[:insert_pos] + script_addition + content[insert_pos:]
# Insert status inline - find the best place based on file type
# For HelloWorld.vue: insert before </h3>
# For App.vue (minimal): insert before the last </p> in template
if "HelloWorld" in str(target_file):
# Insert before closing </h3>
h3_close = content.find("</h3>")
if h3_close != -1:
content = content[:h3_close] + "\n " + status_span + "\n " + content[h3_close:]
else:
# Minimal App.vue - insert before the last </p> before </template>
template_end = content.find("</template>")
if template_end != -1:
# Find last </p> before </template>
last_p = content.rfind("</p>", 0, template_end)
if last_p != -1:
content = content[:last_p] + "\n " + status_span + "\n " + content[last_p:]
# Insert style addition before </style>
style_end = content.rfind("</style>")
if style_end != -1:
content = content[:style_end] + style_addition + content[style_end:]
target_file.write_text(content)
print(f"✅ Patched {target_file}")
return True
# Track .new.py files written during setup (for merge notification)
_new_files_written: list[tuple[Path, Path]] = []
def write_file(
path: Path, content: str, overwrite: bool = True, dry_run: bool = False
path: Path,
content: str,
overwrite: bool = True,
dry_run: bool = False,
executable: bool = False,
fallback_path: Path | None = None,
) -> bool:
"""Write content to a file, handling existing files and dry-run."""
"""Write content to a file, handling existing files and dry-run.
If fallback_path is provided and the file exists without the upgrade marker,
the content will be written to fallback_path instead of being skipped.
"""
exists = path.exists()
if exists and not overwrite:
print(f" Skipping {path} (exists)")
print(f" Skipping {path} (exists)")
return False
# Check if content is the same
if exists:
existing_content = path.read_text()
if existing_content == content:
print(f"✔️ {path} (already up to date)")
return False
# If overwrite requested but file doesn't have upgrade marker
if overwrite and UPGRADE_MARKER not in existing_content:
if fallback_path is not None:
# Write to fallback path instead
return _write_fallback_file(
path, fallback_path, content, dry_run, executable
)
print(f"️ Skipping {path} (customized by user)")
return False
if dry_run:
action = "overwrite" if exists else "create"
print(f"[DRY RUN] Would {action} {path}")
@@ -457,52 +521,107 @@ def write_file(
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
if executable and sys.platform != "win32":
path.chmod(path.stat().st_mode | 0o111)
action = "Updated" if exists else "Created"
print(f"{action} {path}")
return True
def merge_pyproject(data: dict, additions: dict, module_name: str) -> dict:
"""Merge additions into pyproject.toml data."""
result = data.copy()
def _write_fallback_file(
original_path: Path,
fallback_path: Path,
content: str,
dry_run: bool,
executable: bool,
) -> bool:
"""Write content to a fallback .new.py file when original can't be overwritten."""
# Check if fallback already has same content
if fallback_path.exists():
if fallback_path.read_text() == content:
print(f"✔️ {fallback_path} (already up to date)")
return False
if dry_run:
print(f"[DRY RUN] Would create {fallback_path} (original customized by user)")
_new_files_written.append((fallback_path, original_path))
return True
fallback_path.parent.mkdir(parents=True, exist_ok=True)
fallback_path.write_text(content)
if executable and sys.platform != "win32":
fallback_path.chmod(fallback_path.stat().st_mode | 0o111)
print(f"✅ Created {fallback_path} (original customized by user)")
_new_files_written.append((fallback_path, original_path))
return True
def merge_pyproject(
data: tomlkit.TOMLDocument, additions: dict, module_name: str
) -> tomlkit.TOMLDocument:
"""Merge additions into pyproject.toml data, preserving comments and existing values."""
# Ensure hatchling build system is configured
if "build-system" not in result:
result["build-system"] = {}
result["build-system"]["requires"] = ["hatchling"]
result["build-system"]["build-backend"] = "hatchling.build"
if "build-system" not in data:
data["build-system"] = tomlkit.table()
# Add dependencies
if "project" not in result:
result["project"] = {}
if "dependencies" not in result["project"]:
result["project"]["dependencies"] = []
# Add hatchling to requires if not present, preserving existing requires
if "requires" not in data["build-system"]:
data["build-system"]["requires"] = ["hatchling"]
else:
requires = list(data["build-system"]["requires"])
if not any(r.startswith("hatchling") for r in requires):
requires.insert(0, "hatchling")
data["build-system"]["requires"] = requires
if "build-backend" not in data["build-system"]:
data["build-system"]["build-backend"] = "hatchling.build"
# Ensure project table exists
if "project" not in data:
data["project"] = tomlkit.table()
# Add hatch build config
if "tool" not in result:
result["tool"] = {}
if "hatch" not in result["tool"]:
result["tool"]["hatch"] = {}
if "build" not in result["tool"]["hatch"]:
result["tool"]["hatch"]["build"] = {}
if "tool" not in data:
data["tool"] = tomlkit.table()
if "hatch" not in data["tool"]:
data["tool"]["hatch"] = tomlkit.table()
if "build" not in data["tool"]["hatch"]:
data["tool"]["hatch"]["build"] = tomlkit.table()
hatch_build = additions["tool"]["hatch"]["build"]
result["tool"]["hatch"]["build"]["packages"] = [
p.replace("{{MODULE_NAME}}", module_name) for p in hatch_build["packages"]
]
result["tool"]["hatch"]["build"]["artifacts"] = [
a.replace("{{MODULE_NAME}}", module_name) for a in hatch_build["artifacts"]
]
result["tool"]["hatch"]["build"]["only-packages"] = hatch_build["only-packages"]
hatch_build = data["tool"]["hatch"]["build"]
hatch_additions = additions["tool"]["hatch"]["build"]
if "targets" not in result["tool"]["hatch"]["build"]:
result["tool"]["hatch"]["build"]["targets"] = {}
# Set packages if not already set
if "packages" not in hatch_build:
hatch_build["packages"] = [
p.replace("MODULE_NAME", module_name) for p in hatch_additions["packages"]
]
result["tool"]["hatch"]["build"]["targets"]["sdist"] = hatch_build["targets"][
"sdist"
]
# Set artifacts if not already set
if "artifacts" not in hatch_build:
hatch_build["artifacts"] = [
a.replace("MODULE_NAME", module_name) for a in hatch_additions["artifacts"]
]
return result
# Set only-packages if not already set
if "only-packages" not in hatch_build:
hatch_build["only-packages"] = hatch_additions["only-packages"]
# Add sdist target with custom hook
if "targets" not in hatch_build:
hatch_build["targets"] = tomlkit.table()
if "sdist" not in hatch_build["targets"]:
hatch_build["targets"]["sdist"] = tomlkit.table()
if "hooks" not in hatch_build["targets"]["sdist"]:
hatch_build["targets"]["sdist"]["hooks"] = tomlkit.table()
if "custom" not in hatch_build["targets"]["sdist"]["hooks"]:
hatch_build["targets"]["sdist"]["hooks"]["custom"] = tomlkit.table()
if "path" not in hatch_build["targets"]["sdist"]["hooks"]["custom"]:
hatch_build["targets"]["sdist"]["hooks"]["custom"]["path"] = hatch_additions[
"targets"
]["sdist"]["hooks"]["custom"]["path"]
return data
# =============================================================================
@@ -573,9 +692,12 @@ def ensure_python_project(project_dir: Path, dry_run: bool = False) -> bool:
def ensure_frontend(project_dir: Path, dry_run: bool = False) -> bool:
"""Ensure frontend directory exists, run create-vue if needed."""
"""Ensure frontend directory exists with a Vue project, run create-vue if needed."""
frontend_dir = project_dir / "frontend"
if frontend_dir.exists():
package_json = frontend_dir / "package.json"
# Check for package.json, not just directory existence (empty dir shouldn't count)
if package_json.exists():
return True
# Find JS runtime
@@ -632,13 +754,6 @@ def cmd_setup(args: argparse.Namespace) -> int:
project_dir = project_dir.resolve()
dry_run = args.dry_run
# Check if port options were explicitly specified (for updating existing files)
ports_specified = (
args.vite_port != DEFAULT_VITE_PORT
or args.backend_port != DEFAULT_BACKEND_PORT
or args.prod_port != DEFAULT_PROD_PORT
)
# Create project directory if it doesn't exist
if not project_dir.exists():
if dry_run:
@@ -652,12 +767,12 @@ def cmd_setup(args: argparse.Namespace) -> int:
if dry_run:
print("\n🏃 DRY RUN MODE - no changes will be made\n")
# Step 1: Ensure Python project exists
if not ensure_python_project(project_dir, dry_run):
# Step 1: Ensure frontend exists (do this first so cancellation doesn't leave partial setup)
if not ensure_frontend(project_dir, dry_run):
return 1
# Step 2: Ensure frontend exists
if not ensure_frontend(project_dir, dry_run):
# Step 2: Ensure Python project exists
if not ensure_python_project(project_dir, dry_run):
return 1
# Detect module name
@@ -667,24 +782,15 @@ def cmd_setup(args: argparse.Namespace) -> int:
module_name = project_dir.name.replace("-", "_")
print(f"📦 Using module name from directory: {module_name}")
# Ports
vite_port = args.vite_port
backend_port = args.backend_port
prod_port = args.prod_port
# Title for templates
project_title = module_name.replace("_", " ").title()
print(f"📦 Module: {module_name}")
print(f"🔌 Ports: Vite={vite_port}, Backend(dev)={backend_port}, Prod={prod_port}")
# Template variables
tpl_vars = {
"MODULE_NAME": module_name,
"PROJECT_TITLE": project_title,
"VITE_PORT": vite_port,
"BACKEND_PORT": backend_port,
"PROD_PORT": prod_port,
}
module_dir = project_dir / module_name
@@ -713,19 +819,39 @@ def cmd_setup(args: argparse.Namespace) -> int:
module_dir.mkdir(parents=True)
# === Install scripts (always update our own scripts) ===
script_files = [
# util.py and build-frontend.py are internal and always overwritten
# devserver.py can be customized, so use fallback if needed
internal_script_files = [
(fastapi_vue_scripts / "util.py", "scripts/fastapi-vue/util.py"),
(
fastapi_vue_scripts / "build-frontend.py",
"scripts/fastapi-vue/build-frontend.py",
),
(scripts_dir / "devserver.py", "scripts/devserver.py"),
]
for dest_path, template_path in script_files:
for dest_path, template_path in internal_script_files:
template = load_template(template_path)
content = render_template(template, **tpl_vars)
write_file(dest_path, content, overwrite=True, dry_run=dry_run)
write_file(
dest_path,
content,
overwrite=True,
dry_run=dry_run,
)
# devserver.py - use fallback if customized by user
devserver_path = scripts_dir / "devserver.py"
devserver_fallback = scripts_dir / "devserver.new.py"
template = load_template("scripts/devserver.py")
content = render_template(template, **tpl_vars)
write_file(
devserver_path,
content,
overwrite=True,
dry_run=dry_run,
executable=True,
fallback_path=devserver_fallback,
)
# === Handle app module ===
if app_file:
@@ -748,12 +874,21 @@ def cmd_setup(args: argparse.Namespace) -> int:
# === Handle __main__.py ===
main_file = module_dir / "__main__.py"
if not main_file.exists() or ports_specified:
template = load_template("backend/__main__.py")
content = render_template(template, **tpl_vars)
write_file(main_file, content, overwrite=True, dry_run=dry_run)
else:
print(f"⚠️ Skipping {main_file} (exists, use --prod-port to update)")
main_fallback = module_dir / "__main__.new.py"
template = load_template("backend/__main__.py")
main_content = render_template(template, **tpl_vars)
# Use write_file with fallback - it handles all cases:
# - File doesn't exist: create it
# - File exists with marker: update it
# - File exists without marker: write to fallback
write_file(
main_file,
main_content,
overwrite=True,
dry_run=dry_run,
fallback_path=main_fallback,
)
# === Update vite.config.js/ts ===
frontend_dir = project_dir / "frontend"
@@ -769,13 +904,9 @@ def cmd_setup(args: argparse.Namespace) -> int:
vite_config_js = frontend_dir / "vite.config.js"
if vite_config_ts.exists():
patch_vite_config(
vite_config_ts, module_name, backend_port, vite_port, dry_run
)
patch_vite_config(vite_config_ts, module_name, dry_run)
elif vite_config_js.exists():
patch_vite_config(
vite_config_js, module_name, backend_port, vite_port, dry_run
)
patch_vite_config(vite_config_js, module_name, dry_run)
else:
print("⚠️ No vite.config.ts or vite.config.js found in frontend/")
print(" Run create-vue first to generate a Vite config to patch.")
@@ -786,39 +917,43 @@ def cmd_setup(args: argparse.Namespace) -> int:
# === Update pyproject.toml ===
pyproject_path = project_dir / "pyproject.toml"
if pyproject_path.exists():
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)
old_content = pyproject_path.read_text()
data = tomlkit.parse(old_content)
updated = merge_pyproject(data, PYPROJECT_ADDITIONS, module_name)
# Add script entry pointing to the app we found/created
if "scripts" not in updated["project"]:
updated["project"]["scripts"] = {}
script_name = module_name.replace("_", "-")
updated["project"]["scripts"][script_name] = f"{module_name}.__main__:main"
# Only add script entry if project doesn't already have scripts
if "scripts" not in updated["project"] or not updated["project"]["scripts"]:
updated["project"]["scripts"] = tomlkit.table()
script_name = module_name.replace("_", "-")
updated["project"]["scripts"][script_name] = f"{module_name}.__main__:main"
if dry_run:
# Check if content actually changed
new_content = tomlkit.dumps(updated)
if new_content == old_content:
print(f"✔️ {pyproject_path} (already up to date)")
elif dry_run:
print(f"[DRY RUN] Would update {pyproject_path}")
else:
with open(pyproject_path, "wb") as f:
tomli_w.dump(updated, f)
pyproject_path.write_text(new_content)
print(f"✅ Updated {pyproject_path}")
# === Add dependencies using uv ===
if dry_run:
print("[DRY RUN] Would run: uv add 'fastapi[standard]' fastapi-vue")
print("[DRY RUN] Would run: uv add --group dev httpx")
print("[DRY RUN] Would run: uv add -U 'fastapi[standard]' fastapi-vue")
print("[DRY RUN] Would run: uv add -U --group dev httpx")
else:
print("📦 Adding dependencies...")
result = subprocess.run(
["uv", "add", "fastapi[standard]", "fastapi-vue"],
["uv", "add", "-U", "fastapi[standard]", "fastapi-vue"],
cwd=project_dir,
check=False,
)
if result.returncode != 0:
print("⚠️ Failed to add main dependencies")
result = subprocess.run(
["uv", "add", "--group", "dev", "httpx"],
["uv", "add", "-U", "--group", "dev", "httpx"],
cwd=project_dir,
check=False,
)
@@ -854,18 +989,23 @@ def cmd_setup(args: argparse.Namespace) -> int:
cd_cmd = "" if project_dir == Path.cwd() else f"cd {project_dir}\n "
script_name = module_name.replace("_", "-")
print(f"""
Next steps:
message = SETUP_COMPLETE_MESSAGE.replace("CD_CMD", cd_cmd).replace(
"SCRIPT_NAME", script_name
)
print(message)
1. Build for production:
{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}
""")
# Show merge note if any .new.py files were written
if _new_files_written:
print()
print(
"⚠️ Note: Some files could not be auto-upgraded because you customized them."
)
print(" Please manually merge the following files:")
for new_file, original_file in _new_files_written:
print(f"{new_file.name}{original_file.name}")
print()
# Clear the list for potential subsequent runs
_new_files_written.clear()
return 0
@@ -904,60 +1044,24 @@ Examples:
fastapi-vue-setup my-new-project Create a new project from scratch
fastapi-vue-setup . Set up integration in current directory
fastapi-vue-setup . --dry-run Preview what would be done
fastapi-vue-setup . --vite-port 3000 Update port configuration
""",
)
parser.add_argument(
"project_dir",
nargs="?",
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(
"--vite-port",
type=int,
default=DEFAULT_VITE_PORT,
help=f"Vite dev server port (default: {DEFAULT_VITE_PORT})",
)
parser.add_argument(
"--backend-port",
type=int,
default=DEFAULT_BACKEND_PORT,
help=f"Backend API port in dev mode (default: {DEFAULT_BACKEND_PORT})",
)
parser.add_argument(
"--prod-port",
type=int,
default=DEFAULT_PROD_PORT,
help=f"Production server port (default: {DEFAULT_PROD_PORT})",
)
parser.add_argument(
"--dry-run", action="store_true", help="Show what would be done"
)
args = parser.parse_args()
# Handle default project directory with safety check
if args.project_dir is None:
cwd = Path.cwd()
if not is_uninitialized_folder(cwd) and not is_already_patched(cwd):
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 = "."
parser.print_help()
return 0
return cmd_setup(args)
+1 -1
View File
@@ -9,7 +9,7 @@ description = "Tool to create or patch FastAPI+Vue projects with integrated buil
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"tomli-w>=1.0.0",
"tomlkit>=0.12.0",
]
[project.urls]
-3
View File
@@ -1,3 +0,0 @@
"""{{PROJECT_TITLE}} package."""
__version__ = "0.1.0"
+46 -23
View File
@@ -1,31 +1,54 @@
"""Entry point for the application."""
# auto-upgrade@fastapi-vue-setup - remove this if you modify this file
import argparse
import asyncio
import os
import sys
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():
"""Run the FastAPI application using uvicorn."""
import uvicorn
if len(sys.argv) > 1:
endpoint = sys.argv[1]
if ":" in endpoint:
host, port = endpoint.rsplit(":", 1)
host = host or "localhost"
port = int(port)
else:
host = "localhost"
port = int(endpoint)
else:
host = "localhost"
port = {{PROD_PORT}}
uvicorn.run(
"{{MODULE_NAME}}.app:app",
host=host,
port=port,
log_level="info",
parser = argparse.ArgumentParser(description="Run the MODULE_NAME server.")
parser.add_argument(
"endpoint",
nargs="?",
help=(
f"Endpoint (default: localhost:{DEFAULT_PORT}). "
"Forms: host:port | :port | [ipv6]:port | ip | host | unix:/path.sock"
),
)
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__":
+8 -15
View File
@@ -1,17 +1,12 @@
"""Main FastAPI application."""
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi_vue import Frontend
# Frontend static file server - configure options here
# Vue Frontend static files
frontend = Frontend(
Path(__file__).parent / "frontend-build",
spa=True,
favicon="/assets/favicon",
cached=["/assets/"],
Path(__file__).with_name("frontend-build"), spa=True, cached=["/assets/"]
)
@@ -22,19 +17,17 @@ async def lifespan(app: FastAPI):
yield
app = FastAPI(title="{{PROJECT_TITLE}}", lifespan=lifespan)
app = FastAPI(title="PROJECT_TITLE", lifespan=lifespan)
# Add API routes here...
# Health check endpoint for the Vue demo app to verify the backend is running
@app.get("/api/health")
async def health_check():
"""Health check endpoint for the dev server."""
return {"status": "ok"}
# Add your API routes here
# @app.get("/api/example")
# async def example():
# return {"message": "Hello World"}
# Final catch-all route for frontend files (keep at end of file)
frontend.route(app, "/")
+16 -20
View File
@@ -6,34 +6,30 @@
* - Builds to the Python module's frontend-build directory
*
* Environment variables (with defaults):
* VITE_PORT=5173 - Vite dev server port
* VITE_BACKEND_URL=http://localhost:5180 - Backend API URL for proxying
* FASTAPI_VUE_BACKEND_URL=http://localhost:5180 - Backend API URL for proxying
*/
const backendUrl =
process.env.VITE_BACKEND_URL || "http://localhost:{{BACKEND_PORT}}";
const vitePort = parseInt(process.env.VITE_PORT || "{{VITE_PORT}}");
const backendUrl = process.env.FASTAPI_VUE_BACKEND_URL || "http://localhost:5180"
export default function fastapiVue({ paths = ["/api"] } = {}) {
// Build proxy configuration for each path
const proxy = {}
for (const path of paths) {
proxy[path] = {
target: backendUrl,
changeOrigin: false,
ws: true,
}
}
export default function fastapiVue() {
return {
name: "fastapi-vite",
config: () => ({
server: {
host: "localhost",
port: vitePort,
strictPort: true,
proxy: {
"/api": {
target: backendUrl,
changeOrigin: false,
ws: true,
},
},
},
server: { proxy },
build: {
outDir: "../{{MODULE_NAME}}/frontend-build",
outDir: "../MODULE_NAME/frontend-build",
emptyOutDir: true,
},
}),
};
}
}
Regular → Executable
+80 -71
View File
@@ -1,36 +1,41 @@
#!/usr/bin/env -S uv run
# 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]
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.
Backend always listens on localhost:{{BACKEND_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 asyncio
import contextlib
import ipaddress
import os
import sys
from pathlib import Path
from sys import stderr
from urllib.parse import urlparse
import httpx
from fastapi_vue.hostutil import parse_endpoint
exec((Path(__file__).parent / "fastapi-vue/util.py").read_text("UTF-8")) # noqa: S102
DEFAULT_HOST = "localhost"
DEFAULT_VITE_PORT = {{VITE_PORT}}
BACKEND_PORT = {{BACKEND_PORT}}
DEFAULT_VITE_PORT = 5173
DEFAULT_BACKEND_PORT = 5180
FRONTEND_PATH = Path(__file__).parent.parent / "frontend"
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
@@ -39,43 +44,8 @@ BUN_BUG = """\
"""
def parse_endpoint(value: str | None) -> tuple[str | None, int, bool]:
"""Parse an endpoint for Vite (no unix socket support).
Returns (host, port, all_ifaces).
"""
if not value:
return DEFAULT_HOST, DEFAULT_VITE_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_VITE_PORT, False
# Use urllib.parse for everything else
parsed = urlparse(f"//{value}")
host = parsed.hostname or DEFAULT_HOST
port = parsed.port or DEFAULT_VITE_PORT
return host, port, False
def resolve_frontend_tools(
vite_host: str | None, vite_port: int, all_ifaces: bool
vite_port: int, all_ifaces: bool
) -> tuple[list[str], list[str], str]:
"""Resolve frontend install and dev commands.
@@ -86,7 +56,7 @@ def resolve_frontend_tools(
stderr.write(f"┃ ⚠️ Frontend source not found at {FRONTEND_PATH}\n")
raise SystemExit(1)
result = find_js_runtime()
result = find_js_runtime() # noqa # type: ignore
if result is None:
if not os.environ.get("JS_RUNTIME"):
stderr.write("┃ ⚠️ deno, npm or bun needed to run the frontend server.\n")
@@ -106,12 +76,15 @@ def resolve_frontend_tools(
}
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:
dev_cmd.append("--host")
elif vite_host:
dev_cmd.extend(["--host", vite_host])
if name == "bun":
stderr.write(BUN_BUG)
@@ -119,13 +92,15 @@ def resolve_frontend_tools(
return install_cmd, dev_cmd, name
async def wait_for_backend():
async def wait_for_backend(host: str, port: int):
"""Wait for the backend to be ready by polling the health endpoint."""
max_attempts = 50
url = f"http://{host}:{port}"
async with httpx.AsyncClient() as client:
for attempt in range(max_attempts):
try:
await client.get(f"http://localhost:{BACKEND_PORT}", timeout=1.0)
await client.get(url, timeout=1.0)
stderr.write("✓ Backend ready!\n")
return True
except httpx.RequestError:
@@ -155,19 +130,29 @@ async def _terminate_process(proc: asyncio.subprocess.Process, name: str) -> Non
async def run_devserver(
vite_host: str | None, vite_port: int, all_ifaces: bool
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
)
install_cmd, dev_cmd, tool_name = resolve_frontend_tools(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}"
)
os.environ["FASTAPI_VUE_FRONTEND_URL"] = f"http://localhost:{vite_port}"
# Tell Vite where the backend is (for proxying /api requests)
os.environ["VITE_BACKEND_URL"] = f"http://localhost:{BACKEND_PORT}"
os.environ["FASTAPI_VUE_BACKEND_URL"] = f"http://{backend_host}:{backend_port}"
backend_cmd = [
"uvicorn",
"MODULE_NAME.app:app",
"--host",
backend_host,
"--port",
str(backend_port),
"--reload",
]
cwd = str(Path(__file__).parent.parent)
frontend_cwd = str(FRONTEND_PATH)
@@ -185,22 +170,13 @@ async def run_devserver(
await asyncio.sleep(0.1)
# Start backend (concurrent with install)
backend_cmd = [
"uvicorn",
"{{MODULE_NAME}}.app:app",
"--host",
"localhost",
"--port",
str(BACKEND_PORT),
"--reload",
]
stderr.write(f">>> {' '.join(backend_cmd)}\n")
backend_proc = await asyncio.create_subprocess_exec(*backend_cmd, cwd=cwd)
# 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(), name="backend_ready"
wait_for_backend(backend_host, backend_port), name="backend_ready"
)
done, pending = await asyncio.wait(
@@ -257,11 +233,44 @@ async def run_devserver(
def main():
hostport = sys.argv[1] if len(sys.argv) > 1 else None
vite_host, vite_port, all_ifaces = parse_endpoint(hostport)
parser = argparse.ArgumentParser(
description="Run Vite and FastAPI development servers",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=EPILOG,
)
parser.add_argument(
"frontend",
nargs="?",
metavar="host:port",
help="Vite frontend endpoint (default: localhost:5173)",
)
parser.add_argument(
"--backend",
metavar="host:port",
help="FastAPI backend endpoint (default: localhost:5180)",
)
args = parser.parse_args()
# parse_endpoint returns list of dicts with host/port or uds keys
# Multiple entries means bind all interfaces (IPv4 + IPv6)
vite_endpoints = parse_endpoint(args.frontend, DEFAULT_VITE_PORT)
backend_endpoints = parse_endpoint(args.backend, DEFAULT_BACKEND_PORT)
# Vite doesn't support unix sockets
if "uds" in vite_endpoints[0]:
stderr.write("┃ ⚠️ Unix sockets not supported for frontend\n")
raise SystemExit(1)
if "uds" in backend_endpoints[0]:
stderr.write("┃ ⚠️ Unix sockets not supported for backend\n")
raise SystemExit(1)
vite_port = vite_endpoints[0]["port"]
all_ifaces = len(vite_endpoints) > 1
backend_host = backend_endpoints[0]["host"]
backend_port = backend_endpoints[0]["port"]
with contextlib.suppress(KeyboardInterrupt):
asyncio.run(run_devserver(vite_host, vite_port, all_ifaces))
asyncio.run(run_devserver(vite_port, all_ifaces, backend_host, backend_port))
if __name__ == "__main__":
@@ -4,7 +4,7 @@ import subprocess
from pathlib import Path
from sys import stderr
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
from hatchling.builders.hooks.plugin.interface import BuildHookInterface # type: ignore
exec(Path(__file__).with_name("util.py").read_text("UTF-8")) # noqa: S102
@@ -23,7 +23,7 @@ class CustomBuildHook(BuildHookInterface):
super().initialize(version, build_data)
stderr.write(">>> Building the frontend\n")
install_cmd, build_cmd = find_build_tool()
install_cmd, build_cmd = find_build_tool() # noqa # type: ignore
try:
run(install_cmd, cwd="frontend")
+2 -1
View File
@@ -51,8 +51,9 @@ def find_build_tool():
"npm": ("install",),
"bun": ("--bun", "install"),
}
# Run vite directly for deno to avoid npm-run-all2/run-p issues
build = {
"deno": ("task", "build"),
"deno": ("run", "-A", "npm:vite", "build"),
"npm": ("run", "build"),
"bun": ("--bun", "run", "build"),
}