reflex 0.7.14a5__py3-none-any.whl → 0.8.0__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 (236) hide show
  1. reflex/.templates/jinja/app/rxconfig.py.jinja2 +4 -1
  2. reflex/.templates/jinja/web/package.json.jinja2 +1 -1
  3. reflex/.templates/jinja/web/pages/_app.js.jinja2 +21 -11
  4. reflex/.templates/jinja/web/pages/_document.js.jinja2 +1 -1
  5. reflex/.templates/jinja/web/pages/base_page.js.jinja2 +0 -1
  6. reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +4 -0
  7. reflex/.templates/jinja/web/styles/styles.css.jinja2 +1 -0
  8. reflex/.templates/jinja/web/utils/context.js.jinja2 +25 -8
  9. reflex/.templates/web/app/entry.client.js +8 -0
  10. reflex/.templates/web/app/routes.js +10 -0
  11. reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +12 -37
  12. reflex/.templates/web/postcss.config.js +1 -1
  13. reflex/.templates/web/react-router.config.js +6 -0
  14. reflex/.templates/web/styles/__reflex_style_reset.css +399 -0
  15. reflex/.templates/web/utils/client_side_routing.js +21 -19
  16. reflex/.templates/web/utils/react-theme.js +92 -0
  17. reflex/.templates/web/utils/state.js +251 -100
  18. reflex/.templates/web/vite-plugin-safari-cachebust.js +160 -0
  19. reflex/.templates/web/vite.config.js +39 -0
  20. reflex/__init__.py +1 -6
  21. reflex/__init__.pyi +327 -192
  22. reflex/app.py +103 -152
  23. reflex/base.py +1 -87
  24. reflex/compiler/compiler.py +70 -19
  25. reflex/compiler/templates.py +3 -3
  26. reflex/compiler/utils.py +91 -33
  27. reflex/components/__init__.py +0 -2
  28. reflex/components/__init__.pyi +34 -18
  29. reflex/components/base/__init__.py +1 -5
  30. reflex/components/base/__init__.pyi +30 -21
  31. reflex/components/base/app_wrap.pyi +7 -7
  32. reflex/components/base/body.pyi +7 -7
  33. reflex/components/base/document.py +18 -14
  34. reflex/components/base/document.pyi +88 -38
  35. reflex/components/base/error_boundary.pyi +7 -7
  36. reflex/components/base/fragment.pyi +7 -7
  37. reflex/components/base/link.pyi +12 -12
  38. reflex/components/base/meta.py +4 -15
  39. reflex/components/base/meta.pyi +31 -31
  40. reflex/components/base/script.py +60 -58
  41. reflex/components/base/script.pyi +248 -34
  42. reflex/components/base/strict_mode.pyi +7 -7
  43. reflex/components/component.py +146 -217
  44. reflex/components/core/__init__.py +1 -0
  45. reflex/components/core/__init__.pyi +77 -37
  46. reflex/components/core/auto_scroll.pyi +7 -7
  47. reflex/components/core/banner.pyi +33 -33
  48. reflex/components/core/client_side_routing.py +7 -6
  49. reflex/components/core/client_side_routing.pyi +8 -59
  50. reflex/components/core/clipboard.pyi +7 -7
  51. reflex/components/core/debounce.py +1 -0
  52. reflex/components/core/debounce.pyi +7 -7
  53. reflex/components/core/foreach.py +5 -4
  54. reflex/components/core/helmet.py +14 -0
  55. reflex/components/{next/base.pyi → core/helmet.pyi} +12 -10
  56. reflex/components/core/html.pyi +7 -7
  57. reflex/components/core/match.py +3 -3
  58. reflex/components/core/sticky.pyi +21 -20
  59. reflex/components/core/upload.py +4 -2
  60. reflex/components/core/upload.pyi +26 -25
  61. reflex/components/datadisplay/__init__.pyi +13 -7
  62. reflex/components/datadisplay/code.py +14 -79
  63. reflex/components/datadisplay/code.pyi +11 -13
  64. reflex/components/datadisplay/dataeditor.pyi +38 -15
  65. reflex/components/datadisplay/shiki_code_block.py +5 -3
  66. reflex/components/datadisplay/shiki_code_block.pyi +16 -15
  67. reflex/components/dynamic.py +5 -5
  68. reflex/components/el/__init__.pyi +506 -246
  69. reflex/components/el/element.pyi +7 -7
  70. reflex/components/el/elements/__init__.pyi +504 -245
  71. reflex/components/el/elements/base.pyi +7 -7
  72. reflex/components/el/elements/forms.pyi +146 -101
  73. reflex/components/el/elements/inline.pyi +142 -142
  74. reflex/components/el/elements/media.pyi +131 -130
  75. reflex/components/el/elements/metadata.pyi +32 -32
  76. reflex/components/el/elements/other.pyi +37 -37
  77. reflex/components/el/elements/scripts.pyi +17 -17
  78. reflex/components/el/elements/sectioning.pyi +77 -77
  79. reflex/components/el/elements/tables.pyi +52 -52
  80. reflex/components/el/elements/typography.pyi +77 -77
  81. reflex/components/field.py +175 -0
  82. reflex/components/gridjs/datatable.py +2 -2
  83. reflex/components/gridjs/datatable.pyi +14 -14
  84. reflex/components/lucide/icon.py +6 -2
  85. reflex/components/lucide/icon.pyi +19 -17
  86. reflex/components/markdown/markdown.py +5 -3
  87. reflex/components/markdown/markdown.pyi +7 -7
  88. reflex/components/moment/moment.py +1 -1
  89. reflex/components/moment/moment.pyi +7 -7
  90. reflex/components/plotly/plotly.py +12 -6
  91. reflex/components/plotly/plotly.pyi +50 -49
  92. reflex/components/props.py +376 -27
  93. reflex/components/radix/__init__.pyi +123 -65
  94. reflex/components/radix/primitives/__init__.pyi +6 -4
  95. reflex/components/radix/primitives/accordion.py +8 -1
  96. reflex/components/radix/primitives/accordion.pyi +37 -37
  97. reflex/components/radix/primitives/base.pyi +12 -12
  98. reflex/components/radix/primitives/drawer.pyi +56 -55
  99. reflex/components/radix/primitives/form.pyi +63 -53
  100. reflex/components/radix/primitives/progress.pyi +26 -25
  101. reflex/components/radix/primitives/slider.pyi +27 -27
  102. reflex/components/radix/themes/__init__.pyi +5 -6
  103. reflex/components/radix/themes/base.py +3 -3
  104. reflex/components/radix/themes/base.pyi +42 -42
  105. reflex/components/radix/themes/color_mode.py +5 -6
  106. reflex/components/radix/themes/color_mode.pyi +17 -17
  107. reflex/components/radix/themes/components/__init__.pyi +75 -38
  108. reflex/components/radix/themes/components/alert_dialog.pyi +37 -37
  109. reflex/components/radix/themes/components/aspect_ratio.pyi +7 -7
  110. reflex/components/radix/themes/components/avatar.pyi +7 -7
  111. reflex/components/radix/themes/components/badge.pyi +7 -7
  112. reflex/components/radix/themes/components/button.pyi +7 -7
  113. reflex/components/radix/themes/components/callout.pyi +26 -25
  114. reflex/components/radix/themes/components/card.pyi +7 -7
  115. reflex/components/radix/themes/components/checkbox.pyi +16 -15
  116. reflex/components/radix/themes/components/checkbox_cards.pyi +12 -12
  117. reflex/components/radix/themes/components/checkbox_group.pyi +12 -12
  118. reflex/components/radix/themes/components/context_menu.pyi +67 -67
  119. reflex/components/radix/themes/components/data_list.pyi +22 -22
  120. reflex/components/radix/themes/components/dialog.pyi +36 -35
  121. reflex/components/radix/themes/components/dropdown_menu.pyi +42 -42
  122. reflex/components/radix/themes/components/hover_card.pyi +21 -20
  123. reflex/components/radix/themes/components/icon_button.pyi +7 -7
  124. reflex/components/radix/themes/components/inset.pyi +7 -7
  125. reflex/components/radix/themes/components/popover.pyi +22 -22
  126. reflex/components/radix/themes/components/progress.pyi +7 -7
  127. reflex/components/radix/themes/components/radio.pyi +7 -7
  128. reflex/components/radix/themes/components/radio_cards.pyi +12 -12
  129. reflex/components/radix/themes/components/radio_group.pyi +21 -20
  130. reflex/components/radix/themes/components/scroll_area.pyi +7 -7
  131. reflex/components/radix/themes/components/segmented_control.pyi +12 -12
  132. reflex/components/radix/themes/components/select.pyi +46 -45
  133. reflex/components/radix/themes/components/separator.pyi +7 -7
  134. reflex/components/radix/themes/components/skeleton.pyi +7 -7
  135. reflex/components/radix/themes/components/slider.pyi +17 -9
  136. reflex/components/radix/themes/components/spinner.pyi +7 -7
  137. reflex/components/radix/themes/components/switch.pyi +7 -7
  138. reflex/components/radix/themes/components/table.pyi +37 -37
  139. reflex/components/radix/themes/components/tabs.pyi +26 -25
  140. reflex/components/radix/themes/components/text_area.pyi +15 -9
  141. reflex/components/radix/themes/components/text_field.pyi +32 -19
  142. reflex/components/radix/themes/components/tooltip.pyi +7 -7
  143. reflex/components/radix/themes/layout/__init__.pyi +27 -14
  144. reflex/components/radix/themes/layout/base.pyi +7 -7
  145. reflex/components/radix/themes/layout/box.pyi +7 -7
  146. reflex/components/radix/themes/layout/center.pyi +7 -7
  147. reflex/components/radix/themes/layout/container.pyi +7 -7
  148. reflex/components/radix/themes/layout/flex.pyi +7 -7
  149. reflex/components/radix/themes/layout/grid.pyi +7 -7
  150. reflex/components/radix/themes/layout/list.pyi +26 -25
  151. reflex/components/radix/themes/layout/section.pyi +7 -7
  152. reflex/components/radix/themes/layout/spacer.pyi +7 -7
  153. reflex/components/radix/themes/layout/stack.pyi +17 -17
  154. reflex/components/radix/themes/typography/__init__.pyi +7 -5
  155. reflex/components/radix/themes/typography/blockquote.pyi +7 -7
  156. reflex/components/radix/themes/typography/code.pyi +7 -7
  157. reflex/components/radix/themes/typography/heading.pyi +7 -7
  158. reflex/components/radix/themes/typography/link.py +46 -11
  159. reflex/components/radix/themes/typography/link.pyi +312 -9
  160. reflex/components/radix/themes/typography/text.pyi +36 -35
  161. reflex/components/react_player/audio.pyi +10 -8
  162. reflex/components/react_player/react_player.pyi +7 -7
  163. reflex/components/react_player/video.pyi +10 -8
  164. reflex/components/recharts/__init__.pyi +208 -100
  165. reflex/components/recharts/cartesian.py +10 -8
  166. reflex/components/recharts/cartesian.pyi +90 -94
  167. reflex/components/recharts/charts.py +4 -2
  168. reflex/components/recharts/charts.pyi +49 -49
  169. reflex/components/recharts/general.pyi +31 -31
  170. reflex/components/recharts/polar.py +8 -4
  171. reflex/components/recharts/polar.pyi +23 -23
  172. reflex/components/recharts/recharts.py +2 -2
  173. reflex/components/recharts/recharts.pyi +12 -12
  174. reflex/components/sonner/toast.py +3 -3
  175. reflex/components/sonner/toast.pyi +9 -9
  176. reflex/config.py +10 -113
  177. reflex/constants/__init__.py +2 -2
  178. reflex/constants/base.py +28 -11
  179. reflex/constants/compiler.py +12 -3
  180. reflex/constants/event.py +1 -0
  181. reflex/constants/installer.py +26 -20
  182. reflex/constants/route.py +27 -8
  183. reflex/constants/state.py +2 -0
  184. reflex/custom_components/custom_components.py +0 -14
  185. reflex/environment.py +77 -5
  186. reflex/event.py +178 -81
  187. reflex/experimental/__init__.py +0 -30
  188. reflex/istate/__init__.py +69 -0
  189. reflex/istate/manager.py +1 -0
  190. reflex/istate/proxy.py +5 -3
  191. reflex/page.py +0 -27
  192. reflex/plugins/__init__.py +3 -2
  193. reflex/plugins/base.py +5 -1
  194. reflex/plugins/shared_tailwind.py +215 -0
  195. reflex/plugins/sitemap.py +206 -0
  196. reflex/plugins/tailwind_v3.py +15 -108
  197. reflex/plugins/tailwind_v4.py +18 -110
  198. reflex/reflex.py +1 -0
  199. reflex/route.py +157 -75
  200. reflex/state.py +171 -155
  201. reflex/testing.py +86 -16
  202. reflex/utils/build.py +38 -82
  203. reflex/utils/exec.py +83 -175
  204. reflex/utils/export.py +2 -2
  205. reflex/utils/format.py +1 -5
  206. reflex/utils/imports.py +5 -16
  207. reflex/utils/misc.py +67 -0
  208. reflex/utils/prerequisites.py +66 -68
  209. reflex/utils/processes.py +24 -47
  210. reflex/utils/pyi_generator.py +44 -49
  211. reflex/utils/serializers.py +14 -1
  212. reflex/utils/telemetry.py +0 -15
  213. reflex/utils/types.py +197 -62
  214. reflex/vars/__init__.py +2 -0
  215. reflex/vars/base.py +367 -134
  216. {reflex-0.7.14a5.dist-info → reflex-0.8.0.dist-info}/METADATA +15 -8
  217. reflex-0.8.0.dist-info/RECORD +403 -0
  218. reflex/.templates/web/next.config.js +0 -7
  219. reflex/components/base/head.py +0 -20
  220. reflex/components/base/head.pyi +0 -116
  221. reflex/components/next/__init__.py +0 -10
  222. reflex/components/next/base.py +0 -7
  223. reflex/components/next/image.py +0 -117
  224. reflex/components/next/image.pyi +0 -94
  225. reflex/components/next/link.py +0 -20
  226. reflex/components/next/link.pyi +0 -67
  227. reflex/components/next/video.py +0 -38
  228. reflex/components/next/video.pyi +0 -68
  229. reflex/components/suneditor/__init__.py +0 -5
  230. reflex/components/suneditor/editor.py +0 -269
  231. reflex/components/suneditor/editor.pyi +0 -199
  232. reflex/experimental/layout.py +0 -254
  233. reflex-0.7.14a5.dist-info/RECORD +0 -407
  234. {reflex-0.7.14a5.dist-info → reflex-0.8.0.dist-info}/WHEEL +0 -0
  235. {reflex-0.7.14a5.dist-info → reflex-0.8.0.dist-info}/entry_points.txt +0 -0
  236. {reflex-0.7.14a5.dist-info → reflex-0.8.0.dist-info}/licenses/LICENSE +0 -0
