From 3c06e8f211db8c3e0876733343dbd75b83866323 Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Mon, 29 Jan 2024 17:51:30 -0500 Subject: [PATCH] Fix CSS compiler Execute template post processing lifecycle on app boot Close #392 --- lib/beacon/config.ex | 25 ++++----------- lib/beacon/content.ex | 8 ++--- lib/beacon/lifecycle/template.ex | 7 +++-- lib/beacon/loader/component_module_loader.ex | 2 +- lib/beacon/loader/layout_module_loader.ex | 2 +- lib/beacon/loader/page_module_loader.ex | 11 +++++-- lib/beacon/router.ex | 8 ----- lib/beacon/tailwind_compiler.ex | 20 +++--------- lib/beacon/template/heex.ex | 33 ++++++++------------ test/beacon/tailwind_compiler_test.exs | 20 +++++++++++- test/test_helper.exs | 21 +++++++++++-- 11 files changed, 80 insertions(+), 77 deletions(-) diff --git a/lib/beacon/config.ex b/lib/beacon/config.ex index 3642435d..5ded81ab 100644 --- a/lib/beacon/config.ex +++ b/lib/beacon/config.ex @@ -94,7 +94,7 @@ defmodule Beacon.Config do {identifier :: atom(), fun :: (template :: String.t(), Beacon.Template.LoadMetadata.t() -> - {:cont, String.t() | Beacon.Template.t()} | {:halt, String.t() | Beacon.Template.t()} | {:halt, Exception.t()})} + {:cont, String.t()} | {:halt, String.t()} | {:halt, Exception.t()})} ]} ]} | {:render_template, @@ -154,16 +154,10 @@ defmodule Beacon.Config do } @default_load_template [ - {:heex, - [ - safe_code_check: &Beacon.Template.HEEx.safe_code_check/2, - compile_heex: &Beacon.Template.HEEx.compile/2 - ]}, + {:heex, []}, {:markdown, [ - convert_to_html: &Beacon.Template.Markdown.convert_to_html/2, - safe_code_check: &Beacon.Template.HEEx.safe_code_check/2, - compile_heex: &Beacon.Template.HEEx.compile/2 + convert_to_html: &Beacon.Template.Markdown.convert_to_html/2 ]} ] @@ -277,8 +271,7 @@ defmodule Beacon.Config do load_template: [ {:custom_format, [ - validate: fn template, _metadata -> MyEngine.validate(template) end, - build_rendered: fn template, _metadata -> %Phoenix.LiveView.Rendered{static: template} end, + validate: fn template, _metadata -> MyEngine.validate(template) end ]} ], render_template: [ @@ -312,18 +305,12 @@ defmodule Beacon.Config do ] lifecycle: [ load_template: [ - heex: [ - safe_code_check: &Beacon.Template.HEEx.safe_code_check/2, - compile_heex: &Beacon.Template.HEEx.compile/2 - ], + heex: [], markdown: [ convert_to_html: &Beacon.Template.Markdown.convert_to_html/2, - safe_code_check: &Beacon.Template.HEEx.safe_code_check/2, - compile_heex: &Beacon.Template.HEEx.compile/2 ], custom_format: [ - validate: #Function<41.3316493/2 in :erl_eval.expr/6>, - build_rendered: #Function<41.3316494/2 in :erl_eval.expr/6> + validate: #Function<41.3316493/2 in :erl_eval.expr/6> ] ], render_template: [ diff --git a/lib/beacon/content.ex b/lib/beacon/content.ex index 8717b795..3622e4df 100644 --- a/lib/beacon/content.ex +++ b/lib/beacon/content.ex @@ -2072,10 +2072,10 @@ defmodule Beacon.Content do defp do_validate_template(changeset, field, :heex = _format, template, metadata) when is_binary(template) do Changeset.validate_change(changeset, field, fn ^field, template -> - case Beacon.Template.HEEx.compile(template, metadata) do - {:cont, _ast} -> [] - {:halt, %{description: description}} -> [{field, {"invalid", compilation_error: description}}] - {:halt, _} -> [{field, "invalid"}] + case Beacon.Template.HEEx.compile(metadata.site, metadata.path, template) do + {:ok, _ast} -> [] + {:error, %{description: description}} -> [{field, {"invalid", compilation_error: description}}] + {:error, _} -> [{field, "invalid"}] end end) end diff --git a/lib/beacon/lifecycle/template.ex b/lib/beacon/lifecycle/template.ex index ab83fc57..9b5c291c 100644 --- a/lib/beacon/lifecycle/template.ex +++ b/lib/beacon/lifecycle/template.ex @@ -33,7 +33,8 @@ defmodule Beacon.Lifecycle.Template do lifecycle else raise Beacon.LoaderError, """ - For site: #{site_config.site} + failed to validate lifecycle input for site: #{site_config.site} + #{format_allowed_error_text(format_allowed?, sub_key, allowed_formats)} #{unconfigured_error_text(format_configured?, sub_key)} @@ -81,9 +82,9 @@ defmodule Beacon.Lifecycle.Template do @doc """ Load a `page` template using the registered format used on the `page`. - This stage runs after fetching the page from the database and before storing the template into ETS. + This stage runs after fetching the page from the database and before compiling and storing the template into ETS. """ - @spec load_template(Beacon.Content.Page.t()) :: Beacon.Template.t() + @spec load_template(Beacon.Content.Page.t()) :: String.t() def load_template(page) do lifecycle = Lifecycle.execute(__MODULE__, page.site, :load_template, page.template, sub_key: page.format, context: %{path: page.path}) lifecycle.output diff --git a/lib/beacon/loader/component_module_loader.ex b/lib/beacon/loader/component_module_loader.ex index 0a40a505..f80fcb57 100644 --- a/lib/beacon/loader/component_module_loader.ex +++ b/lib/beacon/loader/component_module_loader.ex @@ -33,7 +33,7 @@ defmodule Beacon.Loader.ComponentModuleLoader do defp render_component(%Content.Component{site: site, name: name, body: body}) do file = "site-#{site}-component-#{name}" - ast = Beacon.Template.HEEx.compile_heex_template!(file, body) + {:ok, ast} = Beacon.Template.HEEx.compile(site, "", body, file) quote do def render(unquote(name), var!(assigns)) when is_map(var!(assigns)) do diff --git a/lib/beacon/loader/layout_module_loader.ex b/lib/beacon/loader/layout_module_loader.ex index b5c5cd6c..0bc86a7d 100644 --- a/lib/beacon/loader/layout_module_loader.ex +++ b/lib/beacon/loader/layout_module_loader.ex @@ -30,7 +30,7 @@ defmodule Beacon.Loader.LayoutModuleLoader do defp render_layout(layout) do file = "site-#{layout.site}-layout-#{layout.title}" - ast = Beacon.Template.HEEx.compile_heex_template!(file, layout.template) + {:ok, ast} = Beacon.Template.HEEx.compile(layout.site, "", layout.template, file) quote do def render(var!(assigns)) when is_map(var!(assigns)) do diff --git a/lib/beacon/loader/page_module_loader.ex b/lib/beacon/loader/page_module_loader.ex index 6bcca6ce..2d8a9338 100644 --- a/lib/beacon/loader/page_module_loader.ex +++ b/lib/beacon/loader/page_module_loader.ex @@ -4,6 +4,7 @@ defmodule Beacon.Loader.PageModuleLoader do alias Beacon.Content alias Beacon.Lifecycle alias Beacon.Loader + alias Beacon.Template.HEEx require Logger @@ -208,7 +209,9 @@ defmodule Beacon.Loader.PageModuleLoader do end defp render(page, :request) do - primary = Lifecycle.Template.load_template(page) + primary_template = Lifecycle.Template.load_template(page) + {:ok, primary} = HEEx.compile(page.site, page.path, primary_template) + variants = load_variants(page) case variants do @@ -248,10 +251,14 @@ defmodule Beacon.Loader.PageModuleLoader do %{variants: variants} = Beacon.Repo.preload(page, :variants) for variant <- variants do + page = %{page | template: variant.template} + template = Lifecycle.Template.load_template(page) + {:ok, page} = HEEx.compile(page.site, page.path, template) + [ variant.name, variant.weight, - Lifecycle.Template.load_template(%{page | template: variant.template}) + page ] end end diff --git a/lib/beacon/router.ex b/lib/beacon/router.ex index 4ee73c3e..e49e92d5 100644 --- a/lib/beacon/router.ex +++ b/lib/beacon/router.ex @@ -221,14 +221,6 @@ defmodule Beacon.Router do |> List.flatten() end - def dump_page_modules(site, fun \\ &Function.identity/1) do - site - |> dump_pages() - |> Enum.map(fn {{^site, _path} = key, {_page_id, _layout_id, _format, page_module, _component_module}} -> - fun.(Tuple.append(key, page_module)) - end) - end - @doc false def lookup_path(site, path) do lookup_path(@ets_table, site, path) diff --git a/lib/beacon/tailwind_compiler.ex b/lib/beacon/tailwind_compiler.ex index 31fc9564..cc974e73 100644 --- a/lib/beacon/tailwind_compiler.ex +++ b/lib/beacon/tailwind_compiler.ex @@ -189,17 +189,10 @@ defmodule Beacon.TailwindCompiler do end) end), Task.async(fn -> - # parse from laoded pages (ETS) so it can fetch callback transformations - # thay may include additoinal stylesheet classes as the markdown parser does - Beacon.Router.dump_page_modules(site, fn {_site, path, page_module} -> - template = - page_module - |> Beacon.Template.render() - |> fetch_static() - |> List.to_string() - - page_path = Path.join(tmp_dir, "#{site}_page_#{remove_special_chars(path)}.template") - File.write!(page_path, template) + Enum.map(Content.list_published_pages(site), fn page -> + page_path = Path.join(tmp_dir, "#{site}_page_#{remove_special_chars(page.path)}.template") + post_processed_template = Beacon.Lifecycle.Template.load_template(page) + File.write!(page_path, post_processed_template) page_path end) end), @@ -215,9 +208,6 @@ defmodule Beacon.TailwindCompiler do |> List.flatten() end - defp fetch_static(%{static: static}), do: static - defp fetch_static(_), do: [] - # import app css into input css used by tailwind-cli to load tailwind functions and directives defp generate_input_css_file!(tmp_dir) do content = ~S| @@ -246,7 +236,7 @@ defmodule Beacon.TailwindCompiler do input_css_path end - defp remove_special_chars(name), do: String.replace(name, ~r/[^[:alnum:]_-]+/, "_") + defp remove_special_chars(name), do: String.replace(name, ~r/[^[:alnum:]_]+/, "_") # include paths for the following scenarios: # - regular app diff --git a/lib/beacon/template/heex.ex b/lib/beacon/template/heex.ex index 66aa6395..ee1250e5 100644 --- a/lib/beacon/template/heex.ex +++ b/lib/beacon/template/heex.ex @@ -3,39 +3,35 @@ defmodule Beacon.Template.HEEx do Handle loading and compilation of HEEx templates. """ - require Logger - @doc """ Check if the template is safe. Perform the check using https://github.com/TheFirstAvenger/safe_code """ - @spec safe_code_check(Beacon.Template.t(), Beacon.Template.LoadMetadata.t()) :: {:cont, Beacon.Template.t()} | {:halt, Exception.t()} - def safe_code_check(template, _metadata) when is_binary(template) do + @spec safe_code_check(String.t()) :: {:ok, String.t()} | {:error, Exception.t()} + def safe_code_check(template) when is_binary(template) do # TODO: enable safe code when it's ready to parse complex templates # SafeCode.Validator.validate!(template, extra_function_validators: Beacon.Loader.SafeCodeImpl) - {:cont, template} + {:ok, template} rescue exception -> - {:halt, exception} + {:error, exception} end @doc """ Compile `template` returning its AST. """ - @spec compile(Beacon.Template.t(), Beacon.Template.LoadMetadata.t()) :: {:cont, Beacon.Template.ast()} | {:halt, Exception.t()} - def compile(template, metadata) when is_binary(template) do - file = "site-#{metadata.site}-page-#{metadata.path}" - ast = compile_heex_template!(file, template) - # :cont so others can reuse this step - {:cont, ast} + @spec compile(Beacon.Types.Site.t(), String.t(), String.t()) :: {:ok, Beacon.Template.ast()} | {:error, Exception.t()} + def compile(site, path, template, file \\ nil) when is_atom(site) and is_binary(path) and is_binary(template) do + file = if file, do: file, else: "site-#{site}-path-#{path}" + ast = compile_template!(file, template) + {:ok, ast} rescue exception -> - {:halt, exception} + {:error, exception} end - @doc false - def compile_heex_template!(file, template) do + defp compile_template!(file, template) do opts = [ engine: Phoenix.LiveView.TagEngine, @@ -80,11 +76,8 @@ defmodule Beacon.Template.HEEx do ] env = %{env | functions: functions} - - {rendered, _} = - "nofile" - |> compile_heex_template!(template) - |> Code.eval_quoted([assigns: assigns], env) + {:ok, ast} = compile(site, "", template) + {rendered, _} = Code.eval_quoted(ast, [assigns: assigns], env) rendered |> Phoenix.HTML.Safe.to_iodata() diff --git a/test/beacon/tailwind_compiler_test.exs b/test/beacon/tailwind_compiler_test.exs index fc22cda8..129db315 100644 --- a/test/beacon/tailwind_compiler_test.exs +++ b/test/beacon/tailwind_compiler_test.exs @@ -33,7 +33,20 @@ defmodule Beacon.TailwindCompilerTest do published_page_fixture( layout_id: layout.id, - path: "/a", + path: "/tailwind-test", + template: """ +
+

Some Values:

+ <%= for val <- @beacon_live_data[:vals] do %> + <%= my_component("sample_component", val: val) %> + <% end %> +
+ """ + ) + + published_page_fixture( + layout_id: layout.id, + path: "/tailwind-test-post-process", template: """

Some Values:

@@ -88,6 +101,11 @@ defmodule Beacon.TailwindCompilerTest do refute output =~ "text-gray-300" end) end + + test "fetch post processed page templates" do + assert {:ok, output} = TailwindCompiler.compile(@site) + assert output =~ "text-blue-200" + end end describe "compile template" do diff --git a/test/test_helper.exs b/test/test_helper.exs index 8e0074f1..2e381661 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -19,7 +19,22 @@ Supervisor.start_link( tailwind_config: Path.join([File.cwd!(), "test", "support", "tailwind.config.templates.js.eex"]), data_source: Beacon.BeaconTest.BeaconDataSource, live_socket_path: "/custom_live", - extra_page_fields: [Beacon.BeaconTest.PageFields.TagsField] + extra_page_fields: [Beacon.BeaconTest.PageFields.TagsField], + lifecycle: [ + load_template: [ + {:heex, + [ + tailwind_test: fn + template, %{site: :my_site, path: "/tailwind-test-post-process"} -> + template = String.replace(template, "text-gray-200", "text-blue-200") + {:cont, template} + + template, _metadata -> + {:cont, template} + end + ]} + ] + ] ], [ site: :s3_site, @@ -58,8 +73,8 @@ Supervisor.start_link( [ div_to_p: fn template, _metadata -> {:cont, String.replace(template, "div", "p")} end, assigns: fn template, _metadata -> {:cont, String.replace(template, "{ title }", "Beacon")} end, - compile: fn template, _metadata -> - ast = Beacon.Template.HEEx.compile_heex_template!("nofile", template) + compile: fn template, metadata -> + {:ok, ast} = Beacon.Template.HEEx.compile(metadata.site, metadata.path, template) {:cont, ast} end, eval: fn template, _metadata ->