Skip to content

Move compilation orchestration from App into compiler module with plugin pipeline#6274

Draft
FarhanAliRaza wants to merge 8 commits intoreflex-dev:mainfrom
FarhanAliRaza:compiler-hooks-plugins-switch
Draft

Move compilation orchestration from App into compiler module with plugin pipeline#6274
FarhanAliRaza wants to merge 8 commits intoreflex-dev:mainfrom
FarhanAliRaza:compiler-hooks-plugins-switch

Conversation

@FarhanAliRaza
Copy link
Copy Markdown
Collaborator

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.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

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?

After these steps, you're ready to open a pull request.

a. Give a descriptive title to your PR.

b. Describe your changes.

c. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such).

Replace 6 separate recursive tree walks (_get_all_imports, _get_all_hooks,
_get_all_custom_code, _get_all_dynamic_imports, _get_all_refs,
_get_all_app_wrap_components) with a single collect_component_tree_artifacts
walk that gathers all compilation data in one pass.

Wire the new collector into app.py, compiler.py, and utils.py. Add
CompilerPlugin protocol, CompilerHooks dispatcher, and BaseContext/
PageContext/CompileContext types as foundations for the async plugin
pipeline.
Refactor the monolithic plugins.py into a plugins/ package:
- base.py: core framework (CompilerPlugin base class, CompilerHooks, contexts)
- builtin.py: concrete plugins matching legacy recursive collectors
- __init__.py: re-exports all public names

Adds ApplyStylePlugin, DefaultPagePlugin, and Consolidate*Plugin classes
that replicate legacy page compilation behavior. Expands test coverage
with per-plugin verification and full pipeline integration tests.
…gin pipeline

Extract the monolithic App._compile() method into compiler.compile_app(), routing
page evaluation through the async CompileContext/CompilerHooks plugin pipeline.
Remove ExecutorType enum and associated env vars (REFLEX_COMPILE_EXECUTOR,
REFLEX_COMPILE_PROCESSES, REFLEX_COMPILE_THREADS) in favor of a simple
ThreadPoolExecutor. Delete ExecutorSafeFunctions class, move _app_root() and
page compilation helpers into compiler.py as standalone functions, and rename
the old compile_app() to compile_app_root() to avoid collision.
@FarhanAliRaza FarhanAliRaza marked this pull request as draft April 1, 2026 19:08
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 1, 2026

Merging this PR will not alter performance

✅ 8 untouched benchmarks


Comparing FarhanAliRaza:compiler-hooks-plugins-switch (513b840) with main (980a97d)

Open in CodSpeed

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 1, 2026

Greptile Summary

This PR moves the compilation orchestration that previously lived in App._compile() into reflex/compiler/compiler.py, replacing the old concurrent-futures-based pipeline with an async plugin pipeline (CompilerHooksCompileContext). The ExecutorType enum and its associated environment variables (REFLEX_COMPILE_EXECUTOR, REFLEX_COMPILE_PROCESSES, REFLEX_COMPILE_THREADS) are removed along with all parallel page-compilation machinery, while page rendering is kept concurrent via asyncio.to_thread.

Key changes:

  • New reflex/compiler/plugins/ package introduces CompilerPlugin protocol, CompilerHooks dispatcher, and BaseContext/PageContext/CompileContext dataclasses.
  • Eight built-in plugins (DefaultPagePlugin, ApplyStylePlugin, six Consolidate* plugins) replace the recursive _get_all_* collector methods.
  • compile_app(Component) → tuple[str, str] renamed to compile_app_root; a new compile_app(App, ...) is the top-level orchestrator.
  • Plugin pre_compile save-tasks now run sequentially rather than in a thread/process pool.
  • Race-condition fix in tests/units/istate/manager/test_redis.py: a single shared modify_2_started event is replaced with a counter-gated contenders_started event.

Issues found:

  • P1 — overlay_component deprecation notice silently dropped: _setup_overlay_component() (which issues a console.deprecate() with removal_version="0.9.0") is never called in the new pipeline. Users who still set overlay_component will get no warning before the feature is removed.
  • P2 — compile_app API change breaks external callers: The old compile_app(Component) signature is now compile_app_root; the new compile_app(App, ...) is incompatible. No deprecation shim is provided.
  • P2 — asyncio.run in synchronous compile_app: Will raise RuntimeError if called from within an already-running event loop.
  • P2 — ConsolidateCustomCodePlugin._collect_prop_custom_code runs in the pre-yield phase before prop components are compiled through the pipeline, so transforms by later plugins won't be reflected in collected custom code.

