Skip to content

Instantly share code, notes, and snippets.

@Fudoshiki
Last active December 20, 2024 13:19
Show Gist options
  • Save Fudoshiki/014d730f27e44916f92a28b69e777d2f to your computer and use it in GitHub Desktop.
Save Fudoshiki/014d730f27e44916f92a28b69e777d2f to your computer and use it in GitHub Desktop.
recompile exception
defmodule PhoenixAssetPipeline.Helpers do
@moduledoc false
import PhoenixAssetPipeline.Obfuscator
defmacro __before_compile__(env) do
classes = Module.get_attribute(env.module, :__classes__, [])
recompile? = Enum.any?(classes, &match?({:error, _}, &1))
quote do
def __mix_recompile__?, do: unquote(recompile?)
def __verify_classes__(_) do
unquote(__MODULE__).__verify_classes__(unquote(classes), unquote(Macro.escape(env)))
end
end
end
defmacro __using__(_) do
quote do
import unquote(__MODULE__)
Module.register_attribute(__MODULE__, :__classes__, accumulate: true)
@after_verify {__MODULE__, :__verify_classes__}
@before_compile unquote(__MODULE__)
end
end
defmacro class(class_name) do
class_name
|> obfuscate()
|> Enum.map(fn
{:ok, {_, short_name} = class} ->
Module.put_attribute(__CALLER__.module, :__classes__, {:ok, class})
short_name
{:error, class} ->
Module.put_attribute(__CALLER__.module, :__classes__, {:error, class})
nil
end)
|> Enum.sort()
end
defp obfuscate([_ | _] = class_names) do
for class_name <- class_names do
case obfuscate_class(class_name) do
{:ok, short_name} -> {:ok, short_name}
{:error, class_name} -> {:error, class_name}
end
end
end
defp obfuscate(class_name), do: obfuscate([class_name])
def __verify_classes__(classes, env) do
for {:error, {class_name, _}} <- classes do
IO.warn("Invalid class name: #{class_name}", Macro.Env.stacktrace(env))
end
:ok
end
end
defmodule PhoenixAssetPipeline.Obfuscator do
@moduledoc false
@pattern ~r/(?:(?<prefix>(?:[a-z0-9\[\]&:\(\)\.\-]+:)+))?(?<class>(?!(?:\d+)$)(?:[a-zA-Z_][a-zA-Z0-9_-]*|\[[^\]]*\]))/iu
def obfuscate_class(class_name) when is_binary(class_name) do
if Regex.match?(~r/^#{@pattern.source}$/, class_name),
do: {:ok, {class_name, minify(class_name)}},
else: {:error, {class_name, nil}}
end
def obfuscate_class(class_name), do: {:error, {class_name, nil}}
def obfuscate_css(_) do
""
end
def obfuscate_js(_) do
""
end
defp minify(class_name), do: String.first(class_name)
end
@Fudoshiki
Copy link
Author

Fudoshiki commented Dec 20, 2024

Usage

Add elixirc_options: [warnings_as_errors: true],

defmodule PhoenixAssetPipelineExample.MixProject do
  use Mix.Project

  def project do
    [
      aliases: aliases(),
      app: :phoenix_asset_pipeline_example,
      deps: deps(),
      elixir: "~> 1.17",
      elixirc_options: [warnings_as_errors: true],
      elixirc_paths: elixirc_paths(Mix.env()),
      listeners: [Phoenix.CodeReloader],
      start_permanent: Mix.env() == :prod,
      version: "0.1.0"
    ]
  end
  ....
defmodule PhoenixAssetPipelineExampleWeb.Layouts do
  @moduledoc """
  This module holds different layouts used by your application.

  See the `layouts` directory for all templates available.
  The "root" layout is a skeleton rendered as part of the
  application router. The "app" layout is set as the default
  layout on both `use PhoenixAssetPipelineExampleWeb, :controller` and
  `use PhoenixAssetPipelineExampleWeb, :live_view`.
  """
  use PhoenixAssetPipelineExampleWeb, :html
  use PhoenixAssetPipeline.Helpers

  embed_templates "layouts/*"
end
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="csrf-token" content={get_csrf_token()} />
    <.live_title default="PhoenixAssetPipelineExample" suffix=" · Phoenix Framework">
      {assigns[:page_title]}
    </.live_title>
    <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
    <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
    </script>
  </head>
  <body class={class(["bg-white", "dark:bg-gray-900"])}>
    {@inner_content}
  </body>
</html>

@Fudoshiki
Copy link
Author

Fudoshiki commented Dec 20, 2024

Runtime (works, as expected)

  1. mix phx.server
  2. Write an incorrect class <body class={class(["bg-wh ite", "dark:bg-gray-900"])}> and it will raise an error in runtime (as expected).
  3. Fix the class back, the page will reload (as expected).

Mix compile (doesn’t work)

  1. mix phx.server is not running.
  2. Write an incorrect class <body class={class(["bg-wh ite", "dark:bg-gray-900"])}>.
  3. Start mix phx.server, and it will raise an error. (as expected)
  4. Fix the class back to <body class={class(["bg-white", "dark:bg-gray-900"])}>.
  5. Run mix phx.server, and only PhoenixAssetPipelineExampleWeb.Layout will be recompiled, and you will get an error:

Снимок экрана 2024-12-20 в 15 37 22

To avoid this, you need to force recompiling PhoenixAssetPipeline.Helpers.

Remove _build folder or add __mix_recompile?__, do: true to PhoenixAssetPipeline.Helpers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment