Source code for meta_package_manager.managers.pnpm

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

from __future__ import annotations

import json

from extra_platforms import ALL_PLATFORMS

from ..capabilities import search_capabilities, version_not_implemented
from ..manager import PackageManager

TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Iterator

    from ..package import Package


[docs] class PNPM(PackageManager): """See command equivalences at: https://github.com/antfu-collective/ni?tab=readme-ov-file#ni. .. note:: All operations target the global scope via ``--global``, like the :py:class:`meta_package_manager.managers.npm.NPM` manager. """ name = "Node pnpm" homepage_url = "https://pnpm.io" platforms = ALL_PLATFORMS requirement = ">=11.0.0" """`11.0.0 <https://github.com/pnpm/pnpm/releases/tag/v11.0.0>`_ is the first version to ship the ``search`` subcommand. It also clears the ``10.16.0`` floor of ``minimumReleaseAge``, the release-age gate mpm drives for the supply-chain cooldown (see :py:attr:`cooldown_env_var`), so a single floor covers every advertised operation. Older pnpm releases either lack ``search`` or silently ignore the cooldown setting. """ cooldown_env_var = "pnpm_config_minimum_release_age" """pnpm honors a release-age cooldown through its ``minimumReleaseAge`` setting. pnpm reads any setting from an environment variable built by snake-casing the setting name behind a ``pnpm_config_`` prefix (the docs render ``pmOnFail`` as ``pnpm_config_pm_on_fail``), so ``pnpm_config_minimum_release_age`` sets ``minimumReleaseAge`` without touching ``pnpm-workspace.yaml``. Once set, pnpm refuses to install any version published more recently than the configured age, across direct and transitive dependencies. ``minimumReleaseAge`` is expressed in minutes, so :py:meth:`cooldown_env_value` is overridden to emit a minute count. See https://pnpm.io/settings#minimumreleaseage. """
[docs] def cooldown_env_value(self) -> str: """Render :py:attr:`meta_package_manager.execution.CLIExecutor.cooldown` as an integer minute count for pnpm's ``minimumReleaseAge``. Sub-minute cooldowns round up so the gate over-protects rather than silently collapsing to ``0`` (the "no cooldown" sentinel). """ return self.cooldown_rounded_up(60)
@property def installed(self) -> Iterator[Package]: """Fetch installed packages. ``pnpm list --json`` returns an array of project objects; the global scope resolves to a single one whose ``dependencies`` map holds the installed packages. .. code-block:: shell-session $ pnpm list --global --json --depth 0 [ { "name": "global", "dependencies": { "eslint": { "from": "eslint", "version": "9.15.0" }, "typescript": { "from": "typescript", "version": "5.6.3" } } } ] """ output = self.run_cli( "list", "--global", "--json", "--depth", "0", must_succeed=True ) if output: for project in json.loads(output): for pkg_id, pkg_infos in project.get("dependencies", {}).items(): yield self.package( id=pkg_id, installed_version=pkg_infos["version"], ) @property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. ``pnpm outdated`` exits with code ``1`` when it finds outdated packages, but writes the report to ``<stdout>`` and leaves ``<stderr>`` empty. Passing ``must_succeed`` keeps the lenient failure gate that tolerates a non-zero exit with an empty ``<stderr>`` as a benign status code, so the call does not raise (see :py:meth:`meta_package_manager.execution.CLIExecutor.run`). .. code-block:: shell-session $ pnpm outdated --global --json { "eslint": { "current": "9.10.0", "latest": "9.15.0", "wanted": "9.15.0", "isDeprecated": false, "dependencyType": "dependencies" } } """ output = self.run_cli("outdated", "--global", "--json", must_succeed=True) if output: for pkg_id, pkg_infos in json.loads(output).items(): yield self.package( id=pkg_id, installed_version=pkg_infos.get("current"), latest_version=pkg_infos["latest"], )
[docs] @search_capabilities(exact_support=False) def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]: """Fetch matching packages. pnpm queries the registry's ``/-/v1/search`` endpoint and, with ``--json``, emits an array of the matched packages (an empty ``[]`` when none match). .. caution:: Search does not support exact matching: the registry endpoint matches on names, descriptions and keywords, so the framework refilters the raw results for exact queries. .. code-block:: shell-session $ pnpm search --json is-positive [ { "name": "is-positive", "version": "3.1.0", "description": "Check if something is a positive number", "date": "2017-10-24T15:24:08.180Z", "maintainers": [ { "username": "sindresorhus" } ] } ] """ output = self.run_cli("search", "--json", query, must_succeed=True) if output: for pkg_infos in json.loads(output): yield self.package( id=pkg_infos["name"], description=pkg_infos.get("description"), latest_version=pkg_infos["version"], )
[docs] @version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. .. code-block:: shell-session $ pnpm add --global markdown """ return self.run_cli("add", "--global", package_id)
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all packages. .. code-block:: shell-session $ pnpm update --global --latest """ return self.build_cli("update", "--global", "--latest")
[docs] @version_not_implemented def upgrade_one_cli( self, package_id: str, version: str | None = None, ) -> tuple[str, ...]: """Generates the CLI to upgrade the package provided as parameter. .. code-block:: shell-session $ pnpm update --global --latest markdown """ return self.build_cli("update", "--global", "--latest", package_id)
[docs] def remove(self, package_id: str) -> str: """Remove one package. .. code-block:: shell-session $ pnpm remove --global markdown """ return self.run_cli("remove", "--global", package_id)
[docs] def cleanup(self) -> None: """Remove orphan packages from the global content-addressable store. .. code-block:: shell-session $ pnpm store prune """ self.run_cli("store", "prune")