|
@@ -0,0 +1,250 @@
|
|
|
|
+"""Base class for all plugins."""
|
|
|
|
+
|
|
|
|
+from pathlib import Path
|
|
|
|
+from types import SimpleNamespace
|
|
|
|
+
|
|
|
|
+from reflex.constants.base import Dirs
|
|
|
|
+from reflex.plugins.base import Plugin as PluginBase
|
|
|
|
+from reflex.utils.decorator import once
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class Constants(SimpleNamespace):
|
|
|
|
+ """Tailwind constants."""
|
|
|
|
+
|
|
|
|
+ # The Tailwindcss version
|
|
|
|
+ VERSION = "tailwindcss@4.1.7"
|
|
|
|
+ # 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";
|
|
|
|
+
|
|
|
|
+@config '../tailwind.config.js';
|
|
|
|
+"""
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+@once
|
|
|
|
+def tailwind_config_js_template():
|
|
|
|
+ """Get the Tailwind config template.
|
|
|
|
+
|
|
|
|
+ Returns:
|
|
|
|
+ The Tailwind config template.
|
|
|
|
+ """
|
|
|
|
+ from reflex.compiler.templates import from_string
|
|
|
|
+
|
|
|
|
+ source = r"""
|
|
|
|
+{# 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 _index_of_element_that_startswith(lines: list[str], prefix: str) -> int | None:
|
|
|
|
+ return next(
|
|
|
|
+ (i for i, line in enumerate(lines) if line.strip().startswith(prefix)),
|
|
|
|
+ None,
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def add_tailwind_to_postcss_config(postcss_file_content: str) -> str:
|
|
|
|
+ """Add tailwind to the postcss config.
|
|
|
|
+
|
|
|
|
+ Args:
|
|
|
|
+ postcss_file_content: The content of the postcss config file.
|
|
|
|
+
|
|
|
|
+ Returns:
|
|
|
|
+ The modified postcss config file content.
|
|
|
|
+ """
|
|
|
|
+ from reflex.constants import Dirs
|
|
|
|
+
|
|
|
|
+ postcss_file_lines = postcss_file_content.splitlines()
|
|
|
|
+
|
|
|
|
+ if _index_of_element_that_startswith(postcss_file_lines, "tailwindcss") is not None:
|
|
|
|
+ return postcss_file_content
|
|
|
|
+
|
|
|
|
+ line_with_postcss_plugins = _index_of_element_that_startswith(
|
|
|
|
+ postcss_file_lines, "plugins"
|
|
|
|
+ )
|
|
|
|
+ if not line_with_postcss_plugins:
|
|
|
|
+ print( # noqa: T201
|
|
|
|
+ f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. "
|
|
|
|
+ "Please make sure the file exists and is valid."
|
|
|
|
+ )
|
|
|
|
+ return postcss_file_content
|
|
|
|
+
|
|
|
|
+ plugins_to_remove = ['""postcss-import"', "tailwindcss", "autoprefixer"]
|
|
|
|
+ plugins_to_add = ['"@tailwindcss/postcss"']
|
|
|
|
+
|
|
|
|
+ for plugin in plugins_to_remove:
|
|
|
|
+ plugin_index = _index_of_element_that_startswith(postcss_file_lines, plugin)
|
|
|
|
+ if plugin_index is not None:
|
|
|
|
+ postcss_file_lines.pop(plugin_index)
|
|
|
|
+
|
|
|
|
+ for plugin in plugins_to_add[::-1]:
|
|
|
|
+ postcss_file_lines.insert(line_with_postcss_plugins + 1, f" {plugin}: {{}},")
|
|
|
|
+
|
|
|
|
+ return "\n".join(postcss_file_lines)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class Plugin(PluginBase):
|
|
|
|
+ """Plugin for Tailwind CSS."""
|
|
|
|
+
|
|
|
|
+ def get_frontend_development_dependencies(self, **context) -> list[str]:
|
|
|
|
+ """Get the packages required by the plugin.
|
|
|
|
+
|
|
|
|
+ Args:
|
|
|
|
+ **context: The context for 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, "@tailwindcss/postcss4.1.7"]
|
|
|
|
+
|
|
|
|
+ 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_save_task"](compile_tailwind, config)
|
|
|
|
+ context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config)
|