Confidence Score: 4/5

  • Mostly safe to merge but one P1 regression (overlay_component deprecation warning silently dropped) and two meaningful P2 concerns should be addressed first.
  • The plugin architecture and async pipeline are well-structured with solid test coverage. The P1 issue (missing deprecation notice for a feature scheduled for removal in 0.9.0) means users will get a silent breakage upgrade rather than the migration guidance they need. The P2 issues (broken public API contract for compile_app, asyncio.run risk, prop-code pre-collection ordering) are real but lower-urgency. The redis test fix and environment cleanup are clean improvements.
  • reflex/compiler/compiler.py (overlay deprecation call site, asyncio.run usage, compile_app rename); reflex/compiler/plugins/builtin.py (prop custom-code collection ordering)

Important Files Changed

Filename Overview
reflex/compiler/compiler.py Major orchestration logic moved here from app.py; introduces compile_app(App) (replacing compile_app(Component) → now compile_app_root), drops overlay_component deprecation warning, and uses asyncio.run() which fails if called from an existing event loop.
reflex/compiler/plugins/base.py New file: core plugin protocol, CompilerHooks dispatcher, BaseContext/PageContext/CompileContext dataclasses. Well-structured async generator pipeline with correct generator lifecycle management and context-variable isolation per task.
reflex/compiler/plugins/builtin.py New file: eight built-in plugins (ApplyStyle, ConsolidateImports, Hooks, CustomCode, DynamicImports, Refs, AppWrap, DefaultPage). ConsolidateCustomCodePlugin collects prop-tree code in the pre-yield (descending) phase before the tree is compiled, which may miss transforms by later plugins.
reflex/app.py Compilation logic removed from App._compile(); now delegates entirely to compiler.compile_app(). Imports cleaned up accordingly. _app_root and _setup_overlay_component are no longer called in the compile path.
packages/reflex-core/src/reflex_core/environment.py ExecutorType enum and REFLEX_COMPILE_EXECUTOR / REFLEX_COMPILE_PROCESSES / REFLEX_COMPILE_THREADS env-vars removed along with their validation logic, consistent with dropping parallel executor support.
tests/units/compiler/test_plugins.py New test file with thorough coverage of plugin dispatch, context lifecycle, ordering, default pipeline parity with legacy collectors, and CompileContext integration.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI / prerequisites.py
    participant App as App._compile()
    participant CC as compiler.compile_app()
    participant Ctx as CompileContext.compile()
    participant Hooks as CompilerHooks
    participant Plugins as Plugin Pipeline
    participant Thread as asyncio.to_thread

    CLI->>App: _compile(prerender_routes, dry_run, use_rich)
    App->>CC: compile_app(self, ...)
    CC->>CC: _should_compile() check
    CC->>CC: asyncio.run(_compile_with_context())
    CC->>Ctx: async with compile_ctx → compile(apply_overlay=True)
    loop For each page
        Ctx->>Hooks: eval_page(page_fn, page=page)
        Hooks->>Plugins: DefaultPagePlugin.eval_page → PageContext
        Hooks-->>Ctx: PageContext
        Ctx->>Hooks: compile_component(root_component)
        Hooks->>Plugins: ApplyStylePlugin (pre-yield)
        Hooks->>Plugins: ConsolidateCustomCodePlugin (pre-yield)
        Hooks->>Plugins: [other plugins pre-yield]
        Hooks->>Hooks: recurse structural children
        Hooks->>Hooks: traverse prop components (in_prop_tree=True)
        Hooks->>Plugins: [plugins post-yield in reverse]
        Hooks-->>Ctx: compiled root component
        Ctx->>Hooks: compile_page(page_ctx)
        Hooks->>Plugins: ConsolidateImportsPlugin.compile_page
        Ctx->>Thread: asyncio.to_thread(compile_page, route, component)
    end
    CC->>CC: compile_memo_components()
    CC->>CC: _resolve_app_wrap_components()
    CC->>CC: _build_app_root()
    CC->>CC: compile_document_root()
    CC->>CC: plugin.pre_compile() → collect save/modify tasks
    CC->>CC: run save_tasks sequentially
    CC->>CC: compile_contexts() + compile_app_root()
    CC->>CC: write output_mapping to disk
    CC-->>CLI: done
