Skip to main content

lib/mix/tasks/astral.build.ex

defmodule Mix.Tasks.Astral.Build do
  @moduledoc """
  Build an Astral site into static files.

      mix astral.build
      mix astral.build --config astral.config.exs
      mix astral.build --root site --outdir dist

  ## Options

    * `--config` - path to an Astral config file (default: `astral.config.exs` when present)
    * `--root` - site root when no config file is used
    * `--outdir` - output directory override when no config file is used
    * `--pages` - pages directory override when no config file is used
    * `--layouts` - layouts directory override when no config file is used
    * `--public` - public directory override when no config file is used
    * `--assets` - assets directory override when no config file is used
  """

  @shortdoc "Build an Astral site"

  use Mix.Task

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

    {parsed, _argv, invalid} =
      OptionParser.parse(args,
        strict: [
          config: :string,
          root: :string,
          outdir: :string,
          pages: :string,
          layouts: :string,
          public: :string,
          assets: :string
        ]
      )

    reject_invalid!(invalid)

    parsed
    |> build_opts()
    |> Astral.build()
    |> report_result()
  end

  defp reject_invalid!([]), do: :ok

  defp reject_invalid!(invalid) do
    options = Enum.map_join(invalid, ", ", &invalid_option/1)
    Mix.raise("invalid option(s): #{options}")
  end

  defp invalid_option({flag, _value}), do: flag
  defp invalid_option(flag), do: to_string(flag)

  defp build_opts(parsed) do
    parsed
    |> maybe_default_config()
    |> Keyword.take([:config, :root, :outdir, :pages, :layouts, :public, :assets])
  end

  defp maybe_default_config(parsed) do
    if Keyword.has_key?(parsed, :config) or not File.regular?("astral.config.exs") do
      parsed
    else
      Keyword.put(parsed, :config, "astral.config.exs")
    end
  end

  defp report_result({:ok, result}) do
    page_count = length(result.site.pages)
    outdir = Path.relative_to_cwd(result.site.config.outdir)

    Mix.shell().info("[Astral] Built #{page_count} page(s) into #{outdir}")
    print_routes(result.site.pages ++ result.site.routes)
    print_assets(result)
  end

  defp report_result({:error, reason}) do
    Mix.raise("Astral build failed: #{inspect(reason)}")
  end

  defp print_routes([]), do: :ok

  defp print_routes(pages) do
    width =
      pages |> Enum.max_by(&String.length(route_path(&1))) |> then(&String.length(route_path(&1)))

    Mix.shell().info("\nRoutes:")

    Enum.each(pages, fn page ->
      route = String.pad_trailing(route_path(page), width)
      output = Path.relative_to_cwd(page.output_path)
      Mix.shell().info("  #{route}  #{output}")
    end)
  end

  defp route_path(%Astral.Page{} = page), do: page.route_path
  defp route_path(%Astral.Route{} = route), do: route.path

  defp print_assets(%{assets: nil}), do: :ok

  defp print_assets(%{site: site}) do
    manifest = Path.join(site.config.asset_outdir, "manifest.json")

    if File.regular?(manifest) do
      Mix.shell().info("\nAssets:")
      Mix.shell().info("  #{Path.relative_to_cwd(manifest)}")
    end
  end
end