tailwind_v3.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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 _index_of_element_that_startswith(lines: list[str], prefix: str) -> int | None:
  122. return next(
  123. (i for i, line in enumerate(lines) if line.strip().startswith(prefix)),
  124. None,
  125. )
  126. def add_tailwind_to_postcss_config():
  127. """Add tailwind to the postcss config."""
  128. from reflex.constants import Dirs
  129. from reflex.utils.prerequisites import get_web_dir
  130. postcss_file = get_web_dir() / Dirs.POSTCSS_JS
  131. if not postcss_file.exists():
  132. print( # noqa: T201
  133. f"Could not find {Dirs.POSTCSS_JS}. "
  134. "Please make sure the file exists and is valid."
  135. )
  136. return
  137. postcss_file_lines = postcss_file.read_text().splitlines()
  138. if _index_of_element_that_startswith(postcss_file_lines, "tailwindcss") is not None:
  139. return
  140. line_with_postcss_plugins = _index_of_element_that_startswith(
  141. postcss_file_lines, "plugins"
  142. )
  143. if not line_with_postcss_plugins:
  144. print( # noqa: T201
  145. f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. "
  146. "Please make sure the file exists and is valid."
  147. )
  148. return
  149. postcss_import_line = _index_of_element_that_startswith(
  150. postcss_file_lines, '"postcss-import"'
  151. )
  152. postcss_file_lines.insert(
  153. (postcss_import_line or line_with_postcss_plugins) + 1, "tailwindcss: {},"
  154. )
  155. return str(postcss_file), "\n".join(postcss_file_lines)
  156. class TailwindV3Plugin(Plugin):
  157. """Plugin for Tailwind CSS."""
  158. def get_frontend_development_dependancies(self, **context) -> list[str]:
  159. """Get the packages required by the plugin.
  160. Args:
  161. **context: The context for the plugin.
  162. Returns:
  163. A list of packages required by the plugin.
  164. """
  165. from reflex.config import get_config
  166. config = get_config()
  167. return [
  168. plugin if isinstance(plugin, str) else plugin.get("name")
  169. for plugin in (config.tailwind or {}).get("plugins", [])
  170. ] + [Constants.VERSION]
  171. def get_static_assets(self, **context):
  172. """Get the static assets required by the plugin.
  173. Args:
  174. context: The context for the plugin.
  175. Returns:
  176. A list of static assets required by the plugin.
  177. """
  178. return [(Path("styles/tailwind.css"), Constants.TAILWIND_CSS)]
  179. def get_stylesheet_paths(self, **context) -> list[str]:
  180. """Get the paths to the stylesheets required by the plugin relative to the styles directory.
  181. Args:
  182. context: The context for the plugin.
  183. Returns:
  184. A list of paths to the stylesheets required by the plugin.
  185. """
  186. return [Constants.ROOT_STYLE_PATH]
  187. def pre_compile(self, **context):
  188. """Pre-compile the plugin.
  189. Args:
  190. context: The context for the plugin.
  191. """
  192. from reflex.config import get_config
  193. config = get_config().tailwind or {}
  194. config["content"] = config.get("content", Constants.CONTENT)
  195. context["add_task"](compile_tailwind, config)
  196. context["add_task"](add_tailwind_to_postcss_config)