Hello colleagues,
I’m creating a custom component at runtime, and I have no issues using it in the source code or wherever it’s needed — everything works fine at runtime. However, during development, if I try to reference this runtime-defined component in a file like this:
<MyApp.Components.MyButton.my_button class="btn-primary">
This is a button
</MyApp.Components.MyButton.my_button>
I get the following warning in the editor (Code works but still i just see this warning in editor not iex, i am using Zed):
MyApp.Components.MyButton.my_button/1 is undefined (module MyApp.Components.MyButton is not available or is yet to be defined)
(Elixir)
I’d appreciate your help in resolving this issue.
Here’s the code I’m using:
Consider I mixed Phoenix component source and Beacon code
defmodule MishkaCms.Runtime.Compilers.ComponentCompiler do
@moduledoc """
Runtime compiler for Phoenix Components with dynamic content compilation
"""
import Kernel, except: [def: 2, defp: 2]
import Phoenix.Component.Declarative
@supported_attr_types ~w(any string atom boolean integer float list map global)
####################################################################################
############################ (▰˘◡˘▰) Functions (▰˘◡˘▰) #############################
####################################################################################
@doc """
Main function to build component modules from component data
"""
def build_module(params) do
params
|> build_component_information()
|> build_attributes()
|> build_slots()
|> build_component_function()
|> build_helpers()
|> create_module_ast()
end
@doc """
Build component information including metadata
"""
def build_component_information(params) do
component_info_ast =
quote do
def __component_information__() do
%{
component: %{
name: unquote(params[:name]),
site: unquote(params[:site]),
description: unquote(params[:description]),
category: unquote(params[:category]),
timestamp: unquote(Macro.escape(params[:timestamp])),
attrs: unquote(Macro.escape(params[:attrs] || [])),
slots: unquote(Macro.escape(params[:slots] || []))
},
private: %{
id: unquote(params[:id]),
template: unquote(params[:template]),
body: unquote(params[:body]),
format: unquote(params[:format]) || :heex
},
extra: unquote(Macro.escape(params[:extra]))
}
end
def __component_information__(key) when is_atom(key) do
get_in(__component_information__(), [key])
end
def __component_information__(keys) when is_list(keys) do
get_in(__component_information__(), keys)
end
def __component_information__(key) when is_binary(key) do
String.split(key, ".")
|> Enum.map(&String.to_existing_atom/1)
|> then(&get_in(__component_information__(), &1))
rescue
_ -> nil
end
end
{params, [component_info_ast]}
end
@doc """
Build component attributes
"""
def build_attributes({params, functions_ast}) do
attr_setup_ast =
quote do
[] = Phoenix.Component.Declarative.__setup__(__MODULE__, [])
attr = fn name, type, opts ->
Phoenix.Component.Declarative.__attr__!(
__MODULE__,
name,
type,
opts,
__ENV__.line,
__ENV__.file
)
end
end
attributes_ast =
Enum.map(params[:attrs] || [], fn component_attr ->
quote do
attr.(
unquote(String.to_atom(component_attr.name)),
unquote(
attr_type_to_atom(component_attr.type, Map.get(component_attr, :struct_name))
),
unquote(Macro.escape(ignore_invalid_attr_opts(component_attr.opts)))
)
end
end)
{params, functions_ast ++ [attr_setup_ast] ++ attributes_ast}
end
@doc """
Build component slots
"""
def build_slots({params, functions_ast}) do
slot_setup_ast =
quote do
slot = fn name, opts, block ->
Phoenix.Component.Declarative.__slot__!(
__MODULE__,
name,
opts,
__ENV__.line,
__ENV__.file,
fn -> nil end
)
end
end
slots_ast =
Enum.map(params[:slots] || [], fn component_slot ->
quote do
slot.(
unquote(String.to_atom(component_slot.name)),
unquote(ignore_invalid_slot_opts(component_slot.opts)),
do:
unquote_splicing(
for slot_attr <- component_slot.attrs do
quote do
attr.(
unquote(String.to_atom(slot_attr.name)),
unquote(
attr_type_to_atom(slot_attr.type, Map.get(slot_attr, :struct_name))
),
unquote(Macro.escape(ignore_invalid_attr_opts(slot_attr.opts)))
)
end
end
)
)
end
end)
{params, functions_ast ++ [slot_setup_ast] ++ slots_ast}
end
@doc """
Build the main component function with HEEx template compilation
"""
def build_component_function({params, functions_ast}) do
{template_ast, body_ast} = compile_template_and_body(params)
component_function_ast =
quote do
def unquote(String.to_atom(params[:name]))(var!(assigns)) do
unquote(body_ast)
unquote(template_ast)
end
def render(unquote(params[:name]), var!(assigns)) when is_map(var!(assigns)) do
unquote(body_ast)
unquote(template_ast)
end
end
{params, functions_ast ++ [component_function_ast]}
end
@doc """
Build helper functions for the component
"""
def build_helpers({params, functions_ast}) do
helpers_ast =
Enum.map(params[:helpers] || [], fn helper ->
args = Code.string_to_quoted!(helper.args)
quote do
def unquote(String.to_atom(helper.name))(unquote(args)) do
unquote(Code.string_to_quoted!(helper.code))
end
end
end)
{params, functions_ast ++ helpers_ast}
end
@doc """
Create the final module AST
"""
def create_module_ast({params, functions_ast}) do
quote do
defmodule unquote(params[:component_module]) do
import Phoenix.Component.Declarative
import Phoenix.Component.Declarative
import Phoenix.HTML
import Phoenix.Component
unquote_splicing(functions_ast)
end
end
end
####################################################################################
############################ (▰˘◡˘▰) Helpers (▰˘◡˘▰) ###############################
####################################################################################
defp compile_template_and_body(params) do
body_ast =
if params[:body] && params[:body] != "" do
Code.string_to_quoted!(params[:body])
else
quote do: nil
end
# Compile template using EEx with Phoenix LiveView engine
template_ast =
if params[:template] && params[:template] != "" do
opts = [
engine: Phoenix.LiveView.TagEngine,
line: 1,
indentation: 0,
file: "mishka_cms_component_#{params[:name]}_#{params[:site]}",
caller: make_env(),
source: params[:template],
trim: true,
tag_handler: Phoenix.LiveView.HTMLEngine
]
EEx.compile_string(params[:template], opts)
else
quote do: ~H""
end
{template_ast, body_ast}
end
@doc """
Create environment for template compilation
"""
def make_env() do
imports = [
Phoenix.HTML,
Phoenix.Component,
Phoenix.LiveView.Helpers
]
Enum.reduce(imports, __ENV__, fn module, env ->
with true <- :erlang.module_loaded(module),
{:ok, env} <- define_import(env, module) do
env
else
{:error, error} ->
require Logger
Logger.warning("failed to import #{module}: #{error}")
env
_ ->
env
end
end)
end
defp define_import(env, module) do
meta = []
Macro.Env.define_import(env, meta, module)
end
defp attr_type_to_atom(component_type, struct_name) when component_type == "struct" do
Module.concat([struct_name])
end
defp attr_type_to_atom(component_type, _struct_name)
when component_type in @supported_attr_types do
String.to_atom(component_type)
end
defp ignore_invalid_attr_opts(opts) do
Keyword.take(opts, [:required, :default, :examples, :values, :doc])
end
defp ignore_invalid_slot_opts(opts) do
Keyword.take(opts, [:required, :validate_attrs, :doc])
end
end
By the way, i compile the ast like this
defp compile_quoted(quoted, file, module) do
{result, diagnostics} = Code.with_diagnostics(fn -> do_compile_and_load(quoted, file) end)
diagnostics = Enum.uniq(diagnostics)
case result do
{:ok, module} -> {:ok, module, diagnostics}
{:error, error} -> {:error, module, {error, diagnostics}}
end
end
# Performs the actual compilation and loading
defp do_compile_and_load(quoted, file) do
[{module, _}] = :elixir_compiler.quoted(quoted, file, fn _, _ -> :ok end)
{:ok, module}
rescue
error ->
{:error, error}
end
It dose not effect on my code, but I think it might confuse the user.
Thank you in advance