From 1119e3415c9f1437d1616c75726d4d2a758e3905 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza Date: Tue, 26 May 2026 00:47:05 +0500 Subject: [PATCH 1/3] perf: tree-shake bundled libs and ship only used Radix color scales Replace the blanket `import * as` of every bundled library with per-library named imports collected from the app's actual static usage (plus tags captured during dynamic-component serialization), so Rolldown can drop unused exports. Keep star imports for internal `$/utils/*` modules, `react`/`@emotion/react`, and any tag set that isn't a valid JS identifier. Plumb `theme_roots` through the Radix Themes plugin and the Tailwind v3/v4 root style so only the Radix CSS token files for accent/gray colors referenced by `Theme` components are emitted, with the monolithic stylesheet retained as a fallback for state-driven or unrecognized colors. --- .../src/reflex_base/compiler/templates.py | 101 ++++++++-- .../src/reflex_base/components/dynamic.py | 16 ++ .../src/reflex_base/plugins/base.py | 3 +- .../reflex_base/plugins/shared_tailwind.py | 22 +++ .../src/reflex_base/plugins/tailwind_v3.py | 47 +++-- .../src/reflex_base/plugins/tailwind_v4.py | 50 +++-- .../src/reflex_components_radix/plugin.py | 179 ++++++++++++++++- reflex/compiler/compiler.py | 142 +++++++++++--- tests/units/compiler/test_compiler.py | 185 +++++++++++++++++- tests/units/test_app.py | 7 +- 10 files changed, 661 insertions(+), 91 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/compiler/templates.py b/packages/reflex-base/src/reflex_base/compiler/templates.py index 08bb95a71d9..dfbe5660050 100644 --- a/packages/reflex-base/src/reflex_base/compiler/templates.py +++ b/packages/reflex-base/src/reflex_base/compiler/templates.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import re from collections.abc import Iterable, Mapping from typing import TYPE_CHECKING, Any, Literal @@ -161,12 +162,85 @@ def document_root_template(*, imports: list[_ImportDict], document: dict[str, An }}""" +_JS_IDENTIFIER_RE = re.compile(r"^[A-Za-z_$][\w$]*$") + + +def _normalize_window_lib_alias(lib: str) -> str: + """Produce a JS identifier from a library path by stripping ``$/`` and ``@`` and replacing ``/ - .`` with ``_``. + + Args: + lib: The library path to normalize. + + Returns: + A JS-safe identifier derived from the library path. + """ + return ( + lib + .replace("$/", "") + .replace("@", "") + .replace("/", "_") + .replace("-", "_") + .replace(".", "_") + ) + + +def _render_window_reflex_block( + window_library_imports: dict[str, set[str] | None], +) -> tuple[str, str]: + """Render the extra imports + useEffect block for window.__reflex. + + External libraries (``@radix-ui/themes`` etc.) use named imports derived + from the app's actual usage so Rolldown can tree-shake unused exports; + a star import would pin the library's entire surface onto the critical + path. Internal ``$/utils/*`` modules still use star imports since their + surface is small and Reflex-controlled. A library whose declared tag + set contains anything that isn't a valid JS identifier also falls back + to a star import rather than emit a SyntaxError. + + Args: + window_library_imports: Mapping from library path to the set of + named exports to expose (external libs) or ``None`` (internal + libs, star import). + + Returns: + A tuple of ``(import_block, useEffect_body)``. Both are empty when + no dynamic components are in play. + """ + if not window_library_imports: + return "", "" + import_lines: list[str] = [] + entries: list[str] = [] + for lib, names in window_library_imports.items(): + alias = f"__reflex_{_normalize_window_lib_alias(lib)}" + if names is None or any(not _JS_IDENTIFIER_RE.match(n) for n in names): + import_lines.append(f'import * as {alias} from "{lib}";') + entries.append(f' "{lib}": {alias},') + else: + sorted_names = sorted(names) + specs = ", ".join(f"{n} as {alias}_{n}" for n in sorted_names) + import_lines.append(f'import {{ {specs} }} from "{lib}";') + obj_entries = ", ".join(f"{n}: {alias}_{n}" for n in sorted_names) + entries.append(f' "{lib}": {{ {obj_entries} }},') + if not entries: + return "", "" + import_block = "\n".join(import_lines) + entries_str = "\n".join(entries) + effect = ( + " useEffect(() => {\n" + ' window["__reflex"] = {\n' + f"{entries_str}\n" + " };\n" + " }, []);\n" + ) + return import_block, effect + + def app_root_template( *, imports: list[_ImportDict], custom_codes: Iterable[str], hooks: dict[str, VarData | None], - window_libraries: list[tuple[str, str]], + window_library_imports: dict[str, set[str] | None], render: dict[str, Any], dynamic_imports: set[str], ): @@ -176,7 +250,8 @@ def app_root_template( imports: The list of import statements. custom_codes: The set of custom code snippets. hooks: The dictionary of hooks. - window_libraries: The list of window libraries. + window_library_imports: Per-library named-export surface for + ``window.__reflex`` (see ``collect_window_library_imports``). render: The dictionary of render functions. dynamic_imports: The set of dynamic imports. @@ -188,14 +263,9 @@ def app_root_template( custom_code_str = "\n".join(custom_codes) - import_window_libraries = "\n".join([ - f'import * as {lib_alias} from "{lib_path}";' - for lib_alias, lib_path in window_libraries - ]) - - window_imports_str = "\n".join([ - f' "{lib_path}": {lib_alias},' for lib_alias, lib_path in window_libraries - ]) + window_imports_block, window_reflex_effect = _render_window_reflex_block( + window_library_imports + ) return f""" {imports_str} @@ -204,19 +274,12 @@ def app_root_template( import {{ ThemeProvider }} from '$/utils/react-theme'; import {{ Layout as AppLayout }} from './_document'; import {{ Outlet }} from 'react-router'; -{import_window_libraries} +{window_imports_block} {custom_code_str} function ReflexProviders({{children}}) {{ - useEffect(() => {{ - // Make contexts and state objects available globally for dynamic eval'd components - let windowImports = {{ - {window_imports_str} - }}; - window["__reflex"] = windowImports; - }}, []); - +{window_reflex_effect} return jsx(ThemeProvider, {{defaultTheme: defaultColorMode, attribute: "class"}}, jsx(StateProvider, {{}}, jsx(EventLoopProvider, {{}}, diff --git a/packages/reflex-base/src/reflex_base/components/dynamic.py b/packages/reflex-base/src/reflex_base/components/dynamic.py index a668bd341fc..73164e94ac1 100644 --- a/packages/reflex-base/src/reflex_base/components/dynamic.py +++ b/packages/reflex-base/src/reflex_base/components/dynamic.py @@ -42,6 +42,17 @@ def reset_bundled_libraries() -> None: bundled_libraries.extend(DEFAULT_BUNDLED_LIBRARIES) +# Tags reachable only through eval'd dynamic components -- captured during +# Component serialization so ``collect_window_library_imports`` can expose +# them on ``window.__reflex`` (otherwise ``evalReactComponent`` can't resolve). +dynamic_component_imports: dict[str, set[imports.ImportVar]] = {} + + +def reset_dynamic_component_imports() -> None: + """Clear the captured dynamic-component import set.""" + dynamic_component_imports.clear() + + def bundle_library(component: Union["Component", str]): """Bundle a library with the component. @@ -103,6 +114,11 @@ def make_component(component: Component) -> str: component_imports = component._get_all_imports() compiler._apply_common_imports(component_imports) + for lib, ivs in component_imports.items(): + named = {iv for iv in ivs if iv.tag and not iv.is_default} + if named: + dynamic_component_imports.setdefault(lib, set()).update(named) + imports = {} for lib, names in component_imports.items(): formatted_lib_name = format_library_name(lib) diff --git a/packages/reflex-base/src/reflex_base/plugins/base.py b/packages/reflex-base/src/reflex_base/plugins/base.py index 6673309840b..b435e986bf3 100644 --- a/packages/reflex-base/src/reflex_base/plugins/base.py +++ b/packages/reflex-base/src/reflex_base/plugins/base.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, ParamSpec, Protocol, TypedDict -from typing_extensions import Unpack +from typing_extensions import NotRequired, Unpack class HookOrder(str, Enum): @@ -55,6 +55,7 @@ class PreCompileContext(CommonContext): add_modify_task: Callable[[str, Callable[[str], str]], None] radix_themes_plugin: Any unevaluated_pages: Sequence["UnevaluatedPage"] + theme_roots: NotRequired[Sequence["BaseComponent | None"]] class PostCompileContext(CommonContext): diff --git a/packages/reflex-base/src/reflex_base/plugins/shared_tailwind.py b/packages/reflex-base/src/reflex_base/plugins/shared_tailwind.py index 62180093ee6..1c915228599 100644 --- a/packages/reflex-base/src/reflex_base/plugins/shared_tailwind.py +++ b/packages/reflex-base/src/reflex_base/plugins/shared_tailwind.py @@ -1,6 +1,7 @@ """Tailwind CSS configuration types for Reflex plugins.""" import dataclasses +import re from collections.abc import Mapping from copy import deepcopy from typing import Any, Literal, TypedDict @@ -9,6 +10,27 @@ from .base import Plugin as PluginBase +_RADIX_IMPORT_RE = re.compile( + r"^@import (?:url\(['\"]|['\"])@radix-ui/themes/[^'\"]+['\"](?:\))?(?:\s+layer\(\w+\))?;\s*\n?", + re.MULTILINE, +) + + +def strip_radix_theme_imports(css: str) -> tuple[str, int]: + """Remove every Radix Themes @import line from a stylesheet. + + Handles both the monolithic ``styles.css`` and the granular per-token + imports emitted by the compiler. + + Args: + css: The stylesheet content. + + Returns: + The stripped content and the number of imports removed. + """ + return _RADIX_IMPORT_RE.subn("", css) + + TailwindPluginImport = TypedDict( "TailwindPluginImport", { diff --git a/packages/reflex-base/src/reflex_base/plugins/tailwind_v3.py b/packages/reflex-base/src/reflex_base/plugins/tailwind_v3.py index 66f575db5c2..f76573dbcb2 100644 --- a/packages/reflex-base/src/reflex_base/plugins/tailwind_v3.py +++ b/packages/reflex-base/src/reflex_base/plugins/tailwind_v3.py @@ -1,17 +1,23 @@ """Base class for all plugins.""" import dataclasses +from collections.abc import Sequence from pathlib import Path from types import SimpleNamespace +from typing import TYPE_CHECKING from reflex_base.constants.base import Dirs from reflex_base.constants.compiler import Ext, PageNames from reflex_base.plugins.shared_tailwind import ( TailwindConfig, TailwindPlugin, + strip_radix_theme_imports, tailwind_config_js_template, ) +if TYPE_CHECKING: + from reflex_base.components.component import BaseComponent + class Constants(SimpleNamespace): """Tailwind constants.""" @@ -29,7 +35,7 @@ class Constants(SimpleNamespace): ROOT_STYLE_CONTENT = """ @import "tailwindcss/base"; -{radix_import} +{radix_imports} @tailwind components; @tailwind utilities; @@ -54,23 +60,32 @@ def compile_config(config: TailwindConfig): ) -def compile_root_style(include_radix_themes: bool = True): +def compile_root_style( + include_radix_themes: bool = True, + theme_roots: Sequence["BaseComponent | None"] | None = None, +): """Compile the Tailwind root style. Args: - include_radix_themes: Whether to include the Radix stylesheet import. + include_radix_themes: Whether to emit any Radix stylesheet imports. + theme_roots: Component roots used to detect which Radix color scales are + actually referenced so only those CSS files are imported. Returns: The compiled Tailwind root style. """ - from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET + from reflex_components_radix.plugin import get_radix_themes_stylesheets + radix_imports = "" + if include_radix_themes: + radix_imports = "\n".join( + f"@import url('{sheet}');" + for sheet in get_radix_themes_stylesheets(theme_roots) + ) return str( Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH ), Constants.ROOT_STYLE_CONTENT.format( - radix_import=( - f"@import url('{RADIX_THEMES_STYLESHEET}');" if include_radix_themes else "" - ), + radix_imports=radix_imports, ) @@ -129,15 +144,13 @@ def add_tailwind_to_css_file( Returns: The modified css file content. """ - from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET - if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content: return css_file_content - if include_radix_themes and RADIX_THEMES_STYLESHEET in css_file_content: - return css_file_content.replace( - f"@import url('{RADIX_THEMES_STYLESHEET}');", - Constants.TAILWIND_CSS, - ) + + if include_radix_themes: + stripped, count = strip_radix_theme_imports(css_file_content) + if count > 0: + return stripped.rstrip() + "\n" + Constants.TAILWIND_CSS + "\n" lines = css_file_content.splitlines() insert_at = next( @@ -179,7 +192,11 @@ def pre_compile(self, **context): context["add_save_task"](compile_config, self.get_unversioned_config()) include_radix_themes = context["radix_themes_plugin"].enabled - context["add_save_task"](compile_root_style, include_radix_themes) + context["add_save_task"]( + compile_root_style, + include_radix_themes, + context.get("theme_roots"), + ) context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config) context["add_modify_task"]( str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)), diff --git a/packages/reflex-base/src/reflex_base/plugins/tailwind_v4.py b/packages/reflex-base/src/reflex_base/plugins/tailwind_v4.py index a3067edd582..3a8a86dab00 100644 --- a/packages/reflex-base/src/reflex_base/plugins/tailwind_v4.py +++ b/packages/reflex-base/src/reflex_base/plugins/tailwind_v4.py @@ -1,17 +1,23 @@ """Base class for all plugins.""" import dataclasses +from collections.abc import Sequence from pathlib import Path from types import SimpleNamespace +from typing import TYPE_CHECKING from reflex_base.constants.base import Dirs from reflex_base.constants.compiler import Ext, PageNames from reflex_base.plugins.shared_tailwind import ( TailwindConfig, TailwindPlugin, + strip_radix_theme_imports, tailwind_config_js_template, ) +if TYPE_CHECKING: + from reflex_base.components.component import BaseComponent + class Constants(SimpleNamespace): """Tailwind constants.""" @@ -29,7 +35,8 @@ class Constants(SimpleNamespace): ROOT_STYLE_CONTENT = """@layer theme, base, components, utilities; @import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/preflight.css" layer(base); -{radix_import}@import "tailwindcss/utilities.css" layer(utilities); +{radix_imports} +@import "tailwindcss/utilities.css" layer(utilities); @config "../tailwind.config.js"; """ @@ -52,25 +59,32 @@ def compile_config(config: TailwindConfig): ) -def compile_root_style(include_radix_themes: bool = True): +def compile_root_style( + include_radix_themes: bool = True, + theme_roots: Sequence["BaseComponent | None"] | None = None, +): """Compile the Tailwind root style. Args: - include_radix_themes: Whether to include the Radix stylesheet import. + include_radix_themes: Whether to emit any Radix stylesheet imports. + theme_roots: Component roots used to detect which Radix color scales are + actually referenced so only those CSS files are imported. Returns: The compiled Tailwind root style. """ - from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET + from reflex_components_radix.plugin import get_radix_themes_stylesheets + radix_imports = "" + if include_radix_themes: + radix_imports = "\n".join( + f'@import "{sheet}" layer(components);' + for sheet in get_radix_themes_stylesheets(theme_roots) + ) return str( Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH ), Constants.ROOT_STYLE_CONTENT.format( - radix_import=( - f'@import "{RADIX_THEMES_STYLESHEET}" layer(components);\n' - if include_radix_themes - else "" - ), + radix_imports=radix_imports, ) @@ -133,15 +147,13 @@ def add_tailwind_to_css_file( Returns: The modified css file content. """ - from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET - if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content: return css_file_content - if include_radix_themes and RADIX_THEMES_STYLESHEET in css_file_content: - return css_file_content.replace( - f"@import url('{RADIX_THEMES_STYLESHEET}');", - Constants.TAILWIND_CSS, - ) + + if include_radix_themes: + stripped, count = strip_radix_theme_imports(css_file_content) + if count > 0: + return stripped.rstrip() + "\n" + Constants.TAILWIND_CSS + "\n" lines = css_file_content.splitlines() insert_at = next( @@ -184,7 +196,11 @@ def pre_compile(self, **context): context["add_save_task"](compile_config, self.get_unversioned_config()) include_radix_themes = context["radix_themes_plugin"].enabled - context["add_save_task"](compile_root_style, include_radix_themes) + context["add_save_task"]( + compile_root_style, + include_radix_themes, + context.get("theme_roots"), + ) context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config) context["add_modify_task"]( str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)), diff --git a/packages/reflex-components-radix/src/reflex_components_radix/plugin.py b/packages/reflex-components-radix/src/reflex_components_radix/plugin.py index 150183a17ad..d11bd69211b 100644 --- a/packages/reflex-components-radix/src/reflex_components_radix/plugin.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/plugin.py @@ -3,12 +3,14 @@ from __future__ import annotations import dataclasses +from collections.abc import Iterable from typing import TYPE_CHECKING, Any from reflex_base.components.component import BaseComponent, Component from reflex_base.components.dynamic import bundle_library from reflex_base.plugins.base import Plugin from reflex_base.utils import console +from reflex_base.vars.base import get_python_literal from reflex_components_radix import themes from reflex_components_radix.themes.base import RadixThemesComponent @@ -22,6 +24,172 @@ _DEPRECATION_VERSION = "0.9.0" _REMOVAL_VERSION = "1.0" +_RADIX_THEMES_TOKENS_BASE = "@radix-ui/themes/tokens/base.css" +_RADIX_THEMES_COMPONENTS = "@radix-ui/themes/components.css" +_RADIX_THEMES_UTILITIES = "@radix-ui/themes/utilities.css" + +# Natural accent/gray pairings per https://www.radix-ui.com/themes/docs/theme/color#natural-pairing +_RADIX_ACCENT_TO_AUTO_GRAY: dict[str, str] = { + "tomato": "mauve", + "red": "mauve", + "ruby": "mauve", + "crimson": "mauve", + "pink": "mauve", + "plum": "mauve", + "purple": "mauve", + "violet": "mauve", + "iris": "slate", + "indigo": "slate", + "blue": "slate", + "sky": "slate", + "cyan": "slate", + "teal": "sage", + "jade": "sage", + "mint": "sage", + "green": "sage", + "grass": "sage", + "orange": "sand", + "amber": "sand", + "yellow": "sand", + "lime": "sand", + "brown": "sand", + "bronze": "sand", + "gold": "sand", + "gray": "gray", +} + +_RADIX_VALID_GRAYS = frozenset({"gray", "mauve", "slate", "sage", "olive", "sand"}) + + +def _radix_color_stylesheet(color: str) -> str: + """Return the granular CSS import path for a single Radix color scale. + + Args: + color: The Radix color name (e.g. ``"blue"``, ``"slate"``). + + Returns: + The Radix Themes CSS path for that color. + """ + return f"@radix-ui/themes/tokens/colors/{color}.css" + + +def _resolve_theme_color( + component: BaseComponent, prop_name: str +) -> tuple[str | None, bool]: + """Resolve a Theme color prop into either a literal or a dynamic marker. + + Args: + component: The Theme component being inspected. + prop_name: The color prop name (``"accent_color"`` or ``"gray_color"``). + + Returns: + A tuple ``(literal, is_dynamic)``. ``literal`` is the resolved string + when the prop is set to a static value; ``is_dynamic`` is True only + when the prop is set but cannot be resolved at compile time. + """ + raw = getattr(component, prop_name, None) + if raw is None: + return None, False + literal = get_python_literal(raw) + if isinstance(literal, str): + return literal, False + return None, True + + +def _walk_components(root: BaseComponent) -> Iterable[BaseComponent]: + """Yield ``root`` and every descendant reachable via ``.children``. + + Args: + root: The component subtree to walk. + + Yields: + Each component in the subtree, including ``root`` itself. + """ + stack: list[BaseComponent] = [root] + while stack: + node = stack.pop() + yield node + children = getattr(node, "children", None) + if children: + stack.extend(children) + + +def _collect_radix_theme_colors( + roots: Iterable[BaseComponent | None], +) -> tuple[set[str], set[str], bool]: + """Walk component trees for Theme components and collect their colors. + + Args: + roots: Component trees to walk for Theme components. + + Returns: + A tuple ``(accent_colors, gray_colors, needs_fallback)``. + ``needs_fallback`` is True if any Theme has a state-driven color + or an unrecognized color literal -- in either case the caller falls + back to the monolithic stylesheet, so a typo never resolves to a 404 + ``tokens/colors/.css`` import. + """ + accents: set[str] = set() + grays: set[str] = set() + needs_fallback = False + for root in roots: + if root is None: + continue + for node in _walk_components(root): + if getattr(node, "tag", None) != "Theme": + continue + accent, accent_dynamic = _resolve_theme_color(node, "accent_color") + gray, gray_dynamic = _resolve_theme_color(node, "gray_color") + if accent_dynamic or gray_dynamic: + needs_fallback = True + if accent is not None and accent not in _RADIX_ACCENT_TO_AUTO_GRAY: + needs_fallback = True + continue + if gray is not None and gray != "auto" and gray not in _RADIX_VALID_GRAYS: + needs_fallback = True + continue + if accent: + accents.add(accent) + if gray and gray != "auto": + grays.add(gray) + # When gray_color is unset or "auto", Radix pairs the accent with a + # natural gray scale -- ship that scale too. + if accent and (gray is None or gray == "auto"): + grays.add(_RADIX_ACCENT_TO_AUTO_GRAY[accent]) + return accents, grays, needs_fallback + + +def get_radix_themes_stylesheets( + roots: Iterable[BaseComponent | None] | None = None, +) -> list[str]: + """Return the Radix Themes stylesheets to import. + + Importing the granular per-color CSS files (tokens/base.css + + tokens/colors/.css + components.css + utilities.css) instead of + the monolithic ``styles.css`` lets the bundler drop the ~30 unused color + scales. Falls back to the monolithic stylesheet when ``roots`` is None, + when no static accent is detected, when any Theme uses a state-driven + color (so runtime color switches still work), or when a Theme references + a color name not recognized by the granular file layout (which would + otherwise resolve to a 404 ``tokens/colors/.css``). + + Args: + roots: Component trees to scan for Theme components. + + Returns: + Ordered list of stylesheet paths to import. + """ + if roots is None: + return [RADIX_THEMES_STYLESHEET] + accents, grays, needs_fallback = _collect_radix_theme_colors(roots) + if needs_fallback or not accents: + return [RADIX_THEMES_STYLESHEET] + sheets = [_RADIX_THEMES_TOKENS_BASE] + sheets.extend(_radix_color_stylesheet(c) for c in sorted(grays)) + sheets.extend(_radix_color_stylesheet(c) for c in sorted(accents)) + sheets.extend([_RADIX_THEMES_COMPONENTS, _RADIX_THEMES_UTILITIES]) + return sheets + @dataclasses.dataclass class RadixThemesPlugin(Plugin): @@ -46,8 +214,15 @@ def create_implicit(cls) -> RadixThemesPlugin: return cls(enabled=False, _explicit=False) def get_stylesheet_paths(self, **context: Any) -> tuple[str, ...]: - """Return the Radix Themes stylesheet when enabled.""" - return (RADIX_THEMES_STYLESHEET,) if self.enabled else () + """Return the Radix Themes stylesheets when enabled. + + When ``theme_roots`` are supplied via context, only the color scales + actually referenced by ``Theme`` components are shipped (falling back + to the monolithic stylesheet when a color is state-driven). + """ + if not self.enabled: + return () + return tuple(get_radix_themes_stylesheets(context.get("theme_roots"))) def get_frontend_dependencies(self, **context: Any) -> tuple[str, ...]: """Return the Radix Themes package when enabled.""" diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index a0a934c1b6d..2b1fa77966a 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -6,6 +6,7 @@ import sys from collections.abc import Callable, Iterable, Sequence from inspect import getmodule +from itertools import chain from pathlib import Path from typing import TYPE_CHECKING, Any @@ -18,6 +19,7 @@ CustomComponent, evaluate_style_namespaces, ) +from reflex_base.components.dynamic import bundled_libraries, dynamic_component_imports from reflex_base.config import get_config from reflex_base.constants.compiler import PageNames, ResetStylesheet from reflex_base.constants.state import FIELD_MARKER @@ -25,7 +27,7 @@ from reflex_base.plugins import CompileContext, CompilerHooks, PageContext, Plugin from reflex_base.style import SYSTEM_COLOR_MODE from reflex_base.utils.exceptions import ReflexError -from reflex_base.utils.format import to_title_case +from reflex_base.utils.format import format_library_name, to_title_case from reflex_base.utils.imports import ImportVar from reflex_base.vars.base import LiteralVar, Var from reflex_components_core.base.app_wrap import AppWrap @@ -46,8 +48,6 @@ from reflex.utils.exec import get_compile_context, is_prod_mode from reflex.utils.prerequisites import get_web_dir -RADIX_THEMES_STYLESHEET = "@radix-ui/themes/styles.css" - def _set_progress_total( progress: Progress | console.PoorProgress, @@ -114,45 +114,90 @@ def _compile_document_root(root: Component) -> str: ) -def _normalize_library_name(lib: str) -> str: - """Normalize the library name. +# Path-like prefixes mark Reflex-controlled internal modules (e.g. "$/utils/..."); +# the rest are external npm libraries where star imports defeat tree-shaking. +_INTERNAL_LIB_PREFIXES = ("$/", "/", ".") + +# Runtime-eval'd dynamic components reach for ``window.__reflex.react`` and +# ``window.__reflex['@emotion/react']`` as if they were the whole module +# (``state.js`` aliases ``window.React = window.__reflex.react``). Tree-shaking +# these to the host app's static surface drops APIs that user custom_code or +# third-party libraries may legitimately read at runtime, so always star-import. +_ALWAYS_STAR_IMPORT_LIBS = frozenset({"react", "@emotion/react"}) + + +def collect_window_library_imports( + import_sources: Iterable[dict[str, list[ImportVar]]], +) -> dict[str, set[str] | None]: + """Build the ``window.__reflex`` surface for runtime-eval'd code. + + Each bundled library gets either a set of named exports (for external libs, + collected from the app's actual static usage so Rolldown can tree-shake) or + ``None`` (for internal Reflex modules, which use a star import). + + External library tags come from two sources: (1) static page / app-root + imports, and (2) tags captured during compile-time serialization of + dynamic Component values (Component-typed state field defaults, computed + Component vars evaluated when generating the initial state) -- see + ``reflex_base.components.dynamic.dynamic_component_imports``. + + Takes an iterable of import dicts (one per page / app_root / memo group) + rather than a pre-merged dict so callers don't have to fold the same + library's named exports across sources before calling. Args: - lib: The library name to normalize. + import_sources: One import dict per source (page, app_root, etc). Returns: - The normalized library name. + Mapping from library path to either the set of named exports to expose + (external libs) or None (internal libs, use star import). """ - if lib == "react": - return "React" - return lib.replace("$/", "").replace("@", "").replace("/", "_").replace("-", "_") + per_lib_tags: dict[str, set[str]] = {} + for source in chain(import_sources, (dynamic_component_imports,)): + for imported_lib, import_vars in source.items(): + key = format_library_name(imported_lib) + for iv in import_vars: + if iv.tag and not iv.is_default: + per_lib_tags.setdefault(key, set()).add(iv.tag) + + result: dict[str, set[str] | None] = {} + for lib in bundled_libraries: + if lib.startswith(_INTERNAL_LIB_PREFIXES) or lib in _ALWAYS_STAR_IMPORT_LIBS: + result[lib] = None + continue + tags = per_lib_tags.get(lib) + if tags: + result[lib] = tags + return result -def _compile_app(app_root: Component) -> str: +def _compile_app( + app_root: Component, + window_library_imports: dict[str, set[str] | None] | None = None, + app_root_imports: dict[str, list[ImportVar]] | None = None, +) -> str: """Compile the app template component. Args: app_root: The app root to compile. + window_library_imports: Per-library named-export surface to expose on + ``window.__reflex`` for dynamic components. Empty/None skips the + bootstrap entirely. + app_root_imports: Precomputed result of ``app_root._get_all_imports()``; + pass it to avoid a second full-tree walk on the hot path. Returns: The compiled app. """ - from reflex_base.components.dynamic import bundled_libraries - - window_libraries = [ - (_normalize_library_name(name), name) for name in bundled_libraries - ] - - window_libraries_deduped = list(dict.fromkeys(window_libraries)) - - app_root_imports = app_root._get_all_imports() + if app_root_imports is None: + app_root_imports = app_root._get_all_imports() _apply_common_imports(app_root_imports) return templates.app_root_template( imports=utils.compile_imports(app_root_imports), custom_codes=app_root._get_all_custom_code(), hooks=app_root._get_all_hooks(), - window_libraries=window_libraries_deduped, + window_library_imports=window_library_imports or {}, render=app_root.render(), dynamic_imports=app_root._get_all_dynamic_imports(), ) @@ -227,6 +272,7 @@ def compile_root_stylesheet( stylesheets: list[str], reset_style: bool = True, plugins: Sequence[Plugin] | None = None, + theme_roots: Sequence[BaseComponent | None] | None = None, ) -> tuple[str, str]: """Compile the root stylesheet. @@ -234,13 +280,15 @@ def compile_root_stylesheet( stylesheets: The stylesheets to include in the root stylesheet. reset_style: Whether to include CSS reset for margin and padding. plugins: The effective plugins for the active compile. + theme_roots: Component roots to scan for Theme components so only the + used Radix color scales are shipped. Returns: The path and code of the compiled root stylesheet. """ output_path = utils.get_root_stylesheet_path() - code = _compile_root_stylesheet(stylesheets, reset_style, plugins) + code = _compile_root_stylesheet(stylesheets, reset_style, plugins, theme_roots) return output_path, code @@ -284,6 +332,7 @@ def _compile_root_stylesheet( stylesheets: list[str], reset_style: bool = True, plugins: Sequence[Plugin] | None = None, + theme_roots: Sequence[BaseComponent | None] | None = None, ) -> str: """Compile the root stylesheet. @@ -291,6 +340,8 @@ def _compile_root_stylesheet( stylesheets: The stylesheets to include in the root stylesheet. reset_style: Whether to include CSS reset for margin and padding. plugins: The effective plugins for the active compile. + theme_roots: Component roots to scan for Theme components so only the + used Radix color scales are shipped. Returns: The compiled root stylesheet. @@ -307,8 +358,11 @@ def _compile_root_stylesheet( sheets.append(f"./{ResetStylesheet.FILENAME}") active_plugins = get_config().plugins if plugins is None else plugins + plugin_context: dict[str, Any] = {"theme_roots": theme_roots} sheets.extend([ - sheet for plugin in active_plugins for sheet in plugin.get_stylesheet_paths() + sheet + for plugin in active_plugins + for sheet in plugin.get_stylesheet_paths(**plugin_context) ]) failed_to_import_sass = False @@ -574,22 +628,29 @@ def compile_document_root( return output_path, code -def compile_app_root(app_root: Component) -> tuple[str, str]: +def compile_app_root( + app_root: Component, + window_library_imports: dict[str, set[str] | None] | None = None, + app_root_imports: dict[str, list[ImportVar]] | None = None, +) -> tuple[str, str]: """Compile the app root. Args: app_root: The app root component to compile. + window_library_imports: Per-library named-export surface for + ``window.__reflex`` (see ``collect_window_library_imports``). Pass + ``None`` to skip emitting ``window.__reflex`` entirely. + app_root_imports: Precomputed ``app_root._get_all_imports()``; reused + from the caller to avoid a second tree walk. Returns: The path and code of the compiled app wrapper. """ - # Get the path for the output file. output_path = str( get_web_dir() / constants.Dirs.PAGES / constants.PageNames.APP_ROOT ) - # Compile the document root. - code = _compile_app(app_root) + code = _compile_app(app_root, window_library_imports, app_root_imports) return output_path, code @@ -1000,9 +1061,14 @@ def compile_app( ``True`` when a real frontend compile ran, ``False`` when the call short-circuited (backend-only paths that only re-evaluate pages). """ - from reflex_base.components.dynamic import bundle_library, reset_bundled_libraries + from reflex_base.components.dynamic import ( + bundle_library, + reset_bundled_libraries, + reset_dynamic_component_imports, + ) from reflex_base.utils.exceptions import ReflexRuntimeError + reset_dynamic_component_imports() app._apply_decorated_pages() app._pages = {} @@ -1126,7 +1192,13 @@ def compile_app( app_wrappers = _resolve_app_wrap_components(app, compile_ctx.app_wrap_components) app_root = app._app_root(app_wrappers) - all_imports = utils.merge_imports(all_imports, app_root._get_all_imports()) + app_root_imports = app_root._get_all_imports() + all_imports = utils.merge_imports(all_imports, app_root_imports) + + theme_roots: list[BaseComponent | None] = [ + app_root, + *(p.root_component for p in compile_ctx.compiled_pages.values()), + ] memo_component_files, memo_components_imports = compile_memo_components( dict.fromkeys(CUSTOM_COMPONENTS.values()), @@ -1186,6 +1258,7 @@ def add_save_task( )), radix_themes_plugin=radix_themes_plugin, unevaluated_pages=list(app._unevaluated_pages.values()), + theme_roots=theme_roots, ) if save_tasks: @@ -1198,6 +1271,7 @@ def add_save_task( app.stylesheets, app.reset_style, plugins=compiler_plugins, + theme_roots=theme_roots, ) ) progress.advance(task) @@ -1221,7 +1295,15 @@ def add_save_task( ) progress.advance(task) - compile_results.append(compile_app_root(app_root)) + window_library_imports = collect_window_library_imports( + chain( + (app_root_imports,), + (p.frontend_imports for p in compile_ctx.compiled_pages.values()), + ) + ) + compile_results.append( + compile_app_root(app_root, window_library_imports, app_root_imports) + ) progress.advance(task) progress.stop() diff --git a/tests/units/compiler/test_compiler.py b/tests/units/compiler/test_compiler.py index e2f1b769668..d4465b57e24 100644 --- a/tests/units/compiler/test_compiler.py +++ b/tests/units/compiler/test_compiler.py @@ -1,6 +1,8 @@ import importlib.util import os from pathlib import Path +from types import SimpleNamespace +from typing import TYPE_CHECKING, cast import pytest from pytest_mock import MockerFixture @@ -14,10 +16,14 @@ from reflex_components_core.base.document import Links, Scripts from reflex_components_core.el.elements.metadata import Head, Link, Meta from reflex_components_core.el.elements.other import Html +from reflex_components_radix.plugin import get_radix_themes_stylesheets import reflex as rx from reflex.compiler import compiler, utils +if TYPE_CHECKING: + from reflex_base.components.component import BaseComponent + @pytest.mark.parametrize( ("fields", "test_default", "test_rest"), @@ -383,15 +389,25 @@ def test_compile_app_root_omits_radix_window_library_by_default(): def test_compile_app_root_includes_radix_window_library_when_bundled(): - """Bundled Radix libraries should be exposed to window.__reflex.""" + """Bundled Radix libraries are exposed to window.__reflex via named imports + derived from the app's actual static usage (so Rolldown can tree-shake). + """ + from reflex_base.utils.imports import ImportVar + reset_bundled_libraries() try: bundle_library("@radix-ui/themes@3.3.0") - _, code = compiler.compile_app_root(rx.el.div("hello")) + window_library_imports = compiler.collect_window_library_imports([ + {"@radix-ui/themes@3.3.0": [ImportVar(tag="Theme")]}, + ]) + _, code = compiler.compile_app_root(rx.el.div("hello"), window_library_imports) - assert 'import * as radix_ui_themes from "@radix-ui/themes";' in code - assert '"@radix-ui/themes": radix_ui_themes' in code + assert ( + 'import { Theme as __reflex_radix_ui_themes_Theme } from "@radix-ui/themes";' + in code + ) + assert '"@radix-ui/themes": { Theme: __reflex_radix_ui_themes_Theme }' in code finally: reset_bundled_libraries() @@ -425,6 +441,167 @@ def test_compile_nonexistent_stylesheet(tmp_path, mocker: MockerFixture): compiler.compile_root_stylesheet(stylesheets) +def test_radix_themes_stylesheets_no_roots_falls_back_to_monolith(): + """When no roots are provided, use the monolithic stylesheet.""" + assert get_radix_themes_stylesheets(None) == ["@radix-ui/themes/styles.css"] + + +def test_radix_themes_stylesheets_literal_accent_emits_granular_imports(): + """A literal accent_color emits only the needed granular imports.""" + sheets = get_radix_themes_stylesheets([rx.theme(accent_color="blue")]) + assert sheets == [ + "@radix-ui/themes/tokens/base.css", + # blue's natural gray pairing is slate + "@radix-ui/themes/tokens/colors/slate.css", + "@radix-ui/themes/tokens/colors/blue.css", + "@radix-ui/themes/components.css", + "@radix-ui/themes/utilities.css", + ] + + +def test_radix_themes_stylesheets_explicit_gray_overrides_auto_pairing(): + """An explicit gray_color replaces the accent's auto-paired gray.""" + sheets = get_radix_themes_stylesheets([ + rx.theme(accent_color="red", gray_color="mauve") + ]) + assert "@radix-ui/themes/tokens/colors/mauve.css" in sheets + assert "@radix-ui/themes/tokens/colors/red.css" in sheets + # The default auto pairing for red is also mauve, so no extra colors. + color_sheets = [s for s in sheets if "/colors/" in s] + assert len(color_sheets) == 2 + + +def test_radix_themes_stylesheets_nested_themes_union_colors(): + """Nested Theme components contribute the union of their colors.""" + root = rx.box( + rx.theme(accent_color="green"), + rx.theme(accent_color="pink"), + ) + sheets = get_radix_themes_stylesheets([root]) + color_sheets = {s for s in sheets if "/colors/" in s} + assert "@radix-ui/themes/tokens/colors/green.css" in color_sheets + assert "@radix-ui/themes/tokens/colors/pink.css" in color_sheets + + +def test_radix_themes_stylesheets_dynamic_color_falls_back_to_monolith(): + """A state-driven Theme color forces the monolithic stylesheet.""" + from typing import Literal + + class _S(rx.State): + color: Literal["red", "blue"] = "red" + + sheets = get_radix_themes_stylesheets([rx.theme(accent_color=_S.color)]) + assert sheets == ["@radix-ui/themes/styles.css"] + + +def test_radix_themes_stylesheets_unknown_color_falls_back_to_monolith(): + """Defensive: an unrecognized accent color (e.g. Radix adds new ones that + don't map to a granular file in our pinned layout) falls back to the + monolithic stylesheet instead of emitting a 404 ``tokens/colors/X.css``. + """ + fake_theme = cast( + "BaseComponent", + SimpleNamespace( + tag="Theme", accent_color="paprika", gray_color=None, children=() + ), + ) + assert get_radix_themes_stylesheets([fake_theme]) == ["@radix-ui/themes/styles.css"] + + +def test_radix_themes_stylesheets_unknown_gray_falls_back_to_monolith(): + """Same defense for an unrecognized gray color.""" + fake_theme = cast( + "BaseComponent", + SimpleNamespace( + tag="Theme", accent_color="blue", gray_color="taupe", children=() + ), + ) + assert get_radix_themes_stylesheets([fake_theme]) == ["@radix-ui/themes/styles.css"] + + +@pytest.fixture +def _isolate_dynamic_imports(): + """Reset window-import state so each test sees only its own bundled libs.""" + from reflex_base.components.dynamic import reset_dynamic_component_imports + + reset_dynamic_component_imports() + reset_bundled_libraries() + bundle_library("@radix-ui/themes@3.3.0") + yield + reset_dynamic_component_imports() + reset_bundled_libraries() + + +@pytest.mark.usefixtures("_isolate_dynamic_imports") +def test_collect_window_library_imports_internal_modules_always_star_imported(): + """Internal Reflex modules map to None (star import) so dynamic components + and plugins reading ``window.__reflex`` find what they need even when the + app has no static external references. + """ + result = compiler.collect_window_library_imports([{}]) + assert result["$/utils/state"] is None + assert "@radix-ui/themes" not in result + + +@pytest.mark.usefixtures("_isolate_dynamic_imports") +def test_collect_window_library_imports_external_lib_uses_named_imports(): + """External libraries on ``window.__reflex`` use named imports so Rolldown + can tree-shake unused exports. + """ + sources = [ + {"$/utils/state": [ImportVar(tag="evalReactComponent")]}, + { + "@radix-ui/themes@3.3.0": [ + ImportVar(tag="Theme"), + ImportVar(tag="Button"), + ] + }, + ] + result = compiler.collect_window_library_imports(sources) + assert result["@radix-ui/themes"] == {"Theme", "Button"} + + +@pytest.mark.usefixtures("_isolate_dynamic_imports") +def test_collect_window_library_imports_unions_dynamic_component_tags(): + """Tags captured during dynamic-Component serialization are unioned into + the named-import surface so runtime-eval'd code finds them on + ``window.__reflex``. + """ + from reflex_base.components.dynamic import dynamic_component_imports + + sources = [{"@radix-ui/themes@3.3.0": [ImportVar(tag="Theme")]}] + dynamic_component_imports["@radix-ui/themes@3.3.0"] = {ImportVar(tag="Flex")} + + result = compiler.collect_window_library_imports(sources) + assert result["@radix-ui/themes"] == {"Theme", "Flex"} + + +@pytest.mark.usefixtures("_isolate_dynamic_imports") +def test_collect_window_library_imports_react_is_always_star_imported(): + """``react`` and ``@emotion/react`` must expose the full module on + ``window.__reflex`` -- ``state.js`` aliases ``window.React`` to + ``window.__reflex.react``, and runtime code may legitimately read APIs + the host app didn't statically import. + """ + sources = [{"react": [ImportVar(tag="useState")]}] + result = compiler.collect_window_library_imports(sources) + assert result["react"] is None + assert result["@emotion/react"] is None + + +def test_render_window_reflex_block_falls_back_to_star_for_invalid_tag(): + """If any declared tag isn't a valid JS identifier, the library falls back + to a star import rather than emit ``import { Foo.Bar as ... }`` (SyntaxError). + """ + from reflex_base.compiler.templates import _render_window_reflex_block + + import_block, _ = _render_window_reflex_block({ + "@some/lib": {"Foo.Bar"}, + }) + assert "Foo.Bar" not in import_block + assert 'import * as __reflex_some_lib from "@some/lib";' in import_block + + def test_create_document_root(): """Test that the document root is created correctly.""" # Test with no components. diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 887f6a3d8b4..405553a16c5 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -38,7 +38,8 @@ import reflex as rx from reflex import AdminDash, constants -from reflex.app import App, ComponentCallable, upload +from reflex._upload import upload +from reflex.app import App, ComponentCallable from reflex.environment import environment from reflex.istate.data import RouterData from reflex.istate.manager.disk import StateManagerDisk @@ -2159,7 +2160,7 @@ def test_compile_with_radix_component_auto_enables_radix_plugin( web_dir / constants.Dirs.PAGES / constants.PageNames.APP_ROOT ).read_text() - assert "@radix-ui/themes/styles.css" in root_stylesheet + assert "@radix-ui/themes/tokens/colors/blue.css" in root_stylesheet assert 'RadixThemesTheme,{accentColor:"blue"' in app_root mock_deprecate.assert_called_once() assert ( @@ -2193,7 +2194,7 @@ def test_compile_with_legacy_app_theme_warns_and_enables_radix_plugin( web_dir / constants.Dirs.PAGES / constants.PageNames.APP_ROOT ).read_text() - assert "@radix-ui/themes/styles.css" in root_stylesheet + assert "@radix-ui/themes/tokens/colors/plum.css" in root_stylesheet assert 'RadixThemesTheme,{accentColor:"plum"' in app_root mock_deprecate.assert_called_once() assert mock_deprecate.call_args.kwargs["feature_name"] == "App(theme=...)" From b485c49da1815ba863fcbf1778c9c1ebb25970c9 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza Date: Tue, 26 May 2026 20:54:04 +0500 Subject: [PATCH 2/3] fix: avoid duplicate Radix color stylesheet when accent matches gray When accent_color and its paired gray resolve to the same scale (e.g. accent_color="gray"), the tokens CSS file was imported twice. Subtract accents from grays before extending the sheet list so each color file is emitted only once. --- .../src/reflex_components_radix/plugin.py | 4 +++- tests/units/compiler/test_compiler.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/reflex-components-radix/src/reflex_components_radix/plugin.py b/packages/reflex-components-radix/src/reflex_components_radix/plugin.py index d11bd69211b..42da2e5f89c 100644 --- a/packages/reflex-components-radix/src/reflex_components_radix/plugin.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/plugin.py @@ -185,7 +185,9 @@ def get_radix_themes_stylesheets( if needs_fallback or not accents: return [RADIX_THEMES_STYLESHEET] sheets = [_RADIX_THEMES_TOKENS_BASE] - sheets.extend(_radix_color_stylesheet(c) for c in sorted(grays)) + # An accent and its paired gray can be the same scale (e.g. accent_color="gray" + # auto-pairs with "gray"); subtract to avoid importing the same file twice. + sheets.extend(_radix_color_stylesheet(c) for c in sorted(grays - accents)) sheets.extend(_radix_color_stylesheet(c) for c in sorted(accents)) sheets.extend([_RADIX_THEMES_COMPONENTS, _RADIX_THEMES_UTILITIES]) return sheets diff --git a/tests/units/compiler/test_compiler.py b/tests/units/compiler/test_compiler.py index d4465b57e24..f3898a40e8f 100644 --- a/tests/units/compiler/test_compiler.py +++ b/tests/units/compiler/test_compiler.py @@ -471,6 +471,17 @@ def test_radix_themes_stylesheets_explicit_gray_overrides_auto_pairing(): assert len(color_sheets) == 2 +def test_radix_themes_stylesheets_gray_accent_no_duplicate_import(): + """accent_color='gray' auto-pairs with 'gray' -- emit gray.css only once.""" + sheets = get_radix_themes_stylesheets([rx.theme(accent_color="gray")]) + assert sheets.count("@radix-ui/themes/tokens/colors/gray.css") == 1 + # And with an explicit matching gray_color too. + sheets = get_radix_themes_stylesheets([ + rx.theme(accent_color="gray", gray_color="gray") + ]) + assert sheets.count("@radix-ui/themes/tokens/colors/gray.css") == 1 + + def test_radix_themes_stylesheets_nested_themes_union_colors(): """Nested Theme components contribute the union of their colors.""" root = rx.box( From 36f67502ac727892e01932ab6886cc9ea66feb16 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza Date: Wed, 3 Jun 2026 23:32:43 +0500 Subject: [PATCH 3/3] revert: ship monolithic Radix Themes stylesheet again Drop the per-color-scale tree-shaking that emitted granular @radix-ui/themes/tokens/colors/.css imports and threaded theme_roots through the compiler and plugin context. Restore the single @radix-ui/themes/styles.css import, removing get_radix_themes_stylesheets, strip_radix_theme_imports, and the PreCompileContext.theme_roots field. --- .../src/reflex_base/plugins/base.py | 3 +- .../reflex_base/plugins/shared_tailwind.py | 22 --- .../src/reflex_base/plugins/tailwind_v3.py | 47 ++--- .../src/reflex_base/plugins/tailwind_v4.py | 50 ++--- .../src/reflex_components_radix/plugin.py | 181 +----------------- reflex/compiler/compiler.py | 20 +- tests/units/compiler/test_compiler.py | 95 --------- tests/units/test_app.py | 4 +- 8 files changed, 39 insertions(+), 383 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/plugins/base.py b/packages/reflex-base/src/reflex_base/plugins/base.py index b435e986bf3..6673309840b 100644 --- a/packages/reflex-base/src/reflex_base/plugins/base.py +++ b/packages/reflex-base/src/reflex_base/plugins/base.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, ParamSpec, Protocol, TypedDict -from typing_extensions import NotRequired, Unpack +from typing_extensions import Unpack class HookOrder(str, Enum): @@ -55,7 +55,6 @@ class PreCompileContext(CommonContext): add_modify_task: Callable[[str, Callable[[str], str]], None] radix_themes_plugin: Any unevaluated_pages: Sequence["UnevaluatedPage"] - theme_roots: NotRequired[Sequence["BaseComponent | None"]] class PostCompileContext(CommonContext): diff --git a/packages/reflex-base/src/reflex_base/plugins/shared_tailwind.py b/packages/reflex-base/src/reflex_base/plugins/shared_tailwind.py index 1c915228599..62180093ee6 100644 --- a/packages/reflex-base/src/reflex_base/plugins/shared_tailwind.py +++ b/packages/reflex-base/src/reflex_base/plugins/shared_tailwind.py @@ -1,7 +1,6 @@ """Tailwind CSS configuration types for Reflex plugins.""" import dataclasses -import re from collections.abc import Mapping from copy import deepcopy from typing import Any, Literal, TypedDict @@ -10,27 +9,6 @@ from .base import Plugin as PluginBase -_RADIX_IMPORT_RE = re.compile( - r"^@import (?:url\(['\"]|['\"])@radix-ui/themes/[^'\"]+['\"](?:\))?(?:\s+layer\(\w+\))?;\s*\n?", - re.MULTILINE, -) - - -def strip_radix_theme_imports(css: str) -> tuple[str, int]: - """Remove every Radix Themes @import line from a stylesheet. - - Handles both the monolithic ``styles.css`` and the granular per-token - imports emitted by the compiler. - - Args: - css: The stylesheet content. - - Returns: - The stripped content and the number of imports removed. - """ - return _RADIX_IMPORT_RE.subn("", css) - - TailwindPluginImport = TypedDict( "TailwindPluginImport", { diff --git a/packages/reflex-base/src/reflex_base/plugins/tailwind_v3.py b/packages/reflex-base/src/reflex_base/plugins/tailwind_v3.py index f76573dbcb2..dbf3c0939d7 100644 --- a/packages/reflex-base/src/reflex_base/plugins/tailwind_v3.py +++ b/packages/reflex-base/src/reflex_base/plugins/tailwind_v3.py @@ -1,23 +1,17 @@ """Base class for all plugins.""" import dataclasses -from collections.abc import Sequence from pathlib import Path from types import SimpleNamespace -from typing import TYPE_CHECKING from reflex_base.constants.base import Dirs from reflex_base.constants.compiler import Ext, PageNames from reflex_base.plugins.shared_tailwind import ( TailwindConfig, TailwindPlugin, - strip_radix_theme_imports, tailwind_config_js_template, ) -if TYPE_CHECKING: - from reflex_base.components.component import BaseComponent - class Constants(SimpleNamespace): """Tailwind constants.""" @@ -35,7 +29,7 @@ class Constants(SimpleNamespace): ROOT_STYLE_CONTENT = """ @import "tailwindcss/base"; -{radix_imports} +{radix_import} @tailwind components; @tailwind utilities; @@ -60,32 +54,23 @@ def compile_config(config: TailwindConfig): ) -def compile_root_style( - include_radix_themes: bool = True, - theme_roots: Sequence["BaseComponent | None"] | None = None, -): +def compile_root_style(include_radix_themes: bool = True): """Compile the Tailwind root style. Args: - include_radix_themes: Whether to emit any Radix stylesheet imports. - theme_roots: Component roots used to detect which Radix color scales are - actually referenced so only those CSS files are imported. + include_radix_themes: Whether to include the Radix stylesheet import. Returns: The compiled Tailwind root style. """ - from reflex_components_radix.plugin import get_radix_themes_stylesheets + from reflex_components_radix.plugin import RADIX_THEMES_STYLESHEET - radix_imports = "" - if include_radix_themes: - radix_imports = "\n".join( - f"@import url('{sheet}');" - for sheet in get_radix_themes_stylesheets(theme_roots) - ) return str( Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH ), Constants.ROOT_STYLE_CONTENT.format( - radix_imports=radix_imports, + radix_import=( + f"@import url('{RADIX_THEMES_STYLESHEET}');" if include_radix_themes else "" + ), ) @@ -144,13 +129,15 @@ def add_tailwind_to_css_file( Returns: The modified css file content. """ + from reflex_components_radix.plugin import RADIX_THEMES_STYLESHEET + if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content: return css_file_content - - if include_radix_themes: - stripped, count = strip_radix_theme_imports(css_file_content) - if count > 0: - return stripped.rstrip() + "\n" + Constants.TAILWIND_CSS + "\n" + if include_radix_themes and RADIX_THEMES_STYLESHEET in css_file_content: + return css_file_content.replace( + f"@import url('{RADIX_THEMES_STYLESHEET}');", + Constants.TAILWIND_CSS, + ) lines = css_file_content.splitlines() insert_at = next( @@ -192,11 +179,7 @@ def pre_compile(self, **context): context["add_save_task"](compile_config, self.get_unversioned_config()) include_radix_themes = context["radix_themes_plugin"].enabled - context["add_save_task"]( - compile_root_style, - include_radix_themes, - context.get("theme_roots"), - ) + context["add_save_task"](compile_root_style, include_radix_themes) context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config) context["add_modify_task"]( str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)), diff --git a/packages/reflex-base/src/reflex_base/plugins/tailwind_v4.py b/packages/reflex-base/src/reflex_base/plugins/tailwind_v4.py index 3a8a86dab00..15143ad8b77 100644 --- a/packages/reflex-base/src/reflex_base/plugins/tailwind_v4.py +++ b/packages/reflex-base/src/reflex_base/plugins/tailwind_v4.py @@ -1,23 +1,17 @@ """Base class for all plugins.""" import dataclasses -from collections.abc import Sequence from pathlib import Path from types import SimpleNamespace -from typing import TYPE_CHECKING from reflex_base.constants.base import Dirs from reflex_base.constants.compiler import Ext, PageNames from reflex_base.plugins.shared_tailwind import ( TailwindConfig, TailwindPlugin, - strip_radix_theme_imports, tailwind_config_js_template, ) -if TYPE_CHECKING: - from reflex_base.components.component import BaseComponent - class Constants(SimpleNamespace): """Tailwind constants.""" @@ -35,8 +29,7 @@ class Constants(SimpleNamespace): ROOT_STYLE_CONTENT = """@layer theme, base, components, utilities; @import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/preflight.css" layer(base); -{radix_imports} -@import "tailwindcss/utilities.css" layer(utilities); +{radix_import}@import "tailwindcss/utilities.css" layer(utilities); @config "../tailwind.config.js"; """ @@ -59,32 +52,25 @@ def compile_config(config: TailwindConfig): ) -def compile_root_style( - include_radix_themes: bool = True, - theme_roots: Sequence["BaseComponent | None"] | None = None, -): +def compile_root_style(include_radix_themes: bool = True): """Compile the Tailwind root style. Args: - include_radix_themes: Whether to emit any Radix stylesheet imports. - theme_roots: Component roots used to detect which Radix color scales are - actually referenced so only those CSS files are imported. + include_radix_themes: Whether to include the Radix stylesheet import. Returns: The compiled Tailwind root style. """ - from reflex_components_radix.plugin import get_radix_themes_stylesheets + from reflex_components_radix.plugin import RADIX_THEMES_STYLESHEET - radix_imports = "" - if include_radix_themes: - radix_imports = "\n".join( - f'@import "{sheet}" layer(components);' - for sheet in get_radix_themes_stylesheets(theme_roots) - ) return str( Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH ), Constants.ROOT_STYLE_CONTENT.format( - radix_imports=radix_imports, + radix_import=( + f'@import "{RADIX_THEMES_STYLESHEET}" layer(components);\n' + if include_radix_themes + else "" + ), ) @@ -147,13 +133,15 @@ def add_tailwind_to_css_file( Returns: The modified css file content. """ + from reflex_components_radix.plugin import RADIX_THEMES_STYLESHEET + if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content: return css_file_content - - if include_radix_themes: - stripped, count = strip_radix_theme_imports(css_file_content) - if count > 0: - return stripped.rstrip() + "\n" + Constants.TAILWIND_CSS + "\n" + if include_radix_themes and RADIX_THEMES_STYLESHEET in css_file_content: + return css_file_content.replace( + f"@import url('{RADIX_THEMES_STYLESHEET}');", + Constants.TAILWIND_CSS, + ) lines = css_file_content.splitlines() insert_at = next( @@ -196,11 +184,7 @@ def pre_compile(self, **context): context["add_save_task"](compile_config, self.get_unversioned_config()) include_radix_themes = context["radix_themes_plugin"].enabled - context["add_save_task"]( - compile_root_style, - include_radix_themes, - context.get("theme_roots"), - ) + context["add_save_task"](compile_root_style, include_radix_themes) context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config) context["add_modify_task"]( str(Path(Dirs.STYLES) / (PageNames.STYLESHEET_ROOT + Ext.CSS)), diff --git a/packages/reflex-components-radix/src/reflex_components_radix/plugin.py b/packages/reflex-components-radix/src/reflex_components_radix/plugin.py index 42da2e5f89c..150183a17ad 100644 --- a/packages/reflex-components-radix/src/reflex_components_radix/plugin.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/plugin.py @@ -3,14 +3,12 @@ from __future__ import annotations import dataclasses -from collections.abc import Iterable from typing import TYPE_CHECKING, Any from reflex_base.components.component import BaseComponent, Component from reflex_base.components.dynamic import bundle_library from reflex_base.plugins.base import Plugin from reflex_base.utils import console -from reflex_base.vars.base import get_python_literal from reflex_components_radix import themes from reflex_components_radix.themes.base import RadixThemesComponent @@ -24,174 +22,6 @@ _DEPRECATION_VERSION = "0.9.0" _REMOVAL_VERSION = "1.0" -_RADIX_THEMES_TOKENS_BASE = "@radix-ui/themes/tokens/base.css" -_RADIX_THEMES_COMPONENTS = "@radix-ui/themes/components.css" -_RADIX_THEMES_UTILITIES = "@radix-ui/themes/utilities.css" - -# Natural accent/gray pairings per https://www.radix-ui.com/themes/docs/theme/color#natural-pairing -_RADIX_ACCENT_TO_AUTO_GRAY: dict[str, str] = { - "tomato": "mauve", - "red": "mauve", - "ruby": "mauve", - "crimson": "mauve", - "pink": "mauve", - "plum": "mauve", - "purple": "mauve", - "violet": "mauve", - "iris": "slate", - "indigo": "slate", - "blue": "slate", - "sky": "slate", - "cyan": "slate", - "teal": "sage", - "jade": "sage", - "mint": "sage", - "green": "sage", - "grass": "sage", - "orange": "sand", - "amber": "sand", - "yellow": "sand", - "lime": "sand", - "brown": "sand", - "bronze": "sand", - "gold": "sand", - "gray": "gray", -} - -_RADIX_VALID_GRAYS = frozenset({"gray", "mauve", "slate", "sage", "olive", "sand"}) - - -def _radix_color_stylesheet(color: str) -> str: - """Return the granular CSS import path for a single Radix color scale. - - Args: - color: The Radix color name (e.g. ``"blue"``, ``"slate"``). - - Returns: - The Radix Themes CSS path for that color. - """ - return f"@radix-ui/themes/tokens/colors/{color}.css" - - -def _resolve_theme_color( - component: BaseComponent, prop_name: str -) -> tuple[str | None, bool]: - """Resolve a Theme color prop into either a literal or a dynamic marker. - - Args: - component: The Theme component being inspected. - prop_name: The color prop name (``"accent_color"`` or ``"gray_color"``). - - Returns: - A tuple ``(literal, is_dynamic)``. ``literal`` is the resolved string - when the prop is set to a static value; ``is_dynamic`` is True only - when the prop is set but cannot be resolved at compile time. - """ - raw = getattr(component, prop_name, None) - if raw is None: - return None, False - literal = get_python_literal(raw) - if isinstance(literal, str): - return literal, False - return None, True - - -def _walk_components(root: BaseComponent) -> Iterable[BaseComponent]: - """Yield ``root`` and every descendant reachable via ``.children``. - - Args: - root: The component subtree to walk. - - Yields: - Each component in the subtree, including ``root`` itself. - """ - stack: list[BaseComponent] = [root] - while stack: - node = stack.pop() - yield node - children = getattr(node, "children", None) - if children: - stack.extend(children) - - -def _collect_radix_theme_colors( - roots: Iterable[BaseComponent | None], -) -> tuple[set[str], set[str], bool]: - """Walk component trees for Theme components and collect their colors. - - Args: - roots: Component trees to walk for Theme components. - - Returns: - A tuple ``(accent_colors, gray_colors, needs_fallback)``. - ``needs_fallback`` is True if any Theme has a state-driven color - or an unrecognized color literal -- in either case the caller falls - back to the monolithic stylesheet, so a typo never resolves to a 404 - ``tokens/colors/.css`` import. - """ - accents: set[str] = set() - grays: set[str] = set() - needs_fallback = False - for root in roots: - if root is None: - continue - for node in _walk_components(root): - if getattr(node, "tag", None) != "Theme": - continue - accent, accent_dynamic = _resolve_theme_color(node, "accent_color") - gray, gray_dynamic = _resolve_theme_color(node, "gray_color") - if accent_dynamic or gray_dynamic: - needs_fallback = True - if accent is not None and accent not in _RADIX_ACCENT_TO_AUTO_GRAY: - needs_fallback = True - continue - if gray is not None and gray != "auto" and gray not in _RADIX_VALID_GRAYS: - needs_fallback = True - continue - if accent: - accents.add(accent) - if gray and gray != "auto": - grays.add(gray) - # When gray_color is unset or "auto", Radix pairs the accent with a - # natural gray scale -- ship that scale too. - if accent and (gray is None or gray == "auto"): - grays.add(_RADIX_ACCENT_TO_AUTO_GRAY[accent]) - return accents, grays, needs_fallback - - -def get_radix_themes_stylesheets( - roots: Iterable[BaseComponent | None] | None = None, -) -> list[str]: - """Return the Radix Themes stylesheets to import. - - Importing the granular per-color CSS files (tokens/base.css + - tokens/colors/.css + components.css + utilities.css) instead of - the monolithic ``styles.css`` lets the bundler drop the ~30 unused color - scales. Falls back to the monolithic stylesheet when ``roots`` is None, - when no static accent is detected, when any Theme uses a state-driven - color (so runtime color switches still work), or when a Theme references - a color name not recognized by the granular file layout (which would - otherwise resolve to a 404 ``tokens/colors/.css``). - - Args: - roots: Component trees to scan for Theme components. - - Returns: - Ordered list of stylesheet paths to import. - """ - if roots is None: - return [RADIX_THEMES_STYLESHEET] - accents, grays, needs_fallback = _collect_radix_theme_colors(roots) - if needs_fallback or not accents: - return [RADIX_THEMES_STYLESHEET] - sheets = [_RADIX_THEMES_TOKENS_BASE] - # An accent and its paired gray can be the same scale (e.g. accent_color="gray" - # auto-pairs with "gray"); subtract to avoid importing the same file twice. - sheets.extend(_radix_color_stylesheet(c) for c in sorted(grays - accents)) - sheets.extend(_radix_color_stylesheet(c) for c in sorted(accents)) - sheets.extend([_RADIX_THEMES_COMPONENTS, _RADIX_THEMES_UTILITIES]) - return sheets - @dataclasses.dataclass class RadixThemesPlugin(Plugin): @@ -216,15 +46,8 @@ def create_implicit(cls) -> RadixThemesPlugin: return cls(enabled=False, _explicit=False) def get_stylesheet_paths(self, **context: Any) -> tuple[str, ...]: - """Return the Radix Themes stylesheets when enabled. - - When ``theme_roots`` are supplied via context, only the color scales - actually referenced by ``Theme`` components are shipped (falling back - to the monolithic stylesheet when a color is state-driven). - """ - if not self.enabled: - return () - return tuple(get_radix_themes_stylesheets(context.get("theme_roots"))) + """Return the Radix Themes stylesheet when enabled.""" + return (RADIX_THEMES_STYLESHEET,) if self.enabled else () def get_frontend_dependencies(self, **context: Any) -> tuple[str, ...]: """Return the Radix Themes package when enabled.""" diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 1ca33b3e6a8..ebed56556b1 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -270,7 +270,6 @@ def compile_root_stylesheet( stylesheets: list[str], reset_style: bool = True, plugins: Sequence[Plugin] | None = None, - theme_roots: Sequence[BaseComponent | None] | None = None, ) -> tuple[str, str]: """Compile the root stylesheet. @@ -278,15 +277,13 @@ def compile_root_stylesheet( stylesheets: The stylesheets to include in the root stylesheet. reset_style: Whether to include CSS reset for margin and padding. plugins: The effective plugins for the active compile. - theme_roots: Component roots to scan for Theme components so only the - used Radix color scales are shipped. Returns: The path and code of the compiled root stylesheet. """ output_path = utils.get_root_stylesheet_path() - code = _compile_root_stylesheet(stylesheets, reset_style, plugins, theme_roots) + code = _compile_root_stylesheet(stylesheets, reset_style, plugins) return output_path, code @@ -330,7 +327,6 @@ def _compile_root_stylesheet( stylesheets: list[str], reset_style: bool = True, plugins: Sequence[Plugin] | None = None, - theme_roots: Sequence[BaseComponent | None] | None = None, ) -> str: """Compile the root stylesheet. @@ -338,8 +334,6 @@ def _compile_root_stylesheet( stylesheets: The stylesheets to include in the root stylesheet. reset_style: Whether to include CSS reset for margin and padding. plugins: The effective plugins for the active compile. - theme_roots: Component roots to scan for Theme components so only the - used Radix color scales are shipped. Returns: The compiled root stylesheet. @@ -356,11 +350,8 @@ def _compile_root_stylesheet( sheets.append(f"./{ResetStylesheet.FILENAME}") active_plugins = get_config().plugins if plugins is None else plugins - plugin_context: dict[str, Any] = {"theme_roots": theme_roots} sheets.extend([ - sheet - for plugin in active_plugins - for sheet in plugin.get_stylesheet_paths(**plugin_context) + sheet for plugin in active_plugins for sheet in plugin.get_stylesheet_paths() ]) failed_to_import_sass = False @@ -1162,11 +1153,6 @@ def compile_app( app_root_imports = app_root._get_all_imports() all_imports = utils.merge_imports(all_imports, app_root_imports) - theme_roots: list[BaseComponent | None] = [ - app_root, - *(p.root_component for p in compile_ctx.compiled_pages.values()), - ] - memo_component_files, memo_components_imports = compile_memo_components( ( *MEMOS.values(), @@ -1224,7 +1210,6 @@ def add_save_task( )), radix_themes_plugin=radix_themes_plugin, unevaluated_pages=list(app._unevaluated_pages.values()), - theme_roots=theme_roots, ) if save_tasks: @@ -1237,7 +1222,6 @@ def add_save_task( app.stylesheets, app.reset_style, plugins=compiler_plugins, - theme_roots=theme_roots, ) ) progress.advance(task) diff --git a/tests/units/compiler/test_compiler.py b/tests/units/compiler/test_compiler.py index f3898a40e8f..d9f0ff5c7e9 100644 --- a/tests/units/compiler/test_compiler.py +++ b/tests/units/compiler/test_compiler.py @@ -1,8 +1,6 @@ import importlib.util import os from pathlib import Path -from types import SimpleNamespace -from typing import TYPE_CHECKING, cast import pytest from pytest_mock import MockerFixture @@ -16,14 +14,10 @@ from reflex_components_core.base.document import Links, Scripts from reflex_components_core.el.elements.metadata import Head, Link, Meta from reflex_components_core.el.elements.other import Html -from reflex_components_radix.plugin import get_radix_themes_stylesheets import reflex as rx from reflex.compiler import compiler, utils -if TYPE_CHECKING: - from reflex_base.components.component import BaseComponent - @pytest.mark.parametrize( ("fields", "test_default", "test_rest"), @@ -441,95 +435,6 @@ def test_compile_nonexistent_stylesheet(tmp_path, mocker: MockerFixture): compiler.compile_root_stylesheet(stylesheets) -def test_radix_themes_stylesheets_no_roots_falls_back_to_monolith(): - """When no roots are provided, use the monolithic stylesheet.""" - assert get_radix_themes_stylesheets(None) == ["@radix-ui/themes/styles.css"] - - -def test_radix_themes_stylesheets_literal_accent_emits_granular_imports(): - """A literal accent_color emits only the needed granular imports.""" - sheets = get_radix_themes_stylesheets([rx.theme(accent_color="blue")]) - assert sheets == [ - "@radix-ui/themes/tokens/base.css", - # blue's natural gray pairing is slate - "@radix-ui/themes/tokens/colors/slate.css", - "@radix-ui/themes/tokens/colors/blue.css", - "@radix-ui/themes/components.css", - "@radix-ui/themes/utilities.css", - ] - - -def test_radix_themes_stylesheets_explicit_gray_overrides_auto_pairing(): - """An explicit gray_color replaces the accent's auto-paired gray.""" - sheets = get_radix_themes_stylesheets([ - rx.theme(accent_color="red", gray_color="mauve") - ]) - assert "@radix-ui/themes/tokens/colors/mauve.css" in sheets - assert "@radix-ui/themes/tokens/colors/red.css" in sheets - # The default auto pairing for red is also mauve, so no extra colors. - color_sheets = [s for s in sheets if "/colors/" in s] - assert len(color_sheets) == 2 - - -def test_radix_themes_stylesheets_gray_accent_no_duplicate_import(): - """accent_color='gray' auto-pairs with 'gray' -- emit gray.css only once.""" - sheets = get_radix_themes_stylesheets([rx.theme(accent_color="gray")]) - assert sheets.count("@radix-ui/themes/tokens/colors/gray.css") == 1 - # And with an explicit matching gray_color too. - sheets = get_radix_themes_stylesheets([ - rx.theme(accent_color="gray", gray_color="gray") - ]) - assert sheets.count("@radix-ui/themes/tokens/colors/gray.css") == 1 - - -def test_radix_themes_stylesheets_nested_themes_union_colors(): - """Nested Theme components contribute the union of their colors.""" - root = rx.box( - rx.theme(accent_color="green"), - rx.theme(accent_color="pink"), - ) - sheets = get_radix_themes_stylesheets([root]) - color_sheets = {s for s in sheets if "/colors/" in s} - assert "@radix-ui/themes/tokens/colors/green.css" in color_sheets - assert "@radix-ui/themes/tokens/colors/pink.css" in color_sheets - - -def test_radix_themes_stylesheets_dynamic_color_falls_back_to_monolith(): - """A state-driven Theme color forces the monolithic stylesheet.""" - from typing import Literal - - class _S(rx.State): - color: Literal["red", "blue"] = "red" - - sheets = get_radix_themes_stylesheets([rx.theme(accent_color=_S.color)]) - assert sheets == ["@radix-ui/themes/styles.css"] - - -def test_radix_themes_stylesheets_unknown_color_falls_back_to_monolith(): - """Defensive: an unrecognized accent color (e.g. Radix adds new ones that - don't map to a granular file in our pinned layout) falls back to the - monolithic stylesheet instead of emitting a 404 ``tokens/colors/X.css``. - """ - fake_theme = cast( - "BaseComponent", - SimpleNamespace( - tag="Theme", accent_color="paprika", gray_color=None, children=() - ), - ) - assert get_radix_themes_stylesheets([fake_theme]) == ["@radix-ui/themes/styles.css"] - - -def test_radix_themes_stylesheets_unknown_gray_falls_back_to_monolith(): - """Same defense for an unrecognized gray color.""" - fake_theme = cast( - "BaseComponent", - SimpleNamespace( - tag="Theme", accent_color="blue", gray_color="taupe", children=() - ), - ) - assert get_radix_themes_stylesheets([fake_theme]) == ["@radix-ui/themes/styles.css"] - - @pytest.fixture def _isolate_dynamic_imports(): """Reset window-import state so each test sees only its own bundled libs.""" diff --git a/tests/units/test_app.py b/tests/units/test_app.py index a6700ea7b3c..2461ce18afc 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -2160,7 +2160,7 @@ def test_compile_with_radix_component_auto_enables_radix_plugin( web_dir / constants.Dirs.PAGES / constants.PageNames.APP_ROOT ).read_text() - assert "@radix-ui/themes/tokens/colors/blue.css" in root_stylesheet + assert "@radix-ui/themes/styles.css" in root_stylesheet assert 'RadixThemesTheme,{accentColor:"blue"' in app_root mock_deprecate.assert_called_once() assert ( @@ -2194,7 +2194,7 @@ def test_compile_with_legacy_app_theme_warns_and_enables_radix_plugin( web_dir / constants.Dirs.PAGES / constants.PageNames.APP_ROOT ).read_text() - assert "@radix-ui/themes/tokens/colors/plum.css" in root_stylesheet + assert "@radix-ui/themes/styles.css" in root_stylesheet assert 'RadixThemesTheme,{accentColor:"plum"' in app_root mock_deprecate.assert_called_once() assert mock_deprecate.call_args.kwargs["feature_name"] == "App(theme=...)"