- DEVMODE removed; FASTAPI_VUE=MEDIAHIVE is set at entrypoints and fastapi_vue.env drives dev-mode checks - CLI roots now reach the server via fastapi-vue's env(Config) teleport (mediahive.config.config) instead of MEDIAHIVE_ROOTS - Legacy media_folder config field and migration removed - mediahive.* logger level set via log_config (DEBUG in dev, INFO in prod) in both server.run() and winmain's patched uvicorn config
238 lines
7.0 KiB
Python
238 lines
7.0 KiB
Python
"""Utilities used at build time and in devserver script. No dependencies."""
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
MIN_NODE_VERSION = 20
|
|
|
|
|
|
class _Formatter(logging.Formatter):
|
|
"""Prefix formatter, intentionally different from fastapi_vue.logging.
|
|
|
|
INFO and below pass through unprefixed so messages can use their own
|
|
markings (>>>, ###); WARNING and above get an emoji prefix.
|
|
"""
|
|
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
if record.levelno >= logging.ERROR:
|
|
return f"🛑 {record.getMessage()}"
|
|
if record.levelno >= logging.WARNING:
|
|
return f"💣 {record.getMessage()}"
|
|
return record.getMessage()
|
|
|
|
|
|
_handler = logging.StreamHandler()
|
|
_handler.setFormatter(_Formatter())
|
|
logger = logging.getLogger("fastapi-vue")
|
|
logger.addHandler(_handler)
|
|
logger.setLevel(logging.INFO)
|
|
logger.propagate = False # own handler; do not double-print via a configured root
|
|
|
|
|
|
def _check_node_version(node_path: str) -> None:
|
|
"""Check if Node.js version is >= 20.
|
|
|
|
Raises RuntimeError if version is too old or cannot be determined.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
[node_path, "--version"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
version_str = result.stdout.strip()
|
|
# Parse version like "v20.10.0" or "v18.17.1"
|
|
match = re.match(r"v(\d+)", version_str)
|
|
if match:
|
|
major_version = int(match.group(1))
|
|
if major_version >= MIN_NODE_VERSION:
|
|
return
|
|
msg = f"Node.js {version_str} found, but v{MIN_NODE_VERSION}+ required"
|
|
raise RuntimeError(msg)
|
|
except subprocess.CalledProcessError, FileNotFoundError, ValueError:
|
|
pass
|
|
msg = "Could not determine Node.js version"
|
|
raise RuntimeError(msg)
|
|
|
|
|
|
def _validate_npm_runtime(tool: str) -> bool:
|
|
"""Validate npm runtime by checking Node.js version. Returns True if valid."""
|
|
node_path = shutil.which("node", path=str(Path(tool).parent))
|
|
if node_path is None:
|
|
return False
|
|
try:
|
|
_check_node_version(node_path)
|
|
except RuntimeError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _find_runtime_from_env(options: list[str]) -> tuple[str, str] | None:
|
|
"""Find runtime specified by JS_RUNTIME environment variable."""
|
|
js_runtime_env = os.environ.get("JS_RUNTIME")
|
|
if not js_runtime_env:
|
|
return None
|
|
|
|
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 and not runtime_name.startswith(option):
|
|
continue
|
|
|
|
tool = shutil.which(js_runtime)
|
|
if tool is None:
|
|
msg = f"JS_RUNTIME={js_runtime_env}: {option} not found"
|
|
raise RuntimeError(msg)
|
|
|
|
if option == "npm":
|
|
node_path = shutil.which("node", path=str(Path(tool).parent))
|
|
if node_path is None:
|
|
msg = f"JS_RUNTIME={js_runtime_env}: node not found"
|
|
raise RuntimeError(msg)
|
|
_check_node_version(node_path)
|
|
|
|
return tool, option
|
|
|
|
msg = f"JS_RUNTIME={js_runtime_env} not recognized"
|
|
raise RuntimeError(msg)
|
|
|
|
|
|
def _auto_detect_runtime(options: list[str]) -> tuple[str, str]:
|
|
"""Auto-detect JavaScript runtime from available options."""
|
|
node_version_error: RuntimeError | None = None
|
|
|
|
for option in options:
|
|
tool = shutil.which(option)
|
|
if not tool:
|
|
continue
|
|
|
|
if option == "npm" and not _validate_npm_runtime(tool):
|
|
try:
|
|
node_path = shutil.which("node", path=str(Path(tool).parent))
|
|
if node_path:
|
|
_check_node_version(node_path)
|
|
except RuntimeError as e:
|
|
node_version_error = e
|
|
continue
|
|
|
|
return tool, option
|
|
|
|
if node_version_error:
|
|
raise node_version_error
|
|
msg = "Node.js (v20+), Deno or Bun is required but none was found"
|
|
raise RuntimeError(msg)
|
|
|
|
|
|
def find_js_runtime() -> tuple[str, str]:
|
|
"""Find a JavaScript runtime from JS_RUNTIME env or auto-detect.
|
|
|
|
Returns (tool_path, tool_name) where tool_name is "deno", "npm", or "bun".
|
|
Raises RuntimeError if no suitable runtime is found.
|
|
"""
|
|
options = ["npm", "deno", "bun"]
|
|
|
|
# Check for JS_RUNTIME environment variable
|
|
if result := _find_runtime_from_env(options):
|
|
return result
|
|
|
|
# Auto-detect
|
|
return _auto_detect_runtime(options)
|
|
|
|
|
|
def find_build_tool() -> tuple[list[str], list[str]]:
|
|
"""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"),
|
|
}
|
|
# Run vite directly for deno to avoid npm-run-all2/run-p issues
|
|
build = {
|
|
"deno": ("run", "-A", "npm:vite", "build"),
|
|
"npm": ("run", "build"),
|
|
"bun": ("--bun", "run", "build"),
|
|
}
|
|
|
|
tool, name = find_js_runtime()
|
|
return [tool, *install[name]], [tool, *build[name]]
|
|
|
|
|
|
def find_dev_tool() -> list[str]:
|
|
"""Find JavaScript runtime and construct dev command.
|
|
|
|
Returns dev_cmd (without vite-specific args).
|
|
Raises RuntimeError if no runtime is found.
|
|
"""
|
|
dev_args = {
|
|
"deno": ("run", "-A", "npm:vite"),
|
|
"npm": ("--silent", "run", "dev", "--"),
|
|
"bun": ("run", "dev", "--"),
|
|
}
|
|
|
|
tool, name = find_js_runtime()
|
|
|
|
if name == "bun":
|
|
logger.warning(
|
|
"Bun has a WS proxy bug (github.com/oven-sh/bun/issues/9882). Consider npm.",
|
|
)
|
|
|
|
return [tool, *dev_args[name]]
|
|
|
|
|
|
def find_install_tool() -> list[str]:
|
|
"""Find JavaScript runtime and construct install command.
|
|
|
|
Returns install_cmd.
|
|
Raises RuntimeError if no runtime is found.
|
|
"""
|
|
install_args = {
|
|
"deno": ("install", "--quiet", "--allow-scripts=npm:vue-demi"),
|
|
"npm": ("install", "--silent"),
|
|
"bun": ("install", "--silent"),
|
|
}
|
|
|
|
tool, name = find_js_runtime()
|
|
return [tool, *install_args[name]]
|
|
|
|
|
|
def build(folder: str = "frontend") -> None:
|
|
"""Build the frontend in the specified folder.
|
|
|
|
Raises SystemExit(1) on failure.
|
|
"""
|
|
logger.info(">>> Building %s", folder)
|
|
|
|
try:
|
|
install_cmd, build_cmd = find_build_tool()
|
|
except RuntimeError as e:
|
|
logger.warning(e)
|
|
raise SystemExit(1) from None
|
|
|
|
def run(cmd: list[str]) -> None:
|
|
display_cmd = [Path(cmd[0]).stem, *cmd[1:]]
|
|
logger.info("### %s", " ".join(display_cmd))
|
|
subprocess.run(cmd, check=True, cwd=folder)
|
|
|
|
try:
|
|
run(install_cmd)
|
|
logger.info("")
|
|
run(build_cmd)
|
|
except subprocess.CalledProcessError:
|
|
raise SystemExit(1) from None
|