reflex 0.7.3a2__py3-none-any.whl → 0.7.4a1__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.

@@ -1,46 +1,22 @@
1
- """File for constants related to the installation process. (Bun/FNM/Node)."""
1
+ """File for constants related to the installation process. (Bun/Node)."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import platform
6
- from pathlib import Path
7
5
  from types import SimpleNamespace
8
6
 
9
7
  from .base import IS_WINDOWS
10
8
  from .utils import classproperty
11
9
 
12
10
 
13
- def get_fnm_name() -> str | None:
14
- """Get the appropriate fnm executable name based on the current platform.
15
-
16
- Returns:
17
- The fnm executable name for the current platform.
18
- """
19
- platform_os = platform.system()
20
-
21
- if platform_os == "Windows":
22
- return "fnm-windows"
23
- elif platform_os == "Darwin":
24
- return "fnm-macos"
25
- elif platform_os == "Linux":
26
- machine = platform.machine()
27
- if machine == "arm" or machine.startswith("armv7"):
28
- return "fnm-arm32"
29
- elif machine.startswith("aarch") or machine.startswith("armv8"):
30
- return "fnm-arm64"
31
- return "fnm-linux"
32
- return None
33
-
34
-
35
11
  # Bun config.
36
12
  class Bun(SimpleNamespace):
37
13
  """Bun constants."""
38
14
 
39
15
  # The Bun version.
40
- VERSION = "1.2.0"
16
+ VERSION = "1.2.4"
41
17
 
42
18
  # Min Bun Version
43
- MIN_VERSION = "1.1.0"
19
+ MIN_VERSION = "1.2.4"
44
20
 
45
21
  # URL to bun install script.
46
22
  INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/bun_install.sh"
@@ -81,43 +57,6 @@ registry = "{registry}"
81
57
  """
82
58
 
83
59
 
84
- # FNM config.
85
- class Fnm(SimpleNamespace):
86
- """FNM constants."""
87
-
88
- # The FNM version.
89
- VERSION = "1.35.1"
90
-
91
- FILENAME = get_fnm_name()
92
-
93
- # The URL to the fnm release binary
94
- INSTALL_URL = (
95
- f"https://github.com/Schniz/fnm/releases/download/v{VERSION}/{FILENAME}.zip"
96
- )
97
-
98
- @classproperty
99
- @classmethod
100
- def DIR(cls) -> Path:
101
- """The directory to store fnm.
102
-
103
- Returns:
104
- The directory to store fnm.
105
- """
106
- from reflex.config import environment
107
-
108
- return environment.REFLEX_DIR.get() / "fnm"
109
-
110
- @classproperty
111
- @classmethod
112
- def EXE(cls):
113
- """The fnm executable binary.
114
-
115
- Returns:
116
- The fnm executable binary.
117
- """
118
- return cls.DIR / ("fnm.exe" if IS_WINDOWS else "fnm")
119
-
120
-
121
60
  # Node / NPM config
122
61
  class Node(SimpleNamespace):
123
62
  """Node/ NPM constants."""
@@ -127,42 +66,6 @@ class Node(SimpleNamespace):
127
66
  # The minimum required node version.
128
67
  MIN_VERSION = "18.18.0"
129
68
 
130
- @classproperty
131
- @classmethod
132
- def BIN_PATH(cls):
133
- """The node bin path.
134
-
135
- Returns:
136
- The node bin path.
137
- """
138
- return (
139
- Fnm.DIR
140
- / "node-versions"
141
- / f"v{cls.VERSION}"
142
- / "installation"
143
- / ("bin" if not IS_WINDOWS else "")
144
- )
145
-
146
- @classproperty
147
- @classmethod
148
- def PATH(cls):
149
- """The default path where node is installed.
150
-
151
- Returns:
152
- The default path where node is installed.
153
- """
154
- return cls.BIN_PATH / ("node.exe" if IS_WINDOWS else "node")
155
-
156
- @classproperty
157
- @classmethod
158
- def NPM_PATH(cls):
159
- """The default path where npm is installed.
160
-
161
- Returns:
162
- The default path where npm is installed.
163
- """
164
- return cls.BIN_PATH / "npm"
165
-
166
69
 
