reflex 0.7.3a1__py3-none-any.whl → 0.7.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of reflex might be problematic. Click here for more details.

Files changed (50) hide show
  1. reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +9 -1
  2. reflex/.templates/web/utils/state.js +1 -1
  3. reflex/app.py +21 -5
  4. reflex/app_mixins/middleware.py +2 -3
  5. reflex/base.py +3 -3
  6. reflex/compiler/compiler.py +68 -8
  7. reflex/components/component.py +6 -3
  8. reflex/components/core/client_side_routing.py +3 -3
  9. reflex/components/core/cond.py +20 -12
  10. reflex/components/core/upload.py +1 -1
  11. reflex/components/dynamic.py +2 -4
  12. reflex/components/lucide/icon.py +20 -27
  13. reflex/components/plotly/plotly.py +9 -9
  14. reflex/components/recharts/recharts.py +2 -2
  15. reflex/components/sonner/toast.py +1 -1
  16. reflex/config.py +23 -23
  17. reflex/constants/__init__.py +1 -2
  18. reflex/constants/base.py +3 -0
  19. reflex/constants/installer.py +8 -105
  20. reflex/custom_components/custom_components.py +8 -3
  21. reflex/reflex.py +22 -4
  22. reflex/state.py +9 -1
  23. reflex/testing.py +7 -1
  24. reflex/utils/build.py +3 -4
  25. reflex/utils/exec.py +156 -75
  26. reflex/utils/net.py +107 -18
  27. reflex/utils/path_ops.py +15 -25
  28. reflex/utils/prerequisites.py +225 -189
  29. reflex/utils/processes.py +70 -35
  30. reflex/utils/redir.py +3 -1
  31. reflex/utils/registry.py +16 -8
  32. reflex/vars/base.py +2 -38
  33. reflex/vars/datetime.py +10 -34
  34. reflex/vars/number.py +16 -112
  35. reflex/vars/sequence.py +99 -108
  36. {reflex-0.7.3a1.dist-info → reflex-0.7.4.dist-info}/METADATA +32 -23
  37. {reflex-0.7.3a1.dist-info → reflex-0.7.4.dist-info}/RECORD +58 -68
  38. {reflex-0.7.3a1.dist-info → reflex-0.7.4.dist-info}/WHEEL +1 -1
  39. {reflex-0.7.3a1.dist-info → reflex-0.7.4.dist-info}/entry_points.txt +0 -3
  40. benchmarks/__init__.py +0 -3
  41. benchmarks/benchmark_compile_times.py +0 -147
  42. benchmarks/benchmark_imports.py +0 -128
  43. benchmarks/benchmark_lighthouse.py +0 -75
  44. benchmarks/benchmark_package_size.py +0 -135
  45. benchmarks/benchmark_web_size.py +0 -106
  46. benchmarks/conftest.py +0 -20
  47. benchmarks/lighthouse.sh +0 -77
  48. benchmarks/utils.py +0 -74
  49. reflex/app_module_for_backend.py +0 -33
  50. {reflex-0.7.3a1.dist-info → reflex-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -8,13 +8,13 @@ import functools
8
8
  import importlib
9
9
  import importlib.metadata
10
10
  import importlib.util
11
+ import io
11
12
  import json
12
13
  import os
13
14
  import platform
14
15
  import random
15
16
  import re
16
17
  import shutil
17
- import stat
18
18
  import sys
19
19
  import tempfile
20
20
  import time
@@ -23,7 +23,7 @@ import zipfile
23
23
  from datetime import datetime
24
24
  from pathlib import Path
25
25
  from types import ModuleType
26
- from typing import Callable, NamedTuple
26
+ from typing import Callable, NamedTuple, Sequence
27
27
  from urllib.parse import urlparse
28
28
 
29
29
  import httpx
@@ -43,13 +43,11 @@ from reflex.utils.exceptions import (
43
43
  SystemPackageMissingError,
44
44
  )
45
45
  from reflex.utils.format import format_library_name
46
- from reflex.utils.registry import _get_npm_registry
46
+ from reflex.utils.registry import get_npm_registry
47
47
 
48
48
  if typing.TYPE_CHECKING:
49
49
  from reflex.app import App
50
50
 
51
- CURRENTLY_INSTALLING_NODE = False
52
-
53
51
 
54
52
  class AppInfo(NamedTuple):
55
53
  """A tuple containing the app instance and module."""
@@ -117,11 +115,13 @@ def check_latest_package_version(package_name: str):
117
115
  if environment.REFLEX_CHECK_LATEST_VERSION.get() is False:
118
116
  return
119
117
  try:
118
+ console.debug(f"Checking for the latest version of {package_name}...")
120
119
  # Get the latest version from PyPI
121
120
  current_version = importlib.metadata.version(package_name)
122
121
  url = f"https://pypi.org/pypi/{package_name}/json"
123
122
  response = net.get(url)
124
123
  latest_version = response.json()["info"]["version"]
124
+ console.debug(f"Latest version of {package_name}: {latest_version}")
125
125
  if get_or_set_last_reflex_version_check_datetime():
126
126
  # Versions were already checked and saved in reflex.json, no need to warn again
127
127
  return
@@ -131,6 +131,7 @@ def check_latest_package_version(package_name: str):
131
131
  f"Your version ({current_version}) of {package_name} is out of date. Upgrade to {latest_version} with 'pip install {package_name} --upgrade'"
132
132
  )
