diff --git a/lib/live_view_native/component.ex b/lib/live_view_native/component.ex index 59ded7b..8e614ae 100644 --- a/lib/live_view_native/component.ex +++ b/lib/live_view_native/component.ex @@ -167,12 +167,16 @@ defmodule LiveViewNative.Component do case LiveViewNative.fetch_plugin(format) do {:ok, plugin} -> quote do + Module.register_attribute(__MODULE__, :template_files, accumulate: true) + Module.register_attribute(__MODULE__, :embeded_templates_opts, accumulate: true) + import LiveViewNative.Renderer, only: [ delegate_to_target: 1, delegate_to_target: 2, embed_templates: 1, embed_templates: 2 ] + use unquote(plugin.component) if (unquote(opts[:as])) do @@ -180,6 +184,7 @@ defmodule LiveViewNative.Component do end @before_compile LiveViewNative.Component + @before_compile {LiveViewNative.Renderer, :__inject_mix_recompile__} end :error -> diff --git a/lib/live_view_native/renderer.ex b/lib/live_view_native/renderer.ex index f851cd7..4999e8b 100644 --- a/lib/live_view_native/renderer.ex +++ b/lib/live_view_native/renderer.ex @@ -17,6 +17,34 @@ defmodule LiveViewNative.Renderer do end end + @doc false + defmacro __inject_mix_recompile__(_env) do + quote do + @template_file_hash @template_files |> Enum.sort() |> :erlang.md5() + + @doc false + def __mix_recompile__? do + files = + @embeded_templates_opts + |> Enum.reduce([], fn({root, pattern, name}, templates_acc) -> + root + |> LiveViewNative.Renderer.find_templates(pattern, __MODULE__, name) + |> Enum.reduce(templates_acc, fn + {:no_embed, _reason}, templates_acc -> templates_acc + {:embed, templates}, templates_acc -> templates_acc ++ templates + end) + end) + + file_hash = + files + |> Enum.sort() + |> :erlang.md5() + + !(file_hash == @template_file_hash) + end + end + end + @doc false defmacro delegate_to_target(name, opts \\ []) do %{module: module} = env = __CALLER__ @@ -33,6 +61,7 @@ defmodule LiveViewNative.Renderer do [] else quote do + @doc false def unquote(name)(var!(assigns)) do interface = LiveViewNative.Utils.get_interface(var!(assigns)) apply(__MODULE__, unquote(name), [var!(assigns), interface]) @@ -71,13 +100,20 @@ defmodule LiveViewNative.Renderer do %{module: module} = env = __CALLER__ native_opts = Module.get_attribute(module, :native_opts) format = native_opts[:format] - root = build_root(env.file, opts[:root]) + name = opts[:name] - root - |> Phoenix.Template.find_all(pattern) - |> Enum.chunk_by(&chunk_name(&1)) - |> ensure_naming_uniq(env, opts[:name]) + + attr_ast = quote do + Module.put_attribute(__MODULE__, :embeded_templates_opts, { + unquote(root), + unquote(pattern), + unquote(name) + }) + end + + templates_ast = root + |> find_templates(pattern, module, name) |> Enum.map(&(__embed_templates__(&1, format: format, name: opts[:name], @@ -85,6 +121,30 @@ defmodule LiveViewNative.Renderer do root: root, pattern: pattern ))) + + [attr_ast | templates_ast] + end + + @doc false + def find_templates(root, pattern, module, default_name) do + root + |> Phoenix.Template.find_all(pattern) + |> Enum.chunk_by(&chunk_name(&1)) + |> ensure_naming_uniq(pattern, default_name) + |> Enum.map(fn(templates) -> + name = build_name(templates, default_name) + render? = case Code.ensure_compiled(module) do + {:error, _} -> Module.defines?(module, {name, 2}) + {:module, _} -> false + end + + case {render?, templates} do + {true, [_template | _templates]} -> {:no_embed, :render_defined_with_templates} + {true, []} -> {:no_embed, :render_defined_no_templates} + {false, []} -> {:no_embed, :no_render_no_templates} + {false, templates} -> {:embed, templates} + end + end) end # this function ensures there is a single template group when applying a custom render function name @@ -115,74 +175,71 @@ defmodule LiveViewNative.Renderer do |> List.first() end - defp __embed_templates__(templates, opts) do + defp __embed_templates__({:no_embed, reason}, opts) do %{module: module} = env = opts[:env] - format = opts[:format] - name = build_name(templates, opts[:name]) + name = build_name([], opts[:name]) - render? = Module.defines?(module, {name, 2}) - filename = build_filename(module, format) - - case {render?, templates} do - {true, [_template | _templates]} -> + case reason do + :render_defined_with_templates -> IO.warn( - "You have #{module}.render/2 defined as well as at least one template file. You must remove " <> - " #{module}.render/2 if you wish to use any template files.", + "You have #{module}.#{name}/2 defined as well as at least one template file. You must remove " <> + " #{module}.#{name}/2 if you wish to use any template files.", Macro.Env.stacktrace(env) ) [] - - {true, []} -> [] - - {false, []} -> + :render_defined_no_templates -> [] + :no_render_no_templates -> IO.warn( "You do not have any templates or any `render/2` functions defined for #{module}.", Macro.Env.stacktrace(env) ) [] - - {false, templates} -> - templates - |> Enum.sort(&(String.length(&1) >= String.length(&2))) - |> Enum.map(fn(template) -> - - engine = Map.fetch!(LiveViewNative.Template.engines(), format) - ast = engine.compile(template, filename) - - case extract_target(template, format) do - nil -> - quote do - @file unquote(template) - @external_resource unquote(template) - def unquote(name)(var!(assigns), _interface) do - unquote(ast) - end - end - - target -> - quote do - @file unquote(template) - @external_resource unquote(template) - def unquote(name)(var!(assigns), %{"target" => unquote(target)}) do - unquote(ast) - end - end - end - end) + _unmatched_reason -> [] end - |> inject_target_delegate(name) end - defp inject_target_delegate([], _name), do: [] - defp inject_target_delegate(quoted_renders, name) do - quoted_render = - quote do - delegate_to_target unquote(name) - end + defp __embed_templates__({:embed, templates}, opts) do + %{module: module} = opts[:env] + format = opts[:format] + name = build_name(templates, opts[:name]) + filename = build_filename(module, format) + + templates + |> Enum.sort(&(String.length(&1) >= String.length(&2))) + |> Enum.map(fn(template) -> + + engine = Map.fetch!(LiveViewNative.Template.engines(), format) + ast = engine.compile(template, filename) + + case extract_target(template, format) do + nil -> + quote do + @file unquote(template) + @external_resource unquote(template) + @template_files unquote(template) + @doc false + def unquote(name)(var!(assigns), _interface) do + unquote(ast) + end + end - [quoted_render | quoted_renders] + target -> + quote do + @file unquote(template) + @external_resource unquote(template) + @template_files unquote(template) + @doc false + def unquote(name)(var!(assigns), %{"target" => unquote(target)}) do + unquote(ast) + end + end + end + end) + |> List.insert_at(-1, quote do + delegate_to_target unquote(name) + end) end defp extract_target(template, format) do diff --git a/mix.lock b/mix.lock index b613e46..a70c12d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,12 @@ %{ - "castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"}, + "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "ex_doc": {:hex, :ex_doc, "0.32.1", "21e40f939515373bcdc9cffe65f3b3543f05015ac6c3d01d991874129d173420", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5142c9db521f106d61ff33250f779807ed2a88620e472ac95dc7d59c380113da"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, - "floki": {:hex, :floki, "0.36.1", "712b7f2ba19a4d5a47dfe3e74d81876c95bbcbee44fe551f0af3d2a388abb3da", [:mix], [], "hexpm", "21ba57abb8204bcc70c439b423fc0dd9f0286de67dc82773a14b0200ada0995f"}, + "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "live_view_native_test": {:git, "https://github.com/liveview-native/live_view_native_test.git", "f36efa463e172df27d50ab0bcbd16f2e59e6c05b", [tag: "v0.3.0"]}, "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},