Skip to main content

lib/quickbeam/js/exports.ex

defmodule QuickBEAM.JS.Exports do
  @moduledoc false

  @type export_map :: String.t() | [export_map()] | %{String.t() => export_map()} | nil

  @spec parse(map()) :: %{String.t() => String.t() | map()} | nil
  def parse(%{"exports" => exports}) when is_binary(exports), do: %{"." => exports}

  def parse(%{"exports" => exports}) when is_map(exports) do
    if subpath_exports?(exports), do: exports, else: %{"." => exports}
  end

  def parse(_), do: nil

  @spec resolve(map(), String.t(), [String.t()]) :: {:ok, String.t()} | :error
  def resolve(export_map, subpath, conditions \\ ["default"]) do
    export_map
    |> candidates(subpath, conditions)
    |> Enum.find_value(:error, &{:ok, &1})
  end

  defp subpath_exports?(map) do
    Enum.any?(Map.keys(map), &String.starts_with?(&1, "."))
  end

  defp candidates(export_map, subpath, conditions) when is_map(export_map) do
    exact = export_map |> Map.get(subpath) |> target_candidates(conditions)

    wildcard =
      Enum.flat_map(export_map, fn {pattern, target} ->
        case wildcard_replacement(pattern, subpath) do
          nil -> []
          replacement -> replace_wildcards(target_candidates(target, conditions), replacement)
        end
      end)

    exact ++ wildcard
  end

  defp candidates(_, _, _), do: []

  defp target_candidates(nil, _conditions), do: []
  defp target_candidates(path, _conditions) when is_binary(path), do: [path]

  defp target_candidates(list, conditions) when is_list(list) do
    Enum.flat_map(list, &target_candidates(&1, conditions))
  end

  defp target_candidates(target, conditions) when is_map(target) do
    Enum.flat_map(conditions, fn condition ->
      target |> Map.get(condition) |> target_candidates(conditions)
    end)
  end

  defp target_candidates(_, _), do: []

  defp replace_wildcards(paths, replacement) do
    Enum.map(paths, &String.replace(&1, "*", replacement))
  end

  defp wildcard_replacement(pattern, subpath) do
    case String.split(pattern, "*", parts: 2) do
      [prefix, suffix] ->
        if String.starts_with?(subpath, prefix) and String.ends_with?(subpath, suffix) do
          subpath
          |> String.trim_leading(prefix)
          |> trim_suffix(suffix)
        end

      _ ->
        nil
    end
  end

  defp trim_suffix(value, ""), do: value
  defp trim_suffix(value, suffix), do: String.trim_trailing(value, suffix)
end