浏览代码

Support for importing functions from tailwind plugins (#5121)

* Support for importing functions from tailwind plugins

* fix: handle frontend packages for import type tailwind plugins

* fix: tailwind jinja2 template

* fix: tailwind jinja2 template to adjust new imports (no breaking change)

* fix: handle command generation for tailwind plugins (dict-type)

* fix: accept normal strings as package name

* fix: format_full_name to handle dict && add 2 test cases for dict type npm packages

* change how template handles nested dicts

* one last time: pre-commit resolved

* go back to original one but with dict support
Adarsh Gourab Mahalik 1 月之前
父节点
当前提交
d75ea9a402

+ 65 - 31
reflex/.templates/jinja/web/tailwind.config.js.jinja2

@@ -1,32 +1,66 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
-	content: {{content|json_dumps}},
-	theme: {{theme|json_dumps}},
-	plugins: [
-	{% for plugin in plugins %}
-		require({{plugin|json_dumps}}),
-	{% endfor %}
-	],
-	{% if presets is defined %}
-	presets: [
-	{% for preset in presets %}
-		require({{preset|json_dumps}})
-	{% endfor %}
-	],
-	{% endif %}
-	{% if darkMode is defined %}
-	darkMode: {{darkMode|json_dumps}},
-	{% endif %}
-	{% if corePlugins is defined %}
-	corePlugins: {{corePlugins|json_dumps}},
-	{% endif %}
-	{% if important is defined %}
-	important: {{important|json_dumps}},
-	{% endif %}
-	{% if prefix is defined %}
-	prefix: {{prefix|json_dumps}},
-	{% endif %}
-	{% if separator is defined %}
-	separator: {{separator|json_dumps}},
-	{% endif %}
+{# 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 %}
+  ]
 };

+ 17 - 2
reflex/utils/format.py

@@ -643,17 +643,32 @@ def format_ref(ref: str) -> str:
     return f"ref_{clean_ref}"
 
 
-def format_library_name(library_fullname: str):
+def format_library_name(library_fullname: str | dict[str, Any]) -> str:
     """Format the name of a library.
 
     Args:
-        library_fullname: The fullname of the library.
+        library_fullname: The library reference, either as a string or a dictionary with a 'name' key.
 
     Returns:
         The name without the @version if it was part of the name
+
+    Raises:
+        KeyError: If library_fullname is a dictionary without a 'name' key.
+        TypeError: If library_fullname or its 'name' value is not a string.
     """
+    # If input is a dictionary, extract the 'name' key
+    if isinstance(library_fullname, dict):
+        if "name" not in library_fullname:
+            raise KeyError("Dictionary input must contain a 'name' key")
+        library_fullname = library_fullname["name"]
+
+    # Process the library name as a string
+    if not isinstance(library_fullname, str):
+        raise TypeError("Library name must be a string")
+
     if library_fullname.startswith("https://"):
         return library_fullname
+
     lib, at, version = library_fullname.rpartition("@")
     if not lib:
         lib = at + version

+ 4 - 1
reflex/utils/prerequisites.py

@@ -1314,7 +1314,10 @@ def install_frontend_packages(packages: set[str], config: Config):
                 "--legacy-peer-deps",
                 "-d",
                 constants.Tailwind.VERSION,
-                *((config.tailwind or {}).get("plugins", [])),
+                *[
+                    plugin if isinstance(plugin, str) else plugin.get("name")
+                    for plugin in (config.tailwind or {}).get("plugins", [])
+                ],
             ],
             fallbacks=fallbacks,
             analytics_enabled=True,

+ 2 - 0
tests/units/test_prerequisites.py

@@ -97,6 +97,8 @@ def test_update_next_config(config, export, expected_output):
             ["foo", "@bar/baz", "foo", "@bar/baz@3.2.1"],
             ["@bar/baz", "foo"],
         ),
+        (["@bar/baz", {"name": "foo"}], ["@bar/baz", "foo"]),
+        (["@bar/baz", {"name": "@foo/baz"}], ["@bar/baz", "@foo/baz"]),
     ),
 )
 def test_transpile_packages(transpile_packages, expected_transpile_packages):