Make .web configurable with REFLEX_WEB_WORKDIR (#3462)

This commit is contained in:
Thomas Brandého 2024-06-17 22:17:00 +02:00 committed by GitHub
parent 6fdc5a84db
commit ed05f57fc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 270 additions and 230 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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:

View File

@ -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:

View File

@ -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,

View File

@ -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.

View File

@ -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"

View File

@ -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",

View File

@ -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"

View File

@ -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",
}

View File

@ -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:

View File

@ -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)],
)

View File

@ -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")

View File

@ -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)

View File

@ -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)

View File

@ -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),
)

View File

@ -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):

View File

@ -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()