tailwind_v3.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. """Base class for all plugins."""
  2. from types import SimpleNamespace
  3. from reflex.constants.base import Dirs
  4. from reflex.constants.compiler import Ext, PageNames
  5. from reflex.plugins.base import Plugin as PluginBase
  6. from reflex.utils.decorator import once
  7. class Constants(SimpleNamespace):
  8. """Tailwind constants."""
  9. # The Tailwindcss version
  10. VERSION = "tailwindcss@3.4.17"
  11. # The Tailwind config.
  12. CONFIG = "tailwind.config.js"
  13. # Default Tailwind content paths
  14. CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}", "./utils/**/*.{js,ts,jsx,tsx}"]
  15. # Relative tailwind style path to root stylesheet in Dirs.STYLES.
  16. ROOT_STYLE_PATH = "./tailwind.css"
  17. # The default tailwind css.
  18. TAILWIND_CSS = """@import "tailwindcss/base";
  19. @import url('{radix_url}');
  20. @tailwind components;
  21. @tailwind utilities;
  22. """
  23. @once
  24. def tailwind_config_js_template():
  25. """Get the Tailwind config template.
  26. Returns:
  27. The Tailwind config template.
  28. """
  29. from reflex.compiler.templates import from_string
  30. source = r"""
  31. {# Helper macro to render JS objects and arrays #}
  32. {% macro render_js(val, indent=2, level=0) -%}
  33. {%- set space = ' ' * (indent * level) -%}
  34. {%- set next_space = ' ' * (indent * (level + 1)) -%}
  35. {%- if val is mapping -%}
  36. {
  37. {%- for k, v in val.items() %}
  38. {{ next_space }}{{ k if k is string and k.isidentifier() else k|tojson }}: {{ render_js(v, indent, level + 1) }}{{ "," if not loop.last }}
  39. {%- endfor %}
  40. {{ space }}}
  41. {%- elif val is iterable and val is not string -%}
  42. [
  43. {%- for item in val %}
  44. {{ next_space }}{{ render_js(item, indent, level + 1) }}{{ "," if not loop.last }}
  45. {%- endfor %}
  46. {{ space }}]
  47. {%- else -%}
  48. {{ val | tojson }}
  49. {%- endif -%}
  50. {%- endmacro %}
  51. {# Extract destructured imports from plugin dicts only #}
  52. {%- set imports = [] %}
  53. {%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
  54. {%- set _ = imports.append(plugin.import) %}
  55. {%- endfor %}
  56. /** @type {import('tailwindcss').Config} */
  57. {%- for imp in imports %}
  58. const { {{ imp.name }} } = require({{ imp.from | tojson }});
  59. {%- endfor %}
  60. module.exports = {
  61. content: {{ render_js(content) }},
  62. theme: {{ render_js(theme) }},
  63. {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
  64. {% if corePlugins is defined %}corePlugins: {{ render_js(corePlugins) }},{% endif %}
  65. {% if important is defined %}important: {{ important | tojson }},{% endif %}
  66. {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
  67. {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
  68. {% if presets is defined %}
  69. presets: [
  70. {% for preset in presets %}
  71. require({{ preset | tojson }}){{ "," if not loop.last }}
  72. {% endfor %}
  73. ],
  74. {% endif %}
  75. plugins: [
  76. {% for plugin in plugins %}
  77. {% if plugin is mapping %}
  78. {% if plugin.call is defined %}
  79. {{ plugin.call }}(
  80. {%- if plugin.args is defined -%}
  81. {{ render_js(plugin.args) }}
  82. {%- endif -%}
  83. ){{ "," if not loop.last }}
  84. {% else %}
  85. require({{ plugin.name | tojson }}){{ "," if not loop.last }}
  86. {% endif %}
  87. {% else %}
  88. require({{ plugin | tojson }}){{ "," if not loop.last }}
  89. {% endif %}
  90. {% endfor %}
  91. ]
  92. };
  93. """
  94. return from_string(source)
  95. def _compile_tailwind(
  96. config: dict,
  97. ) -> str:
  98. """Compile the Tailwind config.
  99. Args:
  100. config: The Tailwind config.
  101. Returns:
  102. The compiled Tailwind config.
  103. """
  104. return tailwind_config_js_template().render(
  105. **config,
  106. )
  107. def compile_tailwind(
  108. config: dict,
  109. ):
  110. """Compile the Tailwind config.
  111. Args:
  112. config: The Tailwind config.
  113. Returns:
  114. The compiled Tailwind config.
  115. """
  116. from reflex.utils.prerequisites import get_web_dir
  117. # Get the path for the output file.
  118. output_path = str((get_web_dir() / Constants.CONFIG).absolute())
  119. # Compile the config.
  120. code = _compile_tailwind(config)
  121. return output_path, code
  122. def _index_of_element_that_has(haystack: list[str], needle: str) -> int | None:
  123. return next(
  124. (i for i, line in enumerate(haystack) if needle in line),
  125. None,
  126. )
  127. def add_tailwind_to_postcss_config(postcss_file_content: str) -> str:
  128. """Add tailwind to the postcss config.
  129. Args:
  130. postcss_file_content: The content of the postcss config file.
  131. Returns:
  132. The modified postcss config file content.
  133. """
  134. from reflex.constants import Dirs
  135. postcss_file_lines = postcss_file_content.splitlines()
  136. if _index_of_element_that_has(postcss_file_lines, "tailwindcss") is not None:
  137. return postcss_file_content
  138. line_with_postcss_plugins = _index_of_element_that_has(
  139. postcss_file_lines, "plugins"
  140. )
  141. if not line_with_postcss_plugins:
  142. print( # noqa: T201
  143. f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. "
  144. "Please make sure the file exists and is valid."
  145. )
  146. return postcss_file_content
  147. postcss_import_line = _index_of_element_that_has(
  148. postcss_file_lines, '"postcss-import"'
  149. )
  150. postcss_file_lines.insert(
  151. (postcss_import_line or line_with_postcss_plugins) + 1, "tailwindcss: {},"
  152. )
  153. return "\n".join(postcss_file_lines)
  154. def add_tailwind_to_css_file(css_file_content: str) -> str:
  155. """Add tailwind to the css file.
  156. Args:
  157. css_file_content: The content of the css file.
  158. Returns:
  159. The modified css file content.
  160. """
  161. from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
  162. if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content:
  163. return css_file_content
  164. if RADIX_THEMES_STYLESHEET not in css_file_content:
  165. print( # noqa: T201
  166. f"Could not find line with '{RADIX_THEMES_STYLESHEET}' in {Dirs.STYLES}. "
  167. "Please make sure the file exists and is valid."
  168. )
  169. return css_file_content
  170. return css_file_content.replace(
  171. f"@import url('{RADIX_THEMES_STYLESHEET}');",
  172. Constants.TAILWIND_CSS.format(radix_url=RADIX_THEMES_STYLESHEET),
  173. )
  174. class Plugin(PluginBase):
  175. """Plugin for Tailwind CSS."""
  176. def get_frontend_development_dependencies(self, **context) -> list[str]:
  177. """Get the packages required by the plugin.
  178. Args:
  179. **context: The context for the plugin.
  180. Returns:
  181. A list of packages required by the plugin.
  182. """
  183. from reflex.config import get_config
  184. config = get_config()
  185. return [
  186. plugin if isinstance(plugin, str) else plugin.get("name")
  187. for plugin in (config.tailwind or {}).get("plugins", [])
  188. ] + [Constants.VERSION]
  189. def pre_compile(self, **context):
  190. """Pre-compile the plugin.
  191. Args:
  192. context: The context for the plugin.
  193. """
  194. from reflex.config import get_config
  195. config = get_config().tailwind or {}
  196. config["content"] = config.get("content", Constants.CONTENT)
  197. context["add_save_task"](compile_tailwind, config)
  198. context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config)
  199. context["add_modify_task"](
  200. Dirs.STYLES + "/" + PageNames.STYLESHEET_ROOT + Ext.CSS,
  201. add_tailwind_to_css_file,
  202. )