瀏覽代碼

move tailwind to its own module

Khaleel Al-Adhami 1 月之前
父節點
當前提交
cf6de367fd

+ 1 - 1
pyi_hashes.json

@@ -56,7 +56,7 @@
   "reflex/components/radix/primitives/progress.pyi": "c62a0c44e0d440701174fcca93bf8fbe",
   "reflex/components/radix/primitives/progress.pyi": "c62a0c44e0d440701174fcca93bf8fbe",
   "reflex/components/radix/primitives/slider.pyi": "10196fb967c9cde3860a930a526b6c51",
   "reflex/components/radix/primitives/slider.pyi": "10196fb967c9cde3860a930a526b6c51",
   "reflex/components/radix/themes/__init__.pyi": "a15f9464ad99f248249ffa8e6deea4cf",
   "reflex/components/radix/themes/__init__.pyi": "a15f9464ad99f248249ffa8e6deea4cf",
-  "reflex/components/radix/themes/base.pyi": "a3c3c3b72fd3d8f1e38990e5c461b682",
+  "reflex/components/radix/themes/base.pyi": "8487120ce233ad4a81cb8d03a004d7f2",
   "reflex/components/radix/themes/color_mode.pyi": "e18fe42952d10f5733f3baf4789c4bb5",
   "reflex/components/radix/themes/color_mode.pyi": "e18fe42952d10f5733f3baf4789c4bb5",
   "reflex/components/radix/themes/components/__init__.pyi": "87bb9ffff641928562da1622d2ca5993",
   "reflex/components/radix/themes/components/__init__.pyi": "87bb9ffff641928562da1622d2ca5993",
   "reflex/components/radix/themes/components/alert_dialog.pyi": "8e1dde62450296310a116ed066bd51e3",
   "reflex/components/radix/themes/components/alert_dialog.pyi": "8e1dde62450296310a116ed066bd51e3",

+ 0 - 66
reflex/.templates/jinja/web/tailwind.config.js.jinja2

@@ -1,66 +0,0 @@
-{# Helper macro to render JS objects and arrays #}
-{% macro render_js(val, indent=2, level=0) -%}
-{%- set space = ' ' * (indent * level) -%}
-{%- set next_space = ' ' * (indent * (level + 1)) -%}
-
-{%- if val is mapping -%}
-{
-{%- for k, v in val.items() %}
-{{ next_space }}{{ k if k is string and k.isidentifier() else k|tojson }}: {{ render_js(v, indent, level + 1) }}{{ "," if not loop.last }}
-{%- endfor %}
-{{ space }}}
-{%- elif val is iterable and val is not string -%}
-[
-{%- for item in val %}
-{{ next_space }}{{ render_js(item, indent, level + 1) }}{{ "," if not loop.last }}
-{%- endfor %}
-{{ space }}]
-{%- else -%}
-{{ val | tojson }}
-{%- endif -%}
-{%- endmacro %}
-
-{# Extract destructured imports from plugin dicts only #}
-{%- set imports = [] %}
-{%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
-  {%- set _ = imports.append(plugin.import) %}
-{%- endfor %}
-
-/** @type {import('tailwindcss').Config} */
-{%- for imp in imports %}
-const { {{ imp.name }} } = require({{ imp.from | tojson }});  
-{%- endfor %}
-
-module.exports = {
-  content: {{ render_js(content) }},
-  theme: {{ render_js(theme) }},
-  {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
-  {% if corePlugins is defined %}corePlugins: {{ render_js(corePlugins) }},{% endif %}
-  {% if important is defined %}important: {{ important | tojson }},{% endif %}
-  {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
-  {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
-  {% if presets is defined %}
-  presets: [
-    {% for preset in presets %}
-    require({{ preset | tojson }}){{ "," if not loop.last }}
-    {% endfor %}
-  ],
-  {% endif %}
-  plugins: [
-    {% for plugin in plugins %}
-      {% if plugin is mapping %}
-        {% if plugin.call is defined %}
-          {{ plugin.call }}(
-            {%- if plugin.args is defined -%}
-              {{ render_js(plugin.args) }}
-            {%- endif -%}
-          ){{ "," if not loop.last }}
-        {% else %}
-          require({{ plugin.name | tojson }}){{ "," if not loop.last }}
-        {% endif %}
-      {% else %}
-        require({{ plugin | tojson }}){{ "," if not loop.last }}
-      {% endif %}
-    {% endfor %}
-  ]
-};

+ 0 - 1
reflex/.templates/web/postcss.config.js

@@ -1,7 +1,6 @@
 module.exports = {
 module.exports = {
   plugins: {
   plugins: {
     "postcss-import": {},
     "postcss-import": {},
-    tailwindcss: {},
     autoprefixer: {},
     autoprefixer: {},
   },
   },
 };
 };

+ 0 - 6
reflex/.templates/web/styles/tailwind.css

@@ -1,6 +0,0 @@
-@import "tailwindcss/base";
-
-@import "@radix-ui/themes/styles.css";
-
-@tailwind components;
-@tailwind utilities;

+ 19 - 15
reflex/app.py

@@ -18,7 +18,7 @@ from datetime import datetime
 from pathlib import Path
 from pathlib import Path
 from timeit import default_timer as timer
 from timeit import default_timer as timer
 from types import SimpleNamespace
 from types import SimpleNamespace
-from typing import TYPE_CHECKING, Any, BinaryIO, get_args, get_type_hints
+from typing import TYPE_CHECKING, Any, BinaryIO, ParamSpec, get_args, get_type_hints
 
 
 from fastapi import FastAPI, HTTPException, Request
 from fastapi import FastAPI, HTTPException, Request
 from fastapi import UploadFile as FastAPIUploadFile
 from fastapi import UploadFile as FastAPIUploadFile
@@ -308,6 +308,9 @@ class UnevaluatedPage:
         )
         )
 
 
 
 
+P = ParamSpec("P")
+
+
 @dataclasses.dataclass()
 @dataclasses.dataclass()
 class App(MiddlewareMixin, LifespanMixin):
 class App(MiddlewareMixin, LifespanMixin):
     """The main Reflex app that encapsulates the backend and frontend.
     """The main Reflex app that encapsulates the backend and frontend.
@@ -961,11 +964,6 @@ class App(MiddlewareMixin, LifespanMixin):
         frontend_packages = get_config().frontend_packages
         frontend_packages = get_config().frontend_packages
         _frontend_packages = []
         _frontend_packages = []
         for package in frontend_packages:
         for package in frontend_packages:
-            if package in (get_config().tailwind or {}).get("plugins", []):
-                console.warn(
-                    f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`"
-                )
-                continue
             if package in page_imports:
             if package in page_imports:
                 console.warn(
                 console.warn(
                     f"React packages and their dependencies are inferred from Component.library and Component.lib_dependencies, remove `{package}` from `frontend_packages`"
                     f"React packages and their dependencies are inferred from Component.library and Component.lib_dependencies, remove `{package}` from `frontend_packages`"
@@ -1312,6 +1310,16 @@ class App(MiddlewareMixin, LifespanMixin):
                     ),
                     ),
                 )
                 )
 
 
+        for plugin in config.plugins:
+            for static_file_path, content in plugin.get_static_assets():
+                resulting_path = (
+                    Path.cwd() / prerequisites.get_web_dir() / static_file_path
+                )
+                if isinstance(content, str):
+                    resulting_path.write_text(content)
+                else:
+                    resulting_path.write_bytes(content)
+
         executor = ExecutorType.get_executor_from_environment()
         executor = ExecutorType.get_executor_from_environment()
 
 
         for route, component in zip(self._pages, page_components, strict=True):
         for route, component in zip(self._pages, page_components, strict=True):
@@ -1322,7 +1330,9 @@ class App(MiddlewareMixin, LifespanMixin):
         with console.timing("Compile to Javascript"), executor as executor:
         with console.timing("Compile to Javascript"), executor as executor:
             result_futures: list[concurrent.futures.Future[tuple[str, str]]] = []
             result_futures: list[concurrent.futures.Future[tuple[str, str]]] = []
 
 
-            def _submit_work(fn: Callable[..., tuple[str, str]], *args, **kwargs):
+            def _submit_work(
+                fn: Callable[P, tuple[str, str]], *args: P.args, **kwargs: P.kwargs
+            ):
                 f = executor.submit(fn, *args, **kwargs)
                 f = executor.submit(fn, *args, **kwargs)
                 f.add_done_callback(lambda _: progress.advance(task))
                 f.add_done_callback(lambda _: progress.advance(task))
                 result_futures.append(f)
                 result_futures.append(f)
@@ -1340,14 +1350,8 @@ class App(MiddlewareMixin, LifespanMixin):
             # Compile the theme.
             # Compile the theme.
             _submit_work(compile_theme, self.style)
             _submit_work(compile_theme, self.style)
 
 
-            # Compile the Tailwind config.
-            if config.tailwind is not None:
-                config.tailwind["content"] = config.tailwind.get(
-                    "content", constants.Tailwind.CONTENT
-                )
-                _submit_work(compiler.compile_tailwind, config.tailwind)
-            else:
-                _submit_work(compiler.remove_tailwind_from_postcss)
+            for plugin in config.plugins:
+                plugin.pre_compile(add_task=_submit_work)
 
 
             # Wait for all compilation tasks to complete.
             # Wait for all compilation tasks to complete.
             compile_results.extend(
             compile_results.extend(

+ 6 - 60
reflex/compiler/compiler.py

@@ -225,12 +225,12 @@ def _compile_root_stylesheet(stylesheets: list[str]) -> str:
     Raises:
     Raises:
         FileNotFoundError: If a specified stylesheet in assets directory does not exist.
         FileNotFoundError: If a specified stylesheet in assets directory does not exist.
     """
     """
-    # Add tailwind css if enabled.
-    sheets = (
-        [constants.Tailwind.ROOT_STYLE_PATH]
-        if get_config().tailwind is not None
-        else []
-    )
+    # Add stylesheets from plugins.
+    sheets = [
+        sheet
+        for plugin in get_config().plugins
+        for sheet in plugin.get_stylesheet_paths()
+    ] + ["@radix-ui/themes/styles.css"]
 
 
     failed_to_import_sass = False
     failed_to_import_sass = False
     assets_app_path = Path.cwd() / constants.Dirs.APP_ASSETS
     assets_app_path = Path.cwd() / constants.Dirs.APP_ASSETS
@@ -437,22 +437,6 @@ def _compile_stateful_components(
     )
     )
 
 
 
 
-def _compile_tailwind(
-    config: dict,
-) -> str:
-    """Compile the Tailwind config.
-
-    Args:
-        config: The Tailwind config.
-
-    Returns:
-        The compiled Tailwind config.
-    """
-    return templates.TAILWIND_CONFIG.render(
-        **config,
-    )
-
-
 def compile_document_root(
 def compile_document_root(
     head_components: list[Component],
     head_components: list[Component],
     html_lang: str | None = None,
     html_lang: str | None = None,
@@ -599,44 +583,6 @@ def compile_stateful_components(
     return output_path, code, page_components
     return output_path, code, page_components
 
 
 
 
-def compile_tailwind(
-    config: dict,
-):
-    """Compile the Tailwind config.
-
-    Args:
-        config: The Tailwind config.
-
-    Returns:
-        The compiled Tailwind config.
-    """
-    # Get the path for the output file.
-    output_path = str((get_web_dir() / constants.Tailwind.CONFIG).absolute())
-
-    # Compile the config.
-    code = _compile_tailwind(config)
-    return output_path, code
-
-
-def remove_tailwind_from_postcss() -> tuple[str, str]:
-    """If tailwind is not to be used, remove it from postcss.config.js.
-
-    Returns:
-        The path and code of the compiled postcss.config.js.
-    """
-    # Get the path for the output file.
-    output_path = str(get_web_dir() / constants.Dirs.POSTCSS_JS)
-
-    code = [
-        line
-        for line in Path(output_path).read_text().splitlines(keepends=True)
-        if "tailwindcss: " not in line
-    ]
-
-    # Compile the config.
-    return output_path, "".join(code)
-
-
 def purge_web_pages_dir():
 def purge_web_pages_dir():
     """Empty out .web/pages directory."""
     """Empty out .web/pages directory."""
     if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR.get():
     if not is_prod_mode() and environment.REFLEX_PERSIST_WEB_DIR.get():

+ 12 - 3
reflex/compiler/templates.py

@@ -98,6 +98,18 @@ def get_template(name: str) -> Template:
     return ReflexJinjaEnvironment().get_template(name=name)
     return ReflexJinjaEnvironment().get_template(name=name)
 
 
 
 
+def from_string(source: str) -> Template:
+    """Get render function that work with a template.
+
+    Args:
+        source: The template source.
+
+    Returns:
+        A render function.
+    """
+    return ReflexJinjaEnvironment().from_string(source=source)
+
+
 # Template for the Reflex config file.
 # Template for the Reflex config file.
 RXCONFIG = get_template("app/rxconfig.py.jinja2")
 RXCONFIG = get_template("app/rxconfig.py.jinja2")
 
 
@@ -113,9 +125,6 @@ THEME = get_template("web/utils/theme.js.jinja2")
 # Template for the context file.
 # Template for the context file.
 CONTEXT = get_template("web/utils/context.js.jinja2")
 CONTEXT = get_template("web/utils/context.js.jinja2")
 
 
-# Template for Tailwind config.
-TAILWIND_CONFIG = get_template("web/tailwind.config.js.jinja2")
-
 # Template to render a component tag.
 # Template to render a component tag.
 COMPONENT = get_template("web/pages/component.js.jinja2")
 COMPONENT = get_template("web/pages/component.js.jinja2")
 
 

+ 1 - 10
reflex/components/radix/themes/base.py

@@ -7,7 +7,6 @@ from typing import Any, ClassVar, Literal
 from reflex.components import Component
 from reflex.components import Component
 from reflex.components.core.breakpoints import Responsive
 from reflex.components.core.breakpoints import Responsive
 from reflex.components.tags import Tag
 from reflex.components.tags import Tag
-from reflex.config import get_config
 from reflex.utils.imports import ImportDict, ImportVar
 from reflex.utils.imports import ImportDict, ImportVar
 from reflex.vars.base import Var
 from reflex.vars.base import Var
 
 
@@ -239,17 +238,9 @@ class Theme(RadixThemesComponent):
         Returns:
         Returns:
             The import dict.
             The import dict.
         """
         """
-        _imports: ImportDict = {
+        return {
             "$/utils/theme.js": [ImportVar(tag="theme", is_default=True)],
             "$/utils/theme.js": [ImportVar(tag="theme", is_default=True)],
         }
         }
-        if get_config().tailwind is None:
-            # When tailwind is disabled, import the radix-ui styles directly because they will
-            # not be included in the tailwind.css file.
-            _imports[""] = ImportVar(
-                tag="@radix-ui/themes/styles.css",
-                install=False,
-            )
-        return _imports
 
 
     def _render(self, props: dict[str, Any] | None = None) -> Tag:
     def _render(self, props: dict[str, Any] | None = None) -> Tag:
         tag = super()._render(props)
         tag = super()._render(props)

+ 7 - 0
reflex/config.py

@@ -34,6 +34,8 @@ import pydantic.v1 as pydantic
 from reflex import constants
 from reflex import constants
 from reflex.base import Base
 from reflex.base import Base
 from reflex.constants.base import LogLevel
 from reflex.constants.base import LogLevel
+from reflex.plugins.base import Plugin
+from reflex.plugins.tailwind_v3 import TailwindV3Plugin
 from reflex.utils import console
 from reflex.utils import console
 from reflex.utils.exceptions import ConfigError, EnvironmentVarValueError
 from reflex.utils.exceptions import ConfigError, EnvironmentVarValueError
 from reflex.utils.types import (
 from reflex.utils.types import (
@@ -888,6 +890,8 @@ class Config(Base):
     # Extra overlay function to run after the app is built. Formatted such that `from path_0.path_1... import path[-1]`, and calling it with no arguments would work. For example, "reflex.components.moment.moment".
     # Extra overlay function to run after the app is built. Formatted such that `from path_0.path_1... import path[-1]`, and calling it with no arguments would work. For example, "reflex.components.moment.moment".
     extra_overlay_function: str | None = None
     extra_overlay_function: str | None = None
 
 
+    plugins: list[Plugin] = []
+
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         """Initialize the config values.
         """Initialize the config values.
 
 
@@ -917,6 +921,9 @@ class Config(Base):
         self._non_default_attributes.update(kwargs)
         self._non_default_attributes.update(kwargs)
         self._replace_defaults(**kwargs)
         self._replace_defaults(**kwargs)
 
 
+        if self.tailwind is not None:
+            self.plugins.append(TailwindV3Plugin())
+
         if (
         if (
             self.state_manager_mode == constants.StateManagerMode.REDIS
             self.state_manager_mode == constants.StateManagerMode.REDIS
             and not self.redis_url
             and not self.redis_url

+ 0 - 2
reflex/constants/__init__.py

@@ -58,7 +58,6 @@ from .route import (
     RouteVar,
     RouteVar,
 )
 )
 from .state import StateManagerMode
 from .state import StateManagerMode
-from .style import Tailwind
 
 
 __all__ = [
 __all__ = [
     "ALEMBIC_CONFIG",
     "ALEMBIC_CONFIG",
@@ -113,6 +112,5 @@ __all__ = [
     "RouteVar",
     "RouteVar",
     "SocketEvent",
     "SocketEvent",
     "StateManagerMode",
     "StateManagerMode",
-    "Tailwind",
     "Templates",
     "Templates",
 ]
 ]

+ 0 - 16
reflex/constants/style.py

@@ -1,16 +0,0 @@
-"""Style constants."""
-
-from types import SimpleNamespace
-
-
-class Tailwind(SimpleNamespace):
-    """Tailwind constants."""
-
-    # The Tailwindcss version
-    VERSION = "tailwindcss@3.4.17"
-    # The Tailwind config.
-    CONFIG = "tailwind.config.js"
-    # Default Tailwind content paths
-    CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"]
-    # Relative tailwind style path to root stylesheet in Dirs.STYLES.
-    ROOT_STYLE_PATH = "./tailwind.css"

+ 1 - 0
reflex/plugins/__init__.py

@@ -0,0 +1 @@
+"""Reflex Plugin System."""

+ 99 - 0
reflex/plugins/base.py

@@ -0,0 +1,99 @@
+"""Base class for all plugins."""
+
+from collections.abc import Callable, Sequence
+from pathlib import Path
+from typing import ParamSpec, Protocol, TypedDict, TypeVarTuple
+
+from typing_extensions import Unpack
+
+
+class CommonContext(TypedDict):
+    """Common context for all plugins."""
+
+
+P = ParamSpec("P")
+
+
+class AddTaskProtcol(Protocol):
+    """Protocol for adding a task to the pre-compile context."""
+
+    def __call__(
+        self, task: Callable[P, tuple[str, str]], /, *args: P.args, **kwargs: P.kwargs
+    ) -> None:
+        """Add a task to the pre-compile context.
+
+        Args:
+            task: The task to add.
+            args: The arguments to pass to the task
+            kwargs: The keyword arguments to pass to the task
+        """
+
+
+class PreCompileContext(CommonContext):
+    """Context for pre-compile hooks."""
+
+    add_task: AddTaskProtcol
+
+
+Types = TypeVarTuple("Types")
+
+
+class Plugin:
+    """Base class for all plugins."""
+
+    def get_frontend_development_dependancies(
+        self, **context: Unpack[CommonContext]
+    ) -> list[str] | set[str] | tuple[str, ...]:
+        """Get the NPM packages required by the plugin for development.
+
+        Args:
+            context: The context for the plugin.
+
+        Returns:
+            A list of packages required by the plugin for development.
+        """
+        return []
+
+    def get_frontend_dependancies(
+        self, **context: Unpack[CommonContext]
+    ) -> list[str] | set[str] | tuple[str, ...]:
+        """Get the NPM packages required by the plugin.
+
+        Args:
+            context: The context for the plugin.
+
+        Returns:
+            A list of packages required by the plugin.
+        """
+        return []
+
+    def get_static_assets(
+        self, **context: Unpack[CommonContext]
+    ) -> Sequence[tuple[Path, str | bytes]]:
+        """Get the static assets required by the plugin.
+
+        Args:
+            context: The context for the plugin.
+
+        Returns:
+            A list of static assets required by the plugin.
+        """
+        return []
+
+    def get_stylesheet_paths(self, **context: Unpack[CommonContext]) -> Sequence[str]:
+        """Get the paths to the stylesheets required by the plugin relative to the styles directory.
+
+        Args:
+            context: The context for the plugin.
+
+        Returns:
+            A list of paths to the stylesheets required by the plugin.
+        """
+        return []
+
+    def pre_compile(self, **context: Unpack[PreCompileContext]) -> None:
+        """Called before the compilation of the plugin.
+
+        Args:
+            context: The context for the plugin.
+        """

+ 233 - 0
reflex/plugins/tailwind_v3.py

@@ -0,0 +1,233 @@
+"""Base class for all plugins."""
+
+from pathlib import Path
+from types import SimpleNamespace
+
+from reflex.plugins.base import Plugin
+from reflex.utils.decorator import once
+
+
+class Constants(SimpleNamespace):
+    """Tailwind constants."""
+
+    # The Tailwindcss version
+    VERSION = "tailwindcss@3.4.17"
+    # The Tailwind config.
+    CONFIG = "tailwind.config.js"
+    # Default Tailwind content paths
+    CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"]
+    # Relative tailwind style path to root stylesheet in Dirs.STYLES.
+    ROOT_STYLE_PATH = "./tailwind.css"
+
+    # The default tailwind css.
+    TAILWIND_CSS = """
+@import "tailwindcss/base";
+
+@tailwind components;
+@tailwind utilities;
+"""
+
+
+@once
+def tailwind_config_js_template():
+    """Get the Tailwind config template.
+
+    Returns:
+        The Tailwind config template.
+    """
+    from reflex.compiler.templates import from_string
+
+    source = """
+{# Helper macro to render JS objects and arrays #}
+{% macro render_js(val, indent=2, level=0) -%}
+{%- set space = ' ' * (indent * level) -%}
+{%- set next_space = ' ' * (indent * (level + 1)) -%}
+
+{%- if val is mapping -%}
+{
+{%- for k, v in val.items() %}
+{{ next_space }}{{ k if k is string and k.isidentifier() else k|tojson }}: {{ render_js(v, indent, level + 1) }}{{ "," if not loop.last }}
+{%- endfor %}
+{{ space }}}
+{%- elif val is iterable and val is not string -%}
+[
+{%- for item in val %}
+{{ next_space }}{{ render_js(item, indent, level + 1) }}{{ "," if not loop.last }}
+{%- endfor %}
+{{ space }}]
+{%- else -%}
+{{ val | tojson }}
+{%- endif -%}
+{%- endmacro %}
+
+{# Extract destructured imports from plugin dicts only #}
+{%- set imports = [] %}
+{%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
+  {%- set _ = imports.append(plugin.import) %}
+{%- endfor %}
+
+/** @type {import('tailwindcss').Config} */
+{%- for imp in imports %}
+const { {{ imp.name }} } = require({{ imp.from | tojson }});
+{%- endfor %}
+
+module.exports = {
+  content: {{ render_js(content) }},
+  theme: {{ render_js(theme) }},
+  {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
+  {% if corePlugins is defined %}corePlugins: {{ render_js(corePlugins) }},{% endif %}
+  {% if important is defined %}important: {{ important | tojson }},{% endif %}
+  {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
+  {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
+  {% if presets is defined %}
+  presets: [
+    {% for preset in presets %}
+    require({{ preset | tojson }}){{ "," if not loop.last }}
+    {% endfor %}
+  ],
+  {% endif %}
+  plugins: [
+    {% for plugin in plugins %}
+      {% if plugin is mapping %}
+        {% if plugin.call is defined %}
+          {{ plugin.call }}(
+            {%- if plugin.args is defined -%}
+              {{ render_js(plugin.args) }}
+            {%- endif -%}
+          ){{ "," if not loop.last }}
+        {% else %}
+          require({{ plugin.name | tojson }}){{ "," if not loop.last }}
+        {% endif %}
+      {% else %}
+        require({{ plugin | tojson }}){{ "," if not loop.last }}
+      {% endif %}
+    {% endfor %}
+  ]
+};
+"""
+
+    return from_string(source)
+
+
+def _compile_tailwind(
+    config: dict,
+) -> str:
+    """Compile the Tailwind config.
+
+    Args:
+        config: The Tailwind config.
+
+    Returns:
+        The compiled Tailwind config.
+    """
+    return tailwind_config_js_template().render(
+        **config,
+    )
+
+
+def compile_tailwind(
+    config: dict,
+):
+    """Compile the Tailwind config.
+
+    Args:
+        config: The Tailwind config.
+
+    Returns:
+        The compiled Tailwind config.
+    """
+    from reflex.utils.prerequisites import get_web_dir
+
+    # Get the path for the output file.
+    output_path = str((get_web_dir() / Constants.CONFIG).absolute())
+
+    # Compile the config.
+    code = _compile_tailwind(config)
+    return output_path, code
+
+
+def add_tailwind_to_postcss_config():
+    """Add tailwind to the postcss config."""
+    from reflex.constants import Dirs
+    from reflex.utils.prerequisites import get_web_dir
+
+    postcss_file = get_web_dir() / Dirs.POSTCSS_JS
+    if not postcss_file.exists():
+        raise ValueError(
+            f"Could not find {Dirs.POSTCSS_JS}. "
+            "Please make sure the file exists and is valid."
+        )
+
+    postcss_file_lines = postcss_file.read_text().splitlines()
+
+    line_with_postcss_plugins = next(
+        (
+            i
+            for i, line in enumerate(postcss_file_lines)
+            if line.strip().startswith("plugins")
+        ),
+        None,
+    )
+    if not line_with_postcss_plugins:
+        raise ValueError(
+            f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. "
+            "Please make sure the file exists and is valid."
+        )
+
+    postcss_file_lines.insert(line_with_postcss_plugins + 1, "tailwindcss: {},")
+
+    return str(postcss_file), "\n".join(postcss_file_lines)
+
+
+class TailwindV3Plugin(Plugin):
+    """Plugin for Tailwind CSS."""
+
+    def get_frontend_development_dependancies(self, **context) -> list[str]:
+        """Get the packages required by the plugin.
+
+        Returns:
+            A list of packages required by the plugin.
+        """
+        from reflex.config import get_config
+
+        config = get_config()
+        return [
+            plugin if isinstance(plugin, str) else plugin.get("name")
+            for plugin in (config.tailwind or {}).get("plugins", [])
+        ] + [Constants.VERSION]
+
+    def get_static_assets(self, **context):
+        """Get the static assets required by the plugin.
+
+        Args:
+            context: The context for the plugin.
+
+        Returns:
+            A list of static assets required by the plugin.
+        """
+        return [(Path("styles/tailwind.css"), Constants.TAILWIND_CSS)]
+
+    def get_stylesheet_paths(self, **context) -> list[str]:
+        """Get the paths to the stylesheets required by the plugin relative to the styles directory.
+
+        Args:
+            context: The context for the plugin.
+
+        Returns:
+            A list of paths to the stylesheets required by the plugin.
+        """
+        return [Constants.ROOT_STYLE_PATH]
+
+    def pre_compile(self, **context):
+        """Pre-compile the plugin.
+
+        Args:
+            context: The context for the plugin.
+        """
+        from reflex.config import get_config
+
+        config = get_config().tailwind or {}
+
+        config["content"] = config.get("content", Constants.CONTENT)
+        context["add_task"](compile_tailwind, config)
+        context["add_task"](add_tailwind_to_postcss_config)

+ 18 - 87
reflex/utils/prerequisites.py

@@ -17,7 +17,6 @@ import re
 import shutil
 import shutil
 import sys
 import sys
 import tempfile
 import tempfile
-import time
 import typing
 import typing
 import zipfile
 import zipfile
 from collections.abc import Callable, Sequence
 from collections.abc import Callable, Sequence
@@ -40,10 +39,7 @@ from reflex.compiler import templates
 from reflex.config import Config, environment, get_config
 from reflex.config import Config, environment, get_config
 from reflex.utils import console, net, path_ops, processes
 from reflex.utils import console, net, path_ops, processes
 from reflex.utils.decorator import once
 from reflex.utils.decorator import once
-from reflex.utils.exceptions import (
-    GeneratedCodeHasNoFunctionDefsError,
-    SystemPackageMissingError,
-)
+from reflex.utils.exceptions import SystemPackageMissingError
 from reflex.utils.format import format_library_name
 from reflex.utils.format import format_library_name
 from reflex.utils.registry import get_npm_registry
 from reflex.utils.registry import get_npm_registry
 
 
@@ -1306,47 +1302,42 @@ def install_frontend_packages(packages: set[str], config: Config):
     primary_package_manager = install_package_managers[0]
     primary_package_manager = install_package_managers[0]
     fallbacks = install_package_managers[1:]
     fallbacks = install_package_managers[1:]
 
 
-    processes.run_process_with_fallbacks(
-        [primary_package_manager, "install", "--legacy-peer-deps"],
+    run_package_manager = functools.partial(
+        processes.run_process_with_fallbacks,
         fallbacks=fallbacks,
         fallbacks=fallbacks,
         analytics_enabled=True,
         analytics_enabled=True,
-        show_status_message="Installing base frontend packages",
         cwd=get_web_dir(),
         cwd=get_web_dir(),
         shell=constants.IS_WINDOWS,
         shell=constants.IS_WINDOWS,
         env=env,
         env=env,
     )
     )
 
 
-    if config.tailwind is not None:
-        processes.run_process_with_fallbacks(
+    run_package_manager(
+        [primary_package_manager, "install", "--legacy-peer-deps"],
+        show_status_message="Installing base frontend packages",
+    )
+
+    development_deps: set[str] = set()
+    for plugin in config.plugins:
+        development_deps.update(plugin.get_frontend_development_dependancies())
+        packages.update(plugin.get_frontend_dependancies())
+
+    if development_deps:
+        run_package_manager(
             [
             [
                 primary_package_manager,
                 primary_package_manager,
                 "add",
                 "add",
                 "--legacy-peer-deps",
                 "--legacy-peer-deps",
                 "-d",
                 "-d",
-                constants.Tailwind.VERSION,
-                *[
-                    plugin if isinstance(plugin, str) else plugin.get("name")
-                    for plugin in (config.tailwind or {}).get("plugins", [])
-                ],
+                *development_deps,
             ],
             ],
-            fallbacks=fallbacks,
-            analytics_enabled=True,
-            show_status_message="Installing tailwind",
-            cwd=get_web_dir(),
-            shell=constants.IS_WINDOWS,
-            env=env,
+            show_status_message="Installing frontend development dependencies",
         )
         )
 
 
     # Install custom packages defined in frontend_packages
     # Install custom packages defined in frontend_packages
     if len(packages) > 0:
     if len(packages) > 0:
-        processes.run_process_with_fallbacks(
+        run_package_manager(
             [primary_package_manager, "add", "--legacy-peer-deps", *packages],
             [primary_package_manager, "add", "--legacy-peer-deps", *packages],
-            fallbacks=fallbacks,
-            analytics_enabled=True,
             show_status_message="Installing frontend packages from config and components",
             show_status_message="Installing frontend packages from config and components",
-            cwd=get_web_dir(),
-            shell=constants.IS_WINDOWS,
-            env=env,
         )
         )
 
 
 
 
@@ -1932,66 +1923,6 @@ def get_init_cli_prompt_options() -> list[Template]:
     ]
     ]
 
 
 
 
-def initialize_main_module_index_from_generation(app_name: str, generation_hash: str):
-    """Overwrite the `index` function in the main module with reflex.build generated code.
-
-    Args:
-        app_name: The name of the app.
-        generation_hash: The generation hash from reflex.build.
-
-    Raises:
-        GeneratedCodeHasNoFunctionDefsError: If the fetched code has no function definitions
-            (the refactored reflex code is expected to have at least one root function defined).
-    """
-    # Download the reflex code for the generation.
-    url = constants.Templates.REFLEX_BUILD_CODE_URL.format(
-        generation_hash=generation_hash
-    )
-    resp = net.get(url)
-    while resp.status_code == httpx.codes.SERVICE_UNAVAILABLE:
-        console.debug("Waiting for the code to be generated...")
-        time.sleep(1)
-        resp = net.get(url)
-    resp.raise_for_status()
-
-    # Determine the name of the last function, which renders the generated code.
-    defined_funcs = re.findall(r"def ([a-zA-Z_]+)\(", resp.text)
-    if not defined_funcs:
-        raise GeneratedCodeHasNoFunctionDefsError(
-            f"No function definitions found in generated code from {url!r}."
-        )
-    render_func_name = defined_funcs[-1]
-
-    def replace_content(_match: re.Match) -> str:
-        return "\n".join(
-            [
-                resp.text,
-                "",
-                "def index() -> rx.Component:",
-                f"    return {render_func_name}()",
-                "",
-                "",
-            ],
-        )
-
-    main_module_path = Path(app_name, app_name + constants.Ext.PY)
-    main_module_code = main_module_path.read_text()
-
-    main_module_code = re.sub(
-        r"def index\(\).*:\n([^\n]\s+.*\n+)+",
-        replace_content,
-        main_module_code,
-    )
-    # Make the app use light mode until flexgen enforces the conversion of
-    # tailwind colors to radix colors.
-    main_module_code = re.sub(
-        r"app\s*=\s*rx\.App\(\s*\)",
-        'app = rx.App(theme=rx.theme(color_mode="light"))',
-        main_module_code,
-    )
-    main_module_path.write_text(main_module_code)
-
-
 def format_address_width(address_width: str | None) -> int | None:
 def format_address_width(address_width: str | None) -> int | None:
     """Cast address width to an int.
     """Cast address width to an int.