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/1 and render/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_attributes(arg)

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_component_function(arg)

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:

  1. Merges default values into assigns
  2. Executes any body code (if present)
  3. Compiles and renders the HEEx template
  4. 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(params)

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

build_event_fallback(arg)

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_helpers(arg)

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.

build_module(params)

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_attributes(arg)

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_slots(arg)

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.

check_root(module)

@spec check_root(module()) :: :ok | {:error, String.t()}

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 root

and 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_module_ast(arg)

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:

  • defmodule declaration 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.

make_env()

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

Failed imports are logged but don't stop the compilation process, ensuring robustness even when some modules aren't available.