reflex 0.7.14a5__py3-none-any.whl → 0.8.0a1__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 (205) 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 +16 -10
  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/utils/context.js.jinja2 +25 -6
  7. reflex/.templates/web/app/entry.client.js +8 -0
  8. reflex/.templates/web/app/routes.js +10 -0
  9. reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +12 -32
  10. reflex/.templates/web/postcss.config.js +1 -1
  11. reflex/.templates/web/react-router.config.js +6 -0
  12. reflex/.templates/web/utils/client_side_routing.js +21 -19
  13. reflex/.templates/web/utils/react-theme.js +89 -0
  14. reflex/.templates/web/utils/state.js +155 -67
  15. reflex/.templates/web/vite.config.js +32 -0
  16. reflex/__init__.py +1 -6
  17. reflex/__init__.pyi +0 -4
  18. reflex/app.py +69 -132
  19. reflex/base.py +1 -87
  20. reflex/compiler/compiler.py +40 -3
  21. reflex/compiler/utils.py +54 -28
  22. reflex/components/__init__.py +0 -2
  23. reflex/components/__init__.pyi +0 -3
  24. reflex/components/base/__init__.py +1 -5
  25. reflex/components/base/__init__.pyi +4 -6
  26. reflex/components/base/app_wrap.pyi +5 -4
  27. reflex/components/base/body.pyi +5 -4
  28. reflex/components/base/document.py +18 -14
  29. reflex/components/base/document.pyi +83 -27
  30. reflex/components/base/error_boundary.pyi +5 -4
  31. reflex/components/base/fragment.pyi +5 -4
  32. reflex/components/base/link.pyi +9 -7
  33. reflex/components/base/meta.pyi +17 -13
  34. reflex/components/base/script.py +60 -58
  35. reflex/components/base/script.pyi +246 -31
  36. reflex/components/base/strict_mode.pyi +5 -4
  37. reflex/components/component.py +109 -194
  38. reflex/components/core/__init__.py +1 -0
  39. reflex/components/core/__init__.pyi +1 -0
  40. reflex/components/core/auto_scroll.pyi +5 -4
  41. reflex/components/core/banner.pyi +25 -19
  42. reflex/components/core/client_side_routing.py +7 -6
  43. reflex/components/core/client_side_routing.pyi +6 -56
  44. reflex/components/core/clipboard.pyi +5 -4
  45. reflex/components/core/debounce.py +1 -0
  46. reflex/components/core/debounce.pyi +5 -4
  47. reflex/components/core/foreach.py +3 -2
  48. reflex/components/core/helmet.py +14 -0
  49. reflex/components/{next/base.pyi → core/helmet.pyi} +10 -7
  50. reflex/components/core/html.pyi +5 -4
  51. reflex/components/core/sticky.pyi +17 -13
  52. reflex/components/core/upload.py +2 -1
  53. reflex/components/core/upload.pyi +21 -16
  54. reflex/components/datadisplay/code.pyi +9 -7
  55. reflex/components/datadisplay/dataeditor.pyi +5 -4
  56. reflex/components/datadisplay/shiki_code_block.pyi +13 -10
  57. reflex/components/dynamic.py +4 -5
  58. reflex/components/el/element.pyi +5 -4
  59. reflex/components/el/elements/base.pyi +5 -4
  60. reflex/components/el/elements/forms.pyi +69 -52
  61. reflex/components/el/elements/inline.pyi +113 -85
  62. reflex/components/el/elements/media.pyi +105 -79
  63. reflex/components/el/elements/metadata.pyi +25 -19
  64. reflex/components/el/elements/other.pyi +29 -22
  65. reflex/components/el/elements/scripts.pyi +13 -10
  66. reflex/components/el/elements/sectioning.pyi +61 -46
  67. reflex/components/el/elements/tables.pyi +41 -31
  68. reflex/components/el/elements/typography.pyi +61 -46
  69. reflex/components/field.py +175 -0
  70. reflex/components/gridjs/datatable.py +2 -2
  71. reflex/components/gridjs/datatable.pyi +11 -9
  72. reflex/components/lucide/icon.py +6 -2
  73. reflex/components/lucide/icon.pyi +15 -10
  74. reflex/components/markdown/markdown.pyi +5 -4
  75. reflex/components/moment/moment.pyi +5 -4
  76. reflex/components/plotly/plotly.pyi +19 -10
  77. reflex/components/props.py +376 -27
  78. reflex/components/radix/primitives/accordion.py +8 -1
  79. reflex/components/radix/primitives/accordion.pyi +29 -22
  80. reflex/components/radix/primitives/base.pyi +9 -7
  81. reflex/components/radix/primitives/drawer.pyi +45 -34
  82. reflex/components/radix/primitives/form.pyi +41 -31
  83. reflex/components/radix/primitives/progress.pyi +21 -16
  84. reflex/components/radix/primitives/slider.pyi +21 -16
  85. reflex/components/radix/themes/base.py +3 -3
  86. reflex/components/radix/themes/base.pyi +33 -25
  87. reflex/components/radix/themes/color_mode.pyi +13 -10
  88. reflex/components/radix/themes/components/alert_dialog.pyi +29 -22
  89. reflex/components/radix/themes/components/aspect_ratio.pyi +5 -4
  90. reflex/components/radix/themes/components/avatar.pyi +5 -4
  91. reflex/components/radix/themes/components/badge.pyi +5 -4
  92. reflex/components/radix/themes/components/button.pyi +5 -4
  93. reflex/components/radix/themes/components/callout.pyi +21 -16
  94. reflex/components/radix/themes/components/card.pyi +5 -4
  95. reflex/components/radix/themes/components/checkbox.pyi +13 -10
  96. reflex/components/radix/themes/components/checkbox_cards.pyi +9 -7
  97. reflex/components/radix/themes/components/checkbox_group.pyi +9 -7
  98. reflex/components/radix/themes/components/context_menu.pyi +53 -40
  99. reflex/components/radix/themes/components/data_list.pyi +17 -13
  100. reflex/components/radix/themes/components/dialog.pyi +29 -22
  101. reflex/components/radix/themes/components/dropdown_menu.pyi +33 -25
  102. reflex/components/radix/themes/components/hover_card.pyi +17 -13
  103. reflex/components/radix/themes/components/icon_button.pyi +5 -4
  104. reflex/components/radix/themes/components/inset.pyi +5 -4
  105. reflex/components/radix/themes/components/popover.pyi +17 -13
  106. reflex/components/radix/themes/components/progress.pyi +5 -4
  107. reflex/components/radix/themes/components/radio.pyi +5 -4
  108. reflex/components/radix/themes/components/radio_cards.pyi +9 -7
  109. reflex/components/radix/themes/components/radio_group.pyi +17 -13
  110. reflex/components/radix/themes/components/scroll_area.pyi +5 -4
  111. reflex/components/radix/themes/components/segmented_control.pyi +9 -7
  112. reflex/components/radix/themes/components/select.pyi +37 -28
  113. reflex/components/radix/themes/components/separator.pyi +5 -4
  114. reflex/components/radix/themes/components/skeleton.pyi +5 -4
  115. reflex/components/radix/themes/components/slider.pyi +5 -4
  116. reflex/components/radix/themes/components/spinner.pyi +5 -4
  117. reflex/components/radix/themes/components/switch.pyi +5 -4
  118. reflex/components/radix/themes/components/table.pyi +29 -22
  119. reflex/components/radix/themes/components/tabs.pyi +21 -16
  120. reflex/components/radix/themes/components/text_area.pyi +5 -4
  121. reflex/components/radix/themes/components/text_field.pyi +13 -10
  122. reflex/components/radix/themes/components/tooltip.pyi +5 -4
  123. reflex/components/radix/themes/layout/base.pyi +5 -4
  124. reflex/components/radix/themes/layout/box.pyi +5 -4
  125. reflex/components/radix/themes/layout/center.pyi +5 -4
  126. reflex/components/radix/themes/layout/container.pyi +5 -4
  127. reflex/components/radix/themes/layout/flex.pyi +5 -4
  128. reflex/components/radix/themes/layout/grid.pyi +5 -4
  129. reflex/components/radix/themes/layout/list.pyi +21 -16
  130. reflex/components/radix/themes/layout/section.pyi +5 -4
  131. reflex/components/radix/themes/layout/spacer.pyi +5 -4
  132. reflex/components/radix/themes/layout/stack.pyi +13 -10
  133. reflex/components/radix/themes/typography/blockquote.pyi +5 -4
  134. reflex/components/radix/themes/typography/code.pyi +5 -4
  135. reflex/components/radix/themes/typography/heading.pyi +5 -4
  136. reflex/components/radix/themes/typography/link.py +42 -9
  137. reflex/components/radix/themes/typography/link.pyi +311 -6
  138. reflex/components/radix/themes/typography/text.pyi +29 -22
  139. reflex/components/react_player/audio.pyi +5 -4
  140. reflex/components/react_player/react_player.pyi +5 -4
  141. reflex/components/react_player/video.pyi +5 -4
  142. reflex/components/recharts/cartesian.py +2 -1
  143. reflex/components/recharts/cartesian.pyi +65 -46
  144. reflex/components/recharts/charts.py +4 -2
  145. reflex/components/recharts/charts.pyi +36 -24
  146. reflex/components/recharts/general.pyi +24 -18
  147. reflex/components/recharts/polar.py +8 -4
  148. reflex/components/recharts/polar.pyi +16 -10
  149. reflex/components/recharts/recharts.pyi +9 -7
  150. reflex/components/sonner/toast.py +2 -2
  151. reflex/components/sonner/toast.pyi +7 -6
  152. reflex/config.py +3 -77
  153. reflex/constants/__init__.py +1 -0
  154. reflex/constants/base.py +38 -8
  155. reflex/constants/compiler.py +4 -2
  156. reflex/constants/event.py +1 -0
  157. reflex/constants/installer.py +23 -16
  158. reflex/constants/state.py +2 -0
  159. reflex/custom_components/custom_components.py +0 -14
  160. reflex/environment.py +1 -1
  161. reflex/event.py +178 -81
  162. reflex/experimental/__init__.py +0 -30
  163. reflex/istate/proxy.py +5 -3
  164. reflex/page.py +0 -27
  165. reflex/plugins/__init__.py +3 -2
  166. reflex/plugins/base.py +5 -1
  167. reflex/plugins/shared_tailwind.py +158 -0
  168. reflex/plugins/sitemap.py +206 -0
  169. reflex/plugins/tailwind_v3.py +13 -106
  170. reflex/plugins/tailwind_v4.py +15 -108
  171. reflex/reflex.py +1 -0
  172. reflex/state.py +134 -140
  173. reflex/testing.py +57 -9
  174. reflex/utils/build.py +9 -69
  175. reflex/utils/exec.py +59 -161
  176. reflex/utils/export.py +1 -1
  177. reflex/utils/imports.py +0 -4
  178. reflex/utils/misc.py +28 -0
  179. reflex/utils/prerequisites.py +62 -59
  180. reflex/utils/processes.py +6 -6
  181. reflex/utils/pyi_generator.py +21 -9
  182. reflex/utils/serializers.py +14 -1
  183. reflex/utils/types.py +196 -61
  184. reflex/vars/__init__.py +2 -0
  185. reflex/vars/base.py +367 -134
  186. {reflex-0.7.14a5.dist-info → reflex-0.8.0a1.dist-info}/METADATA +12 -5
  187. {reflex-0.7.14a5.dist-info → reflex-0.8.0a1.dist-info}/RECORD +190 -196
  188. reflex/.templates/web/next.config.js +0 -7
  189. reflex/components/base/head.py +0 -20
  190. reflex/components/base/head.pyi +0 -116
  191. reflex/components/next/__init__.py +0 -10
  192. reflex/components/next/base.py +0 -7
  193. reflex/components/next/image.py +0 -117
  194. reflex/components/next/image.pyi +0 -94
  195. reflex/components/next/link.py +0 -20
  196. reflex/components/next/link.pyi +0 -67
  197. reflex/components/next/video.py +0 -38
  198. reflex/components/next/video.pyi +0 -68
  199. reflex/components/suneditor/__init__.py +0 -5
  200. reflex/components/suneditor/editor.py +0 -269
  201. reflex/components/suneditor/editor.pyi +0 -199
  202. reflex/experimental/layout.py +0 -254
  203. {reflex-0.7.14a5.dist-info → reflex-0.8.0a1.dist-info}/WHEEL +0 -0
  204. {reflex-0.7.14a5.dist-info → reflex-0.8.0a1.dist-info}/entry_points.txt +0 -0
  205. {reflex-0.7.14a5.dist-info → reflex-0.8.0a1.dist-info}/licenses/LICENSE +0 -0
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,158 @@
1
+ """Tailwind CSS configuration types for Reflex plugins."""
2
+
3
+ import dataclasses
4
+ from typing import Any, Literal, TypedDict
5
+
6
+ from typing_extensions import NotRequired
7
+
8
+ from reflex.utils.decorator import once
9
+
10
+ from .base import Plugin as PluginBase
11
+
12
+ TailwindPluginImport = TypedDict(
13
+ "TailwindPluginImport",
14
+ {
15
+ "name": str,
16
+ "from": str,
17
+ },
18
+ )
19
+
20
+ TailwindPluginWithCallConfig = TypedDict(
21
+ "TailwindPluginWithCallConfig",
22
+ {
23
+ "name": str,
24
+ "import": NotRequired[TailwindPluginImport],
25
+ "call": str,
26
+ "args": NotRequired[dict[str, Any]],
27
+ },
28
+ )
29
+
30
+ TailwindPluginWithoutCallConfig = TypedDict(
31
+ "TailwindPluginWithoutCallConfig",
32
+ {
33
+ "name": str,
34
+ "import": NotRequired[TailwindPluginImport],
35
+ },
36
+ )
37
+
38
+ TailwindPluginConfig = (
39
+ TailwindPluginWithCallConfig | TailwindPluginWithoutCallConfig | str
40
+ )
41
+
42
+
43
+ class TailwindConfig(TypedDict):
44
+ """Tailwind CSS configuration options.
45
+
46
+ See: https://tailwindcss.com/docs/configuration
47
+ """
48
+
49
+ content: NotRequired[list[str]]
50
+ important: NotRequired[str | bool]
51
+ prefix: NotRequired[str]
52
+ separator: NotRequired[str]
53
+ presets: NotRequired[list[str]]
54
+ darkMode: NotRequired[Literal["media", "class", "selector"]]
55
+ theme: NotRequired[dict[str, Any]]
56
+ corePlugins: NotRequired[list[str] | dict[str, bool]]
57
+ plugins: NotRequired[list[TailwindPluginConfig]]
58
+
59
+
60
+ @once
61
+ def tailwind_config_js_template():
62
+ """Get the Tailwind config template.
63
+
64
+ Returns:
65
+ The Tailwind config template.
66
+ """
67
+ from reflex.compiler.templates import from_string
68
+
69
+ source = r"""
70
+ {# Extract destructured imports from plugin dicts only #}
71
+ {%- set imports = [] %}
72
+
73
+ {%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
74
+ {%- set _ = imports.append(plugin.import) %}
75
+ {%- endfor %}
76
+
77
+ {%- for imp in imports %}
78
+ import { {{ imp.name }} } from {{ imp.from | tojson }};
79
+ {%- endfor %}
80
+
81
+ {%- for plugin in plugins %}
82
+ {% if plugin is mapping and plugin.call is not defined %}
83
+ import plugin{{ loop.index }} from {{ plugin.name | tojson }};
84
+ {%- elif plugin is not mapping %}
85
+ import plugin{{ loop.index }} from {{ plugin | tojson }};
86
+ {%- endif %}
87
+ {%- endfor %}
88
+
89
+ {%- for preset in presets %}
90
+ import preset{{ loop.index }} from {{ preset | tojson }};
91
+ {%- endfor %}
92
+
93
+ export default {
94
+ content: {{ (content if content is defined else DEFAULT_CONTENT) | tojson }},
95
+ {% if theme is defined %}theme: {{ theme | tojson }},{% else %}theme: {},{% endif %}
96
+ {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
97
+ {% if corePlugins is defined %}corePlugins: {{ corePlugins | tojson }},{% endif %}
98
+ {% if important is defined %}important: {{ important | tojson }},{% endif %}
99
+ {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
100
+ {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
101
+ {% if presets is defined %}
102
+ presets: [
103
+ {% for preset in presets %}
104
+ preset{{ loop.index }},
105
+ {% endfor %}
106
+ ],
107
+ {% endif %}
108
+ plugins: [
109
+ {% for plugin in plugins %}
110
+ {% if plugin is mapping and plugin.call is defined %}
111
+ {{ plugin.call }}(
112
+ {%- if plugin.args is defined -%}
113
+ {{ plugin.args | tojson }}
114
+ {%- endif -%}
115
+ ),
116
+ {% else %}
117
+ plugin{{ loop.index }},
118
+ {% endif %}
119
+ {% endfor %}
120
+ ]
121
+ };
122
+ """
123
+
124
+ return from_string(source)
125
+
126
+
127
+ @dataclasses.dataclass
128
+ class TailwindPlugin(PluginBase):
129
+ """Plugin for Tailwind CSS."""
130
+
131
+ config: TailwindConfig = dataclasses.field(
132
+ default_factory=lambda: TailwindConfig(
133
+ plugins=[
134
+ "@tailwindcss/typography",
135
+ ],
136
+ )
137
+ )
138
+
139
+ def get_config(self) -> TailwindConfig:
140
+ """Get the Tailwind CSS configuration.
141
+
142
+ Returns:
143
+ The Tailwind CSS configuration.
144
+ """
145
+ from reflex.config import get_config
146
+
147
+ rxconfig_config = getattr(get_config(), "tailwind", None)
148
+
149
+ if rxconfig_config is not None and rxconfig_config != self.config:
150
+ from reflex.utils import console
151
+
152
+ console.warn(
153
+ "It seems you have provided a tailwind configuration in your call to `rx.Config`."
154
+ f" You should provide the configuration as an argument to `rx.plugins.{self.__class__.__name__}()` instead."
155
+ )
156
+ return rxconfig_config
157
+
158
+ return self.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)
@@ -1,12 +1,16 @@
1
1
  """Base class for all plugins."""
