Skip to content

Add compiler plugin hooks and plugins and move compilation pipeline out of App#6260

Open
FarhanAliRaza wants to merge 32 commits intoreflex-dev:mainfrom
FarhanAliRaza:compiler-hooks
Open

Add compiler plugin hooks and plugins and move compilation pipeline out of App#6260
FarhanAliRaza wants to merge 32 commits intoreflex-dev:mainfrom
FarhanAliRaza:compiler-hooks

Conversation

@FarhanAliRaza
Copy link
Copy Markdown
Collaborator

@FarhanAliRaza FarhanAliRaza commented Mar 31, 2026

Summary

  • Introduce a CompilerPlugin protocol with enter_component/leave_component/eval_page/compile_page hooks, enabling third-party plugins to participate in the frontend compilation pipeline
  • Move the frontend compilation pipeline from App._compile() into compiler.compile_app(), decoupling compilation logic from the App class
  • Remove ExecutorType/ExecutorSafeFunctions abstractions from environment.py in favor of a sequential plugin-driven compilation model
  • Add built-in plugins: DefaultPagePlugin, ApplyStylePlugin, DefaultCollectorPlugin that replicate the existing compilation behavior
  • Fix memo component compilation ordering so app-wrap components are included
  • Fix DefaultPagePlugin to preserve Var-backed page titles instead of replacing them with the default string

Key Changes

  • packages/reflex-core/src/reflex_core/plugins/compiler.py (new): Core plugin infrastructure — CompilerPlugin protocol, CompileContext, PageContext, CompilerHooks, and component tree traversal logic
  • reflex/compiler/plugins/builtin.py (new): Built-in plugins (DefaultPagePlugin, ApplyStylePlugin, DefaultCollectorPlugin) that replicate existing compilation behavior
  • reflex/compiler/compiler.py: Refactored compile_app() to use plugin-driven compilation via CompilerHooks
  • reflex/app.py: Removed ~430 lines of compilation logic, App._compile() now delegates to compiler.compile_app()
  • packages/reflex-core/src/reflex_core/environment.py: Removed ExecutorType and ExecutorSafeFunctions (no longer needed)

Test Plan

  • New unit tests in tests/units/compiler/test_plugins.py (855 lines) covering:
    • Plugin hook dispatch (enter/leave component, eval/compile page)
    • CompileContext lifecycle and state management
    • DefaultPagePlugin behavior including Var-backed titles
    • ApplyStylePlugin style application
    • DefaultCollectorPlugin import/custom code collection
    • Route deduplication and error handling
    • End-to-end compilation matching legacy output
  • New test in tests/units/test_app.py verifying memo components are written to shared components module
  • Existing test suite passes without modification

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

closes #6210
closes #6211
closes #6212
Closes #6213

@FarhanAliRaza FarhanAliRaza marked this pull request as draft March 31, 2026 16:50
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 31, 2026

Greptile Summary

This PR lays the plugin-architecture foundations for single-pass page compilation in Reflex, introducing reflex/compiler/plugins.py with CompilerPlugin protocol, CompilerHooks dispatcher, and BaseContext/PageContext/CompileContext task-local context managers. It also fixes a flaky Redis oplock-contention test and ships a comprehensive 435-line test suite.

Key changes:

  • CompilerPlugin protocol with three async hooks (eval_page, compile_page, compile_component as an async generator for pre/post tree traversal)
  • CompilerHooks implements stop-on-first-result semantics for eval_page and middleware-style enter/unwind for compile_component
  • BaseContext.__init_subclass__ creates a per-subclass ContextVar, giving each context type task-local isolation without requiring manual registration
  • PageContext accumulates imports, hooks, custom code, dynamic imports, refs, and app-wrap components; CompileContext orchestrates multi-page compilation with duplicate-route detection
  • Redis test fix replaces a racy shared event (set by whichever of two tasks happened to run first) with a counter that only fires after both contenders have executed past their synchronous setup

