Skip to content

Commit

Permalink
Fixed new template re-compilation issue
Browse files Browse the repository at this point in the history
If a new template was added when the Phoenix server is running that new template
wasn't being picked up by the CodeReloader because it wasn't being tracked. The originaly implementation
copy-pasta'd the Phoenix LiveView implementation which gets around this because the template
names are known AOT even if the template doesn't exist yet so that `@external_resource` was tracked
but in LVN because we incorporate the TARGET into the template name (i.e. `home_live.swiftui+ios.neex`)
we cannot use this method. The solution was to implement `__mix_recompile__?` and track file collection
hashes for comparing if a new file was added or not that *should* be treated as a template for the
given LiveView Native Render Component
  • Loading branch information
bcardarella committed May 1, 2024
1 parent 70d0c2e commit 858372f
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 58 deletions.
5 changes: 5 additions & 0 deletions lib/live_view_native/component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -167,19 +167,24 @@ 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
@before_compile LiveViewNative.Renderer
end

@before_compile LiveViewNative.Component
@before_compile {LiveViewNative.Renderer, :__inject_mix_recompile__}
end

:error ->
Expand Down
169 changes: 113 additions & 56 deletions lib/live_view_native/renderer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand All @@ -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])
Expand Down Expand Up @@ -71,20 +100,51 @@ 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],
env: env,
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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -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"},
Expand Down

0 comments on commit 858372f

Please sign in to comment.