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-.css` / `app-.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