tailwind_v3.py 6.5 KB

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