Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 82 additions & 19 deletions packages/reflex-base/src/reflex_base/compiler/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import json
import re
from collections.abc import Iterable, Mapping
from typing import TYPE_CHECKING, Any, Literal

Expand Down Expand Up @@ -161,12 +162,85 @@ def document_root_template(*, imports: list[_ImportDict], document: dict[str, An
}}"""


_JS_IDENTIFIER_RE = re.compile(r"^[A-Za-z_$][\w$]*$")


def _normalize_window_lib_alias(lib: str) -> str:
"""Produce a JS identifier from a library path by stripping ``$/`` and ``@`` and replacing ``/ - .`` with ``_``.

Args:
lib: The library path to normalize.

Returns:
A JS-safe identifier derived from the library path.
"""
return (
lib
.replace("$/", "")
.replace("@", "")
.replace("/", "_")
.replace("-", "_")
.replace(".", "_")
)


def _render_window_reflex_block(
window_library_imports: dict[str, set[str] | None],
) -> tuple[str, str]:
"""Render the extra imports + useEffect block for window.__reflex.

External libraries (``@radix-ui/themes`` etc.) use named imports derived
from the app's actual usage so Rolldown can tree-shake unused exports;
a star import would pin the library's entire surface onto the critical
path. Internal ``$/utils/*`` modules still use star imports since their
surface is small and Reflex-controlled. A library whose declared tag
set contains anything that isn't a valid JS identifier also falls back
to a star import rather than emit a SyntaxError.

Args:
window_library_imports: Mapping from library path to the set of
named exports to expose (external libs) or ``None`` (internal
libs, star import).

Returns:
A tuple of ``(import_block, useEffect_body)``. Both are empty when
no dynamic components are in play.
"""
if not window_library_imports:
return "", ""
import_lines: list[str] = []
entries: list[str] = []
for lib, names in window_library_imports.items():
alias = f"__reflex_{_normalize_window_lib_alias(lib)}"
if names is None or any(not _JS_IDENTIFIER_RE.match(n) for n in names):
import_lines.append(f'import * as {alias} from "{lib}";')
entries.append(f' "{lib}": {alias},')
else:
sorted_names = sorted(names)
specs = ", ".join(f"{n} as {alias}_{n}" for n in sorted_names)
import_lines.append(f'import {{ {specs} }} from "{lib}";')
obj_entries = ", ".join(f"{n}: {alias}_{n}" for n in sorted_names)
entries.append(f' "{lib}": {{ {obj_entries} }},')
if not entries:
return "", ""
import_block = "\n".join(import_lines)
entries_str = "\n".join(entries)
effect = (
" useEffect(() => {\n"
' window["__reflex"] = {\n'
f"{entries_str}\n"
" };\n"
" }, []);\n"
)
return import_block, effect


def app_root_template(
*,
imports: list[_ImportDict],
custom_codes: Iterable[str],
hooks: dict[str, VarData | None],
window_libraries: list[tuple[str, str]],
window_library_imports: dict[str, set[str] | None],
render: dict[str, Any],
dynamic_imports: set[str],
):
Expand All @@ -176,7 +250,8 @@ def app_root_template(
imports: The list of import statements.
custom_codes: The set of custom code snippets.
hooks: The dictionary of hooks.
window_libraries: The list of window libraries.
window_library_imports: Per-library named-export surface for
``window.__reflex`` (see ``collect_window_library_imports``).
render: The dictionary of render functions.
dynamic_imports: The set of dynamic imports.

Expand All @@ -188,14 +263,9 @@ def app_root_template(

custom_code_str = "\n".join(custom_codes)

import_window_libraries = "\n".join([
f'import * as {lib_alias} from "{lib_path}";'
for lib_alias, lib_path in window_libraries
])

window_imports_str = "\n".join([
f' "{lib_path}": {lib_alias},' for lib_alias, lib_path in window_libraries
])
window_imports_block, window_reflex_effect = _render_window_reflex_block(
window_library_imports
)

return f"""
{imports_str}
Expand All @@ -204,19 +274,12 @@ def app_root_template(
import {{ ThemeProvider }} from '$/utils/react-theme';
import {{ Layout as AppLayout }} from './_document';
import {{ Outlet }} from 'react-router';
{import_window_libraries}
{window_imports_block}

{custom_code_str}

function ReflexProviders({{children}}) {{
useEffect(() => {{
// Make contexts and state objects available globally for dynamic eval'd components
let windowImports = {{
{window_imports_str}
}};
window["__reflex"] = windowImports;
}}, []);

{window_reflex_effect}
return jsx(ThemeProvider, {{defaultTheme: defaultColorMode, attribute: "class"}},
jsx(StateProvider, {{}},
jsx(EventLoopProvider, {{}},
Expand Down
16 changes: 16 additions & 0 deletions packages/reflex-base/src/reflex_base/components/dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ def reset_bundled_libraries() -> None:
bundled_libraries.extend(DEFAULT_BUNDLED_LIBRARIES)


# Tags reachable only through eval'd dynamic components -- captured during
# Component serialization so ``collect_window_library_imports`` can expose
# them on ``window.__reflex`` (otherwise ``evalReactComponent`` can't resolve).
dynamic_component_imports: dict[str, set[imports.ImportVar]] = {}


def reset_dynamic_component_imports() -> None:
"""Clear the captured dynamic-component import set."""
dynamic_component_imports.clear()


def bundle_library(component: Union["Component", str]):
"""Bundle a library with the component.

Expand Down Expand Up @@ -102,6 +113,11 @@ def make_component(component: Component) -> str:
component_imports = component._get_all_imports()
compiler._apply_common_imports(component_imports)

for lib, ivs in component_imports.items():
named = {iv for iv in ivs if iv.tag and not iv.is_default}
if named:
dynamic_component_imports.setdefault(lib, set()).update(named)

imports = {}
for lib, names in component_imports.items():
formatted_lib_name = format_library_name(lib)
Expand Down
4 changes: 2 additions & 2 deletions packages/reflex-base/src/reflex_base/plugins/tailwind_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def compile_root_style(include_radix_themes: bool = True):
Returns:
The compiled Tailwind root style.
"""
from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
from reflex_components_radix.plugin import RADIX_THEMES_STYLESHEET

return str(
Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH
Expand Down Expand Up @@ -129,7 +129,7 @@ def add_tailwind_to_css_file(
Returns:
The modified css file content.
"""
from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
from reflex_components_radix.plugin import RADIX_THEMES_STYLESHEET

if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content:
return css_file_content
Expand Down
4 changes: 2 additions & 2 deletions packages/reflex-base/src/reflex_base/plugins/tailwind_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def compile_root_style(include_radix_themes: bool = True):
Returns:
The compiled Tailwind root style.
"""
from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
from reflex_components_radix.plugin import RADIX_THEMES_STYLESHEET

return str(
Path(Dirs.STYLES) / Constants.ROOT_STYLE_PATH
Expand Down Expand Up @@ -133,7 +133,7 @@ def add_tailwind_to_css_file(
Returns:
The modified css file content.
"""
from reflex.compiler.compiler import RADIX_THEMES_STYLESHEET
from reflex_components_radix.plugin import RADIX_THEMES_STYLESHEET

if Constants.TAILWIND_CSS.splitlines()[0] in css_file_content:
return css_file_content
Expand Down
Loading
Loading