133
133
  except Exception:
134
+ console.debug(f"Failed to check for the latest version of {package_name}.")
134
135
  pass
135
136
 
136
137
 
@@ -186,29 +187,11 @@ def get_node_version() -> version.Version | None:
186
187
  try:
187
188
  result = processes.new_process([node_path, "-v"], run=True)
188
189
  # The output will be in the form "vX.Y.Z", but version.parse() can handle it
189
- return version.parse(result.stdout) # pyright: ignore [reportArgumentType]
190
+ return version.parse(result.stdout)
190
191
  except (FileNotFoundError, TypeError):
191
192
  return None
192
193
 
193
194
 
194
- def get_fnm_version() -> version.Version | None:
195
- """Get the version of fnm.
196
-
197
- Returns:
198
- The version of FNM.
199
- """
200
- try:
201
- result = processes.new_process([constants.Fnm.EXE, "--version"], run=True)
202
- return version.parse(result.stdout.split(" ")[1]) # pyright: ignore [reportOptionalMemberAccess, reportAttributeAccessIssue]
203
- except (FileNotFoundError, TypeError):
204
- return None
205
- except version.InvalidVersion as e:
206
- console.warn(
207
- f"The detected fnm version ({e.args[0]}) is not valid. Defaulting to None."
208
- )
209
- return None
210
-
211
-
212
195
  def get_bun_version() -> version.Version | None:
213
196
  """Get the version of bun.
214
197
 
@@ -221,7 +204,7 @@ def get_bun_version() -> version.Version | None:
221
204
  try:
222
205
  # Run the bun -v command and capture the output
223
206
  result = processes.new_process([str(bun_path), "-v"], run=True)
224
- return version.parse(str(result.stdout)) # pyright: ignore [reportArgumentType]
207
+ return version.parse(str(result.stdout))
225
208
  except FileNotFoundError:
226
209
  return None
227
210
  except version.InvalidVersion as e:
@@ -231,42 +214,107 @@ def get_bun_version() -> version.Version | None:
231
214
  return None
232
215
 
233
216
 
234
- def get_install_package_manager(on_failure_return_none: bool = False) -> str | None:
235
- """Get the package manager executable for installation.
236
- Currently, bun is used for installation only.
217
+ def prefer_npm_over_bun() -> bool:
218
+ """Check if npm should be preferred over bun.
219
+
220
+ Returns:
221
+ If npm should be preferred over bun.
222
+ """
223
+ return npm_escape_hatch() or (
224
+ constants.IS_WINDOWS and windows_check_onedrive_in_path()
225
+ )
226
+
227
+
228
+ def get_nodejs_compatible_package_managers(
229
+ raise_on_none: bool = True,
230
+ ) -> Sequence[str]:
231
+ """Get the package manager executable for installation. Typically, bun is used for installation.
237
232
 
238
233
  Args:
239
- on_failure_return_none: Whether to return None on failure.
234
+ raise_on_none: Whether to raise an error if the package manager is not found.
240
235
 
241
236
  Returns:
242
237
  The path to the package manager.
238
+
239
+ Raises:
240
+ FileNotFoundError: If the package manager is not found and raise_on_none is True.
241
+ """
242
+ bun_package_manager = (
243
+ str(bun_path) if (bun_path := path_ops.get_bun_path()) else None
244
+ )
245
+
246
+ npm_package_manager = (
247
+ str(npm_path) if (npm_path := path_ops.get_npm_path()) else None
248
+ )
249
+
250
+ if prefer_npm_over_bun():
251
+ package_managers = [npm_package_manager, bun_package_manager]
252
+ else:
253
+ package_managers = [bun_package_manager, npm_package_manager]
254
+
255
+ package_managers = list(filter(None, package_managers))
256
+
257
+ if not package_managers and not raise_on_none:
258
+ raise FileNotFoundError(
259
+ "Bun or npm not found. You might need to rerun `reflex init` or install either."
260
+ )
261
+
262
+ return package_managers
263
+
264
+
265
+ def is_outdated_nodejs_installed():
266
+ """Check if the installed Node.js version is outdated.
267
+
268
+ Returns:
269
+ If the installed Node.js version is outdated.
243
270
  """
