Skip to main content

lib/npm/package/files.ex

defmodule NPM.Package.Files do
  @moduledoc """
  Analyzes which files would be included when publishing a package.

  Uses the `files` field from package.json, default inclusions,
  and .npmignore rules.
  """

  @always_included ~w(package.json README.md README LICENSE LICENCE COPYING CHANGELOG)
  @default_excluded ~w(.git .svn .hg node_modules .npm .DS_Store)

  @doc """
  Returns the files whitelist from package.json.
  """
  @spec whitelist(map()) :: [String.t()] | nil
  def whitelist(%{"files" => files}) when is_list(files), do: files
  def whitelist(_), do: nil

  @doc """
  Checks if a file is always included regardless of `files` field.
  """
  @spec always_included?(String.t()) :: boolean()
  def always_included?(filename) do
    basename = Path.basename(filename)
    upcased = String.upcase(basename)

    Enum.any?(@always_included, fn pattern ->
      upcased == String.upcase(pattern) or
        String.starts_with?(upcased, String.upcase(pattern) <> ".")
    end)
  end

  @doc """
  Checks if a file is always excluded.
  """
  @spec always_excluded?(String.t()) :: boolean()
  def always_excluded?(filename) do
    basename = Path.basename(filename)
    Enum.any?(@default_excluded, &(basename == &1))
  end

  @doc """
  Returns the main entry point.
  """
  @spec main_entry(map()) :: String.t()
  def main_entry(%{"main" => main}) when is_binary(main), do: main
  def main_entry(%{"module" => mod}) when is_binary(mod), do: mod
  def main_entry(_), do: "index.js"

  @doc """
  Lists all entry points (main, module, browser, types, exports).
  """
  @spec entry_points(map()) :: [String.t()]
  def entry_points(data) do
    fields = ~w(main module browser types typings)

    explicit =
      Enum.flat_map(fields, fn field ->
        case Map.get(data, field) do
          val when is_binary(val) -> [val]
          _ -> []
        end
      end)

    (explicit ++ exports_files(data))
    |> Enum.uniq()
    |> Enum.sort()
  end

  @doc """
  Checks if package.json has a files whitelist.
  """
  @spec has_whitelist?(map()) :: boolean()
  def has_whitelist?(data), do: whitelist(data) != nil

  @doc """
  Returns the list of always-included file patterns.
  """
  @spec default_includes :: [String.t()]
  def default_includes, do: @always_included

  defp exports_files(%{"exports" => exports}) when is_binary(exports), do: [exports]

  defp exports_files(%{"exports" => exports}) when is_map(exports) do
    exports
    |> flatten_exports()
    |> Enum.filter(&is_binary/1)
  end

  defp exports_files(_), do: []

  defp flatten_exports(map) when is_map(map),
    do: Enum.flat_map(map, fn {_, v} -> flatten_exports(v) end)

  defp flatten_exports(val), do: [val]
end