Files
pnpm/patching/config/test/getPatchInfo.test.ts
Zoltan Kochan aeb06caae9 refactor: simplify patchedDependencies lockfile format (#10911)
* refactor: simplify patchedDependencies lockfile format to map selectors to hashes

Remove the `path` field from patchedDependencies in the lockfile, changing the
format from `Record<string, { path: string, hash: string }>` to
`Record<string, string>` (selector → hash). The path was never consumed from
the lockfile — patch file paths come from user config, not the lockfile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: migrate old patchedDependencies format when reading lockfile

When reading a lockfile with the old `{ path, hash }` format for
patchedDependencies, extract just the hash string. This ensures
backwards compatibility with existing lockfiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: carry patchFilePath through patch groups for runtime patch application

The previous commit removed `path` from the lockfile format but also
accidentally dropped it from the runtime PatchInfo type. This broke
patch application since `applyPatchToDir` needs the file path.

- Add optional `patchFilePath` to `PatchInfo` for runtime use
- Build patch groups with resolved file paths in install
- Fix `build-modules` to use `patchFilePath` instead of `file.path`
- Fix `calcPatchHashes` call site in `checkDepsStatus` (extra arg)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update remaining references to old PatchFile type

- Update getPatchInfo tests to use { hash, key } instead of { file, key }
- Fix createDeployFiles to handle patchedDependencies as hash strings
- Fix configurationalDependencies test assertion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: throw when patch exists but patchFilePath is missing

Also guard against undefined patchedDependencies entry when
ignorePackageManifest is true.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: don't join lockfileDir with already-absolute patch file paths

opts.patchedDependencies values are already absolute paths, so
path.join(opts.lockfileDir, absolutePath) created invalid doubled
paths like /project/home/runner/work/pnpm/...

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use path.resolve for patch file paths and address Copilot review

- Use path.resolve instead of path.join to correctly handle both
  relative and absolute patch file paths
- Use PnpmError instead of plain Error for missing patch file path
- Only copy patchedDependencies to deploy output when manifest
  provides the patch file paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pass rootProjectManifest in deploy patchedDependencies test

The test was missing rootProjectManifest, so createDeployFiles could
not find the manifest's patchedDependencies to propagate to the
deploy output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:26:48 +01:00

168 lines
5.7 KiB
TypeScript

import { getPatchInfo } from '../src/getPatchInfo.js'
import type { PatchGroupRecord } from '../src/index.js'
test('getPatchInfo(undefined, ...) returns undefined', () => {
expect(getPatchInfo(undefined, 'foo', '1.0.0')).toBeUndefined()
})
test('getPatchInfo() returns an exact version patch if the name and version match', () => {
const patchedDependencies = {
foo: {
exact: {
'1.0.0': {
hash: '00000000000000000000000000000000',
key: 'foo@1.0.0',
},
},
range: [],
all: undefined,
},
} satisfies PatchGroupRecord
expect(getPatchInfo(patchedDependencies, 'foo', '1.0.0')).toStrictEqual(patchedDependencies.foo.exact['1.0.0'])
expect(getPatchInfo(patchedDependencies, 'foo', '1.1.0')).toBeUndefined()
expect(getPatchInfo(patchedDependencies, 'foo', '2.0.0')).toBeUndefined()
expect(getPatchInfo(patchedDependencies, 'bar', '1.0.0')).toBeUndefined()
})
test('getPatchInfo() returns a range version patch if the name matches and the version satisfied', () => {
const patchedDependencies = {
foo: {
exact: {},
range: [{
version: '1',
patch: {
hash: '00000000000000000000000000000000',
key: 'foo@1',
},
}],
all: undefined,
},
} satisfies PatchGroupRecord
expect(getPatchInfo(patchedDependencies, 'foo', '1.0.0')).toStrictEqual(patchedDependencies.foo.range[0].patch)
expect(getPatchInfo(patchedDependencies, 'foo', '1.1.0')).toStrictEqual(patchedDependencies.foo.range[0].patch)
expect(getPatchInfo(patchedDependencies, 'foo', '2.0.0')).toBeUndefined()
expect(getPatchInfo(patchedDependencies, 'bar', '1.0.0')).toBeUndefined()
})
test('getPatchInfo() returns name-only patch if the name matches', () => {
const patchedDependencies = {
foo: {
exact: {},
range: [],
all: {
hash: '00000000000000000000000000000000',
key: 'foo',
},
},
} satisfies PatchGroupRecord
expect(getPatchInfo(patchedDependencies, 'foo', '1.0.0')).toStrictEqual(patchedDependencies.foo.all)
expect(getPatchInfo(patchedDependencies, 'foo', '1.1.0')).toStrictEqual(patchedDependencies.foo.all)
expect(getPatchInfo(patchedDependencies, 'foo', '2.0.0')).toStrictEqual(patchedDependencies.foo.all)
expect(getPatchInfo(patchedDependencies, 'bar', '1.0.0')).toBeUndefined()
})
test('exact version patches override version range patches, version range patches override name-only patches', () => {
const patchedDependencies = {
foo: {
exact: {
'1.0.0': {
hash: '00000000000000000000000000000000',
key: 'foo@1.0.0',
},
'1.1.0': {
hash: '00000000000000000000000000000000',
key: 'foo@1.1.0',
},
},
range: [
{
version: '1',
patch: {
hash: '00000000000000000000000000000000',
key: 'foo@1',
},
},
{
version: '2',
patch: {
hash: '00000000000000000000000000000000',
key: 'foo@2',
},
},
],
all: {
hash: '00000000000000000000000000000000',
key: 'foo',
},
},
} satisfies PatchGroupRecord
expect(getPatchInfo(patchedDependencies, 'foo', '1.0.0')).toStrictEqual(patchedDependencies.foo.exact['1.0.0'])
expect(getPatchInfo(patchedDependencies, 'foo', '1.1.0')).toStrictEqual(patchedDependencies.foo.exact['1.1.0'])
expect(getPatchInfo(patchedDependencies, 'foo', '1.1.1')).toStrictEqual(patchedDependencies.foo.range[0].patch)
expect(getPatchInfo(patchedDependencies, 'foo', '2.0.0')).toStrictEqual(patchedDependencies.foo.range[1].patch)
expect(getPatchInfo(patchedDependencies, 'foo', '2.1.0')).toStrictEqual(patchedDependencies.foo.range[1].patch)
expect(getPatchInfo(patchedDependencies, 'foo', '3.0.0')).toStrictEqual(patchedDependencies.foo.all)
expect(getPatchInfo(patchedDependencies, 'bar', '1.0.0')).toBeUndefined()
})
test('getPatchInfo(_, name, version) throws an error when name@version matches more than one version range patches', () => {
const patchedDependencies = {
foo: {
exact: {},
range: [
{
version: '>=1.0.0 <3.0.0',
patch: {
hash: '00000000000000000000000000000000',
key: 'foo@>=1.0.0 <3.0.0',
},
},
{
version: '>=2.0.0',
patch: {
hash: '00000000000000000000000000000000',
key: 'foo@>=2.0.0',
},
},
],
all: undefined,
},
} satisfies PatchGroupRecord
expect(() => getPatchInfo(patchedDependencies, 'foo', '2.1.0')).toThrow(expect.objectContaining({
code: 'ERR_PNPM_PATCH_KEY_CONFLICT',
message: 'Unable to choose between 2 version ranges to patch foo@2.1.0: >=1.0.0 <3.0.0, >=2.0.0',
hint: 'Explicitly set the exact version (foo@2.1.0) to resolve conflict',
}))
})
test('getPatchInfo(_, name, version) does not throw an error when name@version matches an exact version patch and more than one version range patches', () => {
const patchedDependencies = {
foo: {
exact: {
'2.1.0': {
hash: '00000000000000000000000000000000',
key: 'foo@>=1.0.0 <3.0.0',
},
},
range: [
{
version: '>=1.0.0 <3.0.0',
patch: {
hash: '00000000000000000000000000000000',
key: 'foo@>=1.0.0 <3.0.0',
},
},
{
version: '>=2.0.0',
patch: {
hash: '00000000000000000000000000000000',
key: 'foo@>=2.0.0',
},
},
],
all: undefined,
},
} satisfies PatchGroupRecord
expect(getPatchInfo(patchedDependencies, 'foo', '2.1.0')).toStrictEqual(patchedDependencies.foo.exact['2.1.0'])
})