reflex/istate/__init__.py CHANGED
@@ -1 +1,70 @@
1
1
  """This module will provide interfaces for the state."""
2
+
3
+ import pickle
4
+ import sys
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ # Errors caught during pickling of state
9
+ HANDLED_PICKLE_ERRORS = (
10
+ pickle.PicklingError,
11
+ AttributeError,
12
+ IndexError,
13
+ TypeError,
14
+ ValueError,
15
+ )
16
+
17
+
18
+ def _is_picklable(obj: Any, dumps: Callable[[object], bytes]) -> bool:
19
+ try:
20
+ dumps(obj)
21
+ except Exception:
22
+ return False
23
+ else:
24
+ return True
25
+
26
+
27
+ def debug_failed_pickles(obj: object, dumps: Callable[[object], bytes]):
28
+ """Recursively check the picklability of an object and its contents.
29
+
30
+ Args:
31
+ obj: The object to check.
32
+ dumps: The pickle dump function to use.
33
+
34
+ Raises:
35
+ HANDLED_PICKLE_ERRORS: If the object or any of its contents are not picklable.
36
+ """
37
+ if _is_picklable(obj, dumps):
38
+ return
39
+ if sys.version_info < (3, 11):
40
+ return
41
+ if isinstance(obj, dict):
42
+ for k, v in obj.items():
43
+ try:
44
+ debug_failed_pickles(v, dumps)
45
+ except HANDLED_PICKLE_ERRORS as e:
46
+ e.add_note(f"While pickling dict value for key {k!r}")
47
+ raise
48
+ try:
49
+ debug_failed_pickles(k, dumps)
50
+ except HANDLED_PICKLE_ERRORS as e:
51
+ e.add_note(f"While pickling dict key {k!r}")
52
+ raise
53
+ return
54
+ if isinstance(obj, (list, tuple)):
55
+ for i, v in enumerate(obj):
56
+ try:
57
+ debug_failed_pickles(v, dumps)
58
+ except HANDLED_PICKLE_ERRORS as e: # noqa: PERF203
59
+ e.add_note(f"While pickling index {i} of {type(obj).__name__}")
60
+ raise
61
+ return
62
+ picklable_thing = obj.__getstate__()
63
+ if picklable_thing is not None:
64
+ debug_failed_pickles(picklable_thing, dumps)
65
+ else:
66
+ try:
67
+ dumps(obj)
68
+ except HANDLED_PICKLE_ERRORS as e:
69
+ e.add_note(f"While pickling object of type {type(obj).__name__}")
70
+ raise
reflex/istate/manager.py CHANGED
@@ -179,6 +179,7 @@ def _default_token_expiration() -> int:
179
179
 
