lib/phoenix_copy.ex

defmodule Phoenix.Copy do
  @moduledoc """
  Copy static assets for your Phoenix app during development and deployment.

  This project was created to manage static asset files. Paired with other standalone build tools
  like `esbuild` and `tailwind`, this can reduce your dependence on JavaScript-based build tools
  like Webpack.

  ## Configuration

  Before using the project, you must configure one or more profiles. Each profile defines a source
  and destination for the copy. In `config/config.exs`, add:

      config :phoenix_copy,
        default: [
          source: Path.expand("source/", __DIR__),
          destination: Path.expand("destination/", __DIR__)
        ]

  This defines a profile `:default`. You can define as many profiles as you wish. By using
  `Path.expand/2`, you may define the source and destination as paths relative to the configuration
  file.

  ## Direct Usage

  To copy files once, use `run/1` with the name of the configured profile:

      run(:default)

  To watch for changes and continually copy files, use `watch/1`:

      watch(:default)

  Note that `watch/1` will block execution.
  """

  @doc """
  Returns the configuration of the given `profile`.
  """
  @spec config_for!(atom) :: Keyword.t()
  def config_for!(profile) when is_atom(profile) do
    Application.get_env(:phoenix_copy, profile) ||
      raise ArgumentError, """
      unknown copy profile. Make sure the profile is defined in your config/config.exs file, such as:

          config :phoenix_copy,
            #{profile}: [
              source: Path.expand("../assets/static", __DIR__),
              destination: Path.expand("../priv/static", __DIR__)
            ]
      """
  end

  @doc """
  Copies files from the configured source and destination for the given `profile`.

  Returns a list of copied files.
  """
  @spec run(atom) :: [binary]
  def run(profile \\ :default)

  def run(profile) when is_atom(profile) do
    config = config_for!(profile)

    source = Keyword.fetch!(config, :source)
    destination = Keyword.fetch!(config, :destination)

    File.cp_r!(source, destination)
  end

  @doc """
  Watch for changes in the configured `source` and copy files to the `destination`.

  Also performs an initial copy of the files immediately.
  """
  @spec watch(atom) :: term
  def watch(profile \\ :default)

  def watch(profile) when is_atom(profile) do
    run(profile)
    config = config_for!(profile)

    source = Keyword.fetch!(config, :source)
    destination = Keyword.fetch!(config, :destination)

    Task.async(Phoenix.Copy.Watcher, :start_link, [[source, destination]])
    |> Task.await(:infinity)
  end
end