defmodule NPM.Diagnostics do
alias NPM.Config.Npmrc
@moduledoc """
Runs diagnostic checks on a project for common npm issues.
"""
@doc """
Runs all diagnostic checks.
"""
@spec run(String.t()) :: [map()]
def run(project_dir) do
[]
|> check_package_json(project_dir)
|> check_lockfile(project_dir)
|> check_node_modules(project_dir)
|> check_npmrc(project_dir)
|> Enum.reverse()
end
@doc """
Formats diagnostic results.
"""
@spec format([map()]) :: String.t()
def format([]), do: "All checks passed."
def format(issues) do
issues
|> Enum.map_join("\n", fn issue ->
icon = if issue.level == :error, do: "✗", else: "!"
"#{icon} [#{issue.check}] #{issue.message}"
end)
end
@doc """
Counts issues by level.
"""
@spec counts([map()]) :: map()
def counts(issues) do
%{
errors: Enum.count(issues, &(&1.level == :error)),
warnings: Enum.count(issues, &(&1.level == :warning)),
total: length(issues)
}
end
defp check_package_json(issues, dir) do
path = Path.join(dir, "package.json")
if File.exists?(path) do
issues
else
[%{level: :error, check: "package.json", message: "package.json not found"} | issues]
end
end
defp check_lockfile(issues, dir) do
lock_path = Path.join(dir, "npm.lock")
pkg_lock_path = Path.join(dir, "package-lock.json")
cond do
File.exists?(lock_path) -> issues
File.exists?(pkg_lock_path) -> issues
true -> [%{level: :warning, check: "lockfile", message: "No lockfile found"} | issues]
end
end
defp check_node_modules(issues, dir) do
nm_path = Path.join(dir, "node_modules")
if File.dir?(nm_path) do
issues
else
[
%{
level: :warning,
check: "node_modules",
message: "node_modules not found — run npm install"
}
| issues
]
end
end
defp check_npmrc(issues, dir) do
npmrc_path = Path.join(dir, ".npmrc")
case File.exists?(npmrc_path) && Npmrc.read(npmrc_path) do
{:ok, config} -> check_npmrc_auth(issues, config)
_ -> issues
end
end
defp check_npmrc_auth(issues, config) do
if Npmrc.has_auth?(config) do
[
%{
level: :warning,
check: "npmrc",
message: ".npmrc contains auth tokens — ensure it's gitignored"
}
| issues
]
else
issues
end
end
end