diff --git a/fastapi_vue_setup.py b/fastapi_vue_setup.py index 45c0d87..419615b 100644 --- a/fastapi_vue_setup.py +++ b/fastapi_vue_setup.py @@ -8,11 +8,12 @@ Usage: Options: --module-name NAME Python module name (auto-detected from pyproject.toml) --ports DEFAULT,VITE,DEV Port configuration (default: 3100,3100,3200) - --dry-run Show what would be done without making changes + --dry Show what would be done without making changes """ import argparse import ast +import importlib.metadata import os import re import shutil @@ -23,6 +24,8 @@ from textwrap import indent import tomlkit +version = importlib.metadata.version("fastapi-vue-setup") + # Track Python files written/patched for ruff formatting _python_files_to_format: list[Path] = [] @@ -30,14 +33,14 @@ _python_files_to_format: list[Path] = [] TEMPLATE_DIR = Path(__file__).parent / "template" -def ruff_sort_imports(files: list[Path], dry_run: bool = False) -> None: +def ruff_sort_imports(files: list[Path], dry: bool = False) -> None: """Run ruff to sort imports in the given Python files.""" if not files: return py_files = [str(f) for f in files if f.suffix == ".py" and f.exists()] if not py_files: return - if dry_run: + if dry: print(f"šŸ”§ Would run ruff import sorting on {len(py_files)} files") return print("šŸ”§ Ruff isort on modified files") @@ -482,7 +485,7 @@ def render_template(template: str, **kwargs) -> str: def patch_app_file( - path: Path, module_name: str, app_var: str, dry_run: bool = False + path: Path, module_name: str, app_var: str, dry: bool = False ) -> bool: """Patch an existing app.py with frontend integration. @@ -640,7 +643,7 @@ def patch_app_file( print(f"āš ļø Skipping {path} (no changes needed)") return False - if dry_run: + if dry: print(f"āœ… Would patch {path}") return True @@ -669,7 +672,7 @@ def patch_app_file( def patch_vite_config( path: Path, module_name: str, - dry_run: bool = False, + dry: bool = False, ) -> bool: """Patch an existing vite.config.js/ts by adding fastapi-vue plugin. @@ -731,7 +734,7 @@ def patch_vite_config( print(f"ā„¹ļø Skipping {path} (no changes needed)") return False - if dry_run: + if dry: print(f"āœ… Would patch {path}") return True @@ -740,7 +743,7 @@ def patch_vite_config( return True -def patch_frontend_health_check(frontend_dir: Path, dry_run: bool = False) -> bool: +def patch_frontend_health_check(frontend_dir: Path, dry: bool = False) -> bool: """Patch Vue app to include FastAPI backend health check. Tries HelloWorld.vue first (full demo), then falls back to App.vue (minimal). @@ -831,7 +834,7 @@ def patch_frontend_health_check(frontend_dir: Path, dry_run: bool = False) -> bo print(f"ā„¹ļø Skipping {target_file} (no changes needed)") return False - if dry_run: + if dry: print(f"āœ… Would patch {target_file}") return True @@ -848,7 +851,7 @@ _OLD_VITE_PLUGIN_SHA256 = ( def _upgrade_old_vite_plugin( - path: Path, module_name: str, dry_run: bool = False + path: Path, module_name: str, dry: bool = False ) -> None: """Remove old vite-plugin-fastapi.js that lacks auto-upgrade marker. @@ -869,7 +872,7 @@ def _upgrade_old_vite_plugin( digest = hashlib.sha256(normalized.encode()).hexdigest() if digest != _OLD_VITE_PLUGIN_SHA256: return # Modified by user, don't touch - if dry_run: + if dry: print(f"šŸ”„ Would upgrade old {path}") return path.unlink() @@ -884,7 +887,7 @@ def write_file( path: Path, content: str, overwrite: bool = True, - dry_run: bool = False, + dry: bool = False, executable: bool = False, fallback_path: Path | None = None, force: bool = False, @@ -913,12 +916,12 @@ def write_file( if fallback_path is not None: # Write to fallback path instead return _write_fallback_file( - path, fallback_path, content, dry_run, executable + path, fallback_path, content, dry, executable ) print(f"ā„¹ļø Skipping {path} (customized by user)") return False - if dry_run: + if dry: action = "overwrite" if exists else "create" print(f"āœ… Would {action} {path}") return True @@ -938,7 +941,7 @@ def _write_fallback_file( original_path: Path, fallback_path: Path, content: str, - dry_run: bool, + dry: bool, executable: bool, ) -> bool: """Write content to a fallback .new.py file when original can't be overwritten.""" @@ -948,7 +951,7 @@ def _write_fallback_file( print(f"āœ”ļø {fallback_path} (already up to date)") return False - if dry_run: + if dry: print(f"āœ… Would create {fallback_path} (original customized by user)") _new_files_written.append((fallback_path, original_path)) return True @@ -1085,13 +1088,13 @@ def find_js_runtime() -> tuple[str, str] | None: return None -def ensure_python_project(project_dir: Path, dry_run: bool = False) -> bool: +def ensure_python_project(project_dir: Path, dry: bool = False) -> bool: """Ensure pyproject.toml exists, run uv init if needed.""" pyproject = project_dir / "pyproject.toml" if pyproject.exists(): return True - if dry_run: + if dry: print(f"šŸ“¦ Would run: uv init {project_dir}") return True @@ -1111,7 +1114,7 @@ def ensure_python_project(project_dir: Path, dry_run: bool = False) -> bool: return True -def ensure_frontend(project_dir: Path, dry_run: bool = False) -> bool: +def ensure_frontend(project_dir: Path, dry: bool = False) -> bool: """Ensure frontend directory exists with a Vue project, run create-vue if needed.""" frontend_dir = project_dir / "frontend" package_json = frontend_dir / "package.json" @@ -1135,7 +1138,7 @@ def ensure_frontend(project_dir: Path, dry_run: bool = False) -> bool: } create_cmd = create_vue_commands[js_name] - if dry_run: + if dry: print(f"šŸŽØ Would run: {' '.join(create_cmd)}") return True @@ -1175,11 +1178,11 @@ def cmd_setup(args: argparse.Namespace) -> int: project_dir = Path.cwd() / project_path project_dir = project_dir.resolve() - dry_run = args.dry_run + dry = args.dry # Create project directory if it doesn't exist if not project_dir.exists(): - if dry_run: + if dry: print(f"āœ… Would create directory: {project_dir}") else: project_dir.mkdir(parents=True) @@ -1187,15 +1190,15 @@ def cmd_setup(args: argparse.Namespace) -> int: print(f"šŸ”§ Setting up project: {project_dir}") - if dry_run: + if dry: print("\nšŸƒ DRY RUN MODE - no changes will be made\n") # Step 1: Ensure frontend exists (do this first so cancellation doesn't leave partial setup) - if not ensure_frontend(project_dir, dry_run): + if not ensure_frontend(project_dir, dry): return 1 # Step 2: Ensure Python project exists - if not ensure_python_project(project_dir, dry_run): + if not ensure_python_project(project_dir, dry): return 1 # Detect module name @@ -1261,7 +1264,7 @@ def cmd_setup(args: argparse.Namespace) -> int: tpl_vars["APP_MODULE"] = "app" # Create directories - if not dry_run: + if not dry: if not module_dir.exists(): module_dir.mkdir(parents=True) fastapi_vue_scripts.mkdir(parents=True, exist_ok=True) @@ -1273,7 +1276,7 @@ def cmd_setup(args: argparse.Namespace) -> int: # Remove obsolete util.py if present obsolete_util = fastapi_vue_scripts / "util.py" if obsolete_util.exists(): - if dry_run: + if dry: print(f"šŸ—‘ļø Would remove obsolete {obsolete_util}") else: obsolete_util.unlink() @@ -1290,7 +1293,7 @@ def cmd_setup(args: argparse.Namespace) -> int: dest_path, content, overwrite=True, - dry_run=dry_run, + dry=dry, force=True, # Internal files, always overwrite ) @@ -1303,7 +1306,7 @@ def cmd_setup(args: argparse.Namespace) -> int: devserver_path, content, overwrite=True, - dry_run=dry_run, + dry=dry, executable=True, fallback_path=devserver_fallback, ) @@ -1311,7 +1314,7 @@ def cmd_setup(args: argparse.Namespace) -> int: # === Handle app module === if app_file: # Existing app: patch with import, route, and try to patch lifespan - patch_app_file(app_file, module_name, app_var, dry_run=dry_run) + patch_app_file(app_file, module_name, app_var, dry=dry) else: # No app: create full app.py # Create __init__.py if missing @@ -1319,13 +1322,13 @@ def cmd_setup(args: argparse.Namespace) -> int: if not init_file.exists(): template = load_template("backend/__init__.py") content = render_template(template, **tpl_vars) - write_file(init_file, content, overwrite=False, dry_run=dry_run) + write_file(init_file, content, overwrite=False, dry=dry) # Create app.py app_file_path = module_dir / "app.py" template = load_template("backend/app.py") content = render_template(template, **tpl_vars) - write_file(app_file_path, content, overwrite=False, dry_run=dry_run) + write_file(app_file_path, content, overwrite=False, dry=dry) # === Handle __main__.py === main_file = module_dir / "__main__.py" @@ -1342,7 +1345,7 @@ def cmd_setup(args: argparse.Namespace) -> int: main_file, main_content, overwrite=True, - dry_run=dry_run, + dry=dry, fallback_path=main_fallback, ) elif not existing_cli: @@ -1351,7 +1354,7 @@ def cmd_setup(args: argparse.Namespace) -> int: main_file, main_content, overwrite=False, - dry_run=dry_run, + dry=dry, ) else: # No file but has existing entrypoint - don't create (user has custom CLI setup) @@ -1363,25 +1366,25 @@ def cmd_setup(args: argparse.Namespace) -> int: # Install the vite plugin file (always update) plugin_file = frontend_dir / "vite-plugin-fastapi.js" # Upgrade old plugin versions that lack the auto-upgrade marker - _upgrade_old_vite_plugin(plugin_file, module_name, dry_run) + _upgrade_old_vite_plugin(plugin_file, module_name, dry) template = load_template("frontend/vite-plugin-fastapi.js") content = render_template(template, **tpl_vars) - write_file(plugin_file, content, overwrite=True, dry_run=dry_run) + write_file(plugin_file, content, overwrite=True, dry=dry) # Find existing vite config (prefer .ts, fall back to .js) vite_config_ts = frontend_dir / "vite.config.ts" vite_config_js = frontend_dir / "vite.config.js" if vite_config_ts.exists(): - patch_vite_config(vite_config_ts, module_name, dry_run) + patch_vite_config(vite_config_ts, module_name, dry) elif vite_config_js.exists(): - patch_vite_config(vite_config_js, module_name, dry_run) + patch_vite_config(vite_config_js, module_name, dry) 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.") # Patch Vue app with backend health check - patch_frontend_health_check(frontend_dir, dry_run) + patch_frontend_health_check(frontend_dir, dry) # === Update pyproject.toml === pyproject_path = project_dir / "pyproject.toml" @@ -1402,7 +1405,7 @@ def cmd_setup(args: argparse.Namespace) -> int: if new_content == old_content: print(f"āœ”ļø {pyproject_path} (already up to date)") - elif dry_run: + elif dry: print(f"āœ… Would update {pyproject_path}") else: pyproject_path.write_text(new_content, "UTF-8", newline="\n") @@ -1415,7 +1418,7 @@ def cmd_setup(args: argparse.Namespace) -> int: gitignore_content = gitignore_path.read_bytes() if b"frontend-build" in gitignore_content: print("āœ”ļø .gitignore (frontend-build already ignored)") - elif dry_run: + elif dry: print(f"āœ… Would add {gitignore_entry} to .gitignore") else: nl = b"\r\n" if b"\r\n" in gitignore_content else b"\n" @@ -1424,15 +1427,15 @@ def cmd_setup(args: argparse.Namespace) -> int: gitignore_content + suffix + gitignore_entry.encode() + nl ) print(f"āœ… Added {gitignore_entry} to .gitignore") - elif dry_run: + elif dry: print(f"āœ… Would create .gitignore with {gitignore_entry}") else: gitignore_path.write_text(f"{gitignore_entry}\n", "UTF-8", newline="\n") print("āœ… Created .gitignore") # === Add dependencies using uv === - ruff_sort_imports(_python_files_to_format, dry_run=dry_run) - if dry_run: + ruff_sort_imports(_python_files_to_format, dry=dry) + if dry: print("šŸ“¦ Would add: fastapi[standard], fastapi-vue, httpx (dev only)") else: print("šŸ“¦ Dependencies") @@ -1496,14 +1499,14 @@ def is_already_patched(path: Path) -> bool: def main() -> int: parser = argparse.ArgumentParser( - description="Set up FastAPI+Vue projects with integrated build/dev systems", + description=f"fastapi-vue-setup {version} - FastAPI + Vue project setup tool", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 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 . --ports=8000,5173,8080 Custom ports (default,vite,dev) + fastapi-vue-setup . --dry Preview what would be done + fastapi-vue-setup . --ports=8000,5173,8080 Change default ports (backend, vite dev, backend dev) """, ) parser.add_argument( @@ -1512,15 +1515,14 @@ Examples: default=None, help="Project directory (use . for current directory)", ) + parser.add_argument("--version", action="version", version=version) parser.add_argument("--module-name", help="Python module name (auto-detected)") parser.add_argument( "--ports", - metavar="DEFAULT,VITE,DEV", + metavar="BACKEND,VITE,DEV", help="Port configuration as comma-separated values (default: 3100,3100,3200)", ) - parser.add_argument( - "--dry-run", action="store_true", help="Show what would be done" - ) + parser.add_argument("--dry", action="store_true", help="Show what would be done") args = parser.parse_args()