feat(patching): version ranges and strict patches (#9236)
* feat: replace a config This is a test commit to see if CI fails * feat: `strictPatches` * docs: future instruction * feat: `strictPatches` → `allowPatchFailure` * docs(changeset): correction * test: getOptionsFromRootManifest * fix: allowFailure * feat: groupPatchedDependencies * fix: update code after merge * fix: star spec * feat: error on invalid ranges * fix: eslint * docs: change task * feat(patching): version ranges * fix: `verifyPatches` * refactor: move types to `@pnpm/patching.types` * docs(changeset): add missing package * refactor: move `verifyPatches` to `@pnpm/patching.config` * test: fix * feat: change error message of unused patches * refactor: exact options into an interface * test(patching): version range * test(patching): allowPatchFailure * docs: change wording * docs: change wording * test(patching): version range error * test(patching): legacy behavior * test: don't use star * test(patching): strict versionless * test: strictPatches * chore(deps): `@pnpm/logger` must be peer * docs: fix grammar * refactor: rename `blank` to `all` * refactor: use string * refactor: use array for `PatchGroup.range` * refactor: stop re-exporting `allPatchKeys` * feat: re-export `PatchGroupRangeItem` * refactor: move error creation into a class * docs: replace "versionless" with "name-only" * docs: coherent wordings * test: exact version overrides range conflict * test: tweak * docs: consistent wordings * docs: correct wordings * refactor: rename `allowPatchFailure` to `ignorePatchFailures` * feat: replace `strictPatches` with `ignorePatchFailures` * docs: legacy behavior * feat: introduce `allowUnusedPatches` * docs(changeset): update * docs: remove outdated comment * docs: backward-compatibility
This commit is contained in:
35 files changed
+1525
-101
No files matched your search
@@ -31,10 +31,17 @@
|
||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/patching.types": "workspace:*"
|
||||
"@pnpm/dependency-path": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/patching.types": "workspace:*",
|
||||
"semver": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pnpm/logger": ">=5.1.0 <1001.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/patching.config": "workspace:*"
|
||||
"@pnpm/patching.config": "workspace:*",
|
||||
"@types/semver": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { type PatchGroupRecord } from '@pnpm/patching.types'
|
||||
|
||||
export function * allPatchKeys (patchedDependencies: PatchGroupRecord): Generator<string> {
|
||||
for (const name in patchedDependencies) {
|
||||
const group = patchedDependencies[name]
|
||||
for (const version in group.exact) {
|
||||
yield group.exact[version].key
|
||||
}
|
||||
for (const item of group.range) {
|
||||
yield item.patch.key
|
||||
}
|
||||
if (group.all) {
|
||||
yield group.all.key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { type ExtendedPatchInfo, type PatchGroupRangeItem, type PatchGroupRecord } from '@pnpm/patching.types'
|
||||
import { satisfies } from 'semver'
|
||||
|
||||
class PatchKeyConflictError extends PnpmError {
|
||||
constructor (
|
||||
pkgName: string,
|
||||
pkgVersion: string,
|
||||
satisfied: Array<Pick<PatchGroupRangeItem, 'version'>>
|
||||
) {
|
||||
const pkgId = `${pkgName}@${pkgVersion}`
|
||||
const satisfiedVersions = satisfied.map(({ version }) => version)
|
||||
const message = `Unable to choose between ${satisfied.length} version ranges to patch ${pkgId}: ${satisfiedVersions.join(', ')}`
|
||||
super('PATCH_KEY_CONFLICT', message, {
|
||||
hint: `Explicitly set the exact version (${pkgId}) to resolve conflict`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function getPatchInfo (
|
||||
patchFileGroups: PatchGroupRecord | undefined,
|
||||
pkgName: string,
|
||||
pkgVersion: string
|
||||
): ExtendedPatchInfo | undefined {
|
||||
if (!patchFileGroups?.[pkgName]) return undefined
|
||||
|
||||
const exactVersion = patchFileGroups[pkgName].exact[pkgVersion]
|
||||
if (exactVersion) return exactVersion
|
||||
|
||||
const satisfied = patchFileGroups[pkgName].range.filter(item => satisfies(pkgVersion, item.version))
|
||||
if (satisfied.length > 1) {
|
||||
throw new PatchKeyConflictError(pkgName, pkgVersion, satisfied)
|
||||
}
|
||||
if (satisfied.length === 1) {
|
||||
return satisfied[0].patch
|
||||
}
|
||||
|
||||
return patchFileGroups[pkgName].all
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as dp from '@pnpm/dependency-path'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { type PatchFile, type PatchGroup, type PatchGroupRecord } from '@pnpm/patching.types'
|
||||
import { validRange } from 'semver'
|
||||
|
||||
export function groupPatchedDependencies (patchedDependencies: Record<string, PatchFile>): PatchGroupRecord {
|
||||
const result: PatchGroupRecord = {}
|
||||
function getGroup (name: string): PatchGroup {
|
||||
let group: PatchGroup | undefined = result[name]
|
||||
if (group) return group
|
||||
group = {
|
||||
exact: {},
|
||||
range: [],
|
||||
all: undefined,
|
||||
}
|
||||
result[name] = group
|
||||
return group
|
||||
}
|
||||
|
||||
for (const key in patchedDependencies) {
|
||||
const file = patchedDependencies[key]
|
||||
const { name, version, nonSemverVersion } = dp.parse(key)
|
||||
|
||||
if (name && version) {
|
||||
getGroup(name).exact[version] = { strict: true, file, key }
|
||||
continue
|
||||
}
|
||||
|
||||
if (name && nonSemverVersion) {
|
||||
if (!validRange(nonSemverVersion)) {
|
||||
throw new PnpmError('PATCH_NON_SEMVER_RANGE', `${nonSemverVersion} is not a valid semantic version range.`)
|
||||
}
|
||||
if (nonSemverVersion.trim() === '*') {
|
||||
getGroup(name).all = { strict: true, file, key }
|
||||
} else {
|
||||
getGroup(name).range.push({
|
||||
version: nonSemverVersion,
|
||||
patch: { strict: true, file, key },
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Set `strict` to `false` to preserve backward compatibility.
|
||||
getGroup(key).all = { strict: false, file, key }
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,29 +1,11 @@
|
||||
import { type PatchFile, type PatchInfo } from '@pnpm/patching.types'
|
||||
|
||||
export interface ExtendedPatchInfo extends PatchInfo {
|
||||
key: string
|
||||
}
|
||||
|
||||
export function getPatchInfo (
|
||||
patchedDependencies: Record<string, PatchFile> | undefined,
|
||||
pkgName: string,
|
||||
pkgVersion: string
|
||||
): ExtendedPatchInfo | undefined {
|
||||
if (!patchedDependencies) return undefined
|
||||
const pkgNameAndVersion = `${pkgName}@${pkgVersion}`
|
||||
if (patchedDependencies[pkgNameAndVersion]) {
|
||||
return {
|
||||
file: patchedDependencies[pkgNameAndVersion],
|
||||
key: pkgNameAndVersion,
|
||||
strict: true,
|
||||
}
|
||||
}
|
||||
if (patchedDependencies[pkgName]) {
|
||||
return {
|
||||
file: patchedDependencies[pkgName],
|
||||
key: pkgName,
|
||||
strict: false,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
export {
|
||||
type ExtendedPatchInfo,
|
||||
type PatchFile,
|
||||
type PatchInfo,
|
||||
type PatchGroup,
|
||||
type PatchGroupRangeItem,
|
||||
type PatchGroupRecord,
|
||||
} from '@pnpm/patching.types'
|
||||
export { groupPatchedDependencies } from './groupPatchedDependencies'
|
||||
export { getPatchInfo } from './getPatchInfo'
|
||||
export { type VerifyPatchesOptions, verifyPatches } from './verifyPatches'
|
||||
@@ -0,0 +1,31 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { globalWarn } from '@pnpm/logger'
|
||||
import { type PatchGroupRecord } from '@pnpm/patching.types'
|
||||
import { allPatchKeys } from './allPatchKeys'
|
||||
|
||||
export interface VerifyPatchesOptions {
|
||||
patchedDependencies: PatchGroupRecord
|
||||
appliedPatches: Set<string>
|
||||
allowUnusedPatches: boolean
|
||||
}
|
||||
|
||||
export function verifyPatches ({
|
||||
patchedDependencies,
|
||||
appliedPatches,
|
||||
allowUnusedPatches,
|
||||
}: VerifyPatchesOptions): void {
|
||||
const unusedPatches: string[] = []
|
||||
for (const patchKey of allPatchKeys(patchedDependencies)) {
|
||||
if (!appliedPatches.has(patchKey)) unusedPatches.push(patchKey)
|
||||
}
|
||||
|
||||
if (!unusedPatches.length) return
|
||||
const message = `The following patches were not used: ${unusedPatches.join(', ')}`
|
||||
if (allowUnusedPatches) {
|
||||
globalWarn(message)
|
||||
return
|
||||
}
|
||||
throw new PnpmError('UNUSED_PATCH', message, {
|
||||
hint: 'Either remove them from "patchedDependencies" or update them to match packages in your dependencies.',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import { getPatchInfo } from '../src/getPatchInfo'
|
||||
import { type PatchGroupRecord } from '../src/index'
|
||||
|
||||
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': {
|
||||
file: {
|
||||
path: 'patches/foo@1.0.0.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@1.0.0',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
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: {
|
||||
file: {
|
||||
path: 'patches/foo@1.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@1',
|
||||
strict: true,
|
||||
},
|
||||
}],
|
||||
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: {
|
||||
file: {
|
||||
path: 'patches/foo.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
} 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': {
|
||||
file: {
|
||||
path: 'patches/foo@1.0.0.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@1.0.0',
|
||||
strict: true,
|
||||
},
|
||||
'1.1.0': {
|
||||
file: {
|
||||
path: 'patches/foo@1.1.0.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@1.1.0',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
range: [
|
||||
{
|
||||
version: '1',
|
||||
patch: {
|
||||
file: {
|
||||
path: 'patches/foo@1.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@1',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: '2',
|
||||
patch: {
|
||||
file: {
|
||||
path: 'patches/foo@2.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@2',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
all: {
|
||||
file: {
|
||||
path: 'patches/foo.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
} 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: {
|
||||
file: {
|
||||
path: 'patches/foo_a.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@>=1.0.0 <3.0.0',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: '>=2.0.0',
|
||||
patch: {
|
||||
file: {
|
||||
path: 'patches/foo_b.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@>=2.0.0',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
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': {
|
||||
file: {
|
||||
path: 'patches/foo_a.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@>=1.0.0 <3.0.0',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
range: [
|
||||
{
|
||||
version: '>=1.0.0 <3.0.0',
|
||||
patch: {
|
||||
file: {
|
||||
path: 'patches/foo_b.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@>=1.0.0 <3.0.0',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: '>=2.0.0',
|
||||
patch: {
|
||||
file: {
|
||||
path: 'patches/foo_c.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
},
|
||||
key: 'foo@>=2.0.0',
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
all: undefined,
|
||||
},
|
||||
} satisfies PatchGroupRecord
|
||||
expect(getPatchInfo(patchedDependencies, 'foo', '2.1.0')).toStrictEqual(patchedDependencies.foo.exact['2.1.0'])
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
import { type ExtendedPatchInfo, type PatchFile, type PatchGroupRecord } from '@pnpm/patching.types'
|
||||
import { groupPatchedDependencies } from '../src/groupPatchedDependencies'
|
||||
|
||||
function sanitizePatchGroupRecord (patchGroups: PatchGroupRecord): PatchGroupRecord {
|
||||
for (const name in patchGroups) {
|
||||
patchGroups[name].range.sort((a, b) => a.version.localeCompare(b.version))
|
||||
}
|
||||
return patchGroups
|
||||
}
|
||||
|
||||
const _groupPatchedDependencies: typeof groupPatchedDependencies = patchedDependencies => sanitizePatchGroupRecord(groupPatchedDependencies(patchedDependencies))
|
||||
|
||||
test('groups patchedDependencies according to names, match types, and versions', () => {
|
||||
const patchedDependencies = {
|
||||
'exact-version-only@0.0.0': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/exact-version-only@2.10.patch',
|
||||
},
|
||||
'exact-version-only@1.2.3': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/exact-version-only@1.2.3.patch',
|
||||
},
|
||||
'exact-version-only@2.1.0': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/exact-version-only@2.10.patch',
|
||||
},
|
||||
'version-range-only@~1.2.0': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/version-range-only@~1.2.0.patch',
|
||||
},
|
||||
'version-range-only@4': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/version-range-only@4.patch',
|
||||
},
|
||||
'star-version-range@*': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/star-version-range.patch',
|
||||
},
|
||||
'without-versions': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/without-versions.patch',
|
||||
},
|
||||
'mixed-style@0.1.2': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/mixed-style@0.1.2.patch',
|
||||
},
|
||||
'mixed-style@1.x.x': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/mixed-style@1.x.x.patch',
|
||||
},
|
||||
'mixed-style': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/mixed-style.patch',
|
||||
},
|
||||
} satisfies Record<string, PatchFile>
|
||||
const info = (strict: boolean, key: keyof typeof patchedDependencies): ExtendedPatchInfo => ({
|
||||
strict,
|
||||
key,
|
||||
file: patchedDependencies[key],
|
||||
})
|
||||
expect(_groupPatchedDependencies(patchedDependencies)).toStrictEqual({
|
||||
'exact-version-only': {
|
||||
exact: {
|
||||
'0.0.0': info(true, 'exact-version-only@0.0.0'),
|
||||
'1.2.3': info(true, 'exact-version-only@1.2.3'),
|
||||
'2.1.0': info(true, 'exact-version-only@2.1.0'),
|
||||
},
|
||||
range: [],
|
||||
all: undefined,
|
||||
},
|
||||
'version-range-only': {
|
||||
exact: {},
|
||||
range: [
|
||||
{
|
||||
version: '~1.2.0',
|
||||
patch: info(true, 'version-range-only@~1.2.0'),
|
||||
},
|
||||
{
|
||||
version: '4',
|
||||
patch: info(true, 'version-range-only@4'),
|
||||
},
|
||||
],
|
||||
all: undefined,
|
||||
},
|
||||
'star-version-range': {
|
||||
exact: {},
|
||||
range: [],
|
||||
all: info(true, 'star-version-range@*'),
|
||||
},
|
||||
'without-versions': {
|
||||
exact: {},
|
||||
range: [],
|
||||
all: info(false, 'without-versions'),
|
||||
},
|
||||
'mixed-style': {
|
||||
exact: {
|
||||
'0.1.2': info(true, 'mixed-style@0.1.2'),
|
||||
},
|
||||
range: [
|
||||
{
|
||||
version: '1.x.x',
|
||||
patch: info(true, 'mixed-style@1.x.x'),
|
||||
},
|
||||
],
|
||||
all: info(false, 'mixed-style'),
|
||||
},
|
||||
} as PatchGroupRecord)
|
||||
})
|
||||
|
||||
test('errors on invalid version range', async () => {
|
||||
expect(() => _groupPatchedDependencies({
|
||||
'foo@link:packages/foo': {
|
||||
hash: '00000000000000000000000000000000',
|
||||
path: 'patches/foo.patch',
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
code: 'ERR_PNPM_PATCH_NON_SEMVER_RANGE',
|
||||
}))
|
||||
})
|
||||
@@ -1,11 +1,15 @@
|
||||
import { getPatchInfo } from '../src/index'
|
||||
import { type PatchFile } from '@pnpm/patching.types'
|
||||
import { getPatchInfo, groupPatchedDependencies } from '../src/index'
|
||||
|
||||
const _getPatchInfo = (patchedDependencies: Record<string, PatchFile>, name: string, version: string) =>
|
||||
getPatchInfo(groupPatchedDependencies(patchedDependencies), name, version)
|
||||
|
||||
test('getPatchInfo(undefined, ...) returns undefined', () => {
|
||||
expect(getPatchInfo(undefined, 'foo', '1.0.0')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('getPatchInfo(_, name, version) returns strict=true if name@version exists', () => {
|
||||
expect(getPatchInfo({
|
||||
expect(_getPatchInfo({
|
||||
'foo@1.0.0': {
|
||||
path: 'patches/foo@1.0.0.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
@@ -21,7 +25,7 @@ test('getPatchInfo(_, name, version) returns strict=true if name@version exists'
|
||||
})
|
||||
|
||||
test('getPatchInfo(_, name, version) returns strict=false if name exists and name@version does not exist', () => {
|
||||
expect(getPatchInfo({
|
||||
expect(_getPatchInfo({
|
||||
foo: {
|
||||
path: 'patches/foo.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
@@ -37,7 +41,7 @@ test('getPatchInfo(_, name, version) returns strict=false if name exists and nam
|
||||
})
|
||||
|
||||
test('getPatchInfo(_, name, version) prioritizes name@version over name if both exist', () => {
|
||||
expect(getPatchInfo({
|
||||
expect(_getPatchInfo({
|
||||
foo: {
|
||||
path: 'patches/foo.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
@@ -57,7 +61,7 @@ test('getPatchInfo(_, name, version) prioritizes name@version over name if both
|
||||
})
|
||||
|
||||
test('getPatchInfo(_, name, version) does not access wrong name', () => {
|
||||
expect(getPatchInfo({
|
||||
expect(_getPatchInfo({
|
||||
'bar@1.0.0': {
|
||||
path: 'patches/bar@1.0.0.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
@@ -66,7 +70,7 @@ test('getPatchInfo(_, name, version) does not access wrong name', () => {
|
||||
})
|
||||
|
||||
test('getPatchInfo(_, name, version) does not access wrong version', () => {
|
||||
expect(getPatchInfo({
|
||||
expect(_getPatchInfo({
|
||||
'foo@2.0.0': {
|
||||
path: 'patches/foo@2.0.0.patch',
|
||||
hash: '00000000000000000000000000000000',
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/dependency-path"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/error"
|
||||
},
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,30 @@ export interface PatchFile {
|
||||
hash: string
|
||||
}
|
||||
|
||||
// TODO: replace all occurrences of PatchInfo with PatchFile before the next major version is released
|
||||
export interface PatchInfo {
|
||||
strict: boolean
|
||||
file: PatchFile
|
||||
}
|
||||
|
||||
export interface ExtendedPatchInfo extends PatchInfo {
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface PatchGroupRangeItem {
|
||||
version: string
|
||||
patch: ExtendedPatchInfo
|
||||
}
|
||||
|
||||
/** A group of {@link ExtendedPatchInfo}s which correspond to a package name. */
|
||||
export interface PatchGroup {
|
||||
/** Maps exact versions to {@link ExtendedPatchInfo}. */
|
||||
exact: Record<string, ExtendedPatchInfo>
|
||||
/** Pairs of version ranges and {@link ExtendedPatchInfo}. */
|
||||
range: PatchGroupRangeItem[]
|
||||
/** The {@link ExtendedPatchInfo} without exact versions or version ranges. */
|
||||
all: ExtendedPatchInfo | undefined
|
||||
}
|
||||
|
||||
/** Maps package names to their corresponding groups. */
|
||||
export type PatchGroupRecord = Record<string, PatchGroup>
|
||||
Reference in new issue
Block a user