309 lines
10 KiB
Python
309 lines
10 KiB
Python
"""This module provides utility functions to initialize the frontend skeleton."""
|
|
|
|
import json
|
|
import random
|
|
from pathlib import Path
|
|
|
|
from reflex_base import constants
|
|
from reflex_base.config import Config, get_config
|
|
from reflex_base.environment import environment
|
|
|
|
from reflex.compiler import templates
|
|
from reflex.utils import console, path_ops
|
|
from reflex.utils.prerequisites import get_project_hash, get_web_dir
|
|
from reflex.utils.registry import get_npm_registry
|
|
|
|
|
|
def initialize_gitignore(
|
|
gitignore_file: Path = constants.GitIgnore.FILE,
|
|
files_to_ignore: set[str] | list[str] = constants.GitIgnore.DEFAULTS,
|
|
):
|
|
"""Initialize the template .gitignore file.
|
|
|
|
Args:
|
|
gitignore_file: The .gitignore file to create.
|
|
files_to_ignore: The files to add to the .gitignore file.
|
|
"""
|
|
# Combine with the current ignored files.
|
|
current_ignore: list[str] = []
|
|
if gitignore_file.exists():
|
|
current_ignore = [ln.strip() for ln in gitignore_file.read_text().splitlines()]
|
|
|
|
if files_to_ignore == current_ignore:
|
|
console.debug(f"{gitignore_file} already up to date.")
|
|
return
|
|
files_to_ignore = [ln for ln in files_to_ignore if ln not in current_ignore]
|
|
files_to_ignore += current_ignore
|
|
|
|
# Write files to the .gitignore file.
|
|
gitignore_file.touch(exist_ok=True)
|
|
console.debug(f"Creating {gitignore_file}")
|
|
gitignore_file.write_text("\n".join(files_to_ignore) + "\n")
|
|
|
|
|
|
def _read_dependency_file(file_path: Path) -> tuple[str | None, str | None]:
|
|
"""Read a dependency file with a forgiving encoding strategy.
|
|
|
|
Args:
|
|
file_path: The file to read.
|
|
|
|
Returns:
|
|
A tuple of file content and the encoding used to read it.
|
|
"""
|
|
try:
|
|
return file_path.read_text(), None
|
|
except UnicodeDecodeError:
|
|
pass
|
|
except Exception as e:
|
|
console.error(f"Failed to read {file_path} due to {e}.")
|
|
raise SystemExit(1) from None
|
|
|
|
try:
|
|
return file_path.read_text(encoding="utf-8"), "utf-8"
|
|
except UnicodeDecodeError:
|
|
return None, None
|
|
except Exception as e:
|
|
console.error(f"Failed to read {file_path} due to {e}.")
|
|
raise SystemExit(1) from None
|
|
|
|
|
|
def _has_reflex_requirement_line(requirements_text: str) -> bool:
|
|
"""Check whether requirements.txt already contains reflex.
|
|
|
|
Returns:
|
|
Whether reflex is already present in the requirements text.
|
|
"""
|
|
return any(
|
|
_is_reflex_dependency_spec(line) for line in requirements_text.splitlines()
|
|
)
|
|
|
|
|
|
def _is_reflex_dependency_spec(requirement: str) -> bool:
|
|
"""Check whether a dependency specification refers to the reflex package.
|
|
|
|
Args:
|
|
requirement: The dependency specification to check.
|
|
|
|
Returns:
|
|
Whether the specification refers to the reflex package.
|
|
"""
|
|
requirement = requirement.strip()
|
|
if not requirement.lower().startswith("reflex"):
|
|
return False
|
|
|
|
suffix = requirement[len("reflex") :]
|
|
if suffix.startswith("["):
|
|
extras_end = suffix.find("]")
|
|
if extras_end == -1:
|
|
return False
|
|
suffix = suffix[extras_end + 1 :]
|
|
|
|
return not suffix or suffix.lstrip().startswith((
|
|
"==",
|
|
"!=",
|
|
">=",
|
|
"<=",
|
|
"~=",
|
|
">",
|
|
"<",
|
|
";",
|
|
"@",
|
|
))
|
|
|
|
|
|
def initialize_requirements_txt(
|
|
requirements_file_path: Path = Path(constants.RequirementsTxt.FILE),
|
|
pyproject_file_path: Path = Path(constants.PyprojectToml.FILE),
|
|
) -> bool:
|
|
"""Initialize the requirements.txt file.
|
|
|
|
If a project already uses pyproject.toml, leave dependency management to the
|
|
package manager. Otherwise ensure requirements.txt pins the current Reflex
|
|
version for legacy workflows.
|
|
|
|
Returns:
|
|
True if the user has to update the requirements.txt file.
|
|
"""
|
|
if not requirements_file_path.exists() and pyproject_file_path.exists():
|
|
return False
|
|
|
|
requirements_file_path.touch(exist_ok=True)
|
|
|
|
content, encoding = _read_dependency_file(requirements_file_path)
|
|
if content is None:
|
|
return True
|
|
|
|
if _has_reflex_requirement_line(content):
|
|
console.debug(f"{requirements_file_path} already has reflex as dependency.")
|
|
return False
|
|
|
|
console.debug(
|
|
f"Appending {constants.RequirementsTxt.DEFAULTS_STUB} to {requirements_file_path}"
|
|
)
|
|
with requirements_file_path.open("a", encoding=encoding) as f:
|
|
f.write(
|
|
"\n" + constants.RequirementsTxt.DEFAULTS_STUB + constants.Reflex.VERSION
|
|
)
|
|
|
|
return False
|
|
|
|
|
|
def initialize_web_directory():
|
|
"""Initialize the web directory on reflex init."""
|
|
console.log("Initializing the web directory.")
|
|
|
|
# Reuse the hash if one is already created, so we don't over-write it when running reflex init
|
|
project_hash = get_project_hash()
|
|
|
|
console.debug(f"Copying {constants.Templates.Dirs.WEB_TEMPLATE} to {get_web_dir()}")
|
|
path_ops.copy_tree(constants.Templates.Dirs.WEB_TEMPLATE, str(get_web_dir()))
|
|
|
|
console.debug("Initializing the web directory.")
|
|
initialize_package_json()
|
|
|
|
console.debug("Initializing the bun config file.")
|
|
initialize_bun_config()
|
|
|
|
console.debug("Initializing the .npmrc file.")
|
|
initialize_npmrc()
|
|
|
|
console.debug("Initializing the public directory.")
|
|
path_ops.mkdir(get_web_dir() / constants.Dirs.PUBLIC)
|
|
|
|
console.debug("Initializing the react-router.config.js file.")
|
|
update_react_router_config()
|
|
|
|
console.debug("Initializing the vite.config.js file.")
|
|
initialize_vite_config()
|
|
|
|
console.debug("Initializing the reflex.json file.")
|
|
# Initialize the reflex json file.
|
|
init_reflex_json(project_hash=project_hash)
|
|
|
|
|
|
def update_react_router_config(prerender_routes: bool = False):
|
|
"""Update react-router.config.js config from Reflex config.
|
|
|
|
Args:
|
|
prerender_routes: Whether to enable prerendering of routes.
|
|
"""
|
|
react_router_config_file_path = get_web_dir() / constants.ReactRouter.CONFIG_FILE
|
|
|
|
new_react_router_config = _update_react_router_config(
|
|
get_config(), prerender_routes=prerender_routes
|
|
)
|
|
|
|
# Overwriting the config file triggers a full server reload, so make sure
|
|
# there is actually a diff.
|
|
old_react_router_config = (
|
|
react_router_config_file_path.read_text()
|
|
if react_router_config_file_path.exists()
|
|
else ""
|
|
)
|
|
if old_react_router_config != new_react_router_config:
|
|
react_router_config_file_path.write_text(new_react_router_config)
|
|
|
|
|
|
def _update_react_router_config(config: Config, prerender_routes: bool = False):
|
|
react_router_config = {
|
|
"basename": config.prepend_frontend_path("/"),
|
|
"future": {
|
|
"unstable_optimizeDeps": True,
|
|
},
|
|
"ssr": False,
|
|
}
|
|
|
|
if prerender_routes:
|
|
react_router_config["prerender"] = True
|
|
react_router_config["build"] = constants.Dirs.BUILD_DIR
|
|
|
|
return f"export default {json.dumps(react_router_config)};"
|
|
|
|
|
|
def _compile_package_json():
|
|
return templates.package_json_template(
|
|
scripts={
|
|
"dev": constants.PackageJson.Commands.DEV,
|
|
"export": constants.PackageJson.Commands.EXPORT,
|
|
},
|
|
dependencies=constants.PackageJson.DEPENDENCIES,
|
|
dev_dependencies=constants.PackageJson.DEV_DEPENDENCIES,
|
|
overrides=constants.PackageJson.OVERRIDES,
|
|
)
|
|
|
|
|
|
def initialize_package_json():
|
|
"""Render and write in .web the package.json file."""
|
|
output_path = get_web_dir() / constants.PackageJson.PATH
|
|
output_path.write_text(_compile_package_json())
|
|
|
|
|
|
def _compile_vite_config(config: Config):
|
|
# base must have exactly one trailing slash
|
|
return templates.vite_config_template(
|
|
base=config.prepend_frontend_path("/"),
|
|
hmr=environment.VITE_HMR.get(),
|
|
force_full_reload=environment.VITE_FORCE_FULL_RELOAD.get(),
|
|
experimental_hmr=environment.VITE_EXPERIMENTAL_HMR.get(),
|
|
sourcemap=environment.VITE_SOURCEMAP.get(),
|
|
allowed_hosts=config.vite_allowed_hosts,
|
|
)
|
|
|
|
|
|
def initialize_vite_config():
|
|
"""Render and write in .web the vite.config.js file using Reflex config."""
|
|
vite_config_file_path = get_web_dir() / constants.ReactRouter.VITE_CONFIG_FILE
|
|
vite_config_file_path.write_text(_compile_vite_config(get_config()))
|
|
|
|
|
|
def initialize_bun_config():
|
|
"""Initialize the bun config file."""
|
|
bun_config_path = get_web_dir() / constants.Bun.CONFIG_PATH
|
|
|
|
if (custom_bunfig := Path(constants.Bun.CONFIG_PATH)).exists():
|
|
bunfig_content = custom_bunfig.read_text()
|
|
console.info(f"Copying custom bunfig.toml inside {get_web_dir()} folder")
|
|
else:
|
|
best_registry = get_npm_registry()
|
|
bunfig_content = constants.Bun.DEFAULT_CONFIG.format(registry=best_registry)
|
|
|
|
bun_config_path.write_text(bunfig_content)
|
|
|
|
|
|
def initialize_npmrc():
|
|
"""Initialize the .npmrc file."""
|
|
npmrc_path = get_web_dir() / constants.Node.CONFIG_PATH
|
|
|
|
if (custom_npmrc := Path(constants.Node.CONFIG_PATH)).exists():
|
|
npmrc_content = custom_npmrc.read_text()
|
|
console.info(f"Copying custom .npmrc inside {get_web_dir()} folder")
|
|
else:
|
|
best_registry = get_npm_registry()
|
|
npmrc_content = constants.Node.DEFAULT_CONFIG.format(registry=best_registry)
|
|
|
|
npmrc_path.write_text(npmrc_content)
|
|
|
|
|
|
def init_reflex_json(project_hash: int | None):
|
|
"""Write the hash of the Reflex project to a REFLEX_JSON.
|
|
|
|
Reuse the hash if one is already created, therefore do not
|
|
overwrite it every time we run the reflex init command
|
|
.
|
|
|
|
Args:
|
|
project_hash: The app hash.
|
|
"""
|
|
if project_hash is not None:
|
|
console.debug(f"Project hash is already set to {project_hash}.")
|
|
else:
|
|
# Get a random project hash.
|
|
project_hash = random.getrandbits(128)
|
|
console.debug(f"Setting project hash to {project_hash}.")
|
|
|
|
# Write the hash and version to the reflex json file.
|
|
reflex_json = {
|
|
"version": constants.Reflex.VERSION,
|
|
"project_hash": project_hash,
|
|
}
|
|
path_ops.update_json_file(get_web_dir() / constants.Reflex.JSON, reflex_json)
|