Skip to content

pacquet: lockfile-parity gaps vs pnpm CLI (npm: aliases, minimumReleaseAge, peer suffixes) #12266

Description

@zkochan

Summary

pacquet install --lockfile-only does not yet reproduce the pnpm-lock.yaml
that the pnpm CLI produces from the same clean state. Running it on this
monorepo's own workspace (after rm -rf node_modules pnpm-lock.yaml) and
diffing against a freshly generated pnpm 11.5.2 lockfile (same clean
state, both run back-to-back against the live registry so version drift is
excluded) shows ~3,200 changed lines.

This issue tracks the lockfile-parity gaps. The deep resolver items will be
addressed together in a single parity PR; the self-contained items are
smaller follow-ups.

Reproduction

git worktree add -b parity-repro <branch>
cd <worktree>
rm -rf node_modules pnpm-lock.yaml && pacquet install --lockfile-only   # pacquet
rm -rf node_modules pnpm-lock.yaml && pnpm   install --lockfile-only    # pnpm 11.5.2

The committed lockfile is a two-document file (doc 1 = env lockfile:
configDependencies/packageManagerDependencies; doc 2 = real lockfile).
pacquet emits only the single doc-2-equivalent document — expected and out of
scope here.

Note / correction: an earlier draft of this issue listed
minimumReleaseAge as a cause of version drift (@types/node 22.19.20 vs
22.19.19, etc.). That was a measurement artifact from a stale pnpm
packument cache in the first reference run. Re-running both tools
back-to-back against the live registry, pacquet and pnpm agree on those
versions. minimumReleaseAge is not a parity bug. The single-package
repro ({"@types/node":"^22.19.19"} + minimumReleaseAge: 1440) confirms
pnpm 11.5.2 and pacquet both pick 22.19.20 (which is now >1 day old).

Byte-level formatting

Equivalent content is byte-identical in low-level formatting: 2-space indent,
LF (no CRLF), no tabs, single trailing newline. The only leading-byte
difference is pnpm's --- document marker, present solely because pnpm's file
is multi-document (the env lockfile). No whitespace/quoting drift where
content matches.

Categories

Deep resolver work (single parity PR)

1. Cyclic / ancestor-peer suffix collapse — the dominant cause (~830 lines
touch supports-color alone; cascades widely because changing a peer suffix
changes the depPath key, rewriting every reference).

Minimal repro:

{ "dependencies": { "eslint": "10.4.1", "supports-color": "8.1.1" } }
# pnpm-workspace.yaml
autoInstallPeers: true
-  '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1)':
+  '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(supports-color@8.1.1))':
     dependencies:
       eslint: 10.4.1(supports-color@8.1.1)
       eslint-visitor-keys: 3.4.3

@eslint-community/eslint-utils peer-depends on eslint. eslint is its
walk-ancestor (eslint → eslint-utils → eslint peer). When pacquet computes
eslint-utils's depPath suffix, eslint's own depPath
(eslint@10.4.1(supports-color@8.1.1)) isn't finalized yet, so
build_peer_id
hits its cycle fallback (step 4) and freezes the collapsed eslint@10.4.1.

pnpm's
calculateDepPath
defers depPath computation for nodes with not-yet-resolved peers and
awaits each pending peer's pathsByNodeIdPromises to obtain its full
depPath — falling back to name@version only for genuinely detected
cycles
. pacquet treats every not-yet-known peer as a cycle. The fix is to
port pnpm's deferred two-phase depPath computation with real cycle detection.

Related manifestation in the monorepo: jest's suffix is
30.4.2(@babel/types@7.29.7)(@types/node@…) in pnpm vs
30.4.2(@babel/core@7.29.7(supports-color@8.1.1))(@types/node@…) in pacquet;
typanion is also missing from some suffixes (~58 lines).

1b. Resolved optional peer materialized as a dependency. pacquet adds
supports-color: 8.1.1 to the dependencies: of eslint /
@eslint/config-array; pnpm represents it only via the peer suffix +
transitivePeerDependencies. (Same minimal repro.)

2. npm: aliases not handled in catalogs / overrides / snapshots.
pacquet drops 8 aliased catalog entries (boxen→@zkochan/boxen,
js-yaml→@zkochan/js-yaml, execa→safe-execa, ramda→@pnpm/ramda,
which→@pnpm/which, hosted-git-info→@pnpm/hosted-git-info,
@types/pnpm__byline, @types/zkochan__table), leaves catalog: literal in
overrides instead of expanding, and resolves snapshot deps to the real
package instead of the alias:

   write-yaml-file@5.0.0:
     dependencies:
-      js-yaml: 4.2.0
+      js-yaml: '@zkochan/js-yaml@0.0.11'

Cascades into missing @zkochan/* / @pnpm/* aliased packages.

Self-contained follow-ups

3. Self-links rendered as link:. instead of link: (204 lines):

       '@pnpm-private/updater':
         specifier: workspace:*
-        version: link:.
+        version: 'link:'

4. node runtime dep rendered as node@runtime:26.3.0 instead of
runtime:26.3.0.

5. pnpmfileChecksum not emitted. ✅ Fixed in #12280.

6. patchedDependencies block not emitted (graceful-fs@4.2.11). ✅ Fixed in #12281.


Written by an agent (Claude Code, claude-opus-4-8).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions