MishkaCmsCore. Runtime. Compilers. ComponentCompiler
(Mishka CMS Core v0.0.1)
Copy Markdown
View Source
Runtime compiler for Phoenix Components with dynamic content compilation.
This module provides functionality to dynamically compile Phoenix Components at runtime from configuration data. It generates complete Phoenix Component modules with proper attribute definitions, slot definitions, helper functions, and HEEx template compilation.
Features
- Dynamic component module generation from configuration
- Full Phoenix Component attribute and slot support
- HEEx template compilation with proper context
- Default value handling for all attribute types including @rest
- Helper function integration
- Component metadata and access control information
- Support for struct-based attributes
- Proper Phoenix LiveView integration
- Total
_, …catch-all appended to narrowed variant dispatchers (color_variant,size_class, …) so a component renders a bare class instead of crashing when a page calls a variant/color the install narrowed away
Component Structure
Generated components include:
__component_information__/0- Returns component metadata__component_information__/1- Returns specific metadata by key- Main component function (named after the component title)
update_many/1andrender/1, for a stateful component only- Helper functions (if defined)
- Proper attr/slot declarations for Phoenix Component compliance
Usage
params = %{
title: "alert_box",
component_module: MyApp.Components.AlertBox,
template: ~S'''
<div class={"alert alert-#{@type} #{@class}"} role="alert">
<h4 :if={@title}><%= @title %></h4>
<%= render_slot(@inner_block) %>
<button :if={@closable} class="ml-auto" phx-click={@on_close}>×</button>
</div>
''',
attrs: [
%{name: "type", type: "string", opts: [default: "info", values: ["info", "warning", "error", "success"]]},
%{name: "title", type: "string", opts: [default: nil]},
%{name: "closable", type: "boolean", opts: [default: false]},
%{name: "on_close", type: "string", opts: [default: nil]},
%{name: "class", type: "string", opts: [default: ""]}
],
slots: [
%{name: "inner_block", opts: [required: true, doc: "Main content of the alert"]}
],
helpers: [
%{
name: "format_message",
args: "message, type",
code: ~S"""
case type do
"error" -> "⚠️ " <> message
"success" -> "✅ " <> message
_ -> message
end
"""
}
]
}
ast = ComponentCompiler.build_module(params)
MishkaCmsCore.Runtime.Compilers.ModuleCompiler.compile_module(ast)
Summary
Functions
Build component attributes.
Build the main component function with HEEx template compilation and default values.
Build component information including metadata.
Closes a stateful component's handle_event/3 with a clause that answers anything left over.
Build helper functions for the component.
Main function to build component modules from component data.
Build module-level attributes (e.g. @indicator_positions [...]).
Build component slots.
Renders a freshly compiled stateful component once, and says whether its root will survive a visitor.
Create the final module AST.
Create environment for template compilation.
Functions
Build component attributes.
Generates Phoenix Component attribute declarations from the component configuration.
Creates proper attr/3 calls with type validation and options handling.
Parameters
{params, functions_ast}- Tuple containing params and accumulated AST functions
Returns
Returns updated tuple with attribute setup and individual attribute declarations added. Each attribute is properly validated and converted to Phoenix Component format with:
- Proper type conversion (including struct types)
- Option filtering to valid Phoenix Component options
- Default value handling
Supported Options
Valid attribute options that are preserved:
:required- Whether the attribute is required:default- Default value for the attribute:examples- Example values:values- Allowed values list:doc- Attribute documentation:include- Global attributes to include:exclude- Global attributes to exclude
Build the main component function with HEEx template compilation and default values.
Parameters
{params, functions_ast}- Tuple containing params and accumulated AST functions
Returns
Returns updated tuple with the main component function added.
Generated Functions
component_name(assigns)- Main component function named after the title
It:
- Merges default values into assigns
- Executes any body code (if present)
- Compiles and renders the HEEx template
- Returns the rendered output
Default Value Handling
Default values are extracted from attribute definitions and merged into assigns before rendering, ensuring all attributes have proper defaults including @rest for global attributes.
Build component information including metadata.
Creates AST for the __component_information__ functions that provide access to component
metadata, private information, ACL permissions, and extra data. These functions are used
by the runtime system to introspect component properties.
Parameters
params- Atomized component parameters map
Returns
Returns a tuple {params, functions_ast} where functions_ast contains the AST for:
__component_information__/0- Returns complete metadata map__component_information__/1- Returns metadata by atom key or key path__component_information__/1- Returns metadata by string dot-notation path
Metadata Structure
The metadata includes:
:component- Public component info (name, timestamp, attrs, slots):private- Private runtime info (id, priority, module, site details):acl- Access control permissions:extra- Additional custom metadata
Closes a stateful component's handle_event/3 with a clause that answers anything left over.
Why it is a stage of its own, run last
A component's handle_event/3 runs in whatever LiveView renders it, so an event with no matching
clause is an unmatched function call inside a visitor's whole page rather than inside one widget.
This makes that a no-op.
It has to be emitted AFTER the kit's own clauses, and that is the entire reason this is a separate
stage: Elixir takes the first clause that matches, so a catch-all emitted with the rest of the
module shape — which is built before build_helpers/1 runs — swallows every handler the kit
declared. Measured as a counter that rendered, took a click, and did not move.
Stateless components get nothing: they have no event channel to answer on.
Build helper functions for the component.
Processes helper function definitions and creates corresponding function AST. Helper functions extend component functionality with custom Elixir code.
Parameters
{params, functions_ast}- Tuple containing params and accumulated AST functions
Returns
Returns updated tuple with helper functions added to the AST.
Helper Function Structure
Each helper function definition should contain:
:name- Function name as string:args- Function arguments as Elixir code string:code- Function body as Elixir code string
Example Helper Definition
%{
name: "format_date",
args: "date, format \\ :short",
code: "Calendar.strftime(date, format)"
}This generates a helper function that can be called within the component template.
Main function to build component modules from component data.
Takes component configuration parameters and returns a complete AST for a Phoenix Component module. The function orchestrates the entire compilation process through a pipeline of transformations.
Parameters
params- Map containing component configuration with keys::title- Component name (required):component_module- Target module name (required):template- HEEx template string (optional):body- Elixir code to execute before rendering (optional):attrs- List of attribute definitions (optional):slots- List of slot definitions (optional):helpers- List of helper function definitions (optional):site,:site_id,:host- Site context information:permissions- Access control permissions:extra- Additional metadata
Returns
Returns the complete AST for the component module that can be compiled using
MishkaCmsCore.Runtime.Compilers.ModuleCompiler.compile_module/1.
Example
params = %{
title: "card",
component_module: MyApp.Components.Card,
template: ~S(<div class="card {@class}"><%= @content %></div>),
attrs: [
%{name: "class", type: "string", opts: [default: ""]},
%{name: "content", type: "string", opts: [required: true]}
]
}
ast = build_module(params)
Build module-level attributes (e.g. @indicator_positions [...]).
Reads params[:module_attributes] — a list of %{name, value, when} maps.
Emits Module.put_attribute(__MODULE__, :name, value) calls inside the
module body so the resulting attribute is referenceable by subsequent
attr/3 values: lists.
When-gating is performed by the Importer before this point — the compiler always emits whatever is in the list.
Build component slots.
Generates Phoenix Component slot declarations from the component configuration.
Creates proper slot/3 calls with nested attribute support for slot parameters.
Parameters
{params, functions_ast}- Tuple containing params and accumulated AST functions
Returns
Returns updated tuple with slot setup and individual slot declarations added. Each slot includes:
- Proper slot declaration with options
- Nested attribute definitions for slot parameters
- Option filtering to valid Phoenix Component slot options
Supported Slot Options
Valid slot options that are preserved:
:required- Whether the slot is required:validate_attrs- Whether to validate slot attributes:doc- Slot documentation
Slot attributes support the same options as regular component attributes.
Renders a freshly compiled stateful component once, and says whether its root will survive a visitor.
The failure this turns into a compile failure
Phoenix.LiveView.Diff.component_to_rendered/3 raises
Stateful components must have a single static HTML tag at the rootand it raises inside a VISITOR's LiveView, where nothing contains it — the canvas has
contain/3, a published page has nothing. Worse, the compile succeeded, so
MishkaCmsCore.Runtime.States.CompileFailureState holds no entry and the next request does not
short-circuit to a placeholder either: the page simply dies, again.
%Phoenix.LiveView.Rendered{}.root is false for two sibling tags, for leading text, for {@x}
alone and — the one nobody expects — for <div :if={@x}>, because a conditional root is not a
static one. None of those is unusual markup, and none of them is visible as a problem on the
canvas: LiveViewHelpers.static_component/1 calls render/1 as a plain function, which never
consults root.
Why it is a render and not a read of the AST
Because root is computed by the engine, not stated by the author. Reproducing that judgement
here would be a copy of library internals pinned to a version — the same argument that kept a
hand-written iodata walker out of the canvas.
What it supplies
Each declared attr's default, a sentinel for one with none, [] for every slot, and an id,
because a component asked for an attribute it declares required would raise for a reason that has
nothing to do with its root. Wrapped in a rescue: this runs at compile time, where a rescue is
permitted, and a component that raises for its own reasons is reported as failing to render rather
than as failing this check.
What it cannot see
The shape of the root, and nothing else. HEEx compiles every {…} into a closure the diff calls
later, so a fault inside one is not raised by render/1 at all — which is the same reason the
builder's canvas needs contain/3 — and a raise in the eager body is already rescued by
guarded_body/2, on purpose. The rescue here is a belt for what neither of those lets through.
Answers :ok for a stateless component, which has no such rule.
Create the final module AST.
Assembles all component parts into a complete Phoenix Component module AST. The generated module includes all necessary imports and uses for proper Phoenix Component operation.
Parameters
{params, functions_ast}- Tuple containing params and all accumulated function ASTs
Returns
Returns the complete module AST ready for compilation via ModuleCompiler.compile_module/1.
Generated Module Structure
The final module includes:
defmoduledeclaration with the target module name- Required Phoenix Component imports and uses
- LiveView integration via the configured
:web_module - Runtime component helper imports
- All accumulated function definitions (info, attrs, slots, helpers, main function)
The module is fully self-contained and ready for runtime compilation and loading.
Create environment for template compilation.
Builds a compilation environment with necessary imports for HEEx template processing. Ensures that Phoenix HTML, Component, and LiveView helpers are available during template compilation.
Returns
Returns a Macro.Env struct with all required imports defined for proper template compilation.
Imported Modules
Phoenix.HTML- HTML generation helpersPhoenix.Component- Component helpers and sigil_H
Failed imports are logged but don't stop the compilation process, ensuring robustness even when some modules aren't available.