Skip to content

lockfile: resolve a read against the declared package manager, not filename precedence - #781

Open
colinhacks wants to merge 2 commits into
mainfrom
lockfile-read-declaration
Open

lockfile: resolve a read against the declared package manager, not filename precedence#781
colinhacks wants to merge 2 commits into
mainfrom
lockfile-read-declaration

Conversation

@colinhacks

Copy link
Copy Markdown
Contributor

The defect

In a project declaring npm, a bun.lock sitting beside package-lock.json made nub install rewrite package-lock.json without the resolved, license and engines of exactly the entries bun.lock also names. It also installed bun's pins rather than the project's own. Remove the bun.lock and the same fixture round-tripped byte-for-byte.

mkdir /tmp/repro && cd /tmp/repro
cp <nub-repo>/{package.json,package-lock.json,bun.lock} .
cp package-lock.json /tmp/before.json
nub install
diff -u /tmp/before.json package-lock.json   # 6 entries lose resolved/license/engines

The same shape reproduces with a stray pnpm-lock.yaml or yarn.lock, and with npm-shrinkwrap.json.

Root cause

Reads and writes were resolving different files. The write path asks the declaration-aware resolve_project_lockfile_kind, which honors packageManager and devEngines. The read path walked candidates in raw filename precedence — pnpm > bun > yarn > npm-shrinkwrap > npm — and took the first that existed. Since package-lock.json sorts last, the stray file won the read while npm still won the write, so bun's thinner graph was serialized into npm's file.

Confirmed with RUST_LOG=debug: the install logged peer-context pass (lockfile=Bun) before, lockfile=Npm after. Nothing merged the two lockfiles — the npm one was never read.

The read candidates are now ordered so the declaration's family leads. Reordering rather than filtering keeps this from inventing a failure mode: when a declaration contradicts the disk, or several tools' lockfiles coexist undeclared, detection errors and the read falls back to the old precedence, leaving the write path to raise the error it already raised.

Second defect, independent of any foreign lockfile

The npm writer's root importer entry mirrored only the manifest's dependency fields. Real npm also copies license, bin and engines there, so a genuine rewrite dropped all three even with no stray lockfile present. Both npm normalizations are reproduced: an object license collapses to its type, a string bin expands to { <name-without-scope>: <path> }. Scoped to the root importer — a workspace member's entry has no manifest available on the path that matters.

The signal-exit hoist: intended, left alone

The same rewrite moved node_modules/write-file-atomic/node_modules/signal-exit to node_modules/signal-exit. Differential on the same fixture:

tool and state placement
npm, existing lockfile kept node_modules/write-file-atomic/node_modules/signal-exit
npm, no lockfile (fresh resolve) node_modules/signal-exit
nub, genuine write node_modules/signal-exit

The npm writer recomputes the layout from the flat graph on every write and places each package in the shallowest legal slot. Only one version of signal-exit is demanded in the tree and the root slot is free, so the hoist is what npm itself produces on a fresh resolve. npm differs only in that its tree build is incremental and preserves an existing valid placement.

Verification

  • Reported reproduction: read resolves Npm, package-lock.json byte-identical.
  • Genuine write (nub add is-odd@3.0.1) with bun.lock present: zero resolved/license/engines lost, against 17 fields before.
  • Fresh project carrying license, bin and engines: nub's package-lock.json byte-identical to npm 11.17's.
  • A 13-case declaration-by-lockfile matrix run against the merge-base build and this one: only the declared-with-stray cases changed; ambiguity and declaration-mismatch errors are untouched.
  • Both new tests confirmed red with their fix reverted.
  • Root clippy --all-targets --all-features, cargo fmt --check, root cargo test (59 binaries), and cargo test --workspace in vendor/aube (56 binaries) all pass.

…lename precedence

`parse_lockfile_with_kind_and_options` walked `lockfile_candidates` in raw
filename precedence and read whichever file existed first, while the write path
asks the declaration-aware `resolve_project_lockfile_kind`. In a project
declaring npm, a `bun.lock` sitting beside `package-lock.json` outranks it in
that precedence, so an install resolved against bun's graph and serialized it
back out as `package-lock.json` — dropping `resolved`, `license` and `engines`
from every entry bun's format cannot carry, and installing bun's pins rather
than the project's own.

Reorder the read candidates so the declaration's family leads, falling back to
today's precedence when detection errors, so a contradicted or ambiguous project
still reaches the write path's existing error instead of a new one here.

Separately, the npm writer's root importer entry mirrored only the manifest's
dependency fields. npm also copies `license`, `bin` and `engines` there, so a
genuine rewrite dropped all three from a hand-written lockfile regardless of any
foreign lockfile. Mirror them, with npm's normalization of an object `license`
and a string `bin`, verified byte-for-byte against npm 11.17.
Copilot AI lite review requested due to automatic review settings August 20, 2026 16:16
@pullfrog

pullfrog Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Your Claude subscription has hit its usage limit. It resets at 11pm (UTC). Re-trigger Pullfrog after the reset, or add an ANTHROPIC_API_KEY repo secret — Pullfrog routes around an exhausted subscription automatically when one is present.

Add repo secret → · Model settings → · Setup docs → · Ask in Discord →

Pullfrog  | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nub Error Error Aug 20, 2026 4:29pm

Request Review

npm normalizes every bin path in the root importer entry, stripping the
leading `./` and stripping it repeatedly — `././d.js` lands as `d.js`.
Mirroring the manifest verbatim wrote `./cli.js` where npm writes `cli.js`,
so an alternating npm/nub install churned the lockfile on exactly the entry
this branch set out to stop churning.

Verified against npm 11.17 across all four spellings (`./a.js`, `b.js`,
`./nested/c.js`, `././d.js`): byte-identical bin maps. The test fails with
the normalization reverted.
@pullfrog

pullfrog Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Your Claude subscription has hit its usage limit. It resets at 11pm (UTC). Re-trigger Pullfrog after the reset, or add an ANTHROPIC_API_KEY repo secret — Pullfrog routes around an exhausted subscription automatically when one is present.

Add repo secret → · Model settings → · Setup docs → · Ask in Discord →

Pullfrog  | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants