Skip to content
Merged
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
129 changes: 102 additions & 27 deletions reflex/utils/exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import sys
from collections.abc import Sequence
from pathlib import Path
from typing import NamedTuple, TypedDict
from urllib.parse import urljoin

import psutil
Expand All @@ -27,26 +28,102 @@
frontend_process = None


def detect_package_change(json_file_path: Path) -> str:
"""Calculates the SHA-256 hash of a JSON file and returns it as a hexadecimal string.
def get_package_json_and_hash(package_json_path: Path) -> tuple[PackageJson, str]:
"""Get the content of package.json and its hash.

Args:
json_file_path: The path to the JSON file to be hashed.
package_json_path: The path to the package.json file.

Returns:
str: The SHA-256 hash of the JSON file as a hexadecimal string.

Example:
>>> detect_package_change("package.json")
'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2'
A tuple containing the content of package.json as a dictionary and its SHA-256 hash.
"""
with json_file_path.open("r") as file:
with package_json_path.open("r") as file:
json_data = json.load(file)

# Calculate the hash
json_string = json.dumps(json_data, sort_keys=True)
hash_object = hashlib.sha256(json_string.encode())
return hash_object.hexdigest()
return (json_data, hash_object.hexdigest())


class PackageJson(TypedDict):
"""package.json content."""

dependencies: dict[str, str]
devDependencies: dict[str, str]


class Change(NamedTuple):
"""A named tuple to represent a change in package dependencies."""

added: set[str]
removed: set[str]


def format_change(name: str, change: Change) -> str:
"""Format the change for display.

Args:
name: The name of the change (e.g., "dependencies" or "devDependencies").
change: The Change named tuple containing added and removed packages.

Returns:
A formatted string representing the changes.
"""
if not change.added and not change.removed:
return ""
added_str = ", ".join(sorted(change.added))
removed_str = ", ".join(sorted(change.removed))
change_str = f"{name}:\n"
if change.added:
change_str += f" Added: {added_str}\n"
if change.removed:
change_str += f" Removed: {removed_str}\n"
return change_str.strip()


def get_different_packages(
old_package_json_content: PackageJson,
new_package_json_content: PackageJson,
) -> tuple[Change, Change]:
"""Get the packages that are different between two package JSON contents.

Args:
old_package_json_content: The content of the old package JSON.
new_package_json_content: The content of the new package JSON.

Returns:
A tuple containing two `Change` named tuples:
- The first `Change` contains the changes in the `dependencies` section.
- The second `Change` contains the changes in the `devDependencies` section.
"""

def get_changes(old: dict[str, str], new: dict[str, str]) -> Change:
"""Get the changes between two dictionaries.

Args:
old: The old dictionary of packages.
new: The new dictionary of packages.

Returns:
A `Change` named tuple containing the added and removed packages.
"""
old_keys = set(old.keys())
new_keys = set(new.keys())
added = new_keys - old_keys
removed = old_keys - new_keys
return Change(added=added, removed=removed)

dependencies_change = get_changes(
old_package_json_content.get("dependencies", {}),
new_package_json_content.get("dependencies", {}),
)
dev_dependencies_change = get_changes(
old_package_json_content.get("devDependencies", {}),
new_package_json_content.get("devDependencies", {}),
)

return dependencies_change, dev_dependencies_change
Comment on lines +117 to +126
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dependencies_change = get_changes(
old_package_json_content.get("dependencies", {}),
new_package_json_content.get("dependencies", {}),
)
dev_dependencies_change = get_changes(
old_package_json_content.get("devDependencies", {}),
new_package_json_content.get("devDependencies", {}),
)
return dependencies_change, dev_dependencies_change
return tuple(
get_changes(
old_package_json_content.get(deps_key, {}),
new_package_json_content.get(deps_key, {}),
)
for deps_key in PackageJson.__annotations__
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this makes the typing looser, so i would prefer to keep explicit for now

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense for the explicit.

Are the intermediary variable just there for easier reading ? because we could do explicit in the return expression right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just there for easier reading

yea it's also to declutter a long return line



def kill(proc_pid: int):
Expand Down Expand Up @@ -86,7 +163,7 @@ def run_process_and_launch_url(
from reflex.utils import processes

json_file_path = get_web_dir() / constants.PackageJson.PATH
last_hash = detect_package_change(json_file_path)
last_content, last_hash = get_package_json_and_hash(json_file_path)
process = None
first_run = True

Expand All @@ -105,6 +182,18 @@ def run_process_and_launch_url(
frontend_process = process
if process.stdout:
for line in processes.stream_logs("Starting frontend", process):
new_content, new_hash = get_package_json_and_hash(json_file_path)
if new_hash != last_hash:
dependencies_change, dev_dependencies_change = (
get_different_packages(last_content, new_content)
)
last_content, last_hash = new_content, new_hash
console.info(
"Detected changes in package.json.\n"
+ format_change("Dependencies", dependencies_change)
+ format_change("Dev Dependencies", dev_dependencies_change)
)

match = re.search(constants.Next.FRONTEND_LISTENING_REGEX, line)
if match:
if first_run:
Expand All @@ -119,22 +208,8 @@ def run_process_and_launch_url(
notify_backend()
first_run = False
else:
console.print("New packages detected: Updating app...")
else:
if any(
x in line for x in ("bin executable does not exist on disk",)
):
console.error(
"Try setting `REFLEX_USE_NPM=1` and re-running `reflex init` and `reflex run` to use npm instead of bun:\n"
"`REFLEX_USE_NPM=1 reflex init`\n"
"`REFLEX_USE_NPM=1 reflex run`"
)
new_hash = detect_package_change(json_file_path)
if new_hash != last_hash:
last_hash = new_hash
kill(process.pid)
process = None
break # for line in process.stdout
console.print("Frontend is restarting...")

if process is not None:
break # while True

Expand Down