From ed05f57fc9fe65e875d02d3e75aecc4720dfcdea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Mon, 17 Jun 2024 22:17:00 +0200 Subject: [PATCH] Make `.web` configurable with REFLEX_WEB_WORKDIR (#3462) --- .../test_benchmark_compile_components.py | 9 +- benchmarks/test_benchmark_compile_pages.py | 13 +- reflex/app.py | 6 +- reflex/compiler/compiler.py | 7 +- reflex/compiler/utils.py | 47 +++--- reflex/constants/__init__.py | 3 +- reflex/constants/base.py | 36 ++--- reflex/constants/compiler.py | 4 +- reflex/constants/installer.py | 4 +- reflex/constants/style.py | 11 +- reflex/testing.py | 8 +- reflex/utils/build.py | 144 +++++++++--------- reflex/utils/exec.py | 17 ++- reflex/utils/export.py | 21 +-- reflex/utils/path_ops.py | 72 +++++---- reflex/utils/prerequisites.py | 62 ++++---- reflex/utils/watch.py | 4 +- tests/test_app.py | 2 +- tests/test_telemetry.py | 30 ++-- 19 files changed, 270 insertions(+), 230 deletions(-) diff --git a/benchmarks/test_benchmark_compile_components.py b/benchmarks/test_benchmark_compile_components.py index 14d5d4d89..81d0c2e89 100644 --- a/benchmarks/test_benchmark_compile_components.py +++ b/benchmarks/test_benchmark_compile_components.py @@ -13,6 +13,9 @@ from reflex import constants from reflex.compiler import utils from reflex.testing import AppHarness, chdir from reflex.utils import build +from reflex.utils.prerequisites import get_web_dir + +web_pages = get_web_dir() / constants.Dirs.PAGES def render_component(num: int): @@ -231,7 +234,7 @@ def test_app_10_compile_time_cold(benchmark, app_with_10_components): def setup(): with chdir(app_with_10_components.app_path): - utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"]) + utils.empty_dir(web_pages, ["_app.js"]) app_with_10_components._initialize_app() build.setup_frontend(app_with_10_components.app_path) @@ -284,7 +287,7 @@ def test_app_100_compile_time_cold(benchmark, app_with_100_components): def setup(): with chdir(app_with_100_components.app_path): - utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"]) + utils.empty_dir(web_pages, ["_app.js"]) app_with_100_components._initialize_app() build.setup_frontend(app_with_100_components.app_path) @@ -337,7 +340,7 @@ def test_app_1000_compile_time_cold(benchmark, app_with_1000_components): def setup(): with chdir(app_with_1000_components.app_path): - utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"]) + utils.empty_dir(web_pages, keep_files=["_app.js"]) app_with_1000_components._initialize_app() build.setup_frontend(app_with_1000_components.app_path) diff --git a/benchmarks/test_benchmark_compile_pages.py b/benchmarks/test_benchmark_compile_pages.py index 97e1bb68e..3a0164b2f 100644 --- a/benchmarks/test_benchmark_compile_pages.py +++ b/benchmarks/test_benchmark_compile_pages.py @@ -13,6 +13,9 @@ from reflex import constants from reflex.compiler import utils from reflex.testing import AppHarness, chdir from reflex.utils import build +from reflex.utils.prerequisites import get_web_dir + +web_pages = get_web_dir() / constants.Dirs.PAGES def render_multiple_pages(app, num: int): @@ -320,7 +323,7 @@ def test_app_1_compile_time_cold(benchmark, app_with_one_page): def setup(): with chdir(app_with_one_page.app_path): - utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"]) + utils.empty_dir(web_pages, keep_files=["_app.js"]) app_with_one_page._initialize_app() build.setup_frontend(app_with_one_page.app_path) @@ -375,7 +378,7 @@ def test_app_10_compile_time_cold(benchmark, app_with_ten_pages): def setup(): with chdir(app_with_ten_pages.app_path): - utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"]) + utils.empty_dir(web_pages, keep_files=["_app.js"]) app_with_ten_pages._initialize_app() build.setup_frontend(app_with_ten_pages.app_path) @@ -430,7 +433,7 @@ def test_app_100_compile_time_cold(benchmark, app_with_hundred_pages): def setup(): with chdir(app_with_hundred_pages.app_path): - utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"]) + utils.empty_dir(web_pages, keep_files=["_app.js"]) app_with_hundred_pages._initialize_app() build.setup_frontend(app_with_hundred_pages.app_path) @@ -485,7 +488,7 @@ def test_app_1000_compile_time_cold(benchmark, app_with_thousand_pages): def setup(): with chdir(app_with_thousand_pages.app_path): - utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"]) + utils.empty_dir(web_pages, keep_files=["_app.js"]) app_with_thousand_pages._initialize_app() build.setup_frontend(app_with_thousand_pages.app_path) @@ -540,7 +543,7 @@ def test_app_10000_compile_time_cold(benchmark, app_with_ten_thousand_pages): def setup(): with chdir(app_with_ten_thousand_pages.app_path): - utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"]) + utils.empty_dir(web_pages, keep_files=["_app.js"]) app_with_ten_thousand_pages._initialize_app() build.setup_frontend(app_with_ten_thousand_pages.app_path) diff --git a/reflex/app.py b/reflex/app.py index 5441ce987..cee1998b9 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -751,10 +751,12 @@ class App(LifespanMixin, Base): if should_skip_compile(): return False + nocompile = prerequisites.get_web_dir() / constants.NOCOMPILE_FILE + # Check the nocompile file. - if os.path.exists(constants.NOCOMPILE_FILE): + if nocompile.exists(): # Delete the nocompile file - os.remove(constants.NOCOMPILE_FILE) + nocompile.unlink() return False # By default, compile the app. diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 81d8098de..002c3b750 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -20,6 +20,7 @@ from reflex.state import BaseState from reflex.style import SYSTEM_COLOR_MODE from reflex.utils.exec import is_prod_mode from reflex.utils.imports import ImportVar +from reflex.utils.prerequisites import get_web_dir from reflex.vars import Var @@ -469,7 +470,7 @@ def compile_tailwind( The compiled Tailwind config. """ # Get the path for the output file. - output_path = constants.Tailwind.CONFIG + output_path = get_web_dir() / constants.Tailwind.CONFIG # Compile the config. code = _compile_tailwind(config) @@ -483,7 +484,7 @@ def remove_tailwind_from_postcss() -> tuple[str, str]: The path and code of the compiled postcss.config.js. """ # Get the path for the output file. - output_path = constants.Dirs.POSTCSS_JS + output_path = str(get_web_dir() / constants.Dirs.POSTCSS_JS) code = [ line @@ -502,7 +503,7 @@ def purge_web_pages_dir(): return # Empty out the web pages directory. - utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"]) + utils.empty_dir(get_web_dir() / constants.Dirs.PAGES, keep_files=["_app.js"]) class ExecutorSafeFunctions: diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index cc2d805fa..fde499094 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -3,9 +3,12 @@ from __future__ import annotations import os +from pathlib import Path from typing import Any, Callable, Dict, Optional, Type, Union from urllib.parse import urlparse +from reflex.utils.prerequisites import get_web_dir + try: from pydantic.v1.fields import ModelField except ModuleNotFoundError: @@ -330,7 +333,7 @@ def get_page_path(path: str) -> str: Returns: The path of the compiled JS file. """ - return os.path.join(constants.Dirs.WEB_PAGES, path + constants.Ext.JS) + return str(get_web_dir() / constants.Dirs.PAGES / (path + constants.Ext.JS)) def get_theme_path() -> str: @@ -339,8 +342,10 @@ def get_theme_path() -> str: Returns: The path of the theme style. """ - return os.path.join( - constants.Dirs.WEB_UTILS, constants.PageNames.THEME + constants.Ext.JS + return str( + get_web_dir() + / constants.Dirs.UTILS + / (constants.PageNames.THEME + constants.Ext.JS) ) @@ -350,8 +355,10 @@ def get_root_stylesheet_path() -> str: Returns: The path of the app root file. """ - return os.path.join( - constants.STYLES_DIR, constants.PageNames.STYLESHEET_ROOT + constants.Ext.CSS + return str( + get_web_dir() + / constants.Dirs.STYLES + / (constants.PageNames.STYLESHEET_ROOT + constants.Ext.CSS) ) @@ -361,9 +368,7 @@ def get_context_path() -> str: Returns: The path of the context module. """ - return os.path.join( - constants.Dirs.WEB, constants.Dirs.CONTEXTS_PATH + constants.Ext.JS - ) + return str(get_web_dir() / (constants.Dirs.CONTEXTS_PATH + constants.Ext.JS)) def get_components_path() -> str: @@ -372,7 +377,11 @@ def get_components_path() -> str: Returns: The path of the compiled components. """ - return os.path.join(constants.Dirs.WEB_UTILS, "components" + constants.Ext.JS) + return str( + get_web_dir() + / constants.Dirs.UTILS + / (constants.PageNames.COMPONENTS + constants.Ext.JS), + ) def get_stateful_components_path() -> str: @@ -381,9 +390,10 @@ def get_stateful_components_path() -> str: Returns: The path of the compiled stateful components. """ - return os.path.join( - constants.Dirs.WEB_UTILS, - constants.PageNames.STATEFUL_COMPONENTS + constants.Ext.JS, + return str( + get_web_dir() + / constants.Dirs.UTILS + / (constants.PageNames.STATEFUL_COMPONENTS + constants.Ext.JS) ) @@ -437,23 +447,24 @@ def write_page(path: str, code: str): f.write(code) -def empty_dir(path: str, keep_files: list[str] | None = None): +def empty_dir(path: str | Path, keep_files: list[str] | None = None): """Remove all files and folders in a directory except for the keep_files. Args: path: The path to the directory that will be emptied keep_files: List of filenames or foldernames that will not be deleted. """ + path = Path(path) + # If the directory does not exist, return. - if not os.path.exists(path): + if not path.exists(): return # Remove all files and folders in the directory. keep_files = keep_files or [] - directory_contents = os.listdir(path) - for element in directory_contents: - if element not in keep_files: - path_ops.rm(os.path.join(path, element)) + for element in path.iterdir(): + if element.name not in keep_files: + path_ops.rm(element) def is_valid_url(url) -> bool: diff --git a/reflex/constants/__init__.py b/reflex/constants/__init__.py index 547de703e..6389a8b0a 100644 --- a/reflex/constants/__init__.py +++ b/reflex/constants/__init__.py @@ -62,7 +62,7 @@ from .route import ( RouteRegex, RouteVar, ) -from .style import STYLES_DIR, Tailwind +from .style import Tailwind __ALL__ = [ ALEMBIC_CONFIG, @@ -113,7 +113,6 @@ __ALL__ = [ SETTER_PREFIX, SKIP_COMPILE_ENV_VAR, SocketEvent, - STYLES_DIR, Tailwind, Templates, CompileVars, diff --git a/reflex/constants/base.py b/reflex/constants/base.py index fbbe51707..3fca45e2f 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -25,30 +25,26 @@ class Dirs(SimpleNamespace): EXTERNAL_APP_ASSETS = "external" # The name of the utils file. UTILS = "utils" - # The name of the output static directory. - STATIC = "_static" - # The name of the public html directory served at "/" - PUBLIC = "public" # The name of the state file. STATE_PATH = "/".join([UTILS, "state"]) # The name of the components file. COMPONENTS_PATH = "/".join([UTILS, "components"]) # The name of the contexts file. CONTEXTS_PATH = "/".join([UTILS, "context"]) - # The directory where the app pages are compiled to. - WEB_PAGES = os.path.join(WEB, "pages") - # The directory where the static build is located. - WEB_STATIC = os.path.join(WEB, STATIC) - # The directory where the utils file is located. - WEB_UTILS = os.path.join(WEB, UTILS) - # The directory where the assets are located. - WEB_ASSETS = os.path.join(WEB, PUBLIC) - # The env json file. - ENV_JSON = os.path.join(WEB, "env.json") - # The reflex json file. - REFLEX_JSON = os.path.join(WEB, "reflex.json") - # The path to postcss.config.js - POSTCSS_JS = os.path.join(WEB, "postcss.config.js") + # The name of the output static directory. + STATIC = "_static" + # The name of the public html directory served at "/" + PUBLIC = "public" + # The directory where styles are located. + STYLES = "styles" + # The name of the pages directory. + PAGES = "pages" + # The name of the env json file. + ENV_JSON = "env.json" + # The name of the reflex json file. + REFLEX_JSON = "reflex.json" + # The name of the postcss config file. + POSTCSS_JS = "postcss.config.js" class Reflex(SimpleNamespace): @@ -61,7 +57,7 @@ class Reflex(SimpleNamespace): VERSION = metadata.version(MODULE_NAME) # The reflex json file. - JSON = os.path.join(Dirs.WEB, "reflex.json") + JSON = "reflex.json" # Files and directories used to init a new project. # The directory to store reflex dependencies. @@ -117,7 +113,7 @@ class Next(SimpleNamespace): # The NextJS config file CONFIG_FILE = "next.config.js" # The sitemap config file. - SITEMAP_CONFIG_FILE = os.path.join(Dirs.WEB, "next-sitemap.config.js") + SITEMAP_CONFIG_FILE = "next-sitemap.config.js" # The node modules directory. NODE_MODULES = "node_modules" # The package lock file. diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index 96e8b03ba..0c71dae0d 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -12,7 +12,7 @@ from reflex.utils.imports import ImportVar SETTER_PREFIX = "set_" # The file used to specify no compilation. -NOCOMPILE_FILE = ".web/nocompile" +NOCOMPILE_FILE = "nocompile" class Ext(SimpleNamespace): @@ -80,6 +80,8 @@ class PageNames(SimpleNamespace): DOCUMENT_ROOT = "_document" # The name of the theme page. THEME = "theme" + # The module containing components. + COMPONENTS = "components" # The module containing shared stateful components STATEFUL_COMPONENTS = "stateful_components" diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 6fa0d2e30..cb074e9f3 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -5,7 +5,7 @@ import os import platform from types import SimpleNamespace -from .base import IS_WINDOWS, Dirs, Reflex +from .base import IS_WINDOWS, Reflex def get_fnm_name() -> str | None: @@ -105,7 +105,7 @@ class PackageJson(SimpleNamespace): EXPORT_SITEMAP = "next build && next-sitemap" PROD = "next start" - PATH = os.path.join(Dirs.WEB, "package.json") + PATH = "package.json" DEPENDENCIES = { "@emotion/react": "11.11.1", diff --git a/reflex/constants/style.py b/reflex/constants/style.py index d0f72995e..cd0fa1a2a 100644 --- a/reflex/constants/style.py +++ b/reflex/constants/style.py @@ -1,13 +1,6 @@ """Style constants.""" - -import os from types import SimpleNamespace -from reflex.constants.base import Dirs - -# The directory where styles are located. -STYLES_DIR = os.path.join(Dirs.WEB, "styles") - class Tailwind(SimpleNamespace): """Tailwind constants.""" @@ -15,8 +8,8 @@ class Tailwind(SimpleNamespace): # The Tailwindcss version VERSION = "tailwindcss@3.3.2" # The Tailwind config. - CONFIG = os.path.join(Dirs.WEB, "tailwind.config.js") + CONFIG = "tailwind.config.js" # Default Tailwind content paths CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"] - # Relative tailwind style path to root stylesheet in STYLES_DIR. + # Relative tailwind style path to root stylesheet in Dirs.STYLES. ROOT_STYLE_PATH = "./tailwind.css" diff --git a/reflex/testing.py b/reflex/testing.py index 8fcb50d60..135286b4a 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -330,7 +330,7 @@ class AppHarness: # Start the frontend. self.frontend_process = reflex.utils.processes.new_process( [reflex.utils.prerequisites.get_package_manager(), "run", "dev"], - cwd=self.app_path / reflex.constants.Dirs.WEB, + cwd=self.app_path / reflex.utils.prerequisites.get_web_dir(), env={"PORT": "0"}, **FRONTEND_POPEN_ARGS, ) @@ -854,7 +854,11 @@ class AppHarnessProd(AppHarness): frontend_server: Optional[Subdir404TCPServer] = None def _run_frontend(self): - web_root = self.app_path / reflex.constants.Dirs.WEB_STATIC + web_root = ( + self.app_path + / reflex.utils.prerequisites.get_web_dir() + / reflex.constants.Dirs.STATIC + ) error_page_map = { 404: web_root / "404.html", } diff --git a/reflex/utils/build.py b/reflex/utils/build.py index 65bb8b04c..7a67ec32e 100644 --- a/reflex/utils/build.py +++ b/reflex/utils/build.py @@ -18,7 +18,7 @@ from reflex.utils import console, path_ops, prerequisites, processes def set_env_json(): """Write the upload url to a REFLEX_JSON.""" path_ops.update_json_file( - constants.Dirs.ENV_JSON, + str(prerequisites.get_web_dir() / constants.Dirs.ENV_JSON), {endpoint.name: endpoint.get_url() for endpoint in constants.Endpoint}, ) @@ -55,8 +55,8 @@ def generate_sitemap_config(deploy_url: str, export=False): config = json.dumps(config) - with open(constants.Next.SITEMAP_CONFIG_FILE, "w") as f: - f.write(templates.SITEMAP_CONFIG(config=config)) + sitemap = prerequisites.get_web_dir() / constants.Next.SITEMAP_CONFIG_FILE + sitemap.write_text(templates.SITEMAP_CONFIG(config=config)) def _zip( @@ -129,85 +129,89 @@ def _zip( zipf.write(file, os.path.relpath(file, root_dir)) -def export( - backend: bool = True, +def zip_app( frontend: bool = True, - zip: bool = False, + backend: bool = True, zip_dest_dir: str = os.getcwd(), - deploy_url: str | None = None, upload_db_file: bool = False, ): - """Export the app for deployment. + """Zip up the app. Args: - backend: Whether to zip up the backend app. frontend: Whether to zip up the frontend app. - zip: Whether to zip the app. - zip_dest_dir: The destination directory for created zip files (if any) - deploy_url: The URL of the deployed app. - upload_db_file: Whether to include local sqlite db files from the backend zip. + backend: Whether to zip up the backend app. + zip_dest_dir: The directory to export the zip file to. + upload_db_file: Whether to upload the database file. """ - # Remove the static folder. - path_ops.rm(constants.Dirs.WEB_STATIC) + files_to_exclude = { + constants.ComponentName.FRONTEND.zip(), + constants.ComponentName.BACKEND.zip(), + } + + if frontend: + _zip( + component_name=constants.ComponentName.FRONTEND, + target=os.path.join(zip_dest_dir, constants.ComponentName.FRONTEND.zip()), + root_dir=str(prerequisites.get_web_dir() / constants.Dirs.STATIC), + files_to_exclude=files_to_exclude, + exclude_venv_dirs=False, + ) + + if backend: + _zip( + component_name=constants.ComponentName.BACKEND, + target=os.path.join(zip_dest_dir, constants.ComponentName.BACKEND.zip()), + root_dir=".", + dirs_to_exclude={"__pycache__"}, + files_to_exclude=files_to_exclude, + top_level_dirs_to_exclude={"assets"}, + exclude_venv_dirs=True, + upload_db_file=upload_db_file, + ) + + +def build( + deploy_url: str | None = None, + for_export: bool = False, +): + """Build the app for deployment. + + Args: + deploy_url: The deployment URL. + for_export: Whether the build is for export. + """ + wdir = prerequisites.get_web_dir() + + # Clean the static directory if it exists. + path_ops.rm(str(wdir / constants.Dirs.STATIC)) # The export command to run. command = "export" - if frontend: - checkpoints = [ - "Linting and checking ", - "Creating an optimized production build", - "Route (pages)", - "prerendered as static HTML", - "Collecting page data", - "Finalizing page optimization", - "Collecting build traces", - ] + checkpoints = [ + "Linting and checking ", + "Creating an optimized production build", + "Route (pages)", + "prerendered as static HTML", + "Collecting page data", + "Finalizing page optimization", + "Collecting build traces", + ] - # Generate a sitemap if a deploy URL is provided. - if deploy_url is not None: - generate_sitemap_config(deploy_url, export=zip) - command = "export-sitemap" + # Generate a sitemap if a deploy URL is provided. + if deploy_url is not None: + generate_sitemap_config(deploy_url, export=for_export) + command = "export-sitemap" - checkpoints.extend(["Loading next-sitemap", "Generation completed"]) + checkpoints.extend(["Loading next-sitemap", "Generation completed"]) - # Start the subprocess with the progress bar. - process = processes.new_process( - [prerequisites.get_package_manager(), "run", command], - cwd=constants.Dirs.WEB, - shell=constants.IS_WINDOWS, - ) - processes.show_progress("Creating Production Build", process, checkpoints) - - # Zip up the app. - if zip: - files_to_exclude = { - constants.ComponentName.FRONTEND.zip(), - constants.ComponentName.BACKEND.zip(), - } - if frontend: - _zip( - component_name=constants.ComponentName.FRONTEND, - target=os.path.join( - zip_dest_dir, constants.ComponentName.FRONTEND.zip() - ), - root_dir=constants.Dirs.WEB_STATIC, - files_to_exclude=files_to_exclude, - exclude_venv_dirs=False, - ) - if backend: - _zip( - component_name=constants.ComponentName.BACKEND, - target=os.path.join( - zip_dest_dir, constants.ComponentName.BACKEND.zip() - ), - root_dir=".", - dirs_to_exclude={"__pycache__"}, - files_to_exclude=files_to_exclude, - top_level_dirs_to_exclude={"assets"}, - exclude_venv_dirs=True, - upload_db_file=upload_db_file, - ) + # Start the subprocess with the progress bar. + process = processes.new_process( + [prerequisites.get_package_manager(), "run", command], + cwd=wdir, + shell=constants.IS_WINDOWS, + ) + processes.show_progress("Creating Production Build", process, checkpoints) def setup_frontend( @@ -226,7 +230,7 @@ def setup_frontend( # Copy asset files to public folder. path_ops.cp( src=str(root / constants.Dirs.APP_ASSETS), - dest=str(root / constants.Dirs.WEB_ASSETS), + dest=str(root / prerequisites.get_web_dir() / constants.Dirs.PUBLIC), ) # Set the environment variables in client (env.json). @@ -242,7 +246,7 @@ def setup_frontend( "telemetry", "disable", ], - cwd=constants.Dirs.WEB, + cwd=prerequisites.get_web_dir(), stdout=subprocess.DEVNULL, shell=constants.IS_WINDOWS, ) @@ -259,7 +263,7 @@ def setup_frontend_prod( disable_telemetry: Whether to disable the Next telemetry. """ setup_frontend(root, disable_telemetry) - export(deploy_url=get_config().deploy_url) + build(deploy_url=get_config().deploy_url) def _looks_like_venv_dir(dir_to_check: str) -> bool: diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index dd99d6e8d..77df98116 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -17,6 +17,7 @@ import psutil from reflex import constants from reflex.config import get_config from reflex.utils import console, path_ops +from reflex.utils.prerequisites import get_web_dir from reflex.utils.watch import AssetFolderWatch # For uvicorn windows bug fix (#2335) @@ -82,8 +83,8 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True): """ from reflex.utils import processes - json_file_path = os.path.join(constants.Dirs.WEB, "package.json") - last_hash = detect_package_change(json_file_path) + json_file_path = get_web_dir() / constants.PackageJson.PATH + last_hash = detect_package_change(str(json_file_path)) process = None first_run = True @@ -94,7 +95,7 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True): kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore process = processes.new_process( run_command, - cwd=constants.Dirs.WEB, + cwd=get_web_dir(), shell=constants.IS_WINDOWS, **kwargs, ) @@ -128,7 +129,7 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True): "`REFLEX_USE_NPM=1 reflex init`\n" "`REFLEX_USE_NPM=1 reflex run`" ) - new_hash = detect_package_change(json_file_path) + new_hash = detect_package_change(str(json_file_path)) if new_hash != last_hash: last_hash = new_hash kill(process.pid) @@ -201,10 +202,10 @@ def run_backend( config = get_config() app_module = f"reflex.app_module_for_backend:{constants.CompileVars.APP}" + web_dir = get_web_dir() # Create a .nocompile file to skip compile for backend. - if os.path.exists(constants.Dirs.WEB): - with open(constants.NOCOMPILE_FILE, "w"): - pass + if web_dir.exists(): + (web_dir / constants.NOCOMPILE_FILE).touch() # Run the backend in development mode. uvicorn.run( @@ -214,7 +215,7 @@ def run_backend( log_level=loglevel.value, reload=True, reload_dirs=[config.app_name], - reload_excludes=[constants.Dirs.WEB], + reload_excludes=[str(web_dir)], ) diff --git a/reflex/utils/export.py b/reflex/utils/export.py index 3116f4859..14cde291c 100644 --- a/reflex/utils/export.py +++ b/reflex/utils/export.py @@ -55,15 +55,18 @@ def export( # Set up .web directory and install frontend dependencies. build.setup_frontend(Path.cwd()) - # Export the app. - build.export( - backend=backend, - frontend=frontend, - zip=zipping, - zip_dest_dir=zip_dest_dir, - deploy_url=config.deploy_url, - upload_db_file=upload_db_file, - ) + # Build the static app. + if frontend: + build.build(deploy_url=config.deploy_url, for_export=True) + + # Zip up the app. + if zipping: + build.zip_app( + frontend=frontend, + backend=backend, + zip_dest_dir=zip_dest_dir, + upload_db_file=upload_db_file, + ) # Post a telemetry event. telemetry.send("export") diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index 3073dca0e..39f2138f8 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -14,19 +14,20 @@ from reflex import constants join = os.linesep.join -def rm(path: str): +def rm(path: str | Path): """Remove a file or directory. Args: path: The path to the file or directory. """ - if os.path.isdir(path): + path = Path(path) + if path.is_dir(): shutil.rmtree(path) - elif os.path.isfile(path): - os.remove(path) + elif path.is_file(): + path.unlink() -def cp(src: str, dest: str, overwrite: bool = True) -> bool: +def cp(src: str | Path, dest: str | Path, overwrite: bool = True) -> bool: """Copy a file or directory. Args: @@ -37,11 +38,12 @@ def cp(src: str, dest: str, overwrite: bool = True) -> bool: Returns: Whether the copy was successful. """ + src, dest = Path(src), Path(dest) if src == dest: return False - if not overwrite and os.path.exists(dest): + if not overwrite and dest.exists(): return False - if os.path.isdir(src): + if src.is_dir(): rm(dest) shutil.copytree(src, dest) else: @@ -49,7 +51,7 @@ def cp(src: str, dest: str, overwrite: bool = True) -> bool: return True -def mv(src: str, dest: str, overwrite: bool = True) -> bool: +def mv(src: str | Path, dest: str | Path, overwrite: bool = True) -> bool: """Move a file or directory. Args: @@ -60,25 +62,26 @@ def mv(src: str, dest: str, overwrite: bool = True) -> bool: Returns: Whether the move was successful. """ + src, dest = Path(src), Path(dest) if src == dest: return False - if not overwrite and os.path.exists(dest): + if not overwrite and dest.exists(): return False rm(dest) shutil.move(src, dest) return True -def mkdir(path: str): +def mkdir(path: str | Path): """Create a directory. Args: path: The path to the directory. """ - os.makedirs(path, exist_ok=True) + Path(path).mkdir(parents=True, exist_ok=True) -def ln(src: str, dest: str, overwrite: bool = False) -> bool: +def ln(src: str | Path, dest: str | Path, overwrite: bool = False) -> bool: """Create a symbolic link. Args: @@ -89,19 +92,20 @@ def ln(src: str, dest: str, overwrite: bool = False) -> bool: Returns: Whether the link was successful. """ + src, dest = Path(src), Path(dest) if src == dest: return False - if not overwrite and (os.path.exists(dest) or os.path.islink(dest)): + if not overwrite and (dest.exists() or dest.is_symlink()): return False - if os.path.isdir(src): + if src.is_dir(): rm(dest) - os.symlink(src, dest, target_is_directory=True) + src.symlink_to(dest, target_is_directory=True) else: - os.symlink(src, dest) + src.symlink_to(dest) return True -def which(program: str) -> str | None: +def which(program: str | Path) -> str | Path | None: """Find the path to an executable. Args: @@ -110,7 +114,7 @@ def which(program: str) -> str | None: Returns: The path to the executable. """ - return shutil.which(program) + return shutil.which(str(program)) def get_node_bin_path() -> str | None: @@ -119,10 +123,11 @@ def get_node_bin_path() -> str | None: Returns: The path to the node bin folder. """ - if not os.path.exists(constants.Node.BIN_PATH): + bin_path = Path(constants.Node.BIN_PATH) + if not bin_path.exists(): str_path = which("node") return str(Path(str_path).parent.resolve()) if str_path else str_path - return str(Path(constants.Node.BIN_PATH).resolve()) + return str(bin_path.resolve()) def get_node_path() -> str | None: @@ -131,9 +136,10 @@ def get_node_path() -> str | None: Returns: The path to the node binary file. """ - if not os.path.exists(constants.Node.PATH): - return which("node") - return constants.Node.PATH + node_path = Path(constants.Node.PATH) + if not node_path.exists(): + return str(which("node")) + return str(node_path) def get_npm_path() -> str | None: @@ -142,12 +148,13 @@ def get_npm_path() -> str | None: Returns: The path to the npm binary file. """ - if not os.path.exists(constants.Node.PATH): - return which("npm") - return constants.Node.NPM_PATH + npm_path = Path(constants.Node.NPM_PATH) + if not npm_path.exists(): + return str(which("npm")) + return str(npm_path) -def update_json_file(file_path: str, update_dict: dict[str, int | str]): +def update_json_file(file_path: str | Path, update_dict: dict[str, int | str]): """Update the contents of a json file. Args: @@ -176,7 +183,7 @@ def update_json_file(file_path: str, update_dict: dict[str, int | str]): json.dump(json_object, f, ensure_ascii=False) -def find_replace(directory: str, find: str, replace: str): +def find_replace(directory: str | Path, find: str, replace: str): """Recursively find and replace text in files in a directory. Args: @@ -184,11 +191,10 @@ def find_replace(directory: str, find: str, replace: str): find: The text to find. replace: The text to replace. """ + directory = Path(directory) for root, _dirs, files in os.walk(directory): for file in files: - filepath = os.path.join(root, file) - with open(filepath, "r", encoding="utf-8") as f: - text = f.read() + filepath = Path(root, file) + text = filepath.read_text(encoding="utf-8") text = re.sub(find, replace, text) - with open(filepath, "w") as f: - f.write(text) + filepath.write_text(text) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 8bdb4dde0..40dcf3348 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -58,6 +58,18 @@ class CpuInfo(Base): address_width: Optional[int] +def get_web_dir() -> Path: + """Get the working directory for the next.js commands. + + Can be overriden with REFLEX_WEB_WORKDIR. + + Returns: + The working directory. + """ + workdir = Path(os.getenv("REFLEX_WEB_WORKDIR", constants.Dirs.WEB)) + return workdir + + def check_latest_package_version(package_name: str): """Check if the latest version of the package is installed. @@ -91,15 +103,15 @@ def get_or_set_last_reflex_version_check_datetime(): Returns: The last version check datetime. """ - if not os.path.exists(constants.Reflex.JSON): + reflex_json_file = get_web_dir() / constants.Reflex.JSON + if not reflex_json_file.exists(): return None # Open and read the file - with open(constants.Reflex.JSON, "r") as file: - data: dict = json.load(file) + data = json.loads(reflex_json_file.read_text()) last_version_check_datetime = data.get("last_version_check_datetime") if not last_version_check_datetime: data.update({"last_version_check_datetime": str(datetime.now())}) - path_ops.update_json_file(constants.Reflex.JSON, data) + path_ops.update_json_file(reflex_json_file, data) return last_version_check_datetime @@ -513,12 +525,11 @@ def get_project_hash(raise_on_fail: bool = False) -> int | None: Returns: project_hash: The app hash. """ - if not os.path.exists(constants.Reflex.JSON) and not raise_on_fail: + json_file = get_web_dir() / constants.Reflex.JSON + if not json_file.exists() and not raise_on_fail: return None - # Open and read the file - with open(constants.Reflex.JSON, "r") as file: - data = json.load(file) - return data.get("project_hash") + data = json.loads(json_file.read_text()) + return data.get("project_hash") def initialize_web_directory(): @@ -528,11 +539,11 @@ def initialize_web_directory(): # Re-use the hash if one is already created, so we don't over-write it when running reflex init project_hash = get_project_hash() - path_ops.cp(constants.Templates.Dirs.WEB_TEMPLATE, constants.Dirs.WEB) + path_ops.cp(constants.Templates.Dirs.WEB_TEMPLATE, str(get_web_dir())) initialize_package_json() - path_ops.mkdir(constants.Dirs.WEB_ASSETS) + path_ops.mkdir(get_web_dir() / constants.Dirs.PUBLIC) update_next_config() @@ -555,10 +566,9 @@ def _compile_package_json(): def initialize_package_json(): """Render and write in .web the package.json file.""" - output_path = constants.PackageJson.PATH + output_path = get_web_dir() / constants.PackageJson.PATH code = _compile_package_json() - with open(output_path, "w") as file: - file.write(code) + output_path.write_text(code) def init_reflex_json(project_hash: int | None): @@ -583,7 +593,7 @@ def init_reflex_json(project_hash: int | None): "version": constants.Reflex.VERSION, "project_hash": project_hash, } - path_ops.update_json_file(constants.Reflex.JSON, reflex_json) + path_ops.update_json_file(get_web_dir() / constants.Reflex.JSON, reflex_json) def update_next_config(export=False, transpile_packages: Optional[List[str]] = None): @@ -593,7 +603,7 @@ def update_next_config(export=False, transpile_packages: Optional[List[str]] = N export: if the method run during reflex export. transpile_packages: list of packages to transpile via next.config.js. """ - next_config_file = Path(constants.Dirs.WEB, constants.Next.CONFIG_FILE) + next_config_file = get_web_dir() / constants.Next.CONFIG_FILE next_config = _update_next_config( get_config(), export=export, transpile_packages=transpile_packages @@ -845,9 +855,7 @@ def cached_procedure(cache_file: str, payload_fn: Callable[..., str]): @cached_procedure( - cache_file=os.path.join( - constants.Dirs.WEB, "reflex.install_frontend_packages.cached" - ), + cache_file=str(get_web_dir() / "reflex.install_frontend_packages.cached"), payload_fn=lambda p, c: f"{repr(sorted(list(p)))},{c.json()}", ) def install_frontend_packages(packages: set[str], config: Config): @@ -874,7 +882,7 @@ def install_frontend_packages(packages: set[str], config: Config): fallback=fallback_command, analytics_enabled=True, show_status_message="Installing base frontend packages", - cwd=constants.Dirs.WEB, + cwd=get_web_dir(), shell=constants.IS_WINDOWS, ) @@ -890,7 +898,7 @@ def install_frontend_packages(packages: set[str], config: Config): fallback=fallback_command, analytics_enabled=True, show_status_message="Installing tailwind", - cwd=constants.Dirs.WEB, + cwd=get_web_dir(), shell=constants.IS_WINDOWS, ) @@ -901,7 +909,7 @@ def install_frontend_packages(packages: set[str], config: Config): fallback=fallback_command, analytics_enabled=True, show_status_message="Installing frontend packages from config and components", - cwd=constants.Dirs.WEB, + cwd=get_web_dir(), shell=constants.IS_WINDOWS, ) @@ -933,7 +941,7 @@ def needs_reinit(frontend: bool = True) -> bool: return True # Make sure the .web directory exists in frontend mode. - if not os.path.exists(constants.Dirs.WEB): + if not get_web_dir().exists(): return True # If the template is out of date, then we need to re-init @@ -971,10 +979,10 @@ def is_latest_template() -> bool: Returns: Whether the app is using the latest template. """ - if not os.path.exists(constants.Reflex.JSON): + json_file = get_web_dir() / constants.Reflex.JSON + if not json_file.exists(): return False - with open(constants.Reflex.JSON) as f: # type: ignore - app_version = json.load(f)["version"] + app_version = json.load(json_file.open()).get("version") return app_version == constants.Reflex.VERSION @@ -1170,7 +1178,7 @@ def should_show_rx_chakra_migration_instructions() -> bool: return False existing_init_reflex_version = None - reflex_json = Path(constants.Dirs.REFLEX_JSON) + reflex_json = get_web_dir() / constants.Dirs.REFLEX_JSON if reflex_json.exists(): with reflex_json.open("r") as f: data = json.load(f) diff --git a/reflex/utils/watch.py b/reflex/utils/watch.py index a56635d6e..39b177695 100644 --- a/reflex/utils/watch.py +++ b/reflex/utils/watch.py @@ -9,6 +9,7 @@ from watchdog.events import FileSystemEvent, FileSystemEventHandler from watchdog.observers import Observer from reflex.constants import Dirs +from reflex.utils.prerequisites import get_web_dir class AssetFolderWatch: @@ -90,5 +91,6 @@ class AssetFolderHandler(FileSystemEventHandler): The public file path. """ return src_path.replace( - str(self.root / Dirs.APP_ASSETS), str(self.root / Dirs.WEB_ASSETS) + str(self.root / Dirs.APP_ASSETS), + str(self.root / get_web_dir() / Dirs.PUBLIC), ) diff --git a/tests/test_app.py b/tests/test_app.py index 536b88575..142f0db0b 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1271,7 +1271,7 @@ def compilable_app(tmp_path) -> Generator[tuple[App, Path], None, None]: app_path = tmp_path / "app" web_dir = app_path / ".web" web_dir.mkdir(parents=True) - (web_dir / "package.json").touch() + (web_dir / constants.PackageJson.PATH).touch() app = App(theme=None) app._get_frontend_packages = unittest.mock.Mock() with chdir(app_path): diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 3a9eb17d0..a434779d4 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -1,4 +1,3 @@ -import httpx import pytest from packaging.version import parse as parse_python_version @@ -34,20 +33,23 @@ def test_disable(): @pytest.mark.parametrize("event", ["init", "reinit", "run-dev", "run-prod", "export"]) def test_send(mocker, event): - mocker.patch("httpx.post") - mocker.patch( - "builtins.open", - mocker.mock_open( - read_data='{"project_hash": "78285505863498957834586115958872998605"}' - ), + httpx_post_mock = mocker.patch("httpx.post") + # mocker.patch( + # "builtins.open", + # mocker.mock_open( + # read_data='{"project_hash": "78285505863498957834586115958872998605"}' + # ), + # ) + + # Mock the read_text method of Path + pathlib_path_read_text_mock = mocker.patch( + "pathlib.Path.read_text", + return_value='{"project_hash": "78285505863498957834586115958872998605"}', ) + mocker.patch("platform.platform", return_value="Mocked Platform") telemetry._send(event, telemetry_enabled=True) - httpx.post.assert_called_once() - if telemetry.get_os() == "Windows": - open.assert_called_with(".web\\reflex.json", "r") - elif telemetry.get_os() == "Linux": - open.assert_called_with("/proc/meminfo", "rb", buffering=32768) - else: - open.assert_called_with(".web/reflex.json", "r") + httpx_post_mock.assert_called_once() + + pathlib_path_read_text_mock.assert_called_once()