dynamic.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. """Components that are dynamically generated on the backend."""
  2. from typing import TYPE_CHECKING, Union
  3. from reflex import constants
  4. from reflex.utils import imports
  5. from reflex.utils.exceptions import DynamicComponentMissingLibraryError
  6. from reflex.utils.format import format_library_name
  7. from reflex.utils.serializers import serializer
  8. from reflex.vars import Var, get_unique_variable_name
  9. from reflex.vars.base import VarData, transform
  10. if TYPE_CHECKING:
  11. from reflex.components.component import Component
  12. def get_cdn_url(lib: str) -> str:
  13. """Get the CDN URL for a library.
  14. Args:
  15. lib: The library to get the CDN URL for.
  16. Returns:
  17. The CDN URL for the library.
  18. """
  19. return f"https://cdn.jsdelivr.net/npm/{lib}" + "/+esm"
  20. bundled_libraries = {
  21. "react",
  22. "@radix-ui/themes",
  23. "@emotion/react",
  24. "next/link",
  25. f"$/{constants.Dirs.UTILS}/context",
  26. f"$/{constants.Dirs.UTILS}/state",
  27. f"$/{constants.Dirs.UTILS}/components",
  28. }
  29. def bundle_library(component: Union["Component", str]):
  30. """Bundle a library with the component.
  31. Args:
  32. component: The component to bundle the library with.
  33. Raises:
  34. DynamicComponentMissingLibraryError: Raised when a dynamic component is missing a library.
  35. """
  36. if isinstance(component, str):
  37. bundled_libraries.add(component)
  38. return
  39. if component.library is None:
  40. raise DynamicComponentMissingLibraryError(
  41. "Component must have a library to bundle."
  42. )
  43. bundled_libraries.add(format_library_name(component.library))
  44. def load_dynamic_serializer():
  45. """Load the serializer for dynamic components."""
  46. # Causes a circular import, so we import here.
  47. from reflex.components.component import Component
  48. @serializer
  49. def make_component(component: Component) -> str:
  50. """Generate the code for a dynamic component.
  51. Args:
  52. component: The component to generate code for.
  53. Returns:
  54. The generated code
  55. """
  56. # Causes a circular import, so we import here.
  57. from reflex.compiler import compiler, templates, utils
  58. from reflex.components.base.bare import Bare
  59. component = Bare.create(Var.create(component))
  60. rendered_components = {}
  61. # Include dynamic imports in the shared component.
  62. if dynamic_imports := component._get_all_dynamic_imports():
  63. rendered_components.update(dict.fromkeys(dynamic_imports))
  64. # Include custom code in the shared component.
  65. rendered_components.update(
  66. dict.fromkeys(component._get_all_custom_code()),
  67. )
  68. rendered_components[
  69. templates.STATEFUL_COMPONENT.render(
  70. tag_name="MySSRComponent",
  71. memo_trigger_hooks=[],
  72. component=component,
  73. )
  74. ] = None
  75. libs_in_window = bundled_libraries
  76. component_imports = component._get_all_imports()
  77. compiler._apply_common_imports(component_imports)
  78. imports = {}
  79. for lib, names in component_imports.items():
  80. formatted_lib_name = format_library_name(lib)
  81. if (
  82. not lib.startswith((".", "/", "$/"))
  83. and not lib.startswith("http")
  84. and formatted_lib_name not in libs_in_window
  85. ):
  86. imports[get_cdn_url(lib)] = names
  87. else:
  88. imports[lib] = names
  89. module_code_lines = templates.STATEFUL_COMPONENTS.render(
  90. imports=utils.compile_imports(imports),
  91. memoized_code="\n".join(rendered_components),
  92. ).splitlines()[1:]
  93. # Rewrite imports from `/` to destructure from window
  94. for ix, line in enumerate(module_code_lines[:]):
  95. if line.startswith("import "):
  96. if 'from "$/' in line or 'from "/' in line:
  97. module_code_lines[ix] = (
  98. line.replace("import ", "const ", 1)
  99. .replace(" as ", ": ")
  100. .replace(" from ", " = window['__reflex'][", 1)
  101. + "]"
  102. )
  103. else:
  104. for lib in libs_in_window:
  105. if f'from "{lib}"' in line:
  106. module_code_lines[ix] = (
  107. line.replace("import ", "const ", 1)
  108. .replace(
  109. f' from "{lib}"', f" = window.__reflex['{lib}']", 1
  110. )
  111. .replace(" as ", ": ")
  112. )
  113. if line.startswith("export function"):
  114. module_code_lines[ix] = line.replace(
  115. "export function", "export default function", 1
  116. )
  117. line_stripped = line.strip()
  118. if line_stripped.startswith("{") and line_stripped.endswith("}"):
  119. module_code_lines[ix] = line_stripped[1:-1]
  120. module_code_lines.insert(0, "const React = window.__reflex.react;")
  121. function_line = next(
  122. index
  123. for index, line in enumerate(module_code_lines)
  124. if line.startswith("export default function")
  125. )
  126. module_code_lines = [
  127. line
  128. for _, line in sorted(
  129. enumerate(module_code_lines),
  130. key=lambda x: (
  131. not (x[1].startswith("import ") and x[0] < function_line),
  132. x[0],
  133. ),
  134. )
  135. ]
  136. return "\n".join(
  137. [
  138. "//__reflex_evaluate",
  139. *module_code_lines,
  140. ]
  141. )
  142. @transform
  143. def evaluate_component(js_string: Var[str]) -> Var[Component]:
  144. """Evaluate a component.
  145. Args:
  146. js_string: The JavaScript string to evaluate.
  147. Returns:
  148. The evaluated JavaScript string.
  149. """
  150. unique_var_name = get_unique_variable_name()
  151. return js_string._replace(
  152. _js_expr=unique_var_name,
  153. _var_type=Component,
  154. merge_var_data=VarData.merge(
  155. VarData(
  156. imports={
  157. f"$/{constants.Dirs.STATE_PATH}": [
  158. imports.ImportVar(tag="evalReactComponent"),
  159. ],
  160. "react": [
  161. imports.ImportVar(tag="useState"),
  162. imports.ImportVar(tag="useEffect"),
  163. ],
  164. },
  165. hooks={
  166. f"const [{unique_var_name}, set_{unique_var_name}] = useState(null);": None,
  167. "useEffect(() => {"
  168. "let isMounted = true;"
  169. f"evalReactComponent({js_string!s})"
  170. ".then((component) => {"
  171. "if (isMounted) {"
  172. f"set_{unique_var_name}(component);"
  173. "}"
  174. "});"
  175. "return () => {"
  176. "isMounted = false;"
  177. "};"
  178. "}"
  179. f", [{js_string!s}]);": None,
  180. },
  181. ),
  182. ),
  183. )