244
- if constants.IS_WINDOWS and (
245
- windows_check_onedrive_in_path() or windows_npm_escape_hatch()
271
+ current_version = get_node_version()
272
+ if current_version is not None and current_version < version.parse(
273
+ constants.Node.MIN_VERSION
246
274
  ):
247
- return get_package_manager(on_failure_return_none)
248
- return str(get_config().bun_path)
275
+ console.warn(
276
+ f"Your version ({current_version}) of Node.js is out of date. Upgrade to {constants.Node.MIN_VERSION} or higher."
277
+ )
278
+ return True
279
+ return False
249
280
 
250
281
 
251
- def get_package_manager(on_failure_return_none: bool = False) -> str | None:
252
- """Get the package manager executable for running app.
253
- Currently on unix systems, npm is used for running the app only.
282
+ def get_js_package_executor(raise_on_none: bool = False) -> Sequence[Sequence[str]]:
283
+ """Get the paths to package managers for running commands. Ordered by preference.
284
+ This is currently identical to get_install_package_managers, but may change in the future.
254
285
 
255
286
  Args:
256
- on_failure_return_none: Whether to return None on failure.
287
+ raise_on_none: Whether to raise an error if no package managers is not found.
257
288
 
258
289
  Returns:
259
- The path to the package manager.
290
+ The paths to the package managers as a list of lists, where each list is the command to run and its arguments.
260
291
 
261
292
  Raises:
262
- FileNotFoundError: If the package manager is not found.
293
+ FileNotFoundError: If no package managers are found and raise_on_none is True.
263
294
  """
264
- npm_path = path_ops.get_npm_path()
265
- if npm_path is not None:
266
- return str(npm_path)
267
- if on_failure_return_none:
268
- return None
269
- raise FileNotFoundError("NPM not found. You may need to run `reflex init`.")
295
+ bun_package_manager = (
296
+ [str(bun_path)] + (["--bun"] if is_outdated_nodejs_installed() else [])
297
+ if (bun_path := path_ops.get_bun_path())
298
+ else None
299
+ )
300
+
301
+ npm_package_manager = (
302
+ [str(npm_path)] if (npm_path := path_ops.get_npm_path()) else None
303
+ )
304
+
305
+ if prefer_npm_over_bun():
306
+ package_managers = [npm_package_manager, bun_package_manager]
307
+ else:
308
+ package_managers = [bun_package_manager, npm_package_manager]
309
+
310
+ package_managers = list(filter(None, package_managers))
311
+
312
+ if not package_managers and raise_on_none:
313
+ raise FileNotFoundError(
314
+ "Bun or npm not found. You might need to rerun `reflex init` or install either."
315
+ )
316
+
317
+ return package_managers
270
318
 
271
319
 
272
320
  def windows_check_onedrive_in_path() -> bool:
@@ -278,8 +326,8 @@ def windows_check_onedrive_in_path() -> bool:
278
326
  return "onedrive" in str(Path.cwd()).lower()
279
327
 
280
328
 
281
- def windows_npm_escape_hatch() -> bool:
282
- """For windows, if the user sets REFLEX_USE_NPM, use npm instead of bun.
329
+ def npm_escape_hatch() -> bool:
330
+ """If the user sets REFLEX_USE_NPM, prefer npm over bun.
283
331
 
284
332
  Returns:
285
333
  If the user has set REFLEX_USE_NPM.
@@ -368,6 +416,15 @@ def get_and_validate_app(reload: bool = False) -> AppInfo:
368
416
  return AppInfo(app=app, module=app_module)
369
417
 
370
418
 
419
+ def validate_app(reload: bool = False) -> None:
420
+ """Validate the app instance based on the default config.
421
+
422
+ Args:
423
+ reload: Re-import the app module from disk
424
+ """
425
+ get_and_validate_app(reload=reload)
426
+
427
+
371
428
  def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
372
429
  """Get the app module based on the default config after first compiling it.
373
430
 
@@ -386,6 +443,86 @@ def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
386
443
  return app_module
387
444
 
388
445
 
446
+ def compile_app(reload: bool = False, export: bool = False) -> None:
447
+ """Compile the app module based on the default config.
448
+
449
+ Args:
450
+ reload: Re-import the app module from disk
451
+ export: Compile the app for export
452
+ """
453
+ get_compiled_app(reload=reload, export=export)
454
+
455
+
456
+ def _can_colorize() -> bool:
457
+ """Check if the output can be colorized.
458
+
459
+ Copied from _colorize.can_colorize.
460
+
461
+ https://raw.githubusercontent.com/python/cpython/refs/heads/main/Lib/_colorize.py
462
+
463
+ Returns:
464
+ If the output can be colorized
465
+ """
466
+ file = sys.stdout
467
+
468
+ if not sys.flags.ignore_environment:
469
+ if os.environ.get("PYTHON_COLORS") == "0":
470
+ return False
471
+ if os.environ.get("PYTHON_COLORS") == "1":
472
+ return True
473
+ if os.environ.get("NO_COLOR"):
474
+ return False
475
+ if os.environ.get("FORCE_COLOR"):
476
+ return True
477
+ if os.environ.get("TERM") == "dumb":
478
+ return False
479
+
480
+ if not hasattr(file, "fileno"):
481
+ return False
482
+
483
+ if sys.platform == "win32":
484
+ try:
485
+ import nt
486
+
487
+ if not nt._supports_virtual_terminal():
488
+ return False
489
+ except (ImportError, AttributeError):
490
+ return False
491
+
492
+ try:
493
+ return os.isatty(file.fileno())
494
+ except io.UnsupportedOperation:
495
+ return file.isatty()
496
+
497
+
498
+ def compile_or_validate_app(compile: bool = False) -> bool:
499
+ """Compile or validate the app module based on the default config.
500
+
501
+ Args:
502
+ compile: Whether to compile the app.
503
+
504
+ Returns:
505
+ If the app is compiled successfully.
506
+ """
507
+ try:
508
+ if compile:
509
+ compile_app()
510
+ else:
511
+ validate_app()
512
+ except Exception as e:
513
+ import traceback
514
+
515
+ sys_exception = sys.exception()
516
+
517
+ try:
518
+ colorize = _can_colorize()
519
+ traceback.print_exception(e, colorize=colorize) # pyright: ignore[reportCallIssue]
520
+ except Exception:
521
+ traceback.print_exception(sys_exception)
522
+ return False
523
+ return True
524
+
525
+
389
526
  def get_redis() -> Redis | None:
390
527
  """Get the asynchronous redis client.
391
528
 
@@ -768,11 +905,12 @@ def initialize_app_directory(
768
905
 
769
906
  console.debug(f"Using {template_name=} {template_dir=} {template_code_dir_name=}.")
770
907
 
771
- # Remove all pyc and __pycache__ dirs in template directory.
772
- for pyc_file in template_dir.glob("**/*.pyc"):
773
- pyc_file.unlink()
774
- for pycache_dir in template_dir.glob("**/__pycache__"):
775
- pycache_dir.rmdir()
908
+ # Remove __pycache__ dirs in template directory and current directory.
909
+ for pycache_dir in [
910
+ *template_dir.glob("**/__pycache__"),
911
+ *Path.cwd().glob("**/__pycache__"),
912
+ ]:
913
+ shutil.rmtree(pycache_dir, ignore_errors=True)
776
914
 
777
915
  for file in template_dir.iterdir():
778
916
  # Copy the file to current directory but keep the name the same.
@@ -816,16 +954,22 @@ def initialize_web_directory():
816
954
  # Reuse the hash if one is already created, so we don't over-write it when running reflex init
817
955
  project_hash = get_project_hash()
818
956
 
957
+ console.debug(f"Copying {constants.Templates.Dirs.WEB_TEMPLATE} to {get_web_dir()}")
819
958
  path_ops.cp(constants.Templates.Dirs.WEB_TEMPLATE, str(get_web_dir()))
820
959
 
960
+ console.debug("Initializing the web directory.")
821
961
  initialize_package_json()
822
962
 
963
+ console.debug("Initializing the bun config file.")
823
964
  initialize_bun_config()
824
965
 
966
+ console.debug("Initializing the public directory.")
825
967
  path_ops.mkdir(get_web_dir() / constants.Dirs.PUBLIC)
826
968
 
969
+ console.debug("Initializing the next.config.js file.")
827
970
  update_next_config()
828
971
 
972
+ console.debug("Initializing the reflex.json file.")
829
973
  # Initialize the reflex json file.
830
974
  init_reflex_json(project_hash=project_hash)
831
975
 
@@ -862,7 +1006,7 @@ def initialize_bun_config():
862
1006
  bunfig_content = custom_bunfig.read_text()
863
1007
  console.info(f"Copying custom bunfig.toml inside {get_web_dir()} folder")
864
1008
  else:
865
- best_registry = _get_npm_registry()
1009
+ best_registry = get_npm_registry()
866
1010
  bunfig_content = constants.Bun.DEFAULT_CONFIG.format(registry=best_registry)
867
1011
 
868
1012
  bun_config_path.write_text(bunfig_content)
@@ -970,92 +1114,6 @@ def download_and_run(url: str, *args, show_status: bool = False, **env):
970
1114
  show(f"Installing {url}", process)
971
1115
 
972
1116
 
973
- def download_and_extract_fnm_zip():
974
- """Download and run a script.
975
-
976
- Raises:
977
- Exit: If an error occurs while downloading or extracting the FNM zip.
978
- """
979
- # Download the zip file
980
- url = constants.Fnm.INSTALL_URL
981
- console.debug(f"Downloading {url}")
982
- fnm_zip_file: Path = constants.Fnm.DIR / f"{constants.Fnm.FILENAME}.zip"
983
- # Function to download and extract the FNM zip release.
984
- try:
985
- # Download the FNM zip release.
986
- # TODO: show progress to improve UX
987
- response = net.get(url, follow_redirects=True)
988
- response.raise_for_status()
989
- with fnm_zip_file.open("wb") as output_file:
990
- for chunk in response.iter_bytes():
991
- output_file.write(chunk)
992
-
993
- # Extract the downloaded zip file.
994
- with zipfile.ZipFile(fnm_zip_file, "r") as zip_ref:
995
- zip_ref.extractall(constants.Fnm.DIR)
996
-
997
- console.debug("FNM package downloaded and extracted successfully.")
998
- except Exception as e:
999
- console.error(f"An error occurred while downloading fnm package: {e}")
1000
- raise typer.Exit(1) from e
1001
- finally:
1002
- # Clean up the downloaded zip file.
1003
- path_ops.rm(fnm_zip_file)
1004
-
1005
-
1006
- def install_node():
1007
- """Install fnm and nodejs for use by Reflex.
1008
- Independent of any existing system installations.
1009
- """
1010
- if not constants.Fnm.FILENAME:
1011
- # fnm only support Linux, macOS and Windows distros.
1012
- console.debug("")
1013
- return
1014
-
1015
- # Skip installation if check_node_version() checks out
1016
- if check_node_version():
1017
- console.debug("Skipping node installation as it is already installed.")
1018
- return
1019
-
1020
- path_ops.mkdir(constants.Fnm.DIR)
1021
- if not constants.Fnm.EXE.exists():
1022
- download_and_extract_fnm_zip()
1023
-
1024
- if constants.IS_WINDOWS:
1025
- # Install node
1026
- fnm_exe = Path(constants.Fnm.EXE).resolve()
1027
- fnm_dir = Path(constants.Fnm.DIR).resolve()
1028
- process = processes.new_process(
1029
- [
1030
- "powershell",
1031
- "-Command",
1032
- f'& "{fnm_exe}" install {constants.Node.VERSION} --fnm-dir "{fnm_dir}"',
1033
- ],
1034
- )
1035
- else: # All other platforms (Linux, MacOS).
1036
- # Add execute permissions to fnm executable.
1037
- constants.Fnm.EXE.chmod(stat.S_IXUSR)
1038
- # Install node.
1039
- # Specify arm64 arch explicitly for M1s and M2s.
1040
- architecture_arg = (
1041
- ["--arch=arm64"]
1042
- if platform.system() == "Darwin" and platform.machine() == "arm64"
1043
- else []
1044
- )
1045
-
1046
- process = processes.new_process(
1047
- [
1048
- constants.Fnm.EXE,
1049
- "install",
1050
- *architecture_arg,
1051
- constants.Node.VERSION,
1052
- "--fnm-dir",
1053
- constants.Fnm.DIR,
1054
- ],
1055
- )
1056
- processes.show_status("Installing node", process)
1057
-
1058
-
1059
1117
  def install_bun():
1060
1118
  """Install bun onto the user's system.
1061
1119
 
@@ -1069,7 +1127,9 @@ def install_bun():
1069
1127
  )
