Files
pnpm/lockfile/merger/test/index.ts
Vamsik 659e0ea0cc fix(lockfile): handle non-semver versions in lockfile merger without crashing (#11102)
* fix(lockfile): handle non-semver versions in lockfile merger without crashing

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 15:21:23 +01:00

390 lines
10 KiB
TypeScript

import type { LockfileObject } from '@pnpm/lockfile.types'
import type { DepPath, ProjectId } from '@pnpm/types'
import { mergeLockfileChanges } from '../src/index.js'
const simpleLockfile = {
importers: {
'.': {
dependencies: {
foo: '1.0.0',
},
specifiers: {
foo: '1.0.0',
},
},
},
lockfileVersion: '5.2',
packages: {
'/foo@1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
}
test('picks the newer version when dependencies differ inside importer', () => {
const mergedLockfile = mergeLockfileChanges(
{
...simpleLockfile,
importers: {
['.' as ProjectId]: {
...simpleLockfile.importers['.'],
dependencies: {
foo: '1.2.0',
bar: '3.0.0(qar@1.0.0)',
zoo: '4.0.0(qar@1.0.0)',
},
},
},
},
{
...simpleLockfile,
importers: {
['.' as ProjectId]: {
...simpleLockfile.importers['.'],
dependencies: {
foo: '1.1.0',
bar: '4.0.0(qar@1.0.0)',
zoo: '3.0.0(qar@1.0.0)',
},
},
},
}
)
expect(mergedLockfile.importers['.' as ProjectId].dependencies?.foo).toBe('1.2.0')
expect(mergedLockfile.importers['.' as ProjectId].dependencies?.bar).toBe('4.0.0(qar@1.0.0)')
expect(mergedLockfile.importers['.' as ProjectId].dependencies?.zoo).toBe('4.0.0(qar@1.0.0)')
})
test('picks the newer version when dependencies differ inside package', () => {
const base: LockfileObject = {
importers: {
['.' as ProjectId]: {
dependencies: {
a: '1.0.0',
},
specifiers: {},
},
},
lockfileVersion: '5.2',
packages: {
['/a@1.0.0' as DepPath]: {
dependencies: {
foo: '1.0.0',
},
resolution: {
integrity: '',
},
},
['/foo@1.0.0' as DepPath]: {
resolution: {
integrity: '',
},
},
},
}
const mergedLockfile = mergeLockfileChanges(
{
...base,
packages: {
...base.packages,
['/a@1.0.0' as DepPath]: {
dependencies: {
linked: 'link:../1',
foo: '1.2.0',
bar: '3.0.0(qar@1.0.0)',
zoo: '4.0.0(qar@1.0.0)',
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
['/bar@3.0.0(qar@1.0.0)' as DepPath]: {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
['/zoo@4.0.0(qar@1.0.0)' as DepPath]: {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
['/foo@1.2.0' as DepPath]: {
resolution: {
integrity: '',
},
},
['/qar@1.0.0' as DepPath]: {
resolution: {
integrity: '',
},
},
},
},
{
...base,
packages: {
...base.packages,
['/a@1.0.0' as DepPath]: {
dependencies: {
linked: 'link:../1',
foo: '1.1.0',
bar: '4.0.0(qar@1.0.0)',
zoo: '3.0.0(qar@1.0.0)',
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
['/bar@4.0.0(qar@1.0.0)' as DepPath]: {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
['/zoo@3.0.0(qar@1.0.0)' as DepPath]: {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: '',
},
},
['/foo@1.1.0' as DepPath]: {
resolution: {
integrity: '',
},
},
['/qar@1.0.0' as DepPath]: {
resolution: {
integrity: '',
},
},
},
}
)
expect(mergedLockfile.packages?.['/a@1.0.0' as DepPath].dependencies?.linked).toBe('link:../1')
expect(mergedLockfile.packages?.['/a@1.0.0' as DepPath].dependencies?.foo).toBe('1.2.0')
expect(mergedLockfile.packages?.['/a@1.0.0' as DepPath].dependencies?.bar).toBe('4.0.0(qar@1.0.0)')
expect(mergedLockfile.packages?.['/a@1.0.0' as DepPath].dependencies?.zoo).toBe('4.0.0(qar@1.0.0)')
expect(Object.keys(mergedLockfile.packages ?? {}).sort()).toStrictEqual([
'/a@1.0.0',
'/bar@3.0.0(qar@1.0.0)',
'/bar@4.0.0(qar@1.0.0)',
'/foo@1.0.0',
'/foo@1.1.0',
'/foo@1.2.0',
'/qar@1.0.0',
'/zoo@3.0.0(qar@1.0.0)',
'/zoo@4.0.0(qar@1.0.0)',
])
})
test('prefers our lockfile resolutions when it has newer packages', () => {
const mergedLockfile = mergeLockfileChanges(
{
...simpleLockfile,
packages: {
['/foo@1.0.0' as DepPath]: {
dependencies: {
bar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
['/bar@1.0.0' as DepPath]: {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
},
{
...simpleLockfile,
packages: {
['/foo@1.0.0' as DepPath]: {
dependencies: {
bar: '1.1.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
['/bar@1.1.0' as DepPath]: {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
['/qar@1.0.0' as DepPath]: {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
}
)
expect(mergedLockfile).toStrictEqual({
...simpleLockfile,
packages: {
'/foo@1.0.0': {
dependencies: {
bar: '1.1.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar@1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar@1.1.0': {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/qar@1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
})
})
test('prefers our lockfile resolutions when it has newer packages #2', () => {
const mergedLockfile = mergeLockfileChanges(
{
...simpleLockfile,
packages: {
['/foo@1.0.0' as DepPath]: {
dependencies: {
bar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
['/bar@1.0.0' as DepPath]: {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
},
{
...simpleLockfile,
packages: {
['/foo@1.0.0' as DepPath]: {
dependencies: {
bar: '1.1.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
['/bar@1.1.0' as DepPath]: {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
['/qar@1.0.0' as DepPath]: {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
}
)
expect(mergedLockfile).toStrictEqual({
...simpleLockfile,
packages: {
'/foo@1.0.0': {
dependencies: {
bar: '1.1.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar@1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/bar@1.1.0': {
dependencies: {
qar: '1.0.0',
},
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
'/qar@1.0.0': {
resolution: {
integrity: 'sha512-aBVzCAzfyApU0gg36QgCpJixGtYwuQ4djrn11J+DTB5vE4OmBPuZiulgTCA9ByULgVAyNV2CTpjjvZmxzukSLw==',
},
},
},
})
})
test('does not crash when merging non-semver versions (link: protocol)', () => {
const base: LockfileObject = {
importers: {
['.' as ProjectId]: {
dependencies: { a: '1.0.0' },
specifiers: {},
},
},
lockfileVersion: '5.2',
packages: {
['/a@1.0.0' as DepPath]: {
dependencies: { linked: 'link:../pkg1' },
resolution: { integrity: '' },
},
},
}
const mergedLockfile = mergeLockfileChanges(
base,
{
...base,
packages: {
['/a@1.0.0' as DepPath]: {
dependencies: { linked: 'link:../pkg2' },
resolution: { integrity: '' },
},
},
}
)
// Should not crash and should pick theirs (the incoming change)
expect(mergedLockfile.packages?.['/a@1.0.0' as DepPath].dependencies?.linked).toBe('link:../pkg2')
})