Findings:

  • Prop-component traversal in compile_component discards the return value — no inline comment at the call site to warn plugin authors
  • Unwind loop's _compile_children call can receive None if a plugin yields a malformed tuple — a defensive assertion would give a clearer error
  • mark_contender_started is defined twice with identical logic in two test functions

Confidence Score: 5/5

Safe to merge — all remaining findings are P2 style and documentation suggestions with no impact on runtime correctness.

The core plugin machinery is well-designed and the test coverage is thorough. All three findings are P2: a missing inline comment on the intentional prop-component side-effect-only traversal, a defensive assertion that would improve error messages for malformed plugin generators (not triggerable by correctly-typed plugins), and duplicated test helper logic. None affect correctness or reliability.

reflex/compiler/plugins.py — the prop-component traversal asymmetry and the unwind-loop None-children path are worth a second look before the plugin API is widely adopted.

Important Files Changed

Filename Overview
reflex/compiler/plugins.py New module introducing plugin protocol, CompilerHooks dispatcher, BaseContext/PageContext/CompileContext — well-structured; minor issue with silently-discarded prop-component return values and a latent None-children path in the unwind loop.
tests/units/compiler/test_plugins.py Comprehensive 435-line test suite covering plugin dispatch ordering, tree traversal, context lifecycle, data accumulation, duplicate-route rejection, and factory isolation — thorough and well-organized.
tests/units/istate/manager/test_redis.py Flaky-test fix replaces racy single-event pattern with a counter that fires only after both contending tasks have started; correct for asyncio's cooperative scheduler. Minor: mark_contender_started logic is duplicated in two test functions.
pyproject.toml Adds "asend" to codespell's ignore list to suppress false positives from the async generator send API.

Sequence Diagram

sequenceDiagram
    participant CC as CompileContext.compile()
    participant CH as CompilerHooks
    participant PC as PageContext
    participant P as Plugin (ordered chain)

    CC->>CH: eval_page(page_fn) [stop-on-first]
    CH->>P: eval_page() — first plugin returning non-None wins
    P-->>CH: PageContext
    CH-->>CC: PageContext

    CC->>PC: async with page_ctx (attach ContextVar)

    loop For each structural child
        CC->>CH: compile_component(comp)
        CH->>P: plugin.compile_component() → AsyncGenerator
        Note over P: PRE: anext(gen) — enter phase (registration order)
        CH->>CH: _compile_children(structural_children) [recursive]
        CH->>CH: compile_component(prop_components) [side-effects only]
        Note over P: POST: gen.asend((comp, children)) — unwind (reverse order)
        CH-->>CC: compiled_component
    end

    CC->>CH: compile_page(page_ctx)
    CH->>P: compile_page() — all plugins, registration order
    P-->>CH: (void)

    CC->>PC: exit async with (detach ContextVar)
    CC->>CC: compiled_pages[route] = page_ctx
Loading

Reviews (2): Last reviewed commit: "removed the extra stuff no related to is..." | Re-trigger Greptile

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 31, 2026

Merging this PR will improve performance by ×11

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 5 improved benchmarks
✅ 2 untouched benchmarks
🆕 10 new benchmarks
⏩ 2 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
🆕 test_compile_single_pass_all_artifacts[_stateful_page] N/A 26.8 ms N/A
🆕 test_evaluate_page_single_pass[_complicated_page] N/A 46.7 ms N/A
🆕 test_compile_page_single_pass[_stateful_page] N/A 27.4 ms N/A
🆕 test_get_all_imports_single_pass[_stateful_page] N/A 751.1 µs N/A
🆕 test_evaluate_page_single_pass[_stateful_page] N/A 7.3 ms N/A
test_compile_page[_complicated_page] 90.8 ms 8.3 ms ×11
test_compile_page[_stateful_page] 10.7 ms 1.7 ms ×6.3
test_evaluate_page[_complicated_page] 47.3 ms 45.8 ms +3.15%
🆕 test_compile_page_full_context[_complicated_page] N/A 156.9 ms N/A
test_get_all_imports[_complicated_page] 22.9 ms 2.4 ms ×9.3
🆕 test_compile_page_single_pass[_complicated_page] N/A 143.8 ms N/A
🆕 test_get_all_imports_single_pass[_complicated_page] N/A 3.2 ms N/A
test_get_all_imports[_stateful_page] 3,104.1 µs 498.2 µs ×6.2
🆕 test_compile_single_pass_all_artifacts[_complicated_page] N/A 139.1 ms N/A
🆕 test_compile_page_full_context[_stateful_page] N/A 30.1 ms N/A

Comparing FarhanAliRaza:compiler-hooks (e7dfb81) with main (f41f9cb)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@FarhanAliRaza FarhanAliRaza marked this pull request as ready for review April 1, 2026 11:07
@FarhanAliRaza FarhanAliRaza requested a review from masenf April 1, 2026 11:08
Copy link
Copy Markdown
Collaborator

@masenf masenf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need a test case for the child replacement logic when traversing the tree; we might not strictly need it now, but it will be important when moving the StatefulComponent compilation into the plugin system.

@masenf
Copy link
Copy Markdown
Collaborator

masenf commented Apr 2, 2026

Khaleel and I were discussing this a bit further, and I think it would be better to add these new plugin hooks and hook dispatching system to the existing Plugin base in packages/reflex-core

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.
@FarhanAliRaza FarhanAliRaza changed the title Add single-pass component tree collector and compiler plugin foundations Add compiler plugin hooks and plugins and move compilation pipeline out of App Apr 3, 2026
Move memo component compilation after app_root resolution so app-wrap
components are included. Fix DefaultPagePlugin to preserve Var-backed
titles instead of replacing them with the default string.
Move _get_app_wrap_components collection outside the
`if stateful_component is None` guard so that app wrap components
(e.g. UploadFilesProvider) are collected even when a component is
wrapped as a stateful component. Add test verifying upload pages
correctly emit UploadFilesProvider in the app root.
@FarhanAliRaza
Copy link
Copy Markdown
Collaborator Author

@masenf i dont know why the tests are failing.

Remove the class-level tag_to_stateful_component dict and instead
thread a compile-scoped stateful_component_cache through compile_from()
and create(). This prevents stale cache entries from leaking between
independent compilation runs. Also collect imports and app_wrap_components
from root components in CompileContext so stateful component libraries
and providers (e.g. UploadFilesProvider) are properly propagated.
Update benchmarks to inline helpers using the new plugin API and add
tests covering shared stateful components across pages and cache
isolation between runs.
Component._get_vars had a dead-code cache path: `getattr(self, "__vars", None)`
reads the literal attribute `__vars` but `self.__vars = []` writes to the
name-mangled `_Component__vars`. The cache branch was never taken, and even if
the name-mangling were fixed the missing `return` after `yield from vars` would
have caused duplicate yields on repeated calls.

