defmodule NPM.Cache do alias NPM.Security.RegistryPolicy @moduledoc """ Global package cache. Downloaded packages are stored in `~/.npm_ex/cache///` and reused across projects. The cache is populated on first install and checked before downloading from the registry. """ @doc "Root directory of the global cache." @spec dir :: String.t() def dir do NPM.Config.cache_dir() end @doc "Path to a specific package version in the cache." @spec package_dir(String.t(), String.t()) :: String.t() def package_dir(name, version) do Path.join([dir(), "cache", name, version]) end @doc "Check if a package version is already cached." @spec cached?(String.t(), String.t()) :: boolean() def cached?(name, version) do File.exists?(Path.join(package_dir(name, version), "package.json")) end @doc """ Ensure a package version is in the cache. Downloads and extracts the tarball if not already cached. Returns `{:ok, cache_path}`, `{:ok, :missing_optional}` when an optional package fails to fetch, or `{:error, reason}`. """ @spec ensure(String.t(), String.t(), String.t(), String.t(), keyword()) :: {:ok, String.t()} | {:ok, :missing_optional} | {:error, term()} def ensure(name, version, tarball_url, integrity, opts \\ []) do dest = package_dir(name, version) if cached?(name, version) do {:ok, dest} else RegistryPolicy.validate_url!(tarball_url) case NPM.Tarball.fetch_and_extract(tarball_url, integrity, dest) do {:ok, _count} -> {:ok, dest} {:error, reason} -> handle_fetch_error(reason, dest, opts) end end end defp handle_fetch_error(reason, dest, opts) do if Keyword.get(opts, :optional?, false) do File.rm_rf(dest) {:ok, :missing_optional} else {:error, reason} end end end