Skip to content

Commit dfb9612

Browse files
committed
automagically bind react and react-dom if missing from page
1 parent 696807a commit dfb9612

File tree

4 files changed

+80
-40
lines changed

4 files changed

+80
-40
lines changed

src/js/packages/@reactpy/client/src/bind.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@ export async function infer_bind_from_environment() {
66
const React = await import("react");
77
// @ts-ignore
88
const ReactDOM = await import("react-dom/client");
9-
10-
console.log(
11-
"ReactPy detected 'ReactJS' to bind your JavaScript components.",
12-
);
139
return (node: HTMLElement) => reactjs_bind(node, React, ReactDOM);
1410
} catch {
15-
console.debug(
16-
"ReactPy did not detect a component binding function or a suitable 'importmap'. Using ReactPy's internal framework (Preact) to bind your JavaScript components.",
11+
console.error(
12+
"Unknown error occurred: 'react' is missing within this ReactPy environment! \
13+
Your JavaScript components may not work as expected!",
1714
);
1815
return (node: HTMLElement) => local_preact_bind(node);
1916
}

src/reactpy/executors/utils.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,41 @@ def server_side_component_html(
6464
) -> str:
6565
return (
6666
f'<div id="{element_id}" class="{class_}"></div>'
67+
"<script>"
68+
'if (!document.querySelector("#reactpy-importmap")) {'
69+
" console.debug("
70+
' "ReactPy did not detect a suitable JavaScript import map. Falling back to ReactPy\'s internal framework (Preact)."'
71+
" );"
72+
' const im = document.createElement("script");'
73+
' im.type = "importmap";'
74+
f" im.textContent = '{default_import_map()}';"
75+
' im.id = "reactpy-importmap";'
76+
" document.head.appendChild(im);"
77+
" delete im;"
78+
"}"
79+
"</script>"
6780
'<script type="module" crossorigin="anonymous">'
6881
f'import {{ mountReactPy }} from "{REACTPY_PATH_PREFIX.current}static/index.js";'
6982
"mountReactPy({"
70-
f' mountElement: document.getElementById("{element_id}"),'
71-
f' pathPrefix: "{REACTPY_PATH_PREFIX.current}",'
72-
f' componentPath: "{component_path}",'
73-
f" reconnectInterval: {REACTPY_RECONNECT_INTERVAL.current},"
74-
f" reconnectMaxInterval: {REACTPY_RECONNECT_MAX_INTERVAL.current},"
75-
f" reconnectMaxRetries: {REACTPY_RECONNECT_MAX_RETRIES.current},"
76-
f" reconnectBackoffMultiplier: {REACTPY_RECONNECT_BACKOFF_MULTIPLIER.current},"
83+
f' mountElement: document.getElementById("{element_id}"),'
84+
f' pathPrefix: "{REACTPY_PATH_PREFIX.current}",'
85+
f' componentPath: "{component_path}",'
86+
f" reconnectInterval: {REACTPY_RECONNECT_INTERVAL.current},"
87+
f" reconnectMaxInterval: {REACTPY_RECONNECT_MAX_INTERVAL.current},"
88+
f" reconnectMaxRetries: {REACTPY_RECONNECT_MAX_RETRIES.current},"
89+
f" reconnectBackoffMultiplier: {REACTPY_RECONNECT_BACKOFF_MULTIPLIER.current},"
7790
"});"
7891
"</script>"
7992
)
93+
94+
95+
def default_import_map() -> str:
96+
path_prefix = REACTPY_PATH_PREFIX.current.strip("/")
97+
return f"""{{
98+
"imports": {{
99+
"react": "/{path_prefix}/static/preact.js",
100+
"react-dom": "/{path_prefix}/static/preact-dom.js",
101+
"react-dom/client": "/{path_prefix}/static/preact-dom.js",
102+
"react/jsx-runtime": "/{path_prefix}/static/preact-jsx-runtime.js"
103+
}}
104+
}}""".replace("\n", "").replace(" ", "")

src/reactpy/reactjs/module.py

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from pathlib import Path, PurePosixPath
55
from typing import Any, Literal
66

7-
from reactpy import config
87
from reactpy.config import REACTPY_DEBUG, REACTPY_WEB_MODULES_DIR
98
from reactpy.core.vdom import Vdom
109
from reactpy.reactjs.types import NAME_SOURCE, URL_SOURCE
@@ -15,7 +14,7 @@
1514
resolve_names_from_file,
1615
resolve_names_from_url,
1716
)
18-
from reactpy.types import ImportSourceDict, JavaScriptModule, VdomConstructor
17+
from reactpy.types import ImportSourceDict, JavaScriptModule, VdomConstructor, VdomDict
1918

2019
logger = logging.getLogger(__name__)
2120

@@ -183,7 +182,7 @@ def import_reactjs(
183182
framework: Literal["preact", "react"] | None = None,
184183
version: str | None = None,
185184
use_local: bool = False,
186-
):
185+
) -> VdomDict:
187186
"""
188187
Return an import map script tag for ReactJS or Preact.
189188
Parameters:
@@ -205,6 +204,7 @@ def import_reactjs(
205204
A VDOM script tag containing the import map.
206205
"""
207206
from reactpy import html
207+
from reactpy.executors.utils import default_import_map
208208

