Files
pnpm/lockfile/utils/test/toLockfileResolution.ts
Zoltan Kochan b682b6e81e 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).
2026-05-07 08:14:55 +02:00

120 lines
4.3 KiB
TypeScript

import { expect, test } from '@jest/globals'
import { toLockfileResolution } from '@pnpm/lockfile.utils'
const REGISTRY = 'https://registry.npmjs.org/'
test('keeps the tarball when lockfileIncludeTarballUrl is true', () => {
expect(toLockfileResolution(
{ name: 'foo', version: '1.0.0' },
{ integrity: 'sha512-AAAA', tarball: 'https://registry.npmjs.org/foo/-/foo-1.0.0.tgz' },
REGISTRY,
true
)).toEqual({
integrity: 'sha512-AAAA',
tarball: 'https://registry.npmjs.org/foo/-/foo-1.0.0.tgz',
})
})
test('drops the tarball for standard registry URLs by default', () => {
expect(toLockfileResolution(
{ name: 'foo', version: '1.0.0' },
{ integrity: 'sha512-AAAA', tarball: 'https://registry.npmjs.org/foo/-/foo-1.0.0.tgz' },
REGISTRY
)).toEqual({
integrity: 'sha512-AAAA',
})
})
test('drops the tarball for standard registry URLs when lockfileIncludeTarballUrl is false', () => {
expect(toLockfileResolution(
{ name: 'foo', version: '1.0.0' },
{ integrity: 'sha512-AAAA', tarball: 'https://registry.npmjs.org/foo/-/foo-1.0.0.tgz' },
REGISTRY,
false
)).toEqual({
integrity: 'sha512-AAAA',
})
})
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' },
REGISTRY,
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',
})
})
test('keeps file: tarballs even when lockfileIncludeTarballUrl is false', () => {
// file: tarballs cannot be reconstructed from name+version+registry, so the
// tarball field must remain so the package can be re-fetched on install.
expect(toLockfileResolution(
{ name: 'test-package', version: '1.0.0' },
{ integrity: 'sha512-AAAA', tarball: 'file:test-package-1.0.0.tgz' },
REGISTRY,
false
)).toEqual({
integrity: 'sha512-AAAA',
tarball: 'file:test-package-1.0.0.tgz',
})
})
test('keeps file: tarballs even when lockfileIncludeTarballUrl is undefined', () => {
expect(toLockfileResolution(
{ name: 'test-package', version: '1.0.0' },
{ integrity: 'sha512-AAAA', tarball: 'file:test-package-1.0.0.tgz' },
REGISTRY
)).toEqual({
integrity: 'sha512-AAAA',
tarball: 'file:test-package-1.0.0.tgz',
})
})
test('keeps git-hosted tarballs when lockfileIncludeTarballUrl is false', () => {
expect(toLockfileResolution(
{ name: 'foo', version: '1.0.0' },
{ integrity: 'sha512-AAAA', tarball: 'https://codeload.github.com/foo/bar/tar.gz/abcdef' },
REGISTRY,
false
)).toEqual({
integrity: 'sha512-AAAA',
tarball: 'https://codeload.github.com/foo/bar/tar.gz/abcdef',
gitHosted: true,
})
})
test('records gitHosted on the lockfile entry when set on the resolution', () => {
expect(toLockfileResolution(
{ name: 'foo', version: '1.0.0' },
{ integrity: 'sha512-AAAA', tarball: 'https://codeload.github.com/foo/bar/tar.gz/abcdef', gitHosted: true },
REGISTRY,
true
)).toEqual({
integrity: 'sha512-AAAA',
tarball: 'https://codeload.github.com/foo/bar/tar.gz/abcdef',
gitHosted: true,
})
})