defmodule Mix.Tasks.Npm.Doctor do alias NPM.Install.Lifecycle @shortdoc "Diagnose npm installation issues" @moduledoc """ Run diagnostics on the npm installation. mix npm.doctor Checks: - Package.json validity - Lockfile freshness - node_modules completeness - Platform compatibility - Lifecycle scripts - Deprecated packages """ use Mix.Task @impl true def run([]) do Application.ensure_all_started(:req) checks = [ {"package.json", check_package_json()}, {"package-lock.json", check_lockfile()}, {"node_modules", check_node_modules()}, {"lifecycle scripts", check_lifecycle()}, {"platform compat", check_platform()} ] Enum.each(checks, fn {label, result} -> icon = if result == :ok, do: "✓", else: "✗" msg = if result == :ok, do: "ok", else: elem(result, 1) Mix.shell().info(" #{icon} #{label}: #{msg}") end) end def run(_) do Mix.shell().error("Usage: mix npm.doctor") end defp check_package_json do if File.exists?("package.json"), do: :ok, else: {:warn, "not found"} end defp check_lockfile do cond do File.exists?(NPM.Lockfile.default_path()) -> :ok File.exists?(NPM.Lockfile.legacy_path()) -> {:warn, "legacy npm.lock — run mix npm.install"} true -> {:warn, "not found — run mix npm.install"} end end defp check_node_modules do if File.exists?("node_modules"), do: :ok, else: {:warn, "not found — run mix npm.install"} end defp check_lifecycle do if File.exists?("node_modules") do scripts = Lifecycle.detect_all("node_modules") if map_size(scripts) > 0 do names = Map.keys(scripts) |> Enum.join(", ") {:warn, "#{map_size(scripts)} packages have install scripts: #{names}"} else :ok end else :ok end end defp check_platform do Mix.shell().info(" OS: #{NPM.Platform.current_os()}, CPU: #{NPM.Platform.current_cpu()}") :ok end end