Loading

Comments Outside Diff (1)

  1. reflex/compiler/compiler.py, line 553-569 (link)

    P2 compile_app public API silently replaced with incompatible signature

    The original compile_app(app_root: Component) -> tuple[str, str] function has been renamed to compile_app_root, and a completely new compile_app(app: App, ...) has been placed at the same name. Any external caller who imported and used from reflex.compiler.compiler import compile_app with a Component argument will now receive a TypeError at runtime with no migration path.

    There should be a deprecation shim or a note in the changelog, and the old symbol should be preserved (possibly with a warning) for at least one release cycle:

    def compile_app(app_root_or_app, /, **kwargs):
        import warnings
        if isinstance(app_root_or_app, Component):
            warnings.warn(
                "compile_app(Component) is deprecated; use compile_app_root(component) instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            return compile_app_root(app_root_or_app)
        # new dispatch ...

Reviews (1): Last reviewed commit: "Move compilation orchestration from App ..." | Re-trigger Greptile

Comment on lines +850 to +859
async def _compile_with_context() -> None:
async with compile_ctx:
await compile_ctx.compile(
apply_overlay=True,
evaluate_progress=lambda: progress.advance(task),
render_progress=lambda: progress.advance(task),
)

with console.timing("Compile pages"):
asyncio.run(_compile_with_context())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Deprecation warning for overlay_component silently dropped

_setup_overlay_component() is no longer called anywhere in the new compilation pipeline. That method issues a console.deprecate() with removal_version="0.9.0". The overlay is still applied via CompileContext.compile(apply_overlay=True), but the deprecation notice that guides users to migrate to extra_app_wraps is gone.

Users who set overlay_component will see no warning before it is removed in 0.9.0, breaking their apps silently.

_setup_overlay_component() should be called (or its deprecation logic replicated) before the overlay is applied:

# Emit the deprecation notice for overlay_component before applying it
if app.overlay_component is not None:
    app._setup_overlay_component()   # issues console.deprecate(...)

async def _compile_with_context() -> None:
    async with compile_ctx:
        await compile_ctx.compile(
            apply_overlay=True,
            ...
        )

Or alternatively, call it on line 873 after app._add_optional_endpoints() to preserve the original call-site ordering.

)

with console.timing("Compile pages"):
asyncio.run(_compile_with_context())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 asyncio.run will fail when an event loop is already running

asyncio.run(_compile_with_context()) creates a brand-new event loop. If _compile / compile_app is ever invoked from within an already-running loop — e.g., from an async test, a Jupyter-style environment, or a framework that wraps the CLI in anyio.run — Python raises RuntimeError: This event loop is already running.

Current call sites are synchronous, so this works today, but the constraint is invisible and easy to violate. Consider using asyncio.get_event_loop().run_until_complete(...) with a fallback, or restructuring so the async context runs at a well-known top-level entry point:

try:
    loop = asyncio.get_running_loop()
except RuntimeError:
    loop = None

if loop is not None and loop.is_running():
    # already in an async context; schedule as a task or use nest_asyncio
    import nest_asyncio
    nest_asyncio.apply()
    loop.run_until_complete(_compile_with_context())
else:
    asyncio.run(_compile_with_context())

Or simply document that compile_app must be called from a synchronous context.

Comment on lines +174 to +181
or not isinstance(compiled_component, Component)
):
return

hooks = {}
hooks.update(compiled_component._get_hooks_internal())
if (user_hooks := compiled_component._get_hooks()) is not None:
hooks[user_hooks] = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 _collect_prop_custom_code traverses un-compiled prop children

_collect_prop_custom_code is a synchronous recursive helper called in the pre-yield (descending) phase of compile_component, before any plugin has had a chance to transform the prop component tree. Because the async plugin traversal for prop components happens after structural children are compiled and the generators unwind, any plugin that transforms a prop component's _get_custom_code() output in a later pass will not be reflected in the code accumulated here.

This won't matter for the current default plugin ordering, but it is a subtle contract violation that could silently produce wrong output if future plugins modify prop components. Consider collecting prop custom code in the post-yield phase (the ascending side of the generator), or deferring the collection to compile_page after the full tree has been walked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant