reflex 0.8.6a1__py3-none-any.whl → 0.8.7__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 (37) hide show
  1. reflex/.templates/jinja/web/vite.config.js.jinja2 +4 -1
  2. reflex/.templates/web/app/routes.js +0 -1
  3. reflex/.templates/web/utils/state.js +1 -11
  4. reflex/app.py +15 -23
  5. reflex/components/component.py +6 -4
  6. reflex/components/lucide/icon.py +4 -1
  7. reflex/components/lucide/icon.pyi +4 -1
  8. reflex/components/plotly/plotly.py +9 -9
  9. reflex/components/recharts/recharts.py +2 -2
  10. reflex/components/sonner/toast.py +7 -7
  11. reflex/components/sonner/toast.pyi +8 -8
  12. reflex/config.py +9 -2
  13. reflex/constants/base.py +2 -0
  14. reflex/constants/installer.py +6 -6
  15. reflex/constants/state.py +1 -0
  16. reflex/custom_components/custom_components.py +3 -3
  17. reflex/reflex.py +7 -6
  18. reflex/route.py +4 -0
  19. reflex/state.py +1 -1
  20. reflex/testing.py +3 -5
  21. reflex/utils/build.py +21 -3
  22. reflex/utils/exec.py +11 -11
  23. reflex/utils/frontend_skeleton.py +254 -0
  24. reflex/utils/js_runtimes.py +411 -0
  25. reflex/utils/prerequisites.py +17 -1383
  26. reflex/utils/rename.py +170 -0
  27. reflex/utils/telemetry.py +101 -10
  28. reflex/utils/templates.py +443 -0
  29. reflex/vars/base.py +3 -3
  30. {reflex-0.8.6a1.dist-info → reflex-0.8.7.dist-info}/METADATA +2 -2
  31. {reflex-0.8.6a1.dist-info → reflex-0.8.7.dist-info}/RECORD +34 -33
  32. reflex/.templates/web/utils/client_side_routing.js +0 -45
  33. reflex/components/core/client_side_routing.py +0 -70
  34. reflex/components/core/client_side_routing.pyi +0 -68
  35. {reflex-0.8.6a1.dist-info → reflex-0.8.7.dist-info}/WHEEL +0 -0
  36. {reflex-0.8.6a1.dist-info → reflex-0.8.7.dist-info}/entry_points.txt +0 -0
  37. {reflex-0.8.6a1.dist-info → reflex-0.8.7.dist-info}/licenses/LICENSE +0 -0
@@ -3,26 +3,17 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import contextlib
6
- import dataclasses
7
- import functools
8
6
  import importlib
9
7
  import importlib.metadata
10
8
  import json
11
- import os
12
- import platform
13
9
  import random
14
10
  import re
15
- import shutil
16
11
  import sys
17
- import tempfile
18
12
  import typing
19
- import zipfile
20
- from collections.abc import Sequence
21
13
  from datetime import datetime
22
14
  from pathlib import Path
23
15
  from types import ModuleType
24
16
  from typing import NamedTuple
25
- from urllib.parse import urlparse
26
17
 
27
18
  import click
28
19
  from alembic.util.exc import CommandError
@@ -32,14 +23,11 @@ from redis.asyncio import Redis
32
23
  from redis.exceptions import RedisError
33
24
 
34
25
  from reflex import constants, model
35
- from reflex.compiler import templates
36
26
  from reflex.config import Config, get_config
37
27
  from reflex.environment import environment
38
- from reflex.utils import console, net, path_ops, processes, redir
39
- from reflex.utils.decorator import cached_procedure
40
- from reflex.utils.exceptions import SystemPackageMissingError
28
+ from reflex.utils import console, net, path_ops
29
+ from reflex.utils.decorator import once
41
30
  from reflex.utils.misc import get_module_path
42
- from reflex.utils.registry import get_npm_registry
43
31
 
44
32
  if typing.TYPE_CHECKING:
45
33
  from reflex.app import App
@@ -52,24 +40,6 @@ class AppInfo(NamedTuple):
52
40
  module: ModuleType
53
41
 
54
42
 
55
- @dataclasses.dataclass(frozen=True)
56
- class Template:
57
- """A template for a Reflex app."""
58
-
59
- name: str
60
- description: str
61
- code_url: str
62
-
63
-
64
- @dataclasses.dataclass(frozen=True)
65
- class CpuInfo:
66
- """Model to save cpu info."""
67
-
68
- manufacturer_id: str | None
69
- model_name: str | None
70
- address_width: int | None
71
-
72
-
73
43
  def get_web_dir() -> Path:
74
44
  """Get the working directory for the frontend.
75
45
 
@@ -157,161 +127,7 @@ def set_last_reflex_run_time():
157
127
  )
158
128
 
159
129
 
160
- def check_node_version() -> bool:
161
- """Check the version of Node.js.
162
-
163
- Returns:
164
- Whether the version of Node.js is valid.
165
- """
166
- current_version = get_node_version()
167
- return current_version is not None and current_version >= version.parse(
168
- constants.Node.MIN_VERSION
169
- )
170
-
171
-
172
- def get_node_version() -> version.Version | None:
173
- """Get the version of node.
174
-
175
- Returns:
176
- The version of node.
177
- """
178
- node_path = path_ops.get_node_path()
179
- if node_path is None:
180
- return None
181
- try:
182
- result = processes.new_process([node_path, "-v"], run=True)
183
- # The output will be in the form "vX.Y.Z", but version.parse() can handle it
184
- return version.parse(result.stdout)
185
- except (FileNotFoundError, TypeError):
186
- return None
187
-
188
-
189
- def get_bun_version(bun_path: Path | None = None) -> version.Version | None:
190
- """Get the version of bun.
191
-
192
- Args:
193
- bun_path: The path to the bun executable.
194
-
195
- Returns:
196
- The version of bun.
197
- """
198
- bun_path = bun_path or path_ops.get_bun_path()
199
- if bun_path is None:
200
- return None
201
- try:
202
- # Run the bun -v command and capture the output
203
- result = processes.new_process([str(bun_path), "-v"], run=True)
204
- return version.parse(str(result.stdout))
205
- except FileNotFoundError:
206
- return None
207
- except version.InvalidVersion as e:
208
- console.warn(
209
- f"The detected bun version ({e.args[0]}) is not valid. Defaulting to None."
210
- )
211
- return None
212
-
213
-
214
- def prefer_npm_over_bun() -> bool:
215
- """Check if npm should be preferred over bun.
216
-
217
- Returns:
218
- If npm should be preferred over bun.
219
- """
220
- return npm_escape_hatch() or (
221
- constants.IS_WINDOWS and windows_check_onedrive_in_path()
222
- )
223
-
224
-
225
- def get_nodejs_compatible_package_managers(
226
- raise_on_none: bool = True,
227
- ) -> Sequence[str]:
228
- """Get the package manager executable for installation. Typically, bun is used for installation.
229
-
230
- Args:
231
- raise_on_none: Whether to raise an error if the package manager is not found.
232
-
233
- Returns:
234
- The path to the package manager.
235
-
236
- Raises:
237
- FileNotFoundError: If the package manager is not found and raise_on_none is True.
238
- """
239
- bun_package_manager = (
240
- str(bun_path) if (bun_path := path_ops.get_bun_path()) else None
241
- )
242
-
243
- npm_package_manager = (
244
- str(npm_path) if (npm_path := path_ops.get_npm_path()) else None
245
- )
246
-
247
- if prefer_npm_over_bun():
248
- package_managers = [npm_package_manager, bun_package_manager]
249
- else:
250
- package_managers = [bun_package_manager, npm_package_manager]
251
-
252
- package_managers = list(filter(None, package_managers))
253
-
254
- if not package_managers and raise_on_none:
255
- msg = "Bun or npm not found. You might need to rerun `reflex init` or install either."
256
- raise FileNotFoundError(msg)
257
-
258
- return package_managers
259
-
260
-
261
- def is_outdated_nodejs_installed():
262
- """Check if the installed Node.js version is outdated.
263
-
264
- Returns:
265
- If the installed Node.js version is outdated.
266
- """
267
- current_version = get_node_version()
268
- if current_version is not None and current_version < version.parse(
269
- constants.Node.MIN_VERSION
270
- ):
271
- console.warn(
272
- f"Your version ({current_version}) of Node.js is out of date. Upgrade to {constants.Node.MIN_VERSION} or higher."
273
- )
274
- return True
275
- return False
276
-
277
-
278
- def get_js_package_executor(raise_on_none: bool = False) -> Sequence[Sequence[str]]:
279
- """Get the paths to package managers for running commands. Ordered by preference.
280
- This is currently identical to get_install_package_managers, but may change in the future.
281
-
282
- Args:
283
- raise_on_none: Whether to raise an error if no package managers is not found.
284
-
285
- Returns:
286
- The paths to the package managers as a list of lists, where each list is the command to run and its arguments.
287
-
288
- Raises:
289
- FileNotFoundError: If no package managers are found and raise_on_none is True.
290
- """
291
- bun_package_manager = (
292
- [str(bun_path)] + (["--bun"] if is_outdated_nodejs_installed() else [])
293
- if (bun_path := path_ops.get_bun_path())
294
- else None
295
- )
296
-
297
- npm_package_manager = (
298
- [str(npm_path)] if (npm_path := path_ops.get_npm_path()) else None
299
- )
300
-
301
- if prefer_npm_over_bun():
302
- package_managers = [npm_package_manager, bun_package_manager]
303
- else:
304
- package_managers = [bun_package_manager, npm_package_manager]
305
-
306
- package_managers = list(filter(None, package_managers))
307
-
308
- if not package_managers and raise_on_none:
309
- msg = "Bun or npm not found. You might need to rerun `reflex init` or install either."
310
- raise FileNotFoundError(msg)
311
-
312
- return package_managers
313
-
314
-
130
+ @once
315
131
  def windows_check_onedrive_in_path() -> bool:
316
132
  """For windows, check if oneDrive is present in the project dir path.
