Skip to content

Commit 15f363b

Browse files
committed
Add compiler plugin hooks and move compilation pipeline out of App
Move the frontend compilation pipeline from App._compile into compiler.compile_app(), introducing a CompilerPlugin protocol with enter_component/leave_component/eval_page/compile_page hooks. Remove the ExecutorType/ExecutorSafeFunctions abstractions in favor of a sequential plugin-driven compilation model.
1 parent 2178184 commit 15f363b

File tree

14 files changed

+3030
-757
lines changed

14 files changed

+3030
-757
lines changed

packages/reflex-core/src/reflex_core/components/component.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,7 @@ def _iter_parent_classes_names(cls) -> Iterator[str]:
15561556
yield clz.__name__
15571557

15581558
@classmethod
1559+
@functools.cache
15591560
def _iter_parent_classes_with_method(cls, method: str) -> Sequence[type[Component]]:
15601561
"""Iterate through parent classes that define a given method.
15611562
@@ -1582,7 +1583,7 @@ def _iter_parent_classes_with_method(cls, method: str) -> Sequence[type[Componen
15821583
continue
15831584
seen_methods.add(method_func)
15841585
clzs.append(clz)
1585-
return clzs
1586+
return tuple(clzs)
15861587

15871588
def _get_custom_code(self) -> str | None:
15881589
"""Get custom code for the component.

packages/reflex-core/src/reflex_core/environment.py

Lines changed: 1 addition & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22

33
from __future__ import annotations
44

5-
import concurrent.futures
65
import dataclasses
76
import enum
87
import importlib
9-
import multiprocessing
108
import os
11-
import platform
12-
from collections.abc import Callable, Sequence
9+
from collections.abc import Sequence
1310
from functools import lru_cache
1411
from pathlib import Path
1512
from typing import (
@@ -529,97 +526,6 @@ class PerformanceMode(enum.Enum):
529526
OFF = "off"
530527

531528

532-
class ExecutorType(enum.Enum):
533-
"""Executor for compiling the frontend."""
534-
535-
THREAD = "thread"
536-
PROCESS = "process"
537-
MAIN_THREAD = "main_thread"
538-
539-
@classmethod
540-
def get_executor_from_environment(cls):
541-
"""Get the executor based on the environment variables.
542-
543-
Returns:
544-
The executor.
545-
"""
546-
from reflex_core.utils import console
547-
548-
executor_type = environment.REFLEX_COMPILE_EXECUTOR.get()
549-
550-
reflex_compile_processes = environment.REFLEX_COMPILE_PROCESSES.get()
551-
reflex_compile_threads = environment.REFLEX_COMPILE_THREADS.get()
552-
# By default, use the main thread. Unless the user has specified a different executor.
553-
# Using a process pool is much faster, but not supported on all platforms. It's gated behind a flag.
554-
if executor_type is None:
555-
if (
556-
platform.system() not in ("Linux", "Darwin")
557-
and reflex_compile_processes is not None
558-
):
559-
console.warn("Multiprocessing is only supported on Linux and MacOS.")
560-
561-
if (
562-
platform.system() in ("Linux", "Darwin")
563-
and reflex_compile_processes is not None
564-
):
565-
if reflex_compile_processes == 0:
566-
console.warn(
567-
"Number of processes must be greater than 0. If you want to use the default number of processes, set REFLEX_COMPILE_EXECUTOR to 'process'. Defaulting to None."
568-
)
569-
reflex_compile_processes = None
570-
elif reflex_compile_processes < 0:
571-
console.warn(
572-
"Number of processes must be greater than 0. Defaulting to None."
573-
)
574-
reflex_compile_processes = None
575-
executor_type = ExecutorType.PROCESS
576-
elif reflex_compile_threads is not None:
577-
if reflex_compile_threads == 0:
578-
console.warn(
579-
"Number of threads must be greater than 0. If you want to use the default number of threads, set REFLEX_COMPILE_EXECUTOR to 'thread'. Defaulting to None."
580-
)
581-
reflex_compile_threads = None
582-
elif reflex_compile_threads < 0:
583-
console.warn(
584-
"Number of threads must be greater than 0. Defaulting to None."
585-
)
586-
reflex_compile_threads = None
587-
executor_type = ExecutorType.THREAD
588-
else:
589-
executor_type = ExecutorType.MAIN_THREAD
590-
591-
match executor_type:
592-
case ExecutorType.PROCESS:
593-
executor = concurrent.futures.ProcessPoolExecutor(
594-
max_workers=reflex_compile_processes,
595-
mp_context=multiprocessing.get_context("fork"),
596-
)
597-
case ExecutorType.THREAD:
598-
executor = concurrent.futures.ThreadPoolExecutor(
599-
max_workers=reflex_compile_threads
600-
)
601-
case ExecutorType.MAIN_THREAD:
602-
FUTURE_RESULT_TYPE = TypeVar("FUTURE_RESULT_TYPE")
603-
604-
class MainThreadExecutor:
605-
def __enter__(self):
606-
return self
607-
608-
def __exit__(self, *args):
609-
pass
610-
611-
def submit(
612-
self, fn: Callable[..., FUTURE_RESULT_TYPE], *args, **kwargs
613-
) -> concurrent.futures.Future[FUTURE_RESULT_TYPE]:
614-
future_job = concurrent.futures.Future()
615-
future_job.set_result(fn(*args, **kwargs))
616-
return future_job
617-
618-
executor = MainThreadExecutor()
619-
620-
return executor
621-
622-
623529
class EnvironmentVariables:
624530
"""Environment variables class to instantiate environment variables."""
625531

@@ -660,14 +566,6 @@ class EnvironmentVariables:
660566
Path(constants.Dirs.UPLOADED_FILES)
661567
)
662568

