Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/getting_started/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ This is where the compiled Javascript files will be stored. You will never need

Each Reflex page will compile to a corresponding `.js` file in the `.web/pages` directory.

If Reflex installs frontend dependencies with Bun, the canonical `bun.lock` lives in your project root and should be committed to version control. Reflex mirrors it into `.web` when it needs to run the package manager.

## Assets

The `assets` directory is where you can store any static assets you want to be publicly available. This includes images, fonts, and other files.
Expand Down
3 changes: 3 additions & 0 deletions packages/reflex-base/src/reflex_base/constants/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class Bun(SimpleNamespace):
# Path of the bunfig file
CONFIG_PATH = "bunfig.toml"

# Path of the bun lockfile.
LOCKFILE_PATH = "bun.lock"

@classproperty
@classmethod
def ROOT_PATH(cls):
Expand Down
52 changes: 52 additions & 0 deletions reflex/utils/frontend_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,55 @@ def initialize_requirements_txt(
return False


def get_root_bun_lock_path() -> Path:
"""Get the canonical bun lock path in the app root.

This assumes the current working directory is the Reflex app root.

Returns:
The canonical bun lock path in the app root.
"""
return Path.cwd() / constants.Bun.LOCKFILE_PATH


def get_web_bun_lock_path() -> Path:
"""Get the mirrored bun lock path in the .web directory.

Returns:
The mirrored bun lock path in the .web directory.
"""
return get_web_dir() / constants.Bun.LOCKFILE_PATH


def sync_root_bun_lock_to_web():
"""Mirror the canonical root bun.lock into .web.

If the root lockfile is absent, remove any stale mirrored copy from .web.
"""
root_bun_lock_path = get_root_bun_lock_path()
web_bun_lock_path = get_web_bun_lock_path()

if not root_bun_lock_path.exists():
if web_bun_lock_path.exists():
console.debug(f"Removing stale {web_bun_lock_path}")
path_ops.rm(web_bun_lock_path)
return

console.debug(f"Copying {root_bun_lock_path} to {web_bun_lock_path}")
path_ops.cp(root_bun_lock_path, web_bun_lock_path)


def sync_web_bun_lock_to_root():
"""Persist the mirrored .web bun.lock back to the app root."""
web_bun_lock_path = get_web_bun_lock_path()
if not web_bun_lock_path.exists():
return

root_bun_lock_path = get_root_bun_lock_path()
console.debug(f"Copying {web_bun_lock_path} to {root_bun_lock_path}")
path_ops.cp(web_bun_lock_path, root_bun_lock_path)


def initialize_web_directory():
"""Initialize the web directory on reflex init."""
console.log("Initializing the web directory.")
Expand All @@ -158,6 +207,9 @@ def initialize_web_directory():
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("Restoring the bun lock file.")
sync_root_bun_lock_to_web()

console.debug("Initializing the web directory.")
initialize_package_json()

Expand Down
61 changes: 53 additions & 8 deletions reflex/utils/js_runtimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from reflex_base.utils.decorator import cached_procedure, once
from reflex_base.utils.exceptions import SystemPackageMissingError

from reflex.utils import console, net, path_ops, processes
from reflex.utils import console, frontend_skeleton, net, path_ops, processes
from reflex.utils.prerequisites import get_web_dir, windows_check_onedrive_in_path


Expand Down Expand Up @@ -353,24 +353,59 @@ def remove_existing_bun_installation():
path_ops.rm(constants.Bun.ROOT_PATH)


def _frontend_packages_cache_path() -> Path:
"""Get the cache file path for frontend package installs.

Returns:
The cache file path for frontend package installs.
"""
return get_web_dir() / "reflex.install_frontend_packages.cached"


def _sync_root_bun_lock_for_frontend_install():
"""Sync the canonical bun.lock into .web and invalidate the install cache when needed."""
root_bun_lock_path = frontend_skeleton.get_root_bun_lock_path()
web_bun_lock_path = frontend_skeleton.get_web_bun_lock_path()
cache_file = _frontend_packages_cache_path()

if not root_bun_lock_path.exists():
if web_bun_lock_path.exists():
frontend_skeleton.sync_root_bun_lock_to_web()
if cache_file.exists():
path_ops.rm(cache_file)
return

if not web_bun_lock_path.exists():
frontend_skeleton.sync_root_bun_lock_to_web()
return

if web_bun_lock_path.read_bytes() != root_bun_lock_path.read_bytes():
frontend_skeleton.sync_root_bun_lock_to_web()
if cache_file.exists():
path_ops.rm(cache_file)


@cached_procedure(
cache_file_path=lambda: get_web_dir() / "reflex.install_frontend_packages.cached",
payload_fn=lambda packages, config: f"{sorted(packages)!r},{config.json()}",
cache_file_path=_frontend_packages_cache_path,
payload_fn=lambda packages, config, install_package_managers: (
f"{sorted(packages)!r},{config.json()},{list(install_package_managers)!r}"
),
)
def install_frontend_packages(packages: set[str], config: Config):
def _install_frontend_packages(
packages: set[str],
config: Config,
install_package_managers: Sequence[str],
):
"""Installs the base and custom frontend packages.

Args:
packages: A list of package names to be installed.
config: The config object.
install_package_managers: The package managers available for install.

Example:
>>> install_frontend_packages(["react", "react-dom"], get_config())
"""
install_package_managers = get_nodejs_compatible_package_managers(
raise_on_none=True
)

env = (
{
"NODE_TLS_REJECT_UNAUTHORIZED": "0",
Expand Down Expand Up @@ -419,3 +454,13 @@ def install_frontend_packages(packages: set[str], config: Config):
[primary_package_manager, "add", "--legacy-peer-deps", *packages],
show_status_message="Installing frontend packages from config and components",
)


def install_frontend_packages(packages: set[str], config: Config):
"""Install frontend packages while respecting the canonical root bun.lock."""
install_package_managers = tuple(
get_nodejs_compatible_package_managers(raise_on_none=True)
)
_sync_root_bun_lock_for_frontend_install()
_install_frontend_packages(set(packages), config, install_package_managers)
frontend_skeleton.sync_web_bun_lock_to_root()
Loading
Loading