reflex 0.7.14a6__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 (237) 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 +86 -135
  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.14a6.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/experimental/layout.pyi +0 -814
  234. reflex-0.7.14a6.dist-info/RECORD +0 -408
  235. {reflex-0.7.14a6.dist-info → reflex-0.8.0.dist-info}/WHEEL +0 -0
  236. {reflex-0.7.14a6.dist-info → reflex-0.8.0.dist-info}/entry_points.txt +0 -0
  237. {reflex-0.7.14a6.dist-info → reflex-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,13 +150,10 @@ 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
- plugin if isinstance(plugin, str) else plugin.get("name")
235
- for plugin in (config.tailwind or {}).get("plugins", [])
236
- ] + [Constants.VERSION]
154
+ *super().get_frontend_development_dependencies(**context),
155
+ Constants.VERSION,
156
+ ]
237
157
 
238
158
  def pre_compile(self, **context):
239
159
  """Pre-compile the plugin.
@@ -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_unversioned_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()"
@@ -1,123 +1,44 @@
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):
13
17
  """Tailwind constants."""
14
18
 
15
19
  # The Tailwindcss version
16
- VERSION = "tailwindcss@4.1.7"
20
+ VERSION = "tailwindcss@4.1.10"
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
 
24
28
  # Content of the style content.
25
29
  ROOT_STYLE_CONTENT = """@layer theme, base, components, utilities;
26
- @config "../tailwind.config.js";
27
30
  @import "tailwindcss/theme.css" layer(theme);
28
31
  @import "tailwindcss/preflight.css" layer(base);
29
32
  @import "{radix_url}" layer(components);
30
33
  @import "tailwindcss/utilities.css" layer(utilities);
