reflex 0.7.14a6__py3-none-any.whl → 0.8.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of reflex might be problematic. Click here for more details.
- reflex/.templates/jinja/app/rxconfig.py.jinja2 +4 -1
- reflex/.templates/jinja/web/package.json.jinja2 +1 -1
- reflex/.templates/jinja/web/pages/_app.js.jinja2 +16 -10
- reflex/.templates/jinja/web/pages/_document.js.jinja2 +1 -1
- reflex/.templates/jinja/web/pages/base_page.js.jinja2 +0 -1
- reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +4 -0
- reflex/.templates/jinja/web/utils/context.js.jinja2 +25 -8
- reflex/.templates/web/app/entry.client.js +8 -0
- reflex/.templates/web/app/routes.js +10 -0
- reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +12 -37
- reflex/.templates/web/postcss.config.js +1 -1
- reflex/.templates/web/react-router.config.js +6 -0
- reflex/.templates/web/utils/client_side_routing.js +21 -19
- reflex/.templates/web/utils/react-theme.js +92 -0
- reflex/.templates/web/utils/state.js +160 -67
- reflex/.templates/web/vite.config.js +32 -0
- reflex/__init__.py +1 -6
- reflex/__init__.pyi +0 -4
- reflex/app.py +53 -116
- reflex/base.py +1 -87
- reflex/compiler/compiler.py +41 -8
- reflex/compiler/templates.py +3 -3
- reflex/compiler/utils.py +73 -33
- reflex/components/__init__.py +0 -2
- reflex/components/__init__.pyi +0 -3
- reflex/components/base/__init__.py +1 -5
- reflex/components/base/__init__.pyi +4 -6
- reflex/components/base/app_wrap.pyi +5 -4
- reflex/components/base/body.pyi +5 -4
- reflex/components/base/document.py +18 -14
- reflex/components/base/document.pyi +83 -27
- reflex/components/base/error_boundary.pyi +5 -4
- reflex/components/base/fragment.pyi +5 -4
- reflex/components/base/link.pyi +9 -7
- reflex/components/base/meta.pyi +17 -13
- reflex/components/base/script.py +60 -58
- reflex/components/base/script.pyi +246 -31
- reflex/components/base/strict_mode.pyi +5 -4
- reflex/components/component.py +146 -217
- reflex/components/core/__init__.py +1 -0
- reflex/components/core/__init__.pyi +1 -0
- reflex/components/core/auto_scroll.pyi +5 -4
- reflex/components/core/banner.pyi +25 -19
- reflex/components/core/client_side_routing.py +7 -6
- reflex/components/core/client_side_routing.pyi +6 -56
- reflex/components/core/clipboard.pyi +5 -4
- reflex/components/core/debounce.py +1 -0
- reflex/components/core/debounce.pyi +5 -4
- reflex/components/core/foreach.py +3 -2
- reflex/components/core/helmet.py +14 -0
- reflex/components/{next/base.pyi → core/helmet.pyi} +10 -7
- reflex/components/core/html.pyi +5 -4
- reflex/components/core/sticky.pyi +17 -13
- reflex/components/core/upload.py +2 -1
- reflex/components/core/upload.pyi +21 -16
- reflex/components/datadisplay/code.py +2 -72
- reflex/components/datadisplay/code.pyi +9 -10
- reflex/components/datadisplay/dataeditor.pyi +11 -6
- reflex/components/datadisplay/shiki_code_block.pyi +13 -10
- reflex/components/dynamic.py +5 -5
- reflex/components/el/element.pyi +5 -4
- reflex/components/el/elements/base.pyi +5 -4
- reflex/components/el/elements/forms.pyi +69 -52
- reflex/components/el/elements/inline.pyi +113 -85
- reflex/components/el/elements/media.pyi +105 -79
- reflex/components/el/elements/metadata.pyi +25 -19
- reflex/components/el/elements/other.pyi +29 -22
- reflex/components/el/elements/scripts.pyi +13 -10
- reflex/components/el/elements/sectioning.pyi +61 -46
- reflex/components/el/elements/tables.pyi +41 -31
- reflex/components/el/elements/typography.pyi +61 -46
- reflex/components/field.py +175 -0
- reflex/components/gridjs/datatable.py +2 -2
- reflex/components/gridjs/datatable.pyi +11 -9
- reflex/components/lucide/icon.py +6 -2
- reflex/components/lucide/icon.pyi +15 -10
- reflex/components/markdown/markdown.pyi +5 -4
- reflex/components/moment/moment.pyi +5 -4
- reflex/components/plotly/plotly.pyi +19 -10
- reflex/components/props.py +376 -27
- reflex/components/radix/primitives/accordion.py +8 -1
- reflex/components/radix/primitives/accordion.pyi +29 -22
- reflex/components/radix/primitives/base.pyi +9 -7
- reflex/components/radix/primitives/drawer.pyi +45 -34
- reflex/components/radix/primitives/form.pyi +41 -31
- reflex/components/radix/primitives/progress.pyi +21 -16
- reflex/components/radix/primitives/slider.pyi +21 -16
- reflex/components/radix/themes/base.py +3 -3
- reflex/components/radix/themes/base.pyi +33 -25
- reflex/components/radix/themes/color_mode.pyi +13 -10
- reflex/components/radix/themes/components/alert_dialog.pyi +29 -22
- reflex/components/radix/themes/components/aspect_ratio.pyi +5 -4
- reflex/components/radix/themes/components/avatar.pyi +5 -4
- reflex/components/radix/themes/components/badge.pyi +5 -4
- reflex/components/radix/themes/components/button.pyi +5 -4
- reflex/components/radix/themes/components/callout.pyi +21 -16
- reflex/components/radix/themes/components/card.pyi +5 -4
- reflex/components/radix/themes/components/checkbox.pyi +13 -10
- reflex/components/radix/themes/components/checkbox_cards.pyi +9 -7
- reflex/components/radix/themes/components/checkbox_group.pyi +9 -7
- reflex/components/radix/themes/components/context_menu.pyi +53 -40
- reflex/components/radix/themes/components/data_list.pyi +17 -13
- reflex/components/radix/themes/components/dialog.pyi +29 -22
- reflex/components/radix/themes/components/dropdown_menu.pyi +33 -25
- reflex/components/radix/themes/components/hover_card.pyi +17 -13
- reflex/components/radix/themes/components/icon_button.pyi +5 -4
- reflex/components/radix/themes/components/inset.pyi +5 -4
- reflex/components/radix/themes/components/popover.pyi +17 -13
- reflex/components/radix/themes/components/progress.pyi +5 -4
- reflex/components/radix/themes/components/radio.pyi +5 -4
- reflex/components/radix/themes/components/radio_cards.pyi +9 -7
- reflex/components/radix/themes/components/radio_group.pyi +17 -13
- reflex/components/radix/themes/components/scroll_area.pyi +5 -4
- reflex/components/radix/themes/components/segmented_control.pyi +9 -7
- reflex/components/radix/themes/components/select.pyi +37 -28
- reflex/components/radix/themes/components/separator.pyi +5 -4
- reflex/components/radix/themes/components/skeleton.pyi +5 -4
- reflex/components/radix/themes/components/slider.pyi +5 -4
- reflex/components/radix/themes/components/spinner.pyi +5 -4
- reflex/components/radix/themes/components/switch.pyi +5 -4
- reflex/components/radix/themes/components/table.pyi +29 -22
- reflex/components/radix/themes/components/tabs.pyi +21 -16
- reflex/components/radix/themes/components/text_area.pyi +5 -4
- reflex/components/radix/themes/components/text_field.pyi +13 -10
- reflex/components/radix/themes/components/tooltip.pyi +5 -4
- reflex/components/radix/themes/layout/base.pyi +5 -4
- reflex/components/radix/themes/layout/box.pyi +5 -4
- reflex/components/radix/themes/layout/center.pyi +5 -4
- reflex/components/radix/themes/layout/container.pyi +5 -4
- reflex/components/radix/themes/layout/flex.pyi +5 -4
- reflex/components/radix/themes/layout/grid.pyi +5 -4
- reflex/components/radix/themes/layout/list.pyi +21 -16
- reflex/components/radix/themes/layout/section.pyi +5 -4
- reflex/components/radix/themes/layout/spacer.pyi +5 -4
- reflex/components/radix/themes/layout/stack.pyi +13 -10
- reflex/components/radix/themes/typography/blockquote.pyi +5 -4
- reflex/components/radix/themes/typography/code.pyi +5 -4
- reflex/components/radix/themes/typography/heading.pyi +5 -4
- reflex/components/radix/themes/typography/link.py +46 -11
- reflex/components/radix/themes/typography/link.pyi +311 -6
- reflex/components/radix/themes/typography/text.pyi +29 -22
- reflex/components/react_player/audio.pyi +5 -4
- reflex/components/react_player/react_player.pyi +5 -4
- reflex/components/react_player/video.pyi +5 -4
- reflex/components/recharts/cartesian.py +2 -1
- reflex/components/recharts/cartesian.pyi +65 -46
- reflex/components/recharts/charts.py +4 -2
- reflex/components/recharts/charts.pyi +36 -24
- reflex/components/recharts/general.pyi +24 -18
- reflex/components/recharts/polar.py +8 -4
- reflex/components/recharts/polar.pyi +16 -10
- reflex/components/recharts/recharts.pyi +9 -7
- reflex/components/sonner/toast.py +2 -2
- reflex/components/sonner/toast.pyi +10 -8
- reflex/config.py +3 -77
- reflex/constants/__init__.py +2 -2
- reflex/constants/base.py +28 -11
- reflex/constants/compiler.py +5 -3
- reflex/constants/event.py +1 -0
- reflex/constants/installer.py +22 -16
- reflex/constants/route.py +19 -7
- reflex/constants/state.py +2 -0
- reflex/custom_components/custom_components.py +0 -14
- reflex/environment.py +1 -1
- reflex/event.py +178 -81
- reflex/experimental/__init__.py +0 -30
- reflex/istate/proxy.py +5 -3
- reflex/page.py +0 -27
- reflex/plugins/__init__.py +3 -2
- reflex/plugins/base.py +5 -1
- reflex/plugins/shared_tailwind.py +158 -0
- reflex/plugins/sitemap.py +206 -0
- reflex/plugins/tailwind_v3.py +13 -106
- reflex/plugins/tailwind_v4.py +15 -108
- reflex/reflex.py +1 -0
- reflex/route.py +15 -21
- reflex/state.py +134 -140
- reflex/testing.py +58 -10
- reflex/utils/build.py +38 -82
- reflex/utils/exec.py +59 -161
- reflex/utils/export.py +2 -2
- reflex/utils/imports.py +0 -4
- reflex/utils/misc.py +28 -0
- reflex/utils/prerequisites.py +65 -62
- reflex/utils/processes.py +8 -7
- reflex/utils/pyi_generator.py +21 -9
- reflex/utils/serializers.py +14 -1
- reflex/utils/types.py +196 -61
- reflex/vars/__init__.py +2 -0
- reflex/vars/base.py +367 -134
- {reflex-0.7.14a6.dist-info → reflex-0.8.0a2.dist-info}/METADATA +12 -5
- {reflex-0.7.14a6.dist-info → reflex-0.8.0a2.dist-info}/RECORD +195 -202
- reflex/.templates/web/next.config.js +0 -7
- reflex/components/base/head.py +0 -20
- reflex/components/base/head.pyi +0 -116
- reflex/components/next/__init__.py +0 -10
- reflex/components/next/base.py +0 -7
- reflex/components/next/image.py +0 -117
- reflex/components/next/image.pyi +0 -94
- reflex/components/next/link.py +0 -20
- reflex/components/next/link.pyi +0 -67
- reflex/components/next/video.py +0 -38
- reflex/components/next/video.pyi +0 -68
- reflex/components/suneditor/__init__.py +0 -5
- reflex/components/suneditor/editor.py +0 -269
- reflex/components/suneditor/editor.pyi +0 -199
- reflex/experimental/layout.py +0 -254
- reflex/experimental/layout.pyi +0 -814
- {reflex-0.7.14a6.dist-info → reflex-0.8.0a2.dist-info}/WHEEL +0 -0
- {reflex-0.7.14a6.dist-info → reflex-0.8.0a2.dist-info}/entry_points.txt +0 -0
- {reflex-0.7.14a6.dist-info → reflex-0.8.0a2.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
|
-
)
|
reflex/plugins/__init__.py
CHANGED
|
@@ -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 .
|
|
7
|
-
from .
|
|
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)
|
reflex/plugins/tailwind_v3.py
CHANGED
|
@@ -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.
|
|
9
|
-
|
|
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 = ["./
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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()"
|