167
70
  class PackageJson(SimpleNamespace):
168
71
  """Constants used to build the package.json file."""
@@ -179,11 +82,11 @@ class PackageJson(SimpleNamespace):
179
82
 
180
83
  DEPENDENCIES = {
181
84
  "@emotion/react": "11.14.0",
182
- "axios": "1.7.9",
85
+ "axios": "1.8.3",
183
86
  "json5": "2.2.3",
184
- "next": "15.1.7",
87
+ "next": "15.0.4",
185
88
  "next-sitemap": "4.2.3",
186
- "next-themes": "0.4.4",
89
+ "next-themes": "0.4.6",
187
90
  "react": "19.0.0",
188
91
  "react-dom": "19.0.0",
189
92
  "react-focus-lock": "2.13.6",
@@ -191,8 +94,8 @@ class PackageJson(SimpleNamespace):
191
94
  "universal-cookie": "7.2.2",
192
95
  }
193
96
  DEV_DEPENDENCIES = {
194
- "autoprefixer": "10.4.20",
195
- "postcss": "8.5.1",
97
+ "autoprefixer": "10.4.21",
98
+ "postcss": "8.5.3",
196
99
  "postcss-import": "16.1.0",
197
100
  }
198
101
  OVERRIDES = {
@@ -826,12 +826,19 @@ def _collect_details_for_gallery():
826
826
  Raises:
827
827
  Exit: If pyproject.toml file is ill-formed or the request to the backend services fails.
828
828
  """
829
+ import reflex_cli.constants
829
830
  from reflex_cli.utils import hosting
830
831
 
831
832
  console.rule("[bold]Authentication with Reflex Services")
832
833
  console.print("First let's log in to Reflex backend services.")
833
834
  access_token, _ = hosting.authenticated_token()
834
835
 
836
+ if not access_token:
837
+ console.error(
838
+ "Unable to authenticate with Reflex backend services. Make sure you are logged in."
839
+ )
840
+ raise typer.Exit(code=1)
841
+
835
842
  console.rule("[bold]Custom Component Information")
836
843
  params = {}
837
844
  package_name = None
@@ -845,10 +852,8 @@ def _collect_details_for_gallery():
845
852
  console.print(f"[ Custom component package name ] : {package_name}")
846
853
  params["package_name"] = package_name
847
854
 
848
- config = get_config()
849
-
850
855
  post_custom_components_gallery_endpoint = (
851
- f"{config.cp_backend_url}/custom-components/gallery"
856
+ f"{reflex_cli.constants.Hosting.HOSTING_SERVICE}/custom-components/gallery"
852
857
  )
853
858
 
854
859
  # Check the backend services if the user is allowed to update information of this package is already shared.
reflex/reflex.py CHANGED
@@ -7,13 +7,14 @@ from pathlib import Path
7
7
 
8
8
  import typer
9
9
  import typer.core
10
- from reflex_cli.v2.deployments import check_version, hosting_cli
10
+ from reflex_cli.v2.deployments import hosting_cli
11
11
 
12
12
  from reflex import constants
13
13
  from reflex.config import environment, get_config
14
14
  from reflex.custom_components.custom_components import custom_components_cli
15
15
  from reflex.state import reset_disk_state_manager
16
16
  from reflex.utils import console, redir, telemetry
17
+ from reflex.utils.exec import should_use_granian
17
18
 
18
19
  # Disable typer+rich integration for help panels
19
20
  typer.core.rich = None # pyright: ignore [reportPrivateImportUsage]
@@ -203,9 +204,19 @@ def _run(
203
204
 
204
205
  prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
205
206
 
206
- if frontend:
207
- # Get the app module.
208
- prerequisites.get_compiled_app()
207
+ # Get the app module.
208
+ app_task = prerequisites.compile_app if frontend else prerequisites.validate_app
209
+
210
+ # Granian fails if the app is already imported.
211
+ if should_use_granian():
212
+ import concurrent.futures
213
+
214
+ compile_future = concurrent.futures.ProcessPoolExecutor(max_workers=1).submit(
215
+ app_task
216
+ )
217
+ compile_future.result()
218
+ else:
219
+ app_task()
209
220
 
210
221
  # Warn if schema is not up to date.
211
222
  prerequisites.check_schema_up_to_date()
@@ -386,6 +397,7 @@ def export(
386
397
  def login(loglevel: constants.LogLevel | None = typer.Option(None)):
387
398
  """Authenticate with experimental Reflex hosting service."""
388
399
  from reflex_cli.v2 import cli as hosting_cli
400
+ from reflex_cli.v2.deployments import check_version
389
401
 
390
402
  loglevel = loglevel or get_config().loglevel
391
403
 
@@ -407,6 +419,7 @@ def logout(
407
419
  ):
408
420
  """Log out of access to Reflex hosting service."""
409
421
  from reflex_cli.v2.cli import logout
422
+ from reflex_cli.v2.deployments import check_version
410
423
 
411
424
  check_version()
412
425
 
@@ -567,6 +580,7 @@ def deploy(
567
580
  from reflex_cli.constants.base import LogLevel as HostingLogLevel
568
581
  from reflex_cli.utils import dependency
569
582
  from reflex_cli.v2 import cli as hosting_cli
583
+ from reflex_cli.v2.deployments import check_version
570
584
 
571
585
  from reflex.utils import export as export_utils
572
586
  from reflex.utils import prerequisites
reflex/state.py CHANGED
@@ -905,7 +905,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
905
905
  ]
906
906
  if len(parent_states) >= 2:
907
907
  raise ValueError(f"Only one parent state is allowed {parent_states}.")
908
- return parent_states[0] if len(parent_states) == 1 else None
908
+ # The first non-mixin state in the mro is our parent.
909
+ for base in cls.mro()[1:]:
910
+ if base._mixin or not issubclass(base, BaseState):
911
+ continue
912
+ if base is BaseState:
913
+ break
914
+ return base
915
+ return None # No known parent
909
916
 
910
917
  @classmethod
911
918
  @functools.lru_cache()
@@ -1664,6 +1671,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
1664
1671
 
1665
1672
  raise TypeError(
1666
1673
  f"Your handler {handler.fn.__qualname__} must only return/yield: None, Events or other EventHandlers referenced by their class (i.e. using `type(self)` or other class references)."
1674
+ f" Returned events of types {', '.join(map(str, map(type, events)))!s}."
1667
1675
  )
1668
1676
 
1669
1677
  async def _as_state_update(
reflex/testing.py CHANGED
@@ -371,7 +371,13 @@ class AppHarness:
371
371
 
372
372
  # Start the frontend.
373
373
  self.frontend_process = reflex.utils.processes.new_process(
374
- [reflex.utils.prerequisites.get_package_manager(), "run", "dev"],
374
+ [
375
+ *reflex.utils.prerequisites.get_js_package_executor(raise_on_none=True)[
376
+ 0
377
+ ],
378
+ "run",
379
+ "dev",
380
+ ],
375
381
  cwd=self.app_path / reflex.utils.prerequisites.get_web_dir(),
376
382
  env={"PORT": "0"},
377
383
  **FRONTEND_POPEN_ARGS,
reflex/utils/build.py CHANGED
@@ -212,7 +212,7 @@ def build(
212
212
 
213
213
  # Start the subprocess with the progress bar.
214
214
  process = processes.new_process(
215
- [prerequisites.get_package_manager(), "run", command],
215
+ [*prerequisites.get_js_package_executor(raise_on_none=True)[0], "run", command],
216
216
  cwd=wdir,
217
217
  shell=constants.IS_WINDOWS,
218
218
  )
@@ -231,11 +231,10 @@ def setup_frontend(
231
231
  """
232
232
  # Create the assets dir if it doesn't exist.
233
233
  path_ops.mkdir(constants.Dirs.APP_ASSETS)
234
-
235
- # Copy asset files to public folder.
236
234
  path_ops.cp(
237
235
  src=str(root / constants.Dirs.APP_ASSETS),
238
236
  dest=str(root / prerequisites.get_web_dir() / constants.Dirs.PUBLIC),
237
+ ignore=tuple(f"*.{ext}" for ext in constants.Reflex.STYLESHEETS_SUPPORTED),
239
238
  )
240
239
 
241
240
  # Set the environment variables in client (env.json).
@@ -248,7 +247,7 @@ def setup_frontend(
248
247
  if disable_telemetry:
249
248
  processes.new_process(
250
249
  [
251
- prerequisites.get_package_manager(),
250
+ *prerequisites.get_js_package_executor(raise_on_none=True)[0],
252
251
  "run",
253
252
  "next",
254
253
  "telemetry",
reflex/utils/exec.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import hashlib
6
+ import importlib.util
6
7
  import json
7
8
  import os
8
9
  import platform
@@ -19,6 +20,7 @@ from reflex import constants
19
20
  from reflex.config import environment, get_config
20
21
  from reflex.constants.base import LogLevel
21
22
  from reflex.utils import console, path_ops
23
+ from reflex.utils.decorator import once
22
24
  from reflex.utils.prerequisites import get_web_dir
23
25
 
24
26
  # For uvicorn windows bug fix (#2335)
@@ -154,7 +156,11 @@ def run_frontend(root: Path, port: str, backend_present: bool = True):
154
156
  console.rule("[bold green]App Running")
155
157
  os.environ["PORT"] = str(get_config().frontend_port if port is None else port)
156
158
  run_process_and_launch_url(
157
- [prerequisites.get_package_manager(), "run", "dev"],
159
+ [
160
+ *prerequisites.get_js_package_executor(raise_on_none=True)[0],
161
+ "run",
162
+ "dev",
163
+ ],
158
164
  backend_present,
159
165
  )
160
166
 
@@ -176,18 +182,33 @@ def run_frontend_prod(root: Path, port: str, backend_present: bool = True):
176
182
  # Run the frontend in production mode.
177
183
  console.rule("[bold green]App Running")
178
184
  run_process_and_launch_url(
179
- [prerequisites.get_package_manager(), "run", "prod"],
185
+ [*prerequisites.get_js_package_executor(raise_on_none=True)[0], "run", "prod"],
180
186
  backend_present,
181
187
  )
182
188
 
183
189
 
190
+ @once
191
+ def _warn_user_about_uvicorn():
192
+ console.warn(
193
+ "Using Uvicorn for backend as it is installed. This behavior will change in 0.8.0 to use Granian by default."
194
+ )
195
+
196
+
184
197
  def should_use_granian():
185
198
  """Whether to use Granian for backend.
186
199
 
187
200
  Returns:
188
201
  True if Granian should be used.
189
202
  """
190
- return environment.REFLEX_USE_GRANIAN.get()
203
+ if environment.REFLEX_USE_GRANIAN.get():
204
+ return True
205
+ if (
206
+ importlib.util.find_spec("uvicorn") is None
207
+ or importlib.util.find_spec("gunicorn") is None
208
+ ):
209
+ return True
210
+ _warn_user_about_uvicorn()
211
+ return False
191
212
 
192
213
 
193
214
  def get_app_module():
@@ -196,22 +217,9 @@ def get_app_module():
196
217
  Returns:
197
218
  The app module for the backend.
198
219
  """
199
- return f"reflex.app_module_for_backend:{constants.CompileVars.APP}"
200
-
201
-
202
- def get_granian_target():
203
- """Get the Granian target for the backend.
204
-
205
- Returns:
206
- The Granian target for the backend.
207
- """
208
- import reflex
220
+ config = get_config()
209
221
 
210
- app_module_path = Path(reflex.__file__).parent / "app_module_for_backend.py"
211
-
212
- return (
213
- f"{app_module_path!s}:{constants.CompileVars.APP}.{constants.CompileVars.API}"
214
- )
222
+ return f"{config.module}:{constants.CompileVars.APP}"
215
223
 
216
224
 
217
225
  def run_backend(
@@ -313,7 +321,8 @@ def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel):
313
321
  import uvicorn
314
322
 
315
323
  uvicorn.run(
316
- app=f"{get_app_module()}.{constants.CompileVars.API}",
324
+ app=f"{get_app_module()}",
325
+ factory=True,
317
326
  host=host,
318
327
  port=port,
319
328
  log_level=loglevel.value,
@@ -331,31 +340,37 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
331
340
  loglevel: The log level.
332
341
  """
333
342
  console.debug("Using Granian for backend")
334
- try:
335
- from granian.constants import Interfaces
336
- from granian.log import LogLevels
337
- from granian.server import Server as Granian
338
-
339
- Granian(
340
- target=get_granian_target(),
341
- address=host,
342
- port=port,
343
- interface=Interfaces.ASGI,
344
- log_level=LogLevels(loglevel.value),
345
- reload=True,
346
- reload_paths=get_reload_paths(),
347
- ).serve()
348
- except ImportError:
349
- console.error(
350
- 'InstallError: REFLEX_USE_GRANIAN is set but `granian` is not installed. (run `pip install "granian[reload]>=1.6.0"`)'
351
- )
352
- os._exit(1)
343
+
344
+ from granian.constants import Interfaces
345
+ from granian.log import LogLevels
346
+ from granian.server import MPServer as Granian
347
+
348
+ Granian(
349
+ target=get_app_module(),
350
+ factory=True,
351
+ address=host,
352
+ port=port,
353
+ interface=Interfaces.ASGI,
354
+ log_level=LogLevels(loglevel.value),
355
+ reload=True,
356
+ reload_paths=get_reload_paths(),
357
+ ).serve()
353
358
 
354
359
 
360
+ @once
355
361
  def _get_backend_workers():
356
362
  from reflex.utils import processes
357
363
 
358
364
  config = get_config()
365
+
366
+ if config.gunicorn_workers is not None:
367
+ console.deprecate(
368
+ "config.gunicorn_workers",
369
+ reason="If you're using Granian, use GRANIAN_WORKERS instead.",
370
+ deprecation_version="0.7.4",
371
+ removal_version="0.8.0",
372
+ )
373
+
359
374
  return (
360
375
  processes.get_num_workers()
361
376
  if not config.gunicorn_workers
@@ -363,6 +378,51 @@ def _get_backend_workers():
363
378
  )
364
379
 
365
380
 
381
+ @once
382
+ def _get_backend_timeout():
383
+ config = get_config()
384
+
385
+ if config.timeout is not None:
386
+ console.deprecate(
387
+ "config.timeout",
388
+ reason="If you're using Granian, use GRANIAN_WORKERS_LIFETIME instead.",
389
+ deprecation_version="0.7.4",
390
+ removal_version="0.8.0",
391
+ )
392
+
393
+ return config.timeout
394
+
395
+
396
+ @once
397
+ def _get_backend_max_requests():
398
+ config = get_config()
399
+
400
+ if config.gunicorn_max_requests is not None:
401
+ console.deprecate(
402
+ "config.gunicorn_max_requests",
403
+ reason="",
404
+ deprecation_version="0.7.4",
405
+ removal_version="0.8.0",
406
+ )
407
+
408
+ return config.gunicorn_max_requests
409
+
410
+
411
+ @once
412
+ def _get_backend_max_requests_jitter():
413
+ config = get_config()
414
+
415
+ if config.gunicorn_max_requests_jitter is not None:
416
+ console.deprecate(
417
+ "config.gunicorn_max_requests_jitter",
418
+ reason="",
419
+ deprecation_version="0.7.4",
420
+ removal_version="0.8.0",
421
+ )
422
+
423
+ return config.gunicorn_max_requests_jitter
424
+
425
+
366
426
  def run_backend_prod(
367
427
  host: str,
368
428
  port: int,
@@ -404,17 +464,25 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
404
464
  [
405
465
  "uvicorn",
406
466
  *(
407
- [
467
+ (
408
468
  "--limit-max-requests",
409
- str(config.gunicorn_max_requests),
410
- ]
411
- if config.gunicorn_max_requests > 0
412
- else []
469
+ str(max_requessts),
470
+ )
471
+ if (
472
+ (max_requessts := _get_backend_max_requests()) is not None
473
+ and max_requessts > 0
474
+ )
475
+ else ()
476
+ ),
477
+ *(
478
+ ("--timeout-keep-alive", str(timeout))
479
+ if (timeout := _get_backend_timeout()) is not None
480
+ else ()
413
481
  ),
414
- *("--timeout-keep-alive", str(config.timeout)),
415
482
  *("--host", host),
416
483
  *("--port", str(port)),
417
484
  *("--workers", str(_get_backend_workers())),
485
+ "--factory",
418
486
  app_module,
419
487
  ]
420
488
  if constants.IS_WINDOWS
@@ -422,17 +490,34 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
422
490
  "gunicorn",
423
491
  *("--worker-class", config.gunicorn_worker_class),
424
492
  *(
425
- [
493
+ (
426
494
  "--max-requests",
427
- str(config.gunicorn_max_requests),
495
+ str(max_requessts),
496
+ )
497
+ if (
498
+ (max_requessts := _get_backend_max_requests()) is not None
499
+ and max_requessts > 0
500
+ )
501
+ else ()
502
+ ),
503
+ *(
504
+ (
428
505
  "--max-requests-jitter",
429
- str(config.gunicorn_max_requests_jitter),
430
- ]
431
- if config.gunicorn_max_requests > 0
432
- else []
506
+ str(max_requessts_jitter),
507
+ )
508
+ if (
509
+ (max_requessts_jitter := _get_backend_max_requests_jitter())
510
+ is not None
511
+ and max_requessts_jitter > 0
512
+ )
513
+ else ()
433
514
  ),
434
515
  "--preload",
435
- *("--timeout", str(config.timeout)),
516
+ *(
517
+ ("--timeout", str(timeout))
518
+ if (timeout := _get_backend_timeout()) is not None
519
+ else ()
520
+ ),
436
521
  *("--bind", f"{host}:{port}"),
437
522
  *("--threads", str(_get_backend_workers())),
438
523
  f"{app_module}()",
@@ -468,17 +553,12 @@ def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel):
468
553
 
469
554
  command = [
470
555
  "granian",
471
- "--workers",
472
- str(_get_backend_workers()),
473
- "--log-level",
474
- "critical",
475
- "--host",
476
- host,
477
- "--port",
478
- str(port),
479
- "--interface",
480
- str(Interfaces.ASGI),
481
- get_granian_target(),
556
+ *("--workers", str(_get_backend_workers())),
557
+ *("--log-level", "critical"),
558
+ *("--host", host),
559
+ *("--port", str(port)),
560
+ *("--interface", str(Interfaces.ASGI)),
561
+ *("--factory", get_app_module()),
482
562
  ]
483
563
  processes.new_process(
484
564
  command,
@@ -518,13 +598,8 @@ def output_system_info():
518
598
 
519
599
  system = platform.system()
520
600
 
521
- fnm_info = f"[FNM {prerequisites.get_fnm_version()} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]"
522
-
523
- dependencies.extend(
524
- [
525
- fnm_info,
526
- f"[Bun {prerequisites.get_bun_version()} (Expected: {constants.Bun.VERSION}) (PATH: {path_ops.get_bun_path()})]",
527
- ],
601
+ dependencies.append(
602
+ f"[Bun {prerequisites.get_bun_version()} (Expected: {constants.Bun.VERSION}) (PATH: {path_ops.get_bun_path()})]"
528
603
  )
529
604
 
530
605
  if system == "Linux":
@@ -540,10 +615,10 @@ def output_system_info():
540
615
  console.debug(f"{dep}")
541
616
 
542
617
  console.debug(
543
- f"Using package installer at: {prerequisites.get_install_package_manager(on_failure_return_none=True)}"
618
+ f"Using package installer at: {prerequisites.get_nodejs_compatible_package_managers(raise_on_none=False)}"
544
619
  )
545
620
  console.debug(
546
- f"Using package executer at: {prerequisites.get_package_manager(on_failure_return_none=True)}"
621
+ f"Using package executer at: {prerequisites.get_js_package_executor(raise_on_none=False)}"
547
622
  )
548
623
  if system != "Windows":
549
624
  console.debug(f"Unzip path: {path_ops.which('unzip')}")