Fix the cache (as `_vars_cache`) with a proper early-return. Extend the same
per-instance cache pattern to `_get_imports` and `_get_hooks_internal`, which
share the same dependency on `event_triggers` / `_get_vars`. Unify invalidation
with the existing render-cache clear in `StatefulComponent.create` so all four
caches drop together when `_fix_event_triggers` mutates the component.
Returns:
A page context when the plugin can evaluate the page, otherwise ``None``.
"""
del page_fn, kwargs
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does it del the local vars?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are gonna be overloaded by the actual plugin. So Claude is i think, just trying to use it. Deleting it.

]


class CompilerPlugin(Protocol):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this protocol needed since we have the base plugin definition?



@dataclasses.dataclass(kw_only=True)
class BaseContext:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this class also gets defined in my backend event loop PR as reflex_base.context.base depending on whose merges first, we can refactor

if isinstance(page_ctx.root_component, StatefulComponent):
self.all_imports = merge_imports(
self.all_imports,
page_ctx.root_component._get_all_imports(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this still making the recursive call? isn't this information cached in the PageContext as imports?

…ields

Remove the duplicated _compile_component_without_replacements and
_compile_component_single_enter_fast_path methods in favor of a single
_compile_component_tree walker with inline replacement checks. This
eliminates ~200 lines of duplication and removes several optimization-only
fields (_regular_leave_component_hook_binders, _stateful_leave_component_hook_binders,
_component_hooks_can_replace, _enter_component_hooks, _leave_component_hooks)
with negligible benchmark impact.
_compile_stateful_components now returns its collected imports alongside
the rendered code. CompileContext merges these into all_imports after
compilation, and no longer calls _get_all_imports on the root component
(the single-pass walk already collects structural imports).
CompilerPlugin duplicated methods already on Plugin base class.
PageDefinition was a structural typing protocol only satisfied by
UnevaluatedPage. Replace both with their concrete counterparts.

Give UnevaluatedPage fields defaults so it can be used directly in
tests and benchmarks, eliminating FakePage and BenchmarkPage classes.
# Conflicts:
#	packages/reflex-base/src/reflex_base/components/component.py
#	reflex/app.py
FarhanAliRaza and others added 7 commits April 7, 2026 23:32
The reflex version in reflex-web's pyproject.toml is of no consequence to us,
we want to test the reflex version in the current PR.
Remove the StatefulComponent class (~470 lines) and its two-pass
compile model (evaluate → StatefulComponent.compile_from → render).
Replace it with MemoizeStatefulPlugin, a single-pass compiler plugin
that auto-memoizes stateful subtrees using the experimental memo
infrastructure.

Key changes:

- Delete StatefulComponent from component.py. All memoization logic now
  lives in reflex/compiler/plugins/memoize.py (MemoizeStatefulPlugin)
  and reflex_base/components/memoize_helpers.py (event trigger helpers).

- Remove the shared stateful_components module. Memoized wrappers are
  now compiled as experimental memo components into $/utils/components,
  tracked via CompileContext.memoize_wrappers and auto_memo_components.

- Remove CompilerPlugin protocol — Plugin base class already provides
  the same eval_page/compile_page/enter_component/leave_component
  interface.

- Add PageDefinition protocol so CompileContext.pages is decoupled from
  UnevaluatedPage, allowing test fixtures to provide minimal page-like
  objects.

- Add three specialized tree-walk methods to CompilerHooks
  (_compile_component_without_replacements, _single_enter_fast_path,
  _with_replacements) to avoid replacement-dispatch overhead when no
  plugin can replace components.

- Simplify compile_unevaluated_page to inline page evaluation directly
  (route, style, theme as positional args) instead of going through the
  full plugin pipeline.

- Remove redundant _get_all_imports() and _get_all_app_wrap_components()
  calls from the evaluate loop — the tree walk via DefaultCollectorPlugin
  already collects these into PageContext.

- Remove dead code: _compile_page_from_app wrapper,
  get_stateful_components_path, compile_stateful_components,
  _compile_stateful_components, _get_shared_components_recursive.

- Clean up Plugin.enter_component / leave_component signatures by
  removing the stateful_component parameter from all hooks.

- Remove unused _enter_component_hooks/_leave_component_hooks fields
  from CompilerHooks (only binders are used at runtime).

- Remove dead apply_overlay parameter from CompileContext.compile().
Memo wrappers are transparent in the authored component tree — they
should not trigger _valid_parents checks against themselves. Override
_validate_component_children on ExperimentalMemoComponent to skip the
check, preventing false validation failures when a restricted child
(e.g. _valid_parents = ["ValidParent"]) is wrapped in a memo before
being placed inside its valid parent.
@FarhanAliRaza FarhanAliRaza requested a review from masenf April 7, 2026 21:11
FarhanAliRaza and others added 7 commits April 8, 2026 17:15
Insert a b"\n" probe chunk before the real response body so that an early
client disconnect is detected before the upload handler is enqueued. Add
asyncio.sleep(0) yield point in _upload_buffered_file to let the
disconnect watcher fire first, and return early if disconnect was seen.
Includes a test covering the probe-based disconnect detection path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants