1212import sys
1313from collections .abc import Sequence
1414from pathlib import Path
15+ from typing import NamedTuple , TypedDict
1516from urllib .parse import urljoin
1617
1718import psutil
2728frontend_process = None
2829
2930
30- def detect_package_change ( json_file_path : Path ) -> str :
31- """Calculates the SHA-256 hash of a JSON file and returns it as a hexadecimal string .
31+ def get_package_json_and_hash ( package_json_path : Path ) -> tuple [ PackageJson , str ] :
32+ """Get the content of package.json and its hash .
3233
3334 Args:
34- json_file_path : The path to the JSON file to be hashed .
35+ package_json_path : The path to the package.json file.
3536
3637 Returns:
37- str: The SHA-256 hash of the JSON file as a hexadecimal string.
38-
39- Example:
40- >>> detect_package_change("package.json")
41- 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2'
38+ A tuple containing the content of package.json as a dictionary and its SHA-256 hash.
4239 """
43- with json_file_path .open ("r" ) as file :
40+ with package_json_path .open ("r" ) as file :
4441 json_data = json .load (file )
4542
4643 # Calculate the hash
4744 json_string = json .dumps (json_data , sort_keys = True )
4845 hash_object = hashlib .sha256 (json_string .encode ())
49- return hash_object .hexdigest ()
46+ return (json_data , hash_object .hexdigest ())
47+
48+
49+ class PackageJson (TypedDict ):
50+ """package.json content."""
51+
52+ dependencies : dict [str , str ]
53+ devDependencies : dict [str , str ]
54+
55+
56+ class Change (NamedTuple ):
57+ """A named tuple to represent a change in package dependencies."""
58+
59+ added : set [str ]
60+ removed : set [str ]
61+
62+
63+ def format_change (name : str , change : Change ) -> str :
64+ """Format the change for display.
65+
66+ Args:
67+ name: The name of the change (e.g., "dependencies" or "devDependencies").
68+ change: The Change named tuple containing added and removed packages.
69+
70+ Returns:
71+ A formatted string representing the changes.
72+ """
73+ if not change .added and not change .removed :
74+ return ""
75+ added_str = ", " .join (sorted (change .added ))
76+ removed_str = ", " .join (sorted (change .removed ))
77+ change_str = f"{ name } :\n "
78+ if change .added :
79+ change_str += f" Added: { added_str } \n "
80+ if change .removed :
81+ change_str += f" Removed: { removed_str } \n "
82+ return change_str .strip ()
83+
84+
85+ def get_different_packages (
86+ old_package_json_content : PackageJson ,
87+ new_package_json_content : PackageJson ,
88+ ) -> tuple [Change , Change ]:
89+ """Get the packages that are different between two package JSON contents.
90+
91+ Args:
92+ old_package_json_content: The content of the old package JSON.
93+ new_package_json_content: The content of the new package JSON.
94+
95+ Returns:
96+ A tuple containing two `Change` named tuples:
97+ - The first `Change` contains the changes in the `dependencies` section.
98+ - The second `Change` contains the changes in the `devDependencies` section.
99+ """
100+
101+ def get_changes (old : dict [str , str ], new : dict [str , str ]) -> Change :
102+ """Get the changes between two dictionaries.
103+
104+ Args:
105+ old: The old dictionary of packages.
106+ new: The new dictionary of packages.
107+
108+ Returns:
109+ A `Change` named tuple containing the added and removed packages.
110+ """
111+ old_keys = set (old .keys ())
112+ new_keys = set (new .keys ())
113+ added = new_keys - old_keys
114+ removed = old_keys - new_keys
115+ return Change (added = added , removed = removed )
116+
117+ dependencies_change = get_changes (
118+ old_package_json_content .get ("dependencies" , {}),
119+ new_package_json_content .get ("dependencies" , {}),
120+ )
121+ dev_dependencies_change = get_changes (
122+ old_package_json_content .get ("devDependencies" , {}),
123+ new_package_json_content .get ("devDependencies" , {}),
124+ )
125+
126+ return dependencies_change , dev_dependencies_change
50127
51128
52129def kill (proc_pid : int ):
@@ -86,7 +163,7 @@ def run_process_and_launch_url(
86163 from reflex .utils import processes
87164
88165 json_file_path = get_web_dir () / constants .PackageJson .PATH
89- last_hash = detect_package_change (json_file_path )
166+ last_content , last_hash = get_package_json_and_hash (json_file_path )
90167 process = None
91168 first_run = True
92169
@@ -105,6 +182,18 @@ def run_process_and_launch_url(
105182 frontend_process = process
106183 if process .stdout :
107184 for line in processes .stream_logs ("Starting frontend" , process ):
185+ new_content , new_hash = get_package_json_and_hash (json_file_path )
186+ if new_hash != last_hash :
187+ dependencies_change , dev_dependencies_change = (
188+ get_different_packages (last_content , new_content )
189+ )
190+ last_content , last_hash = new_content , new_hash
191+ console .info (
192+ "Detected changes in package.json.\n "
193+ + format_change ("Dependencies" , dependencies_change )
194+ + format_change ("Dev Dependencies" , dev_dependencies_change )
195+ )
196+
108197 match = re .search (constants .Next .FRONTEND_LISTENING_REGEX , line )
109198 if match :
110199 if first_run :
@@ -119,22 +208,8 @@ def run_process_and_launch_url(
119208 notify_backend ()
120209 first_run = False
121210 else :
122- console .print ("New packages detected: Updating app..." )
123- else :
124- if any (
125- x in line for x in ("bin executable does not exist on disk" ,)
126- ):
127- console .error (
128- "Try setting `REFLEX_USE_NPM=1` and re-running `reflex init` and `reflex run` to use npm instead of bun:\n "
129- "`REFLEX_USE_NPM=1 reflex init`\n "
130- "`REFLEX_USE_NPM=1 reflex run`"
131- )
132- new_hash = detect_package_change (json_file_path )
133- if new_hash != last_hash :
134- last_hash = new_hash
135- kill (process .pid )
136- process = None
137- break # for line in process.stdout
211+ console .print ("Frontend is restarting..." )
212+
138213 if process is not None :
139214 break # while True
140215
0 commit comments