Selaa lähdekoodia

Create ~/.reflex directory and update integration tests (#1419)

Nikhil Rao 1 vuosi sitten
vanhempi
säilyke
e26bba80a6

+ 7 - 5
.github/workflows/integration_examples.yml

@@ -1,16 +1,14 @@
-name: integration-test
+name: integration-test-examples
 
 
 on:
 on:
-  pull_request_review:
-    types: [submitted]
-
+  pull_request:
+    branches: [main]
 
 
 permissions:
 permissions:
   contents: read
   contents: read
 
 
 jobs:
 jobs:
   build:
   build:
-    if: github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'main'
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     # Specify python/node versions to test against
     # Specify python/node versions to test against
@@ -65,6 +63,10 @@ jobs:
     - name: Init Website
     - name: Init Website
       working-directory: ./pynecone-examples/counter
       working-directory: ./pynecone-examples/counter
       run: poetry run reflex init
       run: poetry run reflex init
+    - name: Validate Reflex's own installation of Node
+      run: |
+        /home/runner/.reflex/.nvm/versions/node/v*/bin/npm -v
+        /home/runner/.reflex/.nvm/versions/node/v*/bin/node -v
     - name: Check for errors
     - name: Check for errors
       run: |
       run: |
         chmod +x ./scripts/integration.sh
         chmod +x ./scripts/integration.sh

+ 3 - 5
.github/workflows/integration_website.yml

@@ -1,16 +1,14 @@
-name: integration-test
+name: integration-test-website
 
 
 on:
 on:
-  pull_request_review:
-    types: [submitted]
-
+  pull_request:
+    branches: [ main ]
 
 
 permissions:
 permissions:
   contents: read
   contents: read
 
 
 jobs:
 jobs:
   build:
   build:
-    if: github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'main'
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     # Specify python/node versions to test against
     # Specify python/node versions to test against

+ 20 - 0
.github/workflows/reflex_init_test.yml

@@ -0,0 +1,20 @@
+name: reflex-init-test
+
+on:
+  pull_request:
+    branches:
+      - main
+
+jobs:
+  reflex-install-and-init:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+
+    - shell: bash
+      run: |
+        # Run reflex init in a docker container
+ 
+        # cwd is repo root
+        docker build -f integration/init-test/Dockerfile -t reflex-init-test integration/init-test
+        docker run --rm -v $(pwd):/reflex-repo/ reflex-init-test /reflex-repo/integration/init-test/in_docker_test_script.sh

+ 21 - 0
integration/init-test/Dockerfile

@@ -0,0 +1,21 @@
+FROM ubuntu:latest
+
+ARG USERNAME=kerrigan
+ARG USER_UID=1000
+ARG USER_GID=$USER_UID
+
+RUN groupadd --gid $USER_GID $USERNAME \
+    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
+    #
+    # [Optional] Add sudo support. Omit if you don't need to install software after connecting.
+    && apt-get update \
+    && apt-get install -y sudo curl xz-utils python3 python3-pip python3.10-venv unzip \
+    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
+    && chmod 0440 /etc/sudoers.d/$USERNAME
+
+USER $USERNAME
+
+RUN curl -sSL https://install.python-poetry.org | python3 -
+RUN sudo ln -s /home/$USERNAME/.local/bin/poetry /usr/local/bin/poetry
+
+WORKDIR /home/$USERNAME

+ 14 - 0
integration/init-test/in_docker_test_script.sh

@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+set -ex
+
+echo "Preparing test project dir"
+mkdir hello
+python3 -m venv ~/hello/venv
+source ~/hello/venv/bin/activate
+
+echo "Installing reflex from local repo code"
+cd /reflex-repo
+poetry install
+echo "Running reflex init in test project dir"
+poetry run /bin/bash -c "cd ~/hello && reflex init"

+ 0 - 6
reflex/config.py

@@ -165,12 +165,6 @@ class Config(Base):
     # The environment mode.
     # The environment mode.
     env: constants.Env = constants.Env.DEV
     env: constants.Env = constants.Env.DEV
 
 
-    # The path to the bun executable.
-    bun_path: str = constants.BUN_PATH
-
-    # Disable bun.
-    disable_bun: bool = False
-
     # Additional frontend packages to install.
     # Additional frontend packages to install.
     frontend_packages: List[str] = []
     frontend_packages: List[str] = []
 
 

+ 34 - 22
reflex/constants.py

@@ -1,6 +1,7 @@
 """Constants used throughout the package."""
 """Constants used throughout the package."""
 
 
 import os
 import os
+import platform
 import re
 import re
 from enum import Enum
 from enum import Enum
 from types import SimpleNamespace
 from types import SimpleNamespace
@@ -43,12 +44,41 @@ def get_value(key: str, default: Any = None, type_: Type = str) -> Type:
 MODULE_NAME = "reflex"
 MODULE_NAME = "reflex"
 # The current version of Reflex.
 # The current version of Reflex.
 VERSION = metadata.version(MODULE_NAME)
 VERSION = metadata.version(MODULE_NAME)
-# Minimum version of Node.js required to run Reflex.
+
+# Project dependencies.
+# The directory to store reflex dependencies.
+REFLEX_DIR = os.path.expandvars("$HOME/.reflex")
+
+# Bun config.
+# The Bun version.
+BUN_VERSION = "0.7.0"
+# The directory to store the bun.
+BUN_ROOT_PATH = f"{REFLEX_DIR}/.bun"
+# The bun path.
+BUN_PATH = f"{BUN_ROOT_PATH}/bin/bun"
+# Command to install bun.
+INSTALL_BUN = f"curl -fsSL https://bun.sh/install | env BUN_INSTALL={BUN_ROOT_PATH} bash -s -- bun-v{BUN_VERSION}"
+
+# NVM / Node config.
+# The Node version.
+NODE_VERSION = "18.17.0"
+# The minimum required node version.
 MIN_NODE_VERSION = "16.8.0"
 MIN_NODE_VERSION = "16.8.0"
+# The directory to store nvm.
+NVM_ROOT_PATH = f"{REFLEX_DIR}/.nvm"
+# The nvm path.
+NVM_PATH = f"{NVM_ROOT_PATH}/nvm.sh"
+# The node bin path.
+NODE_BIN_PATH = f"{NVM_ROOT_PATH}/versions/node/v{NODE_VERSION}/bin"
+# The default path where node is installed.
+NODE_PATH = "node" if platform.system() == "Windows" else f"{NODE_BIN_PATH}/node"
+# The default path where npm is installed.
+NPM_PATH = "npm" if platform.system() == "Windows" else f"{NODE_BIN_PATH}/npm"
+# Command to install nvm.
+INSTALL_NVM = f"curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | env NVM_DIR={NVM_ROOT_PATH} bash"
+# Command to install node.
+INSTALL_NODE = f'bash -c "export NVM_DIR={NVM_ROOT_PATH} && . {NVM_ROOT_PATH}/nvm.sh && nvm install {NODE_VERSION}"'
 
 
-# Valid bun versions.
-MIN_BUN_VERSION = "0.5.9"
-MAX_BUN_VERSION = "0.6.9"
 
 
 # Files and directories used to init a new project.
 # Files and directories used to init a new project.
 # The root directory of the reflex library.
 # The root directory of the reflex library.
@@ -110,24 +140,6 @@ BACKEND_PORT = get_value("BACKEND_PORT", "8000")
 API_URL = get_value("API_URL", "http://localhost:8000")
 API_URL = get_value("API_URL", "http://localhost:8000")
 # The deploy url
 # The deploy url
 DEPLOY_URL = get_value("DEPLOY_URL")
 DEPLOY_URL = get_value("DEPLOY_URL")
-# bun root location
-BUN_ROOT_PATH = "$HOME/.bun"
-# The default path where bun is installed.
-BUN_PATH = get_value("BUN_PATH", f"{BUN_ROOT_PATH}/bin/bun")
-# Command to install bun.
-INSTALL_BUN = f"curl -fsSL https://bun.sh/install | bash -s -- bun-v{MAX_BUN_VERSION}"
-# Command to install nvm.
-INSTALL_NVM = (
-    "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash"
-)
-# nvm root location.
-NVM_ROOT_PATH = f"$HOME/.nvm"
-# The default path where node is installed.
-NODE_PATH = get_value(
-    "NODE_PATH", f"{NVM_ROOT_PATH}/versions/node/v{MIN_NODE_VERSION}/bin/node"
-)
-# Command to install node.
-INSTALL_NODE = f". {NVM_ROOT_PATH}/nvm.sh && nvm install {MIN_NODE_VERSION}"
 # Default host in dev mode.
 # Default host in dev mode.
 BACKEND_HOST = get_value("BACKEND_HOST", "0.0.0.0")
 BACKEND_HOST = get_value("BACKEND_HOST", "0.0.0.0")
 # The default timeout when launching the gunicorn server.
 # The default timeout when launching the gunicorn server.

+ 6 - 14
reflex/reflex.py

@@ -32,20 +32,12 @@ def init(
     ),
     ),
 ):
 ):
     """Initialize a new Reflex app in the current directory."""
     """Initialize a new Reflex app in the current directory."""
