reflex 0.7.12__py3-none-any.whl → 0.7.13a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of reflex might be problematic. Click here for more details.
- reflex/.templates/jinja/app/rxconfig.py.jinja2 +1 -0
- reflex/.templates/web/postcss.config.js +0 -1
- reflex/.templates/web/utils/state.js +1 -1
- reflex/__init__.py +1 -0
- reflex/__init__.pyi +1 -0
- reflex/app.py +87 -27
- reflex/compiler/compiler.py +11 -62
- reflex/compiler/templates.py +12 -3
- reflex/compiler/utils.py +20 -4
- reflex/components/component.py +366 -88
- reflex/components/datadisplay/code.py +1 -1
- reflex/components/datadisplay/shiki_code_block.py +97 -86
- reflex/components/datadisplay/shiki_code_block.pyi +4 -2
- reflex/components/el/elements/forms.py +1 -1
- reflex/components/lucide/icon.py +2 -1
- reflex/components/lucide/icon.pyi +1 -0
- reflex/components/plotly/plotly.py +2 -2
- reflex/components/plotly/plotly.pyi +2 -3
- reflex/components/radix/themes/base.py +4 -11
- reflex/components/radix/themes/components/icon_button.py +2 -2
- reflex/components/radix/themes/components/text_field.py +3 -0
- reflex/components/radix/themes/components/text_field.pyi +2 -0
- reflex/components/radix/themes/layout/list.py +1 -1
- reflex/components/tags/iter_tag.py +3 -5
- reflex/config.py +54 -7
- reflex/constants/__init__.py +0 -2
- reflex/event.py +154 -93
- reflex/plugins/__init__.py +7 -0
- reflex/plugins/base.py +101 -0
- reflex/plugins/tailwind_v3.py +255 -0
- reflex/plugins/tailwind_v4.py +257 -0
- reflex/state.py +24 -3
- reflex/utils/build.py +1 -1
- reflex/utils/console.py +1 -1
- reflex/utils/exec.py +18 -0
- reflex/utils/path_ops.py +26 -6
- reflex/utils/prerequisites.py +21 -90
- reflex/utils/pyi_generator.py +12 -2
- reflex/utils/types.py +15 -1
- reflex/vars/base.py +59 -4
- reflex/vars/object.py +8 -0
- {reflex-0.7.12.dist-info → reflex-0.7.13a1.dist-info}/METADATA +2 -2
- {reflex-0.7.12.dist-info → reflex-0.7.13a1.dist-info}/RECORD +47 -46
- scripts/hatch_build.py +17 -0
- reflex/.templates/jinja/web/tailwind.config.js.jinja2 +0 -66
- reflex/.templates/web/styles/tailwind.css +0 -6
- reflex/constants/style.py +0 -16
- {reflex-0.7.12.dist-info → reflex-0.7.13a1.dist-info}/WHEEL +0 -0
- {reflex-0.7.12.dist-info → reflex-0.7.13a1.dist-info}/entry_points.txt +0 -0
- {reflex-0.7.12.dist-info → reflex-0.7.13a1.dist-info}/licenses/LICENSE +0 -0
reflex/event.py
CHANGED
|
@@ -1262,6 +1262,118 @@ def get_hydrate_event(state: BaseState) -> str:
|
|
|
1262
1262
|
return get_event(state, constants.CompileVars.HYDRATE)
|
|
1263
1263
|
|
|
1264
1264
|
|
|
1265
|
+
def _values_returned_from_event(
|
|
1266
|
+
event_spec: ArgsSpec | Sequence[ArgsSpec],
|
|
1267
|
+
) -> list[Any]:
|
|
1268
|
+
return [
|
|
1269
|
+
event_spec_return_type
|
|
1270
|
+
for arg_spec in (
|
|
1271
|
+
[event_spec] if not isinstance(event_spec, Sequence) else list(event_spec)
|
|
1272
|
+
)
|
|
1273
|
+
if (event_spec_return_type := get_type_hints(arg_spec).get("return", None))
|
|
1274
|
+
is not None
|
|
1275
|
+
and get_origin(event_spec_return_type) is tuple
|
|
1276
|
+
]
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def _check_event_args_subclass_of_callback(
|
|
1280
|
+
callback_params_names: list[str],
|
|
1281
|
+
provided_event_types: list[Any],
|
|
1282
|
+
callback_param_name_to_type: dict[str, Any],
|
|
1283
|
+
callback_name: str = "",
|
|
1284
|
+
key: str = "",
|
|
1285
|
+
):
|
|
1286
|
+
"""Check if the event handler arguments are subclass of the callback.
|
|
1287
|
+
|
|
1288
|
+
Args:
|
|
1289
|
+
callback_params_names: The names of the callback parameters.
|
|
1290
|
+
provided_event_types: The event types.
|
|
1291
|
+
callback_param_name_to_type: The callback parameter name to type mapping.
|
|
1292
|
+
callback_name: The name of the callback.
|
|
1293
|
+
key: The key.
|
|
1294
|
+
|
|
1295
|
+
Raises:
|
|
1296
|
+
TypeError: If the event handler arguments are invalid.
|
|
1297
|
+
EventHandlerArgTypeMismatchError: If the event handler arguments do not match the callback.
|
|
1298
|
+
|
|
1299
|
+
# noqa: DAR401 delayed_exceptions[]
|
|
1300
|
+
# noqa: DAR402 EventHandlerArgTypeMismatchError
|
|
1301
|
+
"""
|
|
1302
|
+
type_match_found: dict[str, bool] = {}
|
|
1303
|
+
delayed_exceptions: list[EventHandlerArgTypeMismatchError] = []
|
|
1304
|
+
|
|
1305
|
+
for event_spec_index, event_spec_return_type in enumerate(provided_event_types):
|
|
1306
|
+
args = get_args(event_spec_return_type)
|
|
1307
|
+
|
|
1308
|
+
args_types_without_vars = [
|
|
1309
|
+
arg if get_origin(arg) is not Var else get_args(arg)[0] for arg in args
|
|
1310
|
+
]
|
|
1311
|
+
|
|
1312
|
+
# check that args of event handler are matching the spec if type hints are provided
|
|
1313
|
+
for i, arg in enumerate(callback_params_names[: len(args_types_without_vars)]):
|
|
1314
|
+
if arg not in callback_param_name_to_type:
|
|
1315
|
+
continue
|
|
1316
|
+
|
|
1317
|
+
type_match_found.setdefault(arg, False)
|
|
1318
|
+
|
|
1319
|
+
try:
|
|
1320
|
+
compare_result = typehint_issubclass(
|
|
1321
|
+
args_types_without_vars[i], callback_param_name_to_type[arg]
|
|
1322
|
+
)
|
|
1323
|
+
except TypeError as te:
|
|
1324
|
+
callback_name_context = f" of {callback_name}" if callback_name else ""
|
|
1325
|
+
key_context = f" for {key}" if key else ""
|
|
1326
|
+
raise TypeError(
|
|
1327
|
+
f"Could not compare types {args_types_without_vars[i]} and {callback_param_name_to_type[arg]} for argument {arg}{callback_name_context}{key_context}."
|
|
1328
|
+
) from te
|
|
1329
|
+
|
|
1330
|
+
if compare_result:
|
|
1331
|
+
type_match_found[arg] = True
|
|
1332
|
+
continue
|
|
1333
|
+
else:
|
|
1334
|
+
type_match_found[arg] = False
|
|
1335
|
+
as_annotated_in = (
|
|
1336
|
+
f" as annotated in {callback_name}" if callback_name else ""
|
|
1337
|
+
)
|
|
1338
|
+
delayed_exceptions.append(
|
|
1339
|
+
EventHandlerArgTypeMismatchError(
|
|
1340
|
+
f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {callback_param_name_to_type[arg]}{as_annotated_in} instead."
|
|
1341
|
+
)
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
if all(type_match_found.values()):
|
|
1345
|
+
delayed_exceptions.clear()
|
|
1346
|
+
if event_spec_index:
|
|
1347
|
+
args = get_args(provided_event_types[0])
|
|
1348
|
+
|
|
1349
|
+
args_types_without_vars = [
|
|
1350
|
+
arg if get_origin(arg) is not Var else get_args(arg)[0]
|
|
1351
|
+
for arg in args
|
|
1352
|
+
]
|
|
1353
|
+
|
|
1354
|
+
expect_string = ", ".join(
|
|
1355
|
+
repr(arg) for arg in args_types_without_vars
|
|
1356
|
+
).replace("[", "\\[")
|
|
1357
|
+
|
|
1358
|
+
given_string = ", ".join(
|
|
1359
|
+
repr(callback_param_name_to_type.get(arg, Any))
|
|
1360
|
+
for arg in callback_params_names
|
|
1361
|
+
).replace("[", "\\[")
|
|
1362
|
+
|
|
1363
|
+
as_annotated_in = (
|
|
1364
|
+
f" as annotated in {callback_name}" if callback_name else ""
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
console.warn(
|
|
1368
|
+
f"Event handler {key} expects ({expect_string}) -> () but got ({given_string}) -> (){as_annotated_in} instead. "
|
|
1369
|
+
f"This may lead to unexpected behavior but is intentionally ignored for {key}."
|
|
1370
|
+
)
|
|
1371
|
+
break
|
|
1372
|
+
|
|
1373
|
+
if delayed_exceptions:
|
|
1374
|
+
raise delayed_exceptions[0]
|
|
1375
|
+
|
|
1376
|
+
|
|
1265
1377
|
def call_event_handler(
|
|
1266
1378
|
event_callback: EventHandler | EventSpec,
|
|
1267
1379
|
event_spec: ArgsSpec | Sequence[ArgsSpec],
|
|
@@ -1278,17 +1390,13 @@ def call_event_handler(
|
|
|
1278
1390
|
event_spec: The lambda that define the argument(s) to pass to the event handler.
|
|
1279
1391
|
key: The key to pass to the event handler.
|
|
1280
1392
|
|
|
1281
|
-
Raises:
|
|
1282
|
-
EventHandlerArgTypeMismatchError: If the event handler arguments do not match the event spec. #noqa: DAR402
|
|
1283
|
-
TypeError: If the event handler arguments are invalid.
|
|
1284
|
-
|
|
1285
1393
|
Returns:
|
|
1286
1394
|
The event spec from calling the event handler.
|
|
1287
|
-
|
|
1288
|
-
#noqa: DAR401
|
|
1289
1395
|
"""
|
|
1290
1396
|
event_spec_args = parse_args_spec(event_spec)
|
|
1291
1397
|
|
|
1398
|
+
event_spec_return_types = _values_returned_from_event(event_spec)
|
|
1399
|
+
|
|
1292
1400
|
if isinstance(event_callback, EventSpec):
|
|
1293
1401
|
check_fn_match_arg_spec(
|
|
1294
1402
|
event_callback.handler.fn,
|
|
@@ -1297,6 +1405,32 @@ def call_event_handler(
|
|
|
1297
1405
|
bool(event_callback.handler.state_full_name) + len(event_callback.args),
|
|
1298
1406
|
event_callback.handler.fn.__qualname__,
|
|
1299
1407
|
)
|
|
1408
|
+
|
|
1409
|
+
event_callback_spec_args = list(
|
|
1410
|
+
inspect.signature(event_callback.handler.fn).parameters.keys()
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
try:
|
|
1414
|
+
type_hints_of_provided_callback = get_type_hints(event_callback.handler.fn)
|
|
1415
|
+
except NameError:
|
|
1416
|
+
type_hints_of_provided_callback = {}
|
|
1417
|
+
|
|
1418
|
+
argument_names = [str(arg) for arg, value in event_callback.args]
|
|
1419
|
+
|
|
1420
|
+
_check_event_args_subclass_of_callback(
|
|
1421
|
+
[
|
|
1422
|
+
arg
|
|
1423
|
+
for arg in event_callback_spec_args[
|
|
1424
|
+
bool(event_callback.handler.state_full_name) :
|
|
1425
|
+
]
|
|
1426
|
+
if arg not in argument_names
|
|
1427
|
+
],
|
|
1428
|
+
event_spec_return_types,
|
|
1429
|
+
type_hints_of_provided_callback,
|
|
1430
|
+
event_callback.handler.fn.__qualname__,
|
|
1431
|
+
key or "",
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1300
1434
|
# Handle partial application of EventSpec args
|
|
1301
1435
|
return event_callback.add_args(*event_spec_args)
|
|
1302
1436
|
|
|
@@ -1308,98 +1442,23 @@ def call_event_handler(
|
|
|
1308
1442
|
event_callback.fn.__qualname__,
|
|
1309
1443
|
)
|
|
1310
1444
|
|
|
1311
|
-
all_acceptable_specs = (
|
|
1312
|
-
[event_spec] if not isinstance(event_spec, Sequence) else event_spec
|
|
1313
|
-
)
|
|
1314
|
-
|
|
1315
|
-
event_spec_return_types = list(
|
|
1316
|
-
filter(
|
|
1317
|
-
lambda event_spec_return_type: event_spec_return_type is not None
|
|
1318
|
-
and get_origin(event_spec_return_type) is tuple,
|
|
1319
|
-
(
|
|
1320
|
-
get_type_hints(arg_spec).get("return", None)
|
|
1321
|
-
for arg_spec in all_acceptable_specs
|
|
1322
|
-
),
|
|
1323
|
-
)
|
|
1324
|
-
)
|
|
1325
|
-
type_match_found: dict[str, bool] = {}
|
|
1326
|
-
delayed_exceptions: list[EventHandlerArgTypeMismatchError] = []
|
|
1327
|
-
|
|
1328
|
-
try:
|
|
1329
|
-
type_hints_of_provided_callback = get_type_hints(event_callback.fn)
|
|
1330
|
-
except NameError:
|
|
1331
|
-
type_hints_of_provided_callback = {}
|
|
1332
|
-
|
|
1333
1445
|
if event_spec_return_types:
|
|
1334
1446
|
event_callback_spec_args = list(
|
|
1335
1447
|
inspect.signature(event_callback.fn).parameters.keys()
|
|
1336
1448
|
)
|
|
1337
1449
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
):
|
|
1351
|
-
if arg not in type_hints_of_provided_callback:
|
|
1352
|
-
continue
|
|
1353
|
-
|
|
1354
|
-
type_match_found.setdefault(arg, False)
|
|
1355
|
-
|
|
1356
|
-
try:
|
|
1357
|
-
compare_result = typehint_issubclass(
|
|
1358
|
-
args_types_without_vars[i], type_hints_of_provided_callback[arg]
|
|
1359
|
-
)
|
|
1360
|
-
except TypeError as te:
|
|
1361
|
-
raise TypeError(
|
|
1362
|
-
f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_callback.fn.__qualname__} provided for {key}."
|
|
1363
|
-
) from te
|
|
1364
|
-
|
|
1365
|
-
if compare_result:
|
|
1366
|
-
type_match_found[arg] = True
|
|
1367
|
-
continue
|
|
1368
|
-
else:
|
|
1369
|
-
type_match_found[arg] = False
|
|
1370
|
-
delayed_exceptions.append(
|
|
1371
|
-
EventHandlerArgTypeMismatchError(
|
|
1372
|
-
f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {type_hints_of_provided_callback[arg]} as annotated in {event_callback.fn.__qualname__} instead."
|
|
1373
|
-
)
|
|
1374
|
-
)
|
|
1375
|
-
|
|
1376
|
-
if all(type_match_found.values()):
|
|
1377
|
-
delayed_exceptions.clear()
|
|
1378
|
-
if event_spec_index:
|
|
1379
|
-
args = get_args(event_spec_return_types[0])
|
|
1380
|
-
|
|
1381
|
-
args_types_without_vars = [
|
|
1382
|
-
arg if get_origin(arg) is not Var else get_args(arg)[0]
|
|
1383
|
-
for arg in args
|
|
1384
|
-
]
|
|
1385
|
-
|
|
1386
|
-
expect_string = ", ".join(
|
|
1387
|
-
repr(arg) for arg in args_types_without_vars
|
|
1388
|
-
).replace("[", "\\[")
|
|
1389
|
-
|
|
1390
|
-
given_string = ", ".join(
|
|
1391
|
-
repr(type_hints_of_provided_callback.get(arg, Any))
|
|
1392
|
-
for arg in event_callback_spec_args[1:]
|
|
1393
|
-
).replace("[", "\\[")
|
|
1394
|
-
|
|
1395
|
-
console.warn(
|
|
1396
|
-
f"Event handler {key} expects ({expect_string}) -> () but got ({given_string}) -> () as annotated in {event_callback.fn.__qualname__} instead. "
|
|
1397
|
-
f"This may lead to unexpected behavior but is intentionally ignored for {key}."
|
|
1398
|
-
)
|
|
1399
|
-
break
|
|
1400
|
-
|
|
1401
|
-
if delayed_exceptions:
|
|
1402
|
-
raise delayed_exceptions[0]
|
|
1450
|
+
try:
|
|
1451
|
+
type_hints_of_provided_callback = get_type_hints(event_callback.fn)
|
|
1452
|
+
except NameError:
|
|
1453
|
+
type_hints_of_provided_callback = {}
|
|
1454
|
+
|
|
1455
|
+
_check_event_args_subclass_of_callback(
|
|
1456
|
+
event_callback_spec_args[1:],
|
|
1457
|
+
event_spec_return_types,
|
|
1458
|
+
type_hints_of_provided_callback,
|
|
1459
|
+
event_callback.fn.__qualname__,
|
|
1460
|
+
key or "",
|
|
1461
|
+
)
|
|
1403
1462
|
|
|
1404
1463
|
return event_callback(*event_spec_args)
|
|
1405
1464
|
|
|
@@ -1958,6 +2017,8 @@ class EventCallback(Generic[Unpack[P]], EventActionsMixin):
|
|
|
1958
2017
|
class LambdaEventCallback(Protocol[Unpack[P]]):
|
|
1959
2018
|
"""A protocol for a lambda event callback."""
|
|
1960
2019
|
|
|
2020
|
+
__code__: types.CodeType
|
|
2021
|
+
|
|
1961
2022
|
@overload
|
|
1962
2023
|
def __call__(self: LambdaEventCallback[()]) -> Any: ...
|
|
1963
2024
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Reflex Plugin System."""
|
|
2
|
+
|
|
3
|
+
from .base import CommonContext as CommonContext
|
|
4
|
+
from .base import Plugin as Plugin
|
|
5
|
+
from .base import PreCompileContext as PreCompileContext
|
|
6
|
+
from .tailwind_v3 import Plugin as TailwindV3Plugin
|
|
7
|
+
from .tailwind_v4 import Plugin as TailwindV4Plugin
|
reflex/plugins/base.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Base class for all plugins."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import ParamSpec, Protocol, TypedDict
|
|
6
|
+
|
|
7
|
+
from typing_extensions import Unpack
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CommonContext(TypedDict):
|
|
11
|
+
"""Common context for all plugins."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
P = ParamSpec("P")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AddTaskProtcol(Protocol):
|
|
18
|
+
"""Protocol for adding a task to the pre-compile context."""
|
|
19
|
+
|
|
20
|
+
def __call__(
|
|
21
|
+
self,
|
|
22
|
+
task: Callable[P, list[tuple[str, str]] | tuple[str, str] | None],
|
|
23
|
+
/,
|
|
24
|
+
*args: P.args,
|
|
25
|
+
**kwargs: P.kwargs,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Add a task to the pre-compile context.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
task: The task to add.
|
|
31
|
+
args: The arguments to pass to the task
|
|
32
|
+
kwargs: The keyword arguments to pass to the task
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PreCompileContext(CommonContext):
|
|
37
|
+
"""Context for pre-compile hooks."""
|
|
38
|
+
|
|
39
|
+
add_save_task: AddTaskProtcol
|
|
40
|
+
add_modify_task: Callable[[str, Callable[[str], str]], None]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Plugin:
|
|
44
|
+
"""Base class for all plugins."""
|
|
45
|
+
|
|
46
|
+
def get_frontend_development_dependencies(
|
|
47
|
+
self, **context: Unpack[CommonContext]
|
|
48
|
+
) -> list[str] | set[str] | tuple[str, ...]:
|
|
49
|
+
"""Get the NPM packages required by the plugin for development.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
context: The context for the plugin.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A list of packages required by the plugin for development.
|
|
56
|
+
"""
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
def get_frontend_dependencies(
|
|
60
|
+
self, **context: Unpack[CommonContext]
|
|
61
|
+
) -> list[str] | set[str] | tuple[str, ...]:
|
|
62
|
+
"""Get the NPM packages required by the plugin.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
context: The context for the plugin.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
A list of packages required by the plugin.
|
|
69
|
+
"""
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
def get_static_assets(
|
|
73
|
+
self, **context: Unpack[CommonContext]
|
|
74
|
+
) -> Sequence[tuple[Path, str | bytes]]:
|
|
75
|
+
"""Get the static assets required by the plugin.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
context: The context for the plugin.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
A list of static assets required by the plugin.
|
|
82
|
+
"""
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
def get_stylesheet_paths(self, **context: Unpack[CommonContext]) -> Sequence[str]:
|
|
86
|
+
"""Get the paths to the stylesheets required by the plugin relative to the styles directory.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
context: The context for the plugin.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A list of paths to the stylesheets required by the plugin.
|
|
93
|
+
"""
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
def pre_compile(self, **context: Unpack[PreCompileContext]) -> None:
|
|
97
|
+
"""Called before the compilation of the plugin.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
context: The context for the plugin.
|
|
101
|
+
"""
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Base class for all plugins."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
|
|
6
|
+
from reflex.constants.base import Dirs
|
|
7
|
+
from reflex.constants.compiler import Ext, PageNames
|
|
8
|
+
from reflex.plugins.base import Plugin as PluginBase
|
|
9
|
+
from reflex.utils.decorator import once
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Constants(SimpleNamespace):
|
|
13
|
+
"""Tailwind constants."""
|
|
14
|
+
|
|
15
|
+
# The Tailwindcss version
|
|
16
|
+
VERSION = "tailwindcss@3.4.17"
|
|
17
|
+
# The Tailwind config.
|
|
18
|
+
CONFIG = "tailwind.config.js"
|
|
19
|
+
# Default Tailwind content paths
|
|
20
|
+
CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"]
|
|
21
|
+
# Relative tailwind style path to root stylesheet in Dirs.STYLES.
|
|
22
|
+
ROOT_STYLE_PATH = "./tailwind.css"
|
|
23
|
+
|
|
24
|
+
# Content of the style content.
|
|
25
|
+
ROOT_STYLE_CONTENT = """
|
|
26
|
+
@import "tailwindcss/base";
|
|
27
|
+
|
|
28
|
+
@import url('{radix_url}');
|
|
29
|
+
|
|
30
|
+
@tailwind components;
|
|
31
|
+
@tailwind utilities;
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# The default tailwind css.
|
|
35
|
+
TAILWIND_CSS = "@import url('./tailwind.css');"
|
|
36
|
+
|
|
37
|
+
|
|
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
|
+
):
|
|
122
|
+
"""Compile the Tailwind config.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
config: The Tailwind config.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The compiled Tailwind config.
|
|
129
|
+
"""
|
|
130
|
+
return Constants.CONFIG, tailwind_config_js_template().render(
|
|
131
|
+
**config,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def compile_root_style():
|
|
136
|
+
"""Compile the Tailwind root style.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The compiled Tailwind root style.
|
|
140
|
+
"""
|
|
141
|
+
from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
|
|
142
|
+
|
|
143
|
+
return str(
|
|
144
|
+
Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH
|
|
145
|
+
), Constants.ROOT_STYLE_CONTENT.format(
|
|
146
|
+
radix_url=RADIX_THEMES_STYLESHEET,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _index_of_element_that_has(haystack: list[str], needle: str) -> int | None:
|
|
151
|
+
return next(
|
|
152
|
+
(i for i, line in enumerate(haystack) if needle in line),
|
|
153
|
+
None,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def add_tailwind_to_postcss_config(postcss_file_content: str) -> str:
|
|
158
|
+
"""Add tailwind to the postcss config.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
postcss_file_content: The content of the postcss config file.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The modified postcss config file content.
|
|
165
|
+
"""
|
|
166
|
+
from reflex.constants import Dirs
|
|
167
|
+
|
|
168
|
+
postcss_file_lines = postcss_file_content.splitlines()
|
|
169
|
+
|
|
170
|
+
if _index_of_element_that_has(postcss_file_lines, "tailwindcss") is not None:
|
|
171
|
+
return postcss_file_content
|
|
172
|
+
|
|
173
|
+
line_with_postcss_plugins = _index_of_element_that_has(
|
|
174
|
+
postcss_file_lines, "plugins"
|
|
175
|
+
)
|
|
176
|
+
if not line_with_postcss_plugins:
|
|
177
|
+
print( # noqa: T201
|
|
178
|
+
f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. "
|
|
179
|
+
"Please make sure the file exists and is valid."
|
|
180
|
+
)
|
|
181
|
+
return postcss_file_content
|
|
182
|
+
|
|
183
|
+
postcss_import_line = _index_of_element_that_has(
|
|
184
|
+
postcss_file_lines, '"postcss-import"'
|
|
185
|
+
)
|
|
186
|
+
postcss_file_lines.insert(
|
|
187
|
+
(postcss_import_line or line_with_postcss_plugins) + 1, "tailwindcss: {},"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return "\n".join(postcss_file_lines)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def add_tailwind_to_css_file(css_file_content: str) -> str:
|
|
194
|
+
"""Add tailwind to the css file.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
css_file_content: The content of the css file.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The modified css file content.
|
|
201
|
+
"""
|
|
202
|
+
from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
|
|
203
|
+
|
|
204
|
+
if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content:
|
|
205
|
+
return css_file_content
|
|
206
|
+
if RADIX_THEMES_STYLESHEET not in css_file_content:
|
|
207
|
+
print( # noqa: T201
|
|
208
|
+
f"Could not find line with '{RADIX_THEMES_STYLESHEET}' in {Dirs.STYLES}. "
|
|
209
|
+
"Please make sure the file exists and is valid."
|
|
210
|
+
)
|
|
211
|
+
return css_file_content
|
|
212
|
+
return css_file_content.replace(
|
|
213
|
+
f"@import url('{RADIX_THEMES_STYLESHEET}');",
|
|
214
|
+
Constants.TAILWIND_CSS,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Plugin(PluginBase):
|
|
219
|
+
"""Plugin for Tailwind CSS."""
|
|
220
|
+
|
|
221
|
+
def get_frontend_development_dependencies(self, **context) -> list[str]:
|
|
222
|
+
"""Get the packages required by the plugin.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
**context: The context for the plugin.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
A list of packages required by the plugin.
|
|
229
|
+
"""
|
|
230
|
+
from reflex.config import get_config
|
|
231
|
+
|
|
232
|
+
config = get_config()
|
|
233
|
+
return [
|
|
234
|
+
plugin if isinstance(plugin, str) else plugin.get("name")
|
|
235
|
+
for plugin in (config.tailwind or {}).get("plugins", [])
|
|
236
|
+
] + [Constants.VERSION]
|
|
237
|
+
|
|
238
|
+
def pre_compile(self, **context):
|
|
239
|
+
"""Pre-compile the plugin.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
context: The context for the plugin.
|
|
243
|
+
"""
|
|
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)
|
|
250
|
+
context["add_save_task"](compile_root_style)
|
|
251
|
+
context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config)
|
|
252
|
+
context["add_modify_task"](
|
|
253
|
+
str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)),
|
|
254
|
+
add_tailwind_to_css_file,
|
|
255
|
+
)
|