mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-14 11:35:56 -04:00
fix(lockfile): keep non-derivable tarball URLs when lockfileIncludeTarballUrl 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).
This commit is contained in:
6
.changeset/preserve-non-derivable-tarballs.md
Normal file
6
.changeset/preserve-non-derivable-tarballs.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/lockfile.utils": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Restored the heuristic that preserves tarball URLs in `pnpm-lock.yaml` when they cannot be derived from name+version+registry, even with the default `lockfileIncludeTarballUrl: false`. Without this, `pnpm install --frozen-lockfile` from an empty store fails with `ERR_PNPM_FETCH_404` for packages on registries that serve tarballs from a non-standard path — most notably GitHub Packages (`https://npm.pkg.github.com/download/<scope>/<name>/<version>/<hash>`) and JSR. `lockfileIncludeTarballUrl: true` continues to force the URL into the lockfile for every package [#11276](https://github.com/pnpm/pnpm/issues/11276).
|
||||
@@ -1464,7 +1464,11 @@ test('exclude tarball URL when lockfileIncludeTarballUrl is false', async () =>
|
||||
.toBeUndefined()
|
||||
})
|
||||
|
||||
test('exclude non-standard tarball URL when lockfileIncludeTarballUrl is false', async () => {
|
||||
test('keep non-standard tarball URL when lockfileIncludeTarballUrl is false', async () => {
|
||||
// Tarball URLs that cannot be reconstructed from name+version+registry must
|
||||
// remain in the lockfile even when `lockfileIncludeTarballUrl: false`,
|
||||
// otherwise `--frozen-lockfile` installs from an empty store will 404.
|
||||
// See https://github.com/pnpm/pnpm/issues/11276.
|
||||
const project = prepareEmpty()
|
||||
|
||||
await addDependenciesToPackage({}, ['esprima-fb@3001.1.0-dev-harmony-fb'], testDefaults({ fastUnpack: false, lockfileIncludeTarballUrl: false }))
|
||||
@@ -1472,7 +1476,7 @@ test('exclude non-standard tarball URL when lockfileIncludeTarballUrl is false',
|
||||
const lockfile = project.readLockfile()
|
||||
|
||||
expect((lockfile.packages['esprima-fb@3001.1.0-dev-harmony-fb'].resolution as TarballResolution).tarball)
|
||||
.toBeUndefined()
|
||||
.toBe(`http://localhost:${REGISTRY_MOCK_PORT}/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz`)
|
||||
})
|
||||
|
||||
test('lockfile v6', async () => {
|
||||
|
||||
@@ -14,12 +14,18 @@ export function toLockfileResolution (
|
||||
if (resolution.type !== undefined || !resolution['integrity']) {
|
||||
return resolution as LockfileResolution
|
||||
}
|
||||
// Tarball-typed resolutions are guaranteed to carry a tarball URL by the
|
||||
// resolver, but guard for unexpected inputs (e.g. resolutions deserialized
|
||||
// from external state) so we don't blow up on a missing field.
|
||||
const tarball = resolution['tarball'] as string | undefined
|
||||
if (tarball == null) {
|
||||
return { integrity: resolution['integrity'] }
|
||||
}
|
||||
// Honor the resolver-supplied flag, with a URL fallback for resolutions
|
||||
// that didn't go through the git resolver (e.g. config-dep migrations or
|
||||
// legacy lockfiles read by callers that don't enrich the field).
|
||||
const gitHosted = (resolution as TarballResolution).gitHosted === true ||
|
||||
(tarball != null && isGitHostedTarballUrl(tarball))
|
||||
isGitHostedTarballUrl(tarball)
|
||||
if (lockfileIncludeTarballUrl) {
|
||||
return preservingGitHosted({
|
||||
integrity: resolution['integrity'],
|
||||
@@ -30,22 +36,23 @@ export function toLockfileResolution (
|
||||
// and registry must always stay in the lockfile, otherwise the package can
|
||||
// no longer be re-fetched. This covers local `file:` tarballs and tarballs
|
||||
// served by git providers (GitHub, GitLab, Bitbucket).
|
||||
if (tarball != null && (tarball.startsWith('file:') || gitHosted)) {
|
||||
if (tarball.startsWith('file:') || gitHosted) {
|
||||
return preservingGitHosted({
|
||||
integrity: resolution['integrity'],
|
||||
tarball,
|
||||
}, gitHosted)
|
||||
}
|
||||
if (lockfileIncludeTarballUrl === false) {
|
||||
return {
|
||||
integrity: resolution['integrity'],
|
||||
}
|
||||
}
|
||||
// Sometimes packages are hosted under non-standard tarball URLs.
|
||||
// For instance, when they are hosted on npm Enterprise. See https://github.com/pnpm/pnpm/issues/867
|
||||
// Or in other weird cases, like https://github.com/pnpm/pnpm/issues/1072
|
||||
// Or in other weird cases, like https://github.com/pnpm/pnpm/issues/1072.
|
||||
// Even when the user explicitly sets `lockfileIncludeTarballUrl: false`, we
|
||||
// must preserve such URLs — otherwise the package cannot be re-fetched on a
|
||||
// frozen-lockfile install (e.g. GitHub Packages tarballs at
|
||||
// `https://npm.pkg.github.com/download/<scope>/<name>/<version>/<hash>`).
|
||||
// `lockfileIncludeTarballUrl` only controls whether URLs that *can* be
|
||||
// derived from name+version+registry are written.
|
||||
const expectedTarball = getNpmTarballUrl(pkg.name, pkg.version, { registry })
|
||||
const actualTarball = tarball!.replaceAll('%2f', '/')
|
||||
const actualTarball = tarball.replaceAll('%2f', '/')
|
||||
if (removeProtocol(expectedTarball) !== removeProtocol(actualTarball)) {
|
||||
return preservingGitHosted({
|
||||
integrity: resolution['integrity'],
|
||||
|
||||
@@ -36,7 +36,11 @@ test('drops the tarball for standard registry URLs when lockfileIncludeTarballUr
|
||||
})
|
||||
})
|
||||
|
||||
test('drops the tarball for non-standard registry URLs when lockfileIncludeTarballUrl is false', () => {
|
||||
test('keeps the tarball for non-standard registry URLs when lockfileIncludeTarballUrl is false', () => {
|
||||
// A tarball URL whose host doesn't match the configured registry cannot be
|
||||
// reconstructed from name+version+registry, so dropping it would break
|
||||
// re-fetching on `--frozen-lockfile`. `lockfileIncludeTarballUrl: false`
|
||||
// only suppresses URLs that *can* be reconstructed.
|
||||
expect(toLockfileResolution(
|
||||
{ name: 'esprima-fb', version: '3001.1.0-dev-harmony-fb' },
|
||||
{ integrity: 'sha512-AAAA', tarball: 'https://example.com/esprima-fb/-/esprima-fb-3001.1.0-dev-harmony-fb.tgz' },
|
||||
@@ -44,6 +48,22 @@ test('drops the tarball for non-standard registry URLs when lockfileIncludeTarba
|
||||
false
|
||||
)).toEqual({
|
||||
integrity: 'sha512-AAAA',
|
||||
tarball: 'https://example.com/esprima-fb/-/esprima-fb-3001.1.0-dev-harmony-fb.tgz',
|
||||
})
|
||||
})
|
||||
|
||||
test('keeps GitHub Packages /download/ tarball URLs when lockfileIncludeTarballUrl is false', () => {
|
||||
// GitHub Packages serves tarballs at /download/<scope>/<name>/<version>/<hash>,
|
||||
// which cannot be derived from name+version+registry. See
|
||||
// https://github.com/pnpm/pnpm/issues/11276.
|
||||
expect(toLockfileResolution(
|
||||
{ name: '@example/private', version: '1.2.3' },
|
||||
{ integrity: 'sha512-AAAA', tarball: 'https://npm.pkg.github.com/download/@example/private/1.2.3/0123456789abcdef0123456789abcdef01234567' },
|
||||
'https://npm.pkg.github.com/',
|
||||
false
|
||||
)).toEqual({
|
||||
integrity: 'sha512-AAAA',
|
||||
tarball: 'https://npm.pkg.github.com/download/@example/private/1.2.3/0123456789abcdef0123456789abcdef01234567',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user