180
180
  def reset_disk_state_manager():
181
181
  """Reset the disk state manager."""
182
+ console.debug("Resetting disk state manager.")
182
183
  states_directory = prerequisites.get_states_dir()
183
184
  if states_directory.exists():
184
185
  for path in states_directory.iterdir():
reflex/istate/proxy.py CHANGED
@@ -10,7 +10,7 @@ import inspect
10
10
  import json
11
11
  from collections.abc import Callable, Sequence
12
12
  from types import MethodType
13
- from typing import TYPE_CHECKING, Any, SupportsIndex
13
+ from typing import TYPE_CHECKING, Any, SupportsIndex, TypeVar
14
14
 
15
15
  import pydantic
16
16
  import wrapt
@@ -27,6 +27,8 @@ from reflex.vars.base import Var
27
27
  if TYPE_CHECKING:
28
28
  from reflex.state import BaseState, StateUpdate
29
29
 
30
+ T_STATE = TypeVar("T_STATE", bound="BaseState")
31
+
30
32
 
31
33
  class StateProxy(wrapt.ObjectProxy):
32
34
  """Proxy of a state instance to control mutability of vars for a background task.
@@ -269,7 +271,7 @@ class StateProxy(wrapt.ObjectProxy):
269
271
  raise ImmutableStateError(msg)
270
272
  return self.__wrapped__.get_substate(path)
271
273
 
272
- async def get_state(self, state_cls: type[BaseState]) -> BaseState:
274
+ async def get_state(self, state_cls: type[T_STATE]) -> T_STATE:
273
275
  """Get an instance of the state associated with this token.