2
2
 
3
+ import dataclasses
3
4
  from pathlib import Path
4
5
  from types import SimpleNamespace
5
6
 
6
7
  from reflex.constants.base import Dirs
7
8
  from reflex.constants.compiler import Ext, PageNames
8
- from reflex.plugins.base import Plugin as PluginBase
9
- from reflex.utils.decorator import once
9
+ from reflex.plugins.shared_tailwind import (
10
+ TailwindConfig,
11
+ TailwindPlugin,
12
+ tailwind_config_js_template,
13
+ )
10
14
 
11
15
 
12
16
  class Constants(SimpleNamespace):
@@ -17,7 +21,7 @@ class Constants(SimpleNamespace):
17
21
  # The Tailwind config.
18
22
  CONFIG = "tailwind.config.js"
19
23
  # Default Tailwind content paths
20
- CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"]
24
+ CONTENT = [f"./{Dirs.PAGES}/**/*.{{js,ts,jsx,tsx}}", "./utils/**/*.{js,ts,jsx,tsx}"]
21
25
  # Relative tailwind style path to root stylesheet in Dirs.STYLES.
22
26
  ROOT_STYLE_PATH = "./tailwind.css"
23
27
 
@@ -35,90 +39,7 @@ class Constants(SimpleNamespace):
35
39
  TAILWIND_CSS = "@import url('./tailwind.css');"
36
40
 
37
41
 
38
- @once
39
- def tailwind_config_js_template():
40
- """Get the Tailwind config template.
41
-
42
- Returns:
43
- The Tailwind config template.
44
- """
45
- from reflex.compiler.templates import from_string
46
-
47
- source = r"""
48
- {# Helper macro to render JS objects and arrays #}
49
- {% macro render_js(val, indent=2, level=0) -%}
50
- {%- set space = ' ' * (indent * level) -%}
51
- {%- set next_space = ' ' * (indent * (level + 1)) -%}
52
-
53
- {%- if val is mapping -%}
54
- {
55
- {%- for k, v in val.items() %}
56
- {{ next_space }}{{ k if k is string and k.isidentifier() else k|tojson }}: {{ render_js(v, indent, level + 1) }}{{ "," if not loop.last }}
57
- {%- endfor %}
58
- {{ space }}}
59
- {%- elif val is iterable and val is not string -%}
60
- [
61
- {%- for item in val %}
62
- {{ next_space }}{{ render_js(item, indent, level + 1) }}{{ "," if not loop.last }}
63
- {%- endfor %}
64
- {{ space }}]
65
- {%- else -%}
66
- {{ val | tojson }}
67
- {%- endif -%}
68
- {%- endmacro %}
69
-
70
- {# Extract destructured imports from plugin dicts only #}
71
- {%- set imports = [] %}
72
- {%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
73
- {%- set _ = imports.append(plugin.import) %}
74
- {%- endfor %}
75
-
76
- /** @type {import('tailwindcss').Config} */
77
- {%- for imp in imports %}
78
- const { {{ imp.name }} } = require({{ imp.from | tojson }});
79
- {%- endfor %}
80
-
81
- module.exports = {
82
- content: {{ render_js(content) }},
83
- theme: {{ render_js(theme) }},
84
- {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
85
- {% if corePlugins is defined %}corePlugins: {{ render_js(corePlugins) }},{% endif %}
86
- {% if important is defined %}important: {{ important | tojson }},{% endif %}
87
- {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
88
- {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
89
- {% if presets is defined %}
90
- presets: [
91
- {% for preset in presets %}
92
- require({{ preset | tojson }}){{ "," if not loop.last }}
93
- {% endfor %}
94
- ],
95
- {% endif %}
96
- plugins: [
97
- {% for plugin in plugins %}
98
- {% if plugin is mapping %}
99
- {% if plugin.call is defined %}
100
- {{ plugin.call }}(
101
- {%- if plugin.args is defined -%}
102
- {{ render_js(plugin.args) }}
103
- {%- endif -%}
104
- ){{ "," if not loop.last }}
105
- {% else %}
106
- require({{ plugin.name | tojson }}){{ "," if not loop.last }}
107
- {% endif %}
108
- {% else %}
109
- require({{ plugin | tojson }}){{ "," if not loop.last }}
110
- {% endif %}
111
- {% endfor %}
112
- ]
113
- };
114
- """
115
-
116
- return from_string(source)
117
-
118
-
119
- def compile_config(
120
- config: dict,
121
- ):
42
+ def compile_config(config: TailwindConfig):
122
43
  """Compile the Tailwind config.
123
44
 
124
45
  Args:
@@ -129,6 +50,7 @@ def compile_config(
129
50
  """
