reflex 0.8.12a1__py3-none-any.whl → 0.8.13a1__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.

@@ -8,7 +8,7 @@ from reflex.components.component import Component, MemoizationLeaf, NoSSRCompone
8
8
  class Recharts(Component):
9
9
  """A component that wraps a recharts lib."""
10
10
 
11
- library = "recharts@3.2.0"
11
+ library = "recharts@3.2.1"
12
12
 
13
13
  def _get_style(self) -> dict:
14
14
  return {"wrapperStyle": self.style}
@@ -17,7 +17,7 @@ class Recharts(Component):
17
17
  class RechartsCharts(NoSSRComponent, MemoizationLeaf):
18
18
  """A component that wraps a recharts lib."""
19
19
 
20
- library = "recharts@3.2.0"
20
+ library = "recharts@3.2.1"
21
21
 
22
22
 
23
23
  LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"]
@@ -96,4 +96,6 @@ class Color:
96
96
  Returns:
97
97
  The formatted color.
98
98
  """
99
- return format_color(self.color, self.shade, self.alpha)
99
+ from reflex.vars import LiteralColorVar
100
+
101
+ return LiteralColorVar.create(self).__format__(format_spec)
@@ -143,11 +143,11 @@ class PackageJson(SimpleNamespace):
143
143
  "postcss-import": "16.1.1",
144
144
  "@react-router/dev": _react_router_version,
145
145
  "@react-router/fs-routes": _react_router_version,
146
- "vite": "npm:rolldown-vite@7.1.9",
146
+ "vite": "npm:rolldown-vite@7.1.12",
147
147
  }
148
148
  OVERRIDES = {
149
149
  # This should always match the `react` version in DEPENDENCIES for recharts compatibility.
150
150
  "react-is": _react_version,
151
151
  "cookie": "1.0.2",
152
- "vite": "npm:rolldown-vite@7.1.9",
152
+ "vite": "npm:rolldown-vite@7.1.12",
153
153
  }
reflex/environment.py CHANGED
@@ -657,6 +657,9 @@ class EnvironmentVariables:
657
657
  # Whether to force a full reload on changes.
658
658
  VITE_FORCE_FULL_RELOAD: EnvVar[bool] = env_var(False)
659
659
 
660
+ # Whether to enable SSR for the frontend.
661
+ REFLEX_SSR: EnvVar[bool] = env_var(True)
662
+
660
663
 
661
664
  environment = EnvironmentVariables()
662
665
 
reflex/istate/proxy.py CHANGED
@@ -71,10 +71,15 @@ class StateProxy(wrapt.ObjectProxy):
71
71
  state_instance: The state instance to proxy.
72
72
  parent_state_proxy: The parent state proxy, for linked mutability and context tracking.
73
73
  """
74
+ from reflex.state import _substate_key
75
+
74
76
  super().__init__(state_instance)
75
- # compile is not relevant to backend logic
76
77
  self._self_app = prerequisites.get_and_validate_app().app
77
78
  self._self_substate_path = tuple(state_instance.get_full_name().split("."))
79
+ self._self_substate_token = _substate_key(
80
+ state_instance.router.session.client_token,
81
+ self._self_substate_path,
82
+ )
78
83
  self._self_actx = None
79
84
  self._self_mutable = False
80
85
  self._self_actx_lock = asyncio.Lock()
@@ -127,16 +132,9 @@ class StateProxy(wrapt.ObjectProxy):
127
132
  msg = "The state is already mutable. Do not nest `async with self` blocks."
128
133
  raise ImmutableStateError(msg)
129
134
 
130
- from reflex.state import _substate_key
131
-
132
135
  await self._self_actx_lock.acquire()
133
136
  self._self_actx_lock_holder = current_task
134
- self._self_actx = self._self_app.modify_state(
135
- token=_substate_key(
136
- self.__wrapped__.router.session.client_token,
137
- self._self_substate_path,
138
- )
139
- )
137
+ self._self_actx = self._self_app.modify_state(token=self._self_substate_token)
140
138
  mutable_state = await self._self_actx.__aenter__()
141
139
  super().__setattr__(
142
140
  "__wrapped__", mutable_state.get_substate(self._self_substate_path)
@@ -378,17 +376,6 @@ class MutableProxy(wrapt.ObjectProxy):
378
376
  pydantic.BaseModel.__dict__
379
377
  )
380
378
 
381
- # These types will be wrapped in MutableProxy
382
- __mutable_types__ = (
383
- list,
384
- dict,
385
- set,
386
- Base,
387
- DeclarativeBase,
388
- BaseModelV2,
389
- BaseModelV1,
390
- )
391
-
392
379
  # Dynamically generated classes for tracking dataclass mutations.
393
380
  __dataclass_proxies__: dict[type, type] = {}
394
381
 
@@ -469,20 +456,6 @@ class MutableProxy(wrapt.ObjectProxy):
469
456
  return wrapped(*args, **(kwargs or {}))
470
457
  return None
471
458
 
472
- @classmethod
473
- def _is_mutable_type(cls, value: Any) -> bool:
474
- """Check if a value is of a mutable type and should be wrapped.
475
-
476
- Args:
477
- value: The value to check.
478
-
479
- Returns:
480
- Whether the value is of a mutable type.
481
- """
482
- return isinstance(value, cls.__mutable_types__) or (
483
- dataclasses.is_dataclass(value) and not isinstance(value, Var)
484
- )
485
-
486
459
  @staticmethod
487
460
  def _is_called_from_dataclasses_internal() -> bool:
488
461
  """Check if the current function is called from dataclasses helper.
@@ -514,7 +487,7 @@ class MutableProxy(wrapt.ObjectProxy):
514
487
  if self._is_called_from_dataclasses_internal():
515
488
  return value
516
489
  # Recursively wrap mutable types, but do not re-wrap MutableProxy instances.
517
- if self._is_mutable_type(value) and not isinstance(value, MutableProxy):
490
+ if is_mutable_type(type(value)) and not isinstance(value, MutableProxy):
518
491
  base_cls = globals()[self.__base_proxy__]
519
492
  return base_cls(
520
493
  wrapped=value,
@@ -575,7 +548,7 @@ class MutableProxy(wrapt.ObjectProxy):
575
548
  self._wrap_recursive_decorator,
576
549
  )
577
550
 
578
- if self._is_mutable_type(value) and __name not in (
551
+ if is_mutable_type(type(value)) and __name not in (
579
552
  "__wrapped__",
580
553
  "_self_state",
581
554
  "__dict__",
@@ -764,3 +737,30 @@ class ImmutableMutableProxy(MutableProxy):
764
737
  return super()._mark_dirty(
765
738
  wrapped=wrapped, instance=instance, args=args, kwargs=kwargs
766
739
  )
740
+
741
+
742
+ # These types will be wrapped in MutableProxy
743
+ MUTABLE_TYPES = (
744
+ list,
745
+ dict,
746
+ set,
747
+ Base,
748
+ DeclarativeBase,
749
+ BaseModelV2,
750
+ BaseModelV1,
751
+ )
752
+
753
+
754
+ @functools.lru_cache(maxsize=1024)
755
+ def is_mutable_type(type_: type) -> bool:
756
+ """Check if a type is mutable and should be wrapped.
757
+
758
+ Args:
759
+ type_: The type to check.
760
+
761
+ Returns:
762
+ Whether the type is mutable and should be wrapped.
763
+ """
764
+ return issubclass(type_, MUTABLE_TYPES) or (
765
+ dataclasses.is_dataclass(type_) and not issubclass(type_, Var)
766
+ )
@@ -179,7 +179,7 @@ class TailwindPlugin(PluginBase):
179
179
  config: TailwindConfig = dataclasses.field(
180
180
  default_factory=lambda: TailwindConfig(
181
181
  plugins=[
182
- "@tailwindcss/typography@0.5.16",
182
+ "@tailwindcss/typography@0.5.18",
183
183
  ],
184
184
  )
185
185
  )
reflex/reflex.py CHANGED
@@ -204,7 +204,7 @@ def _run(
204
204
  args = (frontend,)
205
205
  kwargs = {
206
206
  "check_if_schema_up_to_date": True,
207
- "prerender_routes": env == constants.Env.PROD,
207
+ "prerender_routes": exec.should_prerender_routes(),
208
208
  }
209
209
 
210
210
  # Granian fails if the app is already imported.
@@ -216,9 +216,12 @@ def _run(
216
216
  *args,
217
217
  **kwargs,
218
218
  )
219
- compile_future.result()
219
+ return_result = compile_future.result()
220
220
  else:
221
- app_task(*args, **kwargs)
221
+ return_result = app_task(*args, **kwargs)
222
+
223
+ if not return_result:
224
+ raise SystemExit(1)
222
225
 
223
226
  # Get the frontend and backend commands, based on the environment.
224
227
  setup_frontend = frontend_cmd = backend_cmd = None
@@ -422,6 +425,21 @@ def compile(dry: bool, rich: bool):
422
425
  default=constants.Env.PROD.value,
423
426
  help="The environment to export the app in.",
424
427
  )
428
+ @click.option(
429
+ "--exclude-from-backend",
430
+ "backend_excluded_dirs",
431
+ multiple=True,
432
+ type=click.Path(exists=True, path_type=Path, resolve_path=True),
433
+ help="Files or directories to exclude from the backend zip. Can be used multiple times.",
434
+ )
435
+ @click.option(
436
+ "--server-side-rendering/--no-server-side-rendering",
437
+ "--ssr/--no-ssr",
438
+ "ssr",
439
+ default=True,
440
+ is_flag=True,
441
+ help="Whether to enable server side rendering for the frontend.",
442
+ )
425
443
  def export(
426
444
  zip: bool,
427
445
  frontend_only: bool,
@@ -429,11 +447,18 @@ def export(
429
447
  zip_dest_dir: str,
430
448
  upload_db_file: bool,
431
449
  env: LITERAL_ENV,
450
+ backend_excluded_dirs: tuple[Path, ...] = (),
451
+ ssr: bool = True,
432
452
  ):
433
453
  """Export the app to a zip file."""
434
454
  from reflex.utils import export as export_utils
435
455
  from reflex.utils import prerequisites
436
456
 
457
+ if not environment.REFLEX_SSR.is_set():
458
+ environment.REFLEX_SSR.set(ssr)
459
+ elif environment.REFLEX_SSR.get() != ssr:
460
+ ssr = environment.REFLEX_SSR.get()
461
+
437
462
  environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT)
438
463
 
439
464
  should_frontend_run, should_backend_run = prerequisites.check_running_mode(
@@ -455,6 +480,8 @@ def export(
455
480
  upload_db_file=upload_db_file,
456
481
  env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
457
482
  loglevel=config.loglevel.subprocess_level(),
483
+ backend_excluded_dirs=backend_excluded_dirs,
484
+ prerender_routes=ssr,
458
485
  )
459
486
 
460
487
 
@@ -660,6 +687,21 @@ def makemigrations(message: str | None):
660
687
  "--config",
661
688
  help="path to the config file",
662
689
  )
690
+ @click.option(
691
+ "--exclude-from-backend",
692
+ "backend_excluded_dirs",
693
+ multiple=True,
694
+ type=click.Path(exists=True, path_type=Path, resolve_path=True),
695
+ help="Files or directories to exclude from the backend zip. Can be used multiple times.",
696
+ )
697
+ @click.option(
698
+ "--server-side-rendering/--no-server-side-rendering",
699
+ "--ssr/--no-ssr",
700
+ "ssr",
701
+ default=True,
702
+ is_flag=True,
703
+ help="Whether to enable server side rendering for the frontend.",
704
+ )
663
705
  def deploy(
664
706
  app_name: str | None,
665
707
  app_id: str | None,
@@ -673,6 +715,8 @@ def deploy(
673
715
  project_name: str | None,
674
716
  token: str | None,
675
717
  config_path: str | None,
718
+ backend_excluded_dirs: tuple[Path, ...] = (),
719
+ ssr: bool = True,
676
720
  ):
677
721
  """Deploy the app to the Reflex hosting service."""
678
722
  from reflex_cli.utils import dependency
@@ -690,6 +734,11 @@ def deploy(
690
734
 
691
735
  environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.DEPLOY)
692
736
 
737
+ if not environment.REFLEX_SSR.is_set():
738
+ environment.REFLEX_SSR.set(ssr)
739
+ elif environment.REFLEX_SSR.get() != ssr:
740
+ ssr = environment.REFLEX_SSR.get()
741
+
693
742
  # Only check requirements if interactive.
694
743
  # There is user interaction for requirements update.
695
744
  if interactive:
@@ -721,6 +770,8 @@ def deploy(
721
770
  zipping=zipping,
722
771
  loglevel=config.loglevel.subprocess_level(),
723
772
  upload_db_file=upload_db,
773
+ backend_excluded_dirs=backend_excluded_dirs,
774
+ prerender_routes=ssr,
724
775
  )
725
776
  ),
726
777
  regions=list(region),
reflex/state.py CHANGED
@@ -7,6 +7,7 @@ import builtins
7
7
  import contextlib
8
8
  import copy
9
9
  import dataclasses
10
+ import datetime
10
11
  import functools
11
12
  import inspect
12
13
  import pickle
@@ -41,7 +42,7 @@ from reflex.event import (
41
42
  from reflex.istate import HANDLED_PICKLE_ERRORS, debug_failed_pickles
42
43
  from reflex.istate.data import RouterData
43
44
  from reflex.istate.proxy import ImmutableMutableProxy as ImmutableMutableProxy
44
- from reflex.istate.proxy import MutableProxy, StateProxy
45
+ from reflex.istate.proxy import MutableProxy, StateProxy, is_mutable_type
45
46
  from reflex.istate.storage import ClientStorageBase
46
47
  from reflex.model import Model
47
48
  from reflex.utils import console, format, prerequisites, types
@@ -306,6 +307,14 @@ async def _resolve_delta(delta: Delta) -> Delta:
306
307
  return delta
307
308
 
308
309
 
310
+ _deserializers = {
311
+ int: int,
312
+ float: float,
313
+ datetime.datetime: datetime.datetime.fromisoformat,
314
+ datetime.date: datetime.date.fromisoformat,
315
+ datetime.time: datetime.time.fromisoformat,
316
+ }
317
+
309
318
  all_base_state_classes: dict[str, None] = {}
310
319
 
311
320
 
@@ -1359,7 +1368,7 @@ class BaseState(EvenMoreBasicBaseState):
1359
1368
  if parent_state is not None:
1360
1369
  return getattr(parent_state, name)
1361
1370
 
1362
- if MutableProxy._is_mutable_type(value) and (
1371
+ if is_mutable_type(type(value)) and (
1363
1372
  name in super().__getattribute__("base_vars") or name in backend_vars
1364
1373
  ):
1365
1374
  # track changes in mutable containers (list, dict, set, etc)
@@ -1872,11 +1881,12 @@ class BaseState(EvenMoreBasicBaseState):
1872
1881
  hinted_args is tuple or hinted_args is tuple
1873
1882
  ):
1874
1883
  payload[arg] = tuple(value)
1875
- elif isinstance(value, str) and (
1876
- hinted_args is int or hinted_args is float
1884
+ elif (
1885
+ isinstance(value, str)
1886
+ and (deserializer := _deserializers.get(hinted_args)) is not None
1877
1887
  ):
1878
1888
  try:
1879
- payload[arg] = hinted_args(value)
1889
+ payload[arg] = deserializer(value)
1880
1890
  except ValueError:
1881
1891
  msg = f"Received a string value ({value}) for {arg} but expected a {hinted_args}"
1882
1892
  raise ValueError(msg) from None
reflex/testing.py CHANGED
@@ -47,6 +47,7 @@ from reflex.state import (
47
47
  )
48
48
  from reflex.utils import console, js_runtimes
49
49
  from reflex.utils.export import export
50
+ from reflex.utils.token_manager import TokenManager
50
51
  from reflex.utils.types import ASGIApp
51
52
 
52
53
  try:
@@ -774,6 +775,19 @@ class AppHarness:
774
775
  self.app_instance._state_manager = app_state_manager
775
776
  await self.state_manager.close()
776
777
 
778
+ def token_manager(self) -> TokenManager:
779
+ """Get the token manager for the app instance.
780
+
781
+ Returns:
782
+ The current token_manager attached to the app's EventNamespace.
783
+ """
784
+ assert self.app_instance is not None
785
+ app_event_namespace = self.app_instance.event_namespace
786
+ assert app_event_namespace is not None
787
+ app_token_manager = app_event_namespace._token_manager
788
+ assert app_token_manager is not None
789
+ return app_token_manager
790
+
777
791
  def poll_for_content(
778
792
  self,
779
793
  element: WebElement,
reflex/utils/build.py CHANGED
@@ -26,14 +26,14 @@ def set_env_json():
26
26
 
27
27
 
28
28
  def _zip(
29
+ *,
29
30
  component_name: constants.ComponentName,
30
- target: str | Path,
31
- root_dir: str | Path,
32
- exclude_venv_dirs: bool,
33
- upload_db_file: bool = False,
34
- dirs_to_exclude: set[str] | None = None,
35
- files_to_exclude: set[str] | None = None,
36
- top_level_dirs_to_exclude: set[str] | None = None,
31
+ target: Path,
32
+ root_directory: Path,
33
+ exclude_venv_directories: bool,
34
+ include_db_file: bool = False,
35
+ directory_names_to_exclude: set[str] | None = None,
36
+ files_to_exclude: set[Path] | None = None,
37
37
  globs_to_include: list[str] | None = None,
38
38
  ) -> None:
39
39
  """Zip utility function.