317
133
 
@@ -321,15 +137,6 @@ def windows_check_onedrive_in_path() -> bool:
321
137
  return "onedrive" in str(Path.cwd()).lower()
322
138
 
323
139
 
324
- def npm_escape_hatch() -> bool:
325
- """If the user sets REFLEX_USE_NPM, prefer npm over bun.
326
-
327
- Returns:
328
- If the user has set REFLEX_USE_NPM.
329
- """
330
- return environment.REFLEX_USE_NPM.get()
331
-
332
-
333
140
  def _check_app_name(config: Config):
334
141
  """Check if the app name is valid and matches the folder structure.
335
142
 
@@ -347,7 +154,7 @@ def _check_app_name(config: Config):
347
154
  )
348
155
  raise RuntimeError(msg)
349
156
 
350
- from reflex.utils.misc import get_module_path, with_cwd_in_syspath
157
+ from reflex.utils.misc import with_cwd_in_syspath
351
158
 
352
159
  with with_cwd_in_syspath():
353
160
  module_path = get_module_path(config.module)
@@ -358,6 +165,7 @@ def _check_app_name(config: Config):
358
165
  else:
359
166
  msg += f"Ensure app_name='{config.app_name}' in rxconfig.py matches your folder structure."
360
167
  raise ModuleNotFoundError(msg)
168
+ config._app_name_is_valid = True
361
169
 
362
170
 
363
171
  def get_app(reload: bool = False) -> ModuleType:
@@ -377,7 +185,9 @@ def get_app(reload: bool = False) -> ModuleType:
377
185
  try:
378
186
  config = get_config()
379
187
 
380
- _check_app_name(config)
188
+ # Avoid hitting disk when the app name has already been validated in this process.
189
+ if not config._app_name_is_valid:
190
+ _check_app_name(config)
381
191
 
382
192
  module = config.module
383
193
  sys.path.insert(0, str(Path.cwd()))
@@ -433,20 +243,6 @@ def get_and_validate_app(
433
243
  return AppInfo(app=app, module=app_module)
434
244
 
435
245
 
436
- def validate_app(
437
- reload: bool = False, check_if_schema_up_to_date: bool = False
438
- ) -> None:
439
- """Validate the app instance based on the default config.
440
-
441
- Args:
442
- reload: Re-import the app module from disk
443
- check_if_schema_up_to_date: If True, check if the schema is up to date.
444
- """
445
- get_and_validate_app(
446
- reload=reload, check_if_schema_up_to_date=check_if_schema_up_to_date
447
- )
448
-
449
-
450
246
  def get_compiled_app(
451
247
  reload: bool = False,
452
248
  prerender_routes: bool = False,
@@ -467,35 +263,10 @@ def get_compiled_app(
467
263
  app, app_module = get_and_validate_app(
468
264
  reload=reload, check_if_schema_up_to_date=check_if_schema_up_to_date
469
265
  )
470
- # For py3.9 compatibility when redis is used, we MUST add any decorator pages
471
- # before compiling the app in a thread to avoid event loop error (REF-2172).
472
- app._apply_decorated_pages()
473
266
  app._compile(prerender_routes=prerender_routes, dry_run=dry_run)
474
267
  return app_module
475
268
 
476
269
 
477
- def compile_app(
478
- reload: bool = False,
479
- prerender_routes: bool = False,
480
- dry_run: bool = False,
481
- check_if_schema_up_to_date: bool = False,
482
- ) -> None:
483
- """Compile the app module based on the default config.
484
-
485
- Args:
486
- reload: Re-import the app module from disk
487
- prerender_routes: Whether to prerender routes.
488
- dry_run: If True, do not write the compiled app to disk.
489
- check_if_schema_up_to_date: If True, check if the schema is up to date.
490
- """
491
- get_compiled_app(
492
- reload=reload,
493
- prerender_routes=prerender_routes,
494
- dry_run=dry_run,
495
- check_if_schema_up_to_date=check_if_schema_up_to_date,
496
- )
497
-
498
-
499
270
  def compile_or_validate_app(
500
271
  compile: bool = False,
501
272
  check_if_schema_up_to_date: bool = False,
@@ -509,12 +280,12 @@ def compile_or_validate_app(
509
280
  prerender_routes: Whether to prerender routes.
510
281
  """
511
282
  if compile:
