1
0

dynamic.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  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 DynamicComponentMissingLibrary
  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 = {"react", "@radix-ui/themes", "@emotion/react", "next/link"}
  21. def bundle_library(component: Union["Component", str]):
  22. """Bundle a library with the component.
  23. Args:
  24. component: The component to bundle the library with.
  25. Raises:
  26. DynamicComponentMissingLibrary: Raised when a dynamic component is missing a library.
  27. """
  28. if isinstance(component, str):
  29. bundled_libraries.add(component)
  30. return
  31. if component.library is None:
  32. raise DynamicComponentMissingLibrary("Component must have a library to bundle.")
  33. bundled_libraries.add(format_library_name(component.library))
  34. def load_dynamic_serializer():
  35. """Load the serializer for dynamic components."""
  36. # Causes a circular import, so we import here.
  37. from reflex.components.component import Component
  38. @serializer
  39. def make_component(component: Component) -> str:
  40. """Generate the code for a dynamic component.
  41. Args:
  42. component: The component to generate code for.
  43. Returns:
  44. The generated code
  45. """
  46. # Causes a circular import, so we import here.
  47. from reflex.compiler import templates, utils
  48. from reflex.components.base.bare import Bare
  49. component = Bare.create(Var.create(component))
  50. rendered_components = {}
  51. # Include dynamic imports in the shared component.
  52. if dynamic_imports := component._get_all_dynamic_imports():
  53. rendered_components.update(
  54. {dynamic_import: None for dynamic_import in dynamic_imports}
  55. )
  56. # Include custom code in the shared component.
  57. rendered_components.update(
  58. {code: None for code in component._get_all_custom_code()},
  59. )
  60. rendered_components[
  61. templates.STATEFUL_COMPONENT.render(
  62. tag_name="MySSRComponent",
  63. memo_trigger_hooks=[],
  64. component=component,
  65. )
  66. ] = None
  67. libs_in_window = bundled_libraries
  68. imports = {}
  69. for lib, names in component._get_all_imports().items():
  70. formatted_lib_name = format_library_name(lib)
  71. if (
  72. not lib.startswith((".", "/", "$/"))
  73. and not lib.startswith("http")
  74. and formatted_lib_name not in libs_in_window
  75. ):
  76. imports[get_cdn_url(lib)] = names
  77. else:
  78. imports[lib] = names
  79. module_code_lines = templates.STATEFUL_COMPONENTS.render(
  80. imports=utils.compile_imports(imports),
  81. memoized_code="\n".join(rendered_components),
  82. ).splitlines()[1:]
  83. # Rewrite imports from `/` to destructure from window
  84. for ix, line in enumerate(module_code_lines[:]):
  85. if line.startswith("import "):
  86. if 'from "$/' in line or 'from "/' in line:
  87. module_code_lines[ix] = (
  88. line.replace("import ", "const ", 1)
  89. .replace(" as ", ": ")
  90. .replace(" from ", " = window['__reflex'][", 1)
  91. + "]"
  92. )
  93. else:
  94. for lib in libs_in_window:
  95. if f'from "{lib}"' in line:
  96. module_code_lines[ix] = (
  97. line.replace("import ", "const ", 1)
  98. .replace(
  99. f' from "{lib}"', f" = window.__reflex['{lib}']", 1
  100. )
  101. .replace(" as ", ": ")
  102. )
  103. if line.startswith("export function"):
  104. module_code_lines[ix] = line.replace(
  105. "export function", "export default function", 1
  106. )
  107. line_stripped = line.strip()
  108. if line_stripped.startswith("{") and line_stripped.endswith("}"):
  109. module_code_lines[ix] = line_stripped[1:-1]
  110. module_code_lines.insert(0, "const React = window.__reflex.react;")
  111. return "\n".join(
  112. [
  113. "//__reflex_evaluate",
  114. *module_code_lines,
  115. ]
  116. )
  117. @transform
  118. def evaluate_component(js_string: Var[str]) -> Var[Component]:
  119. """Evaluate a component.
  120. Args:
  121. js_string: The JavaScript string to evaluate.
  122. Returns:
  123. The evaluated JavaScript string.
  124. """
  125. unique_var_name = get_unique_variable_name()
  126. return js_string._replace(
  127. _js_expr=unique_var_name,
  128. _var_type=Component,
  129. merge_var_data=VarData.merge(
  130. VarData(
  131. imports={
  132. f"$/{constants.Dirs.STATE_PATH}": [
  133. imports.ImportVar(tag="evalReactComponent"),
  134. ],
  135. "react": [
  136. imports.ImportVar(tag="useState"),
  137. imports.ImportVar(tag="useEffect"),
  138. ],
  139. },
  140. hooks={
  141. f"const [{unique_var_name}, set_{unique_var_name}] = useState(null);": None,
  142. "useEffect(() => {"
  143. "let isMounted = true;"
  144. f"evalReactComponent({js_string!s})"
  145. ".then((component) => {"
  146. "if (isMounted) {"
  147. f"set_{unique_var_name}(component);"
  148. "}"
  149. "});"
  150. "return () => {"
  151. "isMounted = false;"
  152. "};"
  153. "}"
  154. f", [{js_string!s}]);": None,
  155. },
  156. ),
  157. ),
  158. )