fix(lockfile): keep non-derivable tarball URLs when lockfileIncludeTarballUrl is false#11509
Conversation
…ludeTarballUrl is false `lockfile-include-tarball-url` defaults to `false`, so for the vast majority of users the early return added by #10621 silently dropped tarball URLs that cannot be reconstructed from registry+name+version — breaking `pnpm install --frozen-lockfile` from an empty store on GitHub Packages (`https://npm.pkg.github.com/download/<scope>/<name>/<version>/<hash>`), JSR, and similar registries. `false` now matches the historical (v10) heuristic: tarball URLs are written when they are non-reconstructable, otherwise omitted. `true` continues to force every tarball URL into the lockfile. Refs #11276, #11407.
Replace "reconstructable" with "derivable" and avoid the cspell-flagged "mypkg" placeholder in the new test fixture.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR restores a heuristic that preserves tarball URLs in pnpm-lock.yaml when those URLs cannot be derived from package name + version + registry, even if ChangesTarball URL Preservation Heuristic
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR fixes a regression in pnpm v11 lockfile writing where lockfile-include-tarball-url=false (the default) could drop tarball URLs that cannot be reconstructed from name+version+registry, breaking pnpm install --frozen-lockfile on registries with non-standard tarball paths (e.g. GitHub Packages /download/ URLs, JSR).
Changes:
- Removes the
lockfileIncludeTarballUrl === falseearly-return so the “non-derivable tarball” heuristic always runs. - Updates/unit-adds tests to assert non-derivable tarball URLs are preserved even when
lockfileIncludeTarballUrlis false. - Adds a changeset documenting the behavioral fix and affected packages.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| lockfile/utils/src/toLockfileResolution.ts | Restores heuristic behavior to preserve non-derivable tarball URLs even when lockfileIncludeTarballUrl is false. |
| lockfile/utils/test/toLockfileResolution.ts | Updates/adds unit tests to cover non-derivable URLs (including GitHub Packages /download/). |
| installing/deps-installer/test/lockfile.ts | Updates integration test to assert non-standard tarball URLs remain in the written lockfile. |
| .changeset/preserve-non-derivable-tarballs.md | Adds a changeset describing the regression and fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lockfile/utils/src/toLockfileResolution.ts (1)
48-55:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd null check before comparing tarball URLs at line 49.
The non-null assertion
tarball!on line 49 is unsafe. A resolution can reach this code path with anundefinedtarball if:
lockfileIncludeTarballUrlis falsy- The tarball is neither
file:nor from a git hostWhen this occurs,
tarball!.replaceAll(...)crashes with aTypeError. Add a guard before the URL comparison:Suggested fix
const expectedTarball = getNpmTarballUrl(pkg.name, pkg.version, { registry }) + if (tarball == null) { + return { + integrity: resolution['integrity'], + } + } const actualTarball = tarball.replaceAll('%2f', '/')🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lockfile/utils/src/toLockfileResolution.ts` around lines 48 - 55, The code uses tarball! when computing actualTarball and can throw if tarball is undefined; update the block in toLockfileResolution.ts to first check that tarball is non-null/undefined before calling replaceAll and comparing to expectedTarball (i.e., only compute actualTarball = tarball.replaceAll('%2f','/') and call removeProtocol comparison when tarball != null); if tarball is undefined, skip this comparison and proceed (preserve existing behavior for gitHosted by leaving the call to preservingGitHosted(resolutionFields, gitHosted) only when the comparison fails).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@lockfile/utils/src/toLockfileResolution.ts`:
- Around line 48-55: The code uses tarball! when computing actualTarball and can
throw if tarball is undefined; update the block in toLockfileResolution.ts to
first check that tarball is non-null/undefined before calling replaceAll and
comparing to expectedTarball (i.e., only compute actualTarball =
tarball.replaceAll('%2f','/') and call removeProtocol comparison when tarball !=
null); if tarball is undefined, skip this comparison and proceed (preserve
existing behavior for gitHosted by leaving the call to
preservingGitHosted(resolutionFields, gitHosted) only when the comparison
fails).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2a2b6eb0-c00a-4caf-ac2f-60528ffe93fa
📒 Files selected for processing (4)
.changeset/preserve-non-derivable-tarballs.mdinstalling/deps-installer/test/lockfile.tslockfile/utils/src/toLockfileResolution.tslockfile/utils/test/toLockfileResolution.ts
…ution `TarballResolution.tarball` is typed as required, but callers that deserialize resolutions from external state can violate that. Return early with just `integrity` if the tarball URL is missing instead of asserting non-null at the use site (which previously paired a `as string | undefined` cast with `tarball!.replaceAll(...)` — contradictory signals that confused both readers and review tools).
…rballUrl is false (#11509) * fix(lockfile): keep non-reconstructable tarball URLs when lockfileIncludeTarballUrl is false `lockfile-include-tarball-url` defaults to `false`, so for the vast majority of users the early return added by #10621 silently dropped tarball URLs that cannot be reconstructed from registry+name+version — breaking `pnpm install --frozen-lockfile` from an empty store on GitHub Packages (`https://npm.pkg.github.com/download/<scope>/<name>/<version>/<hash>`), JSR, and similar registries. `false` now matches the historical (v10) heuristic: tarball URLs are written when they are non-reconstructable, otherwise omitted. `true` continues to force every tarball URL into the lockfile. Refs #11276, #11407. * chore: appease cspell Replace "reconstructable" with "derivable" and avoid the cspell-flagged "mypkg" placeholder in the new test fixture. * docs(changeset): use camelCase setting name * fix(lockfile): guard against missing tarball field in toLockfileResolution `TarballResolution.tarball` is typed as required, but callers that deserialize resolutions from external state can violate that. Return early with just `integrity` if the tarball URL is missing instead of asserting non-null at the use site (which previously paired a `as string | undefined` cast with `tarball!.replaceAll(...)` — contradictory signals that confused both readers and review tools).
Summary
lockfile-include-tarball-urldefaults tofalse, so for the vast majority of users the early return added by #10621 silently dropped tarball URLs that cannot be derived fromname+version+registry. This breakspnpm install --frozen-lockfilefrom an empty store on registries that serve tarballs from a non-standard path:https://npm.pkg.github.com/download/<scope>/<name>/<version>/<hash>(ERR_PNPM_FETCH_404 on fetches from the github registry #11276, #11407 follow-up)https://npm.jsr.io/~/<n>/<scope>/<name>(pnpm updateinpnpm@10.31breaks JSR packages #10915)After this PR,
lockfileIncludeTarballUrl: falsematches the historical (v10) heuristic — tarball URLs are written when they cannot be reconstructed, otherwise omitted.lockfileIncludeTarballUrl: truecontinues to force every tarball URL into the lockfile.This aligns with the stated rule from @zkochan on #11407:
The fix removes the
lockfileIncludeTarballUrl === falseearly return intoLockfileResolutionso the URL-comparison branch always runs, which is what the v10 implementation did.Test plan
/download/URLs are kept.installing/deps-installer/test/lockfile.tsthat asserted non-standard tarball URLs were dropped.pnpm --filter @pnpm/lockfile.utils test— 11 passedpnpm --filter @pnpm/installing.deps-installer test -- test/lockfile.ts -t "tarball URL"— 5 passedWritten by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit