reflex 0.8.12a2__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.

reflex/app.py CHANGED
@@ -113,7 +113,12 @@ from reflex.utils import (
113
113
  prerequisites,
114
114
  types,
115
115
  )
116
- from reflex.utils.exec import get_compile_context, is_prod_mode, is_testing_env
116
+ from reflex.utils.exec import (
117
+ get_compile_context,
118
+ is_prod_mode,
119
+ is_testing_env,
120
+ should_prerender_routes,
121
+ )
117
122
  from reflex.utils.imports import ImportVar
118
123
  from reflex.utils.token_manager import TokenManager
119
124
  from reflex.utils.types import ASGIApp, Message, Receive, Scope, Send
@@ -607,7 +612,7 @@ class App(MiddlewareMixin, LifespanMixin):
607
612
  """
608
613
  from reflex.vars.base import GLOBAL_CACHE
609
614
 
610
- self._compile(prerender_routes=is_prod_mode())
615
+ self._compile(prerender_routes=should_prerender_routes())
611
616
 
612
617
  config = get_config()
613
618
 
@@ -108,6 +108,7 @@ class GhostUpload(Fragment):
108
108
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
109
109
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
110
110
  on_drop: EventType[()] | EventType[Any] | None = None,
111
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
111
112
  on_focus: EventType[()] | None = None,
112
113
  on_mount: EventType[()] | None = None,
113
114
  on_mouse_down: EventType[()] | None = None,
@@ -127,6 +128,7 @@ class GhostUpload(Fragment):
127
128
  Args:
128
129
  *children: The children of the component.
129
130
  on_drop: Fired when files are dropped.
131
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
130
132
  style: The style of the component.
131
133
  key: A unique key for the component.
132
134
  id: The id for the component.
@@ -171,6 +173,7 @@ class Upload(MemoizationLeaf):
171
173
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
172
174
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
173
175
  on_drop: EventType[()] | EventType[Any] | None = None,
176
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
174
177
  on_focus: EventType[()] | None = None,
175
178
  on_mount: EventType[()] | None = None,
176
179
  on_mouse_down: EventType[()] | None = None,
@@ -199,6 +202,7 @@ class Upload(MemoizationLeaf):
199
202
  no_drag: Whether to disable drag and drop.
200
203
  no_keyboard: Whether to disable using the space/enter keys to upload.
201
204
  on_drop: Fired when files are dropped.
205
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
202
206
  drag_active_style: Style rules to apply when actively dragging.
203
207
  style: The style of the component.
204
208
  key: A unique key for the component.
@@ -242,6 +246,7 @@ class StyledUpload(Upload):
242
246
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
243
247
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
244
248
  on_drop: EventType[()] | EventType[Any] | None = None,
249
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
245
250
  on_focus: EventType[()] | None = None,
246
251
  on_mount: EventType[()] | None = None,
247
252
  on_mouse_down: EventType[()] | None = None,
@@ -270,6 +275,7 @@ class StyledUpload(Upload):
270
275
  no_drag: Whether to disable drag and drop.
271
276
  no_keyboard: Whether to disable using the space/enter keys to upload.
272
277
  on_drop: Fired when files are dropped.
278
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
273
279
  drag_active_style: Style rules to apply when actively dragging.
274
280
  style: The style of the component.
275
281
  key: A unique key for the component.
@@ -314,6 +320,7 @@ class UploadNamespace(ComponentNamespace):
314
320
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
315
321
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
316
322
  on_drop: EventType[()] | EventType[Any] | None = None,
323
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
317
324
  on_focus: EventType[()] | None = None,
318
325
  on_mount: EventType[()] | None = None,
319
326
  on_mouse_down: EventType[()] | None = None,
@@ -342,6 +349,7 @@ class UploadNamespace(ComponentNamespace):
342
349
  no_drag: Whether to disable drag and drop.
343
350
  no_keyboard: Whether to disable using the space/enter keys to upload.
344
351
  on_drop: Fired when files are dropped.
352
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
345
353
  drag_active_style: Style rules to apply when actively dragging.
346
354
  style: The style of the component.
347
355
  key: A unique key for the component.
@@ -4,9 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any, TypedDict
6
6
 
7
- from reflex.components.component import NoSSRComponent
8
- from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
7
+ from reflex.components.component import Component
8
+ from reflex.components.core.cond import cond
9
+ from reflex.event import EventHandler, no_args_event_spec
10
+ from reflex.utils import console
9
11
  from reflex.vars.base import Var
12
+ from reflex.vars.object import ObjectVar
13
+
14
+ ReactPlayerEvent = ObjectVar[dict[str, dict[str, dict[str, Any]]]]
10
15
 
11
16
 
12
17
  class Progress(TypedDict):
@@ -16,21 +21,123 @@ class Progress(TypedDict):
16
21
  playedSeconds: float
17
22
  loaded: float
18
23
  loadedSeconds: float
24
+ duration: float
25
+
26
+
27
+ def _on_progress_signature(event: ReactPlayerEvent) -> list[Var[Progress]]:
28
+ """Type signature for on_progress event.
29
+
30
+ Args:
31
+ event: The event variable.
32
+
33
+ Returns:
34
+ The progress information extracted from the event.
35
+ """
36
+ player_info = event["target"]["api"]["playerInfo"].to(dict)
37
+ progress_state = player_info["progressState"].to(dict)
38
+ current = progress_state["current"].to(float)
39
+ loaded = progress_state["loaded"].to(float)
40
+ duration = progress_state["duration"].to(float)
41
+ return [
42
+ cond(
43
+ progress_state,
44
+ {
45
+ "played": cond(duration, current / duration, 0.0),
46
+ "playedSeconds": current,
47
+ "loaded": cond(duration, loaded / duration, 0.0),
48
+ "loadedSeconds": loaded,
49
+ "duration": duration,
50
+ },
51
+ {
52
+ "played": 0.0,
53
+ "playedSeconds": 0.0,
54
+ "loaded": 0.0,
55
+ "loadedSeconds": 0.0,
56
+ "duration": 0.0,
57
+ },
58
+ ).to(Progress)
59
+ ]
60
+
61
+
62
+ def _player_info_key_or_zero(event: ReactPlayerEvent, key: str) -> Var[float]:
63
+ """Helper to extract a value from playerInfo or return 0.0 if not available.
64
+
65
+ Args:
66
+ event: The event variable.
67
+ key: The key to extract from playerInfo.
68
+
69
+ Returns:
70
+ The extracted value or 0.0 if not available.
71
+ """
72
+ player_info = event["target"]["api"]["playerInfo"].to(dict)
73
+ return cond(
74
+ player_info[key],
75
+ player_info[key],
76
+ 0.0,
77
+ ).to(float)
78
+
79
+
80
+ def _on_time_update_signature(event: ReactPlayerEvent) -> list[Var[float]]:
81
+ """Type signature for on_time_update event.
82
+
83
+ Args:
84
+ event: The event variable.
85
+
86
+ Returns:
87
+ The current timestamp in seconds.
88
+ """
89
+ return [_player_info_key_or_zero(event, "currentTime")]
90
+
91
+
92
+ def _on_duration_change_signature(event: ReactPlayerEvent) -> list[Var[float]]:
93
+ """Type signature for on_duration_change event.
94
+
95
+ Args:
96
+ event: The event variable.
97
+
98
+ Returns:
99
+ The active media's duration in seconds.
100
+ """
101
+ return [_player_info_key_or_zero(event, "duration")]
102
+
103
+
104
+ def _on_rate_change_signature(event: ReactPlayerEvent) -> list[Var[float]]:
105
+ """Type signature for on_rate_change event.
19
106
 
107
+ Args:
108
+ event: The event variable.
20
109
 
21
- class ReactPlayer(NoSSRComponent):
110
+ Returns:
111
+ The current playback rate.
112
+ """
113
+ return [_player_info_key_or_zero(event, "playbackRate")]
114
+
115
+
116
+ _DEPRECATED_PROP_MAP = {
117
+ "url": "src",
118
+ "on_duration": "on_duration_change",
119
+ "on_playback_rate_change": "on_rate_change",
120
+ "on_seek": "on_seeked",
121
+ "on_buffer": "on_waiting",
122
+ "on_buffer_end": "on_playing",
123
+ "on_enable_pip": "on_enter_picture_in_picture",
124
+ "on_disable_pip": "on_leave_picture_in_picture",
125
+ }
126
+
127
+
128
+ class ReactPlayer(Component):
22
129
  """Using react-player and not implement all props and callback yet.
23
130
  reference: https://github.com/cookpete/react-player.
24
131
  """
25
132
 
26
- library = "react-player@2.16.0"
133
+ library = "react-player@3.3.3"
27
134
 
28
135
  tag = "ReactPlayer"
29
136
 
30
137
  is_default = True
31
138
 
32
139
  # The url of a video or song to play
33
- url: Var[str]
140
+ src: Var[str | list[str] | list[dict[str, str]]]
34
141
 
35
142
  # Set to true or false to pause or play the media
36
143
  playing: Var[bool]
@@ -50,38 +157,44 @@ class ReactPlayer(NoSSRComponent):
50
157
  # Mutes the player
51
158
  muted: Var[bool]
52
159
 
160
+ # Player-specific configuration parameters.
161
+ config: Var[dict[str, Any]]
162
+
53
163
  # Called when media is loaded and ready to play. If playing is set to true, media will play immediately.
54
164
  on_ready: EventHandler[no_args_event_spec]
55
165
 
56
166
  # Called when media starts playing.
57
167
  on_start: EventHandler[no_args_event_spec]
58
168
 
59
- # Called when media starts or resumes playing after pausing or buffering.
169
+ # Called when playing is set to true.
60
170
  on_play: EventHandler[no_args_event_spec]
61
171
 
62
- # Callback containing played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds. eg { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 }
63
- on_progress: EventHandler[passthrough_event_spec(Progress)]
172
+ # Called when media starts or resumes playing after pausing or buffering.
173
+ on_playing: EventHandler[no_args_event_spec]
174
+
175
+ # Called while the video is loading only. Contains played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds. eg { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 }
176
+ on_progress: EventHandler[_on_progress_signature]
177
+
178
+ # Called when the media's current time changes (~4Hz, use .throttle to limit calls to backend).
179
+ on_time_update: EventHandler[_on_time_update_signature]
64
180
 
65
181
  # Callback containing duration of the media, in seconds.
66
- on_duration: EventHandler[passthrough_event_spec(float)]
182
+ on_duration_change: EventHandler[_on_duration_change_signature]
67
183
 
68
184
  # Called when media is paused.
69
185
  on_pause: EventHandler[no_args_event_spec]
70
186
 
71
187
  # Called when media starts buffering.
72
- on_buffer: EventHandler[no_args_event_spec]
188
+ on_waiting: EventHandler[no_args_event_spec]
73
189
 
74
- # Called when media has finished buffering. Works for files, YouTube and Facebook.
75
- on_buffer_end: EventHandler[no_args_event_spec]
190
+ # Called when the media is seeking.
191
+ on_seeking: EventHandler[no_args_event_spec]
76
192
 
77
193
  # Called when media seeks with seconds parameter.
78
- on_seek: EventHandler[passthrough_event_spec(float)]
194
+ on_seeked: EventHandler[_on_time_update_signature]
79
195
 
80
196
  # Called when playback rate of the player changed. Only supported by YouTube, Vimeo (if enabled), Wistia, and file paths.
81
- on_playback_rate_change: EventHandler[no_args_event_spec]
82
-
83
- # Called when playback quality of the player changed. Only supported by YouTube (if enabled).
84
- on_playback_quality_change: EventHandler[no_args_event_spec]
197
+ on_rate_change: EventHandler[_on_rate_change_signature]
85
198
 
86
199
  # Called when media finishes playing. Does not fire when loop is set to true.
87
200
  on_ended: EventHandler[no_args_event_spec]
@@ -93,10 +206,37 @@ class ReactPlayer(NoSSRComponent):
93
206
  on_click_preview: EventHandler[no_args_event_spec]
94
207
 
95
208
  # Called when picture-in-picture mode is enabled.
96
- on_enable_pip: EventHandler[no_args_event_spec]
209
+ on_enter_picture_in_picture: EventHandler[no_args_event_spec]
97
210
 
98
211
  # Called when picture-in-picture mode is disabled.
99
- on_disable_pip: EventHandler[no_args_event_spec]
212
+ on_leave_picture_in_picture: EventHandler[no_args_event_spec]
213
+
214
+ @classmethod
215
+ def create(cls, *children, **props) -> ReactPlayer:
216
+ """Create a component.
217
+
218
+ Args:
219
+ children: The children of the component.
220
+ props: The props of the component.
221
+
222
+ Returns:
223
+ The created component.
224
+
225
+ Raises:
226
+ ValueError: If both a deprecated prop and its replacement are both passed.
227
+ """
228
+ for prop, new_prop in _DEPRECATED_PROP_MAP.items():
229
+ if prop in props:
230
+ if new_prop in props:
231
+ msg = (
232
+ f"The prop {prop!r} is deprecated, but the replacement {new_prop!r} is also passed. Please remove {prop!r}.",
233
+ )
234
+ raise ValueError(msg)
235
+ console.warn(
236
+ f"The prop {prop!r} has been replaced by {new_prop!r}, please update your code.",
237
+ )
238
+ props[new_prop] = props.pop(prop)
239
+ return super().create(*children, **props) # type: ignore[return-value]
100
240
 
101
241
  def _render(self, props: dict[str, Any] | None = None):
102
242
  """Render the component. Adds width and height set to None because
@@ -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
 
@@ -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
@@ -429,6 +432,14 @@ def compile(dry: bool, rich: bool):
429
432
  type=click.Path(exists=True, path_type=Path, resolve_path=True),
430
433
  help="Files or directories to exclude from the backend zip. Can be used multiple times.",
431
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
+ )
432
443
  def export(
433
444
  zip: bool,
434
445
  frontend_only: bool,
@@ -437,11 +448,17 @@ def export(
437
448
  upload_db_file: bool,
438
449
  env: LITERAL_ENV,
439
450
  backend_excluded_dirs: tuple[Path, ...] = (),
451
+ ssr: bool = True,
440
452
  ):
441
453
  """Export the app to a zip file."""
442
454
  from reflex.utils import export as export_utils
443
455
  from reflex.utils import prerequisites
444
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
+
445
462
  environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT)
446
463
 
447
464
  should_frontend_run, should_backend_run = prerequisites.check_running_mode(
@@ -464,6 +481,7 @@ def export(
464
481
  env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
465
482
  loglevel=config.loglevel.subprocess_level(),
466
483
  backend_excluded_dirs=backend_excluded_dirs,
484
+ prerender_routes=ssr,
467
485
  )
468
486
 
469
487
 
@@ -676,6 +694,14 @@ def makemigrations(message: str | None):
676
694
  type=click.Path(exists=True, path_type=Path, resolve_path=True),
677
695
  help="Files or directories to exclude from the backend zip. Can be used multiple times.",
678
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
+ )
679
705
  def deploy(
680
706
  app_name: str | None,
681
707
  app_id: str | None,
@@ -690,6 +716,7 @@ def deploy(
690
716
  token: str | None,
691
717
  config_path: str | None,
692
718
  backend_excluded_dirs: tuple[Path, ...] = (),
719
+ ssr: bool = True,
693
720
  ):
694
721
  """Deploy the app to the Reflex hosting service."""
695
722
  from reflex_cli.utils import dependency
@@ -707,6 +734,11 @@ def deploy(
707
734
 
708
735
  environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.DEPLOY)
709
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
+
710
742
  # Only check requirements if interactive.
711
743
  # There is user interaction for requirements update.
712
744
  if interactive:
@@ -739,6 +771,7 @@ def deploy(
739
771
  loglevel=config.loglevel.subprocess_level(),
740
772
  upload_db_file=upload_db,
741
773
  backend_excluded_dirs=backend_excluded_dirs,
774
+ prerender_routes=ssr,
742
775
  )
743
776
  ),
744
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
@@ -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
 
@@ -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/utils/build.py CHANGED
@@ -215,10 +215,16 @@ def build():
215
215
  )
216
216
  processes.show_progress("Creating Production Build", process, checkpoints)
217
217
  _duplicate_index_html_to_parent_directory(wdir / constants.Dirs.STATIC)
218
- path_ops.cp(
219
- wdir / constants.Dirs.STATIC / constants.ReactRouter.SPA_FALLBACK,
220
- wdir / constants.Dirs.STATIC / "404.html",
221
- )
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
+ )
222
228
 
223
229
  config = get_config()
224
230
 
reflex/utils/exec.py CHANGED
@@ -743,6 +743,17 @@ def is_prod_mode() -> bool:
743
743
  return current_mode == constants.Env.PROD
744
744
 
745
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
+
746
757
  def get_compile_context() -> constants.CompileContext:
747
758
  """Check if the app is compiled for deploy.
748
759
 
reflex/utils/export.py CHANGED
@@ -19,6 +19,7 @@ def export(
19
19
  env: constants.Env = constants.Env.PROD,
20
20
  loglevel: constants.LogLevel = console._LOG_LEVEL,
21
21
  backend_excluded_dirs: tuple[Path, ...] = (),
22
+ prerender_routes: bool = True,
22
23
  ):
23
24
  """Export the app to a zip file.
24
25
 
@@ -33,6 +34,7 @@ def export(
33
34
  env: The environment to use. Defaults to constants.Env.PROD.
34
35
  loglevel: The log level to use. Defaults to console._LOG_LEVEL.
35
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.
36
38
  """
37
39
  config = get_config()
38
40
 
@@ -58,7 +60,7 @@ def export(
58
60
 
59
61
  if frontend:
60
62
  # Ensure module can be imported and app.compile() is called.
61
- prerequisites.get_compiled_app(prerender_routes=True)
63
+ prerequisites.get_compiled_app(prerender_routes=prerender_routes)
62
64
  # Set up .web directory and install frontend dependencies.
63
65
  build.setup_frontend(Path.cwd())
64
66