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
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
"@pnpm/resolve-dependencies": major
|
||||||
|
"@pnpm/headless": major
|
||||||
|
"@pnpm/deps.graph-builder": major
|
||||||
|
"@pnpm/patching.config": major
|
||||||
|
"@pnpm/patching.types": minor
|
||||||
|
"@pnpm/core": patch
|
||||||
|
"pnpm": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add an ability to patch dependencies by version ranges. Exact versions override version ranges, which in turn override name-only patches. Version range `*` is the same as name-only, except that patch application failure will not be ignored.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
patchedDependencies:
|
||||||
|
foo: patches/foo-1.patch
|
||||||
|
foo@^2.0.0: patches/foo-2.patch
|
||||||
|
foo@2.1.0: patches/foo-3.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
The above configuration would apply `patches/foo-3.patch` to `foo@2.1.0`, `patches/foo-2.patch` to all `foo` versions which satisfy `^2.0.0` except `2.1.0`, and `patches/foo-1.patch` to the remaining `foo` versions.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> The version ranges should not overlap. If you want to specialize a sub range, make sure to exclude it from the other keys. For example:
|
||||||
|
>
|
||||||
|
> ```yaml
|
||||||
|
> # pnpm-workspace.yaml
|
||||||
|
> patchedDependencies:
|
||||||
|
> # the specialized sub range
|
||||||
|
> 'foo@2.2.0-2.8.0': patches/foo.2.2.0-2.8.0.patch
|
||||||
|
> # the more general patch, excluding the sub range above
|
||||||
|
> 'foo@>=2.0.0 <2.2.0 || >2.8.0': 'patches/foo.gte2.patch
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> In most cases, however, it's sufficient to just define an exact version to override the range.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
"@pnpm/resolve-dependencies": major
|
||||||
|
"@pnpm/config": minor
|
||||||
|
"@pnpm/core": major
|
||||||
|
"@pnpm/types": minor
|
||||||
|
"@pnpm/headless": minor
|
||||||
|
"@pnpm/patching.config": minor
|
||||||
|
"@pnpm/patching.types": patch
|
||||||
|
"@pnpm/plugin-commands-script-runners": patch
|
||||||
|
"@pnpm/build-modules": minor
|
||||||
|
"pnpm": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Rename `pnpm.allowNonAppliedPatches` to `pnpm.allowUnusedPatches`. The old name is still supported but it would print a deprecation warning message.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
"@pnpm/resolve-dependencies": major
|
||||||
|
"@pnpm/config": minor
|
||||||
|
"@pnpm/core": major
|
||||||
|
"@pnpm/types": minor
|
||||||
|
"@pnpm/headless": minor
|
||||||
|
"@pnpm/patching.config": minor
|
||||||
|
"@pnpm/patching.types": patch
|
||||||
|
"@pnpm/plugin-commands-script-runners": patch
|
||||||
|
"@pnpm/build-modules": minor
|
||||||
|
"pnpm": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `pnpm.ignorePatchFailures` to manage whether pnpm would ignore patch application failures.
|
||||||
|
|
||||||
|
If `ignorePatchFailures` is not set, pnpm would throw an error when patches with exact versions or version ranges fail to apply, and it would ignore failures from name-only patches.
|
||||||
|
|
||||||
|
If `ignorePatchFailures` is explicitly set to `false`, pnpm would throw an error when any type of patch fails to apply.
|
||||||
|
|
||||||
|
If `ignorePatchFailures` is explicitly set to `true`, pnpm would print a warning when any type of patch fails to apply.
|
||||||
@@ -59,6 +59,9 @@
|
|||||||
"realpath-missing": "catalog:",
|
"realpath-missing": "catalog:",
|
||||||
"which": "catalog:"
|
"which": "catalog:"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@pnpm/logger": ">=5.1.0 <1001.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pnpm/config": "workspace:*",
|
"@pnpm/config": "workspace:*",
|
||||||
"@pnpm/prepare": "workspace:*",
|
"@pnpm/prepare": "workspace:*",
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ import {
|
|||||||
type PnpmSettings,
|
type PnpmSettings,
|
||||||
} from '@pnpm/types'
|
} from '@pnpm/types'
|
||||||
import mapValues from 'ramda/src/map'
|
import mapValues from 'ramda/src/map'
|
||||||
|
import omit from 'ramda/src/omit'
|
||||||
import pick from 'ramda/src/pick'
|
import pick from 'ramda/src/pick'
|
||||||
|
import { globalWarn } from '@pnpm/logger'
|
||||||
|
|
||||||
export type OptionsFromRootManifest = {
|
export type OptionsFromRootManifest = {
|
||||||
allowedDeprecatedVersions?: AllowedDeprecatedVersions
|
allowedDeprecatedVersions?: AllowedDeprecatedVersions
|
||||||
allowNonAppliedPatches?: boolean
|
allowUnusedPatches?: boolean
|
||||||
|
ignorePatchFailures?: boolean
|
||||||
overrides?: Record<string, string>
|
overrides?: Record<string, string>
|
||||||
neverBuiltDependencies?: string[]
|
neverBuiltDependencies?: string[]
|
||||||
onlyBuiltDependencies?: string[]
|
onlyBuiltDependencies?: string[]
|
||||||
@@ -29,11 +32,13 @@ export type OptionsFromRootManifest = {
|
|||||||
export function getOptionsFromRootManifest (manifestDir: string, manifest: ProjectManifest): OptionsFromRootManifest {
|
export function getOptionsFromRootManifest (manifestDir: string, manifest: ProjectManifest): OptionsFromRootManifest {
|
||||||
const settings: OptionsFromRootManifest = getOptionsFromPnpmSettings(manifestDir, {
|
const settings: OptionsFromRootManifest = getOptionsFromPnpmSettings(manifestDir, {
|
||||||
...pick([
|
...pick([
|
||||||
'allowNonAppliedPatches',
|
|
||||||
'allowedDeprecatedVersions',
|
'allowedDeprecatedVersions',
|
||||||
|
'allowNonAppliedPatches',
|
||||||
|
'allowUnusedPatches',
|
||||||
'configDependencies',
|
'configDependencies',
|
||||||
'ignoredBuiltDependencies',
|
'ignoredBuiltDependencies',
|
||||||
'ignoredOptionalDependencies',
|
'ignoredOptionalDependencies',
|
||||||
|
'ignorePatchFailures',
|
||||||
'neverBuiltDependencies',
|
'neverBuiltDependencies',
|
||||||
'onlyBuiltDependencies',
|
'onlyBuiltDependencies',
|
||||||
'onlyBuiltDependenciesFile',
|
'onlyBuiltDependenciesFile',
|
||||||
@@ -55,7 +60,8 @@ export function getOptionsFromRootManifest (manifestDir: string, manifest: Proje
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getOptionsFromPnpmSettings (manifestDir: string, pnpmSettings: PnpmSettings, manifest?: ProjectManifest): OptionsFromRootManifest {
|
export function getOptionsFromPnpmSettings (manifestDir: string, pnpmSettings: PnpmSettings, manifest?: ProjectManifest): OptionsFromRootManifest {
|
||||||
const settings: OptionsFromRootManifest = { ...pnpmSettings }
|
const renamedKeys = ['allowNonAppliedPatches'] as const satisfies Array<keyof PnpmSettings>
|
||||||
|
const settings: OptionsFromRootManifest = omit(renamedKeys, pnpmSettings)
|
||||||
if (settings.overrides) {
|
if (settings.overrides) {
|
||||||
if (Object.keys(settings.overrides).length === 0) {
|
if (Object.keys(settings.overrides).length === 0) {
|
||||||
delete settings.overrides
|
delete settings.overrides
|
||||||
@@ -73,6 +79,13 @@ export function getOptionsFromPnpmSettings (manifestDir: string, pnpmSettings: P
|
|||||||
settings.patchedDependencies[dep] = path.join(manifestDir, patchFile)
|
settings.patchedDependencies[dep] = path.join(manifestDir, patchFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (pnpmSettings.allowNonAppliedPatches != null) {
|
||||||
|
globalWarn('allowNonAppliedPatches is deprecated, use allowUnusedPatches instead.')
|
||||||
|
settings.allowUnusedPatches ??= pnpmSettings.allowNonAppliedPatches
|
||||||
|
}
|
||||||
|
if (pnpmSettings.ignorePatchFailures != null) {
|
||||||
|
settings.ignorePatchFailures = pnpmSettings.ignorePatchFailures
|
||||||
|
}
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,62 @@ test('getOptionsFromRootManifest() should return the list from onlyBuiltDependen
|
|||||||
expect(options.onlyBuiltDependencies).toStrictEqual(['electron'])
|
expect(options.onlyBuiltDependencies).toStrictEqual(['electron'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('getOptionsFromRootManifest() should derive allowUnusedPatches from allowNonAppliedPatches (legacy behavior)', () => {
|
||||||
|
expect(getOptionsFromRootManifest(process.cwd(), {
|
||||||
|
pnpm: {
|
||||||
|
allowNonAppliedPatches: false,
|
||||||
|
},
|
||||||
|
})).toStrictEqual({
|
||||||
|
allowUnusedPatches: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getOptionsFromRootManifest(process.cwd(), {
|
||||||
|
pnpm: {
|
||||||
|
allowNonAppliedPatches: true,
|
||||||
|
},
|
||||||
|
})).toStrictEqual({
|
||||||
|
allowUnusedPatches: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allowUnusedPatches should override allowNonAppliedPatches', () => {
|
||||||
|
expect(getOptionsFromRootManifest(process.cwd(), {
|
||||||
|
pnpm: {
|
||||||
|
allowNonAppliedPatches: false,
|
||||||
|
allowUnusedPatches: false,
|
||||||
|
},
|
||||||
|
})).toStrictEqual({
|
||||||
|
allowUnusedPatches: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getOptionsFromRootManifest(process.cwd(), {
|
||||||
|
pnpm: {
|
||||||
|
allowNonAppliedPatches: true,
|
||||||
|
allowUnusedPatches: false,
|
||||||
|
},
|
||||||
|
})).toStrictEqual({
|
||||||
|
allowUnusedPatches: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getOptionsFromRootManifest(process.cwd(), {
|
||||||
|
pnpm: {
|
||||||
|
allowNonAppliedPatches: false,
|
||||||
|
allowUnusedPatches: false,
|
||||||
|
},
|
||||||
|
})).toStrictEqual({
|
||||||
|
allowUnusedPatches: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getOptionsFromRootManifest(process.cwd(), {
|
||||||
|
pnpm: {
|
||||||
|
allowNonAppliedPatches: true,
|
||||||
|
allowUnusedPatches: false,
|
||||||
|
},
|
||||||
|
})).toStrictEqual({
|
||||||
|
allowUnusedPatches: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('getOptionsFromRootManifest() should return patchedDependencies', () => {
|
test('getOptionsFromRootManifest() should return patchedDependencies', () => {
|
||||||
const options = getOptionsFromRootManifest(process.cwd(), {
|
const options = getOptionsFromRootManifest(process.cwd(), {
|
||||||
pnpm: {
|
pnpm: {
|
||||||
|
|||||||
+3
-3
@@ -15,8 +15,8 @@ import {
|
|||||||
import { logger } from '@pnpm/logger'
|
import { logger } from '@pnpm/logger'
|
||||||
import { type IncludedDependencies } from '@pnpm/modules-yaml'
|
import { type IncludedDependencies } from '@pnpm/modules-yaml'
|
||||||
import { packageIsInstallable } from '@pnpm/package-is-installable'
|
import { packageIsInstallable } from '@pnpm/package-is-installable'
|
||||||
import { getPatchInfo } from '@pnpm/patching.config'
|
import { type PatchGroupRecord, getPatchInfo } from '@pnpm/patching.config'
|
||||||
import { type PatchFile, type PatchInfo } from '@pnpm/patching.types'
|
import { type PatchInfo } from '@pnpm/patching.types'
|
||||||
import { type DepPath, type SupportedArchitectures, type Registries, type PkgIdWithPatchHash, type ProjectId } from '@pnpm/types'
|
import { type DepPath, type SupportedArchitectures, type Registries, type PkgIdWithPatchHash, type ProjectId } from '@pnpm/types'
|
||||||
import {
|
import {
|
||||||
type PkgRequestFetchResult,
|
type PkgRequestFetchResult,
|
||||||
@@ -63,7 +63,7 @@ export interface LockfileToDepGraphOptions {
|
|||||||
lockfileDir: string
|
lockfileDir: string
|
||||||
nodeVersion: string
|
nodeVersion: string
|
||||||
pnpmVersion: string
|
pnpmVersion: string
|
||||||
patchedDependencies?: Record<string, PatchFile>
|
patchedDependencies?: PatchGroupRecord
|
||||||
registries: Registries
|
registries: Registries
|
||||||
sideEffectsCacheRead: boolean
|
sideEffectsCacheRead: boolean
|
||||||
skipped: Set<DepPath>
|
skipped: Set<DepPath>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export async function buildModules<T extends string> (
|
|||||||
rootDepPaths: T[],
|
rootDepPaths: T[],
|
||||||
opts: {
|
opts: {
|
||||||
allowBuild?: (pkgName: string) => boolean
|
allowBuild?: (pkgName: string) => boolean
|
||||||
|
ignorePatchFailures?: boolean
|
||||||
ignoredBuiltDependencies?: string[]
|
ignoredBuiltDependencies?: string[]
|
||||||
childConcurrency?: number
|
childConcurrency?: number
|
||||||
depsToBuild?: Set<string>
|
depsToBuild?: Set<string>
|
||||||
@@ -103,6 +104,7 @@ async function buildDependency<T extends string> (
|
|||||||
depPath: T,
|
depPath: T,
|
||||||
depGraph: DependenciesGraph<T>,
|
depGraph: DependenciesGraph<T>,
|
||||||
opts: {
|
opts: {
|
||||||
|
ignorePatchFailures?: boolean
|
||||||
extraBinPaths?: string[]
|
extraBinPaths?: string[]
|
||||||
extraNodePaths?: string[]
|
extraNodePaths?: string[]
|
||||||
extraEnv?: Record<string, string>
|
extraEnv?: Record<string, string>
|
||||||
@@ -138,7 +140,10 @@ async function buildDependency<T extends string> (
|
|||||||
let isPatched = false
|
let isPatched = false
|
||||||
if (depNode.patch) {
|
if (depNode.patch) {
|
||||||
const { file, strict } = depNode.patch
|
const { file, strict } = depNode.patch
|
||||||
isPatched = applyPatchToDir({ allowFailure: !strict, patchedDir: depNode.dir, patchFilePath: file.path })
|
// `strict` is a legacy property which was kept to preserve backward compatibility.
|
||||||
|
// Once a major version of pnpm is released, `strict` should be removed completely.
|
||||||
|
const allowFailure: boolean = opts.ignorePatchFailures ?? !strict
|
||||||
|
isPatched = applyPatchToDir({ allowFailure, patchedDir: depNode.dir, patchFilePath: file.path })
|
||||||
}
|
}
|
||||||
const hasSideEffects = !opts.ignoreScripts && await runPostinstallHooks({
|
const hasSideEffects = !opts.ignoreScripts && await runPostinstallHooks({
|
||||||
depPath,
|
depPath,
|
||||||
|
|||||||
@@ -144,7 +144,9 @@ export interface PnpmSettings {
|
|||||||
ignoredOptionalDependencies?: string[]
|
ignoredOptionalDependencies?: string[]
|
||||||
peerDependencyRules?: PeerDependencyRules
|
peerDependencyRules?: PeerDependencyRules
|
||||||
allowedDeprecatedVersions?: AllowedDeprecatedVersions
|
allowedDeprecatedVersions?: AllowedDeprecatedVersions
|
||||||
allowNonAppliedPatches?: boolean
|
allowNonAppliedPatches?: boolean // deprecated: use allowUnusedPatches instead
|
||||||
|
allowUnusedPatches?: boolean
|
||||||
|
ignorePatchFailures?: boolean
|
||||||
patchedDependencies?: Record<string, string>
|
patchedDependencies?: Record<string, string>
|
||||||
updateConfig?: {
|
updateConfig?: {
|
||||||
ignoreDependencies?: string[]
|
ignoreDependencies?: string[]
|
||||||
|
|||||||
@@ -31,10 +31,17 @@
|
|||||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\""
|
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"devDependencies": {
|
||||||
"@pnpm/patching.config": "workspace:*"
|
"@pnpm/patching.config": "workspace:*",
|
||||||
|
"@types/semver": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.12"
|
"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 {
|
||||||
|
type ExtendedPatchInfo,
|
||||||
export interface ExtendedPatchInfo extends PatchInfo {
|
type PatchFile,
|
||||||
key: string
|
type PatchInfo,
|
||||||
}
|
type PatchGroup,
|
||||||
|
type PatchGroupRangeItem,
|
||||||
export function getPatchInfo (
|
type PatchGroupRecord,
|
||||||
patchedDependencies: Record<string, PatchFile> | undefined,
|
} from '@pnpm/patching.types'
|
||||||
pkgName: string,
|
export { groupPatchedDependencies } from './groupPatchedDependencies'
|
||||||
pkgVersion: string
|
export { getPatchInfo } from './getPatchInfo'
|
||||||
): ExtendedPatchInfo | undefined {
|
export { type VerifyPatchesOptions, verifyPatches } from './verifyPatches'
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -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', () => {
|
test('getPatchInfo(undefined, ...) returns undefined', () => {
|
||||||
expect(getPatchInfo(undefined, 'foo', '1.0.0')).toBeUndefined()
|
expect(getPatchInfo(undefined, 'foo', '1.0.0')).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('getPatchInfo(_, name, version) returns strict=true if name@version exists', () => {
|
test('getPatchInfo(_, name, version) returns strict=true if name@version exists', () => {
|
||||||
expect(getPatchInfo({
|
expect(_getPatchInfo({
|
||||||
'foo@1.0.0': {
|
'foo@1.0.0': {
|
||||||
path: 'patches/foo@1.0.0.patch',
|
path: 'patches/foo@1.0.0.patch',
|
||||||
hash: '00000000000000000000000000000000',
|
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', () => {
|
test('getPatchInfo(_, name, version) returns strict=false if name exists and name@version does not exist', () => {
|
||||||
expect(getPatchInfo({
|
expect(_getPatchInfo({
|
||||||
foo: {
|
foo: {
|
||||||
path: 'patches/foo.patch',
|
path: 'patches/foo.patch',
|
||||||
hash: '00000000000000000000000000000000',
|
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', () => {
|
test('getPatchInfo(_, name, version) prioritizes name@version over name if both exist', () => {
|
||||||
expect(getPatchInfo({
|
expect(_getPatchInfo({
|
||||||
foo: {
|
foo: {
|
||||||
path: 'patches/foo.patch',
|
path: 'patches/foo.patch',
|
||||||
hash: '00000000000000000000000000000000',
|
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', () => {
|
test('getPatchInfo(_, name, version) does not access wrong name', () => {
|
||||||
expect(getPatchInfo({
|
expect(_getPatchInfo({
|
||||||
'bar@1.0.0': {
|
'bar@1.0.0': {
|
||||||
path: 'patches/bar@1.0.0.patch',
|
path: 'patches/bar@1.0.0.patch',
|
||||||
hash: '00000000000000000000000000000000',
|
hash: '00000000000000000000000000000000',
|
||||||
@@ -66,7 +70,7 @@ test('getPatchInfo(_, name, version) does not access wrong name', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('getPatchInfo(_, name, version) does not access wrong version', () => {
|
test('getPatchInfo(_, name, version) does not access wrong version', () => {
|
||||||
expect(getPatchInfo({
|
expect(_getPatchInfo({
|
||||||
'foo@2.0.0': {
|
'foo@2.0.0': {
|
||||||
path: 'patches/foo@2.0.0.patch',
|
path: 'patches/foo@2.0.0.patch',
|
||||||
hash: '00000000000000000000000000000000',
|
hash: '00000000000000000000000000000000',
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
"../../__typings__/**/*.d.ts"
|
"../../__typings__/**/*.d.ts"
|
||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../packages/dependency-path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../packages/error"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../types"
|
"path": "../types"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,30 @@ export interface PatchFile {
|
|||||||
hash: string
|
hash: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: replace all occurrences of PatchInfo with PatchFile before the next major version is released
|
||||||
export interface PatchInfo {
|
export interface PatchInfo {
|
||||||
strict: boolean
|
strict: boolean
|
||||||
file: PatchFile
|
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>
|
||||||
@@ -92,6 +92,7 @@
|
|||||||
"@pnpm/package-requester": "workspace:*",
|
"@pnpm/package-requester": "workspace:*",
|
||||||
"@pnpm/parse-overrides": "workspace:*",
|
"@pnpm/parse-overrides": "workspace:*",
|
||||||
"@pnpm/parse-wanted-dependency": "workspace:*",
|
"@pnpm/parse-wanted-dependency": "workspace:*",
|
||||||
|
"@pnpm/patching.config": "workspace:*",
|
||||||
"@pnpm/pkg-manager.direct-dep-linker": "workspace:*",
|
"@pnpm/pkg-manager.direct-dep-linker": "workspace:*",
|
||||||
"@pnpm/read-modules-dir": "workspace:*",
|
"@pnpm/read-modules-dir": "workspace:*",
|
||||||
"@pnpm/read-project-manifest": "workspace:*",
|
"@pnpm/read-project-manifest": "workspace:*",
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export async function getPeerDependencyIssues (
|
|||||||
{
|
{
|
||||||
currentLockfile: ctx.currentLockfile,
|
currentLockfile: ctx.currentLockfile,
|
||||||
allowedDeprecatedVersions: {},
|
allowedDeprecatedVersions: {},
|
||||||
allowNonAppliedPatches: false,
|
allowUnusedPatches: false,
|
||||||
catalogs: opts.catalogs,
|
catalogs: opts.catalogs,
|
||||||
defaultUpdateDepth: -1,
|
defaultUpdateDepth: -1,
|
||||||
dedupePeerDependents: opts.dedupePeerDependents,
|
dedupePeerDependents: opts.dedupePeerDependents,
|
||||||
|
|||||||
@@ -112,7 +112,8 @@ export interface StrictInstallOptions {
|
|||||||
enableModulesDir: boolean
|
enableModulesDir: boolean
|
||||||
modulesCacheMaxAge: number
|
modulesCacheMaxAge: number
|
||||||
allowedDeprecatedVersions: AllowedDeprecatedVersions
|
allowedDeprecatedVersions: AllowedDeprecatedVersions
|
||||||
allowNonAppliedPatches: boolean
|
ignorePatchFailures?: boolean
|
||||||
|
allowUnusedPatches: boolean
|
||||||
preferSymlinkedExecutables: boolean
|
preferSymlinkedExecutables: boolean
|
||||||
resolutionMode: 'highest' | 'time-based' | 'lowest-direct'
|
resolutionMode: 'highest' | 'time-based' | 'lowest-direct'
|
||||||
resolvePeersFromWorkspaceRoot: boolean
|
resolvePeersFromWorkspaceRoot: boolean
|
||||||
@@ -169,7 +170,8 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
allowedDeprecatedVersions: {},
|
allowedDeprecatedVersions: {},
|
||||||
allowNonAppliedPatches: false,
|
allowUnusedPatches: false,
|
||||||
|
ignorePatchFailures: undefined,
|
||||||
autoInstallPeers: true,
|
autoInstallPeers: true,
|
||||||
autoInstallPeersFromHighestMatch: false,
|
autoInstallPeersFromHighestMatch: false,
|
||||||
childConcurrency: 5,
|
childConcurrency: 5,
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import {
|
|||||||
writeLockfiles,
|
writeLockfiles,
|
||||||
writeWantedLockfile,
|
writeWantedLockfile,
|
||||||
cleanGitBranchLockfiles,
|
cleanGitBranchLockfiles,
|
||||||
type PatchFile,
|
|
||||||
type CatalogSnapshots,
|
type CatalogSnapshots,
|
||||||
} from '@pnpm/lockfile.fs'
|
} from '@pnpm/lockfile.fs'
|
||||||
import { writePnpFile } from '@pnpm/lockfile-to-pnp'
|
import { writePnpFile } from '@pnpm/lockfile-to-pnp'
|
||||||
@@ -46,6 +45,7 @@ import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.pre
|
|||||||
import { logger, globalInfo, streamParser } from '@pnpm/logger'
|
import { logger, globalInfo, streamParser } from '@pnpm/logger'
|
||||||
import { getAllDependenciesFromManifest, getAllUniqueSpecs } from '@pnpm/manifest-utils'
|
import { getAllDependenciesFromManifest, getAllUniqueSpecs } from '@pnpm/manifest-utils'
|
||||||
import { writeModulesManifest } from '@pnpm/modules-yaml'
|
import { writeModulesManifest } from '@pnpm/modules-yaml'
|
||||||
|
import { type PatchGroupRecord, groupPatchedDependencies } from '@pnpm/patching.config'
|
||||||
import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
|
import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
|
||||||
import {
|
import {
|
||||||
getWantedDependencies,
|
getWantedDependencies,
|
||||||
@@ -367,6 +367,7 @@ export async function mutateModules (
|
|||||||
path: path.join(opts.lockfileDir, patchFile.path),
|
path: path.join(opts.lockfileDir, patchFile.path),
|
||||||
}), patchedDependencies)
|
}), patchedDependencies)
|
||||||
: undefined
|
: undefined
|
||||||
|
const patchGroups = patchedDependenciesWithResolvedPath && groupPatchedDependencies(patchedDependenciesWithResolvedPath)
|
||||||
const frozenLockfile = opts.frozenLockfile ||
|
const frozenLockfile = opts.frozenLockfile ||
|
||||||
opts.frozenLockfileIfExists && ctx.existsNonEmptyWantedLockfile
|
opts.frozenLockfileIfExists && ctx.existsNonEmptyWantedLockfile
|
||||||
let outdatedLockfileSettings = false
|
let outdatedLockfileSettings = false
|
||||||
@@ -418,7 +419,7 @@ export async function mutateModules (
|
|||||||
const frozenInstallResult = await tryFrozenInstall({
|
const frozenInstallResult = await tryFrozenInstall({
|
||||||
frozenLockfile,
|
frozenLockfile,
|
||||||
needsFullResolution,
|
needsFullResolution,
|
||||||
patchedDependenciesWithResolvedPath,
|
patchGroups,
|
||||||
upToDateLockfileMajorVersion,
|
upToDateLockfileMajorVersion,
|
||||||
})
|
})
|
||||||
if (frozenInstallResult !== null) {
|
if (frozenInstallResult !== null) {
|
||||||
@@ -543,7 +544,7 @@ export async function mutateModules (
|
|||||||
pruneVirtualStore,
|
pruneVirtualStore,
|
||||||
scriptsOpts,
|
scriptsOpts,
|
||||||
updateLockfileMinorVersion: true,
|
updateLockfileMinorVersion: true,
|
||||||
patchedDependencies: patchedDependenciesWithResolvedPath,
|
patchedDependencies: patchGroups,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -582,12 +583,12 @@ export async function mutateModules (
|
|||||||
async function tryFrozenInstall ({
|
async function tryFrozenInstall ({
|
||||||
frozenLockfile,
|
frozenLockfile,
|
||||||
needsFullResolution,
|
needsFullResolution,
|
||||||
patchedDependenciesWithResolvedPath,
|
patchGroups,
|
||||||
upToDateLockfileMajorVersion,
|
upToDateLockfileMajorVersion,
|
||||||
}: {
|
}: {
|
||||||
frozenLockfile: boolean
|
frozenLockfile: boolean
|
||||||
needsFullResolution: boolean
|
needsFullResolution: boolean
|
||||||
patchedDependenciesWithResolvedPath?: Record<string, PatchFile>
|
patchGroups?: PatchGroupRecord
|
||||||
upToDateLockfileMajorVersion: boolean
|
upToDateLockfileMajorVersion: boolean
|
||||||
}): Promise<InnerInstallResult | { needsFullResolution: boolean } | null> {
|
}): Promise<InnerInstallResult | { needsFullResolution: boolean } | null> {
|
||||||
const isFrozenInstallPossible =
|
const isFrozenInstallPossible =
|
||||||
@@ -692,7 +693,7 @@ Note that in CI environments, this setting is enabled by default.`,
|
|||||||
pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '',
|
pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '',
|
||||||
},
|
},
|
||||||
currentHoistedLocations: ctx.modulesFile?.hoistedLocations,
|
currentHoistedLocations: ctx.modulesFile?.hoistedLocations,
|
||||||
patchedDependencies: patchedDependenciesWithResolvedPath,
|
patchedDependencies: patchGroups,
|
||||||
selectedProjectDirs: projects.map((project) => project.rootDir),
|
selectedProjectDirs: projects.map((project) => project.rootDir),
|
||||||
allProjects: ctx.projects,
|
allProjects: ctx.projects,
|
||||||
prunedAt: ctx.modulesFile?.prunedAt,
|
prunedAt: ctx.modulesFile?.prunedAt,
|
||||||
@@ -919,7 +920,7 @@ type InstallFunction = (
|
|||||||
projects: ImporterToUpdate[],
|
projects: ImporterToUpdate[],
|
||||||
ctx: PnpmContext,
|
ctx: PnpmContext,
|
||||||
opts: Omit<StrictInstallOptions, 'patchedDependencies'> & {
|
opts: Omit<StrictInstallOptions, 'patchedDependencies'> & {
|
||||||
patchedDependencies?: Record<string, PatchFile>
|
patchedDependencies?: PatchGroupRecord
|
||||||
makePartialCurrentLockfile: boolean
|
makePartialCurrentLockfile: boolean
|
||||||
needsFullResolution: boolean
|
needsFullResolution: boolean
|
||||||
neverBuiltDependencies?: string[]
|
neverBuiltDependencies?: string[]
|
||||||
@@ -1031,7 +1032,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
|||||||
projects,
|
projects,
|
||||||
{
|
{
|
||||||
allowedDeprecatedVersions: opts.allowedDeprecatedVersions,
|
allowedDeprecatedVersions: opts.allowedDeprecatedVersions,
|
||||||
allowNonAppliedPatches: opts.allowNonAppliedPatches,
|
allowUnusedPatches: opts.allowUnusedPatches,
|
||||||
autoInstallPeers: opts.autoInstallPeers,
|
autoInstallPeers: opts.autoInstallPeers,
|
||||||
autoInstallPeersFromHighestMatch: opts.autoInstallPeersFromHighestMatch,
|
autoInstallPeersFromHighestMatch: opts.autoInstallPeersFromHighestMatch,
|
||||||
catalogs: opts.catalogs,
|
catalogs: opts.catalogs,
|
||||||
@@ -1203,6 +1204,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
|||||||
}
|
}
|
||||||
ignoredBuilds = (await buildModules(dependenciesGraph, rootNodes, {
|
ignoredBuilds = (await buildModules(dependenciesGraph, rootNodes, {
|
||||||
allowBuild,
|
allowBuild,
|
||||||
|
ignorePatchFailures: opts.ignorePatchFailures,
|
||||||
ignoredBuiltDependencies: opts.ignoredBuiltDependencies,
|
ignoredBuiltDependencies: opts.ignoredBuiltDependencies,
|
||||||
childConcurrency: opts.childConcurrency,
|
childConcurrency: opts.childConcurrency,
|
||||||
depsStateCache,
|
depsStateCache,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { testDefaults } from '../utils'
|
|||||||
|
|
||||||
const f = fixtures(__dirname)
|
const f = fixtures(__dirname)
|
||||||
|
|
||||||
test('patch package', async () => {
|
test('patch package with exact version', async () => {
|
||||||
const reporter = sinon.spy()
|
const reporter = sinon.spy()
|
||||||
const project = prepareEmpty()
|
const project = prepareEmpty()
|
||||||
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
||||||
@@ -109,7 +109,102 @@ test('patch package', async () => {
|
|||||||
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched')
|
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('patch package reports warning if not all patches are applied and allowNonAppliedPatches is set', async () => {
|
test('patch package with version range', async () => {
|
||||||
|
const reporter = sinon.spy()
|
||||||
|
const project = prepareEmpty()
|
||||||
|
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
const patchedDependencies = {
|
||||||
|
'is-positive@1': patchPath,
|
||||||
|
}
|
||||||
|
const opts = testDefaults({
|
||||||
|
neverBuiltDependencies: undefined,
|
||||||
|
onlyBuiltDependencies: [],
|
||||||
|
fastUnpack: false,
|
||||||
|
sideEffectsCacheRead: true,
|
||||||
|
sideEffectsCacheWrite: true,
|
||||||
|
patchedDependencies,
|
||||||
|
reporter,
|
||||||
|
}, {}, {}, { packageImportMethod: 'hardlink' })
|
||||||
|
await install({
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0',
|
||||||
|
},
|
||||||
|
}, opts)
|
||||||
|
|
||||||
|
expect(reporter.calledWithMatch({
|
||||||
|
packageNames: [],
|
||||||
|
level: 'debug',
|
||||||
|
name: 'pnpm:ignored-scripts',
|
||||||
|
} as IgnoredScriptsLog)).toBeTruthy()
|
||||||
|
|
||||||
|
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).toContain('// patched')
|
||||||
|
|
||||||
|
const patchFileHash = await createHexHashFromFile(patchPath)
|
||||||
|
const lockfile = project.readLockfile()
|
||||||
|
expect(lockfile.patchedDependencies).toStrictEqual({
|
||||||
|
'is-positive@1': {
|
||||||
|
path: path.relative(process.cwd(), patchedDependencies['is-positive@1']).replaceAll('\\', '/'),
|
||||||
|
hash: patchFileHash,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(lockfile.snapshots[`is-positive@1.0.0(patch_hash=${patchFileHash})`]).toBeTruthy()
|
||||||
|
|
||||||
|
const filesIndexFile = path.join(opts.storeDir, 'index/c7/1ccf199e0fdae37aad13946b937d67bcd35fa111b84d21b3a19439cfdc2812-is-positive@1.0.0.json')
|
||||||
|
const filesIndex = loadJsonFile.sync<PackageFilesIndex>(filesIndexFile)
|
||||||
|
const sideEffectsKey = `${ENGINE_NAME};patch=${patchFileHash}`
|
||||||
|
const patchedFileIntegrity = filesIndex.sideEffects?.[sideEffectsKey].added?.['index.js']?.integrity
|
||||||
|
expect(patchedFileIntegrity).toBeTruthy()
|
||||||
|
const originalFileIntegrity = filesIndex.files['index.js'].integrity
|
||||||
|
expect(originalFileIntegrity).toBeTruthy()
|
||||||
|
// The integrity of the original file differs from the integrity of the patched file
|
||||||
|
expect(originalFileIntegrity).not.toEqual(patchedFileIntegrity)
|
||||||
|
|
||||||
|
// The same with frozen lockfile
|
||||||
|
rimraf('node_modules')
|
||||||
|
await install({
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
...opts,
|
||||||
|
frozenLockfile: true,
|
||||||
|
})
|
||||||
|
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).toContain('// patched')
|
||||||
|
|
||||||
|
// The same with frozen lockfile and hoisted node_modules
|
||||||
|
rimraf('node_modules')
|
||||||
|
await install({
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
...opts,
|
||||||
|
frozenLockfile: true,
|
||||||
|
nodeLinker: 'hoisted',
|
||||||
|
})
|
||||||
|
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).toContain('// patched')
|
||||||
|
|
||||||
|
process.chdir('..')
|
||||||
|
fs.mkdirSync('project2')
|
||||||
|
process.chdir('project2')
|
||||||
|
|
||||||
|
await install({
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0',
|
||||||
|
},
|
||||||
|
}, testDefaults({
|
||||||
|
fastUnpack: false,
|
||||||
|
sideEffectsCacheRead: true,
|
||||||
|
sideEffectsCacheWrite: true,
|
||||||
|
offline: true,
|
||||||
|
}, {}, {}, { packageImportMethod: 'hardlink' }))
|
||||||
|
|
||||||
|
// The original file did not break, when a patched version was created
|
||||||
|
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('patch package reports warning if not all patches are applied and allowUnusedPatches is set', async () => {
|
||||||
prepareEmpty()
|
prepareEmpty()
|
||||||
const reporter = jest.fn()
|
const reporter = jest.fn()
|
||||||
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
||||||
@@ -123,7 +218,7 @@ test('patch package reports warning if not all patches are applied and allowNonA
|
|||||||
sideEffectsCacheRead: true,
|
sideEffectsCacheRead: true,
|
||||||
sideEffectsCacheWrite: true,
|
sideEffectsCacheWrite: true,
|
||||||
patchedDependencies,
|
patchedDependencies,
|
||||||
allowNonAppliedPatches: true,
|
allowUnusedPatches: true,
|
||||||
reporter,
|
reporter,
|
||||||
}, {}, {}, { packageImportMethod: 'hardlink' })
|
}, {}, {}, { packageImportMethod: 'hardlink' })
|
||||||
await install({
|
await install({
|
||||||
@@ -134,7 +229,7 @@ test('patch package reports warning if not all patches are applied and allowNonA
|
|||||||
expect(reporter).toHaveBeenCalledWith(
|
expect(reporter).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
level: 'warn',
|
level: 'warn',
|
||||||
message: 'The following patches were not applied: is-negative@1.0.0',
|
message: 'The following patches were not used: is-negative@1.0.0',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -159,7 +254,7 @@ test('patch package throws an exception if not all patches are applied', async (
|
|||||||
'is-positive': '1.0.0',
|
'is-positive': '1.0.0',
|
||||||
},
|
},
|
||||||
}, opts)
|
}, opts)
|
||||||
).rejects.toThrow('The following patches were not applied: is-negative@1.0.0')
|
).rejects.toThrow('The following patches were not used: is-negative@1.0.0')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('the patched package is updated if the patch is modified', async () => {
|
test('the patched package is updated if the patch is modified', async () => {
|
||||||
@@ -399,7 +494,7 @@ test('patch package when the patched package has no dependencies and appears mul
|
|||||||
].sort())
|
].sort())
|
||||||
})
|
})
|
||||||
|
|
||||||
test('patch package should fail when the patch could not be applied', async () => {
|
test('patch package should fail when the exact version patch fails to apply', async () => {
|
||||||
prepareEmpty()
|
prepareEmpty()
|
||||||
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
||||||
|
|
||||||
@@ -420,3 +515,103 @@ test('patch package should fail when the patch could not be applied', async () =
|
|||||||
|
|
||||||
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched')
|
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('patch package should fail when the version range patch fails to apply', async () => {
|
||||||
|
prepareEmpty()
|
||||||
|
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
const patchedDependencies = {
|
||||||
|
'is-positive@>=3': patchPath,
|
||||||
|
}
|
||||||
|
const opts = testDefaults({
|
||||||
|
fastUnpack: false,
|
||||||
|
sideEffectsCacheRead: true,
|
||||||
|
sideEffectsCacheWrite: true,
|
||||||
|
patchedDependencies,
|
||||||
|
}, {}, {}, { packageImportMethod: 'hardlink' })
|
||||||
|
await expect(install({
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0',
|
||||||
|
},
|
||||||
|
}, opts)).rejects.toThrow(/Could not apply patch/)
|
||||||
|
|
||||||
|
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('patch package should print a warning when the patch fails to apply and ignorePatchFailures is set to true', async () => {
|
||||||
|
prepareEmpty()
|
||||||
|
const reporter = jest.fn()
|
||||||
|
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
const patchedDependencies = {
|
||||||
|
'is-positive@3.1.0': patchPath,
|
||||||
|
}
|
||||||
|
const opts = testDefaults({
|
||||||
|
fastUnpack: false,
|
||||||
|
ignorePatchFailures: true,
|
||||||
|
sideEffectsCacheRead: true,
|
||||||
|
sideEffectsCacheWrite: true,
|
||||||
|
patchedDependencies,
|
||||||
|
reporter,
|
||||||
|
}, {}, {}, { packageImportMethod: 'hardlink' })
|
||||||
|
await install({
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0',
|
||||||
|
},
|
||||||
|
}, opts)
|
||||||
|
|
||||||
|
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched')
|
||||||
|
expect(reporter).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringMatching(/Could not apply patch/),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('patch package should print a warning when the name-only patch fails to apply (legacy behavior)', async () => {
|
||||||
|
prepareEmpty()
|
||||||
|
const reporter = jest.fn()
|
||||||
|
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
const patchedDependencies = {
|
||||||
|
'is-positive': patchPath,
|
||||||
|
}
|
||||||
|
const opts = testDefaults({
|
||||||
|
fastUnpack: false,
|
||||||
|
sideEffectsCacheRead: true,
|
||||||
|
sideEffectsCacheWrite: true,
|
||||||
|
patchedDependencies,
|
||||||
|
reporter,
|
||||||
|
}, {}, {}, { packageImportMethod: 'hardlink' })
|
||||||
|
await install({
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0',
|
||||||
|
},
|
||||||
|
}, opts)
|
||||||
|
|
||||||
|
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched')
|
||||||
|
expect(reporter).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
message: expect.stringMatching(/Could not apply patch/),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('patch package should fail when the name-only range patch fails to apply and ignorePatchFailures is explicitly set to false', async () => {
|
||||||
|
prepareEmpty()
|
||||||
|
const patchPath = path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
const patchedDependencies = {
|
||||||
|
'is-positive': patchPath,
|
||||||
|
}
|
||||||
|
const opts = testDefaults({
|
||||||
|
fastUnpack: false,
|
||||||
|
ignorePatchFailures: false,
|
||||||
|
sideEffectsCacheRead: true,
|
||||||
|
sideEffectsCacheWrite: true,
|
||||||
|
patchedDependencies,
|
||||||
|
}, {}, {}, { packageImportMethod: 'hardlink' })
|
||||||
|
await expect(install({
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0',
|
||||||
|
},
|
||||||
|
}, opts)).rejects.toThrow(/Could not apply patch/)
|
||||||
|
|
||||||
|
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched')
|
||||||
|
})
|
||||||
@@ -126,6 +126,9 @@
|
|||||||
{
|
{
|
||||||
"path": "../../packages/which-version-is-pinned"
|
"path": "../../packages/which-version-is-pinned"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "../../patching/config"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../../pkg-manifest/manifest-utils"
|
"path": "../../pkg-manifest/manifest-utils"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { linkBins, linkBinsOfPackages } from '@pnpm/link-bins'
|
|||||||
import {
|
import {
|
||||||
getLockfileImporterId,
|
getLockfileImporterId,
|
||||||
type LockfileObject,
|
type LockfileObject,
|
||||||
type PatchFile,
|
|
||||||
readCurrentLockfile,
|
readCurrentLockfile,
|
||||||
readWantedLockfile,
|
readWantedLockfile,
|
||||||
writeLockfiles,
|
writeLockfiles,
|
||||||
@@ -50,6 +49,7 @@ import {
|
|||||||
type Modules,
|
type Modules,
|
||||||
writeModulesManifest,
|
writeModulesManifest,
|
||||||
} from '@pnpm/modules-yaml'
|
} from '@pnpm/modules-yaml'
|
||||||
|
import { type PatchGroupRecord } from '@pnpm/patching.config'
|
||||||
import { type HoistingLimits } from '@pnpm/real-hoist'
|
import { type HoistingLimits } from '@pnpm/real-hoist'
|
||||||
import { readPackageJsonFromDir } from '@pnpm/read-package-json'
|
import { readPackageJsonFromDir } from '@pnpm/read-package-json'
|
||||||
import { readProjectManifestOnly, safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
|
import { readProjectManifestOnly, safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
|
||||||
@@ -107,6 +107,7 @@ export interface Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HeadlessOptions {
|
export interface HeadlessOptions {
|
||||||
|
ignorePatchFailures?: boolean
|
||||||
neverBuiltDependencies?: string[]
|
neverBuiltDependencies?: string[]
|
||||||
ignoredBuiltDependencies?: string[]
|
ignoredBuiltDependencies?: string[]
|
||||||
onlyBuiltDependencies?: string[]
|
onlyBuiltDependencies?: string[]
|
||||||
@@ -143,7 +144,7 @@ export interface HeadlessOptions {
|
|||||||
modulesDir?: string
|
modulesDir?: string
|
||||||
virtualStoreDir?: string
|
virtualStoreDir?: string
|
||||||
virtualStoreDirMaxLength: number
|
virtualStoreDirMaxLength: number
|
||||||
patchedDependencies?: Record<string, PatchFile>
|
patchedDependencies?: PatchGroupRecord
|
||||||
scriptsPrependNodePath?: boolean | 'warn-only'
|
scriptsPrependNodePath?: boolean | 'warn-only'
|
||||||
scriptShell?: string
|
scriptShell?: string
|
||||||
shellEmulator?: boolean
|
shellEmulator?: boolean
|
||||||
@@ -531,6 +532,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
|
|||||||
ignoredBuilds = (await buildModules(graph, Array.from(directNodes), {
|
ignoredBuilds = (await buildModules(graph, Array.from(directNodes), {
|
||||||
allowBuild,
|
allowBuild,
|
||||||
ignoredBuiltDependencies: opts.ignoredBuiltDependencies,
|
ignoredBuiltDependencies: opts.ignoredBuiltDependencies,
|
||||||
|
ignorePatchFailures: opts.ignorePatchFailures,
|
||||||
childConcurrency: opts.childConcurrency,
|
childConcurrency: opts.childConcurrency,
|
||||||
extraBinPaths,
|
extraBinPaths,
|
||||||
extraEnv,
|
extraEnv,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import path from 'path'
|
|||||||
import {
|
import {
|
||||||
type LockfileObject,
|
type LockfileObject,
|
||||||
type PackageSnapshot,
|
type PackageSnapshot,
|
||||||
type PatchFile,
|
|
||||||
type ProjectSnapshot,
|
type ProjectSnapshot,
|
||||||
} from '@pnpm/lockfile.fs'
|
} from '@pnpm/lockfile.fs'
|
||||||
import {
|
import {
|
||||||
@@ -13,7 +12,7 @@ import {
|
|||||||
} from '@pnpm/lockfile.utils'
|
} from '@pnpm/lockfile.utils'
|
||||||
import { type IncludedDependencies } from '@pnpm/modules-yaml'
|
import { type IncludedDependencies } from '@pnpm/modules-yaml'
|
||||||
import { packageIsInstallable } from '@pnpm/package-is-installable'
|
import { packageIsInstallable } from '@pnpm/package-is-installable'
|
||||||
import { getPatchInfo } from '@pnpm/patching.config'
|
import { type PatchGroupRecord, getPatchInfo } from '@pnpm/patching.config'
|
||||||
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
|
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
|
||||||
import { type DepPath, type SupportedArchitectures, type ProjectId, type Registries } from '@pnpm/types'
|
import { type DepPath, type SupportedArchitectures, type ProjectId, type Registries } from '@pnpm/types'
|
||||||
import {
|
import {
|
||||||
@@ -44,7 +43,7 @@ export interface LockfileToHoistedDepGraphOptions {
|
|||||||
nodeVersion: string
|
nodeVersion: string
|
||||||
pnpmVersion: string
|
pnpmVersion: string
|
||||||
registries: Registries
|
registries: Registries
|
||||||
patchedDependencies?: Record<string, PatchFile>
|
patchedDependencies?: PatchGroupRecord
|
||||||
sideEffectsCacheRead: boolean
|
sideEffectsCacheRead: boolean
|
||||||
skipped: Set<string>
|
skipped: Set<string>
|
||||||
storeController: StoreController
|
storeController: StoreController
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { PnpmError } from '@pnpm/error'
|
|
||||||
import {
|
import {
|
||||||
packageManifestLogger,
|
packageManifestLogger,
|
||||||
} from '@pnpm/core-loggers'
|
} from '@pnpm/core-loggers'
|
||||||
import { globalWarn } from '@pnpm/logger'
|
|
||||||
import {
|
import {
|
||||||
type LockfileObject,
|
type LockfileObject,
|
||||||
type ProjectSnapshot,
|
type ProjectSnapshot,
|
||||||
@@ -13,6 +11,7 @@ import {
|
|||||||
getSpecFromPackageManifest,
|
getSpecFromPackageManifest,
|
||||||
type PinnedVersion,
|
type PinnedVersion,
|
||||||
} from '@pnpm/manifest-utils'
|
} from '@pnpm/manifest-utils'
|
||||||
|
import { verifyPatches } from '@pnpm/patching.config'
|
||||||
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
|
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
|
||||||
import {
|
import {
|
||||||
type DependenciesField,
|
type DependenciesField,
|
||||||
@@ -116,7 +115,7 @@ export async function resolveDependencies (
|
|||||||
preserveWorkspaceProtocol: boolean
|
preserveWorkspaceProtocol: boolean
|
||||||
saveWorkspaceProtocol: 'rolling' | boolean
|
saveWorkspaceProtocol: 'rolling' | boolean
|
||||||
lockfileIncludeTarballUrl?: boolean
|
lockfileIncludeTarballUrl?: boolean
|
||||||
allowNonAppliedPatches?: boolean
|
allowUnusedPatches?: boolean
|
||||||
}
|
}
|
||||||
): Promise<ResolveDependenciesResult> {
|
): Promise<ResolveDependenciesResult> {
|
||||||
const _toResolveImporter = toResolveImporter.bind(null, {
|
const _toResolveImporter = toResolveImporter.bind(null, {
|
||||||
@@ -148,9 +147,9 @@ export async function resolveDependencies (
|
|||||||
Object.keys(opts.wantedLockfile.importers).length === importers.length
|
Object.keys(opts.wantedLockfile.importers).length === importers.length
|
||||||
) {
|
) {
|
||||||
verifyPatches({
|
verifyPatches({
|
||||||
patchedDependencies: Object.keys(opts.patchedDependencies),
|
patchedDependencies: opts.patchedDependencies,
|
||||||
appliedPatches,
|
appliedPatches,
|
||||||
allowNonAppliedPatches: opts.allowNonAppliedPatches,
|
allowUnusedPatches: opts.allowUnusedPatches,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,29 +327,6 @@ export async function resolveDependencies (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyPatches (
|
|
||||||
{
|
|
||||||
patchedDependencies,
|
|
||||||
appliedPatches,
|
|
||||||
allowNonAppliedPatches,
|
|
||||||
}: {
|
|
||||||
patchedDependencies: string[]
|
|
||||||
appliedPatches: Set<string>
|
|
||||||
allowNonAppliedPatches: boolean
|
|
||||||
}
|
|
||||||
): void {
|
|
||||||
const nonAppliedPatches: string[] = patchedDependencies.filter((patchKey) => !appliedPatches.has(patchKey))
|
|
||||||
if (!nonAppliedPatches.length) return
|
|
||||||
const message = `The following patches were not applied: ${nonAppliedPatches.join(', ')}`
|
|
||||||
if (allowNonAppliedPatches) {
|
|
||||||
globalWarn(message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
throw new PnpmError('PATCH_NOT_APPLIED', message, {
|
|
||||||
hint: 'Either remove them from "patchedDependencies" or update them to match packages in your dependencies.',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDirectDependenciesToLockfile (
|
function addDirectDependenciesToLockfile (
|
||||||
newManifest: ProjectManifest,
|
newManifest: ProjectManifest,
|
||||||
projectSnapshot: ProjectSnapshot,
|
projectSnapshot: ProjectSnapshot,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
pkgSnapshotToResolution,
|
pkgSnapshotToResolution,
|
||||||
} from '@pnpm/lockfile.utils'
|
} from '@pnpm/lockfile.utils'
|
||||||
import { logger } from '@pnpm/logger'
|
import { logger } from '@pnpm/logger'
|
||||||
import { getPatchInfo } from '@pnpm/patching.config'
|
import { type PatchGroupRecord, getPatchInfo } from '@pnpm/patching.config'
|
||||||
import { pickRegistryForPackage } from '@pnpm/pick-registry-for-package'
|
import { pickRegistryForPackage } from '@pnpm/pick-registry-for-package'
|
||||||
import {
|
import {
|
||||||
type DirectoryResolution,
|
type DirectoryResolution,
|
||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
} from '@pnpm/types'
|
} from '@pnpm/types'
|
||||||
import * as dp from '@pnpm/dependency-path'
|
import * as dp from '@pnpm/dependency-path'
|
||||||
import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.preferred-versions'
|
import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.preferred-versions'
|
||||||
import { type PatchFile, type PatchInfo } from '@pnpm/patching.types'
|
import { type PatchInfo } from '@pnpm/patching.types'
|
||||||
import normalizePath from 'normalize-path'
|
import normalizePath from 'normalize-path'
|
||||||
import exists from 'path-exists'
|
import exists from 'path-exists'
|
||||||
import pDefer from 'p-defer'
|
import pDefer from 'p-defer'
|
||||||
@@ -148,7 +148,7 @@ export interface ResolutionContext {
|
|||||||
resolvedPkgsById: ResolvedPkgsById
|
resolvedPkgsById: ResolvedPkgsById
|
||||||
outdatedDependencies: Record<PkgResolutionId, string>
|
outdatedDependencies: Record<PkgResolutionId, string>
|
||||||
childrenByParentId: ChildrenByParentId
|
childrenByParentId: ChildrenByParentId
|
||||||
patchedDependencies?: Record<string, PatchFile>
|
patchedDependencies?: PatchGroupRecord
|
||||||
pendingNodes: PendingNode[]
|
pendingNodes: PendingNode[]
|
||||||
wantedLockfile: LockfileObject
|
wantedLockfile: LockfileObject
|
||||||
currentLockfile: LockfileObject
|
currentLockfile: LockfileObject
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { resolveFromCatalog } from '@pnpm/catalogs.resolver'
|
import { resolveFromCatalog } from '@pnpm/catalogs.resolver'
|
||||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||||
import { type LockfileObject } from '@pnpm/lockfile.types'
|
import { type LockfileObject } from '@pnpm/lockfile.types'
|
||||||
import { type PatchFile } from '@pnpm/patching.types'
|
import { type PatchGroupRecord } from '@pnpm/patching.config'
|
||||||
import { type PreferredVersions, type Resolution, type WorkspacePackages } from '@pnpm/resolver-base'
|
import { type PreferredVersions, type Resolution, type WorkspacePackages } from '@pnpm/resolver-base'
|
||||||
import { type StoreController } from '@pnpm/store-controller-types'
|
import { type StoreController } from '@pnpm/store-controller-types'
|
||||||
import {
|
import {
|
||||||
@@ -98,7 +98,7 @@ export interface ResolveDependenciesOptions {
|
|||||||
autoInstallPeers?: boolean
|
autoInstallPeers?: boolean
|
||||||
autoInstallPeersFromHighestMatch?: boolean
|
autoInstallPeersFromHighestMatch?: boolean
|
||||||
allowedDeprecatedVersions: AllowedDeprecatedVersions
|
allowedDeprecatedVersions: AllowedDeprecatedVersions
|
||||||
allowNonAppliedPatches: boolean
|
allowUnusedPatches: boolean
|
||||||
catalogs?: Catalogs
|
catalogs?: Catalogs
|
||||||
currentLockfile: LockfileObject
|
currentLockfile: LockfileObject
|
||||||
dedupePeerDependents?: boolean
|
dedupePeerDependents?: boolean
|
||||||
@@ -112,7 +112,7 @@ export interface ResolveDependenciesOptions {
|
|||||||
}
|
}
|
||||||
nodeVersion?: string
|
nodeVersion?: string
|
||||||
registries: Registries
|
registries: Registries
|
||||||
patchedDependencies?: Record<string, PatchFile>
|
patchedDependencies?: PatchGroupRecord
|
||||||
pnpmVersion: string
|
pnpmVersion: string
|
||||||
preferredVersions?: PreferredVersions
|
preferredVersions?: PreferredVersions
|
||||||
preferWorkspacePackages?: boolean
|
preferWorkspacePackages?: boolean
|
||||||
|
|||||||
Generated
+21
@@ -1460,6 +1460,9 @@ importers:
|
|||||||
'@pnpm/git-utils':
|
'@pnpm/git-utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/git-utils
|
version: link:../../packages/git-utils
|
||||||
|
'@pnpm/logger':
|
||||||
|
specifier: '>=5.1.0 <1001.0.0'
|
||||||
|
version: 1000.0.0
|
||||||
'@pnpm/matcher':
|
'@pnpm/matcher':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../matcher
|
version: link:../matcher
|
||||||
@@ -4150,13 +4153,28 @@ importers:
|
|||||||
|
|
||||||
patching/config:
|
patching/config:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@pnpm/dependency-path':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/dependency-path
|
||||||
|
'@pnpm/error':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/error
|
||||||
|
'@pnpm/logger':
|
||||||
|
specifier: '>=5.1.0 <1001.0.0'
|
||||||
|
version: 1000.0.0
|
||||||
'@pnpm/patching.types':
|
'@pnpm/patching.types':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../types
|
version: link:../types
|
||||||
|
semver:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 7.7.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@pnpm/patching.config':
|
'@pnpm/patching.config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: 'link:'
|
version: 'link:'
|
||||||
|
'@types/semver':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 7.5.3
|
||||||
|
|
||||||
patching/plugin-commands-patching:
|
patching/plugin-commands-patching:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4456,6 +4474,9 @@ importers:
|
|||||||
'@pnpm/parse-wanted-dependency':
|
'@pnpm/parse-wanted-dependency':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/parse-wanted-dependency
|
version: link:../../packages/parse-wanted-dependency
|
||||||
|
'@pnpm/patching.config':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../patching/config
|
||||||
'@pnpm/pkg-manager.direct-dep-linker':
|
'@pnpm/pkg-manager.direct-dep-linker':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../direct-dep-linker
|
version: link:../direct-dep-linker
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
diff --git a/index.js b/index.js
|
||||||
|
index 8e020ca..ff3aee4 100644
|
||||||
|
--- a/index.js
|
||||||
|
+++ b/index.js
|
||||||
|
@@ -5,5 +5,6 @@ module.exports = function (n) {
|
||||||
|
throw new TypeError('Expected a number');
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // patched
|
||||||
|
return n >= 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { preparePackages } from '@pnpm/prepare'
|
||||||
|
import { fixtures } from '@pnpm/test-fixtures'
|
||||||
|
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||||
|
import { execPnpmSync } from '../utils'
|
||||||
|
|
||||||
|
const f = fixtures(__dirname)
|
||||||
|
|
||||||
|
test('allowUnusedPatches=false errors on unused patches', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
allowUnusedPatches: false,
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive': patchFile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should fail
|
||||||
|
const { status, stdout } = execPnpmSync(['install'])
|
||||||
|
expect(status).not.toBe(0)
|
||||||
|
expect(stdout.toString()).toContain('ERR_PNPM_UNUSED_PATCH')
|
||||||
|
expect(stdout.toString()).toContain('The following patches were not used: is-positive')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allowUnusedPatches=true warns about unused patches', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
allowUnusedPatches: true,
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive': patchFile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should not fail
|
||||||
|
const { stdout } = execPnpmSync(['install'], { expectSuccess: true })
|
||||||
|
|
||||||
|
// pnpm install should print a warning regarding unused patches
|
||||||
|
expect(stdout.toString()).toContain('The following patches were not used: is-positive')
|
||||||
|
})
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import { preparePackages } from '@pnpm/prepare'
|
||||||
|
import { fixtures } from '@pnpm/test-fixtures'
|
||||||
|
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||||
|
import { execPnpmSync } from '../utils'
|
||||||
|
|
||||||
|
const f = fixtures(__dirname)
|
||||||
|
|
||||||
|
describe('ignorePatchFailures=undefined (necessary for backward-compatibility)', () => {
|
||||||
|
test('errors on exact version patch failures', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive@1.0.0': patchFile, // should succeed
|
||||||
|
'is-positive@3.1.0': patchFile, // should fail
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should fail
|
||||||
|
const { status, stdout } = execPnpmSync(['install'])
|
||||||
|
expect(status).not.toBe(0)
|
||||||
|
expect(stdout.toString()).toContain('ERR_PNPM_PATCH_FAILED')
|
||||||
|
const errorLines = stdout.toString().split('\n').filter(line => line.includes('ERR_PNPM_PATCH_FAILED'))
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining(patchFile)])
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining('is-positive@3.1.0')])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('errors on version range patch failures', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive@>=1': patchFile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should fail
|
||||||
|
const { status, stdout } = execPnpmSync(['install'])
|
||||||
|
expect(status).not.toBe(0)
|
||||||
|
expect(stdout.toString()).toContain('ERR_PNPM_PATCH_FAILED')
|
||||||
|
const errorLines = stdout.toString().split('\n').filter(line => line.includes('ERR_PNPM_PATCH_FAILED'))
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining(patchFile)])
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining('is-positive@3.1.0')])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('errors on star version range patch failures', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive@*': patchFile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should fail
|
||||||
|
const { status, stdout } = execPnpmSync(['install'])
|
||||||
|
expect(status).not.toBe(0)
|
||||||
|
expect(stdout.toString()).toContain('ERR_PNPM_PATCH_FAILED')
|
||||||
|
const errorLines = stdout.toString().split('\n').filter(line => line.includes('ERR_PNPM_PATCH_FAILED'))
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining(patchFile)])
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining('is-positive@3.1.0')])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows name-only patches to fail with a warning (legacy behavior)', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive': patchFile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should not fail
|
||||||
|
const { stdout } = execPnpmSync(['install'], { expectSuccess: true })
|
||||||
|
|
||||||
|
// pnpm install should print a warning about patch application failure
|
||||||
|
expect(stdout.toString()).toContain('Could not apply patch')
|
||||||
|
|
||||||
|
// the patch should apply to is-positive@1.0.0
|
||||||
|
expect(fs.readFileSync('foo/node_modules/is-positive/index.js', 'utf-8')).toContain('// patched')
|
||||||
|
|
||||||
|
// the patch should not apply to is-positive@3.2.1
|
||||||
|
expect(fs.readFileSync('bar/node_modules/is-positive/index.js', 'utf-8')).not.toContain('// patched')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ignorePatchFailures=true', () => {
|
||||||
|
test('allows exact version patches to fail with a warning', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive@1.0.0': patchFile, // should succeed
|
||||||
|
'is-positive@3.1.0': patchFile, // should fail
|
||||||
|
},
|
||||||
|
ignorePatchFailures: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should not fail
|
||||||
|
const { stdout } = execPnpmSync(['install'], { expectSuccess: true })
|
||||||
|
|
||||||
|
// pnpm install should print a warning about patch application failure
|
||||||
|
expect(stdout.toString()).toContain('Could not apply patch')
|
||||||
|
|
||||||
|
// the patch should apply to is-positive@1.0.0
|
||||||
|
expect(fs.readFileSync('foo/node_modules/is-positive/index.js', 'utf-8')).toContain('// patched')
|
||||||
|
|
||||||
|
// the patch should not apply to is-positive@3.2.1
|
||||||
|
expect(fs.readFileSync('bar/node_modules/is-positive/index.js', 'utf-8')).not.toContain('// patched')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows version range patches to fail with a warning', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive@>=1': patchFile,
|
||||||
|
},
|
||||||
|
ignorePatchFailures: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should not fail
|
||||||
|
const { stdout } = execPnpmSync(['install'], { expectSuccess: true })
|
||||||
|
|
||||||
|
// pnpm install should print a warning about patch application failure
|
||||||
|
expect(stdout.toString()).toContain('Could not apply patch')
|
||||||
|
|
||||||
|
// the patch should apply to is-positive@1.0.0
|
||||||
|
expect(fs.readFileSync('foo/node_modules/is-positive/index.js', 'utf-8')).toContain('// patched')
|
||||||
|
|
||||||
|
// the patch should not apply to is-positive@3.2.1
|
||||||
|
expect(fs.readFileSync('bar/node_modules/is-positive/index.js', 'utf-8')).not.toContain('// patched')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows star version range patches to fail with a warning', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive@*': patchFile,
|
||||||
|
},
|
||||||
|
ignorePatchFailures: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should not fail
|
||||||
|
const { stdout } = execPnpmSync(['install'], { expectSuccess: true })
|
||||||
|
|
||||||
|
// pnpm install should print a warning about patch application failure
|
||||||
|
expect(stdout.toString()).toContain('Could not apply patch')
|
||||||
|
|
||||||
|
// the patch should apply to is-positive@1.0.0
|
||||||
|
expect(fs.readFileSync('foo/node_modules/is-positive/index.js', 'utf-8')).toContain('// patched')
|
||||||
|
|
||||||
|
// the patch should not apply to is-positive@3.2.1
|
||||||
|
expect(fs.readFileSync('bar/node_modules/is-positive/index.js', 'utf-8')).not.toContain('// patched')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows name-only patches to fail with a warning', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive': patchFile,
|
||||||
|
},
|
||||||
|
ignorePatchFailures: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should not fail
|
||||||
|
const { stdout } = execPnpmSync(['install'], { expectSuccess: true })
|
||||||
|
|
||||||
|
// pnpm install should print a warning about patch application failure
|
||||||
|
expect(stdout.toString()).toContain('Could not apply patch')
|
||||||
|
|
||||||
|
// the patch should apply to is-positive@1.0.0
|
||||||
|
expect(fs.readFileSync('foo/node_modules/is-positive/index.js', 'utf-8')).toContain('// patched')
|
||||||
|
|
||||||
|
// the patch should not apply to is-positive@3.2.1
|
||||||
|
expect(fs.readFileSync('bar/node_modules/is-positive/index.js', 'utf-8')).not.toContain('// patched')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ignorePatchFailures=false', () => {
|
||||||
|
test('errors on exact version patch failures', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive@1.0.0': patchFile, // should succeed
|
||||||
|
'is-positive@3.1.0': patchFile, // should fail
|
||||||
|
},
|
||||||
|
ignorePatchFailures: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should fail
|
||||||
|
const { status, stdout } = execPnpmSync(['install'])
|
||||||
|
expect(status).not.toBe(0)
|
||||||
|
expect(stdout.toString()).toContain('ERR_PNPM_PATCH_FAILED')
|
||||||
|
const errorLines = stdout.toString().split('\n').filter(line => line.includes('ERR_PNPM_PATCH_FAILED'))
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining(patchFile)])
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining('is-positive@3.1.0')])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('errors on version range patch failures', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive@>=1': patchFile,
|
||||||
|
},
|
||||||
|
ignorePatchFailures: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install not fail
|
||||||
|
const { status, stdout } = execPnpmSync(['install'])
|
||||||
|
expect(status).not.toBe(0)
|
||||||
|
expect(stdout.toString()).toContain('ERR_PNPM_PATCH_FAILED')
|
||||||
|
const errorLines = stdout.toString().split('\n').filter(line => line.includes('ERR_PNPM_PATCH_FAILED'))
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining(patchFile)])
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining('is-positive@3.1.0')])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('errors on star version range patch failures', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive@*': patchFile,
|
||||||
|
},
|
||||||
|
ignorePatchFailures: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install not fail
|
||||||
|
const { status, stdout } = execPnpmSync(['install'])
|
||||||
|
expect(status).not.toBe(0)
|
||||||
|
expect(stdout.toString()).toContain('ERR_PNPM_PATCH_FAILED')
|
||||||
|
const errorLines = stdout.toString().split('\n').filter(line => line.includes('ERR_PNPM_PATCH_FAILED'))
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining(patchFile)])
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining('is-positive@3.1.0')])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows name-only patches to fail with a warning', async () => {
|
||||||
|
preparePackages([
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '1.0.0', // is-positive@1.0.0.patch should succeed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
version: '0.0.0',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'is-positive': '3.1.0', // is-positive@1.0.0.patch should fail
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const patchFile = f.find('patch-pkg/is-positive@1.0.0.patch')
|
||||||
|
|
||||||
|
writeYamlFile('pnpm-workspace.yaml', {
|
||||||
|
packages: ['**', '!store/**'],
|
||||||
|
patchedDependencies: {
|
||||||
|
'is-positive': patchFile,
|
||||||
|
},
|
||||||
|
ignorePatchFailures: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// pnpm install should fail
|
||||||
|
const { status, stdout } = execPnpmSync(['install'])
|
||||||
|
expect(status).not.toBe(0)
|
||||||
|
expect(stdout.toString()).toContain('ERR_PNPM_PATCH_FAILED')
|
||||||
|
const errorLines = stdout.toString().split('\n').filter(line => line.includes('ERR_PNPM_PATCH_FAILED'))
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining(patchFile)])
|
||||||
|
expect(errorLines).toStrictEqual([expect.stringContaining('is-positive@3.1.0')])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in new issue
Block a user