dynamic.py 6.2 KB

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