@@ -41,49 +41,62 @@ def _zip(
41
41
  Args:
42
42
  component_name: The name of the component: backend or frontend.
43
43
  target: The target zip file.
44
- root_dir: The root directory to zip.
45
- exclude_venv_dirs: Whether to exclude venv directories.
46
- upload_db_file: Whether to include local sqlite db files.
47
- dirs_to_exclude: The directories to exclude.
44
+ root_directory: The root directory to zip.
45
+ exclude_venv_directories: Whether to exclude venv directories.
46
+ include_db_file: Whether to include local sqlite db files.
47
+ directory_names_to_exclude: The directory names to exclude.
48
48
  files_to_exclude: The files to exclude.
49
- top_level_dirs_to_exclude: The top level directory names immediately under root_dir to exclude. Do not exclude folders by these names further in the sub-directories.
50
- globs_to_include: Apply these globs from the root_dir and always include them in the zip.
49
+ globs_to_include: Apply these globs from the root_directory and always include them in the zip.
51
50
 
52
51
  """
53
52
  target = Path(target)
54
- root_dir = Path(root_dir)
55
- dirs_to_exclude = dirs_to_exclude or set()
53
+ root_directory = Path(root_directory).resolve()
54
+ directory_names_to_exclude = directory_names_to_exclude or set()
56
55
  files_to_exclude = files_to_exclude or set()
57
- files_to_zip: list[str] = []
56
+ files_to_zip: list[Path] = []
58
57
  # Traverse the root directory in a top-down manner. In this traversal order,
59
58
  # we can modify the dirs list in-place to remove directories we don't want to include.
60
- for root, dirs, files in os.walk(root_dir, topdown=True, followlinks=True):
61
- root = Path(root)
59
+ for directory_path, subdirectories_names, subfiles_names in os.walk(
60
+ root_directory, topdown=True, followlinks=True
61
+ ):
62
+ directory_path = Path(directory_path).resolve()
62
63
  # Modify the dirs in-place so excluded and hidden directories are skipped in next traversal.
63
- dirs[:] = [
64
- d
65
- for d in dirs
66
- if (basename := Path(d).resolve().name) not in dirs_to_exclude
67
- and not basename.startswith(".")
68
- and (not exclude_venv_dirs or not _looks_like_venv_dir(root / d))
64
+ subdirectories_names[:] = [
65
+ subdirectory_name
66
+ for subdirectory_name in subdirectories_names
67
+ if subdirectory_name not in directory_names_to_exclude
68
+ and not any(
69
+ (directory_path / subdirectory_name).samefile(exclude)
70
+ for exclude in files_to_exclude
71
+ if exclude.exists()
72
+ )
73
+ and not subdirectory_name.startswith(".")
74
+ and (
75
+ not exclude_venv_directories
76
+ or not _looks_like_venv_directory(directory_path / subdirectory_name)
77
+ )
69
78
  ]
70
- # If we are at the top level with root_dir, exclude the top level dirs.
71
- if top_level_dirs_to_exclude and root == root_dir:
72
- dirs[:] = [d for d in dirs if d not in top_level_dirs_to_exclude]
73
79
  # Modify the files in-place so the hidden files and db files are excluded.
74
- files[:] = [
75
- f
76
- for f in files
77
- if not f.startswith(".") and (upload_db_file or not f.endswith(".db"))
80
+ subfiles_names[:] = [
81
+ subfile_name
82
+ for subfile_name in subfiles_names
83
+ if not subfile_name.startswith(".")
84
+ and (include_db_file or not subfile_name.endswith(".db"))
78
85
  ]
79
86
  files_to_zip += [
80
- str(root / file) for file in files if file not in files_to_exclude
87
+ directory_path / subfile_name
88
+ for subfile_name in subfiles_names
89
+ if not any(
90
+ (directory_path / subfile_name).samefile(excluded_file)
91
+ for excluded_file in files_to_exclude
92
+ if excluded_file.exists()
93
+ )
81
94
  ]
82
95
  if globs_to_include:
83
96
  for glob in globs_to_include:
84
97
  files_to_zip += [
85
- str(file)
86
- for file in root_dir.glob(glob)
98
+ file
99
+ for file in root_directory.glob(glob)
87
100
  if file.name not in files_to_exclude
88
101
  ]
89
102
  # Create a progress bar for zipping the component.
@@ -100,14 +113,15 @@ def _zip(
100
113
  for file in files_to_zip:
101
114
  console.debug(f"{target}: {file}", progress=progress)
102
115
  progress.advance(task)
103
- zipf.write(file, Path(file).relative_to(root_dir))
116
+ zipf.write(file, Path(file).relative_to(root_directory))
104
117
 
105
118
 
106
119
  def zip_app(
107
120
  frontend: bool = True,
108
121
  backend: bool = True,
109
122
  zip_dest_dir: str | Path | None = None,
110
- upload_db_file: bool = False,
123
+ include_db_file: bool = False,
124
+ backend_excluded_dirs: tuple[Path, ...] = (),
111
125
  ):
112
126
  """Zip up the app.
113
127
 
@@ -115,41 +129,41 @@ def zip_app(
115
129
  frontend: Whether to zip up the frontend app.
116
130
  backend: Whether to zip up the backend app.
117
131
  zip_dest_dir: The directory to export the zip file to.
118
- upload_db_file: Whether to upload the database file.
132
+ include_db_file: Whether to include the database file.
133
+ backend_excluded_dirs: A tuple of files or directories to exclude from the backend zip. Defaults to ().
119
134
  """
120
135
  zip_dest_dir = zip_dest_dir or Path.cwd()
121
136
  zip_dest_dir = Path(zip_dest_dir)
122
137
  files_to_exclude = {
123
- constants.ComponentName.FRONTEND.zip(),
124
- constants.ComponentName.BACKEND.zip(),
138
+ Path(constants.ComponentName.FRONTEND.zip()).resolve(),
139
+ Path(constants.ComponentName.BACKEND.zip()).resolve(),
125
140
  }
126
141
 
127
142
  if frontend:
128
143
  _zip(
129
144
  component_name=constants.ComponentName.FRONTEND,
130
145
  target=zip_dest_dir / constants.ComponentName.FRONTEND.zip(),
131
- root_dir=prerequisites.get_web_dir() / constants.Dirs.STATIC,
146
+ root_directory=prerequisites.get_web_dir() / constants.Dirs.STATIC,
132
147
  files_to_exclude=files_to_exclude,
133
- exclude_venv_dirs=False,
148
+ exclude_venv_directories=False,
134
149
  )
135
150
 
136
151
  if backend:
137
152
  _zip(
138
153
  component_name=constants.ComponentName.BACKEND,
139
154
  target=zip_dest_dir / constants.ComponentName.BACKEND.zip(),
140
- root_dir=Path.cwd(),
141
- dirs_to_exclude={"__pycache__"},
142
- files_to_exclude=files_to_exclude,
143
- top_level_dirs_to_exclude={"assets"},
144
- exclude_venv_dirs=True,
145
- upload_db_file=upload_db_file,
155
+ root_directory=Path.cwd(),
156
+ directory_names_to_exclude={"__pycache__"},
157
+ files_to_exclude=files_to_exclude | set(backend_excluded_dirs),
158
+ exclude_venv_directories=True,
159
+ include_db_file=include_db_file,
146
160
  globs_to_include=[
147
161
  str(Path(constants.Dirs.WEB) / constants.Dirs.BACKEND / "*")
148
162
  ],
149
163
  )
150
164
 
151
165
 
152
- def _duplicate_index_html_to_parent_dir(directory: Path):
166
+ def _duplicate_index_html_to_parent_directory(directory: Path):
153
167
  """Duplicate index.html in the child directories to the given directory.
154
168
 
155
169
  This makes accessing /route and /route/ work in production.
@@ -169,7 +183,7 @@ def _duplicate_index_html_to_parent_dir(directory: Path):
169
183
  else:
170
184
  console.debug(f"Skipping {index_html}, already exists at {target}")
171
185
  # Recursively call this function for the child directory.
172
- _duplicate_index_html_to_parent_dir(child)
186
+ _duplicate_index_html_to_parent_directory(child)
173
187
 
174
188
 
175
189
  def build():
@@ -200,11 +214,17 @@ def build():
200
214
  },
201
215
  )