130
51
  return Constants.CONFIG, tailwind_config_js_template().render(
131
52
  **config,
53
+ DEFAULT_CONTENT=Constants.CONTENT,
132
54
  )
133
55
 
134
56
 
@@ -215,7 +137,8 @@ def add_tailwind_to_css_file(css_file_content: str) -> str:
215
137
  )
216
138
 
217
139
 
218
- class Plugin(PluginBase):
140
+ @dataclasses.dataclass
141
+ class TailwindV3Plugin(TailwindPlugin):
219
142
  """Plugin for Tailwind CSS."""
220
143
 
221
144
  def get_frontend_development_dependencies(self, **context) -> list[str]:
@@ -227,12 +150,9 @@ class Plugin(PluginBase):
227
150
  Returns:
228
151
  A list of packages required by the plugin.
229
152
  """
230
- from reflex.config import get_config
231
-
232
- config = get_config()
233
153
  return [
234
154
  plugin if isinstance(plugin, str) else plugin.get("name")
235
- for plugin in (config.tailwind or {}).get("plugins", [])
155
+ for plugin in self.get_config().get("plugins", [])
236
156
  ] + [Constants.VERSION]
237
157
 
238
158
  def pre_compile(self, **context):
@@ -241,23 +161,10 @@ class Plugin(PluginBase):
241
161
  Args:
242
162
  context: The context for the plugin.
243
163
  """
244
- from reflex.config import get_config
245
-
246
- config = get_config().tailwind or {}
247
-
248
- config["content"] = config.get("content", Constants.CONTENT)
249
- context["add_save_task"](compile_config, config)
164
+ context["add_save_task"](compile_config, self.get_config())
250
165
  context["add_save_task"](compile_root_style)
251
166
  context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config)
252
167
  context["add_modify_task"](
253
168
  str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)),
254
169
  add_tailwind_to_css_file,
255
170
  )
256
-
257
- def __repr__(self):
258
- """Return a string representation of the plugin.
259
-
260
- Returns:
261
- A string representation of the plugin.
262
- """
263
- return "TailwindV3Plugin()"