Source code for meta_package_manager.managers.yarn

# 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
import re
from functools import cached_property

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 Yarn(PackageManager): """Virtual base shared by Yarn Classic and Yarn Berry. The two Yarn lines grew incompatible CLIs, so mpm models them as separate managers, {class}`YarnClassic` and {class}`YarnBerry`, both invoking the same `yarn` binary. The reported version decides which one is active: Classic claims the `1.x` range, Berry `2.x` and later. Only the shared cache-cleanup operation lives on this base. Command equivalences with the sibling JS managers are listed in [antfu-collective/ni](https://github.com/antfu-collective/ni?tab=readme-ov-file#ni). """ homepage_url = "https://yarnpkg.com" logo = "yarn" platforms = ALL_PLATFORMS virtual = True
[docs] def cleanup_cache(self) -> None: """Removes things we don't need anymore. See [`yarn cache clean`](https://yarnpkg.com/cli/cache/clean). ```{code-block} shell-session $ yarn cache clean --all yarn cache v1.22.19 success Cleared cache. ✨ Done in 0.35s. ``` """ self.run_cli("cache", "clean", "--all")
[docs] class YarnClassic(Yarn): """Yarn Classic, the `1.x` line. mpm claims this class for any `yarn` binary reporting a `1.x` version and drives it through the `yarn global` command family, so installs, upgrades and removals target the global prefix. Its `--json` output is a stream of one JSON object per line, not a single document, so every query is parsed line by line. ```{note} Yarn has [no dedicated search command](https://github.com/yarnpkg/yarn/issues/778#issuecomment-253146299) by maintainer decision, so `search` is simulated with `yarn info` and only resolves exact package names. ``` """ maintenance_note = ( "Yarn Classic (`1.x`) is [frozen](https://github.com/yarnpkg/yarn), taking " "only security fixes; the actively developed " "[Yarn Berry](https://github.com/yarnpkg/berry) line has a different CLI and " "is wrapped separately as the `yarn-berry` manager." ) id = "yarn" name = "Yarn Classic" requirement = ">=1.20.0,<2.0.0" cli_names = ("yarn",) pre_args = ("--silent",) _INSTALLED_REGEXP = re.compile( r"^.+\"data\":\"\\\"(?P<package_id>\S+)" r"@(?P<version>\S+)\\\" has binaries:\"\}$", re.MULTILINE, ) """ ```{code-block} shell-session $ yarn --version 1.22.11 ``` """ @property def installed(self) -> Iterator[Package]: """Fetch installed packages. ```{code-block} shell-session $ yarn global --json list --depth 0 {"type":"activityStart","data":{"id":0}} {"type":"activityTick","data":{"id":0,"name":"awesome-lint@^0.18.0"}} {"type":"activityTick","data":{"id":0,"name":"arrify@^2.0.1"}} {"type":"activityTick","data":{"id":0,"name":"case@^1.6.3"}} {"type":"activityTick","data":{"id":0,"name":"emoji-regex@^9.2.0"}} {"type":"activityEnd","data":{"id":0}} {"type":"progressStart","data":{"id":0,"total":327}} {"type":"progressTick","data":{"id":0,"current":1}} {"type":"progressTick","data":{"id":0,"current":2}} {"type":"progressTick","data":{"id":0,"current":3}} {"type":"progressTick","data":{"id":0,"current":4}} {"type":"progressTick","data":{"id":0,"current":5}} {"type":"progressFinish","data":{"id":0}} {"type":"info","data":"\"awesome-lint@0.18.0\" has binaries:"} {"type":"list","data":{"type":"bins-awesome-lint","items":["awesome-lint"]}} ``` ```{code-block} console $ yarn global list --depth 0 yarn global v1.22.19 info "awesome-lint@0.18.0" has binaries: - awesome-lint ✨ Done in 0.13s. ``` """ output = self.run_cli( "global", "--json", "list", "--depth", "0", must_succeed=True ) for package_id, version in self._INSTALLED_REGEXP.findall(output): yield self.package(id=package_id, installed_version=version)
[docs] @cached_property def global_dir(self) -> str: """Locate the global directory. ```{code-block} shell-session $ yarn global dir ~/.config/yarn/global ``` """ return self.run_cli("global", "dir", force_exec=True).rstrip()
@property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. Yarn emits its `--json` output as a stream of one JSON object per line (the sample below elides the `info` color-legend object; the human-readable transcript further down shows it): ```{code-block} shell-session $ yarn --json outdated --cwd ~/.config/yarn/global {"type":"warning","data":"package.json: No license field"} {"type":"table","data":{"head":["Package","Current","Wanted","Latest","Package Type","URL"],"body":[["markdown","0.4.0","0.4.0","0.5.0","dependencies","git://github.com/evilstreak/markdown-js.git"]]}} ``` ```{code-block} console $ yarn outdated --cwd ~/.config/yarn/global yarn outdated v1.22.19 warning package.json: No license field info Color legend : "<red>" : Major Update backward-incompatible updates "<yellow>" : Minor Update backward-compatible features "<green>" : Patch Update backward-compatible bug fixes Package Current Wanted Latest Package Type URL markdown 0.4.0 0.4.0 0.5.0 dependencies git://github.com/.../md-js.git ✨ Done in 0.95s. ``` """ output = self.run_cli( "--json", "outdated", "--cwd", self.global_dir, must_succeed=True ) if output: for line in output.splitlines(): if not line: continue obj = json.loads(line) if obj["type"] == "table": for package in obj["data"]["body"]: if package[2] == "linked": continue yield self.package( id=package[0], installed_version=package[1], latest_version=package[3], )
[docs] @search_capabilities(extended_support=False, exact_support=False) def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]: """Fetch matching packages. ```{warning} Yarn maintainers have [decided to not implement a dedicated search command](https://github.com/yarnpkg/yarn/issues/778#issuecomment-253146299). Search is simulated by a direct call to `yarn info`, and as a result only works for exact match. ``` ```{code-block} shell-session $ yarn --json info python | jq { "type": "inspect", "data": { "name": "python", "description": "Interact with python child process", "dist-tags": { "latest": "0.0.4" }, "versions": [ "0.0.0", "0.0.1", "0.0.2", "0.0.3", "0.0.4" ], "maintainers": [ { "name": "drderidder", "email": "drderidder@gmail.com" } ], "time": { "modified": "2017-09-16T05:26:13.151Z", "created": "2011-07-11T01:59:04.362Z", "0.0.0": "2011-07-11T01:59:05.137Z", "0.0.1": "2011-07-17T05:23:33.166Z", "0.0.2": "2011-07-20T03:42:50.379Z", "0.0.3": "2014-06-08T00:39:08.562Z", "0.0.4": "2015-01-25T02:48:07.820Z" }, "author": { "name": "Darren DeRidder" }, "repository": { "type": "git", "url": "git://github.com/73rhodes/node-python.git" }, "homepage": "https://github.com/73rhodes/node-python", "bugs": { "url": "https://github.com/73rhodes/node-python/issues" }, "readmeFilename": "README.md", "users": { "dewang-mistry": true, "goliatone": true, "sapanbhuta": true, "aditcmarix": true, "imlucas": true, "heyderpd": true, "ukuli": true, "chbardel": true, "asaupup": true, "nuwaio": true }, "version": "0.0.4", "main": "./lib/python.js", "engines": { "node": ">= 0.4.1" }, "gitHead": "69754aaa57658193916a1bf5fc391198098f74f6", "scripts": {}, "dist": { "shasum": "3094e898ef17a33aa9c3e973b3848a38e47d1818", "tarball": "https://registry.npmjs.org/python/-/python-1.tgz" }, "directories": {} } } ``` """ output = self.run_cli("--json", "info", query, must_succeed=True) if output: for line in output.splitlines(): if not line: continue result = json.loads(line) if result["type"] == "inspect": package = result["data"] yield self.package( id=package["name"], description=package["description"], latest_version=package["version"], )
[docs] @version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. ```{code-block} shell-session $ yarn --silent global add awesome-lint yarn global v1.22.19 [1/4] πŸ” Resolving packages... [2/4] 🚚 Fetching packages... [3/4] πŸ”— Linking dependencies... [4/4] πŸ”¨ Building fresh packages... success Installed "awesome-lint@0.18.0" with binaries: - awesome-lint ✨ Done in 16.15s. ``` """ return self.run_cli("global", "add", package_id)
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all outdated packages. ```{code-block} shell-session $ yarn --silent global upgrade --latest yarn global v1.22.19 [1/4] πŸ” Resolving packages... [2/4] 🚚 Fetching packages... [3/4] πŸ”— Linking dependencies... [4/4] πŸ”¨ Rebuilding all packages... success Saved lockfile. success Saved 271 new dependencies. info Direct dependencies β”œβ”€ awesome-lint@0.18.0 └─ markdown@0.5.0 info All dependencies β”œβ”€ @babel/code-frame@7.18.6 β”œβ”€ @babel/helper-validator-identifier@7.18.6 β”œβ”€ @nodelib/fs.scandir@2.1.5 β”œβ”€ array-to-sentence@1.1.0 β”œβ”€ array-union@2.1.0 β”œβ”€ awesome-lint@0.18.0 β”œβ”€ fs.realpath@1.0.0 (...) └─ zwitch@1.0.5 ✨ Done in 19.89s. ``` """ return self.build_cli("global", "upgrade", "--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 provided package. ```{code-block} shell-session $ yarn --silent global upgrade markdown --latest yarn global v1.22.19 [1/4] πŸ” Resolving packages... [2/4] 🚚 Fetching packages... [3/4] πŸ”— Linking dependencies... [4/4] πŸ”¨ Rebuilding all packages... success Saved lockfile. success Saved 2 new dependencies. info Direct dependencies └─ markdown@0.5.0 info All dependencies β”œβ”€ markdown@0.5.0 └─ nopt@2.1.2 ✨ Done in 1.77s. ``` """ return self.build_cli("global", "upgrade", package_id, "--latest")
[docs] def remove(self, package_id: str) -> str: """Remove one package. ```{code-block} shell-session $ yarn --silent global remove awesome-lint yarn global v1.22.19 [1/2] πŸ—‘ Removing module awesome-lint... [2/2] πŸ”¨ Regenerating lockfile and installing missing dependencies... success Uninstalled packages. ✨ Done in 0.21s. ``` """ return self.run_cli("global", "remove", package_id)
[docs] class YarnBerry(Yarn): """Yarn Berry, the `2.x` and later line. mpm claims this class for any `yarn` binary reporting a `2.x` or newer version. ```{warning} Yarn Berry removed the `yarn global` command family entirely: it has no notion of globally installed packages. Only `search` is available, while `installed`, `outdated`, `install`, `upgrade` and `remove` are all unsupported. ``` ```{note} `search` is simulated with `yarn npm info` and only resolves exact package names. ``` """ id = "yarn-berry" name = "Yarn Berry" requirement = ">=2.0.0" cli_names = ("yarn",)
[docs] @search_capabilities(extended_support=False, exact_support=False) def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]: """Fetch matching packages. ```{warning} Search is simulated by a direct call to `yarn npm info`, and as a result only works for exact match. ``` ```{code-block} shell-session $ yarn npm info python --json | jq { "name": "python", "description": "Interact with python child process", "dist-tags": { "latest": "0.0.4" }, "versions": [ "0.0.0", "0.0.1", "0.0.2", "0.0.3", "0.0.4" ], "version": "0.0.4", (...) } ``` """ output = self.run_cli("npm", "info", query, "--json") if output: package = json.loads(output) yield self.package( id=package["name"], description=package.get("description"), latest_version=package.get("dist-tags", {}).get("latest") or package.get("version"), )