Skip to main content

lib/mix/tasks/attached_dashboard.build_assets.ex

defmodule Mix.Tasks.AttachedDashboard.BuildAssets do
  @shortdoc "Builds CSS and JS assets for AttachedDashboard"

  @moduledoc """
  Builds and hashes the CSS and JS assets for the AttachedDashboard.

      mix attached_dashboard.build_assets

  Steps:
  1. Runs Tailwind CSS (v4) to compile `assets/css/app.css`.
  2. Runs esbuild to bundle `assets/js/app.js`.
  3. MD5-hashes the output files and renames them to `app-<md5>.css` / `app-<md5>.js`.
  4. Writes `priv/static/manifest.json`.
  """

  use Mix.Task

  @priv_static "priv/static"
  @assets_static "assets/static"

  @impl Mix.Task
  def run(_args) do
    static_dir = Path.expand(@priv_static, app_dir())

    File.rm_rf!(static_dir)
    File.mkdir_p!(Path.join(static_dir, "css"))
    File.mkdir_p!(Path.join(static_dir, "js"))

    run_tailwind()
    run_esbuild()

    {css_hash, js_hash} = hash_and_rename(static_dir)
    static_hashes = copy_static(static_dir)
    write_manifest(static_dir, css_hash, js_hash, static_hashes)

    Mix.shell().info("AttachedDashboard assets built: css=#{css_hash} js=#{js_hash}")
  end

  defp app_dir, do: File.cwd!()

  defp run_tailwind do
    Mix.shell().info("Building CSS...")
    apply(Tailwind, :run, [:attached_dashboard, ~w(--minify)])
  end

  defp run_esbuild do
    Mix.shell().info("Building JS...")
    apply(Esbuild, :run, [:attached_dashboard, ~w(--minify)])
  end

  defp hash_and_rename(static_dir) do
    css_path = Path.join([static_dir, "css", "app.css"])
    js_path = Path.join([static_dir, "js", "app.js"])

    css_hash = md5_file(css_path)
    js_hash = md5_file(js_path)

    File.rename!(css_path, Path.join([static_dir, "css", "app-#{css_hash}.css"]))
    File.rename!(js_path, Path.join([static_dir, "js", "app-#{js_hash}.js"]))

    {css_hash, js_hash}
  end

  defp md5_file(path), do: path |> File.read!() |> md5()
  defp md5(content), do: content |> then(&:crypto.hash(:md5, &1)) |> Base.encode16(case: :lower)

  defp copy_static(static_dir) do
    assets_static_dir = Path.expand(@assets_static, app_dir())

    for filename <- File.ls!(assets_static_dir), into: %{} do
      src = Path.join(assets_static_dir, filename)
      content = File.read!(src)
      hash = md5(content)
      ext = Path.extname(filename)
      base = Path.basename(filename, ext)
      File.cp!(src, Path.join(static_dir, "#{base}-#{hash}#{ext}"))
      {filename, hash}
    end
  end

  defp write_manifest(static_dir, css_hash, js_hash, static_hashes) do
    built = %{
      "css/app.css" => "css/app-#{css_hash}.css",
      "js/app.js" => "js/app-#{js_hash}.js"
    }

    static =
      Map.new(static_hashes, fn {filename, hash} ->
        ext = Path.extname(filename)
        base = Path.basename(filename, ext)
        {filename, "#{base}-#{hash}#{ext}"}
      end)

    path = Path.join(static_dir, "manifest.json")
    File.write!(path, Jason.encode!(Map.merge(built, static), pretty: true))
  end
end