+    # Get the app name.
     app_name = prerequisites.get_default_app_name() if name is None else name
     app_name = prerequisites.get_default_app_name() if name is None else name
-
-    # Make sure they don't name the app "reflex".
-    if app_name == constants.MODULE_NAME:
-        console.print(
-            f"[red]The app directory cannot be named [bold]{constants.MODULE_NAME}."
-        )
-        raise typer.Exit()
-
     console.rule(f"[bold]Initializing {app_name}")
     console.rule(f"[bold]Initializing {app_name}")
-    # Set up the web directory.
-    prerequisites.validate_and_install_bun()
-    prerequisites.validate_and_install_node()
-    prerequisites.initialize_web_directory()
+
+    # Set up the web project.
+    prerequisites.initialize_frontend_dependencies()
 
 
     # Migrate Pynecone projects to Reflex.
     # Migrate Pynecone projects to Reflex.
     prerequisites.migrate_to_reflex()
     prerequisites.migrate_to_reflex()
@@ -57,11 +49,11 @@ def init(
         build.set_reflex_project_hash()
         build.set_reflex_project_hash()
         telemetry.send("init", get_config().telemetry_enabled)
         telemetry.send("init", get_config().telemetry_enabled)
     else:
     else:
-        build.set_reflex_project_hash()
         telemetry.send("reinit", get_config().telemetry_enabled)
         telemetry.send("reinit", get_config().telemetry_enabled)
 
 
     # Initialize the .gitignore.
     # Initialize the .gitignore.
     prerequisites.initialize_gitignore()
     prerequisites.initialize_gitignore()
+
     # Finish initializing the app.
     # Finish initializing the app.
     console.log(f"[bold green]Finished Initializing: {app_name}")
     console.log(f"[bold green]Finished Initializing: {app_name}")
 
 
@@ -85,7 +77,7 @@ def run(
     """Run the app in the current directory."""
     """Run the app in the current directory."""
     if platform.system() == "Windows":
     if platform.system() == "Windows":
         console.print(
         console.print(
-            "[yellow][WARNING] We strongly advise you to use Windows Subsystem for Linux (WSL) for optimal performance when using Reflex. Due to compatibility issues with one of our dependencies, Bun, you may experience slower performance on Windows. By using WSL, you can expect to see a significant speed increase."
+            "[yellow][WARNING] We strongly advise using Windows Subsystem for Linux (WSL) for optimal performance with reflex."
         )
         )
     # Set ports as os env variables to take precedence over config and
     # Set ports as os env variables to take precedence over config and
     # .env variables(if override_os_envs flag in config is set to False).
     # .env variables(if override_os_envs flag in config is set to False).

+ 1 - 1
reflex/testing.py

@@ -179,7 +179,7 @@ class AppHarness:
         frontend_env = os.environ.copy()
         frontend_env = os.environ.copy()
         frontend_env["PORT"] = "0"
         frontend_env["PORT"] = "0"
         self.frontend_process = subprocess.Popen(
         self.frontend_process = subprocess.Popen(
-            [reflex.utils.prerequisites.get_package_manager(), "run", "dev"],
+            [reflex.utils.prerequisites.get_install_package_manager(), "run", "dev"],
             stdout=subprocess.PIPE,
             stdout=subprocess.PIPE,
             encoding="utf-8",
             encoding="utf-8",
             cwd=self.app_path / reflex.constants.WEB_DIR,
             cwd=self.app_path / reflex.constants.WEB_DIR,

+ 3 - 9
reflex/utils/build.py

@@ -206,21 +206,15 @@ def setup_frontend(
     loglevel: constants.LogLevel = constants.LogLevel.ERROR,
     loglevel: constants.LogLevel = constants.LogLevel.ERROR,
     disable_telemetry: bool = True,
     disable_telemetry: bool = True,
 ):
 ):
-    """Set up the frontend.
+    """Set up the frontend to run the app.
 
 
     Args:
     Args:
         root: The root path of the project.
         root: The root path of the project.
         loglevel: The log level to use.
         loglevel: The log level to use.
         disable_telemetry: Whether to disable the Next telemetry.
         disable_telemetry: Whether to disable the Next telemetry.
     """
     """
-    # Validate bun version.
-    prerequisites.validate_and_install_bun(initialize=False)
-
-    # Initialize the web directory if it doesn't exist.
-    web_dir = prerequisites.create_web_directory(root)
-
     # Install frontend packages.
     # Install frontend packages.
-    prerequisites.install_frontend_packages(web_dir)
+    prerequisites.install_frontend_packages()
 
 
     # Copy asset files to public folder.
     # Copy asset files to public folder.
     path_ops.cp(
     path_ops.cp(
@@ -228,7 +222,7 @@ def setup_frontend(
         dest=str(root / constants.WEB_ASSETS_DIR),
         dest=str(root / constants.WEB_ASSETS_DIR),
     )
     )
 
 
-    # set the environment variables in client(env.json)
+    # Set the environment variables in client (env.json).
     set_environment_variables()
     set_environment_variables()
 
 
     # Disable the Next telemetry.
     # Disable the Next telemetry.

+ 2 - 9
reflex/utils/exec.py

@@ -90,9 +90,7 @@ def run_frontend_prod(
 
 
     # Run the frontend in production mode.
     # Run the frontend in production mode.
     console.rule("[bold green]App Running")
     console.rule("[bold green]App Running")
-    run_process_and_launch_url(
-        [prerequisites.get_package_manager(), "run", "prod"], loglevel
-    )
+    run_process_and_launch_url([constants.NPM_PATH, "run", "prod"], loglevel)
 
 
 
 
 def run_backend(
 def run_backend(
@@ -122,12 +120,7 @@ def run_backend(
         "--reload-dir",
         "--reload-dir",
         app_name.split(".")[0],
         app_name.split(".")[0],
     ]
     ]
-    process = subprocess.Popen(cmd)
-
-    try:
-        process.wait()
-    except KeyboardInterrupt:
-        process.terminate()
+    subprocess.run(cmd)
 
 
 
 
 def run_backend_prod(
 def run_backend_prod(

+ 115 - 105
reflex/utils/prerequisites.py

@@ -24,25 +24,30 @@ from reflex import constants, model
 from reflex.config import get_config
 from reflex.config import get_config
 from reflex.utils import console, path_ops
 from reflex.utils import console, path_ops
 
 
+IS_WINDOWS = platform.system() == "Windows"
 
 
-def check_node_version(min_version=constants.MIN_NODE_VERSION):
-    """Check the version of Node.js.
 
 
-    Args:
-        min_version: The minimum version of Node.js required.
+def check_node_version():
+    """Check the version of Node.js.
 
 
     Returns:
     Returns:
-        Whether the version of Node.js is high enough.
+        Whether the version of Node.js is valid.
     """
     """
     try:
     try:
         # Run the node -v command and capture the output
         # Run the node -v command and capture the output
         result = subprocess.run(
         result = subprocess.run(
-            ["node", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
+            [constants.NODE_PATH, "-v"],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
         )
         )
         # The output will be in the form "vX.Y.Z", but version.parse() can handle it
         # The output will be in the form "vX.Y.Z", but version.parse() can handle it
         current_version = version.parse(result.stdout.decode())
         current_version = version.parse(result.stdout.decode())
         # Compare the version numbers
         # Compare the version numbers
-        return current_version >= version.parse(min_version)
+        return (
+            current_version >= version.parse(constants.MIN_NODE_VERSION)
+            if IS_WINDOWS
+            else current_version == version.parse(constants.NODE_VERSION)
+        )
     except Exception:
     except Exception:
         return False
         return False
 
 
@@ -56,7 +61,7 @@ def get_bun_version() -> Optional[version.Version]:
     try:
     try:
         # Run the bun -v command and capture the output
         # Run the bun -v command and capture the output
         result = subprocess.run(
         result = subprocess.run(
-            [os.path.expandvars(get_config().bun_path), "-v"],
+            [constants.BUN_PATH, "-v"],
             stdout=subprocess.PIPE,
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
             stderr=subprocess.PIPE,
         )
         )
@@ -65,26 +70,50 @@ def get_bun_version() -> Optional[version.Version]:
         return None
         return None
 
 
 
 
-def get_package_manager() -> str:
-    """Get the package manager executable.
+def get_windows_package_manager() -> str:
+    """Get the package manager for windows.
 
 
     Returns:
     Returns:
-        The path to the package manager.
+        The path to the package manager for windows.
 
 
     Raises:
     Raises:
         FileNotFoundError: If bun or npm is not installed.
         FileNotFoundError: If bun or npm is not installed.
     """
     """
-    config = get_config()
+    npm_path = path_ops.which("npm")
+    if npm_path is None:
+        raise FileNotFoundError("Reflex requires npm to be installed on Windows.")
+    return npm_path
+
+
+def get_install_package_manager() -> str:
+    """Get the package manager executable for installation.
+      currently on unix systems, bun is used for installation only.
+
+    Returns:
+        The path to the package manager.
+    """
+    get_config()
 
 
     # On Windows, we use npm instead of bun.
     # On Windows, we use npm instead of bun.
-    if platform.system() == "Windows" or config.disable_bun:
-        npm_path = path_ops.which("npm")
-        if npm_path is None:
-            raise FileNotFoundError("Reflex requires npm to be installed on Windows.")
-        return npm_path
+    if platform.system() == "Windows":
+        return get_windows_package_manager()
 
 
     # On other platforms, we use bun.
     # On other platforms, we use bun.
-    return os.path.expandvars(get_config().bun_path)
+    return constants.BUN_PATH
+
+
+def get_package_manager() -> str:
+    """Get the package manager executable for running app.
+      currently on unix systems, npm is used for running the app only.
+
+    Returns:
+        The path to the package manager.
+    """
+    get_config()
+
+    if platform.system() == "Windows":
+        return get_windows_package_manager()
+    return constants.NPM_PATH
 
 
 
 
 def get_app() -> ModuleType:
 def get_app() -> ModuleType:
@@ -133,8 +162,20 @@ def get_default_app_name() -> str:
 
 
     Returns:
     Returns:
         The default app name.
         The default app name.
+
+    Raises:
+        Exit: if the app directory name is reflex.
     """
     """
-    return os.getcwd().split(os.path.sep)[-1].replace("-", "_")
+    app_name = os.getcwd().split(os.path.sep)[-1].replace("-", "_")
+
+    # Make sure the app is not named "reflex".
+    if app_name == constants.MODULE_NAME:
+        console.print(
+            f"[red]The app directory cannot be named [bold]{constants.MODULE_NAME}."
+        )
+        raise typer.Exit()
+
+    return app_name
 
 
 
 
 def create_config(app_name: str):
 def create_config(app_name: str):
@@ -151,21 +192,6 @@ def create_config(app_name: str):
         f.write(templates.RXCONFIG.render(app_name=app_name, config_name=config_name))
         f.write(templates.RXCONFIG.render(app_name=app_name, config_name=config_name))
 
 
 
 
-def create_web_directory(root: Path) -> str:
-    """Creates a web directory in the given root directory
-    and returns the path to the directory.
-
-    Args:
-        root (Path): The root directory of the project.
-
-    Returns:
-        The path to the web directory.
-    """
-    web_dir = str(root / constants.WEB_DIR)
-    path_ops.cp(constants.WEB_TEMPLATE_DIR, web_dir, overwrite=False)
-    return web_dir
-
-
 def initialize_gitignore():
 def initialize_gitignore():
     """Initialize the template .gitignore file."""
     """Initialize the template .gitignore file."""
     # The files to add to the .gitignore file.
     # The files to add to the .gitignore file.
@@ -223,89 +249,65 @@ def initialize_web_directory():
         json.dump(reflex_json, f, ensure_ascii=False)
         json.dump(reflex_json, f, ensure_ascii=False)
 
 
 
 
-def validate_and_install_bun(initialize=True):
-    """Check that bun version requirements are met. If they are not,
-    ask user whether to install required version.
-
-    Args:
-        initialize: whether this function is called on `reflex init` or `reflex run`.
-
-    Raises:
-        Exit: If the bun version is not supported.
-
-    """
-    bun_version = get_bun_version()
-    if bun_version is not None and (
-        bun_version < version.parse(constants.MIN_BUN_VERSION)
-        or bun_version > version.parse(constants.MAX_BUN_VERSION)
-    ):
-        console.print(
-            f"""[red]Bun version {bun_version} is not supported by Reflex. Please change your to bun version to be between {constants.MIN_BUN_VERSION} and {constants.MAX_BUN_VERSION}."""
-        )
-        action = console.ask(
-            "Enter 'yes' to install the latest supported bun version or 'no' to exit.",
-            choices=["yes", "no"],
-            default="no",
-        )
-
-        if action == "yes":
-            remove_existing_bun_installation()
-            install_bun()
-            return
-        else:
-            raise typer.Exit()
+def initialize_bun():
+    """Check that bun requirements are met, and install if not."""
+    if IS_WINDOWS:
+        # Bun is not supported on Windows.
+        return
 
 
-    if initialize:
+    # Check the bun version.
+    if get_bun_version() != version.parse(constants.BUN_VERSION):
+        remove_existing_bun_installation()
         install_bun()
         install_bun()
 
 
 
 
 def remove_existing_bun_installation():
 def remove_existing_bun_installation():
     """Remove existing bun installation."""
     """Remove existing bun installation."""
-    package_manager = get_package_manager()
-    if os.path.exists(package_manager):
-        console.log("Removing bun...")
-        path_ops.rm(os.path.expandvars(constants.BUN_ROOT_PATH))
+    if os.path.exists(constants.BUN_PATH):
+        path_ops.rm(constants.BUN_ROOT_PATH)
 
 
 
 
-def validate_and_install_node():
+def initialize_node():
     """Validate nodejs have install or not."""
     """Validate nodejs have install or not."""
     if not check_node_version():
     if not check_node_version():
         install_node()
         install_node()
 
 
 
 
 def install_node():
 def install_node():
-    """Install nvm and nodejs onto the user's system.
-
+    """Install nvm and nodejs for use by Reflex.
+       Independent of any existing system installations.
 
 
     Raises:
     Raises:
         FileNotFoundError: if unzip or curl packages are not found.
         FileNotFoundError: if unzip or curl packages are not found.
         Exit: if installation failed
         Exit: if installation failed
     """
     """
-    if platform.system() != "Windows":
-        # Only install if bun is not already installed.
-        console.log("Installing nvm...")
+    if IS_WINDOWS:
+        console.print(
+            f"[red]Node.js version {constants.NODE_VERSION} or higher is required to run Reflex."
+        )
+        raise typer.Exit()
 
 
-        # Check if curl is installed
-        curl_path = path_ops.which("curl")
-        if curl_path is None:
-            raise FileNotFoundError("Reflex requires curl to be installed.")
+    # Only install if bun is not already installed.
+    console.log("Installing nvm...")
 
 
-        result = subprocess.run(constants.INSTALL_NVM, shell=True)
+    # Check if curl is installed
+    # TODO no need to shell out to curl
+    curl_path = path_ops.which("curl")
+    if curl_path is None:
+        raise FileNotFoundError("Reflex requires curl to be installed.")
 
 
-        if result.returncode != 0:
-            raise typer.Exit(code=result.returncode)
+    # Create the nvm directory and install.
+    path_ops.mkdir(constants.NVM_ROOT_PATH)
+    result = subprocess.run(constants.INSTALL_NVM, shell=True)
 
 
-        console.log("Installing node...")
-        result = subprocess.run(constants.INSTALL_NODE, shell=True)
+    if result.returncode != 0:
+        raise typer.Exit(code=result.returncode)
 
 
-        if result.returncode != 0:
-            raise typer.Exit(code=result.returncode)
+    console.log("Installing node...")
+    result = subprocess.run(constants.INSTALL_NODE, shell=True)
 
 
-    else:
-        console.print(
-            f"[red]Node.js version {constants.MIN_NODE_VERSION} or higher is required to run Reflex."
-        )
-        raise typer.Exit()
+    if result.returncode != 0:
+        raise typer.Exit(code=result.returncode)
 
 
 
 
 def install_bun():
 def install_bun():
@@ -316,12 +318,12 @@ def install_bun():
         Exit: if installation failed
         Exit: if installation failed
     """
     """
     # Bun is not supported on Windows.
     # Bun is not supported on Windows.
-    if platform.system() == "Windows":
+    if IS_WINDOWS:
         console.log("Skipping bun installation on Windows.")
         console.log("Skipping bun installation on Windows.")
         return
         return
 
 
     # Only install if bun is not already installed.
     # Only install if bun is not already installed.
-    if not os.path.exists(get_package_manager()):
+    if not os.path.exists(constants.BUN_PATH):
         console.log("Installing bun...")
         console.log("Installing bun...")
 
 
         # Check if curl is installed
         # Check if curl is installed
@@ -340,20 +342,15 @@ def install_bun():
             raise typer.Exit(code=result.returncode)
             raise typer.Exit(code=result.returncode)
 
 
 
 
-def install_frontend_packages(web_dir: str):
-    """Installs the base and custom frontend packages
-    into the given web directory.
-
-    Args:
-        web_dir: The directory where the frontend code is located.
-    """
+def install_frontend_packages():
+    """Installs the base and custom frontend packages."""
     # Install the frontend packages.
     # Install the frontend packages.
     console.rule("[bold]Installing frontend packages")
     console.rule("[bold]Installing frontend packages")
 
 
     # Install the base packages.
     # Install the base packages.
     subprocess.run(
     subprocess.run(
-        [get_package_manager(), "install"],
-        cwd=web_dir,
+        [get_install_package_manager(), "install"],
+        cwd=constants.WEB_DIR,
         stdout=subprocess.PIPE,
         stdout=subprocess.PIPE,
     )
     )
 
 
@@ -361,8 +358,8 @@ def install_frontend_packages(web_dir: str):
     packages = get_config().frontend_packages
     packages = get_config().frontend_packages
     if len(packages) > 0:
     if len(packages) > 0:
         subprocess.run(
         subprocess.run(
-            [get_package_manager(), "add", *packages],
-            cwd=web_dir,
+            [get_install_package_manager(), "add", *packages],
+            cwd=constants.WEB_DIR,
             stdout=subprocess.PIPE,
             stdout=subprocess.PIPE,
         )
         )
 
 
@@ -389,6 +386,19 @@ def is_latest_template() -> bool:
     return app_version == constants.VERSION
     return app_version == constants.VERSION
 
 
 
 
+def initialize_frontend_dependencies():
+    """Initialize all the frontend dependencies."""
+    # Create the reflex directory.
+    path_ops.mkdir(constants.REFLEX_DIR)
+
+    # Install the frontend dependencies.
+    initialize_bun()
+    initialize_node()
+
+    # Set up the web directory.
+    initialize_web_directory()
+
+
 def check_admin_settings():
 def check_admin_settings():
     """Check if admin settings are set and valid for logging in cli app."""
     """Check if admin settings are set and valid for logging in cli app."""
     admin_dash = get_config().admin_dash
     admin_dash = get_config().admin_dash

+ 3 - 1
reflex/utils/processes.py

@@ -134,8 +134,10 @@ def new_process(args, **kwargs):
     Returns:
     Returns:
         Execute a child program in a new process.
         Execute a child program in a new process.
     """
     """
+    env = os.environ.copy()
+    env["PATH"] = os.pathsep.join([constants.NODE_BIN_PATH, env["PATH"]])
     kwargs = {
     kwargs = {
-        "env": os.environ,
+        "env": env,
         "stderr": subprocess.STDOUT,
         "stderr": subprocess.STDOUT,
         "stdout": subprocess.PIPE,  # Redirect stdout to a pipe
         "stdout": subprocess.PIPE,  # Redirect stdout to a pipe
         "universal_newlines": True,  # Set universal_newlines to True for text mode
         "universal_newlines": True,  # Set universal_newlines to True for text mode

+ 67 - 37
tests/test_utils.py

@@ -1,9 +1,11 @@
 import os
 import os
+import subprocess
 import typing
 import typing
 from pathlib import Path
 from pathlib import Path
 from typing import Any, List, Union
 from typing import Any, List, Union
 
 
 import pytest
 import pytest
+import typer
 from packaging import version
 from packaging import version
 
 
 from reflex import Env, constants
 from reflex import Env, constants
@@ -18,7 +20,7 @@ def get_above_max_version():
         max bun version plus one.
         max bun version plus one.
 
 
     """
     """
-    semantic_version_list = constants.MAX_BUN_VERSION.split(".")
+    semantic_version_list = constants.BUN_VERSION.split(".")
     semantic_version_list[-1] = str(int(semantic_version_list[-1]) + 1)  # type: ignore
     semantic_version_list[-1] = str(int(semantic_version_list[-1]) + 1)  # type: ignore
     return ".".join(semantic_version_list)
     return ".".join(semantic_version_list)
 
 
@@ -261,7 +263,7 @@ def test_format_route(route: str, expected: bool):
         (VMAXPLUS1, False, "yes"),
         (VMAXPLUS1, False, "yes"),
     ],
     ],
 )
 )
-def test_bun_validate_and_install(mocker, bun_version, is_valid, prompt_input):
+def test_initialize_bun(mocker, bun_version, is_valid, prompt_input):
     """Test that the bun version on host system is validated properly. Also test that
     """Test that the bun version on host system is validated properly. Also test that
     the required bun version is installed should the user opt for it.
     the required bun version is installed should the user opt for it.
 
 
@@ -279,48 +281,23 @@ def test_bun_validate_and_install(mocker, bun_version, is_valid, prompt_input):
         "reflex.utils.prerequisites.remove_existing_bun_installation"
         "reflex.utils.prerequisites.remove_existing_bun_installation"
     )
     )
 
 
-    prerequisites.validate_and_install_bun()
+    prerequisites.initialize_bun()
     if not is_valid:
     if not is_valid:
         remove_existing_bun_installation.assert_called_once()
         remove_existing_bun_installation.assert_called_once()
     bun_install.assert_called_once()
     bun_install.assert_called_once()
 
 
 
 
-def test_bun_validation_exception(mocker):
-    """Test that an exception is thrown and program exists when user selects no when asked
-    whether to install bun or not.
-
-    Args:
-        mocker: Pytest mocker.
-    """
-    mocker.patch("reflex.utils.prerequisites.get_bun_version", return_value=V056)
-    mocker.patch("reflex.utils.prerequisites.console.ask", return_value="no")
-
-    with pytest.raises(RuntimeError):
-        prerequisites.validate_and_install_bun()
-
-
-def test_remove_existing_bun_installation(mocker, tmp_path):
+def test_remove_existing_bun_installation(mocker):
     """Test that existing bun installation is removed.
     """Test that existing bun installation is removed.
 
 
     Args:
     Args:
         mocker: Pytest mocker.
         mocker: Pytest mocker.
-        tmp_path: test path.
     """
     """
-    bun_location = tmp_path / ".bun"
-    bun_location.mkdir()
-
-    mocker.patch(
-        "reflex.utils.prerequisites.get_package_manager",
-        return_value=str(bun_location),
-    )
-    mocker.patch(
-        "reflex.utils.prerequisites.os.path.expandvars",
-        return_value=str(bun_location),
-    )
+    mocker.patch("reflex.utils.prerequisites.os.path.exists", return_value=True)
+    rm = mocker.patch("reflex.utils.prerequisites.path_ops.rm", mocker.Mock())
 
 
     prerequisites.remove_existing_bun_installation()
     prerequisites.remove_existing_bun_installation()
-
-    assert not bun_location.exists()
+    rm.assert_called_once()
 
 
 
 
 def test_setup_frontend(tmp_path, mocker):
 def test_setup_frontend(tmp_path, mocker):
@@ -331,19 +308,15 @@ def test_setup_frontend(tmp_path, mocker):
         tmp_path: root path of test case data directory
         tmp_path: root path of test case data directory
         mocker: mocker object to allow mocking
         mocker: mocker object to allow mocking
     """
     """
-    web_folder = tmp_path / ".web"
-    web_public_folder = web_folder / "public"
+    web_public_folder = tmp_path / ".web" / "public"
     assets = tmp_path / "assets"
     assets = tmp_path / "assets"
     assets.mkdir()
     assets.mkdir()
     (assets / "favicon.ico").touch()
     (assets / "favicon.ico").touch()
 
 
-    assert str(web_folder) == prerequisites.create_web_directory(tmp_path)
-
     mocker.patch("reflex.utils.prerequisites.install_frontend_packages")
     mocker.patch("reflex.utils.prerequisites.install_frontend_packages")
     mocker.patch("reflex.utils.build.set_environment_variables")
     mocker.patch("reflex.utils.build.set_environment_variables")
 
 
     build.setup_frontend(tmp_path, disable_telemetry=False)
     build.setup_frontend(tmp_path, disable_telemetry=False)
-    assert web_folder.exists()
     assert web_public_folder.exists()
     assert web_public_folder.exists()
     assert (web_public_folder / "favicon.ico").exists()
     assert (web_public_folder / "favicon.ico").exists()
 
 
@@ -518,3 +491,60 @@ def test_initialize_non_existent_gitignore(tmp_path, mocker, gitignore_exists):
     assert gitignore_file.exists()
     assert gitignore_file.exists()
     file_content = [line.strip() for line in gitignore_file.open().readlines()]
     file_content = [line.strip() for line in gitignore_file.open().readlines()]
     assert set(file_content) - expected == set()
     assert set(file_content) - expected == set()
+
+
+def test_app_default_name(tmp_path, mocker):
+    """Test that an error is raised if the app name is reflex.
+
+    Args:
+        tmp_path: Test working dir.
+        mocker: Pytest mocker object.
+    """
+    reflex = tmp_path / "reflex"
+    reflex.mkdir()
+
+    mocker.patch("reflex.utils.prerequisites.os.getcwd", return_value=str(reflex))
+
+    with pytest.raises(typer.Exit):
+        prerequisites.get_default_app_name()
+
+
+def test_node_install_windows(mocker):
+    """Require user to install node manually for windows if node is not installed.
+
+    Args:
+        mocker: Pytest mocker object.
+    """
+    mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", True)
+    mocker.patch("reflex.utils.prerequisites.check_node_version", return_value=False)
+
+    with pytest.raises(typer.Exit):
+        prerequisites.initialize_node()
+
+
+def test_node_install_unix(tmp_path, mocker):
+    nvm_root_path = tmp_path / ".reflex" / ".nvm"
+
+    mocker.patch("reflex.utils.prerequisites.constants.NVM_ROOT_PATH", nvm_root_path)
+    subprocess_run = mocker.patch(
+        "reflex.utils.prerequisites.subprocess.run",
+        return_value=subprocess.CompletedProcess(args="", returncode=0),
+    )
+
+    prerequisites.install_node()
+
+    assert nvm_root_path.exists()
+    subprocess_run.assert_called()
+    subprocess_run.call_count = 2
+
+
+def test_node_install_without_curl(mocker):
+    """Test that an error is thrown when installing node with curl not installed.
+
+    Args:
+        mocker: Pytest mocker object.
+    """
+    mocker.patch("reflex.utils.prerequisites.path_ops.which", return_value=None)
+
+    with pytest.raises(FileNotFoundError):
+        prerequisites.install_node()