pnpm выпустил 12.0: переписал менеджер пакетов на Rust

pnpm выпустил 12.0: переписал менеджер пакетов на Rust

Вышла стабильная версия pnpm 12.0. Это переписанный на Rust движок пакетного менеджера, но команда pnpm подчёркивает: это не миграция, команды, флаги, настройки и формат lock-файла версии 11 полностью перенесены, а документация описывает обе версии параллельно. На npm тег latest пока указывает на ветку pnpm 11, поэтому версию 12 нужно ставить отдельно командой pnpm self-update next-12; Homebrew, winget, Scoop и Chocolatey пока её не предлагают.

В релизе есть несколько несовместимых изменений. Во-первых, спецификатор git-зависимости для репозиториев на GitHub, GitLab и Bitbucket теперь трактуется как идентификатор репозитория, а не выбор транспорта: формы github:owner/repo, owner/repo, git+https://… и git+ssh://git@… разрешаются через канонический HTTPS-адрес хоста, и в lock-файле для этих хостов больше никогда не хранится SSH-адрес. Чтобы по-прежнему тянуть приватный репозиторий по SSH, нужно настроить подмену URL в самом git (git config --global url."git@github.com:".insteadOf https://github.com/), pnpm вызывает git снаружи, и подмена сработает для всех его git-операций. Незнакомые хосты сохраняют исходный адрес как есть, включая SSH, а URL со встроенными учётными данными сохраняется дословно и никогда не превращается в адрес архива хоста.

Во-вторых, нераспознанная настройка в pnpm-workspace.yaml больше не проглатывается молча, опечатка вроде minimumReleaseAge раньше просто теряла смысл настройки и никак об этом не сообщала. Теперь pnpm сообщает о такой настройке и, если ключ похож на опечатку, предлагает ближайшее реальное имя. Если проект зафиксировал версию pnpm, которой соответствует запущенная версия, команда падает с ошибкой ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS, раз версия совпадает, настройка не могла предназначаться для другой версии pnpm, и это ошибка, которую нужно исправить. Во всех остальных случаях это просто предупреждение, чтобы ещё не приведённый в порядок проект продолжал работать.

В-третьих, lock-файлы для циклических графов зависимостей стали каноническими: участники каждого цикла теперь упорядочиваются по идентификатору пакета, а разрыв цикла всегда происходит в одном и том же месте независимо от того, с какой стороны установка вошла в цикл. Благодаря этому lock-файл становится чистой функцией от графа зависимостей, переупорядоченные импортёры, переупорядоченные зависимости и повторные установки дают побайтово идентичный файл, чего раньше не было. На больших воркспейсах с большим числом циклов разрешение пиров теперь в 2, 3 раза быстрее, использует примерно на 25% меньше памяти и даёт заметно меньший по размеру lock-файл. Существующие lock-файлы продолжают работать без изменений: --frozen-lockfile читает их как есть, а первая установка, которая реально пересчитывает разрешение, один раз переключает ключи вариантов пиров для циклических пакетов.

В-четвёртых, на Linux настройка packageImportMethod: auto теперь сначала пробует жёсткую ссылку (hardlink), а не reflink: на btrfs создание reflink копирует служебную структуру экстентов файловой системы, тогда как hardlink, это всего одна запись в каталоге, и переключение примерно вдвое сокращает время, которое установка тратит на материализацию node_modules из тёплого стора. На ext4 поведение не меняется (там клонирование никогда не поддерживалось, так что auto и раньше начинал с hardlink), а macOS по-прежнему сначала пробует клонирование через APFS clonefile. Настройка packageImportMethod: clone по-прежнему явно требует клонирования.

В-пятых, при включённом engineStrict установка теперь падает, если несовместимый по версии движка пакет достижим через обычное ребро dependencies у устанавливаемого пакета, даже если всё это поддерево висит на записи optionalDependencies. В pnpm 11 такой пакет просто устанавливался с предупреждением. Пакеты, достижимые только через опциональные рёбра или через пакет, который сам был пропущен, по-прежнему пропускаются в обеих версиях.

Из новых возможностей: глобально установленные node, deno и bun теперь следуют версии, которую зафиксировал текущий проект, а не всегда запускают глобальную версию, это включает настройка globalShims (по умолчанию { node: true, deno: true, bun: true }), причём стабильный релиз Node.js проверяется по подписям команды релизов Node.js и переключается без вопросов, а для остального (Deno, Bun, пререлизы Node.js, бинарники обычных пакетов) pnpm один раз на проект спрашивает «Do you trust this project?» и запоминает ответ локально на машине. Далее, pnpm теперь умеет ставить и другие пакетные менеджеры, npm, Yarn Classic, Yarn Berry, Yarn 6 и Bun, каждый получается через доверенный реестр пакетных менеджеров и проверяется по подписи npm для точной версии перед запуском; это используется, когда git-зависимость требует другой менеджер, для разовых команд через pnx (pnx yarn@4 install, pnx npm@11 ci) и для pnpm shim add yarn.

Ещё одна новая возможность, «ревизии реестра»: реестр может отдать заменяющий артефакт для уже опубликованной версии (например, пересборку с закрытой уязвимостью), не меняя номер версии и не переписывая байты по каноническому адресу name@version. pnpm адресует такой артефакт по полному SHA-512-дайджесту и фиксирует его в lock-файле отдельной строкой revision; запись без ревизии, это ревизия 0, то есть оригинал, так что lock-файл, не принявший ни одной замены, побайтово совпадает с сегодняшним. Кроме того, pnpm init теперь фиксирует последнюю выпущенную версию pnpm, а не ту, что запустила команду, чтобы проект, созданный устаревшей версией pnpm, не наследовал эту устарелость через собственный пин; появилось пакетное подтверждение отложенных публикаций (pnpm stage approve), настройка audit.ignorePrune для audit --fix, а глобальные команды теперь отказываются запускаться под sudo с ошибкой ERR_PNPM_SUDO_NOT_SUPPORTED. Отдельно, как proof of concept, добавлен опциональный удалённый кеш побочных эффектов сборки: он позволяет переиспользовать результат сборки зависимости между машинами через подписанные организацией артефакты вместо локального запуска lifecycle-скриптов; пока он работает только на Linux/glibc x64 и arm64, а любой сбой кеша откатывается на обычную локальную сборку.

