Skip to main content

lib/npm/package/manifest.ex

defmodule NPM.Package.Manifest do
  alias NPM.Resolution.Exports

  @moduledoc """
  Generate a complete package manifest from `package.json`.

  Aggregates data from the package.json file into a structured
  manifest used by publishing, auditing, and analysis tools.
  """

  @type t :: %{
          name: String.t() | nil,
          version: String.t() | nil,
          license: String.t() | nil,
          module_type: :esm | :cjs,
          dependencies: %{String.t() => String.t()},
          dev_dependencies: %{String.t() => String.t()},
          optional_dependencies: %{String.t() => String.t()},
          scripts: %{String.t() => String.t()},
          engines: %{String.t() => String.t()},
          exports: map() | nil,
          files: [String.t()] | nil
        }

  @doc """
  Build a manifest from a `package.json` file.
  """
  @spec from_file(String.t()) :: {:ok, t()} | {:error, term()}
  def from_file(path \\ "package.json") do
    case File.read(path) do
      {:ok, content} -> {:ok, from_json(content)}
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Build a manifest from a JSON string.
  """
  @spec from_json(String.t()) :: t()
  def from_json(json) do
    data = NPM.JSON.decode!(json)

    %{
      name: Map.get(data, "name"),
      version: Map.get(data, "version"),
      license: Map.get(data, "license"),
      module_type: Exports.module_type(data),
      dependencies: Map.get(data, "dependencies", %{}),
      dev_dependencies: Map.get(data, "devDependencies", %{}),
      optional_dependencies: Map.get(data, "optionalDependencies", %{}),
      scripts: Map.get(data, "scripts", %{}),
      engines: Map.get(data, "engines", %{}),
      exports: Exports.parse(data),
      files: Map.get(data, "files")
    }
  end

  @doc """
  Count total dependency count across all types.
  """
  @spec dep_count(t()) :: non_neg_integer()
  def dep_count(manifest) do
    map_size(manifest.dependencies) +
      map_size(manifest.dev_dependencies) +
      map_size(manifest.optional_dependencies)
  end

  @doc """
  Check if the manifest has any scripts defined.
  """
  @spec has_scripts?(t()) :: boolean()
  def has_scripts?(manifest), do: map_size(manifest.scripts) > 0

  @doc """
  Get all dependency names across all types.
  """
  @spec all_dep_names(t()) :: [String.t()]
  def all_dep_names(manifest) do
    [manifest.dependencies, manifest.dev_dependencies, manifest.optional_dependencies]
    |> Enum.flat_map(&Map.keys/1)
    |> Enum.uniq()
    |> Enum.sort()
  end
end