Skip to main content

lib/mix/tasks/duskmoon_bundler.dev.ex

defmodule Mix.Tasks.DuskmoonBundler.Dev do
  @shortdoc "Start the DuskmoonBundler dev watcher with HMR"
  @moduledoc """
  Start the DuskmoonBundler file watcher for development.

  Reads configuration from `config :duskmoon_bundler` and `config :duskmoon_bundler, :server`.
  Pass a profile name as the first argument to use a named profile.
  CLI flags override config values.

      mix duskmoon_bundler.dev
      mix duskmoon_bundler.dev my_app_web

  ## Options

    * `--root` — asset source directory (default from config or `"assets"`)
    * `--watch-dir` — additional directory to watch (repeatable)
    * `--tailwind` — enable Tailwind CSS rebuilds
    * `--tailwind-css` — custom Tailwind input CSS file
    * `--tailwind-outdir` — directory to write rebuilt CSS (default: `"priv/static/assets/css"`)
    * `--target` — JS target (default: `es2020`)
  """
  use Mix.Task

  alias DuskmoonBundler.Config
  alias DuskmoonBundler.Paths

  @impl true
  def run(args) do
    Mix.Task.run("app.start")

    {parsed, argv, _invalid} =
      OptionParser.parse(args,
        strict: [
          root: :string,
          watch_dir: [:string, :keep],
          tailwind: :boolean,
          tailwind_css: :string,
          tailwind_outdir: :string,
          target: :string
        ]
      )

    profile = parse_profile(argv)
    config = Config.build(profile)
    server_config = Config.server(profile)
    tailwind_config = Config.tailwind(profile)

    root = Keyword.get(parsed, :root) || to_string(config.root)
    target = Keyword.get(parsed, :target) || to_string(config.target)

    tailwind? =
      Keyword.get(parsed, :tailwind) ||
        (tailwind_config != [] and Keyword.get(parsed, :tailwind, true))

    cli_watch_dirs = Keyword.get_values(parsed, :watch_dir)

    watch_dirs =
      case cli_watch_dirs do
        [] -> server_config.watch_dirs
        list -> list
      end

    watch_dirs = if tailwind? and watch_dirs == [], do: [Paths.lib()], else: watch_dirs

    tailwind_css = Keyword.get(parsed, :tailwind_css) || tailwind_config[:css]
    tailwind_outdir = Keyword.get(parsed, :tailwind_outdir, Path.join(config.outdir, "css"))

    opts = [
      root: root,
      watch_dirs: watch_dirs,
      tailwind: tailwind?,
      tailwind_css: tailwind_css,
      tailwind_outdir: tailwind_outdir,
      target: target,
      name: watcher_name(profile)
    ]

    {:ok, _pid} = DuskmoonBundler.Watcher.start_link(opts)

    Mix.shell().info("[DuskmoonBundler] Watching #{opts[:root]}...")

    if tailwind? do
      Mix.shell().info(
        "[DuskmoonBundler] Tailwind CSS enabled (watching #{Enum.join(watch_dirs, ", ")})"
      )
    end

    unless iex_running?() do
      Process.sleep(:infinity)
    end
  end

  defp parse_profile(args), do: DuskmoonBundler.Config.Profile.from_args(args)

  defp watcher_name(nil), do: DuskmoonBundler.Watcher

  defp watcher_name(profile) do
    Module.concat(DuskmoonBundler.Watcher, profile |> Atom.to_string() |> Macro.camelize())
  end

  @dialyzer {:nowarn_function, iex_running?: 0}
  defp iex_running? do
    Code.ensure_loaded?(IEx) and IEx.started?()
  end
end