34
+ @config "../tailwind.config.js";
31
35
  """
32
36
 
33
37
  # The default tailwind css.
34
38
  TAILWIND_CSS = "@import url('./tailwind.css');"
35
39
 
36
40
 
37
- @once
38
- def tailwind_config_js_template():
39
- """Get the Tailwind config template.
40
-
41
- Returns:
42
- The Tailwind config template.
43
- """
44
- from reflex.compiler.templates import from_string
45
-
46
- source = r"""
47
- {# Helper macro to render JS objects and arrays #}
48
- {% macro render_js(val, indent=2, level=0) -%}
49
- {%- set space = ' ' * (indent * level) -%}
50
- {%- set next_space = ' ' * (indent * (level + 1)) -%}
51
-
52
- {%- if val is mapping -%}
53
- {
54
- {%- for k, v in val.items() %}
55
- {{ next_space }}{{ k if k is string and k.isidentifier() else k|tojson }}: {{ render_js(v, indent, level + 1) }}{{ "," if not loop.last }}
56
- {%- endfor %}
57
- {{ space }}}
58
- {%- elif val is iterable and val is not string -%}
59
- [
60
- {%- for item in val %}
61
- {{ next_space }}{{ render_js(item, indent, level + 1) }}{{ "," if not loop.last }}
62
- {%- endfor %}
63
- {{ space }}]
64
- {%- else -%}
65
- {{ val | tojson }}
66
- {%- endif -%}
67
- {%- endmacro %}
68
-
69
- {# Extract destructured imports from plugin dicts only #}
70
- {%- set imports = [] %}
71
- {%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
72
- {%- set _ = imports.append(plugin.import) %}
73
- {%- endfor %}
74
-
75
- /** @type {import('tailwindcss').Config} */
76
- {%- for imp in imports %}
77
- const { {{ imp.name }} } = require({{ imp.from | tojson }});
78
- {%- endfor %}
79
-
80
- module.exports = {
81
- content: {{ render_js(content) }},
82
- theme: {{ render_js(theme) }},
83
- {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
84
- {% if corePlugins is defined %}corePlugins: {{ render_js(corePlugins) }},{% endif %}
85
- {% if important is defined %}important: {{ important | tojson }},{% endif %}
86
- {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
87
- {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
88
- {% if presets is defined %}
89
- presets: [
90
- {% for preset in presets %}
91
- require({{ preset | tojson }}){{ "," if not loop.last }}
92
- {% endfor %}
93
- ],
94
- {% endif %}
95
- plugins: [
96
- {% for plugin in plugins %}
97
- {% if plugin is mapping %}
98
- {% if plugin.call is defined %}
99
- {{ plugin.call }}(
100
- {%- if plugin.args is defined -%}
101
- {{ render_js(plugin.args) }}
102
- {%- endif -%}
103
- ){{ "," if not loop.last }}
104
- {% else %}
105
- require({{ plugin.name | tojson }}){{ "," if not loop.last }}
106
- {% endif %}
107
- {% else %}
108
- require({{ plugin | tojson }}){{ "," if not loop.last }}
109
- {% endif %}
110
- {% endfor %}
111
- ]
112
- };
113
- """
114
-
115
- return from_string(source)
116
-
117
-
118
- def compile_config(
119
- config: dict,
120
- ):
41
+ def compile_config(config: TailwindConfig):
121
42
  """Compile the Tailwind config.
122
43
 
123
44
  Args:
@@ -128,6 +49,7 @@ def compile_config(
128
49
  """
129
50
  return Constants.CONFIG, tailwind_config_js_template().render(
130
51
  **config,
52
+ DEFAULT_CONTENT=Constants.CONTENT,
131
53
  )
132
54
 
133
55
 
@@ -218,7 +140,8 @@ def add_tailwind_to_css_file(css_file_content: str) -> str:
218
140
  )
219
141
 
220
142
 
221
- class Plugin(PluginBase):
143
+ @dataclasses.dataclass
144
+ class TailwindV4Plugin(TailwindPlugin):
222
145
  """Plugin for Tailwind CSS."""
223
146
 
224
147
  def get_frontend_development_dependencies(self, **context) -> list[str]:
@@ -230,13 +153,11 @@ class Plugin(PluginBase):
230
153
  Returns:
231
154
  A list of packages required by the plugin.
232
155
  """
233
- from reflex.config import get_config
234
-
235
- config = get_config()
236
156
  return [
237
- plugin if isinstance(plugin, str) else plugin.get("name")
238
- for plugin in (config.tailwind or {}).get("plugins", [])
239
- ] + [Constants.VERSION, "@tailwindcss/postcss@4.1.7"]
157
+ *super().get_frontend_development_dependencies(**context),
158
+ Constants.VERSION,
159
+ "@tailwindcss/postcss@4.1.10",
160
+ ]
240
161
 
241
162
  def pre_compile(self, **context):
242
163
  """Pre-compile the plugin.
@@ -244,23 +165,10 @@ class Plugin(PluginBase):
244
165
  Args:
245
166
  context: The context for the plugin.
246
167
  """
247
- from reflex.config import get_config
248
-
249
- config = get_config().tailwind or {}
250
-
251
- config["content"] = config.get("content", Constants.CONTENT)
252
- context["add_save_task"](compile_config, config)
168
+ context["add_save_task"](compile_config, self.get_unversioned_config())
253
169
  context["add_save_task"](compile_root_style)
254
170
  context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config)
255
171
  context["add_modify_task"](
256
172
  str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)),
257
173
  add_tailwind_to_css_file,
258
174
  )
259
-
260
- def __repr__(self):
261
- """Return a string representation of the plugin.
262
-
263
- Returns:
264
- A string representation of the plugin.
265
- """
266
- return "TailwindV4Plugin()"
reflex/reflex.py CHANGED
@@ -204,6 +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
208
  }
208
209
 
209
210
  # Granian fails if the app is already imported.
reflex/route.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
+ from collections.abc import Callable
6
7
 
7
8
  from reflex import constants
8
9
 
@@ -16,15 +17,40 @@ def verify_route_validity(route: str) -> None:
16
17
  Raises:
17
18
  ValueError: If the route is invalid.
18
19
  """
19
- pattern = catchall_in_route(route)
20
- if pattern and not route.endswith(pattern):
21
- msg = f"Catch-all must be the last part of the URL: {route}"
22
- raise ValueError(msg)
23
- if route == "api" or route.startswith("api/"):
24
- msg = (
25
- f"Cannot have a route prefixed with 'api/': {route} (conflicts with NextJS)"
26
- )
27
- raise ValueError(msg)
20
+ route_parts = route.removeprefix("/").split("/")
21
+ for i, part in enumerate(route_parts):
22
+ if constants.RouteRegex.SLUG.fullmatch(part):
23
+ continue
24
+ if not part.startswith("[") or not part.endswith("]"):
25
+ msg = (
26
+ f"Route part `{part}` is not valid. Reflex only supports "
27
+ "alphabetic characters, underscores, and hyphens in route parts. "
28
+ )
29
+ raise ValueError(msg)
30
+ if part.startswith(("[[...", "[...")):
31
+ if part != constants.RouteRegex.SPLAT_CATCHALL:
32
+ msg = f"Catchall pattern `{part}` is not valid. Only `{constants.RouteRegex.SPLAT_CATCHALL}` is allowed."
33
+ raise ValueError(msg)
34
+ if i != len(route_parts) - 1:
35
+ msg = f"Catchall pattern `{part}` must be at the end of the route."
36
+ raise ValueError(msg)
37
+ continue
38
+ if part.startswith("[["):
39
+ if constants.RouteRegex.OPTIONAL_ARG.fullmatch(part):
40
+ continue
41
+ msg = (
42
+ f"Route part `{part}` with optional argument is not valid. "
43
+ "Reflex only supports optional arguments that start with an alphabetic character or underscore, "
44
+ "followed by alphanumeric characters or underscores."
45
+ )
46
+ raise ValueError(msg)
47
+ if not constants.RouteRegex.ARG.fullmatch(part):
48
+ msg = (
49
+ f"Route part `{part}` with argument is not valid. "
50
+ "Reflex only supports argument names that start with an alphabetic character or underscore, "
51
+ "followed by alphanumeric characters or underscores."
52
+ )
53
+ raise ValueError(msg)
28
54
 
29
55
 
30
56
  def get_route_args(route: str) -> dict[str, str]:
@@ -38,103 +64,159 @@ def get_route_args(route: str) -> dict[str, str]:
38
64
  """
39
65
  args = {}
40
66
 
41
- def add_route_arg(match: re.Match[str], type_: str):
42
- """Add arg from regex search result.
43
-
44
- Args:
45
- match: Result of a regex search
46
- type_: The assigned type for this arg
47
-
48
- Raises:
49
- ValueError: If the route is invalid.
50
- """
51
- arg_name = match.groups()[0]
67
+ def _add_route_arg(arg_name: str, type_: str):
52
68
  if arg_name in args:
53
- msg = f"Arg name [{arg_name}] is used more than once in this URL"
69
+ msg = (
70
+ f"Arg name `{arg_name}` is used more than once in the route `{route}`."
71
+ )
54
72
  raise ValueError(msg)
55
73
  args[arg_name] = type_
56
74
 
57
75
  # Regex to check for route args.
58
- check = constants.RouteRegex.ARG
59
- check_strict_catchall = constants.RouteRegex.STRICT_CATCHALL
60
- check_opt_catchall = constants.RouteRegex.OPT_CATCHALL
76
+ argument_regex = constants.RouteRegex.ARG
77
+ optional_argument_regex = constants.RouteRegex.OPTIONAL_ARG
61
78
 
62
79
  # Iterate over the route parts and check for route args.
63
80
  for part in route.split("/"):
64
- match_opt = check_opt_catchall.match(part)
65
- if match_opt:
66
- add_route_arg(match_opt, constants.RouteArgType.LIST)
81
+ if part == constants.RouteRegex.SPLAT_CATCHALL:
82
+ _add_route_arg("splat", constants.RouteArgType.LIST)
67
83
  break
68
84
 
69
- match_strict = check_strict_catchall.match(part)
70
- if match_strict:
71
- add_route_arg(match_strict, constants.RouteArgType.LIST)
72
- break
85
+ optional_argument = optional_argument_regex.match(part)
86
+ if optional_argument:
87
+ _add_route_arg(optional_argument.group(1), constants.RouteArgType.SINGLE)
88
+ continue
89
+
90
+ argument = argument_regex.match(part)
91
+ if argument:
92
+ _add_route_arg(argument.group(1), constants.RouteArgType.SINGLE)
93
+ continue
73
94
 
74
- match = check.match(part)
75
- if match:
76
- # Add the route arg to the list.
77
- add_route_arg(match, constants.RouteArgType.SINGLE)
78
95
  return args
79
96
 
80
97
 
81
- def catchall_in_route(route: str) -> str:
82
- """Extract the catchall part from a route.
98
+ def replace_brackets_with_keywords(input_string: str) -> str:
99
+ """Replace brackets and everything inside it in a string with a keyword.
100
+
101
+ Example:
102
+ >>> replace_brackets_with_keywords("/posts")
103
+ '/posts'
104
+ >>> replace_brackets_with_keywords("/posts/[slug]")
105
+ '/posts/__SINGLE_SEGMENT__'
106
+ >>> replace_brackets_with_keywords("/posts/[slug]/comments")
107
+ '/posts/__SINGLE_SEGMENT__/comments'
108
+ >>> replace_brackets_with_keywords("/posts/[[slug]]")
109
+ '/posts/__DOUBLE_SEGMENT__'
110
+ >>> replace_brackets_with_keywords("/posts/[[...splat]]")
111
+ '/posts/__DOUBLE_CATCHALL_SEGMENT__'
83
112
 
84
113
  Args:
85
- route: the route from which to extract
114
+ input_string: String to replace.
86
115
 
87
116
  Returns:
88
- str: the catchall part of the URI
117
+ new string containing keywords.
89
118
  """
90
- match_ = constants.RouteRegex.CATCHALL.search(route)
91
- return match_.group() if match_ else ""
119
+ # Replace [<slug>] with __SINGLE_SEGMENT__
120
+ return constants.RouteRegex.ARG.sub(
121
+ constants.RouteRegex.SINGLE_SEGMENT,
122
+ # Replace [[slug]] with __DOUBLE_SEGMENT__
123
+ constants.RouteRegex.OPTIONAL_ARG.sub(
124
+ constants.RouteRegex.DOUBLE_SEGMENT,
125
+ # Replace [[...splat]] with __DOUBLE_CATCHALL_SEGMENT__
126
+ input_string.replace(
127
+ constants.RouteRegex.SPLAT_CATCHALL,
128
+ constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
129
+ ),
130
+ ),
131
+ )
92
132
 
93
133
 
94
- def catchall_prefix(route: str) -> str:
95
- """Extract the prefix part from a route that contains a catchall.
134
+ def route_specifity(keyworded_route: str) -> tuple[int, int, int]:
135
+ """Get the specificity of a route with keywords.
136
+
137
+ The smaller the number, the more specific the route is.
96
138
 
97
139
  Args:
98
- route: the route from which to extract
140
+ keyworded_route: The route with keywords.
99
141
 
100
142
  Returns:
101
- str: the prefix part of the URI
143
+ A tuple containing the counts of double catchall segments,
144
+ double segments, and single segments in the route.
102
145
  """
103
- pattern = catchall_in_route(route)
104
- return route.replace(pattern, "") if pattern else ""
146
+ return (
147
+ keyworded_route.count(constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT),
148
+ keyworded_route.count(constants.RouteRegex.DOUBLE_SEGMENT),
149
+ keyworded_route.count(constants.RouteRegex.SINGLE_SEGMENT),
150
+ )
105
151
 
106
152
 
107
- def replace_brackets_with_keywords(input_string: str) -> str:
108
- """Replace brackets and everything inside it in a string with a keyword.
153
+ def get_route_regex(keyworded_route: str) -> re.Pattern:
154
+ """Get the regex pattern for a route with keywords.
109
155
 
110
156
  Args:
111
- input_string: String to replace.
157
+ keyworded_route: The route with keywords.
112
158
 
113
159
  Returns:
114
- new string containing keywords.
160
+ A compiled regex pattern for the route.
115
161
  """
116
- # /posts -> /post
117
- # /posts/[slug] -> /posts/__SINGLE_SEGMENT__
118
- # /posts/[slug]/comments -> /posts/__SINGLE_SEGMENT__/comments
119
- # /posts/[[slug]] -> /posts/__DOUBLE_SEGMENT__
120
- # / posts/[[...slug2]]-> /posts/__DOUBLE_CATCHALL_SEGMENT__
121
- # /posts/[...slug3]-> /posts/__SINGLE_CATCHALL_SEGMENT__
122
-
123
- # Replace [[...<slug>]] with __DOUBLE_CATCHALL_SEGMENT__
124
- output_string = re.sub(
125
- r"\[\[\.\.\..+?\]\]",
126
- constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
127
- input_string,
128
- )
129
- # Replace [...<slug>] with __SINGLE_CATCHALL_SEGMENT__
130
- output_string = re.sub(
131
- r"\[\.\.\..+?\]",
132
- constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
133
- output_string,
134
- )
135
- # Replace [[<slug>]] with __DOUBLE_SEGMENT__
136
- output_string = re.sub(
137
- r"\[\[.+?\]\]", constants.RouteRegex.DOUBLE_SEGMENT, output_string
162
+ if keyworded_route == "index":
163
+ return re.compile(re.escape("/"))
164
+ path_parts = keyworded_route.split("/")
165
+ regex_parts = []
166
+ for part in path_parts:
167
+ if part == constants.RouteRegex.SINGLE_SEGMENT:
168
+ # Match a single segment (/slug)
169
+ regex_parts.append(r"/[^/]*")
170
+ elif part == constants.RouteRegex.DOUBLE_SEGMENT:
171
+ # Match a single optional segment (/slug or nothing)
172
+ regex_parts.append(r"(/[^/]+)?")
173
+ elif part == constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT:
174
+ regex_parts.append(".*")
175
+ else:
176
+ regex_parts.append(re.escape("/" + part))
177
+ # Join the regex parts and compile the regex
178
+ regex_pattern = "".join(regex_parts)
179
+ regex_pattern = f"^{regex_pattern}/?$"
180
+ return re.compile(regex_pattern)
181
+
182
+
183
+ def get_router(routes: list[str]) -> Callable[[str], str | None]:
184
+ """Get a function that computes the route for a given path.
185
+
186
+ Args:
187
+ routes: A list of routes to match against.
188
+
189
+ Returns:
190
+ A function that takes a path and returns the first matching route,
191
+ or None if no match is found.
192
+ """
193
+ keyworded_routes = {
194
+ replace_brackets_with_keywords(route): route for route in routes
195
+ }
196
+ sorted_routes_by_specifity = sorted(
197
+ keyworded_routes.items(),
198
+ key=lambda item: route_specifity(item[0]),
138
199
  )
139
- # Replace [<slug>] with __SINGLE_SEGMENT__
140
- return re.sub(r"\[.+?\]", constants.RouteRegex.SINGLE_SEGMENT, output_string)
200
+ regexed_routes = [
201
+ (get_route_regex(keyworded_route), original_route)
202
+ for keyworded_route, original_route in sorted_routes_by_specifity
203
+ ]
204
+
205
+ def get_route(path: str) -> str | None:
206
+ """Get the first matching route for a given path.
207
+
208
+ Args:
209
+ path: The path to match against the routes.
210
+
211
+ Returns:
212
+ The first matching route, or None if no match is found.
213
+ """
214
+ path = "/" + path.removeprefix("/").removesuffix("/")
215
+ if path == "/index":
216
+ path = "/"
217
+ for regex, original_route in regexed_routes:
218
+ if regex.fullmatch(path):
219
+ return original_route
220
+ return None
221
+
222
+ return get_route