defmodule NPM.Install.Rebuild do
@moduledoc """
Detects packages needing native rebuilds.
Identifies packages with install scripts, binding.gyp files,
or other indicators that they contain native addons.
"""
@native_indicators ["binding.gyp", "CMakeLists.txt", "Makefile"]
@doc """
Scans node_modules for packages with native addons.
"""
@spec scan(String.t()) :: [map()]
def scan(node_modules_dir \\ "node_modules") do
case File.ls(node_modules_dir) do
{:ok, entries} ->
entries
|> Enum.reject(&String.starts_with?(&1, "."))
|> Enum.flat_map(&scan_entry(node_modules_dir, &1))
|> Enum.sort_by(& &1.name)
_ ->
[]
end
end
@doc """
Checks if a specific package has native components.
"""
@spec native?(String.t(), String.t()) :: boolean()
def native?(package_dir, name \\ "") do
has_native_files?(package_dir) or has_install_script?(package_dir, name)
end
@doc """
Returns the list of packages that need rebuild after Node.js upgrade.
"""
@spec needs_rebuild(String.t()) :: [String.t()]
def needs_rebuild(node_modules_dir \\ "node_modules") do
scan(node_modules_dir)
|> Enum.map(& &1.name)
end
@doc """
Formats scan results for display.
"""
@spec format_results([map()]) :: String.t()
def format_results([]), do: "No native addons found."
def format_results(packages) do
header = "Native addons (#{length(packages)}):\n"
body = Enum.map_join(packages, "\n", &" #{&1.name} — #{&1.reason}")
header <> body
end
defp scan_entry(nm_dir, entry) do
if String.starts_with?(entry, "@") do
scan_scoped(nm_dir, entry)
else
check_native(nm_dir, entry, entry)
end
end
defp scan_scoped(nm_dir, scope) do
scope_dir = Path.join(nm_dir, scope)
case File.ls(scope_dir) do
{:ok, subs} -> Enum.flat_map(subs, &check_native(scope_dir, &1, "#{scope}/#{&1}"))
_ -> []
end
end
defp check_native(parent, entry, full_name) do
pkg_dir = Path.join(parent, entry)
cond do
has_native_files?(pkg_dir) ->
[%{name: full_name, reason: "has native build files"}]
has_install_script?(pkg_dir, full_name) ->
[%{name: full_name, reason: "has install script"}]
true ->
[]
end
end
defp has_native_files?(pkg_dir) do
Enum.any?(@native_indicators, &File.exists?(Path.join(pkg_dir, &1)))
end
defp has_install_script?(pkg_dir, _name) do
pkg_json = Path.join(pkg_dir, "package.json")
case File.read(pkg_json) do
{:ok, content} ->
data = NPM.JSON.decode!(content)
scripts = data["scripts"] || %{}
Map.has_key?(scripts, "install") or Map.has_key?(scripts, "postinstall")
_ ->
false
end
rescue
_ -> false
end
end