defmodule Mix.Tasks.Qs.Gen.AddTailwind do use Mix.Task @tailwind_config """ config :tailwind, version: "3.0.23", default: [ args: ~w( --config=tailwind.config.js --input=css/app.css --output=../priv/static/assets/app.css ), cd: Path.expand("../assets", __DIR__) ] """ @tailwind_dep "\t\t\t{:tailwind, \"~> 0.1\", runtime: Mix.env() == :dev}," @tailwind_watcher "\t\ttailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}" @impl Mix.Task def run(_args) do add_into( "mix.exs", fn {:defp, _, [{:deps, _, nil}, _]} -> true _ -> false end, @tailwind_dep ) add_after( "config/config.exs", fn {:config, _, [:esbuild, _]} -> true _ -> false end, @tailwind_config ) add_into( "config/dev.exs", fn node -> Keyword.keyword?(node) and Keyword.has_key?(node, :watchers) end, @tailwind_watcher ) end defp add_into(file_name, is_match, addition), do: add_to_file(file_name, is_match, 0, addition) defp add_after(file_name, is_match, addition), do: add_to_file(file_name, is_match, 2, addition) defp add_to_file(file_name, is_match, line_offset, addition) do contents = File.read!(file_name) ast = Code.string_to_quoted!(contents) {_start, max_line} = find_span(ast, is_match) if not is_nil(max_line) do Mix.shell().info("Patching #{file_name}") contents = contents |> String.split("\n") |> List.insert_at(max_line + line_offset, addition) |> Enum.join("\n") File.write!(file_name, contents) end end defp find_span_end(node) do {_, max_line} = Macro.postwalk(node, 0, fn item, acc -> case item do {_, [line: line], _} -> {item, max(acc, line)} node -> {node, acc} end end) max_line end defp find_span(ast, is_match) do {_, span} = Macro.postwalk(ast, nil, fn node, acc -> if is_match.(node) do case node do {_, [line: line], _} -> {node, {line, find_span_end(node)}} _ -> {node, {0, find_span_end(node)}} end else {node, acc} end end) span end end