Skip to content

Commit

Permalink
Add RuntimeCSS.compile/2 callback
Browse files Browse the repository at this point in the history
To allow generating a small stylesheet for a snippet template,
to be used on the page build.

Related to BeaconCMS/beacon_live_admin#80
  • Loading branch information
leandrocp committed Nov 21, 2023
1 parent a2471d1 commit f0bcf76
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 43 deletions.
37 changes: 25 additions & 12 deletions lib/beacon/registry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/beacon/runtime_css.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 61 additions & 19 deletions lib/beacon/tailwind_compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())}
Expand All @@ -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.
Expand Down Expand Up @@ -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"])

Expand Down
23 changes: 16 additions & 7 deletions test/beacon/registry_test.exs
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
defmodule Beacon.RegistryTest do
use ExUnit.Case, async: true

alias Beacon.Registry

test "running_sites" do

Check failure on line 4 in test/beacon/registry_test.exs

View workflow job for this annotation

GitHub Actions / test: OTP 23 | Elixir 1.13.0 | Phoenix 1.7.0 | LiveView 0.19.0

test running_sites (Beacon.RegistryTest)
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{
Expand All @@ -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
Expand Down
24 changes: 20 additions & 4 deletions test/beacon/tailwind_compiler_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"
Expand All @@ -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|<div class="text-gray-50">|)
assert css =~ "text-gray-50"
end)
end
end
end
5 changes: 5 additions & 0 deletions test/support/tailwind.config.custom.js.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
content: [
<%= @beacon_content %>
]
}
File renamed without changes.
2 changes: 1 addition & 1 deletion test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Supervisor.start_link(
[
site: :my_site,
endpoint: Beacon.BeaconTest.Endpoint,
tailwind_config: Path.join([File.cwd!(), "test", "support", "tailwind.config.js.eex"]),
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]
Expand Down

0 comments on commit f0bcf76

Please sign in to comment.