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.compiler import utils
from reflex.testing import AppHarness, chdir from reflex.testing import AppHarness, chdir
from reflex.utils import build 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): def render_component(num: int):
@ -231,7 +234,7 @@ def test_app_10_compile_time_cold(benchmark, app_with_10_components):
def setup(): def setup():
with chdir(app_with_10_components.app_path): 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() app_with_10_components._initialize_app()
build.setup_frontend(app_with_10_components.app_path) 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(): def setup():
with chdir(app_with_100_components.app_path): 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() app_with_100_components._initialize_app()
build.setup_frontend(app_with_100_components.app_path) 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(): def setup():
with chdir(app_with_1000_components.app_path): 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() app_with_1000_components._initialize_app()
build.setup_frontend(app_with_1000_components.app_path) 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.compiler import utils
from reflex.testing import AppHarness, chdir from reflex.testing import AppHarness, chdir
from reflex.utils import build 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): 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(): def setup():
with chdir(app_with_one_page.app_path): 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() app_with_one_page._initialize_app()
build.setup_frontend(app_with_one_page.app_path) 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(): def setup():
with chdir(app_with_ten_pages.app_path): 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() app_with_ten_pages._initialize_app()
build.setup_frontend(app_with_ten_pages.app_path) 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(): def setup():
with chdir(app_with_hundred_pages.app_path): 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() app_with_hundred_pages._initialize_app()
build.setup_frontend(app_with_hundred_pages.app_path) 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(): def setup():
with chdir(app_with_thousand_pages.app_path): 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() app_with_thousand_pages._initialize_app()
build.setup_frontend(app_with_thousand_pages.app_path) 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(): def setup():
with chdir(app_with_ten_thousand_pages.app_path): 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() app_with_ten_thousand_pages._initialize_app()
build.setup_frontend(app_with_ten_thousand_pages.app_path) build.setup_frontend(app_with_ten_thousand_pages.app_path)

View File

