compiler.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. """Compiler for the reflex apps."""
  2. from __future__ import annotations
  3. import os
  4. from pathlib import Path
  5. from typing import Iterable, Optional, Type
  6. from reflex import constants
  7. from reflex.compiler import templates, utils
  8. from reflex.components.component import (
  9. BaseComponent,
  10. Component,
  11. ComponentStyle,
  12. CustomComponent,
  13. StatefulComponent,
  14. )
  15. from reflex.config import get_config
  16. from reflex.state import BaseState
  17. from reflex.style import LIGHT_COLOR_MODE
  18. from reflex.utils.imports import ImportVar
  19. def _compile_document_root(root: Component) -> str:
  20. """Compile the document root.
  21. Args:
  22. root: The document root to compile.
  23. Returns:
  24. The compiled document root.
  25. """
  26. return templates.DOCUMENT_ROOT.render(
  27. imports=utils.compile_imports(root.get_imports()),
  28. document=root.render(),
  29. )
  30. def _compile_app(app_root: Component) -> str:
  31. """Compile the app template component.
  32. Args:
  33. app_root: The app root to compile.
  34. Returns:
  35. The compiled app.
  36. """
  37. return templates.APP_ROOT.render(
  38. imports=utils.compile_imports(app_root.get_imports()),
  39. custom_codes=app_root.get_custom_code(),
  40. hooks=app_root.get_hooks(),
  41. render=app_root.render(),
  42. )
  43. def _compile_theme(theme: dict) -> str:
  44. """Compile the theme.
  45. Args:
  46. theme: The theme to compile.
  47. Returns:
  48. The compiled theme.
  49. """
  50. return templates.THEME.render(theme=theme)
  51. def _is_dev_mode() -> bool:
  52. return os.environ.get("REFLEX_ENV_MODE", "dev") == "dev"
  53. def _compile_contexts(state: Optional[Type[BaseState]], theme: Component) -> str:
  54. """Compile the initial state and contexts.
  55. Args:
  56. state: The app state.
  57. theme: The top-level app theme.
  58. Returns:
  59. The compiled context file.
  60. """
  61. return (
  62. templates.CONTEXT.render(
  63. initial_state=utils.compile_state(state),
  64. state_name=state.get_name(),
  65. client_storage=utils.compile_client_storage(state),
  66. is_dev_mode=_is_dev_mode(),
  67. default_color_mode=getattr(theme, "appearance", LIGHT_COLOR_MODE),
  68. )
  69. if state
  70. else templates.CONTEXT.render(
  71. is_dev_mode=_is_dev_mode(),
  72. default_color_mode=getattr(theme, "appearance", LIGHT_COLOR_MODE),
  73. )
  74. )
  75. def _compile_page(
  76. component: Component,
  77. state: Type[BaseState],
  78. ) -> str:
  79. """Compile the component given the app state.
  80. Args:
  81. component: The component to compile.
  82. state: The app state.
  83. Returns:
  84. The compiled component.
  85. """
  86. imports = component.get_imports()
  87. imports = utils.compile_imports(imports)
  88. # Compile the code to render the component.
  89. kwargs = {"state_name": state.get_name()} if state else {}
  90. return templates.PAGE.render(
  91. imports=imports,
  92. dynamic_imports=component.get_dynamic_imports(),
  93. custom_codes=component.get_custom_code(),
  94. hooks=component.get_hooks(),
  95. render=component.render(),
  96. **kwargs,
  97. )
  98. def compile_root_stylesheet(stylesheets: list[str]) -> tuple[str, str]:
  99. """Compile the root stylesheet.
  100. Args:
  101. stylesheets: The stylesheets to include in the root stylesheet.
  102. Returns:
  103. The path and code of the compiled root stylesheet.
  104. """
  105. output_path = utils.get_root_stylesheet_path()
  106. code = _compile_root_stylesheet(stylesheets)
  107. return output_path, code
  108. def _compile_root_stylesheet(stylesheets: list[str]) -> str:
  109. """Compile the root stylesheet.
  110. Args:
  111. stylesheets: The stylesheets to include in the root stylesheet.
  112. Returns:
  113. The compiled root stylesheet.
  114. Raises:
  115. FileNotFoundError: If a specified stylesheet in assets directory does not exist.
  116. """
  117. # Add tailwind css if enabled.
  118. sheets = (
  119. [constants.Tailwind.ROOT_STYLE_PATH]
  120. if get_config().tailwind is not None
  121. else []
  122. )
  123. for stylesheet in stylesheets:
  124. if not utils.is_valid_url(stylesheet):
  125. # check if stylesheet provided exists.
  126. stylesheet_full_path = (
  127. Path.cwd() / constants.Dirs.APP_ASSETS / stylesheet.strip("/")
  128. )
  129. if not os.path.exists(stylesheet_full_path):
  130. raise FileNotFoundError(
  131. f"The stylesheet file {stylesheet_full_path} does not exist."
  132. )
  133. stylesheet = f"@/{stylesheet.strip('/')}"
  134. sheets.append(stylesheet) if stylesheet not in sheets else None
  135. return templates.STYLE.render(stylesheets=sheets)
  136. def _compile_component(component: Component) -> str:
  137. """Compile a single component.
  138. Args:
  139. component: The component to compile.
  140. Returns:
  141. The compiled component.
  142. """
  143. return templates.COMPONENT.render(component=component)
  144. def _compile_components(components: set[CustomComponent]) -> str:
  145. """Compile the components.
  146. Args:
  147. components: The components to compile.
  148. Returns:
  149. The compiled components.
  150. """
  151. imports = {
  152. "react": [ImportVar(tag="memo")],
  153. f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="E"), ImportVar(tag="isTrue")],
  154. }
  155. component_renders = []
  156. # Compile each component.
  157. for component in components:
  158. component_render, component_imports = utils.compile_custom_component(component)
  159. component_renders.append(component_render)
  160. imports = utils.merge_imports(imports, component_imports)
  161. # Compile the components page.
  162. return templates.COMPONENTS.render(
  163. imports=utils.compile_imports(imports),
  164. components=component_renders,
  165. )
  166. def _compile_stateful_components(
  167. page_components: list[BaseComponent],
  168. ) -> str:
  169. """Walk the page components and extract shared stateful components.
  170. Any StatefulComponent that is shared by more than one page will be rendered
  171. to a separate module and marked rendered_as_shared so subsequent
  172. renderings will import the component from the shared module instead of
  173. directly including the code for it.
  174. Args:
  175. page_components: The Components or StatefulComponents to compile.
  176. Returns:
  177. The rendered stateful components code.
  178. """
  179. all_import_dicts = []
  180. rendered_components = {}
  181. def get_shared_components_recursive(component: BaseComponent):
  182. """Get the shared components for a component and its children.
  183. A shared component is a StatefulComponent that appears in 2 or more
  184. pages and is a candidate for writing to a common file and importing
  185. into each page where it is used.
  186. Args:
  187. component: The component to collect shared StatefulComponents for.
  188. """
  189. for child in component.children:
  190. # Depth-first traversal.
  191. get_shared_components_recursive(child)
  192. # When the component is referenced by more than one page, render it
  193. # to be included in the STATEFUL_COMPONENTS module.
  194. # Skip this step in dev mode, thereby avoiding potential hot reload errors for larger apps
  195. if (
  196. isinstance(component, StatefulComponent)
  197. and component.references > 1
  198. and not _is_dev_mode()
  199. ):
  200. # Reset this flag to render the actual component.
  201. component.rendered_as_shared = False
  202. rendered_components.update(
  203. {code: None for code in component.get_custom_code()},
  204. )
  205. all_import_dicts.append(component.get_imports())
  206. # Indicate that this component now imports from the shared file.
  207. component.rendered_as_shared = True
  208. for page_component in page_components:
  209. get_shared_components_recursive(page_component)
  210. # Don't import from the file that we're about to create.
  211. all_imports = utils.merge_imports(*all_import_dicts)
  212. all_imports.pop(
  213. f"/{constants.Dirs.UTILS}/{constants.PageNames.STATEFUL_COMPONENTS}", None
  214. )
  215. return templates.STATEFUL_COMPONENTS.render(
  216. imports=utils.compile_imports(all_imports),
  217. memoized_code="\n".join(rendered_components),
  218. )
  219. def _compile_tailwind(
  220. config: dict,
  221. ) -> str:
  222. """Compile the Tailwind config.
  223. Args:
  224. config: The Tailwind config.
  225. Returns:
  226. The compiled Tailwind config.
  227. """
  228. return templates.TAILWIND_CONFIG.render(
  229. **config,
  230. )
  231. def compile_document_root(head_components: list[Component]) -> tuple[str, str]:
  232. """Compile the document root.
  233. Args:
  234. head_components: The components to include in the head.
  235. Returns:
  236. The path and code of the compiled document root.
  237. """
  238. # Get the path for the output file.
  239. output_path = utils.get_page_path(constants.PageNames.DOCUMENT_ROOT)
  240. # Create the document root.
  241. document_root = utils.create_document_root(head_components)
  242. # Compile the document root.
  243. code = _compile_document_root(document_root)
  244. return output_path, code
  245. def compile_app(app_root: Component) -> tuple[str, str]:
  246. """Compile the app root.
  247. Args:
  248. app_root: The app root component to compile.
  249. Returns:
  250. The path and code of the compiled app wrapper.
  251. """
  252. # Get the path for the output file.
  253. output_path = utils.get_page_path(constants.PageNames.APP_ROOT)
  254. # Compile the document root.
  255. code = _compile_app(app_root)
  256. return output_path, code
  257. def compile_theme(style: ComponentStyle) -> tuple[str, str]:
  258. """Compile the theme.
  259. Args:
  260. style: The style to compile.
  261. Returns:
  262. The path and code of the compiled theme.
  263. """
  264. output_path = utils.get_theme_path()
  265. # Create the theme.
  266. theme = utils.create_theme(style)
  267. # Compile the theme.
  268. code = _compile_theme(theme)
  269. return output_path, code
  270. def compile_contexts(
  271. state: Optional[Type[BaseState]],
  272. theme: Component,
  273. ) -> tuple[str, str]:
  274. """Compile the initial state / context.
  275. Args:
  276. state: The app state.
  277. theme: The top-level app theme.
  278. Returns:
  279. The path and code of the compiled context.
  280. """
  281. # Get the path for the output file.
  282. output_path = utils.get_context_path()
  283. return output_path, _compile_contexts(state, theme)
  284. def compile_page(
  285. path: str, component: Component, state: Type[BaseState]
  286. ) -> tuple[str, str]:
  287. """Compile a single page.
  288. Args:
  289. path: The path to compile the page to.
  290. component: The component to compile.
  291. state: The app state.
  292. Returns:
  293. The path and code of the compiled page.
  294. """
  295. # Get the path for the output file.
  296. output_path = utils.get_page_path(path)
  297. # Add the style to the component.
  298. code = _compile_page(component, state)
  299. return output_path, code
  300. def compile_components(components: set[CustomComponent]):
  301. """Compile the custom components.
  302. Args:
  303. components: The custom components to compile.
  304. Returns:
  305. The path and code of the compiled components.
  306. """
  307. # Get the path for the output file.
  308. output_path = utils.get_components_path()
  309. # Compile the components.
  310. code = _compile_components(components)
  311. return output_path, code
  312. def compile_stateful_components(
  313. pages: Iterable[Component],
  314. ) -> tuple[str, str, list[BaseComponent]]:
  315. """Separately compile components that depend on State vars.
  316. StatefulComponents are compiled as their own component functions with their own
  317. useContext declarations, which allows page components to be stateless and avoid
  318. re-rendering along with parts of the page that actually depend on state.
  319. Args:
  320. pages: The pages to extract stateful components from.
  321. Returns:
  322. The path and code of the compiled stateful components.
  323. """
  324. output_path = utils.get_stateful_components_path()
  325. # Compile the stateful components.
  326. page_components = [StatefulComponent.compile_from(page) or page for page in pages]
  327. code = _compile_stateful_components(page_components)
  328. return output_path, code, page_components
  329. def compile_tailwind(
  330. config: dict,
  331. ):
  332. """Compile the Tailwind config.
  333. Args:
  334. config: The Tailwind config.
  335. Returns:
  336. The compiled Tailwind config.
  337. """
  338. # Get the path for the output file.
  339. output_path = constants.Tailwind.CONFIG
  340. # Compile the config.
  341. code = _compile_tailwind(config)
  342. return output_path, code
  343. def remove_tailwind_from_postcss() -> tuple[str, str]:
  344. """If tailwind is not to be used, remove it from postcss.config.js.
  345. Returns:
  346. The path and code of the compiled postcss.config.js.
  347. """
  348. # Get the path for the output file.
  349. output_path = constants.Dirs.POSTCSS_JS
  350. code = [
  351. line
  352. for line in Path(output_path).read_text().splitlines(keepends=True)
  353. if "tailwindcss: " not in line
  354. ]
  355. # Compile the config.
  356. return output_path, "".join(code)
  357. def purge_web_pages_dir():
  358. """Empty out .web/pages directory."""
  359. if _is_dev_mode() and os.environ.get("REFLEX_PERSIST_WEB_DIR"):
  360. # Skip purging the web directory in dev mode if REFLEX_PERSIST_WEB_DIR is set.
  361. return
  362. # Empty out the web pages directory.
  363. utils.empty_dir(constants.Dirs.WEB_PAGES, keep_files=["_app.js"])