defmodule NPM.Ignore do
@moduledoc """
Parses .npmignore and .gitignore files for package publishing.
Determines which files to include/exclude when packing or publishing
a package, following npm's ignore rules.
"""
@always_ignored ~w(.git .svn CVS .hg .DS_Store node_modules .npmrc)
@never_ignored ~w(package.json README.md LICENSE LICENCE CHANGELOG.md)
@doc """
Reads and parses an ignore file.
"""
@spec read(String.t()) :: [String.t()]
def read(path) do
case File.read(path) do
{:ok, content} -> parse(content)
_ -> []
end
end
@doc """
Parses ignore file content into a list of patterns.
"""
@spec parse(String.t()) :: [String.t()]
def parse(content) do
content
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.reject(&comment_or_blank?(&1))
|> Enum.uniq()
end
@doc """
Checks if a file path is ignored by the given patterns.
"""
@spec ignored?(String.t(), [String.t()]) :: boolean()
def ignored?(path, patterns) do
basename = Path.basename(path)
cond do
always_ignored?(basename) -> true
never_ignored?(basename) -> false
true -> Enum.any?(patterns, &pattern_matches?(path, &1))
end
end
@doc """
Returns the list of always-ignored paths.
"""
@spec always_ignored :: [String.t()]
def always_ignored, do: @always_ignored
@doc """
Returns the list of never-ignored paths.
"""
@spec never_ignored :: [String.t()]
def never_ignored, do: @never_ignored
@doc """
Gets the effective ignore patterns for a package directory.
Checks .npmignore first, falls back to .gitignore.
"""
@spec effective_patterns(String.t()) :: [String.t()]
def effective_patterns(package_dir) do
npmignore = Path.join(package_dir, ".npmignore")
gitignore = Path.join(package_dir, ".gitignore")
case read(npmignore) do
[] -> read(gitignore)
patterns -> patterns
end
end
defp always_ignored?(name), do: name in @always_ignored
defp never_ignored?(name) do
downcased = String.downcase(name)
Enum.any?(@never_ignored, &(String.downcase(&1) == downcased))
end
defp comment_or_blank?(line) do
line == "" or String.starts_with?(line, "#")
end
defp pattern_matches?(path, pattern) do
cond do
String.ends_with?(pattern, "/") ->
dir_pattern = String.trim_trailing(pattern, "/")
String.starts_with?(path, dir_pattern <> "/") or path == dir_pattern
String.contains?(pattern, "/") ->
path == pattern or String.starts_with?(path, pattern <> "/")
true ->
Path.basename(path) == pattern or
path == pattern or
String.ends_with?(path, "/" <> pattern)
end
end
end