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