202
216
  processes.show_progress("Creating Production Build", process, checkpoints)
203
- _duplicate_index_html_to_parent_dir(wdir / constants.Dirs.STATIC)
204
- path_ops.cp(
205
- wdir / constants.Dirs.STATIC / constants.ReactRouter.SPA_FALLBACK,
206
- wdir / constants.Dirs.STATIC / "404.html",
207
- )
217
+ _duplicate_index_html_to_parent_directory(wdir / constants.Dirs.STATIC)
218
+
219
+ spa_fallback = wdir / constants.Dirs.STATIC / constants.ReactRouter.SPA_FALLBACK
220
+ if not spa_fallback.exists():
221
+ spa_fallback = wdir / constants.Dirs.STATIC / "index.html"
222
+
223
+ if spa_fallback.exists():
224
+ path_ops.cp(
225
+ spa_fallback,
226
+ wdir / constants.Dirs.STATIC / "404.html",
227
+ )
208
228
 
209
229
  config = get_config()
210
230
 
@@ -247,6 +267,6 @@ def setup_frontend_prod(
247
267
  build()
248
268
 
249
269
 
250
- def _looks_like_venv_dir(dir_to_check: str | Path) -> bool:
251
- dir_to_check = Path(dir_to_check)
252
- return (dir_to_check / "pyvenv.cfg").exists()
270
+ def _looks_like_venv_directory(directory_to_check: str | Path) -> bool:
271
+ directory_to_check = Path(directory_to_check)
272
+ return (directory_to_check / "pyvenv.cfg").exists()
reflex/utils/exec.py CHANGED
@@ -497,6 +497,7 @@ HOTRELOAD_IGNORE_EXTENSIONS = (
497
497
  "sh",
498
498
  "bash",
499
499
  "log",
500
+ "db",
500
501
  )
501
502
 
502
503
  HOTRELOAD_IGNORE_PATTERNS = (
@@ -742,6 +743,17 @@ def is_prod_mode() -> bool:
742
743
  return current_mode == constants.Env.PROD
743
744
 
744
745
 
746
+ def should_prerender_routes() -> bool:
747
+ """Check if the app should prerender routes.
748
+
749
+ Returns:
750
+ True if the app should prerender routes.
751
+ """
752
+ if not environment.REFLEX_SSR.is_set():
753
+ return is_prod_mode()
754
+ return environment.REFLEX_SSR.get()
755
+
756
+
745
757
  def get_compile_context() -> constants.CompileContext:
746
758
  """Check if the app is compiled for deploy.
747
759
 
reflex/utils/export.py CHANGED
@@ -18,6 +18,8 @@ def export(
18
18
  deploy_url: str | None = None,
19
19
  env: constants.Env = constants.Env.PROD,
20
20
  loglevel: constants.LogLevel = console._LOG_LEVEL,
21
+ backend_excluded_dirs: tuple[Path, ...] = (),
22
+ prerender_routes: bool = True,
21
23
  ):
22
24
  """Export the app to a zip file.
23
25
 
@@ -31,6 +33,8 @@ def export(
31
33
  deploy_url: The deploy URL to use. Defaults to None.
32
34
  env: The environment to use. Defaults to constants.Env.PROD.
33
35
  loglevel: The log level to use. Defaults to console._LOG_LEVEL.
36
+ backend_excluded_dirs: A tuple of files or directories to exclude from the backend zip. Defaults to ().
37
+ prerender_routes: Whether to prerender the routes. Defaults to True.
34
38
  """
35
39
  config = get_config()
36
40
 
@@ -56,7 +60,7 @@ def export(
56
60
 
57
61
  if frontend:
58
62
  # Ensure module can be imported and app.compile() is called.
59
- prerequisites.get_compiled_app(prerender_routes=True)
63
+ prerequisites.get_compiled_app(prerender_routes=prerender_routes)
60
64
  # Set up .web directory and install frontend dependencies.
61
65
  build.setup_frontend(Path.cwd())
62
66
 
@@ -70,7 +74,8 @@ def export(
70
74
  frontend=frontend,
71
75
  backend=backend,
72
76
  zip_dest_dir=zip_dest_dir,
73
- upload_db_file=upload_db_file,
77
+ include_db_file=upload_db_file,
78
+ backend_excluded_dirs=backend_excluded_dirs,
74
79
  )
75
80
 
76
81
  # Post a telemetry event.