Skip to main content

lib/npm/gitignore.ex

defmodule NPM.Gitignore do
  @moduledoc """
  Manages .gitignore entries for npm projects.
  """

  @essential_patterns ["node_modules/", ".npm/"]
  @recommended_patterns ["node_modules/", ".npm/", "*.tgz", ".env", ".env.local"]

  @doc """
  Returns essential gitignore patterns for npm projects.
  """
  @spec essential :: [String.t()]
  def essential, do: @essential_patterns

  @doc """
  Returns recommended gitignore patterns.
  """
  @spec recommended :: [String.t()]
  def recommended, do: @recommended_patterns

  @doc """
  Checks if a .gitignore file covers node_modules.
  """
  @spec covers_node_modules?(String.t()) :: boolean()
  def covers_node_modules?(content) do
    content
    |> String.split("\n")
    |> Enum.any?(fn line ->
      trimmed = String.trim(line)
      trimmed == "node_modules" or trimmed == "node_modules/"
    end)
  end

  @doc """
  Returns missing essential patterns from a .gitignore file.
  """
  @spec missing(String.t()) :: [String.t()]
  def missing(content) do
    lines = content |> String.split("\n") |> Enum.map(&String.trim/1) |> MapSet.new()

    Enum.reject(@essential_patterns, fn pattern ->
      bare = String.trim_trailing(pattern, "/")
      MapSet.member?(lines, pattern) or MapSet.member?(lines, bare)
    end)
  end

  @doc """
  Generates a .gitignore content for an npm project.
  """
  @spec generate(keyword()) :: String.t()
  def generate(opts \\ []) do
    patterns =
      if Keyword.get(opts, :recommended, true),
        do: @recommended_patterns,
        else: @essential_patterns

    extra = Keyword.get(opts, :extra, [])
    (patterns ++ extra) |> Enum.join("\n")
  end

  @doc """
  Checks a .gitignore file on disk.
  """
  @spec check(String.t()) :: {:ok, [String.t()]} | {:error, :not_found}
  def check(project_dir) do
    path = Path.join(project_dir, ".gitignore")

    case File.read(path) do
      {:ok, content} -> {:ok, missing(content)}
      _ -> {:error, :not_found}
    end
  end
end