Skip to main content

lib/npm/type_field.ex

defmodule NPM.TypeField do
  @moduledoc """
  Analyzes the `type` field from package.json.

  The `type` field determines whether `.js` files are treated as
  ES modules (`"module"`) or CommonJS (`"commonjs"`, the default).
  """

  @doc """
  Returns the type field value (default: "commonjs").
  """
  @spec get(map()) :: String.t()
  def get(%{"type" => "module"}), do: "module"
  def get(%{"type" => "commonjs"}), do: "commonjs"
  def get(_), do: "commonjs"

  @doc """
  Checks if the package uses ES modules.
  """
  @spec esm?(map()) :: boolean()
  def esm?(data), do: get(data) == "module"

  @doc """
  Checks if the package uses CommonJS.
  """
  @spec cjs?(map()) :: boolean()
  def cjs?(data), do: get(data) == "commonjs"

  @doc """
  Determines the module system for a given file path.
  """
  @spec module_type(String.t(), map()) :: :esm | :cjs
  def module_type(filepath, data) do
    ext = Path.extname(filepath)

    case ext do
      ".mjs" -> :esm
      ".cjs" -> :cjs
      ".mts" -> :esm
      ".cts" -> :cjs
      _ -> if esm?(data), do: :esm, else: :cjs
    end
  end

  @doc """
  Returns the counts of ESM vs CJS packages.
  """
  @spec stats([map()]) :: map()
  def stats(packages) do
    esm_count = Enum.count(packages, &esm?/1)

    %{
      total: length(packages),
      esm: esm_count,
      cjs: length(packages) - esm_count,
      esm_pct:
        if(packages != [], do: Float.round(esm_count / length(packages) * 100, 1), else: 0.0)
    }
  end

  @doc """
  Checks if a package is a dual CJS/ESM package.
  """
  @spec dual?(map()) :: boolean()
  def dual?(data) do
    has_exports = is_map(data["exports"])
    has_main = is_binary(data["main"])
    has_module = is_binary(data["module"])
    (has_exports and has_main) or (has_main and has_module)
  end
end