1070
1128
 
1071
1129
  # Skip if bun is already installed.
1072
- if get_bun_version() == version.parse(constants.Bun.VERSION):
1130
+ if (current_version := get_bun_version()) and current_version >= version.parse(
1131
+ constants.Bun.MIN_VERSION
1132
+ ):
1073
1133
  console.debug("Skipping bun installation as it is already installed.")
1074
1134
  return
1075
1135
 
@@ -1157,38 +1217,19 @@ def install_frontend_packages(packages: set[str], config: Config):
1157
1217
  packages: A list of package names to be installed.
1158
1218
  config: The config object.
1159
1219
 
1160
- Raises:
1161
- FileNotFoundError: If the package manager is not found.
1162
-
1163
1220
  Example:
1164
1221
  >>> install_frontend_packages(["react", "react-dom"], get_config())
1165
1222
  """
1166
- # unsupported archs(arm and 32bit machines) will use npm anyway. so we dont have to run npm twice
1167
- fallback_command = (
1168
- get_package_manager(on_failure_return_none=True)
1169
- if (
1170
- not constants.IS_WINDOWS
1171
- or (constants.IS_WINDOWS and not windows_check_onedrive_in_path())
1172
- )
1173
- else None
1174
- )
1175
-
1176
- install_package_manager = (
1177
- get_install_package_manager(on_failure_return_none=True) or fallback_command
1223
+ install_package_managers = get_nodejs_compatible_package_managers(
1224
+ raise_on_none=True
1178
1225
  )
1179
1226
 
1180
- if install_package_manager is None:
1181
- raise FileNotFoundError(
1182
- "Could not find a package manager to install frontend packages. You may need to run `reflex init`."
1183
- )
1184
-
1185
- fallback_command = (
1186
- fallback_command if fallback_command is not install_package_manager else None
1187
- )
1227
+ primary_package_manager = install_package_managers[0]
1228
+ fallbacks = install_package_managers[1:]
1188
1229
 
1189
- processes.run_process_with_fallback(
1190
- [install_package_manager, "install", "--legacy-peer-deps"],
1191
- fallback=fallback_command,
1230
+ processes.run_process_with_fallbacks(
1231
+ [primary_package_manager, "install", "--legacy-peer-deps"],
1232
+ fallbacks=fallbacks,
1192
1233
  analytics_enabled=True,
1193
1234
  show_status_message="Installing base frontend packages",
1194
1235
  cwd=get_web_dir(),
@@ -1196,16 +1237,16 @@ def install_frontend_packages(packages: set[str], config: Config):
1196
1237
  )
1197
1238
 
1198
1239
  if config.tailwind is not None:
1199
- processes.run_process_with_fallback(
1240
+ processes.run_process_with_fallbacks(
1200
1241
  [
1201
- install_package_manager,
1242
+ primary_package_manager,
1202
1243
  "add",
1203
1244
  "--legacy-peer-deps",
1204
1245
  "-d",
1205
1246
  constants.Tailwind.VERSION,
1206
1247
  *((config.tailwind or {}).get("plugins", [])),
1207
1248
  ],
1208
- fallback=fallback_command,
1249
+ fallbacks=fallbacks,
1209
1250
  analytics_enabled=True,
1210
1251
  show_status_message="Installing tailwind",
1211
1252
  cwd=get_web_dir(),
@@ -1214,9 +1255,9 @@ def install_frontend_packages(packages: set[str], config: Config):
1214
1255
 
1215
1256
  # Install custom packages defined in frontend_packages
1216
1257
  if len(packages) > 0:
1217
- processes.run_process_with_fallback(
1218
- [install_package_manager, "add", "--legacy-peer-deps", *packages],
1219
- fallback=fallback_command,
1258
+ processes.run_process_with_fallbacks(
1259
+ [primary_package_manager, "add", "--legacy-peer-deps", *packages],
1260
+ fallbacks=fallbacks,
1220
1261
  analytics_enabled=True,
1221
1262
  show_status_message="Installing frontend packages from config and components",
1222
1263
  cwd=get_web_dir(),
@@ -1338,24 +1379,19 @@ def validate_frontend_dependencies(init: bool = True):
1338
1379
  Exit: If the package manager is invalid.
1339
1380
  """