663-
REFLEX_COMPILE_EXECUTOR: EnvVar[ExecutorType | None] = env_var(None)
664-
665-
# Whether to use separate processes to compile the frontend and how many. If not set, defaults to thread executor.
666-
REFLEX_COMPILE_PROCESSES: EnvVar[int | None] = env_var(None)
667-
668-
# Whether to use separate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`.
669-
REFLEX_COMPILE_THREADS: EnvVar[int | None] = env_var(None)
670-
671569
# The directory to store reflex dependencies.
672570
REFLEX_DIR: EnvVar[Path] = env_var(constants.Reflex.DIR)
673571

packages/reflex-core/src/reflex_core/plugins/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,28 @@
22

33
from ._screenshot import ScreenshotPlugin as _ScreenshotPlugin
44
from .base import CommonContext, Plugin, PreCompileContext
5+
from .compiler import (
6+
BaseContext,
7+
CompileContext,
8+
CompilerHooks,
9+
CompilerPlugin,
10+
ComponentAndChildren,
11+
PageContext,
12+
PageDefinition,
13+
)
514
from .sitemap import SitemapPlugin
615
from .tailwind_v3 import TailwindV3Plugin
716
from .tailwind_v4 import TailwindV4Plugin
817

918
__all__ = [
19+
"BaseContext",
1020
"CommonContext",
21+
"CompileContext",
22+
"CompilerHooks",
23+
"CompilerPlugin",
24+
"ComponentAndChildren",
25+
"PageContext",
26+
"PageDefinition",
1127
"Plugin",
1228
"PreCompileContext",
1329
"SitemapPlugin",

packages/reflex-core/src/reflex_core/plugins/base.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
from collections.abc import Callable, Sequence
44
from pathlib import Path
5-
from typing import TYPE_CHECKING, ParamSpec, Protocol, TypedDict
5+
from typing import TYPE_CHECKING, Any, ParamSpec, Protocol, TypedDict
66

77
from typing_extensions import Unpack
88

99
if TYPE_CHECKING:
1010
from reflex.app import App, UnevaluatedPage
11+
from reflex_core.components.component import BaseComponent, StatefulComponent
12+
from reflex_core.plugins.compiler import ComponentAndChildren, PageContext
1113

1214

1315
class CommonContext(TypedDict):
@@ -117,6 +119,93 @@ def post_compile(self, **context: Unpack[PostCompileContext]) -> None:
117119
context: The context for the plugin.
118120
"""
119121

122+
def eval_page(
123+
self,
124+
page_fn: Any,
125+
/,
126+
**kwargs: Any,
127+
) -> "PageContext | None":
128+
"""Evaluate a page-like object into a page context.
129+
130+
Args:
131+
page_fn: The page-like object to evaluate.
132+
kwargs: Additional compiler-specific context.
133+
134+
Returns:
135+
A page context when the plugin can evaluate the page, otherwise ``None``.
136+
"""
137+
del page_fn, kwargs
138+
return None
139+
140+
def compile_page(
141+
self,
142+
page_ctx: "PageContext",
143+
/,
144+
**kwargs: Any,
145+
) -> None:
146+
"""Finalize a page context after its component tree has been traversed."""
147+
del page_ctx, kwargs
148+
return
149+
150+
def enter_component(
151+
self,
152+
comp: "BaseComponent",
153+
/,
154+
*,
155+
page_context: "PageContext",
156+
compile_context: Any,
157+
in_prop_tree: bool = False,
158+
stateful_component: "StatefulComponent | None" = None,
159+
) -> "BaseComponent | ComponentAndChildren | None":
160+
"""Inspect or transform a component before visiting its descendants.
161+
162+
Args:
163+
comp: The component being compiled.
164+
page_context: The active page compilation state.
165+
compile_context: The active compile-run state.
166+
in_prop_tree: Whether the component is being visited through a prop subtree.
167+
stateful_component: The surrounding stateful component, when applicable.
168+
169+
Returns:
170+
An optional replacement component and/or structural children.
171+
"""
172+
del comp, page_context, compile_context, in_prop_tree, stateful_component
173+
return None
174+
175+
def leave_component(
176+
self,
177+
comp: "BaseComponent",
178+
children: tuple["BaseComponent", ...],
179+
/,
180+
*,
181+
page_context: "PageContext",
182+
compile_context: Any,
183+
in_prop_tree: bool = False,
184+
stateful_component: "StatefulComponent | None" = None,
185+
) -> "BaseComponent | ComponentAndChildren | None":
186+
"""Inspect or transform a component after visiting its descendants.
187+
188+
Args:
189+
comp: The component being compiled.
190+
children: The compiled structural children for the component.
191+
page_context: The active page compilation state.
192+
compile_context: The active compile-run state.
193+
in_prop_tree: Whether the component is being visited through a prop subtree.
194+
stateful_component: The surrounding stateful component, when applicable.
195+
196+
Returns:
197+
An optional replacement component and/or structural children.
198+
"""
199+
del (
200+
comp,
201+
children,
202+
page_context,
203+
compile_context,
204+
in_prop_tree,
205+
stateful_component,
206+
)
207+
return None
208+
120209
def __repr__(self):
121210
"""Return a string representation of the plugin.
122211

0 commit comments

Comments
 (0)