512
- compile_app(
283
+ get_compiled_app(
513
284
  check_if_schema_up_to_date=check_if_schema_up_to_date,
514
285
  prerender_routes=prerender_routes,
515
286
  )
516
287
  else:
517
- validate_app(check_if_schema_up_to_date=check_if_schema_up_to_date)
288
+ get_and_validate_app(check_if_schema_up_to_date=check_if_schema_up_to_date)
518
289
 
519
290
 
520
291
  def get_redis() -> Redis | None:
@@ -573,9 +344,9 @@ async def get_redis_status() -> dict[str, bool | None]:
573
344
  """
574
345
  try:
575
346
  status = True
576
- redis_client = get_redis_sync()
347
+ redis_client = get_redis()
577
348
  if redis_client is not None:
578
- redis_client.ping()
349
+ await redis_client.ping()
579
350
  else:
580
351
  status = None
581
352
  except RedisError:
@@ -616,321 +387,6 @@ def validate_app_name(app_name: str | None = None) -> str:
616
387
  return app_name
617
388
 
618
389
 
619
- def rename_path_up_tree(full_path: str | Path, old_name: str, new_name: str) -> Path:
620
- """Rename all instances of `old_name` in the path (file and directories) to `new_name`.
621
- The renaming stops when we reach the directory containing `rxconfig.py`.
622
-
623
- Args:
624
- full_path: The full path to start renaming from.
625
- old_name: The name to be replaced.
626
- new_name: The replacement name.
627
-
628
- Returns:
629
- The updated path after renaming.
630
- """
631
- current_path = Path(full_path)
632
- new_path = None
633
-
634
- while True:
635
- directory, base = current_path.parent, current_path.name
636
- # Stop renaming when we reach the root dir (which contains rxconfig.py)
637
- if current_path.is_dir() and (current_path / "rxconfig.py").exists():
638
- new_path = current_path
639
- break
640
-
641
- if old_name == base.removesuffix(constants.Ext.PY):
642
- new_base = base.replace(old_name, new_name)
643
- new_path = directory / new_base
644
- current_path.rename(new_path)
645
- console.debug(f"Renamed {current_path} -> {new_path}")
646
- current_path = new_path
647
- else:
648
- new_path = current_path
649
-
650
- # Move up the directory tree
651
- current_path = directory
652
-
653
- return new_path
654
-
655
-
656
- def rename_app(new_app_name: str, loglevel: constants.LogLevel):
657
- """Rename the app directory.
658
-
659
- Args:
660
- new_app_name: The new name for the app.
661
- loglevel: The log level to use.
662
-
663
- Raises:
664
- Exit: If the command is not ran in the root dir or the app module cannot be imported.
665
- """
666
- # Set the log level.
667
- console.set_log_level(loglevel)
668
-
669
- if not constants.Config.FILE.exists():
670
- console.error(
671
- "No rxconfig.py found. Make sure you are in the root directory of your app."
672
- )
673
- raise click.exceptions.Exit(1)
674
-
675
- sys.path.insert(0, str(Path.cwd()))
676
-
677
- config = get_config()
678
- module_path = get_module_path(config.module)
679
- if module_path is None:
680
- console.error(f"Could not find module {config.module}.")
681
- raise click.exceptions.Exit(1)
682
-
683
- console.info(f"Renaming app directory to {new_app_name}.")
684
- process_directory(
685
- Path.cwd(),
686
- config.app_name,
687
- new_app_name,
688
- exclude_dirs=[constants.Dirs.WEB, constants.Dirs.APP_ASSETS],
689
- )
690
-
691
- rename_path_up_tree(module_path, config.app_name, new_app_name)
692
-
693
- console.success(f"App directory renamed to [bold]{new_app_name}[/bold].")
694
-
695
-
696
- def rename_imports_and_app_name(file_path: str | Path, old_name: str, new_name: str):
697
- """Rename imports the file using string replacement as well as app_name in rxconfig.py.
698
-
699
- Args:
700
- file_path: The file to process.
701
- old_name: The old name to replace.
702
- new_name: The new name to use.
703
- """
704
- file_path = Path(file_path)
705
- content = file_path.read_text()
706
-
707
- # Replace `from old_name.` or `from old_name` with `from new_name`
708
- content = re.sub(
709
- rf"\bfrom {re.escape(old_name)}(\b|\.|\s)",
710
- lambda match: f"from {new_name}{match.group(1)}",
711
- content,
712
- )
713
-
714
- # Replace `import old_name` with `import new_name`
715
- content = re.sub(
716
- rf"\bimport {re.escape(old_name)}\b",
717
- f"import {new_name}",
718
- content,
719
- )
720
-
721
- # Replace `app_name="old_name"` in rx.Config
722
- content = re.sub(
723
- rf'\bapp_name\s*=\s*["\']{re.escape(old_name)}["\']',
724
- f'app_name="{new_name}"',
725
- content,
726
- )
727
-
728
- # Replace positional argument `"old_name"` in rx.Config
729
- content = re.sub(
730
- rf'\brx\.Config\(\s*["\']{re.escape(old_name)}["\']',
731
- f'rx.Config("{new_name}"',
732
- content,
733
- )
734
-
735
- file_path.write_text(content)
736
-
737
-
738
- def process_directory(
739
- directory: str | Path,
740
- old_name: str,
741
- new_name: str,
742
- exclude_dirs: list | None = None,
743
- extensions: list | None = None,
744
- ):
745
- """Process files with specified extensions in a directory, excluding specified directories.
746
-
747
- Args:
748
- directory: The root directory to process.
749
- old_name: The old name to replace.
750
- new_name: The new name to use.
751
- exclude_dirs: List of directory names to exclude. Defaults to None.
752
- extensions: List of file extensions to process.
753
- """
754
- exclude_dirs = exclude_dirs or []
755
- extensions = extensions or [
756
- constants.Ext.PY,
757
- constants.Ext.MD,
758
- ] # include .md files, typically used in reflex-web.
759
- extensions_set = {ext.lstrip(".") for ext in extensions}
760
- directory = Path(directory)
761
-
762
- root_exclude_dirs = {directory / exclude_dir for exclude_dir in exclude_dirs}
763
-
764
- files = (
765
- p.resolve()
766
- for p in directory.glob("**/*")
767
- if p.is_file() and p.suffix.lstrip(".") in extensions_set
768
- )
769
-
770
- for file_path in files:
771
- if not any(
772
- file_path.is_relative_to(exclude_dir) for exclude_dir in root_exclude_dirs
773
- ):
774
- rename_imports_and_app_name(file_path, old_name, new_name)
775
-
776
-
777
- def create_config(app_name: str):
778
- """Create a new rxconfig file.
779
-
780
- Args:
781
- app_name: The name of the app.
782
- """
783
- # Import here to avoid circular imports.
784
- from reflex.compiler import templates
785
-
786
- config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config"
787
-
788
- console.debug(f"Creating {constants.Config.FILE}")
789
- constants.Config.FILE.write_text(
790
- templates.RXCONFIG.render(app_name=app_name, config_name=config_name)
791
- )
792
-
793
-
794
- def initialize_gitignore(
795
- gitignore_file: Path = constants.GitIgnore.FILE,
796
- files_to_ignore: set[str] | list[str] = constants.GitIgnore.DEFAULTS,
797
- ):
798
- """Initialize the template .gitignore file.
799
-
800
- Args:
801
- gitignore_file: The .gitignore file to create.
802
- files_to_ignore: The files to add to the .gitignore file.
803
- """
804
- # Combine with the current ignored files.
805
- current_ignore: list[str] = []
806
- if gitignore_file.exists():
807
- current_ignore = [ln.strip() for ln in gitignore_file.read_text().splitlines()]
808
-
809
- if files_to_ignore == current_ignore:
810
- console.debug(f"{gitignore_file} already up to date.")
811
- return
812
- files_to_ignore = [ln for ln in files_to_ignore if ln not in current_ignore]
813
- files_to_ignore += current_ignore
814
-
815
- # Write files to the .gitignore file.
816
- gitignore_file.touch(exist_ok=True)
817
- console.debug(f"Creating {gitignore_file}")
818
- gitignore_file.write_text("\n".join(files_to_ignore) + "\n")
819
-
820
-
821
- def initialize_requirements_txt() -> bool:
822
- """Initialize the requirements.txt file.
823
- If absent and no pyproject.toml file exists, generate one for the user.
824
- If the requirements.txt does not have reflex as dependency,
825
- generate a requirement pinning current version and append to
826
- the requirements.txt file.
827
-
828
- Returns:
829
- True if the user has to update the requirements.txt file.
830
-
831
- Raises:
832
- Exit: If the requirements.txt file cannot be read or written to.
833
- """
834
- requirements_file_path = Path(constants.RequirementsTxt.FILE)
835
- if (
836
- not requirements_file_path.exists()
837
- and Path(constants.PyprojectToml.FILE).exists()
838
- ):
839
- return True
840
-
841
- requirements_file_path.touch(exist_ok=True)
842
-
843
- for encoding in [None, "utf-8"]:
844
- try:
845
- content = requirements_file_path.read_text(encoding)
846
- break
847
- except UnicodeDecodeError:
848
- continue
849
- except Exception as e:
850
- console.error(f"Failed to read {requirements_file_path}.")
851
- raise click.exceptions.Exit(1) from e
852
- else:
853
- return True
854
-
855
- for line in content.splitlines():
856
- if re.match(r"^reflex[^a-zA-Z0-9]", line):
857
- console.debug(f"{requirements_file_path} already has reflex as dependency.")
858
- return False
859
-
860
- console.debug(
861
- f"Appending {constants.RequirementsTxt.DEFAULTS_STUB} to {requirements_file_path}"
862
- )
863
- with requirements_file_path.open("a", encoding=encoding) as f:
864
- f.write(
865
- "\n" + constants.RequirementsTxt.DEFAULTS_STUB + constants.Reflex.VERSION
866
- )
867
-
868
- return False
869
-
870
-
871
- def initialize_app_directory(
872
- app_name: str,
873
- template_name: str = constants.Templates.DEFAULT,
874
- template_code_dir_name: str | None = None,
875
- template_dir: Path | None = None,
876
- ):
877
- """Initialize the app directory on reflex init.
878
-
879
- Args:
880
- app_name: The name of the app.
881
- template_name: The name of the template to use.
882
- template_code_dir_name: The name of the code directory in the template.
883
- template_dir: The directory of the template source files.
884
-
885
- Raises:
886
- Exit: If template_name, template_code_dir_name, template_dir combination is not supported.
887
- """
888
- console.log("Initializing the app directory.")
889
-
890
- # By default, use the blank template from local assets.
891
- if template_name == constants.Templates.DEFAULT:
892
- if template_code_dir_name is not None or template_dir is not None:
893
- console.error(
894
- f"Only {template_name=} should be provided, got {template_code_dir_name=}, {template_dir=}."
895
- )
896
- raise click.exceptions.Exit(1)
897
- template_code_dir_name = constants.Templates.Dirs.CODE
898
- template_dir = Path(constants.Templates.Dirs.BASE, "apps", template_name)
899
- else:
900
- if template_code_dir_name is None or template_dir is None:
901
- console.error(
902
- f"For `{template_name}` template, `template_code_dir_name` and `template_dir` should both be provided."
903
- )
904
- raise click.exceptions.Exit(1)
905
-
906
- console.debug(f"Using {template_name=} {template_dir=} {template_code_dir_name=}.")
907
-
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)
914
-
915
- for file in template_dir.iterdir():
916
- # Copy the file to current directory but keep the name the same.
917
- path_ops.cp(str(file), file.name)
918
-
919
- # Rename the template app to the app name.
920
- path_ops.mv(template_code_dir_name, app_name)
921
- path_ops.mv(
922
- Path(app_name) / (template_name + constants.Ext.PY),
923
- Path(app_name) / (app_name + constants.Ext.PY),
924
- )
925
-
926
- # Fix up the imports.
927
- path_ops.find_replace(
928
- app_name,
929
- f"from {template_name}",
930
- f"from {app_name}",
931
- )
932
-
933
-
934
390
  def get_project_hash(raise_on_fail: bool = False) -> int | None:
935
391
  """Get the project hash from the reflex.json file if the file exists.
936
392
 
@@ -947,334 +403,6 @@ def get_project_hash(raise_on_fail: bool = False) -> int | None:
947
403
  return data.get("project_hash")
948
404
 
949
405
 
950
- def initialize_web_directory():
951
- """Initialize the web directory on reflex init."""
952
- console.log("Initializing the web directory.")
953
-
954
- # Reuse the hash if one is already created, so we don't over-write it when running reflex init
955
- project_hash = get_project_hash()
956
-
957
- console.debug(f"Copying {constants.Templates.Dirs.WEB_TEMPLATE} to {get_web_dir()}")
958
- path_ops.copy_tree(constants.Templates.Dirs.WEB_TEMPLATE, str(get_web_dir()))
959
-
960
- console.debug("Initializing the web directory.")
961
- initialize_package_json()
962
-
963
- console.debug("Initializing the bun config file.")
964
- initialize_bun_config()
965
-
966
- console.debug("Initializing the .npmrc file.")
967
- initialize_npmrc()
968
-
969
- console.debug("Initializing the public directory.")
970
- path_ops.mkdir(get_web_dir() / constants.Dirs.PUBLIC)
971
-
972
- console.debug("Initializing the react-router.config.js file.")
973
- update_react_router_config()
974
-
975
- console.debug("Initializing the vite.config.js file.")
976
- initialize_vite_config()
977
-
978
- console.debug("Initializing the reflex.json file.")
979
- # Initialize the reflex json file.
980
- init_reflex_json(project_hash=project_hash)
981
-
982
-
983
- def _compile_package_json():
984
- return templates.PACKAGE_JSON.render(
985
- scripts={
986
- "dev": constants.PackageJson.Commands.DEV,
987
- "export": constants.PackageJson.Commands.EXPORT,
988
- "prod": constants.PackageJson.Commands.PROD,
989
- },
990
- dependencies=constants.PackageJson.DEPENDENCIES,
991
- dev_dependencies=constants.PackageJson.DEV_DEPENDENCIES,
992
- overrides=constants.PackageJson.OVERRIDES,
993
- )
994
-
995
-
996
- def initialize_package_json():
997
- """Render and write in .web the package.json file."""
998
- output_path = get_web_dir() / constants.PackageJson.PATH
999
- output_path.write_text(_compile_package_json())
1000
-
1001
-
1002
- def _compile_vite_config(config: Config):
1003
- # base must have exactly one trailing slash
1004
- base = "/"
1005
- if frontend_path := config.frontend_path.strip("/"):
1006
- base += frontend_path + "/"
1007
- return templates.VITE_CONFIG.render(base=base)
1008
-
1009
-
1010
- def initialize_vite_config():
1011
- """Render and write in .web the vite.config.js file using Reflex config."""
1012
- vite_config_file_path = get_web_dir() / constants.ReactRouter.VITE_CONFIG_FILE
1013
- vite_config_file_path.write_text(_compile_vite_config(get_config()))
1014
-
1015
-
1016
- def initialize_bun_config():
1017
- """Initialize the bun config file."""
1018
- bun_config_path = get_web_dir() / constants.Bun.CONFIG_PATH
1019
-
1020
- if (custom_bunfig := Path(constants.Bun.CONFIG_PATH)).exists():
1021
- bunfig_content = custom_bunfig.read_text()
1022
- console.info(f"Copying custom bunfig.toml inside {get_web_dir()} folder")
1023
- else:
1024
- best_registry = get_npm_registry()
1025
- bunfig_content = constants.Bun.DEFAULT_CONFIG.format(registry=best_registry)
1026
-
1027
- bun_config_path.write_text(bunfig_content)
1028
-
1029
-
1030
- def initialize_npmrc():
1031
- """Initialize the .npmrc file."""
1032
- npmrc_path = get_web_dir() / constants.Node.CONFIG_PATH
1033
-
1034
- if (custom_npmrc := Path(constants.Node.CONFIG_PATH)).exists():
1035
- npmrc_content = custom_npmrc.read_text()
1036
- console.info(f"Copying custom .npmrc inside {get_web_dir()} folder")
1037
- else:
1038
- best_registry = get_npm_registry()
1039
- npmrc_content = constants.Node.DEFAULT_CONFIG.format(registry=best_registry)
1040
-
1041
- npmrc_path.write_text(npmrc_content)
1042
-
1043
-
1044
- def init_reflex_json(project_hash: int | None):
1045
- """Write the hash of the Reflex project to a REFLEX_JSON.
1046
-
1047
- Reuse the hash if one is already created, therefore do not
1048
- overwrite it every time we run the reflex init command
1049
- .
1050
-
1051
- Args:
1052
- project_hash: The app hash.
1053
- """
1054
- if project_hash is not None:
1055
- console.debug(f"Project hash is already set to {project_hash}.")
1056
- else:
1057
- # Get a random project hash.
1058
- project_hash = random.getrandbits(128)
1059
- console.debug(f"Setting project hash to {project_hash}.")
1060
-
1061
- # Write the hash and version to the reflex json file.
1062
- reflex_json = {
1063
- "version": constants.Reflex.VERSION,
1064
- "project_hash": project_hash,
1065
- }
1066
- path_ops.update_json_file(get_web_dir() / constants.Reflex.JSON, reflex_json)
1067
-
1068
-
1069
- def update_react_router_config(prerender_routes: bool = False):
1070
- """Update react-router.config.js config from Reflex config.
1071
-
1072
- Args:
1073
- prerender_routes: Whether to enable prerendering of routes.
1074
- """
1075
- react_router_config_file_path = get_web_dir() / constants.ReactRouter.CONFIG_FILE
1076
-
1077
- new_react_router_config = _update_react_router_config(
1078
- get_config(), prerender_routes=prerender_routes
1079
- )
1080
-
1081
- # Overwriting the config file triggers a full server reload, so make sure
1082
- # there is actually a diff.
1083
- old_react_router_config = (
1084
- react_router_config_file_path.read_text()
1085
- if react_router_config_file_path.exists()
1086
- else ""
1087
- )
1088
- if old_react_router_config != new_react_router_config:
1089
- react_router_config_file_path.write_text(new_react_router_config)
1090
-
1091
-
1092
- def _update_react_router_config(config: Config, prerender_routes: bool = False):
1093
- react_router_config = {
1094
- "basename": "/" + (config.frontend_path or "").removeprefix("/"),
1095
- "future": {
1096
- "unstable_optimizeDeps": True,
1097
- },
1098
- "ssr": False,
1099
- }
1100
-
1101
- if prerender_routes:
1102
- react_router_config["prerender"] = True
1103
- react_router_config["build"] = constants.Dirs.BUILD_DIR
1104
-
1105
- return f"export default {json.dumps(react_router_config)};"
1106
-
1107
-
1108
- def remove_existing_bun_installation():
1109
- """Remove existing bun installation."""
1110
- console.debug("Removing existing bun installation.")
1111
- if Path(get_config().bun_path).exists():
1112
- path_ops.rm(constants.Bun.ROOT_PATH)
1113
-
1114
-
1115
- def download_and_run(url: str, *args, show_status: bool = False, **env):
1116
- """Download and run a script.
1117
-
1118
- Args:
1119
- url: The url of the script.
1120
- args: The arguments to pass to the script.
1121
- show_status: Whether to show the status of the script.
1122
- env: The environment variables to use.
1123
-
1124
- Raises:
1125
- Exit: If the script fails to download.
1126
- """
1127
- import httpx
1128
-
1129
- # Download the script
1130
- console.debug(f"Downloading {url}")
1131
- try:
1132
- response = net.get(url)
1133
- response.raise_for_status()
1134
- except httpx.HTTPError as e:
1135
- console.error(
1136
- f"Failed to download bun install script. You can install or update bun manually from https://bun.com \n{e}"
1137
- )
1138
- raise click.exceptions.Exit(1) from None
1139
-
1140
- # Save the script to a temporary file.
1141
- with tempfile.NamedTemporaryFile() as tempfile_file:
1142
- script = Path(tempfile_file.name)
1143
-
1144
- script.write_text(response.text)
1145
-
1146
- # Run the script.
1147
- env = {**os.environ, **env}
1148
- process = processes.new_process(["bash", str(script), *args], env=env)
1149
- show = processes.show_status if show_status else processes.show_logs
1150
- show(f"Installing {url}", process)
1151
-
1152
-
1153
- def install_bun():
1154
- """Install bun onto the user's system.
1155
-
1156
- Raises:
1157
- SystemPackageMissingError: If "unzip" is missing.
1158
- """
1159
- one_drive_in_path = windows_check_onedrive_in_path()
1160
- if constants.IS_WINDOWS and one_drive_in_path:
1161
- console.warn(
1162
- "Creating project directories in OneDrive is not recommended for bun usage on windows. This will fallback to npm."
1163
- )
1164
-
1165
- bun_path = path_ops.get_bun_path()
1166
-
1167
- # Skip if bun is already installed.
1168
- if (
1169
- bun_path
1170
- and (current_version := get_bun_version(bun_path=bun_path))
1171
- and current_version >= version.parse(constants.Bun.MIN_VERSION)
1172
- ):
1173
- console.debug("Skipping bun installation as it is already installed.")
1174
- return
1175
-
1176
- if bun_path and path_ops.use_system_bun():
1177
- validate_bun(bun_path=bun_path)
1178
- return
1179
-
1180
- # if unzip is installed
1181
- if constants.IS_WINDOWS:
1182
- processes.new_process(
1183
- [
1184
- "powershell",
1185
- "-c",
1186
- f"irm {constants.Bun.WINDOWS_INSTALL_URL}|iex",
1187
- ],
1188
- env={
1189
- "BUN_INSTALL": str(constants.Bun.ROOT_PATH),
1190
- "BUN_VERSION": constants.Bun.VERSION,
1191
- },
1192
- shell=True,
1193
- run=True,
1194
- show_logs=console.is_debug(),
1195
- )
1196
- else:
1197
- if path_ops.which("unzip") is None:
1198
- msg = "unzip"
1199
- raise SystemPackageMissingError(msg)
1200
-
1201
- # Run the bun install script.
1202
- download_and_run(
1203
- constants.Bun.INSTALL_URL,
1204
- f"bun-v{constants.Bun.VERSION}",
1205
- BUN_INSTALL=str(constants.Bun.ROOT_PATH),
1206
- BUN_VERSION=str(constants.Bun.VERSION),
1207
- )
1208
-
1209
-
1210
- @cached_procedure(
1211
- cache_file_path=lambda: get_web_dir() / "reflex.install_frontend_packages.cached",
1212
- payload_fn=lambda packages, config: f"{sorted(packages)!r},{config.json()}",
1213
- )
1214
- def install_frontend_packages(packages: set[str], config: Config):
1215
- """Installs the base and custom frontend packages.
1216
-
1217
- Args:
1218
- packages: A list of package names to be installed.
1219
- config: The config object.
1220
-
1221
- Example:
1222
- >>> install_frontend_packages(["react", "react-dom"], get_config())
1223
- """
1224
- install_package_managers = get_nodejs_compatible_package_managers(
1225
- raise_on_none=True
1226
- )
1227
-
1228
- env = (
1229
- {
1230
- "NODE_TLS_REJECT_UNAUTHORIZED": "0",
1231
- }
1232
- if environment.SSL_NO_VERIFY.get()
1233
- else {}
1234
- )
1235
-
1236
- primary_package_manager = install_package_managers[0]
1237
- fallbacks = install_package_managers[1:]
1238
-
1239
- run_package_manager = functools.partial(
1240
- processes.run_process_with_fallbacks,
1241
- fallbacks=fallbacks,
1242
- analytics_enabled=True,
1243
- cwd=get_web_dir(),
1244
- shell=constants.IS_WINDOWS,
1245
- env=env,
1246
- )
1247
-
1248
- run_package_manager(
1249
- [primary_package_manager, "install", "--legacy-peer-deps"],
1250
- show_status_message="Installing base frontend packages",
1251
- )
1252
-
1253
- development_deps: set[str] = set()
1254
- for plugin in config.plugins:
1255
- development_deps.update(plugin.get_frontend_development_dependencies())
1256
- packages.update(plugin.get_frontend_dependencies())
1257
-
1258
- if development_deps:
1259
- run_package_manager(
1260
- [
1261
- primary_package_manager,
1262
- "add",
1263
- "--legacy-peer-deps",
1264
- "-d",
1265
- *development_deps,
1266
- ],
1267
- show_status_message="Installing frontend development dependencies",
1268
- )
1269
-
1270
- # Install custom packages defined in frontend_packages
1271
- if packages:
1272
- run_package_manager(
1273
- [primary_package_manager, "add", "--legacy-peer-deps", *packages],
1274
- show_status_message="Installing frontend packages from config and components",
1275
- )
1276
-
1277
-
1278
406
  def check_running_mode(frontend: bool, backend: bool) -> tuple[bool, bool]:
1279
407
  """Check if the app is running in frontend or backend mode.
1280
408
 
@@ -1317,8 +445,7 @@ def needs_reinit() -> bool:
1317
445
  if not get_web_dir().exists():
1318
446
  return True
1319
447
 
1320
- # If the template is out of date, then we need to re-init
1321
- if not is_latest_template():
448
+ if not _is_app_compiled_with_same_reflex_version():
1322
449
  return True
1323
450
 
1324
451
  if constants.IS_WINDOWS:
@@ -1334,12 +461,7 @@ def needs_reinit() -> bool:
1334
461
  return False
1335
462
 
1336
463
 
1337
- def is_latest_template() -> bool:
1338
- """Whether the app is using the latest template.
1339
-
1340
- Returns:
1341
- Whether the app is using the latest template.
1342
- """
464
+ def _is_app_compiled_with_same_reflex_version() -> bool:
1343
465
  json_file = get_web_dir() / constants.Reflex.JSON
1344
466
  if not json_file.exists():
1345
467
  return False
@@ -1347,59 +469,6 @@ def is_latest_template() -> bool:
1347
469
  return app_version == constants.Reflex.VERSION
1348
470
 
1349
471
 
1350
- def validate_bun(bun_path: Path | None = None):
1351
- """Validate bun if a custom bun path is specified to ensure the bun version meets requirements.
1352
-
1353
- Args:
1354
- bun_path: The path to the bun executable. If None, the default bun path is used.
1355
-
1356
- Raises:
1357
- Exit: If custom specified bun does not exist or does not meet requirements.
1358
- """
1359
- bun_path = bun_path or path_ops.get_bun_path()
1360
-
1361
- if bun_path is None:
1362
- return
1363
-
1364
- if not path_ops.samefile(bun_path, constants.Bun.DEFAULT_PATH):
1365
- console.info(f"Using custom Bun path: {bun_path}")
1366
- bun_version = get_bun_version()
1367
- if bun_version is None:
1368
- console.error(
1369
- "Failed to obtain bun version. Make sure the specified bun path in your config is correct."
1370
- )
1371
- raise click.exceptions.Exit(1)
1372
- if bun_version < version.parse(constants.Bun.MIN_VERSION):
1373
- console.warn(
1374
- f"Reflex requires bun version {constants.Bun.MIN_VERSION} or higher to run, but the detected version is "
1375
- f"{bun_version}. If you have specified a custom bun path in your config, make sure to provide one "
1376
- f"that satisfies the minimum version requirement. You can upgrade bun by running [bold]bun upgrade[/bold]."
1377
- )
1378
-
1379
-
1380
- def validate_frontend_dependencies(init: bool = True):
1381
- """Validate frontend dependencies to ensure they meet requirements.
1382
-
1383
- Args:
1384
- init: whether running `reflex init`
1385
-
1386
- Raises:
1387
- Exit: If the package manager is invalid.
1388
- """
1389
- if not init:
1390
- try:
1391
- get_js_package_executor(raise_on_none=True)
1392
- except FileNotFoundError as e:
1393
- raise click.exceptions.Exit(1) from e
1394
-
1395
- if prefer_npm_over_bun() and not check_node_version():
1396
- node_version = get_node_version()
1397
- console.error(
1398
- f"Reflex requires node version {constants.Node.MIN_VERSION} or higher to run, but the detected version is {node_version}",
1399
- )
1400
- raise click.exceptions.Exit(1)
1401
-
1402
-
1403
472
  def ensure_reflex_installation_id() -> int | None:
1404
473
  """Ensures that a reflex distinct id has been generated and stored in the reflex directory.
1405
474
 
@@ -1441,6 +510,9 @@ def initialize_reflex_user_directory():
1441
510
 
1442
511
  def initialize_frontend_dependencies():
1443
512
  """Initialize all the frontend dependencies."""
513
+ from reflex.utils.frontend_skeleton import initialize_web_directory
514
+ from reflex.utils.js_runtimes import install_bun, validate_frontend_dependencies
515
+
1444
516
  # validate dependencies before install
1445
517
  console.debug("Validating frontend dependencies.")
1446
518
  validate_frontend_dependencies()
@@ -1507,444 +579,6 @@ def check_schema_up_to_date():
1507
579
  )
