From 76f7e4c31ca99f400f9c1d7d0c4df6540c742047 Mon Sep 17 00:00:00 2001
From: Elijah <elijahahianyo@gmail.com>
Date: Tue, 21 Jan 2025 13:46:45 +0000
Subject: [PATCH] `reflex rename`- B

---
 reflex/reflex.py              |  14 ++++
 reflex/utils/prerequisites.py | 132 ++++++++++++++++++++++++++++++++++
 2 files changed, 146 insertions(+)

diff --git a/reflex/reflex.py b/reflex/reflex.py
index 2d6ebc30c..d3cd4da42 100644
--- a/reflex/reflex.py
+++ b/reflex/reflex.py
@@ -555,6 +555,20 @@ def deploy(
         **extra,
     )
 
+@cli.command()
+def rename(
+    new_name: str = typer.Argument(..., help="The new name for the app."),
+    loglevel: constants.LogLevel = typer.Option(
+        config.loglevel, help="The log level to use."
+    ),
+):
+    """Rename the app in the current directory."""
+    from reflex.utils import prerequisites
+
+    console.set_log_level(loglevel)
+    prerequisites.validate_app_name(new_name)
+    prerequisites.rename_app(new_name)
+
 
 cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
 cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py
index 4f9cc0c14..26cce12e4 100644
--- a/reflex/utils/prerequisites.py
+++ b/reflex/utils/prerequisites.py
@@ -2,11 +2,13 @@
 
 from __future__ import annotations
 
+import ast
 import contextlib
 import dataclasses
 import functools
 import importlib
 import importlib.metadata
+import importlib.util
 import json
 import os
 import platform
@@ -24,6 +26,7 @@ from pathlib import Path
 from types import ModuleType
 from typing import Callable, List, NamedTuple, Optional
 
+import astor
 import httpx
 import typer
 from alembic.util.exc import CommandError
@@ -477,6 +480,135 @@ def validate_app_name(app_name: str | None = None) -> str:
     return app_name
 
 
+def rename_path_up_tree(full_path, old_name, new_name):
+    """
+    Rename all instances of `old_name` in the path (file and directories) to `new_name`.
+    The renaming stops when we reach the directory containing `rxconfig.py`.
+
+    Args:
+        full_path: The full path to start renaming from.
+        old_name: The name to be replaced.
+        new_name: The replacement name.
+
+    Returns:
+        Path: The updated path after renaming.
+    """
+    current_path = Path(full_path)
+    new_path = None
+
+    while True:
+        # Split the current path into its directory and base name
+        directory, base = current_path.parent, current_path.name
+        # Stop renaming when we reach the root dir (which contains rxconfig.py)
+        if current_path.is_dir() and (current_path / "rxconfig.py").exists():
+            new_path = current_path
+            break
+
+        if old_name in base:
+            new_base = base.replace(old_name, new_name)
+            new_path = directory / new_base
+            current_path.rename(new_path)
+            current_path = new_path
+        else:
+            new_path = current_path
+
+        # Move up the directory tree
+        current_path = directory
+
+    return new_path
+
+
+def rename_app(app_name: str):
+    """Rename the app directory."""
+    if not constants.Config.FILE.exists():
+        console.error("No rxconfig.py found. Make sure you are in the root directory of your app.")
+        raise typer.Exit(1)
+
+    config = get_config()
+    module_path = importlib.util.find_spec(config.module)
+    if module_path is None:
+        console.error(f"Could not find module {config.module}.")
+        raise typer.Exit(1)
+
+    if not module_path.origin:
+        console.error(f"Could not find origin for module {config.module}.")
+        raise typer.Exit(1)
+
+    process_directory(
+        Path.cwd(), config.app_name, app_name, exclude_dirs=[".web"]
+    )
+
+    rename_path_up_tree(Path(module_path.origin), config.app_name, app_name)
+
+
+def rename_imports_and_app_name(file_path, old_name, new_name):
+    """
+    Rename imports and update the app_name in the file using string replacement.
+    Handles both keyword and positional arguments for `rx.Config` and import statements.
+    """
+    file_path = Path(file_path)
+    content = file_path.read_text()
+
+    # Replace `from old_name.` or `from old_name` with `from new_name`
+    content = re.sub(
+        rf'\bfrom {re.escape(old_name)}(\b|\.|\s)',
+        lambda match: f'from {new_name}{match.group(1)}',
+        content,
+    )
+
+    # Replace `import old_name` with `import new_name`
+    content = re.sub(
+        rf'\bimport {re.escape(old_name)}\b',
+        f'import {new_name}',
+        content,
+    )
+
+    # Replace `app_name="old_name"` in rx.Config
+    content = re.sub(
+        rf'\bapp_name\s*=\s*["\']{re.escape(old_name)}["\']',
+        f'app_name="{new_name}"',
+        content,
+    )
+
+    # Replace positional argument `"old_name"` in rx.Config
+    content = re.sub(
+        rf'\brx\.Config\(\s*["\']{re.escape(old_name)}["\']',
+        f'rx.Config("{new_name}"',
+        content,
+    )
+
+    file_path.write_text(content)
+
+
+
+def process_directory(directory, old_name, new_name, exclude_dirs=None, extensions=None):
+    """
+    Process files with specified extensions in a directory, excluding specified directories.
+
+    Args:
+        directory (str or Path): The root directory to process.
+        old_name (str): The old name to replace.
+        new_name (str): The new name to use.
+        exclude_dirs (list, optional): List of directory names to exclude. Defaults to None.
+        extensions (list, optional): List of file extensions to process. Defaults to [".py"].
+    """
+    exclude_dirs = exclude_dirs or []
+    extensions = extensions or [".py", ".md"]
+    extensions_set = {ext.lstrip(".") for ext in extensions}
+    directory = Path(directory)
+
+    files = (
+        p.resolve()
+        for p in directory.glob("**/*")
+        if p.is_file() and p.suffix.lstrip(".") in extensions_set
+    )
+
+    for file_path in files:
+        if not any(part in exclude_dirs for part in file_path.parts):
+            rename_imports_and_app_name(file_path, old_name, new_name)
+
+
+
 def create_config(app_name: str):
     """Create a new rxconfig file.