209209
if use_local and (framework or version): # nocov
210210
raise ValueError("use_local cannot be used with framework or version")
@@ -215,55 +215,41 @@ def import_reactjs(
215215

216216
# Import map for ReactPy's local framework (re-exported/bundled/minified version of Preact)
217217
if use_local:
218-
prefix = f"/{config.REACTPY_PATH_PREFIX.current.strip('/')}/static/{framework}"
219218
return html.script(
220-
{"type": "importmap"},
221-
f"""
222-
{{
223-
"imports": {{
224-
"react": "{prefix}.js",
225-
"react-dom": "{prefix}-dom.js",
226-
"react-dom/client": "{prefix}-dom.js",
227-
"react/jsx-runtime": "{prefix}-jsx-runtime.js"
228-
}}
229-
}}
230-
""",
219+
{"type": "importmap", "id": "reactpy-importmap"},
220+
default_import_map(),
231221
)
232222

233223
# Import map for ReactJS from esm.sh
234224
if framework == "react":
235225
version = version or "19"
236226
postfix = "?dev" if REACTPY_DEBUG.current else ""
237227
return html.script(
238-
{"type": "importmap"},
239-
f"""
240-
{{
228+
{"type": "importmap", "id": "reactpy-importmap"},
229+
f"""{{
241230
"imports": {{
242231
"react": "https://esm.sh/react@{version}{postfix}",
243232
"react-dom": "https://esm.sh/react-dom@{version}{postfix}",
244233
"react-dom/client": "https://esm.sh/react-dom@{version}/client{postfix}",
245234
"react/jsx-runtime": "https://esm.sh/react@{version}/jsx-runtime{postfix}"
246235
}}
247-
}}
248-
""",
236+
}}""".replace("\n", "").replace(" ", ""),
249237
)
250238

251239
# Import map for Preact from esm.sh
252240
if framework == "preact":
253241
version = version or "10"
254242
postfix = "?dev" if REACTPY_DEBUG.current else ""
255243
return html.script(
256-
{"type": "importmap"},
257-
f"""
258-
{{
244+
{"type": "importmap", "id": "reactpy-importmap"},
245+
f"""{{
259246
"imports": {{
260247
"react": "https://esm.sh/preact@{version}/compat{postfix}",
261248
"react-dom": "https://esm.sh/preact@{version}/compat{postfix}",
262249
"react-dom/client": "https://esm.sh/preact@{version}/compat/client{postfix}",
263250
"react/jsx-runtime": "https://esm.sh/preact@{version}/compat/jsx-runtime{postfix}"
264251
}}
265-
}}
266-
""",
252+
}}""".replace("\n", "").replace(" ", ""),
267253
)
268254

269255

tests/test_reactjs/test_modules_from_npm.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from reactpy.reactjs import component_from_npm, import_reactjs
77
from reactpy.testing import GITHUB_ACTIONS, BackendFixture, DisplayFixture
88

9+
MISSING_IMPORT_MAP_MSG = "ReactPy did not detect a suitable import map on your page."
10+
911

1012
@pytest.fixture(scope="module")
1113
async def display(browser):
@@ -16,7 +18,7 @@ async def display(browser):
1618

1719

1820
@pytest.mark.flaky(reruns=10 if GITHUB_ACTIONS else 1)
19-
async def test_component_from_npm_react_bootstrap(display: DisplayFixture):
21+
async def test_component_from_npm_react_bootstrap(display: DisplayFixture, caplog):
2022
Button = component_from_npm("react-bootstrap", "Button", version="2")
2123

2224
@reactpy.component
@@ -33,9 +35,12 @@ def App():
3335
await expect(button).to_contain_class("btn")
3436
await expect(button).to_contain_class("btn-primary")
3537

38+
# Ensure missing import map was NOT logged
39+
assert MISSING_IMPORT_MAP_MSG not in " ".join(x.msg for x in caplog.records)
40+
3641

3742
@pytest.mark.flaky(reruns=10 if GITHUB_ACTIONS else 1)
38-
async def test_component_from_npm_react_bootstrap_with_local_framework(browser):
43+
async def test_component_from_npm_react_bootstrap_with_local_framework(browser, caplog):
3944
Button = component_from_npm("react-bootstrap", "Button", version="2")
4045

4146
@reactpy.component
@@ -56,6 +61,33 @@ def App():
5661
await expect(button).to_contain_class("btn")
5762
await expect(button).to_contain_class("btn-primary")
5863

64+
# Ensure missing import map was NOT logged
65+
assert MISSING_IMPORT_MAP_MSG not in " ".join(x.msg for x in caplog.records)
66+
67+
68+
@pytest.mark.flaky(reruns=10 if GITHUB_ACTIONS else 1)
69+
async def test_component_from_npm_without_explicit_reactjs_import(browser, caplog):
70+
Button = component_from_npm("react-bootstrap", "Button", version="2")
71+
72+
@reactpy.component
73+
def App():
74+
return Button({"variant": "primary", "id": "test-button"}, "Click me")
75+
76+
async with BackendFixture() as backend:
77+
async with DisplayFixture(backend=backend, browser=browser) as display:
78+
await display.show(App)
79+
80+
button = display.page.locator("#test-button")
81+
await expect(button).to_have_text("Click me")
82+
83+
# Check if it has the correct class for primary variant
84+
# React Bootstrap buttons usually have 'btn' and 'btn-primary' classes
85+
await expect(button).to_contain_class("btn")
86+
await expect(button).to_contain_class("btn-primary")
87+
88+
# Check if missing import map was logged
89+
assert MISSING_IMPORT_MAP_MSG in " ".join(x.msg for x in caplog.records)
90+
5991

6092
@pytest.mark.flaky(reruns=10 if GITHUB_ACTIONS else 1)
6193
async def test_component_from_npm_material_ui(display: DisplayFixture):

0 commit comments

Comments
 (0)