defmodule Mix.Tasks.Compile.PhoenixKitCssSources do @moduledoc """ Mix compiler that auto-generates CSS @source directives for PhoenixKit modules. Discovers external modules via `PhoenixKit.ModuleDiscovery`, calls `css_sources/0` on each, resolves their paths, and writes `assets/css/_phoenix_kit_sources.css`. The parent app's `app.css` imports this file. Zero-config: add a module with `css_sources/0` and it gets picked up automatically. ## Setup Add to your `mix.exs` compilers list: compilers: [:phoenix_kit_css_sources, :phoenix_live_view] ++ Mix.compilers() And import the generated file in `assets/css/app.css`: @import "./_phoenix_kit_sources.css"; """ use Mix.Task.Compiler require Logger @generated_file "assets/css/_phoenix_kit_sources.css" @impl true def run(_args) do deps = Mix.Project.config()[:deps] || [] # Ensure all dep applications are loaded for module discovery for dep <- Mix.Dep.cached() do Application.ensure_loaded(dep.app) end source_lines = PhoenixKit.ModuleDiscovery.discover_external_modules() |> Enum.flat_map(fn mod -> if Code.ensure_loaded?(mod) and function_exported?(mod, :css_sources, 0) do mod.css_sources() else [] end end) |> Enum.uniq() |> Enum.sort() |> Enum.map(fn app_name -> app_name = to_string(app_name) case find_dep_path(String.to_atom(app_name), deps) do {:path, path} -> "@source \"../../#{path}\";" :hex -> "@source \"../../deps/#{app_name}\";" :not_found -> "@source \"../../deps/#{app_name}\";" end end) content = """ /* Auto-generated by PhoenixKit — do not edit manually. Regenerated on each compilation from css_sources/0 callbacks. */ #{Enum.join(source_lines, "\n")} """ path = Path.join(File.cwd!(), @generated_file) existing = case File.read(path) do {:ok, data} -> data _ -> nil end if existing != content do File.mkdir_p!(Path.dirname(path)) File.write!(path, content) Mix.shell().info( "[PhoenixKit] Updated #{@generated_file} with #{length(source_lines)} source(s)" ) end {:ok, []} end defp find_dep_path(app_name, deps) do dep = Enum.find(deps, fn {name, opts} when is_list(opts) -> name == app_name {name, _version} -> name == app_name {name, _version, opts} when is_list(opts) -> name == app_name _ -> false end) case dep do {_, opts} when is_list(opts) -> if path = Keyword.get(opts, :path), do: {:path, path}, else: :hex {_, _version, opts} when is_list(opts) -> if path = Keyword.get(opts, :path), do: {:path, path}, else: :hex {_, _version} -> :hex nil -> :not_found end end end