Skip to content

Commit 1dd2d1a

Browse files
authored
ENG-8961: Single port is only prod option (#6297)
* single port is only prod option * implement the frontend prod server * remove sirv-cli * fix integration script * respond to greptile comments * ok try that * fix that guy * add finished evaluationing message * add test * we need assignment as well * fix show person from list * fix autosetters * respond to masen feedback
1 parent 90cc0d9 commit 1dd2d1a

18 files changed

Lines changed: 478 additions & 263 deletions

File tree

docs/app/reflex_docs/docgen_pipeline.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,37 @@ def _make_module_name(filename: str) -> str:
7979
return f"{_PARENT_PKG}.{slug}"
8080

8181

82+
def _last_defined_name(content: str) -> str | None:
83+
"""Return the name of the last top-level definition in *content*.
84+
85+
Considers functions, async functions, classes, and simple/annotated
86+
assignments with a value.
87+
88+
Args:
89+
content: A string of Python source code.
90+
91+
Returns:
92+
The name of the last top-level definition, or None if there are none.
93+
"""
94+
import ast
95+
96+
last: str | None = None
97+
for node in ast.parse(content).body:
98+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
99+
last = node.name
100+
elif isinstance(node, ast.Assign):
101+
target = node.targets[0]
102+
if isinstance(target, ast.Name):
103+
last = target.id
104+
elif (
105+
isinstance(node, ast.AnnAssign)
106+
and node.value is not None
107+
and isinstance(node.target, ast.Name)
108+
):
109+
last = node.target.id
110+
return last
111+
112+
82113
def _exec_code(content: str, env: dict, filename: str) -> None:
83114
"""Execute a ``python exec`` code block via an in-memory module.
84115
@@ -379,6 +410,19 @@ def line_break(self, span: LineBreakSpan) -> rx.Component:
379410
# Demo / exec helpers
380411
# ------------------------------------------------------------------
381412

413+
def _exec_and_get_last_callable(self, content: str):
414+
"""Run _exec_code and return the last callable defined by the block."""
415+
_exec_code(content, self.env, self.virtual_filepath)
416+
last_name = _last_defined_name(content)
417+
if last_name is None:
418+
msg = "Exec block defines no function or class"
419+
raise RuntimeError(msg)
420+
last = self.env[last_name]
421+
if not callable(last):
422+
msg = f"Last defined name {last_name!r} is not callable"
423+
raise TypeError(msg)
424+
return last()
425+
382426
def _render_demo(self, content: str, flags: set[str]) -> rx.Component:
383427
"""Render a ``python demo`` block — code + live component."""
384428
comp_id = None
@@ -388,11 +432,9 @@ def _render_demo(self, content: str, flags: set[str]) -> rx.Component:
388432

389433
try:
390434
if "exec" in flags:
391-
_exec_code(content, self.env, self.virtual_filepath)
392-
comp = self.env[list(self.env.keys())[-1]]()
435+
comp = self._exec_and_get_last_callable(content)
393436
elif "graphing" in flags:
394-
_exec_code(content, self.env, self.virtual_filepath)
395-
comp = self.env[list(self.env.keys())[-1]]()
437+
comp = self._exec_and_get_last_callable(content)
396438
parts = content.rpartition("def")
397439
data, code = parts[0], parts[1] + parts[2]
398440
return docgraphing(code, comp=comp, data=data)
@@ -426,11 +468,9 @@ def _render_demo_only(self, content: str, flags: set[str]) -> rx.Component:
426468

427469
try:
428470
if "exec" in flags:
429-
_exec_code(content, self.env, self.virtual_filepath)
430-
comp = self.env[list(self.env.keys())[-1]]()
471+
comp = self._exec_and_get_last_callable(content)
431472
elif "graphing" in flags:
432-
_exec_code(content, self.env, self.virtual_filepath)
433-
comp = self.env[list(self.env.keys())[-1]]()
473+
comp = self._exec_and_get_last_callable(content)
434474
parts = content.rpartition("def")
435475
data, code = parts[0], parts[1] + parts[2]
436476
return docgraphing(code, comp=comp, data=data)

docs/app/reflex_docs/pages/docs/component.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict
101101
name = get_id(f"{component.__qualname__}_{prop.name}")
102102
PropDocsState.add_var(name, bool, False)
103103
var = getattr(PropDocsState, name)
104+
PropDocsState._create_setter(name, var)
104105
setter = getattr(PropDocsState, f"set_{name}")
105106
prop_dict[prop.name] = var
106107
return rx.checkbox(
@@ -133,6 +134,7 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict
133134
name = get_id(f"{component.__qualname__}_{prop.name}")
134135
PropDocsState.add_var(name, str, option)
135136
var = getattr(PropDocsState, name)
137+
PropDocsState._create_setter(name, var)
136138
setter = getattr(PropDocsState, f"set_{name}")
137139
prop_dict[prop.name] = var
138140
return rx.select.root(
@@ -154,6 +156,7 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict
154156
name = get_id(f"{component.__qualname__}_{prop.name}")
155157
PropDocsState.add_var(name, str, option)
156158
var = getattr(PropDocsState, name)
159+
PropDocsState._create_setter(name, var)
157160
setter = getattr(PropDocsState, f"set_{name}")
158161
prop_dict[prop.name] = var
159162

docs/app/rxconfig.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import reflex as rx
22

33
config = rx.Config(
4-
state_auto_setters=True,
5-
port=3000,
64
app_name="reflex_docs",
75
deploy_url="https://reflex.dev",
86
frontend_packages=[
97
"tailwindcss-animated",
108
],
11-
show_build_with_reflex=True,
129
telemetry_enabled=False,
1310
plugins=[rx.plugins.TailwindV4Plugin(), rx.plugins.SitemapPlugin()],
1411
)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Test that evaluating a markdown document twice doesn't break exec blocks.
2+
3+
This reproduces a CI failure where Granian's worker re-evaluates stateful pages
4+
in the same process after the initial compilation. The module-level cache in
5+
_exec_code causes an earlier exec-only block to pre-populate the transformer's
6+
env with names from *all* blocks on the second pass, so
7+
_exec_and_get_last_callable finds no "new" keys and raises RuntimeError.
8+
"""
9+
10+
import sys
11+
from pathlib import Path
12+
13+
import pytest
14+
15+
sys.path.insert(0, str(Path(__file__).parent.parent))
16+
17+
18+
MD_WITH_TWO_EXEC_BLOCKS = """\
19+
```python exec
20+
import reflex as rx
21+
```
22+
23+
# Demo page
24+
25+
```python demo exec
26+
class MyState(rx.State):
27+
count: int = 0
28+
29+
def my_demo():
30+
return rx.text(MyState.count)
31+
```
32+
"""
33+
34+
35+
@pytest.fixture(autouse=True)
36+
def _clear_exec_caches():
37+
"""Reset the module-level caches so each test starts clean."""
38+
from reflex_docs.docgen_pipeline import _executed_blocks, _file_modules
39+
40+
old_blocks = _executed_blocks.copy()
41+
old_modules = _file_modules.copy()
42+
_executed_blocks.clear()
43+
_file_modules.clear()
44+
yield
45+
_executed_blocks.clear()
46+
_executed_blocks.update(old_blocks)
47+
_file_modules.clear()
48+
_file_modules.update(old_modules)
49+
50+
51+
def _render_once(text: str, virtual_filepath: str = "test_double_eval.md"):
52+
from reflex_docgen.markdown import parse_document
53+
54+
from reflex_docs.docgen_pipeline import ReflexDocTransformer
55+
56+
doc = parse_document(text)
57+
transformer = ReflexDocTransformer(
58+
virtual_filepath=virtual_filepath, filename=virtual_filepath
59+
)
60+
return transformer.transform(doc)
61+
62+
63+
def test_double_eval_does_not_crash():
64+
"""Evaluating the same markdown twice must not raise 'Exec block defined nothing new'."""
65+
# First pass — simulates the initial compilation.
66+
_render_once(MD_WITH_TWO_EXEC_BLOCKS)
67+
68+
# Second pass — simulates the Granian worker re-evaluating stateful pages.
69+
# This is the call that fails before the fix.
70+
_render_once(MD_WITH_TWO_EXEC_BLOCKS)
71+
72+
73+
def test_double_eval_browser_javascript():
74+
"""The actual file that triggered the CI failure."""
75+
filepath = (
76+
Path(__file__).parent.parent.parent / "api-reference" / "browser_javascript.md"
77+
)
78+
if not filepath.exists():
79+
pytest.skip(f"{filepath} not found")
80+
81+
from reflex_docs.docgen_pipeline import render_docgen_document
82+
83+
vpath = "docs/api-reference/browser-javascript"
84+
render_docgen_document(vpath, filepath)
85+
render_docgen_document(vpath, filepath)
86+
87+
88+
# ---------------------------------------------------------------------------
89+
# Parametrized test: evaluate every markdown doc file twice
90+
# ---------------------------------------------------------------------------
91+
92+
_app_root = Path(__file__).resolve().parent.parent # …/app/
93+
_docs_dir = _app_root.parent # …/docs/ (parent of app/)
94+
95+
_all_docs: dict[str, str] = {} # virtual_path → actual_path
96+
for _md_file in sorted(_docs_dir.rglob("*.md")):
97+
if _md_file.is_relative_to(_app_root):
98+
continue
99+
_virtual = "docs/" + str(_md_file.relative_to(_docs_dir)).replace("\\", "/")
100+
_all_docs[_virtual] = str(_md_file)
101+
102+
103+
@pytest.fixture(params=list(_all_docs.keys()))
104+
def doc_file(request) -> tuple[str, str]:
105+
"""Yield (virtual_path, actual_path) for each discovered markdown doc."""
106+
virtual_path = request.param
107+
return virtual_path, _all_docs[virtual_path]
108+
109+
110+
def test_double_eval_all_docs(doc_file: tuple[str, str]):
111+
"""Every markdown doc must survive two evaluations without error."""
112+
from reflex_docs.docgen_pipeline import render_docgen_document
113+
114+
virtual_path, actual_path = doc_file
115+
render_docgen_document(virtual_path, actual_path)
116+
render_docgen_document(virtual_path, actual_path)

docs/enterprise/ag_grid/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ column_defs = [
157157
]
158158

159159

160-
def ag_grid_simple_column_filtering():
160+
def ag_grid_column_filter_types():
161161
return rxe.ag_grid(
162162
id="ag_grid_basic_column_filtering",
163163
row_data=df.to_dict("records"),

docs/library/graphing/general/axis.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,14 @@ class AxisState(rx.State):
120120

121121
label_offsets: list[str] = ["-30", "-20", "-10", "0", "10", "20", "30"]
122122

123-
x_axis_postion: str = "bottom"
123+
x_axis_position: str = "bottom"
124124

125125
x_axis_offset: int
126126

127-
y_axis_postion: str = "left"
127+
y_axis_position: str = "left"
128128

129129
y_axis_offset: int
130130

131-
@rx.event
132131
@rx.event
133132
def set_y_axis_position(self, position: str):
134133
self.y_axis_position = position
@@ -169,7 +168,7 @@ def axis_labels():
169168
data_key="name",
170169
label={
171170
"value": "Pages",
172-
"position": AxisState.x_axis_postion,
171+
"position": AxisState.x_axis_position,
173172
"offset": AxisState.x_axis_offset,
174173
},
175174
),
@@ -178,7 +177,7 @@ def axis_labels():
178177
label={
179178
"value": "Views",
180179
"angle": -90,
181-
"position": AxisState.y_axis_postion,
180+
"position": AxisState.y_axis_position,
182181
"offset": AxisState.y_axis_offset,
183182
},
184183
),
@@ -195,8 +194,8 @@ def axis_labels():
195194
rx.text("X Label Position: "),
196195
rx.select(
197196
AxisState.label_positions,
198-
value=AxisState.x_axis_postion,
199-
on_change=AxisState.set_x_axis_postion,
197+
value=AxisState.x_axis_position,
198+
on_change=AxisState.set_x_axis_position,
200199
),
201200
rx.text("X Label Offset: "),
202201
rx.select(
@@ -207,8 +206,8 @@ def axis_labels():
207206
rx.text("Y Label Position: "),
208207
rx.select(
209208
AxisState.label_positions,
210-
value=AxisState.y_axis_postion,
211-
on_change=AxisState.set_y_axis_postion,
209+
value=AxisState.y_axis_position,
210+
on_change=AxisState.set_y_axis_position,
212211
),
213212
rx.text("Y Label Offset: "),
214213
rx.select(

docs/library/tables-and-data-grids/table.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ class TableForEachState(rx.State):
204204
]
205205

206206

207-
def show_person(person: list):
207+
def show_person_from_list(person: list):
208208
"""Show a person in a table row."""
209209
return rx.table.row(
210210
rx.table.cell(person[0]),
@@ -222,7 +222,7 @@ def foreach_table_example():
222222
rx.table.column_header_cell("Group"),
223223
),
224224
),
225-
rx.table.body(rx.foreach(TableForEachState.people, show_person)),
225+
rx.table.body(rx.foreach(TableForEachState.people, show_person_from_list)),
226226
width="100%",
227227
)
228228
```

docs/state_structure/shared_state.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ class CollaborativeCounter(rx.SharedState):
4444
linked_state = await self._link_to("shared-global-counter")
4545
linked_state.count += 1 # Increment count on link
4646

47+
@rx.event
48+
def set_count(self, count: int):
49+
self.count = count
50+
4751
@rx.var
4852
def is_linked(self) -> bool:
4953
return bool(self._linked_to)

packages/reflex-base/src/reflex_base/constants/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
ReactRouter,
2121
Reflex,
2222
ReflexHostingCLI,
23+
RunningMode,
2324
Templates,
2425
)
2526
from .compiler import (

packages/reflex-base/src/reflex_base/constants/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,22 @@ class Env(str, Enum):
202202
PROD = "prod"
203203

204204

205+
class RunningMode(str, Enum):
206+
"""The running modes."""
207+
208+
FRONTEND_ONLY = "frontend-only"
209+
BACKEND_ONLY = "backend-only"
210+
FULLSTACK = "fullstack"
211+
212+
def has_frontend(self) -> bool:
213+
"""Return whether the running mode includes the frontend."""
214+
return self in (RunningMode.FRONTEND_ONLY, RunningMode.FULLSTACK)
215+
216+
def has_backend(self) -> bool:
217+
"""Return whether the running mode includes the backend."""
218+
return self in (RunningMode.BACKEND_ONLY, RunningMode.FULLSTACK)
219+
220+
205221
# Log levels
206222
class LogLevel(str, Enum):
207223
"""The log levels."""

0 commit comments

Comments
 (0)