* 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>
@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.
* 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>
* 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.
* 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
* 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.
* 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
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>
* 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>
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>
@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.
* 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.
* 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.
* 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>
## 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.
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.
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>
Drops `getNodeExecPath`, `getNodeExecPathInBinDir`, and
`getNodeExecPathInNodeDir` along with their now-unused `which` dependency.
None of these helpers were referenced anywhere in the codebase.
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.
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.
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-4326141435https://claude.ai/code/session_01DS9GB92b1Bx9y8Tk9tH1w2
Co-authored-by: Claude <noreply@anthropic.com>
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>
* 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>
* 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>
* 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>