274
276
 
275
277
  Args:
@@ -289,7 +291,7 @@ class StateProxy(wrapt.ObjectProxy):
289
291
  raise ImmutableStateError(msg)
290
292
  return type(self)(
291
293
  await self.__wrapped__.get_state(state_cls), parent_state_proxy=self
292
- )
294
+ ) # pyright: ignore [reportReturnType]
293
295
 
294
296
  async def _as_state_update(self, *args, **kwargs) -> StateUpdate:
295
297
  """Temporarily allow mutability to access parent_state.
reflex/page.py CHANGED
@@ -8,7 +8,6 @@ from typing import Any
8
8
 
9
9
  from reflex.config import get_config
10
10
  from reflex.event import EventType
11
- from reflex.utils import console
12
11
 
13
12
  DECORATED_PAGES: dict[str, list] = defaultdict(list)
14
13
 
@@ -66,29 +65,3 @@ def page(
66
65
  return render_fn
67
66
 
68
67
  return decorator
69
-
70
-
71
- def get_decorated_pages(omit_implicit_routes: bool = True) -> list[dict[str, Any]]:
72
- """Get the decorated pages.
73
-
74
- Args:
75
- omit_implicit_routes: Whether to omit pages where the route will be implicitly guessed later.
76
-
77
- Returns:
78
- The decorated pages.
79
- """
80
- console.deprecate(
81
- "get_decorated_pages",
82
- reason="This function is deprecated and will be removed in a future version.",
83
- deprecation_version="0.7.9",
84
- removal_version="0.8.0",
85
- dedupe=True,
86
- )
87
- return sorted(
88
- [
89
- page_data
90
- for _, page_data in DECORATED_PAGES[get_config().app_name]
91
- if not omit_implicit_routes or "route" in page_data
92
- ],
93
- key=lambda x: x.get("route", ""),
94
- )
@@ -3,5 +3,6 @@
3
3
  from .base import CommonContext as CommonContext
4
4
  from .base import Plugin as Plugin
5
5
  from .base import PreCompileContext as PreCompileContext
6
- from .tailwind_v3 import Plugin as TailwindV3Plugin
7
- from .tailwind_v4 import Plugin as TailwindV4Plugin
6
+ from .sitemap import Plugin as SitemapPlugin
7
+ from .tailwind_v3 import TailwindV3Plugin as TailwindV3Plugin
8
+ from .tailwind_v4 import TailwindV4Plugin as TailwindV4Plugin
reflex/plugins/base.py CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  from collections.abc import Callable, Sequence
4
4
  from pathlib import Path
5
- from typing import ParamSpec, Protocol, TypedDict
5
+ from typing import TYPE_CHECKING, ParamSpec, Protocol, TypedDict
6
6
 
7
7
  from typing_extensions import Unpack
8
8
 
9
+ if TYPE_CHECKING:
10
+ from reflex.app import UnevaluatedPage
11
+
9
12
 
10
13
  class CommonContext(TypedDict):
11
14
  """Common context for all plugins."""
@@ -38,6 +41,7 @@ class PreCompileContext(CommonContext):
38
41
 
39
42
  add_save_task: AddTaskProtcol
40
43
  add_modify_task: Callable[[str, Callable[[str], str]], None]
44
+ unevaluated_pages: Sequence["UnevaluatedPage"]
41
45
 
42
46
 
43
47
  class Plugin:
@@ -0,0 +1,215 @@
1
+ """Tailwind CSS configuration types for Reflex plugins."""
2
+
3
+ import dataclasses
4
+ from copy import deepcopy
5
+ from typing import Any, Literal, TypedDict
6
+
7
+ from typing_extensions import NotRequired
8
+
9
+ from reflex.utils.decorator import once
10
+
11
+ from .base import Plugin as PluginBase
12
+
13
+ TailwindPluginImport = TypedDict(
14
+ "TailwindPluginImport",
15
+ {
16
+ "name": str,
17
+ "from": str,
18
+ },
19
+ )
20
+
21
+ TailwindPluginWithCallConfig = TypedDict(
22
+ "TailwindPluginWithCallConfig",
23
+ {
24
+ "name": str,
25
+ "import": NotRequired[TailwindPluginImport],
26
+ "call": str,
27
+ "args": NotRequired[dict[str, Any]],
28
+ },
29
+ )
30
+
31
+ TailwindPluginWithoutCallConfig = TypedDict(
32
+ "TailwindPluginWithoutCallConfig",
33
+ {
34
+ "name": str,
35
+ "import": NotRequired[TailwindPluginImport],
36
+ },
37
+ )
38
+
39
+ TailwindPluginConfig = (
40
+ TailwindPluginWithCallConfig | TailwindPluginWithoutCallConfig | str
41
+ )
42
+
43
+
44
+ def remove_version_from_plugin(plugin: TailwindPluginConfig) -> TailwindPluginConfig:
45
+ """Remove the version from a plugin name.
46
+
47
+ Args:
48
+ plugin: The plugin to remove the version from.
49
+
50
+ Returns:
51
+ The plugin without the version.
52
+ """
53
+ from reflex.utils.format import format_library_name
54
+
55
+ if isinstance(plugin, str):
56
+ return format_library_name(plugin)
57
+
58
+ if plugin_import := plugin.get("import"):
59
+ plugin_import["from"] = format_library_name(plugin_import["from"])
60
+
61
+ plugin["name"] = format_library_name(plugin["name"])
62
+
63
+ return plugin
64
+
65
+
66
+ class TailwindConfig(TypedDict):
67
+ """Tailwind CSS configuration options.
68
+
69
+ See: https://tailwindcss.com/docs/configuration
70
+ """
71
+
72
+ content: NotRequired[list[str]]
73
+ important: NotRequired[str | bool]
74
+ prefix: NotRequired[str]
75
+ separator: NotRequired[str]
76
+ presets: NotRequired[list[str]]
77
+ darkMode: NotRequired[Literal["media", "class", "selector"]]
78
+ theme: NotRequired[dict[str, Any]]
79
+ corePlugins: NotRequired[list[str] | dict[str, bool]]
80
+ plugins: NotRequired[list[TailwindPluginConfig]]
81
+
82
+
83
+ @once
84
+ def tailwind_config_js_template():
85
+ """Get the Tailwind config template.
86
+
87
+ Returns:
88
+ The Tailwind config template.
89
+ """
90
+ from reflex.compiler.templates import from_string
91
+
92
+ source = r"""
93
+ {# Extract destructured imports from plugin dicts only #}
94
+ {%- set imports = [] %}
95
+
96
+ {%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
97
+ {%- set _ = imports.append(plugin.import) %}
98
+ {%- endfor %}
99
+
100
+ {%- for imp in imports %}
101
+ import { {{ imp.name }} } from {{ imp.from | tojson }};
102
+ {%- endfor %}
103
+
104
+ {%- for plugin in plugins %}
105
+ {% if plugin is mapping and plugin.call is not defined %}
106
+ import plugin{{ loop.index }} from {{ plugin.name | tojson }};
107
+ {%- elif plugin is not mapping %}
108
+ import plugin{{ loop.index }} from {{ plugin | tojson }};
109
+ {%- endif %}
110
+ {%- endfor %}
111
+
112
+ {%- for preset in presets %}
113
+ import preset{{ loop.index }} from {{ preset | tojson }};
114
+ {%- endfor %}
115
+
116
+ export default {
117
+ content: {{ (content if content is defined else DEFAULT_CONTENT) | tojson }},
118
+ {% if theme is defined %}theme: {{ theme | tojson }},{% else %}theme: {},{% endif %}
119
+ {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
120
+ {% if corePlugins is defined %}corePlugins: {{ corePlugins | tojson }},{% endif %}
121
+ {% if important is defined %}important: {{ important | tojson }},{% endif %}
122
+ {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
123
+ {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
124
+ {% if presets is defined %}
125
+ presets: [
126
+ {% for preset in presets %}
127
+ preset{{ loop.index }},
128
+ {% endfor %}
129
+ ],
130
+ {% endif %}
131
+ plugins: [
132
+ {% for plugin in plugins %}
133
+ {% if plugin is mapping and plugin.call is defined %}
134
+ {{ plugin.call }}(
135
+ {%- if plugin.args is defined -%}
136
+ {{ plugin.args | tojson }}
137
+ {%- endif -%}
138
+ ),
139
+ {% else %}
140
+ plugin{{ loop.index }},
141
+ {% endif %}
142
+ {% endfor %}
143
+ ]
144
+ };
145
+ """
146
+
147
+ return from_string(source)
148
+
149
+
150
+ @dataclasses.dataclass
151
+ class TailwindPlugin(PluginBase):
152
+ """Plugin for Tailwind CSS."""
153
+
154
+ config: TailwindConfig = dataclasses.field(
155
+ default_factory=lambda: TailwindConfig(
156
+ plugins=[
157
+ "@tailwindcss/typography@0.5.16",
158
+ ],
159
+ )
160
+ )
161
+
162
+ def get_frontend_development_dependencies(self, **context) -> list[str]:
163
+ """Get the packages required by the plugin.
164
+
165
+ Args:
166
+ **context: The context for the plugin.
167
+
168
+ Returns:
169
+ A list of packages required by the plugin.
170
+ """
171
+ config = self.get_config()
172
+
173
+ return [
174
+ plugin if isinstance(plugin, str) else plugin.get("name")
175
+ for plugin in config.get("plugins", [])
176
+ ] + config.get("presets", [])
177
+
178
+ def get_config(self) -> TailwindConfig:
179
+ """Get the Tailwind CSS configuration.
180
+
181
+ Returns:
182
+ The Tailwind CSS configuration.
183
+ """
184
+ from reflex.config import get_config
185
+
186
+ rxconfig_config = getattr(get_config(), "tailwind", None)
187
+
188
+ if rxconfig_config is not None and rxconfig_config != self.config:
189
+ from reflex.utils import console
190
+
191
+ console.warn(
192
+ "It seems you have provided a tailwind configuration in your call to `rx.Config`."
193
+ f" You should provide the configuration as an argument to `rx.plugins.{self.__class__.__name__}()` instead."
194
+ )
195
+ return rxconfig_config
196
+
197
+ return self.config
198
+
199
+ def get_unversioned_config(self) -> TailwindConfig:
200
+ """Get the Tailwind CSS configuration without version-specific adjustments.
201
+
202
+ Returns:
203
+ The Tailwind CSS configuration without version-specific adjustments.
204
+ """
205
+ from reflex.utils.format import format_library_name
206
+
207
+ config = deepcopy(self.get_config())
208
+ if presets := config.get("presets"):
209
+ # Somehow, having an empty list of presets breaks Tailwind.
210
+ # So we only set the presets if there are any.
211
+ config["presets"] = [format_library_name(preset) for preset in presets]
212
+ config["plugins"] = [
213
+ remove_version_from_plugin(plugin) for plugin in config.get("plugins", [])
214
+ ]
215
+ return config
@@ -0,0 +1,206 @@
1
+ """Sitemap plugin for Reflex."""
2
+
3
+ import datetime
4
+ from collections.abc import Sequence
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+ from typing import TYPE_CHECKING, Literal, TypedDict
8
+ from xml.dom import minidom
9
+ from xml.etree.ElementTree import Element, SubElement, tostring
10
+
11
+ from typing_extensions import NotRequired
12
+
13
+ from reflex import constants
14
+
15
+ from .base import Plugin as PluginBase
16
+
17
+ if TYPE_CHECKING:
18
+ from reflex.app import UnevaluatedPage
19
+
20
+ Location = str
21
+ LastModified = datetime.datetime
22
+ ChangeFrequency = Literal[
23
+ "always", "hourly", "daily", "weekly", "monthly", "yearly", "never"
24
+ ]
25
+ Priority = float
26
+
27
+
28
+ class SitemapLink(TypedDict):
29
+ """A link in the sitemap."""
30
+
31
+ loc: Location
32
+ lastmod: NotRequired[LastModified]
33
+ changefreq: NotRequired[ChangeFrequency]
34
+ priority: NotRequired[Priority]
35
+
36
+
37
+ class SitemapLinkConfiguration(TypedDict):
38
+ """Configuration for a sitemap link."""
39
+
40
+ loc: NotRequired[Location]
41
+ lastmod: NotRequired[LastModified]
42
+ changefreq: NotRequired[ChangeFrequency]
43
+ priority: NotRequired[Priority]
44
+
45
+
46
+ class Constants(SimpleNamespace):
47
+ """Sitemap constants."""
48
+
49
+ FILE_PATH: Path = Path(constants.Dirs.PUBLIC) / "sitemap.xml"
50
+
51
+
52
+ def configuration_with_loc(
53
+ *, config: SitemapLinkConfiguration, deploy_url: str | None, loc: Location
54
+ ) -> SitemapLink:
55
+ """Set the 'loc' field of the configuration.
56
+
57
+ Args:
58
+ config: The configuration dictionary.
59
+ deploy_url: The deployment URL, if any.
60
+ loc: The location to set.
61
+
62
+ Returns:
63
+ A SitemapLink dictionary with the 'loc' field set.
64
+ """
65
+ if deploy_url and not loc.startswith("http://") and not loc.startswith("https://"):
66
+ loc = f"{deploy_url.rstrip('/')}/{loc.lstrip('/')}"
67
+ link: SitemapLink = {"loc": loc}
68
+ if (lastmod := config.get("lastmod")) is not None:
69
+ link["lastmod"] = lastmod
70
+ if (changefreq := config.get("changefreq")) is not None:
71
+ link["changefreq"] = changefreq
72
+ if (priority := config.get("priority")) is not None:
73
+ link["priority"] = min(1.0, max(0.0, priority))
74
+ return link
75
+
76
+
77
+ def generate_xml(links: Sequence[SitemapLink]) -> str:
78
+ """Generate an XML sitemap from a list of links.
79
+
80
+ Args:
81
+ links: A sequence of SitemapLink dictionaries.
82
+
83
+ Returns:
84
+ A pretty-printed XML string representing the sitemap.
85
+ """
86
+ urlset = Element("urlset", xmlns="https://www.sitemaps.org/schemas/sitemap/0.9")
87
+
88
+ for link in links:
89
+ url = SubElement(urlset, "url")
90
+
91
+ loc_element = SubElement(url, "loc")
92
+ loc_element.text = link["loc"]
93
+
94
+ if (changefreq := link.get("changefreq")) is not None:
95
+ changefreq_element = SubElement(url, "changefreq")
96
+ changefreq_element.text = changefreq
97
+
98
+ if (lastmod := link.get("lastmod")) is not None:
99
+ lastmod_element = SubElement(url, "lastmod")
100
+ if isinstance(lastmod, datetime.datetime):
101
+ lastmod = lastmod.isoformat()
102
+ lastmod_element.text = lastmod
103
+
104
+ if (priority := link.get("priority")) is not None:
105
+ priority_element = SubElement(url, "priority")
106
+ priority_element.text = str(priority)
107
+
108
+ rough_string = tostring(urlset, "utf-8")
109
+ reparsed = minidom.parseString(rough_string)
110
+ return reparsed.toprettyxml(indent=" ")
111
+
112
+
113
+ def is_route_dynamic(route: str) -> bool:
114
+ """Check if a route is dynamic.
115
+
116
+ Args:
117
+ route: The route to check.
118
+
119
+ Returns:
120
+ True if the route is dynamic, False otherwise.
121
+ """
122
+ return "[" in route and "]" in route
123
+
124
+
125
+ def generate_links_for_sitemap(
126
+ unevaluated_pages: Sequence["UnevaluatedPage"],
127
+ ) -> list[SitemapLink]:
128
+ """Generate sitemap links from unevaluated pages.
129
+
130
+ Args:
131
+ unevaluated_pages: Sequence of unevaluated pages.
132
+
133
+ Returns:
134
+ A list of SitemapLink dictionaries.
135
+ """
136
+ from reflex.config import get_config
137
+ from reflex.utils import console
138
+
139
+ deploy_url = get_config().deploy_url
140
+
141
+ links: list[SitemapLink] = []
142
+
143
+ for page in unevaluated_pages:
144
+ sitemap_config: SitemapLinkConfiguration = page.context.get("sitemap", {})
145
+
146
+ if is_route_dynamic(page.route) or page.route == "404":
147
+ if not sitemap_config:
148
+ continue
149
+
150
+ if (loc := sitemap_config.get("loc")) is None:
151
+ route_message = (
152
+ "Dynamic route" if is_route_dynamic(page.route) else "Route 404"
153
+ )
154
+ console.warn(
155
+ route_message
156
+ + f" '{page.route}' does not have a 'loc' in sitemap configuration. Skipping."
157
+ )
158
+ continue
159
+
160
+ sitemap_link = configuration_with_loc(
161
+ config=sitemap_config, deploy_url=deploy_url, loc=loc
162
+ )
163
+
164
+ elif (loc := sitemap_config.get("loc")) is not None:
165
+ sitemap_link = configuration_with_loc(
166
+ config=sitemap_config, deploy_url=deploy_url, loc=loc
167
+ )
168
+
169
+ else:
170
+ loc = page.route if page.route != "index" else "/"
171
+ if not loc.startswith("/"):
172
+ loc = "/" + loc
173
+ sitemap_link = configuration_with_loc(
174
+ config=sitemap_config, deploy_url=deploy_url, loc=loc
175
+ )
176
+
177
+ links.append(sitemap_link)
178
+ return links
179
+
180
+
181
+ def sitemap_task(unevaluated_pages: Sequence["UnevaluatedPage"]) -> tuple[str, str]:
182
+ """Task to generate the sitemap XML file.
183
+
184
+ Args:
185
+ unevaluated_pages: Sequence of unevaluated pages.
186
+
187
+ Returns:
188
+ A tuple containing the file path and the generated XML content.
189
+ """
190
+ return (
191
+ str(Constants.FILE_PATH),
192
+ generate_xml(generate_links_for_sitemap(unevaluated_pages)),
193
+ )
194
+
195
+
196
+ class Plugin(PluginBase):
197
+ """Sitemap plugin for Reflex."""
198
+
199
+ def pre_compile(self, **context):
200
+ """Generate the sitemap XML file before compilation.
201
+
202
+ Args:
203
+ context: The context for the plugin.
204
+ """
205
+ unevaluated_pages = context.get("unevaluated_pages", [])
206
+ context["add_save_task"](sitemap_task, unevaluated_pages)