@ -751,10 +751,12 @@ class App(LifespanMixin, Base):
if should_skip_compile(): if should_skip_compile():
return False return False
nocompile = prerequisites.get_web_dir() / constants.NOCOMPILE_FILE
# Check the nocompile file. # Check the nocompile file.
if os.path.exists(constants.NOCOMPILE_FILE): if nocompile.exists():
# Delete the nocompile file # Delete the nocompile file
os.remove(constants.NOCOMPILE_FILE) nocompile.unlink()
return False return False
# By default, compile the app. # 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.style import SYSTEM_COLOR_MODE
from reflex.utils.exec import is_prod_mode from reflex.utils.exec import is_prod_mode
from reflex.utils.imports import ImportVar from reflex.utils.imports import ImportVar
from reflex.utils.prerequisites import get_web_dir
from reflex.vars import Var from reflex.vars import Var
@ -469,7 +470,7 @@ def compile_tailwind(
The compiled Tailwind config. The compiled Tailwind config.
""" """
# Get the path for the output file. # Get the path for the output file.
output_path = constants.Tailwind.CONFIG output_path = get_web_dir() / constants.Tailwind.CONFIG
# Compile the config. # Compile the config.
code = _compile_tailwind(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. The path and code of the compiled postcss.config.js.
""" """
# Get the path for the output file. # Get the path for the output file.
output_path = constants.Dirs.POSTCSS_JS output_path = str(get_web_dir() / constants.Dirs.POSTCSS_JS)
code = [ code = [
line line
@ -502,7 +503,7 @@ def purge_web_pages_dir():
return return
# Empty out the web pages directory. # 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: class ExecutorSafeFunctions:

View File

@ -3,9 +3,12 @@
from __future__ import annotations from __future__ import annotations
import os import os
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Type, Union from typing import Any, Callable, Dict, Optional, Type, Union
from urllib.parse import urlparse from urllib.parse import urlparse
from reflex.utils.prerequisites import get_web_dir
try: try:
from pydantic.v1.fields import ModelField from pydantic.v1.fields import ModelField
except ModuleNotFoundError: except ModuleNotFoundError:
@ -330,7 +333,7 @@ def get_page_path(path: str) -> str:
Returns: Returns:
The path of the compiled JS file. 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: def get_theme_path() -> str:
@ -339,8 +342,10 @@ def get_theme_path() -> str:
Returns: Returns:
The path of the theme style. The path of the theme style.
""" """
return os.path.join( return str(
constants.Dirs.WEB_UTILS, constants.PageNames.THEME + constants.Ext.JS get_web_dir()
/ constants.Dirs.UTILS
/ (constants.PageNames.THEME + constants.Ext.JS)
) )
@ -350,8 +355,10 @@ def get_root_stylesheet_path() -> str:
Returns: Returns:
The path of the app root file. The path of the app root file.
""" """
return os.path.join( return str(
constants.STYLES_DIR, constants.PageNames.STYLESHEET_ROOT + constants.Ext.CSS get_web_dir()
/ constants.Dirs.STYLES
/ (constants.PageNames.STYLESHEET_ROOT + constants.Ext.CSS)
) )
@ -361,9 +368,7 @@ def get_context_path() -> str:
Returns: Returns:
The path of the context module. The path of the context module.
""" """
return os.path.join( return str(get_web_dir() / (constants.Dirs.CONTEXTS_PATH + constants.Ext.JS))
constants.Dirs.WEB, constants.Dirs.CONTEXTS_PATH + constants.Ext.JS
)
def get_components_path() -> str: def get_components_path() -> str:
@ -372,7 +377,11 @@ def get_components_path() -> str:
Returns: Returns:
The path of the compiled components. 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: def get_stateful_components_path() -> str:
@ -381,9 +390,10 @@ def get_stateful_components_path() -> str:
Returns: Returns:
The path of the compiled stateful components. The path of the compiled stateful components.
""" """
return os.path.join( return str(
constants.Dirs.WEB_UTILS, get_web_dir()
constants.PageNames.STATEFUL_COMPONENTS + constants.Ext.JS, / constants.Dirs.UTILS
/ (constants.PageNames.STATEFUL_COMPONENTS + constants.Ext.JS)
) )
@ -437,23 +447,24 @@ def write_page(path: str, code: str):
f.write(code) 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. """Remove all files and folders in a directory except for the keep_files.
Args: Args:
path: The path to the directory that will be emptied path: The path to the directory that will be emptied
keep_files: List of filenames or foldernames that will not be deleted. keep_files: List of filenames or foldernames that will not be deleted.
""" """
path = Path(path)
# If the directory does not exist, return. # If the directory does not exist, return.
if not os.path.exists(path): if not path.exists():
return return
# Remove all files and folders in the directory. # Remove all files and folders in the directory.
keep_files = keep_files or [] keep_files = keep_files or []
directory_contents = os.listdir(path) for element in path.iterdir():
for element in directory_contents: if element.name not in keep_files:
if element not in keep_files: path_ops.rm(element)
path_ops.rm(os.path.join(path, element))
def is_valid_url(url) -> bool: def is_valid_url(url) -> bool:

View File

@ -62,7 +62,7 @@ from .route import (
RouteRegex, RouteRegex,
RouteVar, RouteVar,
) )
from .style import STYLES_DIR, Tailwind from .style import Tailwind
__ALL__ = [ __ALL__ = [
ALEMBIC_CONFIG, ALEMBIC_CONFIG,
@ -113,7 +113,6 @@ __ALL__ = [
SETTER_PREFIX, SETTER_PREFIX,
SKIP_COMPILE_ENV_VAR, SKIP_COMPILE_ENV_VAR,
SocketEvent, SocketEvent,
STYLES_DIR,
Tailwind, Tailwind,
Templates, Templates,
CompileVars, CompileVars,

View File

@ -25,30 +25,26 @@ class Dirs(SimpleNamespace):
EXTERNAL_APP_ASSETS = "external" EXTERNAL_APP_ASSETS = "external"
# The name of the utils file. # The name of the utils file.
UTILS = "utils" 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. # The name of the state file.
STATE_PATH = "/".join([UTILS, "state"]) STATE_PATH = "/".join([UTILS, "state"])
# The name of the components file. # The name of the components file.
COMPONENTS_PATH = "/".join([UTILS, "components"]) COMPONENTS_PATH = "/".join([UTILS, "components"])
# The name of the contexts file. # The name of the contexts file.
CONTEXTS_PATH = "/".join([UTILS, "context"]) CONTEXTS_PATH = "/".join([UTILS, "context"])
# The directory where the app pages are compiled to. # The name of the output static directory.
WEB_PAGES = os.path.join(WEB, "pages") STATIC = "_static"
# The directory where the static build is located. # The name of the public html directory served at "/"
WEB_STATIC = os.path.join(WEB, STATIC) PUBLIC = "public"
# The directory where the utils file is located. # The directory where styles are located.
WEB_UTILS = os.path.join(WEB, UTILS) STYLES = "styles"
# The directory where the assets are located. # The name of the pages directory.
WEB_ASSETS = os.path.join(WEB, PUBLIC) PAGES = "pages"
# The env json file. # The name of the env json file.
ENV_JSON = os.path.join(WEB, "env.json") ENV_JSON = "env.json"
# The reflex json file. # The name of the reflex json file.
REFLEX_JSON = os.path.join(WEB, "reflex.json") REFLEX_JSON = "reflex.json"
# The path to postcss.config.js # The name of the postcss config file.
POSTCSS_JS = os.path.join(WEB, "postcss.config.js") POSTCSS_JS = "postcss.config.js"
class Reflex(SimpleNamespace): class Reflex(SimpleNamespace):
@ -61,7 +57,7 @@ class Reflex(SimpleNamespace):
VERSION = metadata.version(MODULE_NAME) VERSION = metadata.version(MODULE_NAME)
# The reflex json file. # The reflex json file.
JSON = os.path.join(Dirs.WEB, "reflex.json") JSON = "reflex.json"
# Files and directories used to init a new project. # Files and directories used to init a new project.
# The directory to store reflex dependencies. # The directory to store reflex dependencies.
@ -117,7 +113,7 @@ class Next(SimpleNamespace):
# The NextJS config file # The NextJS config file
CONFIG_FILE = "next.config.js" CONFIG_FILE = "next.config.js"
# The sitemap config file. # 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. # The node modules directory.
NODE_MODULES = "node_modules" NODE_MODULES = "node_modules"
# The package lock file. # The package lock file.

View File

@ -12,7 +12,7 @@ from reflex.utils.imports import ImportVar
SETTER_PREFIX = "set_" SETTER_PREFIX = "set_"
# The file used to specify no compilation. # The file used to specify no compilation.
NOCOMPILE_FILE = ".web/nocompile" NOCOMPILE_FILE = "nocompile"
class Ext(SimpleNamespace): class Ext(SimpleNamespace):
@ -80,6 +80,8 @@ class PageNames(SimpleNamespace):
DOCUMENT_ROOT = "_document" DOCUMENT_ROOT = "_document"
# The name of the theme page. # The name of the theme page.
THEME = "theme" THEME = "theme"
# The module containing components.
COMPONENTS = "components"
# The module containing shared stateful components # The module containing shared stateful components
STATEFUL_COMPONENTS = "stateful_components" STATEFUL_COMPONENTS = "stateful_components"

View File

@ -5,7 +5,7 @@ import os
import platform import platform
from types import SimpleNamespace from types import SimpleNamespace
from .base import IS_WINDOWS, Dirs, Reflex from .base import IS_WINDOWS, Reflex
def get_fnm_name() -> str | None: def get_fnm_name() -> str | None:
@ -105,7 +105,7 @@ class PackageJson(SimpleNamespace):
EXPORT_SITEMAP = "next build && next-sitemap" EXPORT_SITEMAP = "next build && next-sitemap"
PROD = "next start" PROD = "next start"
PATH = os.path.join(Dirs.WEB, "package.json") PATH = "package.json"
DEPENDENCIES = { DEPENDENCIES = {
"@emotion/react": "11.11.1", "@emotion/react": "11.11.1",

View File

@ -1,13 +1,6 @@
"""Style constants.""" """Style constants."""
import os
from types import SimpleNamespace 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): class Tailwind(SimpleNamespace):
"""Tailwind constants.""" """Tailwind constants."""
@ -15,8 +8,8 @@ class Tailwind(SimpleNamespace):
# The Tailwindcss version # The Tailwindcss version
VERSION = "tailwindcss@3.3.2" VERSION = "tailwindcss@3.3.2"
# The Tailwind config. # The Tailwind config.
CONFIG = os.path.join(Dirs.WEB, "tailwind.config.js") CONFIG = "tailwind.config.js"
# Default Tailwind content paths # Default Tailwind content paths
CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"] 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" ROOT_STYLE_PATH = "./tailwind.css"

View File

@ -330,7 +330,7 @@ class AppHarness:
# Start the frontend. # Start the frontend.
self.frontend_process = reflex.utils.processes.new_process( self.frontend_process = reflex.utils.processes.new_process(
[reflex.utils.prerequisites.get_package_manager(), "run", "dev"], [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"}, env={"PORT": "0"},
**FRONTEND_POPEN_ARGS, **FRONTEND_POPEN_ARGS,
) )
@ -854,7 +854,11 @@ class AppHarnessProd(AppHarness):
frontend_server: Optional[Subdir404TCPServer] = None frontend_server: Optional[Subdir404TCPServer] = None
def _run_frontend(self): 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 = { error_page_map = {
404: web_root / "404.html", 404: web_root / "404.html",
} }

View File

@ -18,7 +18,7 @@ from reflex.utils import console, path_ops, prerequisites, processes
def set_env_json(): def set_env_json():
"""Write the upload url to a REFLEX_JSON.""" """Write the upload url to a REFLEX_JSON."""
path_ops.update_json_file( 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}, {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) config = json.dumps(config)
with open(constants.Next.SITEMAP_CONFIG_FILE, "w") as f: sitemap = prerequisites.get_web_dir() / constants.Next.SITEMAP_CONFIG_FILE
f.write(templates.SITEMAP_CONFIG(config=config)) sitemap.write_text(templates.SITEMAP_CONFIG(config=config))
def _zip( def _zip(
@ -129,85 +129,89 @@ def _zip(
zipf.write(file, os.path.relpath(file, root_dir)) zipf.write(file, os.path.relpath(file, root_dir))
def export( def zip_app(
backend: bool = True,
frontend: bool = True, frontend: bool = True,
zip: bool = False, backend: bool = True,
zip_dest_dir: str = os.getcwd(), zip_dest_dir: str = os.getcwd(),
deploy_url: str | None = None,
upload_db_file: bool = False, upload_db_file: bool = False,
): ):
"""Export the app for deployment. """Zip up the app.
Args: Args:
backend: Whether to zip up the backend app.
frontend: Whether to zip up the frontend app. frontend: Whether to zip up the frontend app.
zip: Whether to zip the app. backend: Whether to zip up the backend app.
zip_dest_dir: The destination directory for created zip files (if any) zip_dest_dir: The directory to export the zip file to.
deploy_url: The URL of the deployed app. upload_db_file: Whether to upload the database file.
upload_db_file: Whether to include local sqlite db files from the backend zip.
""" """
# Remove the static folder. files_to_exclude = {
path_ops.rm(constants.Dirs.WEB_STATIC) 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. # The export command to run.
command = "export" command = "export"
if frontend: checkpoints = [
checkpoints = [ "Linting and checking ",
"Linting and checking ", "Creating an optimized production build",
"Creating an optimized production build", "Route (pages)",
"Route (pages)", "prerendered as static HTML",
"prerendered as static HTML", "Collecting page data",
"Collecting page data", "Finalizing page optimization",
"Finalizing page optimization", "Collecting build traces",
"Collecting build traces", ]
]
# Generate a sitemap if a deploy URL is provided. # Generate a sitemap if a deploy URL is provided.
if deploy_url is not None: if deploy_url is not None:
generate_sitemap_config(deploy_url, export=zip) generate_sitemap_config(deploy_url, export=for_export)
command = "export-sitemap" command = "export-sitemap"
checkpoints.extend(["Loading next-sitemap", "Generation completed"]) checkpoints.extend(["Loading next-sitemap", "Generation completed"])
# Start the subprocess with the progress bar. # Start the subprocess with the progress bar.
process = processes.new_process( process = processes.new_process(
[prerequisites.get_package_manager(), "run", command], [prerequisites.get_package_manager(), "run", command],
cwd=constants.Dirs.WEB, cwd=wdir,
shell=constants.IS_WINDOWS, shell=constants.IS_WINDOWS,
) )
processes.show_progress("Creating Production Build", process, checkpoints) 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,
)
def setup_frontend( def setup_frontend(
@ -226,7 +230,7 @@ def setup_frontend(
# Copy asset files to public folder. # Copy asset files to public folder.
path_ops.cp( path_ops.cp(
src=str(root / constants.Dirs.APP_ASSETS), 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). # Set the environment variables in client (env.json).
@ -242,7 +246,7 @@ def setup_frontend(
"telemetry", "telemetry",
"disable", "disable",
], ],
cwd=constants.Dirs.WEB, cwd=prerequisites.get_web_dir(),
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
shell=constants.IS_WINDOWS, shell=constants.IS_WINDOWS,
) )
@ -259,7 +263,7 @@ def setup_frontend_prod(
disable_telemetry: Whether to disable the Next telemetry. disable_telemetry: Whether to disable the Next telemetry.
""" """
setup_frontend(root, disable_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: def _looks_like_venv_dir(dir_to_check: str) -> bool:

View File

@ -17,6 +17,7 @@ import psutil
from reflex import constants from reflex import constants
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
from reflex.utils.prerequisites import get_web_dir
from reflex.utils.watch import AssetFolderWatch from reflex.utils.watch import AssetFolderWatch
# For uvicorn windows bug fix (#2335) # 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 from reflex.utils import processes
json_file_path = os.path.join(constants.Dirs.WEB, "package.json") json_file_path = get_web_dir() / constants.PackageJson.PATH
last_hash = detect_package_change(json_file_path) last_hash = detect_package_change(str(json_file_path))
process = None process = None
first_run = True 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 kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
process = processes.new_process( process = processes.new_process(
run_command, run_command,
cwd=constants.Dirs.WEB, cwd=get_web_dir(),
shell=constants.IS_WINDOWS, shell=constants.IS_WINDOWS,
**kwargs, **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 init`\n"
"`REFLEX_USE_NPM=1 reflex run`" "`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: if new_hash != last_hash:
last_hash = new_hash last_hash = new_hash
kill(process.pid) kill(process.pid)
@ -201,10 +202,10 @@ def run_backend(
config = get_config() config = get_config()
app_module = f"reflex.app_module_for_backend:{constants.CompileVars.APP}" 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. # Create a .nocompile file to skip compile for backend.
if os.path.exists(constants.Dirs.WEB): if web_dir.exists():
with open(constants.NOCOMPILE_FILE, "w"): (web_dir / constants.NOCOMPILE_FILE).touch()
pass
# Run the backend in development mode. # Run the backend in development mode.
uvicorn.run( uvicorn.run(
@ -214,7 +215,7 @@ def run_backend(
log_level=loglevel.value, log_level=loglevel.value,
reload=True, reload=True,
reload_dirs=[config.app_name], 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. # Set up .web directory and install frontend dependencies.
build.setup_frontend(Path.cwd()) build.setup_frontend(Path.cwd())
# Export the app. # Build the static app.
build.export( if frontend:
backend=backend, build.build(deploy_url=config.deploy_url, for_export=True)
frontend=frontend,
zip=zipping, # Zip up the app.
zip_dest_dir=zip_dest_dir, if zipping:
deploy_url=config.deploy_url, build.zip_app(
upload_db_file=upload_db_file, frontend=frontend,
) backend=backend,
zip_dest_dir=zip_dest_dir,
upload_db_file=upload_db_file,
)
# Post a telemetry event. # Post a telemetry event.
telemetry.send("export") telemetry.send("export")

View File

@ -14,19 +14,20 @@ from reflex import constants
join = os.linesep.join join = os.linesep.join
def rm(path: str): def rm(path: str | Path):
"""Remove a file or directory. """Remove a file or directory.
Args: Args:
path: The path to the file or directory. path: The path to the file or directory.
""" """
if os.path.isdir(path): path = Path(path)
if path.is_dir():
shutil.rmtree(path) shutil.rmtree(path)
elif os.path.isfile(path): elif path.is_file():
os.remove(path) 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. """Copy a file or directory.
Args: Args:
@ -37,11 +38,12 @@ def cp(src: str, dest: str, overwrite: bool = True) -> bool:
Returns: Returns:
Whether the copy was successful. Whether the copy was successful.
""" """
src, dest = Path(src), Path(dest)
if src == dest: if src == dest:
return False return False
if not overwrite and os.path.exists(dest): if not overwrite and dest.exists():
return False return False
if os.path.isdir(src): if src.is_dir():
rm(dest) rm(dest)
shutil.copytree(src, dest) shutil.copytree(src, dest)
else: else:
@ -49,7 +51,7 @@ def cp(src: str, dest: str, overwrite: bool = True) -> bool:
return True 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. """Move a file or directory.
Args: Args:
@ -60,25 +62,26 @@ def mv(src: str, dest: str, overwrite: bool = True) -> bool:
Returns: Returns:
Whether the move was successful. Whether the move was successful.
""" """
src, dest = Path(src), Path(dest)
if src == dest: if src == dest:
return False return False
if not overwrite and os.path.exists(dest): if not overwrite and dest.exists():
return False return False
rm(dest) rm(dest)
shutil.move(src, dest) shutil.move(src, dest)
return True return True
def mkdir(path: str): def mkdir(path: str | Path):
"""Create a directory. """Create a directory.
Args: Args:
path: The path to the directory. 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. """Create a symbolic link.
Args: Args:
@ -89,19 +92,20 @@ def ln(src: str, dest: str, overwrite: bool = False) -> bool:
Returns: Returns:
Whether the link was successful. Whether the link was successful.
""" """
src, dest = Path(src), Path(dest)
if src == dest: if src == dest:
return False 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 return False
if os.path.isdir(src): if src.is_dir():
rm(dest) rm(dest)
os.symlink(src, dest, target_is_directory=True) src.symlink_to(dest, target_is_directory=True)
else: else:
os.symlink(src, dest) src.symlink_to(dest)
return True return True
def which(program: str) -> str | None: def which(program: str | Path) -> str | Path | None:
"""Find the path to an executable. """Find the path to an executable.
Args: Args:
@ -110,7 +114,7 @@ def which(program: str) -> str | None:
Returns: Returns:
The path to the executable. The path to the executable.
""" """
return shutil.which(program) return shutil.which(str(program))
def get_node_bin_path() -> str | None: def get_node_bin_path() -> str | None:
@ -119,10 +123,11 @@ def get_node_bin_path() -> str | None:
Returns: Returns:
The path to the node bin folder. 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") str_path = which("node")
return str(Path(str_path).parent.resolve()) if str_path else str_path 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: def get_node_path() -> str | None:
@ -131,9 +136,10 @@ def get_node_path() -> str | None:
Returns: Returns:
The path to the node binary file. The path to the node binary file.
""" """
if not os.path.exists(constants.Node.PATH): node_path = Path(constants.Node.PATH)
return which("node") if not node_path.exists():
return constants.Node.PATH return str(which("node"))
return str(node_path)
def get_npm_path() -> str | None: def get_npm_path() -> str | None:
@ -142,12 +148,13 @@ def get_npm_path() -> str | None:
Returns: Returns:
The path to the npm binary file. The path to the npm binary file.
""" """
if not os.path.exists(constants.Node.PATH): npm_path = Path(constants.Node.NPM_PATH)
return which("npm") if not npm_path.exists():
return constants.Node.NPM_PATH 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. """Update the contents of a json file.
Args: 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) 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. """Recursively find and replace text in files in a directory.
Args: Args:
@ -184,11 +191,10 @@ def find_replace(directory: str, find: str, replace: str):
find: The text to find. find: The text to find.
replace: The text to replace. replace: The text to replace.
""" """
directory = Path(directory)
for root, _dirs, files in os.walk(directory): for root, _dirs, files in os.walk(directory):
for file in files: for file in files:
filepath = os.path.join(root, file) filepath = Path(root, file)
with open(filepath, "r", encoding="utf-8") as f: text = filepath.read_text(encoding="utf-8")
text = f.read()
text = re.sub(find, replace, text) text = re.sub(find, replace, text)
with open(filepath, "w") as f: filepath.write_text(text)
f.write(text)

View File

@ -58,6 +58,18 @@ class CpuInfo(Base):
address_width: Optional[int] 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): def check_latest_package_version(package_name: str):
"""Check if the latest version of the package is installed. """Check if the latest version of the package is installed.
@ -91,15 +103,15 @@ def get_or_set_last_reflex_version_check_datetime():
Returns: Returns:
The last version check datetime. 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 return None
# Open and read the file # Open and read the file
with open(constants.Reflex.JSON, "r") as file: data = json.loads(reflex_json_file.read_text())
data: dict = json.load(file)
last_version_check_datetime = data.get("last_version_check_datetime") last_version_check_datetime = data.get("last_version_check_datetime")
if not last_version_check_datetime: if not last_version_check_datetime:
data.update({"last_version_check_datetime": str(datetime.now())}) 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 return last_version_check_datetime
@ -513,12 +525,11 @@ def get_project_hash(raise_on_fail: bool = False) -> int | None:
Returns: Returns:
project_hash: The app hash. 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 return None
# Open and read the file data = json.loads(json_file.read_text())
with open(constants.Reflex.JSON, "r") as file: return data.get("project_hash")
data = json.load(file)
return data.get("project_hash")
def initialize_web_directory(): 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 # 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() 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() initialize_package_json()
path_ops.mkdir(constants.Dirs.WEB_ASSETS) path_ops.mkdir(get_web_dir() / constants.Dirs.PUBLIC)
update_next_config() update_next_config()
@ -555,10 +566,9 @@ def _compile_package_json():
def initialize_package_json(): def initialize_package_json():
"""Render and write in .web the package.json file.""" """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() code = _compile_package_json()
with open(output_path, "w") as file: output_path.write_text(code)
file.write(code)
def init_reflex_json(project_hash: int | None): def init_reflex_json(project_hash: int | None):
@ -583,7 +593,7 @@ def init_reflex_json(project_hash: int | None):
"version": constants.Reflex.VERSION, "version": constants.Reflex.VERSION,
"project_hash": project_hash, "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): 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. export: if the method run during reflex export.
transpile_packages: list of packages to transpile via next.config.js. 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( next_config = _update_next_config(
get_config(), export=export, transpile_packages=transpile_packages get_config(), export=export, transpile_packages=transpile_packages
@ -845,9 +855,7 @@ def cached_procedure(cache_file: str, payload_fn: Callable[..., str]):
@cached_procedure( @cached_procedure(
cache_file=os.path.join( cache_file=str(get_web_dir() / "reflex.install_frontend_packages.cached"),
constants.Dirs.WEB, "reflex.install_frontend_packages.cached"
),
payload_fn=lambda p, c: f"{repr(sorted(list(p)))},{c.json()}", payload_fn=lambda p, c: f"{repr(sorted(list(p)))},{c.json()}",
) )
def install_frontend_packages(packages: set[str], config: Config): 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, fallback=fallback_command,
analytics_enabled=True, analytics_enabled=True,
show_status_message="Installing base frontend packages", show_status_message="Installing base frontend packages",
cwd=constants.Dirs.WEB, cwd=get_web_dir(),
shell=constants.IS_WINDOWS, shell=constants.IS_WINDOWS,
) )
@ -890,7 +898,7 @@ def install_frontend_packages(packages: set[str], config: Config):
fallback=fallback_command, fallback=fallback_command,
analytics_enabled=True, analytics_enabled=True,
show_status_message="Installing tailwind", show_status_message="Installing tailwind",
cwd=constants.Dirs.WEB, cwd=get_web_dir(),
shell=constants.IS_WINDOWS, shell=constants.IS_WINDOWS,
) )
@ -901,7 +909,7 @@ def install_frontend_packages(packages: set[str], config: Config):
fallback=fallback_command, fallback=fallback_command,
analytics_enabled=True, analytics_enabled=True,
show_status_message="Installing frontend packages from config and components", show_status_message="Installing frontend packages from config and components",
cwd=constants.Dirs.WEB, cwd=get_web_dir(),
shell=constants.IS_WINDOWS, shell=constants.IS_WINDOWS,
) )
@ -933,7 +941,7 @@ def needs_reinit(frontend: bool = True) -> bool:
return True return True
# Make sure the .web directory exists in frontend mode. # 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 return True
# If the template is out of date, then we need to re-init # If the template is out of date, then we need to re-init
@ -971,10 +979,10 @@ def is_latest_template() -> bool:
Returns: Returns:
Whether the app is using the latest template. 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 return False
with open(constants.Reflex.JSON) as f: # type: ignore app_version = json.load(json_file.open()).get("version")
app_version = json.load(f)["version"]
return app_version == constants.Reflex.VERSION return app_version == constants.Reflex.VERSION
@ -1170,7 +1178,7 @@ def should_show_rx_chakra_migration_instructions() -> bool:
return False return False
existing_init_reflex_version = None 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(): if reflex_json.exists():
with reflex_json.open("r") as f: with reflex_json.open("r") as f:
data = json.load(f) data = json.load(f)

View File

@ -9,6 +9,7 @@ from watchdog.events import FileSystemEvent, FileSystemEventHandler
from watchdog.observers import Observer from watchdog.observers import Observer
from reflex.constants import Dirs from reflex.constants import Dirs
from reflex.utils.prerequisites import get_web_dir
class AssetFolderWatch: class AssetFolderWatch:
@ -90,5 +91,6 @@ class AssetFolderHandler(FileSystemEventHandler):
The public file path. The public file path.
""" """
return src_path.replace( 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" app_path = tmp_path / "app"
web_dir = app_path / ".web" web_dir = app_path / ".web"
web_dir.mkdir(parents=True) web_dir.mkdir(parents=True)
(web_dir / "package.json").touch() (web_dir / constants.PackageJson.PATH).touch()
app = App(theme=None) app = App(theme=None)
app._get_frontend_packages = unittest.mock.Mock() app._get_frontend_packages = unittest.mock.Mock()
with chdir(app_path): with chdir(app_path):

View File

@ -1,4 +1,3 @@
import httpx
import pytest import pytest
from packaging.version import parse as parse_python_version 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"]) @pytest.mark.parametrize("event", ["init", "reinit", "run-dev", "run-prod", "export"])
def test_send(mocker, event): def test_send(mocker, event):
mocker.patch("httpx.post") httpx_post_mock = mocker.patch("httpx.post")
mocker.patch( # mocker.patch(
"builtins.open", # "builtins.open",
mocker.mock_open( # mocker.mock_open(
read_data='{"project_hash": "78285505863498957834586115958872998605"}' # 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") mocker.patch("platform.platform", return_value="Mocked Platform")
telemetry._send(event, telemetry_enabled=True) telemetry._send(event, telemetry_enabled=True)
httpx.post.assert_called_once() httpx_post_mock.assert_called_once()
if telemetry.get_os() == "Windows":
open.assert_called_with(".web\\reflex.json", "r") pathlib_path_read_text_mock.assert_called_once()
elif telemetry.get_os() == "Linux":
open.assert_called_with("/proc/meminfo", "rb", buffering=32768)
else:
open.assert_called_with(".web/reflex.json", "r")