Skip to content

Single-pass compiler: Introduce CompilerPlugin protocol, CompilerHooks, and context objects #6210

@masenf

Description

@masenf

Summary

Introduce the foundational types for the new single-pass compiler: CompilerPlugin (protocol), CompilerHooks (dispatcher), BaseContext, PageContext, and CompileContext. These are the building blocks that all subsequent compiler work depends on.

This is part of the 2a. Single Pass Compiler roadmap item. The goal is to replace the current multi-pass compilation approach (where the component tree is walked multiple times to extract imports, hooks, custom code, dynamic imports, refs, and app wrap components separately) with a single tree walk using a plugin architecture.

Background & Motivation

Today, compiling a single page involves at least 6 separate recursive tree walks over the same component tree:

  1. _get_all_imports() — collects imports from every component
  2. _get_all_hooks() — collects React hooks
  3. _get_all_custom_code() — collects custom JS code snippets
  4. _get_all_dynamic_imports() — collects dynamic import statements
  5. _get_all_refs() — collects refs
  6. _get_all_app_wrap_components() — collects app wrapper components

Each of these methods in reflex/components/component.py follows the same pattern: call the non-recursive version on self, then recursively call on all children, merging results. This is wasteful — a single walk could collect all information in one pass.

Additionally, compilation logic is currently spread across reflex/app.py (the _compile() method, ~400 lines), reflex/compiler/compiler.py, and reflex/compiler/utils.py, making it hard to extend or adapt for different targets.

Design

A working demo of the proposed architecture is available as a starting point (see the CompilerPlugin protocol, CompilerHooks, BaseContext, PageContext, CompileContext classes in the demo code shared in the project brief). The key ideas:

CompilerPlugin Protocol

class CompilerPlugin(Protocol):
    async def eval_page(self, page_fn, /, **kwargs) -> PageContext | None: ...
    async def compile_page(self, page_ctx, /, **kwargs) -> None: ...
    async def compile_component(self, comp, /, **kwargs) -> AsyncGenerator[...]: ...
  • eval_page: Evaluate a page function into a PageContext. Returns None if the plugin doesn't handle this page.
  • compile_page: Perform a transformation on the PageContext during page compilation.
  • compile_component: Two-phase async generator for component transformation. Pre-yield (descending): sees component before children are visited. Post-yield (ascending): runs after children are visited, receives (component, children) tuple.

CompilerHooks Dispatcher

Holds a tuple[CompilerPlugin, ...] and dispatches hook calls to all registered plugins in order. Plugin ordering controls priority.

Context Objects

  • BaseContext: Async context manager that sets itself as a ContextVar, allowing any code in the call stack to access the current context via ContextClass.get().
  • PageContext: Holds per-page compilation state — name, root_component, imports, module_code, etc.
  • CompileContext: Holds the full compilation state — list of pages, hooks, compiled results.

Acceptance Criteria

  • CompilerPlugin protocol is defined in a new module (e.g. reflex/compiler/plugins.py or reflex/compiler/plugin.py)
  • CompilerHooks dataclass is implemented with _dispatch, eval_page, compile_page, and compile_component methods
  • BaseContext is implemented with ContextVar-based async context management, get() classmethod, and ensure_context_attached() guard
  • PageContext is implemented with fields for: name, route, root_component, imports (list of ParsedImportDict), module_code (set of str), hooks (dict), dynamic_imports (set), refs (dict), app_wrap_components (dict)
  • CompileContext is implemented with fields for: pages, hooks (CompilerHooks), compiled_pages dict, and a compile() method that orchestrates page compilation
  • All new types have docstrings and type annotations
  • Unit tests cover: plugin dispatch ordering, context var lifecycle (enter/exit/get), PageContext field accumulation

Key Files to Understand

  • reflex/components/component.pyBaseComponent._get_all_* methods (lines ~1590-2002) show the current tree-walking pattern
  • reflex/compiler/compiler.py_compile_page(), compile_stateful_components(), _get_shared_components_recursive()
  • reflex/app.pyApp._compile() method (~lines 1151-1549) is the main compilation orchestrator

Notes

  • The context objects use ContextVar so that plugins running during compilation can access PageContext.get() or CompileContext.get() from anywhere in the call stack without explicit parameter passing.
  • Plugin ordering matters: e.g., a FooPlugin that transforms components must run before ConsolidateImportsPlugin so the transformed components' imports are captured.
  • This issue only introduces the types and basic tests. Wiring them into the actual compilation pipeline is a separate issue.

Metadata

Metadata

Assignees

Labels

enhancementAnything you want improved

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions