"""Building the app and initializing all prerequisites.""" from __future__ import annotations import os import zipfile from pathlib import Path, PosixPath from reflex_base import constants from reflex_base.config import get_config from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from reflex.utils import console, js_runtimes, path_ops, prerequisites, processes from reflex.utils.exec import is_in_app_harness def set_env_json(): """Write the upload url to a REFLEX_JSON.""" path_ops.update_json_file( str(prerequisites.get_web_dir() / constants.Dirs.ENV_JSON), { **{endpoint.name: endpoint.get_url() for endpoint in constants.Endpoint}, "TRANSPORT": get_config().transport, "TEST_MODE": is_in_app_harness(), }, ) def _zip( *, component_name: constants.ComponentName, target: Path, root_directory: Path, exclude_venv_directories: bool, include_db_file: bool = False, directory_names_to_exclude: set[str] | None = None, files_to_exclude: set[Path] | None = None, globs_to_include: list[str] | None = None, ) -> None: """Zip utility function. Args: component_name: The name of the component: backend or frontend. target: The target zip file. root_directory: The root directory to zip. exclude_venv_directories: Whether to exclude venv directories. include_db_file: Whether to include local sqlite db files. directory_names_to_exclude: The directory names to exclude. files_to_exclude: The files to exclude. globs_to_include: Apply these globs from the root_directory and always include them in the zip. """ target = Path(target) root_directory = Path(root_directory).resolve() directory_names_to_exclude = directory_names_to_exclude or set() files_to_exclude = files_to_exclude or set() files_to_zip: list[Path] = [] # Traverse the root directory in a top-down manner. In this traversal order, # we can modify the dirs list in-place to remove directories we don't want to include. for directory_path, subdirectories_names, subfiles_names in os.walk( root_directory, topdown=True, followlinks=True ): directory_path = Path(directory_path).resolve() # Modify the dirs in-place so excluded and hidden directories are skipped in next traversal. subdirectories_names[:] = [ subdirectory_name for subdirectory_name in subdirectories_names if subdirectory_name not in directory_names_to_exclude and not any( (directory_path / subdirectory_name).samefile(exclude) for exclude in files_to_exclude if exclude.exists() ) and not subdirectory_name.startswith(".") and ( not exclude_venv_directories or not _looks_like_venv_directory(directory_path / subdirectory_name) ) ] # Modify the files in-place so the hidden files and db files are excluded. subfiles_names[:] = [ subfile_name for subfile_name in subfiles_names if not subfile_name.startswith(".") and (include_db_file or not subfile_name.endswith(".db")) ] files_to_zip += [ directory_path / subfile_name for subfile_name in subfiles_names if not any( (directory_path / subfile_name).samefile(excluded_file) for excluded_file in files_to_exclude if excluded_file.exists() ) ] if globs_to_include: for glob in globs_to_include: files_to_zip += [ file for file in root_directory.glob(glob) if file.name not in files_to_exclude ] # Create a progress bar for zipping the component. progress = Progress( *Progress.get_default_columns()[:-1], MofNCompleteColumn(), TimeElapsedColumn(), ) task = progress.add_task( f"Zipping {component_name.value}:", total=len(files_to_zip) ) with progress, zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipf: for file in files_to_zip: console.debug(f"{target}: {file}", progress=progress) progress.advance(task) zipf.write(file, Path(file).relative_to(root_directory)) def zip_app( frontend: bool = True, backend: bool = True, zip_dest_dir: str | Path | None = None, include_db_file: bool = False, backend_excluded_dirs: tuple[Path, ...] = (), ): """Zip up the app. Args: frontend: Whether to zip up the frontend app. backend: Whether to zip up the backend app. zip_dest_dir: The directory to export the zip file to. include_db_file: Whether to include the database file. backend_excluded_dirs: A tuple of files or directories to exclude from the backend zip. Defaults to (). """ zip_dest_dir = zip_dest_dir or Path.cwd() zip_dest_dir = Path(zip_dest_dir) files_to_exclude = { Path(constants.ComponentName.FRONTEND.zip()).resolve(), Path(constants.ComponentName.BACKEND.zip()).resolve(), } if frontend: _zip( component_name=constants.ComponentName.FRONTEND, target=zip_dest_dir / constants.ComponentName.FRONTEND.zip(), root_directory=prerequisites.get_web_dir() / constants.Dirs.STATIC, files_to_exclude=files_to_exclude, exclude_venv_directories=False, ) if backend: _zip( component_name=constants.ComponentName.BACKEND, target=zip_dest_dir / constants.ComponentName.BACKEND.zip(), root_directory=Path.cwd(), directory_names_to_exclude={"__pycache__"}, files_to_exclude=files_to_exclude | set(backend_excluded_dirs), exclude_venv_directories=True, include_db_file=include_db_file, globs_to_include=[ str(Path(constants.Dirs.WEB) / constants.Dirs.BACKEND / "*") ], ) def _duplicate_index_html_to_parent_directory(directory: Path): """Duplicate index.html in the child directories to the given directory. This makes accessing /route and /route/ work in production. Args: directory: The directory to duplicate index.html to. """ for child in directory.iterdir(): if child.is_dir(): # If the child directory has an index.html, copy it to the parent directory. index_html = child / "index.html" if index_html.exists(): target = directory / (child.name + ".html") if not target.exists(): console.debug(f"Copying {index_html} to {target}") path_ops.cp(index_html, target) else: console.debug(f"Skipping {index_html}, already exists at {target}") # Recursively call this function for the child directory. _duplicate_index_html_to_parent_directory(child) def build(): """Build the app for deployment. Raises: SystemExit: If the build process fails. """ wdir = prerequisites.get_web_dir() # Clean the static directory if it exists. path_ops.rm(str(wdir / constants.Dirs.BUILD_DIR)) checkpoints = [ "building client environment for production...", "modules transformed", "building ssr environment for production...", "built in", ] # Start the subprocess with the progress bar. process = processes.new_process( [ *js_runtimes.get_js_package_executor(raise_on_none=True)[0], "run", "export", ], cwd=wdir, shell=constants.IS_WINDOWS, env={ **os.environ, "NO_COLOR": "1", }, ) processes.show_progress("Creating Production Build", process, checkpoints) process.wait() if process.returncode != 0: console.error( "Failed to build the frontend. Please run with --loglevel debug for more information.", ) raise SystemExit(1) _duplicate_index_html_to_parent_directory(wdir / constants.Dirs.STATIC) spa_fallback = wdir / constants.Dirs.STATIC / constants.ReactRouter.SPA_FALLBACK if not spa_fallback.exists(): spa_fallback = wdir / constants.Dirs.STATIC / "index.html" if spa_fallback.exists(): path_ops.cp( spa_fallback, wdir / constants.Dirs.STATIC / "404.html", ) config = get_config() if frontend_path := config.frontend_path.strip("/"): # Create a subdirectory that matches the configured frontend_path. frontend_path = PosixPath(frontend_path) first_part = frontend_path.parts[0] for child in list((wdir / constants.Dirs.STATIC).iterdir()): if child.is_dir() and child.name == first_part: continue path_ops.mv( child, wdir / constants.Dirs.STATIC / frontend_path / child.name, ) def setup_frontend( root: Path, ): """Set up the frontend to run the app. Args: root: The root path of the project. """ # Set the environment variables in client (env.json). set_env_json() # update the last reflex run time. prerequisites.set_last_reflex_run_time() def setup_frontend_prod( root: Path, ): """Set up the frontend for prod mode. Args: root: The root path of the project. """ setup_frontend(root) build() def _looks_like_venv_directory(directory_to_check: str | Path) -> bool: directory_to_check = Path(directory_to_check) return (directory_to_check / "pyvenv.cfg").exists()