1340
1381
  if not init:
1341
- # we only need to validate the package manager when running app.
1342
- # `reflex init` will install the deps anyway(if applied).
1343
- package_manager = get_package_manager()
1344
- if not package_manager:
1345
- console.error(
1346
- "Could not find NPM package manager. Make sure you have node installed."
1347
- )
1348
- raise typer.Exit(1)
1382
+ try:
1383
+ get_js_package_executor(raise_on_none=True)
1384
+ except FileNotFoundError as e:
1385
+ raise typer.Exit(1) from e
1349
1386
 
1387
+ if prefer_npm_over_bun():
1350
1388
  if not check_node_version():
1351
1389
  node_version = get_node_version()
1352
1390
  console.error(
1353
1391
  f"Reflex requires node version {constants.Node.MIN_VERSION} or higher to run, but the detected version is {node_version}",
1354
1392
  )
1355
1393
  raise typer.Exit(1)
1356
-
1357
- if init:
1358
- # we only need bun for package install on `reflex init`.
1394
+ else:
1359
1395
  validate_bun()
1360
1396
 
1361
1397
 
@@ -1366,6 +1402,7 @@ def ensure_reflex_installation_id() -> int | None:
1366
1402
  Distinct id.
1367
1403
  """
1368
1404
  try:
1405
+ console.debug("Ensuring reflex installation id.")
1369
1406
  initialize_reflex_user_directory()
1370
1407
  installation_id_file = environment.REFLEX_DIR.get() / "installation_id"
1371
1408
 
@@ -1392,6 +1429,7 @@ def ensure_reflex_installation_id() -> int | None:
1392
1429
 
1393
1430
  def initialize_reflex_user_directory():
1394
1431
  """Initialize the reflex user directory."""
1432
+ console.debug(f"Creating {environment.REFLEX_DIR.get()}")
1395
1433
  # Create the reflex directory.
1396
1434
  path_ops.mkdir(environment.REFLEX_DIR.get())
1397
1435
 
@@ -1399,13 +1437,11 @@ def initialize_reflex_user_directory():
1399
1437
  def initialize_frontend_dependencies():
1400
1438
  """Initialize all the frontend dependencies."""
1401
1439
  # validate dependencies before install
1440
+ console.debug("Validating frontend dependencies.")
1402
1441
  validate_frontend_dependencies()
1403
- # Avoid warning about Node installation while we're trying to install it.
1404
- global CURRENTLY_INSTALLING_NODE
1405
- CURRENTLY_INSTALLING_NODE = True
1406
1442
  # Install the frontend dependencies.
1407
- processes.run_concurrently(install_node, install_bun)
1408
- CURRENTLY_INSTALLING_NODE = False
1443
+ console.debug("Installing or validating bun.")
1444
+ install_bun()
1409
1445
  # Set up the web directory.
1410
1446
  initialize_web_directory()
1411
1447
 
@@ -1610,7 +1646,7 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
1610
1646
  console.error(f"Failed to unzip the template: {uze}")
1611
1647
  raise typer.Exit(1) from uze
1612
1648
 
1613
- if len(subdirs := os.listdir(unzip_dir)) != 1:
1649
+ if len(subdirs := list(unzip_dir.iterdir())) != 1:
1614
1650
  console.error(f"Expected one directory in the zip, found {subdirs}")
1615
1651
  raise typer.Exit(1)
1616
1652