Commit Graph

11458 Commits

Author SHA1 Message Date
Khải
0999261182 docs(ai): don't break method chain (#334)
* docs: warn agents against breaking method chains

* docs: allow justified method-chain rewrites

* docs: show the chain-preserving fix and contrast the diffs

* docs: trim chain-preservation section to two diffs

* docs: reframe chain rule around structural shape, not diff size

* docs: drop em dash and contraction from chain-preservation section

* docs: drop redundant meta-sentence from chain-preservation section

* docs: tighten closing paragraph of chain-preservation section

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-30 00:56:52 +02:00
chaoliang yan
4ac15b2512 fix(publish): report generated manifest in publish summary (#11371)
Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
2026-04-29 23:15:04 +02:00
Zoltan Kochan
d00b4952cd Merge branch 'release/11.0' into main 2026-04-29 23:05:17 +02:00
Zoltan Kochan
38ffda2a18 chore(release): 11.0.1 v11.0.1 2026-04-29 23:00:21 +02:00
btea
5e11362c8a fix: sort the keys of the overrides object (#11309) 2026-04-29 22:59:01 +02:00
Dami Oyeniyi
f543b77006 fix: reject null named catalogs in workspace manifest reader (#11231) 2026-04-29 22:58:54 +02:00
Rayan Salhab
16b347620b fix: validate unknown options before implicit run (#11374)
* fix: validate unknown options before implicit run

* test: ignore intentional misspelling in CLI test

---------

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes@example.invalid>
2026-04-29 22:58:44 +02:00
Zoltan Kochan
2d061b74a6 chore: add tracked .git-wt/pr-hook so wt <pr> launches a Claude review (#11383)
@zkochan/git-wt 0.0.3 looks for an executable .git-wt/pr-hook in the
worktree before falling back to ~/.config/git-wt/pr-hook. Shipping the
hook in-repo gives every contributor with Claude Code installed an
auto-launched PR review via `wt <pr-number>`. The hook silently no-ops
when `claude` isn't on PATH so contributors who don't use it aren't
affected.
2026-04-29 22:58:24 +02:00
Allan Kimmer Jensen
f9afe81eed fix(sbom): populate download location for git-sourced dependencies (#11329)
* fix(sbom): populate download location for git-sourced dependencies

* fix(sbom): avoid double git+ prefix when repo already includes it

Address Copilot review on #11329: gitDownloadUrl() would produce
git+git+ssh://... when GitResolution.repo already starts with git+.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-04-29 22:58:06 +02:00
Zoltan Kochan
0fbcf74aac fix: sync packageManager and devEngines.packageManager on self-update (#11395)
* fix: sync packageManager and devEngines.packageManager on self-update

When `package.json` declares both `packageManager` and
`devEngines.packageManager`, `pnpm self-update` previously bumped only
the latter — leaving Corepack (which reads `packageManager`) pinned to
the old version until a manual edit.

Now, when `packageManager` pins pnpm, both fields are rewritten to the
new exact version on update: `packageManager` to `pnpm@<version>`
(without an integrity hash) and `devEngines.packageManager.version` to
the same exact `<version>` (dropping any range operator). When only
`devEngines.packageManager` is declared, the existing range-preserving
behavior is unchanged.

Closes #11388

* refactor: export and reuse parsePackageManager from @pnpm/config.reader

Drop the inline duplicate in self-updater and use the existing
parser from config.reader. Same parsing rules (strips integrity
hash, rejects URL-style refs).

* refactor: collapse devEngines.packageManager array/object branches

Resolve to the underlying pnpm entry first (whether the field is an
array or an object) and run the version-update logic once, instead of
duplicating it across both branches.
2026-04-29 22:57:38 +02:00
Zoltan Kochan
7b83ecfc98 fix: sync env lockfile when devEngines.packageManager version is stale (#11392)
* fix: sync env lockfile when devEngines.packageManager version is stale

Update the env lockfile's `packageManagerDependencies` entry when
`devEngines.packageManager` declares a pnpm version that the lockfile
no longer satisfies. Previously the stale entry was kept even though
the running pnpm matched the declared version, silently breaking the
integrity record.

Closes #11387

* refactor: drop redundant pm.name guard in main.ts (syncEnvLockfile already checks)

* refactor: hoist pm.onFail !== 'ignore' guard so each clause appears once

* test: assert syncEnvLockfile actually rewrites the lockfile entry on disk
2026-04-29 22:57:29 +02:00
Zoltan Kochan
490a97ef34 fix: sync packageManager and devEngines.packageManager on self-update (#11395)
* fix: sync packageManager and devEngines.packageManager on self-update

When `package.json` declares both `packageManager` and
`devEngines.packageManager`, `pnpm self-update` previously bumped only
the latter — leaving Corepack (which reads `packageManager`) pinned to
the old version until a manual edit.

Now, when `packageManager` pins pnpm, both fields are rewritten to the
new exact version on update: `packageManager` to `pnpm@<version>`
(without an integrity hash) and `devEngines.packageManager.version` to
the same exact `<version>` (dropping any range operator). When only
`devEngines.packageManager` is declared, the existing range-preserving
behavior is unchanged.

Closes #11388

* refactor: export and reuse parsePackageManager from @pnpm/config.reader

Drop the inline duplicate in self-updater and use the existing
parser from config.reader. Same parsing rules (strips integrity
hash, rejects URL-style refs).

* refactor: collapse devEngines.packageManager array/object branches

Resolve to the underlying pnpm entry first (whether the field is an
array or an object) and run the version-update logic once, instead of
duplicating it across both branches.
2026-04-29 22:56:33 +02:00
Zoltan Kochan
ca474f1c9c fix: sync env lockfile when devEngines.packageManager version is stale (#11392)
* fix: sync env lockfile when devEngines.packageManager version is stale

Update the env lockfile's `packageManagerDependencies` entry when
`devEngines.packageManager` declares a pnpm version that the lockfile
no longer satisfies. Previously the stale entry was kept even though
the running pnpm matched the declared version, silently breaking the
integrity record.

Closes #11387

* refactor: drop redundant pm.name guard in main.ts (syncEnvLockfile already checks)

* refactor: hoist pm.onFail !== 'ignore' guard so each clause appears once

* test: assert syncEnvLockfile actually rewrites the lockfile entry on disk
2026-04-29 21:07:46 +02:00
Alessio Attilio
601317e7a3 feat: implement native 'pnpm owner' command (#11288)
Implements the native pnpm owner command with ls, add, and rm subcommands, providing a complete replacement for npm owner functionality.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-04-29 16:43:44 +02:00
Allan Kimmer Jensen
4750fd370c fix(sbom): populate download location for git-sourced dependencies (#11329)
* fix(sbom): populate download location for git-sourced dependencies

* fix(sbom): avoid double git+ prefix when repo already includes it

Address Copilot review on #11329: gitDownloadUrl() would produce
git+git+ssh://... when GitResolution.repo already starts with git+.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-04-29 13:54:29 +02:00
Rayan Salhab
32eae3112f fix: clean pnpm workspace state during ci (#11368)
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-04-29 12:55:07 +02:00
Igor Savin
b61e268d57 feat: add support for github prefix and named registries (#11324)
This is consistent with #9358, but implements support for the GitHub Packages npm registry and, more broadly, for vlt-style https://docs.vlt.sh/cli/registries for any registry.

This PR adds a built-in gh: specifier that resolves against the GitHub Packages npm registry, plus a namedRegistries config key so a project can map its own aliases to arbitrary registries. A project can mix public npm packages and private GitHub Packages (or self-hosted) ones without applying a scope-wide registry override to every @scope/* package.

- pnpm add gh:@acme/private writes "@acme/private": "gh:^1.0.0" and resolves from https://npm.pkg.github.com/.
- pnpm add gh:@acme/private@^1.0.0 (with or without an alias) is also supported. Aliased form writes "my-alias": "gh:@acme/private@^1.0.0".
- Auth comes from the existing per-URL .npmrc mechanism, e.g. //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}. No new auth surface.
- @github is intentionally not defaulted to https://npm.pkg.github.com/ - hardcoding that would hijack installs of the public @github/* packages on npmjs.org (e.g. @github/relative-time-element) for users without a scope-wide override. Use gh: to install from GitHub Packages, or configure @github:registry=... yourself if that's really what you want.
- Additional named registries (a self-hosted proxy, GitHub Enterprise Server, etc.) can be configured in pnpm-workspace.yaml:

```yml
namedRegistries:
  gh: https://npm.pkg.github.example.com/   # optional: overrides the built-in `gh` alias for GHES
  work: https://npm.work.example.com/
```

- Then work:@corp/lib@^2.0.0 resolves against https://npm.work.example.com/, and the built-in gh alias can be redirected to a GHES host.
- Env-var substitution (${VAR}) is supported in namedRegistries values, mirroring the .npmrc convention.
- Reserved alias names (npm, jsr, github, workspace, catalog, file, git, http, https, link, patch, and related git host shorthands) cannot be redefined as user-named registries - the resolver throws ERR_PNPM_RESERVED_NAMED_REGISTRY_ALIAS at startup rather than silently shadowing another protocol. Malformed URLs throw ERR_PNPM_INVALID_NAMED_REGISTRY_URL at startup too, instead of failing as a confusing 404 during resolution.
- On publish, createExportableManifest strips any named-registry prefix (both the built-in gh: and any user-configured alias) so npm and yarn consumers can still resolve the dependency via their own scope-registry configuration - mirroring the user-facing requirement when installing such a dep without the prefix.

The prefix is gh: rather than github: because github: is reserved by npm-package-arg / hosted-git-info as a git host shorthand (e.g. github:owner/repo) - reusing it would be a deviation from the specs used by the npm CLI. gh: is  shorter, matches vlt's convention, and cannot collide with any existing npm scheme.

Unlike jsr:, gh: (and any other named-registry alias) does not rewrite the package name - gh:@acme/foo resolves @acme/foo from the GitHub Packages registry as-is. This also means npm/yarn consumers see the original name after the prefix is stripped on publish.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-04-29 12:38:56 +02:00
Zoltan Kochan
3f37d17b23 chore: add tracked .git-wt/pr-hook so wt <pr> launches a Claude review (#11383)
@zkochan/git-wt 0.0.3 looks for an executable .git-wt/pr-hook in the
worktree before falling back to ~/.config/git-wt/pr-hook. Shipping the
hook in-repo gives every contributor with Claude Code installed an
auto-launched PR review via `wt <pr-number>`. The hook silently no-ops
when `claude` isn't on PATH so contributors who don't use it aren't
affected.
2026-04-29 00:36:03 +02:00
Zoltan Kochan
831542ed98 perf(package-manager): cache parent-dir creation in link_file (#286)
* perf(package-manager): cache parent-dir creation in link_file

`link_file` was calling `fs::create_dir_all(parent_dir)` for every
file it materialized, even when the previous file in the same
package had just created the same parent. `create_dir_all` is
idempotent but not free: it still stats every component of the
path on every call. With ~50K files in a typical install and most
packages depositing into a small fixed set of dirs (`lib/`,
`dist/`, `src/`, ...), that's tens of thousands of redundant
stat syscalls during the link phase.

Mirrors pnpm v11's `dirsCreated` set in
`fs/indexed-pkg-importer/src/index.ts`. Add an `EnsuredDirsCache`
type alias (`DashSet<PathBuf>`, concurrent because rayon's
`par_iter` over `cas_paths` calls into `link_file` from multiple
worker threads at once) and thread it through `link_file`'s
signature. Allocate one cache per `create_cas_files` invocation —
that scope is one package's CAS import, where the parent-dir
overlap is densest, and the cache is dropped right after so a
benchmark-style `rm -rf node_modules` between iterations never
sees a stale entry.

Concurrency: the duplicate-mkdir race is benign. If two rayon
threads both find the parent missing in the cache, both call
`create_dir_all` (which is itself idempotent) and both insert.
Worst case: one extra `create_dir_all` per fresh dir on a
contended start, then steady-state cache-hit fast path. No
locks beyond what `DashSet` does internally.

Tests:

* `link_file_populates_ensured_dirs_cache_on_create` — assert the
  parent goes into the cache after a successful link, so later
  siblings actually skip the syscall.
* `link_file_skips_create_dir_all_on_cache_hit` — pre-populate
  the cache with a path that doesn't exist on disk and ask
  `link_file` to materialize a target under it. The cache
  short-circuits `create_dir_all` and the underlying copy fails
  with `NotFound`. Without the short-circuit, `create_dir_all`
  would lazily create the missing dir and the call would
  succeed — that's the failure mode the test pins down.

All other `link_file` callers (10 unit tests in this module) get
a fresh empty `EnsuredDirsCache::new()` per call, which
preserves the prior per-test isolation.

Expected impact on warm-cache installs: the per-file
`create_dir_all` was the second cited bottleneck on the macOS
install gap. Caching collapses N-files calls to N-unique-parents
per package — roughly an order of magnitude on a tree like
`lib/{a,b,c}.js + dist/{a,b,c}.js + src/...`. Wall-time delta
should be visible in the next CI integrated-benchmark.

* refactor(package-manager): port pnpm v11 dir-precompute into create_cas_files

The previous commit on this branch added an `EnsuredDirsCache`
(`DashSet<PathBuf>`) threaded through `link_file` so concurrent
rayon workers could skip redundant per-file `create_dir_all`
syscalls. The commit message claimed this mirrored pnpm v11's
`dirsCreated` set — it doesn't. No such set exists in pnpm v11.

What pnpm actually does in `tryImportIndexedDir` (see
https://github.com/pnpm/pnpm/blob/e30c22f0d5/fs/indexed-pkg-importer/src/importIndexedDir.ts#L185-L198):

  const allDirs = new Set<string>()
  for (const f of filenames.keys()) {
    const dir = path.dirname(f)
    if (dir === '.') continue
    allDirs.add(dir)
  }
  Array.from(allDirs)
    .sort((d1, d2) => d1.length - d2.length) // shortest to longest
    .forEach((dir) => fs.mkdirSync(path.join(newDir, dir), { recursive: true }))
  // ... then file imports happen with NO per-file mkdir

The pattern is: precompute the unique parent dirs upfront, sort
shortest-first so each `mkdir -p` finds its ancestor on disk
(one `mkdirat` per dir instead of walking up), then mkdir
sequentially before any file imports begin. The file-import
primitive (`importFile` in pnpm, `link_file` here) never mkdirs.

Port that structure faithfully:

* Drop `EnsuredDirsCache` and the `dashmap::DashSet` import from
  `link_file`. Drop the `ensured_dirs_cache` parameter and the
  `if let Some(parent_dir) = target_link.parent()` mkdir block.
  `link_file` is now a leaf operation matching pnpm's `importFile`
  contract: caller guarantees the parent exists.
* Drop `LinkFileError::CreateDir` — `link_file` no longer mkdirs,
  so it can no longer fail with this kind of error.
* Add `CreateCasFilesError::CreateDir` since `create_cas_files`
  now owns the mkdir responsibility.
* In `create_cas_files`: build a `HashSet<&str>` of unique relative
  parents from `cas_paths` keys, skip empty (root) entries, sort
  by string-byte-length, then sequentially `create_dir_all` each
  joined absolute path. mkdir `dir_path` itself first to cover
  files at the package root (matches pnpm's `importIndexedDir`
  prelude that mkdirs `newDir` before calling `tryImportIndexedDir`).
* Drop the two cache-behavior tests
  (`link_file_populates_ensured_dirs_cache_on_create`,
  `link_file_skips_create_dir_all_on_cache_hit`) — they pinned
  the cache mechanism, not behavior worth preserving.
* Update the four `link_file` tests that wrote to `nested/dst.txt`
  to `fs::create_dir_all(dst.parent().unwrap())` first, since
  `link_file` no longer auto-mkdirs.

Performance is essentially unchanged (kernel work is identical:
D mkdirs per package either way). The user-space hot loop is
slightly leaner — no per-file `DashSet::contains` shard-locked
lookup — but the difference is well within bench noise. The win
here is structural fidelity to upstream and one less concurrent
data structure on the link path.

All 248 workspace tests pass; clippy clean.
2026-04-29 00:11:25 +02:00
Zoltan Kochan
0434c4e09f perf(store-dir): globalize verifiedFilesCache across the install (#285)
* perf(store-dir): globalize verifiedFilesCache across the install

Hoist `check_pkg_files_integrity`'s dedup `HashSet<PathBuf>` out
of the per-call scope and into an install-scoped
`Arc<DashSet<PathBuf>>` shared by every cached-tarball lookup.
Pnpm's [`store/cafs/src/checkPkgFilesIntegrity.ts`](https://github.com/pnpm/pnpm/blob/main/store/cafs/src/checkPkgFilesIntegrity.ts)
takes the cache as a parameter (`verifiedFilesCache: Set<string>`)
and threads one instance through every snapshot's verify pass for
the whole install — pacquet was allocating a fresh set per call,
so a CAFS blob shared across N packages paid the stat / re-hash N
times instead of once.

Plumbing:

* `pacquet_store_dir` now exports `VerifiedFilesCache`
  (`DashSet<PathBuf>` — concurrent because the verifier fans out
  across `tokio::task::spawn_blocking`) and
  `SharedVerifiedFilesCache` (`Arc<…>`).
* `check_pkg_files_integrity` takes `&VerifiedFilesCache`. Per-file
  dedup logic is unchanged: skip when the resolved CAFS path is
  already in the cache, insert on successful verify, leave the
  cache untouched on failure so the next snapshot re-stats.
* `DownloadTarballToStore` grows a `verified_files_cache` field
  (cloned `Arc` per-package; the SQLite-lookup `spawn_blocking`
  closure consumes its clone). `load_cached_cas_paths` forwards
  it into the `check_pkg_files_integrity` call.
* `InstallPackageBySnapshot`, `InstallPackageFromRegistry`, and
  `InstallWithoutLockfile::install_dependencies_from_registry` all
  thread a `&SharedVerifiedFilesCache` through.
* `CreateVirtualStore::run` and `InstallWithoutLockfile::run`
  allocate the cache once and bind same-Arc clones into each
  per-snapshot future (the `async move` closures can't borrow
  into the surrounding scope, so we clone-then-move the Arc).

Tests:

* `careful_path_dedups_across_calls_via_shared_cache` — plant a
  file, verify it once, *delete the file*, verify again with the
  same cache. Without the shared cache the second verify would
  stat-and-fail; with it, the path is already cached so the
  short-circuit returns `passed: true` despite the missing blob.
  Real installs don't delete files mid-run; the deletion is a
  test handle that proves the dedup actually skipped the stat.
* All other `check_pkg_files_integrity` tests now pass an empty
  `VerifiedFilesCache::new()` (per-test isolation, matching what
  the per-call `HashSet` used to give them).

Expected impact on warm-cache installs: the per-package
`fs::metadata` call inside `verify_file` is the cited bottleneck on
the macOS install gap (#280-style "12-13 s vs 7 s with pnpm"
report from a user). Across-package dedup lets the same shared
CAFS blob — common ones like LICENSE / README / empty
`.npmignore` files — pay one stat for the whole install instead
of one per consuming package. The N-files × N-packages worst
case collapses to N-files.

* docs(package-manager): reword Arc-clone comment in install_without_lockfile

Address Copilot review on #285. The previous wording — "an `async move`
can't borrow into the surrounding scope" — is misleading; an `async
move` block can capture references, the lifetime would just need to
outlive the resulting future. The actual reason for the clone is to
give each per-dependency future its own owned `Arc` handle.
2026-04-28 23:39:29 +02:00
Zoltan Kochan
e30c22f0d5 chore: update pnpm to v11.0.0 2026-04-28 21:46:50 +02:00
Zoltan Kochan
d492724a2c fix(package-manager): handle npm-alias dependency specs (#323)
* fix(package-manager): handle npm-alias dependency specs

`pacquet install` panicked when a manifest entry used the
`npm:<name>@<range>` alias form because the whole spec was fed straight
into `node_semver::Range::parse`. Decompose the spec at the install
layer so the registry sees the real package name and the resolver only
sees the version range, mirroring pnpm's `parseBareSpecifier`. The
manifest key stays as the directory name under `node_modules` while the
virtual store keeps using the registry-returned name, so plain and
aliased entries for the same target share one virtual store directory.

Closes #312

* fix(package-manager): handle latest tag in unversioned npm: alias resolution

- Remove generic `Tag` from `InstallPackageFromRegistry::run()` and parse
  `version_range` as `PackageTag` directly. `PackageTag::from_str` handles
  both `"latest"` (from unversioned `npm:foo` aliases) and pinned versions
  like `"1.0.0"`, while semver ranges (`"^1.0.0"`) fall through to the
  existing range-resolution branch. This prevents the panic where
  `package.pinned_version("latest").unwrap()` was called with a tag string
  that `node_semver::Range` cannot parse.
- Update call sites in `install_without_lockfile.rs` from `.run::<Version>()`
  to `.run()`.
- Add e2e test `unversioned_npm_alias_defaults_to_latest` that installs
  `npm:@pnpm.e2e/hello-world-js-bin` (no `@<version>`) and asserts the
  alias symlink is created and the real-name virtual-store dir exists."

Agent-Logs-Url: https://github.com/pnpm/pacquet/sessions/930e25bb-53da-4614-92cd-d100ab9008fe

Co-authored-by: zkochan <1927579+zkochan@users.noreply.github.com>

* style(package-manager): apply rustfmt to has_real_name_dir check

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: zkochan <1927579+zkochan@users.noreply.github.com>
2026-04-28 16:20:09 +02:00
Anthony Shew
1d5f8b5dca test(modules-yaml): set up parity tests (#309)
## Summary
- Add a [Stage 1](https://github.com/pnpm/pacquet/issues/299) test-porting map for TypeScript parity work.
- Add a `modules-yaml` crate.
- Add the first five 1:1 known-failure ports from pnpm's modules-yaml tests, each pinned to its upstream source permalink.
2026-04-28 16:19:32 +02:00
Zoltan Kochan
1a7c088e4d chore: update pnpm to v11.0.0 2026-04-28 11:51:43 +02:00
Zoltan Kochan
60fe422540 docs: add links to the changelog 2026-04-28 11:51:43 +02:00
Zoltan Kochan
4b822eca85 chore(release): 11.0.0 2026-04-28 11:51:43 +02:00
Zoltan Kochan
bf4214eb55 docs: add links to the changelog 2026-04-28 11:49:30 +02:00
Zoltan Kochan
8aeeff4c46 chore(release): 11.0.0 v11.0.0 2026-04-28 11:27:43 +02:00
Rayan Salhab
5adb37254a fix: validate unknown options before implicit run (#11374)
* fix: validate unknown options before implicit run

* test: ignore intentional misspelling in CLI test

---------

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes@example.invalid>
2026-04-28 00:07:00 +00:00
Zoltan Kochan
77e913c0b4 feat(self-update): point users at the migration guide across major bumps (#11354)
When `pnpm self-update <version>` crosses a pnpm major (upward) from
the version being upgraded from, print a one-line pointer to the
versioned migration guide on pnpm.io.

The "from" version is the project's `packageManager`/`devEngines.packageManager`
pin when present (so the hint still fires if the running pnpm is already
the new major — e.g. corepack-managed), falling back to the running
binary's version otherwise. No-op updates (target === previous) are
silent.

v11 points at https://pnpm.io/11.x/migration. Future majors register an
entry in the in-file `MAJOR_UPGRADE_HINTS` table.
2026-04-28 02:01:06 +02:00
Yuriy Matskanyuk
390b9d188d feat: add info about update started (#11334) 2026-04-28 02:00:58 +02:00
Akihiro Nagai
9b23098cf6 docs(env): update Node.js examples in help output (#11369) 2026-04-28 02:00:15 +02:00
Alessio Attilio
a6badef5de fix: ignore uuid vulnerability (GHSA-w5hq-g745-h8pq) (#11344) 2026-04-28 01:59:54 +02:00
Minha Kang
005eeb776b fix: upgrade peer dependencies with pnpm upgrade --latest (#9928)
The `--latest` flag triggers the `installSome` code path, which built
`currentBareSpecifiers` via `getAllDependenciesFromManifest()` — a
function that excluded peer dependencies. The non-`--latest` path uses
`getWantedDependencies()`, which honors `autoInstallPeers` and includes
them. Pass `autoInstallPeers` through `getAllDependenciesFromManifest`
so both paths agree.

Closes #9900

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-04-28 01:59:44 +02:00
Karl Horky
55e35d0491 docs: add Discord link to README (#11166)
Added Discord link to the README.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-04-28 01:59:32 +02:00
Zoltan Kochan
499bc677e2 chore: remove unused getNodeExecPath helpers (#11336)
Drops `getNodeExecPath`, `getNodeExecPathInBinDir`, and
`getNodeExecPathInNodeDir` along with their now-unused `which` dependency.
None of these helpers were referenced anywhere in the codebase.
2026-04-28 01:59:23 +02:00
Dami Oyeniyi
af2e566ff2 fix: reject null named catalogs in workspace manifest reader (#11231) 2026-04-28 01:53:31 +02:00
btea
94da69635e fix: sort the keys of the overrides object (#11309) 2026-04-28 01:42:38 +02:00
Rayan Salhab
ece94d255a fix: clean pnpm workspace state during ci (#11368)
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-04-28 01:23:38 +02:00
Zoltan Kochan
f3e33d8ecb feat(self-update): point users at the migration guide across major bumps (#11354)
When `pnpm self-update <version>` crosses a pnpm major (upward) from
the version being upgraded from, print a one-line pointer to the
versioned migration guide on pnpm.io.

The "from" version is the project's `packageManager`/`devEngines.packageManager`
pin when present (so the hint still fires if the running pnpm is already
the new major — e.g. corepack-managed), falling back to the running
binary's version otherwise. No-op updates (target === previous) are
silent.

v11 points at https://pnpm.io/11.x/migration. Future majors register an
entry in the in-file `MAJOR_UPGRADE_HINTS` table.
2026-04-28 00:45:23 +02:00
Yuriy Matskanyuk
38a5d700ae feat: add info about update started (#11334) 2026-04-27 23:29:49 +02:00
Akihiro Nagai
3c3e9ce07b docs(env): update Node.js examples in help output (#11369) 2026-04-27 23:27:21 +02:00
Yagiz Nizipli
676fe55306 refactor(tarball): move unit tests out of lib.rs into tests.rs
The inline mod tests block had grown to roughly 1290 lines, dwarfing
the production code in lib.rs and making the file hard to navigate.
Per CODE_STYLE_GUIDE.md ("Unit test file layout"), large unit-test
modules belong in an external file.

Extract the entire #[cfg(test)] mod tests { ... } block into a sibling
crates/tarball/src/tests.rs and reference it from lib.rs with the
standard:

    #[cfg(test)]
    mod tests;

This is pure motion — same 27 tests, same imports (use super::*; still
resolves to lib.rs), no behavior change. lib.rs shrinks from 2334 to
1045 lines.

Closes #307.
2026-04-27 12:49:49 -04:00
Zoltan Kochan
8d45e70a8a docs: add benchmark section to README
Add benchmark section with image to README
2026-04-27 16:11:35 +02:00
Khải
13b4762069 fix(benchmark): fetch resolved sha before checkout (#324)
A bare `git fetch <repo>` writes only the source's `HEAD` into
`FETCH_HEAD`, so a SHA that is not reachable from the source's `HEAD`
never lands in the bench-repo. The subsequent `git checkout <sha>` then
fails with `unable to read tree`. This affects PR branches that are
behind `main`, where `git rev-parse main` on the runner returns a SHA
whose tree is missing in the bench-repo.

Resolve the revision against the source repository first and pass the
resolved SHA to `git fetch <repo> <sha>` so the bench-repo always has
the exact commit the next step is about to check out.

Refs: https://github.com/pnpm/pacquet/pull/321#issuecomment-4326141435

https://claude.ai/code/session_01DS9GB92b1Bx9y8Tk9tH1w2

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-27 15:29:46 +02:00
Khải
840bd8685e ci(benchmark): drop --show-output (#325)
Hyperfine's --show-output piped each install's stdout/stderr (with
TRACE=pacquet=info enabled) into the workflow log, drowning the PR
summary in noise. Suppress per-run output and remove the now-pointless
TRACE env var; the markdown report still captures the timing summary.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-27 15:29:19 +02:00
Khải
51571b78c5 chore(rust): edition 2024 (#322)
* chore(workspace): switch to Rust edition 2024

Bump `edition` to 2024 in the workspace package metadata. The toolchain
is already pinned at 1.95.0 (latest stable as of 2026-04-14), which has
2024 edition support. Migration touches:

- collapse `if let { if let { … } }` chains into `let … && let …` to
  satisfy the new `clippy::collapsible_if` shape under edition 2024
  (let-chains are stable in 2024).
- wrap `env::set_var` / `env::remove_var` calls in `unsafe { … }`
  blocks now that they require an `unsafe` context. The test-only
  `EnvGuard` already serializes env mutation against the rest of the
  test process via a process-wide `Mutex`, so the safety condition
  ("no other thread accesses the env concurrently") still holds; the
  unsafe markers just make that contract explicit.
- bind a `Client` to a named local in `AutoMockInstance::load_or_init`
  so it outlives the borrow inside `MockInstanceOptions`. Edition 2024
  shortens temporary lifetimes in `let` initializers, so the previous
  `&Client::new()` inside a struct literal would have dangled.
- replace `prefetched.get(&index_key).is_none()` with
  `!prefetched.contains_key(&index_key)` per
  `clippy::unnecessary_get_then_check`.

The remaining diff is `cargo fmt` reflowing imports under the 2024
style edition (alphabetical ordering inside grouped `use { … }`
blocks).

* fix(npmrc): collapse Windows get_drive_letter `if let` chain

The `clippy::collapsible_if` lint also fires on the Windows-only
`get_drive_letter` helper, which was missed by the Linux/macOS clippy
pass during the edition 2024 migration. Apply the same `if let … &&
let …` collapse the lint suggested.

* style(lockfile): re-sort imports under 2024 style edition

Files added by `refactor: replace serde_yaml with serde_saphyr` (#320)
on main use the 2021 import-name ordering (`{serialize_yaml, Lockfile}`,
`{ser, ..., SerializerOptions}`). The 2024 style edition sorts use names
alphabetically, so the PR's merge ref tripped `cargo fmt --all -- --check`
once main caught up.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-27 13:41:48 +02:00
Khải
b47423f137 refactor: adopt smart-default (#321)
* refactor(npmrc,tarball): replace hand-written Default impls with SmartDefault

Adds the `smart-default` crate and migrates the two hand-written
`Default` impls whose bodies were field-level constants:

* `RetryOpts` (`crates/tarball/src/lib.rs`): the four retry knobs now
  carry their pnpm-matching defaults as `#[default = ...]` /
  `#[default(Duration::from_millis(...))]` field attributes.
* `Npmrc` (`crates/npmrc/src/lib.rs`): every field now has a
  `#[default(...)]` alongside its existing `#[serde(default = ...)]`.
  `Npmrc::new()` finally calls `Self::default()` instead of round-
  tripping through `serde_ini::from_str("")`, closing the in-line TODO.

`ThrottledClient`'s `Default` is left alone — it delegates to the
complex `new_for_installs()` builder, which is not expressible as
field-level defaults.

Resolves https://github.com/pnpm/pacquet/issues/306.

* refactor(store-dir): replace EncodeState::new with SmartDefault

`EncodeState`'s hand-written `fn new()` only overrode one of three
fields — `next_slot: FIRST_INNER_SLOT`. The other two (`cafs_slots`,
`side_effects_slots`) were initialized to `[None; N]`, which is the
type's `Default::default()`. With `SmartDefault`, the override
becomes a single `#[default(FIRST_INNER_SLOT)]` field attribute and
the explicit constructor goes away.

Use sites switch from `EncodeState::new()` to
`EncodeState::default()`.

* refactor(npmrc): point SmartDefault at the same helpers serde uses

The numeric `Npmrc` defaults (`modules_cache_max_age`, the
`fetch_retry*` family) were encoded twice — once as a `SmartDefault`
literal, once as a `default_*()` helper consumed by
`#[serde(default = "...")]`. Drop the literals and have
`SmartDefault` call the helpers via `_code` instead, so
`Npmrc::default()` and partial-input deserialization can't drift
apart.

Caught by Copilot in the #321 review.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-27 12:51:45 +02:00
Khải
56921af372 refactor: replace serde_yaml with serde_saphyr (#320)
* chore(workspace): replace serde_yaml with serde-saphyr

serde_yaml is deprecated and unmaintained. Switch to serde-saphyr,
which offers panic-free YAML parsing and is the replacement
recommended in pnpm/pacquet#311.

The new crate's API differs from serde_yaml in two places:

- It has no Value type. The single use site,
  ProjectSnapshot::dependencies_meta, holds a passthrough YAML blob
  that ports cleanly to serde_json::Value.

- Its serializer folds long single-line scalars into block style
  (>-) by default, which would garble the integrity hashes in
  pnpm-lock.yaml. A small serialize_yaml helper turns that off via
  prefer_block_scalars: false so the output continues to match the
  plain-scalar format js-yaml (and therefore pnpm) emits.

Resolves https://github.com/pnpm/pacquet/issues/311.

* test(lockfile): tighten serde-saphyr serialize tests

Address review feedback on PR #320:

- Switch resolution.rs callers from `crate::serialize_yaml::to_string`
  to a `use crate::serialize_yaml;` import + `serialize_yaml::to_string`,
  matching the rest of the crate's import style.
- Restore the original single-comparison shape (and `pipe_ref` chain) in
  the `serialize` tests for `PkgName` and `PkgNameVer`. Compare the
  serialized YAML string against the expected literal directly instead
  of doing a roundtrip plus a `Display` check.

* test(lockfile): assert formatting invariants on saved lockfile

`round_trip_parse_save_parse_preserves_lockfile` only checks structural
equality after parse → save → parse, so it can't catch the formatting
regression that motivated `serialize_yaml` (e.g. `integrity: sha512-…`
being emitted as a folded block scalar `>-` instead of a plain scalar
the way pnpm/`js-yaml` does). Add two byte-level assertions:

- the saved bytes must not contain `>-`, and
- they must contain a plain `integrity: sha512-` prefix.

This protects the very invariant `serialize_yaml::to_string` is meant
to enforce.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-27 12:40:23 +02:00
Zoltan Kochan
f73dacf971 chore(release): 0.2.2-5 2026-04-27 12:19:57 +02:00