Skip to content

Single-pass compiler: Implement core compilation plugins (imports, hooks, custom code, dynamic imports, refs) #6211

@masenf

Description

@masenf

Summary

Implement the default set of CompilerPlugins that replicate the current multi-pass tree walking behavior as single-pass plugins. Each plugin collects one category of information during the single tree walk and stashes it in the PageContext.

Depends on ENG-9142 (compiler plugin protocol and context objects).

Background

Today, reflex/compiler/compiler.py::_compile_page() calls these tree-walking methods sequentially:

def _compile_page(component: BaseComponent) -> str:
    imports = component._get_all_imports()          # walk 1
    _apply_common_imports(imports)
    imports = utils.compile_imports(imports)
    return templates.page_template(
        imports=imports,
        dynamic_imports=sorted(component._get_all_dynamic_imports()),  # walk 2
        custom_codes=component._get_all_custom_code(),                 # walk 3
        hooks=component._get_all_hooks(),                              # walk 4
        render=component.render(),                                     # walk 5
    )

And App._compile() does additional walks for:

  • component._get_all_imports() (again, for global import collection)
  • component._get_all_app_wrap_components() (walk 6)

Each walk traverses the entire component tree. With the plugin architecture, all of these become compile_component hooks that run during a single recursive walk.

Plugins to Implement

1. DefaultPagePlugin

  • eval_page: Evaluates the page function into a PageContext (equivalent to compile_unevaluated_page() in reflex/compiler/compiler.py)
  • compile_page: Orchestrates the single recursive tree walk by calling _compile_component_recursive()
  • _compile_component_recursive(): The core single-walk method that dispatches compile_component hooks for each component

Reference: See DefaultPagePlugin in the demo code. The key insight is the two-phase generator pattern — pre-yield sees the component going down, post-yield (via gen.asend((comp, children))) sees the result going back up.

2. ConsolidateImportsPlugin

Replaces Component._get_all_imports() and _apply_common_imports().

async def compile_component(self, comp):
    comp, _ = yield  # post-yield: get the (potentially transformed) component
    if isinstance(comp, Component) and (imports := comp._get_imports()):
        PageContext.get().imports.append(imports)

async def compile_page(self, page_ctx):
    # After tree walk, collapse and merge all collected imports
    page_ctx.imports = [collapse_imports(merge_imports(*page_ctx.imports))]

Current code: Component._get_all_imports() in component.py (~line 1739)

3. ConsolidateHooksPlugin

Replaces Component._get_all_hooks() and _get_all_hooks_internal().

async def compile_component(self, comp):
    comp, _ = yield
    if isinstance(comp, Component):
        hooks = {}
        hooks.update(comp._get_hooks_internal())
        if (h := comp._get_hooks()) is not None:
            hooks[h] = None
        hooks.update(comp._get_added_hooks())
        PageContext.get().hooks.update(hooks)

Current code: Component._get_all_hooks() in component.py (~line 1899)

4. ConsolidateCustomCodePlugin

Replaces Component._get_all_custom_code().

Current code: Component._get_all_custom_code() in component.py (~line 1590)

5. ConsolidateDynamicImportsPlugin

Replaces Component._get_all_dynamic_imports().

Current code: Component._get_all_dynamic_imports() in component.py (~line 1627)

6. ConsolidateRefsPlugin

Replaces Component._get_all_refs().

Current code: Component._get_all_refs() in component.py (~line 1933)

7. ConsolidateAppWrapPlugin

Replaces Component._get_all_app_wrap_components().

Current code: Component._get_all_app_wrap_components() in component.py (~line 1972)

8. ApplyStylePlugin

Replaces Component._add_style_recursive(). Applies styles during the descending phase (pre-yield) of the tree walk.

Current code: Component._add_style_recursive() in component.py (~line 1251)

Acceptance Criteria

  • Each plugin listed above is implemented as a class conforming to CompilerPlugin
  • Each plugin has unit tests that verify it produces identical output to the current _get_all_* method for the same component tree
  • A composed pipeline of all plugins produces the same PageContext data as the current _compile_page() function
  • Integration test: compile a sample page with the new plugin pipeline and compare the resulting JS output to the current compiler output (should be byte-identical)
  • Plugins are collected in a sensible default ordering (style first, then consolidation plugins, with imports last since it needs to see all transformations)

Key Files

  • reflex/components/component.py — All _get_all_* methods and _add_style_recursive
  • reflex/compiler/compiler.py_compile_page(), _apply_common_imports()
  • reflex/compiler/utils.pymerge_imports(), compile_imports(), collapse_imports()

Notes

  • The individual _get_imports(), _get_hooks(), _get_custom_code(), etc. (non-recursive, single-component versions) remain on the Component class. Only the _get_all_* (recursive tree-walking) versions are replaced by plugins.
  • The _get_components_in_props() call in some _get_all_* methods means those plugins need to also walk components embedded in props, not just children.
  • Plugin ordering is critical. For example, ApplyStylePlugin must run before any consolidation plugin. The ConsolidateImportsPlugin.compile_page phase must run after all compile_component hooks have finished collecting imports.

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