Среди исправлений: встроенная база совместимости больше не добавляет зависимости, найденные статическим анализом опубликованных пакетов, такие записи называли пакеты, которые импортировались только ради типов, и в худшем случае ломали проект (пример из релиза: @typescript-eslint/types тянул typescript последней версии, из-за чего под старые версии @typescript-eslint попадал TypeScript 7 и ESLint падал с ошибкой чтения Intrinsic). Также, если ни один каталог выше проекта не поддерживает жёсткие ссылки, например, в песочнице AI-агента с доступом на запись только к проекту или в контейнере, где смонтирован только проект, стор по умолчанию теперь создаётся внутри проекта, в node_modules/.pnpm-store. Наконец, хук pnpmfile hooks.filterLog объявлен устаревшим: pnpm 12 его игнорирует и предупреждает при использовании, вместо него нужно настраивать loglevel.

Ключевые факты

  • pnpm 12.0 стабилен: движок переписан на Rust, но команды, флаги, настройки и формат lock-файла из pnpm 11 полностью сохранены, это не миграция
  • Ставится отдельно (pnpm self-update next-12), потому что npm-тег latest пока указывает на pnpm 11; Homebrew, winget, Scoop и Chocolatey версию 12 ещё не предлагают
  • На больших воркспейсах с циклическими зависимостями разрешение пиров теперь детерминированное (побайтово одинаковый lock-файл), в 2, 3 раза быстрее и использует примерно на 25% меньше памяти
  • Из breaking changes: git-зависимости на GitHub/GitLab/Bitbucket теперь резолвятся через HTTPS хоста (SSH требует отдельной настройки git), а engineStrict теперь ловит несовместимые пакеты и внутри optionalDependencies-поддеревьев
  • Новое: pnpm умеет ставить npm, Yarn и Bun как отдельные пакетные менеджеры, глобальные node/deno/bun следуют версии проекта, а реестр может отдавать патченную ревизию пакета без смены номера версии

Почему это важно

pnpm, один из трёх основных пакетных менеджеров JS/TS-экосистемы, и версия 12.0, это не косметическое обновление, а переписанное на Rust ядро инструмента, которым разработчики пользуются каждый день. При этом команда демонстративно сохранила полную обратную совместимость по командам, флагам и формату lock-файла с версией 11, так что переход не требует миграции проекта. Главные технические выигрыши, детерминизм: разрешение циклических зависимостей теперь даёт побайтово одинаковый lock-файл независимо от порядка установки, а на больших воркспейсах это ещё и в 2, 3 раза быстрее и экономнее по памяти.

Кому это важно

В первую очередь, командам с большими monorepo/воркспейсами, где граф зависимостей содержит циклы (там выигрыш в скорости и памяти самый заметный). Также это касается пользователей Linux на btrfs (ускорение материализации node_modules из тёплого стора), команд, использующих git-зависимости с приватными репозиториями по SSH (потребуется донастройка git), и разработчиков, которые запускают pnpm в песочницах AI-агентов или контейнерах с ограниченным доступом на запись, для них стор теперь создаётся внутри проекта.

Как это применить

Обновление ставится не через обычный pnpm self-update, а через pnpm self-update next-12, поскольку тег latest на npm пока указывает на ветку 11; менеджеры вроде Homebrew, winget, Scoop и Chocolatey версию 12 пока не раздают. Перед переходом стоит свериться со списком breaking changes: если используются git-зависимости с приватными репозиториями по SSH на GitHub/GitLab/Bitbucket, нужно настроить подмену URL в самом git; если включён engineStrict, стоит проверить, не начнут ли падать пакеты, которые раньше пропускались с предупреждением внутри optionalDependencies-поддеревьев. Из новых возможностей можно опробовать globalShims для управления привязкой глобальных node/deno/bun к версии проекта, pnpm init для актуального пина версии pnpm в новых проектах и pnx для разового запуска npm/yarn/bun без установки.

Можно ли доверять

Источник, официальный блог pnpm с описанием релиза 12.0, то есть первичный источник от самой команды разработки инструмента, а не пересказ третьей стороны. Все технические детали в пересказе (breaking changes, новые возможности, цифры по производительности) взяты из этого релиз-поста.

Риски и подводные камни

Несмотря на заявленную обратную совместимость, у релиза есть несколько мест, где старое поведение молча меняется: git-зависимости по SSH на популярных хостах перестают резолвиться как SSH без ручной настройки git, ранее «прощаемые» настройки в pnpm-workspace.yaml могут внезапно провалить команду с ошибкой (если зафиксированная в проекте версия pnpm совпадает с запущенной), а engineStrict начинает ловить несовместимые пакеты там, где раньше молчал. Удалённый кеш побочных эффектов сборки, пока лишь proof of concept, работает только на Linux/glibc x64 и arm64 и требует отдельного доверия к организации, публикующей артефакты. Наконец, версия 12 ещё не попала в основные пакетные менеджеры ОС (Homebrew, winget, Scoop, Chocolatey), так что автоматизированные пайплайны, которые ставят pnpm через них, версию 12 пока не увидят.