diff --git a/lib/beacon/registry.ex b/lib/beacon/registry.ex index 79e76aa4..ad46b7a9 100644 --- a/lib/beacon/registry.ex +++ b/lib/beacon/registry.ex @@ -16,30 +16,43 @@ defmodule Beacon.Registry do @doc false def via(key, value), do: {:via, Registry, {__MODULE__, key, value}} + @doc """ + Return a list of all running sites in the current node. + """ + @spec running_sites() :: [Beacon.Types.Site.t()] + def running_sites do + match = {{:site, :"$1"}, :_, :_} + guards = [] + body = [:"$1"] + + Registry.select(__MODULE__, [{match, guards, body}]) + end + @doc false - def config!(site) do + def config!(site) when is_atom(site) do case lookup({:site, site}) do {_pid, config} -> config _ -> raise RuntimeError, """ - Site #{inspect(site)} was not found. Make sure it's configured and started, + site #{inspect(site)} was not found. Make sure it's configured and started, see `Beacon.start_link/1` for more info. """ end end - @doc """ - Return a list of all running sites in the current node. - """ - @spec running_sites() :: [Beacon.Types.Site.t()] - def running_sites do - match = {{:site, :"$1"}, :_, :_} - guards = [] - body = [:"$1"] - - Registry.select(__MODULE__, [{match, guards, body}]) + @doc false + def update_config(site, fun) when is_atom(site) and is_function(fun, 1) do + result = + Registry.update_value(__MODULE__, {:site, site}, fn config -> + fun.(config) + end) + + case result do + {new_value, _old_value} -> new_value + error -> error + end end defp lookup(site) do diff --git a/lib/beacon/runtime_css.ex b/lib/beacon/runtime_css.ex index 28d64ae1..d40bec5d 100644 --- a/lib/beacon/runtime_css.ex +++ b/lib/beacon/runtime_css.ex @@ -4,12 +4,18 @@ defmodule Beacon.RuntimeCSS do """ @callback compile(Beacon.Types.Site.t()) :: {:ok, String.t()} | {:error, any()} + @callback compile(Beacon.Types.Site.t(), template :: String.t()) :: {:ok, String.t()} | {:error, any()} @doc false def compile(site) when is_atom(site) do Beacon.Config.fetch!(site).css_compiler.compile(site) end + @doc false + def compile(site, template) when is_atom(site) and is_binary(template) do + Beacon.Config.fetch!(site).css_compiler.compile(site, template) + end + @doc false def fetch(site) do case :ets.match(:beacon_assets, {{site, :css}, {:_, :_, :"$1"}}) do diff --git a/lib/beacon/tailwind_compiler.ex b/lib/beacon/tailwind_compiler.ex index dc810464..78b196a6 100644 --- a/lib/beacon/tailwind_compiler.ex +++ b/lib/beacon/tailwind_compiler.ex @@ -27,6 +27,41 @@ defmodule Beacon.TailwindCompiler do @impl Beacon.RuntimeCSS @spec compile(Beacon.Types.Site.t()) :: {:ok, String.t()} | {:error, any()} def compile(site) when is_atom(site) do + tmp_dir = tmp_dir!() + config_file_path = generate_tailwind_config_file(site, tmp_dir, beacon_content(tmp_dir)) + templates_path = generate_template_files!(tmp_dir, site) + input_css_path = generate_input_css_file!(tmp_dir, site) + output = execute(tmp_dir, config_file_path, input_css_path) + cleanup(tmp_dir, templates_path) + {:ok, output} + end + + @impl Beacon.RuntimeCSS + @spec compile(Beacon.Types.Site.t(), template :: String.t()) :: {:ok, String.t()} | {:error, any()} + def compile(site, template) when is_atom(site) and is_binary(template) do + tmp_dir = tmp_dir!() + + content = [ + ?{, + " raw: ", + ?', + template, + ?', + ", extension: ", + ?', + "html", + ?', + " ", + ?} + ] + + config_file_path = generate_tailwind_config_file(site, tmp_dir, content) + input_css_path = generate_input_css_file!(tmp_dir) + output = execute(tmp_dir, config_file_path, input_css_path) + {:ok, output} + end + + defp generate_tailwind_config_file(site, tmp_dir, content) do tailwind_config = tailwind_config!(site) unless Application.get_env(:tailwind, :version) do @@ -36,43 +71,38 @@ defmodule Beacon.TailwindCompiler do Application.put_env(:tailwind, :beacon_runtime, []) - tmp_dir = tmp_dir!() - - generated_config_file_path = - tailwind_config - |> EEx.eval_file(assigns: %{beacon_content: beacon_content(tmp_dir)}) - |> write_file!(tmp_dir, "tailwind.config.js") - - templates_paths = generate_template_files!(tmp_dir, site) - - input_css_path = generate_input_css_file!(tmp_dir, site) + tailwind_config + |> EEx.eval_file(assigns: %{beacon_content: content}) + |> write_file!(tmp_dir, "tailwind.config.js") + end + defp execute(tmp_dir, config_file_path, input_css_file_path) do output_css_path = Path.join(tmp_dir, "generated.css") opts = if Code.ensure_loaded?(Mix.Project) and Mix.env() in [:test, :dev] do ~w( - --config=#{generated_config_file_path} - --input=#{input_css_path} + --config=#{config_file_path} + --input=#{input_css_file_path} --output=#{output_css_path} ) else ~w( - --config=#{generated_config_file_path} - --input=#{input_css_path} + --config=#{config_file_path} + --input=#{input_css_file_path} --output=#{output_css_path} --minify ) end - {cli_output, cli_exit_code} = run(:beacon_runtime, opts) + {cli_output, cli_exit_code} = run_cli(:beacon_runtime, opts) output = if cli_exit_code == 0 do "/* Generated by #{__MODULE__} at #{DateTime.utc_now()} */" <> "\n" <> File.read!(output_css_path) else raise """ - error running tailwind, got exit code: #{cli_exit_code}" + error running tailwind compiler, got exit code: #{cli_exit_code}" Tailwind bin path: #{inspect(Tailwind.bin_path())} Tailwind bin version: #{inspect(Tailwind.bin_version())} @@ -81,15 +111,15 @@ defmodule Beacon.TailwindCompiler do """ end - cleanup(tmp_dir, [generated_config_file_path, input_css_path, output_css_path] ++ templates_paths) + cleanup(tmp_dir, [config_file_path, input_css_file_path, output_css_path]) - {:ok, output} + output end # Run tailwind-cli returning the output and exit code # Note that `:cd` is the root dir for regular and umbrella projects so the paths have to be defined accordingly. # https://github.com/phoenixframework/tailwind/blob/8cf9810474bf37c1b1dd821503d756885534d2ba/lib/tailwind.ex#L192 - def run(profile, extra_args) when is_atom(profile) and is_list(extra_args) do + def run_cli(profile, extra_args) when is_atom(profile) and is_list(extra_args) do if Tailwind.bin_version() == :error do message = """ tailwind-cli binary not found or the installation is invalid. @@ -189,6 +219,18 @@ defmodule Beacon.TailwindCompiler do 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| + @import "tailwindcss/base"; + @import "tailwindcss/components"; + @import "tailwindcss/utilities"; + | + + input_css_path = Path.join(tmp_dir, "input.css") + File.write!(input_css_path, content) + input_css_path + end + defp generate_input_css_file!(tmp_dir, site) do beacon_tailwind_css_path = Path.join([Application.app_dir(:beacon), "priv", "beacon_tailwind.css"]) diff --git a/test/beacon/registry_test.exs b/test/beacon/registry_test.exs index 3729fbb2..f5bd764a 100644 --- a/test/beacon/registry_test.exs +++ b/test/beacon/registry_test.exs @@ -1,13 +1,22 @@ defmodule Beacon.RegistryTest do use ExUnit.Case, async: true - alias Beacon.Registry - test "running_sites" do - running_sites = Registry.running_sites() + running_sites = Beacon.Registry.running_sites() assert Enum.sort(running_sites) == [:data_source_test, :default_meta_tags_test, :lifecycle_test, :lifecycle_test_fail, :my_site, :s3_site] end + test "update_config" do + # register a config in the test process to make Registry.update_value/3 work + assert %Beacon.Config{live_socket_path: "/custom_live"} = config = Beacon.Registry.config!(:my_site) + Registry.register(Beacon.Registry, {:site, :test_update_config}, config) + + assert %Beacon.Config{live_socket_path: "/test_update_config"} = + Beacon.Registry.update_config(:test_update_config, fn config -> + %{config | live_socket_path: "/test_update_config"} + end) + end + describe "config!" do test "return site config for existing sites" do assert %Beacon.Config{ @@ -18,14 +27,14 @@ defmodule Beacon.RegistryTest do safe_code_check: false, site: :my_site, tailwind_config: tailwind_config - } = Registry.config!(:my_site) + } = Beacon.Registry.config!(:my_site) - assert tailwind_config =~ "tailwind.config.js.eex" + assert tailwind_config =~ "tailwind.config.templates.js.eex" end test "raise when not found" do - assert_raise RuntimeError, ~r/Site :invalid was not found/, fn -> - Registry.config!(:invalid) + assert_raise RuntimeError, ~r/site :invalid was not found/, fn -> + Beacon.Registry.config!(:invalid) end end end diff --git a/test/beacon/tailwind_compiler_test.exs b/test/beacon/tailwind_compiler_test.exs index 5c723506..fc22cda8 100644 --- a/test/beacon/tailwind_compiler_test.exs +++ b/test/beacon/tailwind_compiler_test.exs @@ -8,7 +8,7 @@ defmodule Beacon.TailwindCompilerTest do @site :my_site setup_all do - start_supervised!({Beacon.Loader, Beacon.Config.fetch!(:my_site)}) + start_supervised!({Beacon.Loader, Beacon.Config.fetch!(@site)}) :ok end @@ -63,12 +63,12 @@ defmodule Beacon.TailwindCompilerTest do :ok end - describe "compile/2" do + describe "compile site" do setup [:create_page] test "includes classes from all resources" do capture_io(fn -> - assert {:ok, output} = TailwindCompiler.compile(:my_site) + assert {:ok, output} = TailwindCompiler.compile(@site) # test/support/templates/*.*ex assert output =~ "text-red-50" @@ -83,10 +83,26 @@ defmodule Beacon.TailwindCompilerTest do test "do not include classes from unpublished pages" do capture_io(fn -> - assert {:ok, output} = TailwindCompiler.compile(:my_site) + assert {:ok, output} = TailwindCompiler.compile(@site) refute output =~ "text-gray-300" end) end end + + describe "compile template" do + test "compile a specific template binary with custom tailwind config" do + capture_io(fn -> + config = Beacon.Registry.config!(@site) + Registry.register(Beacon.Registry, {:site, :test_tailwind_compile_template}, config) + + Beacon.Registry.update_config(:test_tailwind_compile_template, fn config -> + %{config | tailwind_config: Path.join([File.cwd!(), "test", "support", "tailwind.config.custom.js.eex"])} + end) + + {:ok, css} = TailwindCompiler.compile(:test_tailwind_compile_template, ~S|