1508
580
 
1509
581
 
1510
- def prompt_for_template_options(templates: list[Template]) -> str:
1511
- """Prompt the user to specify a template.
1512
-
1513
- Args:
1514
- templates: The templates to choose from.
1515
-
1516
- Returns:
1517
- The template name the user selects.
1518
-
1519
- Raises:
1520
- Exit: If the user does not select a template.
1521
- """
1522
- # Show the user the URLs of each template to preview.
1523
- console.print("\nGet started with a template:")
1524
-
1525
- # Prompt the user to select a template.
1526
- for index, template in enumerate(templates):
1527
- console.print(f"({index}) {template.description}")
1528
-
1529
- template = console.ask(
1530
- "Which template would you like to use?",
1531
- choices=[str(i) for i in range(len(templates))],
1532
- show_choices=False,
1533
- default="0",
1534
- )
1535
-
1536
- if not template:
1537
- console.error("No template selected.")
1538
- raise click.exceptions.Exit(1)
1539
-
1540
- try:
1541
- template_index = int(template)
1542
- except ValueError:
1543
- console.error("Invalid template selected.")
1544
- raise click.exceptions.Exit(1) from None
1545
-
1546
- if template_index < 0 or template_index >= len(templates):
1547
- console.error("Invalid template selected.")
1548
- raise click.exceptions.Exit(1)
1549
-
1550
- # Return the template.
1551
- return templates[template_index].name
1552
-
1553
-
1554
- def fetch_app_templates(version: str) -> dict[str, Template]:
1555
- """Fetch a dict of templates from the templates repo using github API.
1556
-
1557
- Args:
1558
- version: The version of the templates to fetch.
1559
-
1560
- Returns:
1561
- The dict of templates.
1562
- """
1563
-
1564
- def get_release_by_tag(tag: str) -> dict | None:
1565
- response = net.get(constants.Reflex.RELEASES_URL)
1566
- response.raise_for_status()
1567
- releases = response.json()
1568
- for release in releases:
1569
- if release["tag_name"] == f"v{tag}":
1570
- return release
1571
- return None
1572
-
1573
- release = get_release_by_tag(version)
1574
- if release is None:
1575
- console.warn(f"No templates known for version {version}")
1576
- return {}
1577
-
1578
- assets = release.get("assets", [])
1579
- asset = next((a for a in assets if a["name"] == "templates.json"), None)
1580
- if asset is None:
1581
- console.warn(f"Templates metadata not found for version {version}")
1582
- return {}
1583
- templates_url = asset["browser_download_url"]
1584
-
1585
- templates_data = net.get(templates_url, follow_redirects=True).json()["templates"]
1586
-
1587
- for template in templates_data:
1588
- if template["name"] == "blank":
1589
- template["code_url"] = ""
1590
- continue
1591
- template["code_url"] = next(
1592
- (
1593
- a["browser_download_url"]
1594
- for a in assets
1595
- if a["name"] == f"{template['name']}.zip"
1596
- ),
1597
- None,
1598
- )
1599
-
1600
- filtered_templates = {}
1601
- for tp in templates_data:
1602
- if tp["hidden"] or tp["code_url"] is None:
1603
- continue
1604
- known_fields = {f.name for f in dataclasses.fields(Template)}
1605
- filtered_templates[tp["name"]] = Template(
1606
- **{k: v for k, v in tp.items() if k in known_fields}
1607
- )
1608
- return filtered_templates
1609
-
1610
-
1611
- def create_config_init_app_from_remote_template(app_name: str, template_url: str):
1612
- """Create new rxconfig and initialize app using a remote template.
1613
-
1614
- Args:
1615
- app_name: The name of the app.
1616
- template_url: The path to the template source code as a zip file.
1617
-
1618
- Raises:
1619
- Exit: If any download, file operations fail or unexpected zip file format.
1620
-
1621
- """
1622
- import httpx
1623
-
1624
- # Create a temp directory for the zip download.
1625
- try:
1626
- temp_dir = tempfile.mkdtemp()
1627
- except OSError as ose:
1628
- console.error(f"Failed to create temp directory for download: {ose}")
1629
- raise click.exceptions.Exit(1) from ose
1630
-
1631
- # Use httpx GET with redirects to download the zip file.
1632
- zip_file_path: Path = Path(temp_dir) / "template.zip"
1633
- try:
1634
- # Note: following redirects can be risky. We only allow this for reflex built templates at the moment.
1635
- response = net.get(template_url, follow_redirects=True)
1636
- console.debug(f"Server responded download request: {response}")
1637
- response.raise_for_status()
1638
- except httpx.HTTPError as he:
1639
- console.error(f"Failed to download the template: {he}")
1640
- raise click.exceptions.Exit(1) from he
1641
- try:
1642
- zip_file_path.write_bytes(response.content)
1643
- console.debug(f"Downloaded the zip to {zip_file_path}")
1644
- except OSError as ose:
1645
- console.error(f"Unable to write the downloaded zip to disk {ose}")
1646
- raise click.exceptions.Exit(1) from ose
1647
-
1648
- # Create a temp directory for the zip extraction.
1649
- try:
1650
- unzip_dir = Path(tempfile.mkdtemp())
1651
- except OSError as ose:
1652
- console.error(f"Failed to create temp directory for extracting zip: {ose}")
1653
- raise click.exceptions.Exit(1) from ose
1654
-
1655
- try:
1656
- zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir)
1657
- # The zip file downloaded from github looks like:
1658
- # repo-name-branch/**/*, so we need to remove the top level directory.
1659
- except Exception as uze:
1660
- console.error(f"Failed to unzip the template: {uze}")
1661
- raise click.exceptions.Exit(1) from uze
1662
-
1663
- if len(subdirs := list(unzip_dir.iterdir())) != 1:
1664
- console.error(f"Expected one directory in the zip, found {subdirs}")
1665
- raise click.exceptions.Exit(1)
1666
-
1667
- template_dir = unzip_dir / subdirs[0]
1668
- console.debug(f"Template folder is located at {template_dir}")
1669
-
1670
- # Move the rxconfig file here first.
1671
- path_ops.mv(str(template_dir / constants.Config.FILE), constants.Config.FILE)
1672
- new_config = get_config(reload=True)
1673
-
1674
- # Get the template app's name from rxconfig in case it is different than
1675
- # the source code repo name on github.
1676
- template_name = new_config.app_name
1677
-
1678
- create_config(app_name)
1679
- initialize_app_directory(
1680
- app_name,
1681
- template_name=template_name,
1682
- template_code_dir_name=template_name,
1683
- template_dir=template_dir,
1684
- )
1685
- req_file = Path("requirements.txt")
1686
- if req_file.exists() and len(req_file.read_text().splitlines()) > 1:
1687
- console.info(
1688
- "Run `pip install -r requirements.txt` to install the required python packages for this template."
1689
- )
1690
- # Clean up the temp directories.
1691
- shutil.rmtree(temp_dir)
1692
- shutil.rmtree(unzip_dir)
1693
-
1694
-
1695
- def initialize_default_app(app_name: str):
1696
- """Initialize the default app.
1697
-
1698
- Args:
1699
- app_name: The name of the app.
1700
- """
1701
- create_config(app_name)
1702
- initialize_app_directory(app_name)
1703
-
1704
-
1705
- def validate_and_create_app_using_remote_template(
1706
- app_name: str, template: str, templates: dict[str, Template]
1707
- ):
1708
- """Validate and create an app using a remote template.
1709
-
1710
- Args:
1711
- app_name: The name of the app.
1712
- template: The name of the template.
1713
- templates: The available templates.
1714
-
1715
- Raises:
1716
- Exit: If the template is not found.
1717
- """
1718
- # If user selects a template, it needs to exist.
1719
- if template in templates:
1720
- from reflex_cli.v2.utils import hosting
1721
-
1722
- authenticated_token = hosting.authenticated_token()
1723
- if not authenticated_token or not authenticated_token[0]:
1724
- console.print(
1725
- f"Please use `reflex login` to access the '{template}' template."
1726
- )
1727
- raise click.exceptions.Exit(3)
1728
-
1729
- template_url = templates[template].code_url
1730
- else:
1731
- template_parsed_url = urlparse(template)
1732
- # Check if the template is a github repo.
1733
- if template_parsed_url.hostname == "github.com":
1734
- path = template_parsed_url.path.strip("/").removesuffix(".git")
1735
- template_url = f"https://github.com/{path}/archive/main.zip"
1736
- else:
1737
- console.error(f"Template `{template}` not found or invalid.")
1738
- raise click.exceptions.Exit(1)
1739
-
1740
- if template_url is None:
1741
- return
1742
-
1743
- create_config_init_app_from_remote_template(
1744
- app_name=app_name, template_url=template_url
1745
- )
1746
-
1747
-
1748
- def fetch_remote_templates(
1749
- template: str,
1750
- ) -> tuple[str, dict[str, Template]]:
1751
- """Fetch the available remote templates.
1752
-
1753
- Args:
1754
- template: The name of the template.
1755
-
1756
- Returns:
1757
- The selected template and the available templates.
1758
- """
1759
- available_templates = {}
1760
-
1761
- try:
1762
- # Get the available templates
1763
- available_templates = fetch_app_templates(constants.Reflex.VERSION)
1764
- except Exception as e:
1765
- console.warn("Failed to fetch templates. Falling back to default template.")
1766
- console.debug(f"Error while fetching templates: {e}")
1767
- template = constants.Templates.DEFAULT
1768
-
1769
- return template, available_templates
1770
-
1771
-
1772
- def initialize_app(app_name: str, template: str | None = None) -> str | None:
1773
- """Initialize the app either from a remote template or a blank app. If the config file exists, it is considered as reinit.
1774
-
1775
- Args:
1776
- app_name: The name of the app.
1777
- template: The name of the template to use.
1778
-
1779
- Returns:
1780
- The name of the template.
1781
-
1782
- Raises:
1783
- Exit: If the template is not valid or unspecified.
1784
- """
1785
- # Local imports to avoid circular imports.
1786
- from reflex.utils import telemetry
1787
-
1788
- # Check if the app is already initialized.
1789
- if constants.Config.FILE.exists():
1790
- telemetry.send("reinit")
1791
- return None
1792
-
1793
- templates: dict[str, Template] = {}
1794
-
1795
- # Don't fetch app templates if the user directly asked for DEFAULT.
1796
- if template is not None and (template not in (constants.Templates.DEFAULT,)):
1797
- template, templates = fetch_remote_templates(template)
1798
-
1799
- if template is None:
1800
- template = prompt_for_template_options(get_init_cli_prompt_options())
1801
-
1802
- if template == constants.Templates.CHOOSE_TEMPLATES:
1803
- redir.reflex_templates()
1804
- raise click.exceptions.Exit(0)
1805
-
1806
- if template == constants.Templates.AI:
1807
- redir.reflex_build_redirect()
1808
- raise click.exceptions.Exit(0)
1809
-
1810
- # If the blank template is selected, create a blank app.
1811
- if template == constants.Templates.DEFAULT:
1812
- # Default app creation behavior: a blank app.
1813
- initialize_default_app(app_name)
1814
- else:
1815
- validate_and_create_app_using_remote_template(
1816
- app_name=app_name, template=template, templates=templates
1817
- )
1818
-
1819
- telemetry.send("init", template=template)
1820
-
1821
- return template
1822
-
1823
-
1824
- def get_init_cli_prompt_options() -> list[Template]:
1825
- """Get the CLI options for initializing a Reflex app.
1826
-
1827
- Returns:
1828
- The CLI options.
1829
- """
1830
- return [
1831
- Template(
1832
- name=constants.Templates.DEFAULT,
1833
- description="A blank Reflex app.",
1834
- code_url="",
1835
- ),
1836
- Template(
1837
- name=constants.Templates.AI,
1838
- description="[bold]Try our free AI builder.",
1839
- code_url="",
1840
- ),
1841
- Template(
1842
- name=constants.Templates.CHOOSE_TEMPLATES,
1843
- description="Premade templates built by the Reflex team.",
1844
- code_url="",
1845
- ),
1846
- ]
1847
-
1848
-
1849
- def format_address_width(address_width: str | None) -> int | None:
1850
- """Cast address width to an int.
1851
-
1852
- Args:
1853
- address_width: The address width.
1854
-
1855
- Returns:
1856
- Address width int
1857
- """
1858
- try:
1859
- return int(address_width) if address_width else None
1860
- except ValueError:
1861
- return None
1862
-
1863
-
1864
- def _retrieve_cpu_info() -> CpuInfo | None:
1865
- """Retrieve the CPU info of the host.
1866
-
1867
- Returns:
1868
- The CPU info.
1869
- """
1870
- platform_os = platform.system()
1871
- cpuinfo = {}
1872
- try:
1873
- if platform_os == "Windows":
1874
- cmd = 'powershell -Command "Get-CimInstance Win32_Processor | Select-Object -First 1 | Select-Object AddressWidth,Manufacturer,Name | ConvertTo-Json"'
1875
- output = processes.execute_command_and_return_output(cmd)
1876
- if output:
1877
- cpu_data = json.loads(output)
1878
- cpuinfo["address_width"] = cpu_data["AddressWidth"]
1879
- cpuinfo["manufacturer_id"] = cpu_data["Manufacturer"]
1880
- cpuinfo["model_name"] = cpu_data["Name"]
1881
- elif platform_os == "Linux":
1882
- output = processes.execute_command_and_return_output("lscpu")
1883
- if output:
1884
- lines = output.split("\n")
1885
- for line in lines:
1886
- if "Architecture" in line:
1887
- cpuinfo["address_width"] = (
1888
- 64 if line.split(":")[1].strip() == "x86_64" else 32
1889
- )
1890
- if "Vendor ID:" in line:
1891
- cpuinfo["manufacturer_id"] = line.split(":")[1].strip()
1892
- if "Model name" in line:
1893
- cpuinfo["model_name"] = line.split(":")[1].strip()
1894
- elif platform_os == "Darwin":
1895
- cpuinfo["address_width"] = format_address_width(
1896
- processes.execute_command_and_return_output("getconf LONG_BIT")
1897
- )
1898
- cpuinfo["manufacturer_id"] = processes.execute_command_and_return_output(
1899
- "sysctl -n machdep.cpu.brand_string"
1900
- )
1901
- cpuinfo["model_name"] = processes.execute_command_and_return_output(
1902
- "uname -m"
1903
- )
1904
- except Exception as err:
1905
- console.error(f"Failed to retrieve CPU info. {err}")
1906
- return None
1907
-
1908
- return (
1909
- CpuInfo(
1910
- manufacturer_id=cpuinfo.get("manufacturer_id"),
1911
- model_name=cpuinfo.get("model_name"),
1912
- address_width=cpuinfo.get("address_width"),
1913
- )
1914
- if cpuinfo
1915
- else None
1916
- )
1917
-
1918
-
1919
- @functools.cache
1920
- def get_cpu_info() -> CpuInfo | None:
1921
- """Get the CPU info of the underlining host.
1922
-
1923
- Returns:
1924
- The CPU info.
1925
- """
1926
- cpu_info_file = environment.REFLEX_DIR.get() / "cpu_info.json"
1927
- if cpu_info_file.exists() and (cpu_info := json.loads(cpu_info_file.read_text())):
1928
- return CpuInfo(**cpu_info)
1929
- cpu_info = _retrieve_cpu_info()
1930
- if cpu_info:
1931
- cpu_info_file.parent.mkdir(parents=True, exist_ok=True)
1932
- cpu_info_file.write_text(json.dumps(dataclasses.asdict(cpu_info)))
1933
- return cpu_info
1934
-
1935
-
1936
- def is_generation_hash(template: str) -> bool:
1937
- """Check if the template looks like a generation hash.
1938
-
1939
- Args:
1940
- template: The template name.
1941
-
1942
- Returns:
1943
- True if the template is composed of 32 or more hex characters.
1944
- """
1945
- return re.match(r"^[0-9a-f]{32,}$", template) is not None
1946
-
1947
-
1948
582
  def get_user_tier():
1949
583
  """Get the current user's tier.
1950
584