refactor: simplify patchedDependencies lockfile format (#10911)

* refactor: simplify patchedDependencies lockfile format to map selectors to hashes

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

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

* fix: migrate old patchedDependencies format when reading lockfile

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

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

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

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

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

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

* fix: update remaining references to old PatchFile type

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

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

* fix: throw when patch exists but patchFilePath is missing

Also guard against undefined patchedDependencies entry when
ignorePackageManifest is true.

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

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

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

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

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

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

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

* fix: pass rootProjectManifest in deploy patchedDependencies test

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

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Zoltan Kochan
2026-03-08 19:26:48 +01:00
committed by GitHub
parent 821b36ac20
commit aeb06caae9
25 changed files with 123 additions and 225 deletions

View File

@@ -466,7 +466,6 @@ async function assertWantedLockfileUpToDate (
linkWorkspacePackages,
getManifestsByDir,
getWorkspacePackages,
rootDir,
rootManifestOptions,
} = ctx
@@ -482,7 +481,7 @@ async function assertWantedLockfileUpToDate (
patchedDependencies,
pnpmfileChecksum,
] = await Promise.all([
calcPatchHashes(rootManifestOptions?.patchedDependencies ?? {}, rootDir),
calcPatchHashes(rootManifestOptions?.patchedDependencies ?? {}),
config.hooks?.calculatePnpmfileChecksum?.(),
])

View File

@@ -38,6 +38,7 @@
"@pnpm/core-loggers": "workspace:*",
"@pnpm/dependency-path": "workspace:*",
"@pnpm/deps.graph-sequencer": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/fs.hard-link-dir": "workspace:*",
"@pnpm/lifecycle": "workspace:*",
"@pnpm/link-bins": "workspace:*",

View File

@@ -5,6 +5,7 @@ import util from 'util'
import { calcDepState, type DepsStateCache } from '@pnpm/calc-dep-state'
import { getWorkspaceConcurrency } from '@pnpm/config'
import { skippedOptionalDependencyLogger } from '@pnpm/core-loggers'
import { PnpmError } from '@pnpm/error'
import { runPostinstallHooks } from '@pnpm/lifecycle'
import { linkBins, linkBinsOfPackages } from '@pnpm/link-bins'
import { logger } from '@pnpm/logger'
@@ -166,8 +167,13 @@ async function buildDependency<T extends string> (
await linkBinsOfDependencies(depNode, depGraph, opts)
let isPatched = false
if (depNode.patch) {
const { file } = depNode.patch
isPatched = applyPatchToDir({ patchedDir: depNode.dir, patchFilePath: file.path })
if (!depNode.patch.patchFilePath) {
throw new PnpmError('PATCH_FILE_PATH_MISSING',
`Cannot apply patch for ${depPath}: patch file path is missing`,
{ hint: 'Ensure the package is listed in patchedDependencies configuration' }
)
}
isPatched = applyPatchToDir({ patchedDir: depNode.dir, patchFilePath: depNode.patch.patchFilePath })
}
const hasSideEffects = !opts.ignoreScripts && await runPostinstallHooks({
depPath,
@@ -191,7 +197,7 @@ async function buildDependency<T extends string> (
if ((isPatched || hasSideEffects) && opts.sideEffectsCacheWrite) {
try {
const sideEffectsCacheKey = calcDepState(depGraph, opts.depsStateCache, depPath, {
patchFileHash: depNode.patch?.file.hash,
patchFileHash: depNode.patch?.hash,
includeDepGraphHash: hasSideEffects,
})
await opts.storeController.upload(depNode.dir, {

View File

@@ -27,6 +27,9 @@
{
"path": "../../packages/dependency-path"
},
{
"path": "../../packages/error"
},
{
"path": "../../packages/logger"
},

View File

@@ -150,11 +150,21 @@ export function convertToLockfileObject (lockfile: LockfileFile): LockfileObject
}
return {
...omit(['snapshots'], rest),
patchedDependencies: migratePatchedDependencies(rest.patchedDependencies),
packages,
importers: mapValues(importers ?? {}, revertProjectSnapshot),
}
}
function migratePatchedDependencies (patchedDependencies: Record<string, string | { hash: string }> | undefined): Record<string, string> | undefined {
if (!patchedDependencies) return undefined
const result: Record<string, string> = {}
for (const [key, value] of Object.entries(patchedDependencies)) {
result[key] = typeof value === 'string' ? value : value.hash
}
return result
}
function convertProjectSnapshotToInlineSpecifiersFormat (
projectSnapshot: ProjectSnapshot
): LockfileFileProjectSnapshot {

View File

@@ -31,9 +31,9 @@ test('sorts keys alphabetically', () => {
},
},
patchedDependencies: {
zzz: { path: 'foo', hash: 'bar' },
bar: { path: 'foo', hash: 'bar' },
aaa: { path: 'foo', hash: 'bar' },
zzz: 'bar',
bar: 'bar',
aaa: 'bar',
},
})
@@ -66,9 +66,9 @@ test('sorts keys alphabetically', () => {
},
},
patchedDependencies: {
aaa: { path: 'foo', hash: 'bar' },
bar: { path: 'foo', hash: 'bar' },
zzz: { path: 'foo', hash: 'bar' },
aaa: 'bar',
bar: 'bar',
zzz: 'bar',
},
})
expect(Object.keys(normalizedLockfile.importers?.foo.dependencies ?? {})).toStrictEqual(['aaa', 'bar', 'zzz'])

View File

@@ -1,13 +1,8 @@
import path from 'path'
import pMapValues from 'p-map-values'
import { createHexHashFromFile } from '@pnpm/crypto.hash'
import type { PatchFile } from '@pnpm/lockfile.types'
export async function calcPatchHashes (patches: Record<string, string>, lockfileDir: string): Promise<Record<string, PatchFile>> {
export async function calcPatchHashes (patches: Record<string, string>): Promise<Record<string, string>> {
return pMapValues.default(async (patchFilePath) => {
return {
hash: await createHexHashFromFile(patchFilePath),
path: path.relative(lockfileDir, patchFilePath).replaceAll('\\', '/'),
}
return createHexHashFromFile(patchFilePath)
}, patches)
}

View File

@@ -1,5 +1,5 @@
import type { Catalogs } from '@pnpm/catalogs.types'
import type { LockfileObject, PatchFile } from '@pnpm/lockfile.types'
import type { LockfileObject } from '@pnpm/lockfile.types'
import { allCatalogsAreUpToDate } from '@pnpm/lockfile.verification'
import { equals } from 'ramda'
@@ -32,7 +32,7 @@ export function getOutdatedLockfileSetting (
catalogs?: Catalogs
overrides?: Record<string, string>
packageExtensionsChecksum?: string
patchedDependencies?: Record<string, PatchFile>
patchedDependencies?: Record<string, string>
ignoredOptionalDependencies?: string[]
autoInstallPeers?: boolean
excludeLinksFromLockfile?: boolean

View File

@@ -1,8 +1,7 @@
import type { PatchFile } from '@pnpm/patching.types'
import type { DependenciesMeta, DepPath, ProjectId } from '@pnpm/types'
import type { PlatformAssetTarget } from '@pnpm/resolver-base'
export type { PatchFile, ProjectId }
export type { ProjectId }
export * from './lockfileFileTypes.js'
@@ -19,7 +18,7 @@ export interface LockfileBase {
lockfileVersion: string
overrides?: Record<string, string>
packageExtensionsChecksum?: string
patchedDependencies?: Record<string, PatchFile>
patchedDependencies?: Record<string, string>
pnpmfileChecksum?: string
settings?: LockfileSettings
time?: Record<string, string>

View File

@@ -1,9 +1,9 @@
import * as dp from '@pnpm/dependency-path'
import { PnpmError } from '@pnpm/error'
import type { PatchFile, PatchGroup, PatchGroupRecord } from '@pnpm/patching.types'
import type { PatchGroup, PatchGroupRecord, PatchInfo } from '@pnpm/patching.types'
import { validRange } from 'semver'
export function groupPatchedDependencies (patchedDependencies: Record<string, PatchFile>): PatchGroupRecord {
export function groupPatchedDependencies (patchedDependencies: Record<string, string | PatchInfo>): PatchGroupRecord {
const result: PatchGroupRecord = {}
function getGroup (name: string): PatchGroup {
let group: PatchGroup | undefined = result[name]
@@ -18,11 +18,12 @@ export function groupPatchedDependencies (patchedDependencies: Record<string, Pa
}
for (const key in patchedDependencies) {
const file = patchedDependencies[key]
const value = patchedDependencies[key]
const info = typeof value === 'string' ? { hash: value } : value
const { name, version, nonSemverVersion } = dp.parse(key)
if (name && version) {
getGroup(name).exact[version] = { file, key }
getGroup(name).exact[version] = { ...info, key }
continue
}
@@ -31,17 +32,17 @@ export function groupPatchedDependencies (patchedDependencies: Record<string, Pa
throw new PnpmError('PATCH_NON_SEMVER_RANGE', `${nonSemverVersion} is not a valid semantic version range.`)
}
if (nonSemverVersion.trim() === '*') {
getGroup(name).all = { file, key }
getGroup(name).all = { ...info, key }
} else {
getGroup(name).range.push({
version: nonSemverVersion,
patch: { file, key },
patch: { ...info, key },
})
}
continue
}
getGroup(key).all = { file, key }
getGroup(key).all = { ...info, key }
}
return result

View File

@@ -1,6 +1,5 @@
export {
type ExtendedPatchInfo,
type PatchFile,
type PatchInfo,
type PatchGroup,
type PatchGroupRangeItem,

View File

@@ -10,10 +10,7 @@ test('getPatchInfo() returns an exact version patch if the name and version matc
foo: {
exact: {
'1.0.0': {
file: {
path: 'patches/foo@1.0.0.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@1.0.0',
},
},
@@ -34,10 +31,7 @@ test('getPatchInfo() returns a range version patch if the name matches and the v
range: [{
version: '1',
patch: {
file: {
path: 'patches/foo@1.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@1',
},
}],
@@ -56,10 +50,7 @@ test('getPatchInfo() returns name-only patch if the name matches', () => {
exact: {},
range: [],
all: {
file: {
path: 'patches/foo.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo',
},
},
@@ -75,17 +66,11 @@ test('exact version patches override version range patches, version range patche
foo: {
exact: {
'1.0.0': {
file: {
path: 'patches/foo@1.0.0.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@1.0.0',
},
'1.1.0': {
file: {
path: 'patches/foo@1.1.0.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@1.1.0',
},
},
@@ -93,29 +78,20 @@ test('exact version patches override version range patches, version range patche
{
version: '1',
patch: {
file: {
path: 'patches/foo@1.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@1',
},
},
{
version: '2',
patch: {
file: {
path: 'patches/foo@2.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@2',
},
},
],
all: {
file: {
path: 'patches/foo.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo',
},
},
@@ -137,20 +113,14 @@ test('getPatchInfo(_, name, version) throws an error when name@version matches m
{
version: '>=1.0.0 <3.0.0',
patch: {
file: {
path: 'patches/foo_a.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@>=1.0.0 <3.0.0',
},
},
{
version: '>=2.0.0',
patch: {
file: {
path: 'patches/foo_b.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@>=2.0.0',
},
},
@@ -170,10 +140,7 @@ test('getPatchInfo(_, name, version) does not throw an error when name@version m
foo: {
exact: {
'2.1.0': {
file: {
path: 'patches/foo_a.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@>=1.0.0 <3.0.0',
},
},
@@ -181,20 +148,14 @@ test('getPatchInfo(_, name, version) does not throw an error when name@version m
{
version: '>=1.0.0 <3.0.0',
patch: {
file: {
path: 'patches/foo_b.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@>=1.0.0 <3.0.0',
},
},
{
version: '>=2.0.0',
patch: {
file: {
path: 'patches/foo_c.patch',
hash: '00000000000000000000000000000000',
},
hash: '00000000000000000000000000000000',
key: 'foo@>=2.0.0',
},
},

View File

@@ -1,4 +1,4 @@
import type { ExtendedPatchInfo, PatchFile, PatchGroupRecord } from '@pnpm/patching.types'
import type { ExtendedPatchInfo, PatchGroupRecord } from '@pnpm/patching.types'
import { groupPatchedDependencies } from '../src/groupPatchedDependencies.js'
function sanitizePatchGroupRecord (patchGroups: PatchGroupRecord): PatchGroupRecord {
@@ -11,51 +11,21 @@ function sanitizePatchGroupRecord (patchGroups: PatchGroupRecord): PatchGroupRec
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 patchedDependencies: Record<string, string> = {
'exact-version-only@0.0.0': '00000000000000000000000000000000',
'exact-version-only@1.2.3': '00000000000000000000000000000000',
'exact-version-only@2.1.0': '00000000000000000000000000000000',
'version-range-only@~1.2.0': '00000000000000000000000000000000',
'version-range-only@4': '00000000000000000000000000000000',
'star-version-range@*': '00000000000000000000000000000000',
'without-versions': '00000000000000000000000000000000',
'mixed-style@0.1.2': '00000000000000000000000000000000',
'mixed-style@1.x.x': '00000000000000000000000000000000',
'mixed-style': '00000000000000000000000000000000',
}
const info = (key: keyof typeof patchedDependencies): ExtendedPatchInfo => ({
key,
file: patchedDependencies[key],
hash: patchedDependencies[key],
})
expect(_groupPatchedDependencies(patchedDependencies)).toStrictEqual({
'exact-version-only': {
@@ -108,10 +78,7 @@ test('groups patchedDependencies according to names, match types, and versions',
test('errors on invalid version range', async () => {
expect(() => _groupPatchedDependencies({
'foo@link:packages/foo': {
hash: '00000000000000000000000000000000',
path: 'patches/foo.patch',
},
'foo@link:packages/foo': '00000000000000000000000000000000',
})).toThrow(expect.objectContaining({
code: 'ERR_PNPM_PATCH_NON_SEMVER_RANGE',
}))

View File

@@ -1,7 +1,6 @@
import type { PatchFile } from '@pnpm/patching.types'
import { getPatchInfo, groupPatchedDependencies } from '../src/index.js'
const _getPatchInfo = (patchedDependencies: Record<string, PatchFile>, name: string, version: string) =>
const _getPatchInfo = (patchedDependencies: Record<string, string>, name: string, version: string) =>
getPatchInfo(groupPatchedDependencies(patchedDependencies), name, version)
test('getPatchInfo(undefined, ...) returns undefined', () => {
@@ -10,67 +9,40 @@ test('getPatchInfo(undefined, ...) returns undefined', () => {
test('getPatchInfo(_, name, version) if name@version exists', () => {
expect(_getPatchInfo({
'foo@1.0.0': {
path: 'patches/foo@1.0.0.patch',
hash: '00000000000000000000000000000000',
},
'foo@1.0.0': '00000000000000000000000000000000',
}, 'foo', '1.0.0')).toStrictEqual({
file: {
path: 'patches/foo@1.0.0.patch',
hash: expect.any(String),
},
hash: expect.any(String),
key: 'foo@1.0.0',
})
})
test('getPatchInfo(_, name, version) if name exists but name@version does not exist', () => {
expect(_getPatchInfo({
foo: {
path: 'patches/foo.patch',
hash: '00000000000000000000000000000000',
},
foo: '00000000000000000000000000000000',
}, 'foo', '1.0.0')).toStrictEqual({
file: {
path: 'patches/foo.patch',
hash: expect.any(String),
},
hash: expect.any(String),
key: 'foo',
})
})
test('getPatchInfo(_, name, version) prioritizes name@version over name if both exist', () => {
expect(_getPatchInfo({
foo: {
path: 'patches/foo.patch',
hash: '00000000000000000000000000000000',
},
'foo@1.0.0': {
path: 'patches/foo@1.0.0.patch',
hash: '00000000000000000000000000000000',
},
foo: '00000000000000000000000000000000',
'foo@1.0.0': '00000000000000000000000000000000',
}, 'foo', '1.0.0')).toStrictEqual({
file: {
path: 'patches/foo@1.0.0.patch',
hash: expect.any(String),
},
hash: expect.any(String),
key: 'foo@1.0.0',
})
})
test('getPatchInfo(_, name, version) does not access wrong name', () => {
expect(_getPatchInfo({
'bar@1.0.0': {
path: 'patches/bar@1.0.0.patch',
hash: '00000000000000000000000000000000',
},
'bar@1.0.0': '00000000000000000000000000000000',
}, 'foo', '1.0.0')).toBeUndefined()
})
test('getPatchInfo(_, name, version) does not access wrong version', () => {
expect(_getPatchInfo({
'foo@2.0.0': {
path: 'patches/foo@2.0.0.patch',
hash: '00000000000000000000000000000000',
},
'foo@2.0.0': '00000000000000000000000000000000',
}, 'foo', '1.0.0')).toBeUndefined()
})

View File

@@ -1,11 +1,6 @@
export interface PatchFile {
path: string
hash: string
}
// TODO: replace all occurrences of PatchInfo with PatchFile before the next major version is released
export interface PatchInfo {
file: PatchFile
hash: string
patchFilePath?: string
}
export interface ExtendedPatchInfo extends PatchInfo {

View File

@@ -425,14 +425,18 @@ export async function mutateModules (
const pnpmfileChecksum = await opts.hooks.calculatePnpmfileChecksum?.()
const patchedDependencies = opts.ignorePackageManifest
? ctx.wantedLockfile.patchedDependencies
: (opts.patchedDependencies ? await calcPatchHashes(opts.patchedDependencies, opts.lockfileDir) : {})
const patchedDependenciesWithResolvedPath = patchedDependencies
? mapValues((patchFile) => ({
hash: patchFile.hash,
path: path.join(opts.lockfileDir, patchFile.path),
}), patchedDependencies)
: undefined
const patchGroups = patchedDependenciesWithResolvedPath && groupPatchedDependencies(patchedDependenciesWithResolvedPath)
: (opts.patchedDependencies ? await calcPatchHashes(opts.patchedDependencies) : {})
const patchGroupInput = opts.patchedDependencies
? Object.fromEntries(
Object.entries(patchedDependencies ?? {}).map(([key, hash]) => {
const patchFilePath = opts.patchedDependencies![key]
? path.resolve(opts.lockfileDir, opts.patchedDependencies![key])
: undefined
return [key, { hash, patchFilePath }]
})
)
: patchedDependencies
const patchGroups = patchGroupInput ? groupPatchedDependencies(patchGroupInput) : undefined
const frozenLockfile = opts.frozenLockfile ||
opts.frozenLockfileIfExists && ctx.existsNonEmptyWantedLockfile
let outdatedLockfileSettings = false

View File

@@ -467,7 +467,7 @@ async function linkAllPkgs (
if (opts?.allowBuild?.(depNode.name, depNode.version) !== false) {
sideEffectsCacheKey = calcDepState(opts.depGraph, opts.depsStateCache, depNode.depPath, {
includeDepGraphHash: !opts.ignoreScripts && depNode.requiresBuild, // true when is built
patchFileHash: depNode.patch?.file.hash,
patchFileHash: depNode.patch?.hash,
})
}
}

View File

@@ -54,10 +54,7 @@ test('patch package with exact version', async () => {
const patchFileHash = await createHexHashFromFile(patchPath)
const lockfile = project.readLockfile()
expect(lockfile.patchedDependencies).toStrictEqual({
'is-positive@1.0.0': {
path: path.relative(process.cwd(), patchedDependencies['is-positive@1.0.0']).replaceAll('\\', '/'),
hash: patchFileHash,
},
'is-positive@1.0.0': patchFileHash,
})
expect(lockfile.snapshots[`is-positive@1.0.0(patch_hash=${patchFileHash})`]).toBeTruthy()
@@ -154,10 +151,7 @@ test('patch package with version range', async () => {
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,
},
'is-positive@1': patchFileHash,
})
expect(lockfile.snapshots[`is-positive@1.0.0(patch_hash=${patchFileHash})`]).toBeTruthy()
@@ -326,10 +320,7 @@ test('patch package when scripts are ignored', async () => {
const patchFileHash = await createHexHashFromFile(patchPath)
const lockfile = project.readLockfile()
expect(lockfile.patchedDependencies).toStrictEqual({
'is-positive@1.0.0': {
path: path.relative(process.cwd(), patchedDependencies['is-positive@1.0.0']).replaceAll('\\', '/'),
hash: patchFileHash,
},
'is-positive@1.0.0': patchFileHash,
})
expect(lockfile.snapshots[`is-positive@1.0.0(patch_hash=${patchFileHash})`]).toBeTruthy()
@@ -419,10 +410,7 @@ test('patch package when the package is not in allowBuilds list', async () => {
const patchFileHash = await createHexHashFromFile(patchPath)
const lockfile = project.readLockfile()
expect(lockfile.patchedDependencies).toStrictEqual({
'is-positive@1.0.0': {
path: path.relative(process.cwd(), patchedDependencies['is-positive@1.0.0']).replaceAll('\\', '/'),
hash: patchFileHash,
},
'is-positive@1.0.0': patchFileHash,
})
expect(lockfile.snapshots[`is-positive@1.0.0(patch_hash=${patchFileHash})`]).toBeTruthy()

View File

@@ -890,7 +890,7 @@ async function linkAllPkgs (
if (opts?.allowBuild?.(depNode.name, depNode.version) !== false) {
sideEffectsCacheKey = calcDepState(opts.depGraph, opts.depsStateCache, depNode.dir, {
includeDepGraphHash: !opts.ignoreScripts && depNode.requiresBuild, // true when is built
patchFileHash: depNode.patch?.file.hash,
patchFileHash: depNode.patch?.hash,
})
}
}

View File

@@ -118,7 +118,7 @@ async function linkAllPkgsInOrder (
if (opts?.allowBuild?.(depNode.name, depNode.version) !== false) {
sideEffectsCacheKey = _calcDepState(dir, {
includeDepGraphHash: !opts.ignoreScripts && depNode.requiresBuild, // true when is built
patchFileHash: depNode.patch?.file.hash,
patchFileHash: depNode.patch?.hash,
})
}
}

View File

@@ -1480,7 +1480,7 @@ async function resolveDependency (
const patch = getPatchInfo(ctx.patchedDependencies, pkg.name, pkg.version)
if (patch) {
ctx.appliedPatches.add(patch.key)
pkgIdWithPatchHash = `${pkgIdWithPatchHash}(patch_hash=${patch.file.hash})` as PkgIdWithPatchHash
pkgIdWithPatchHash = `${pkgIdWithPatchHash}(patch_hash=${patch.hash})` as PkgIdWithPatchHash
}
// We are building the dependency tree only until there are new packages

3
pnpm-lock.yaml generated
View File

@@ -2507,6 +2507,9 @@ importers:
'@pnpm/deps.graph-sequencer':
specifier: workspace:*
version: link:../../deps/graph-sequencer
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/fs.hard-link-dir':
specifier: workspace:*
version: link:../../fs/hard-link-dir

View File

@@ -35,8 +35,7 @@ test('patch from configuration dependency is applied via updateConfig hook', asy
expect(fs.existsSync('node_modules/@pnpm.e2e/foo/index.js')).toBeTruthy()
const lockfile = project.readLockfile()
// The patch path goes through the global virtual store since config deps are symlinked there
expect(lockfile.patchedDependencies['@pnpm.e2e/foo'].path).toContain('@pnpm.e2e/has-patch-for-foo/@pnpm.e2e__foo@100.0.0.patch')
expect(lockfile.patchedDependencies['@pnpm.e2e/foo']).toEqual(expect.any(String))
})
test('catalog applied by configurational dependency hook', async () => {

View File

@@ -152,17 +152,14 @@ export function createDeployFiles ({
}
if (lockfile.patchedDependencies) {
result.lockfile.patchedDependencies = {}
result.manifest.pnpm!.patchedDependencies = {}
for (const name in lockfile.patchedDependencies) {
const patchInfo = lockfile.patchedDependencies[name]
const resolvedPath = path.resolve(rootProjectManifestDir, patchInfo.path)
const relativePath = normalizePath(path.relative(deployDir, resolvedPath))
result.manifest.pnpm!.patchedDependencies[name] = relativePath
result.lockfile.patchedDependencies[name] = {
hash: patchInfo.hash,
path: relativePath,
const manifestPatchedDeps = rootProjectManifest?.pnpm?.patchedDependencies
if (manifestPatchedDeps) {
result.lockfile.patchedDependencies = { ...lockfile.patchedDependencies }
result.manifest.pnpm!.patchedDependencies = {}
for (const name in manifestPatchedDeps) {
const resolvedPath = path.resolve(rootProjectManifestDir, manifestPatchedDeps[name])
const relativePath = normalizePath(path.relative(deployDir, resolvedPath))
result.manifest.pnpm!.patchedDependencies[name] = relativePath
}
}
}

View File

@@ -4,7 +4,7 @@ import url from 'url'
import { install } from '@pnpm/plugin-commands-installation'
import { assertProject } from '@pnpm/assert-project'
import { preparePackages } from '@pnpm/prepare'
import type { PatchFile, LockfileFile, LockfilePackageSnapshot } from '@pnpm/lockfile.types'
import type { LockfileFile, LockfilePackageSnapshot } from '@pnpm/lockfile.types'
import { filterPackagesFromDir } from '@pnpm/workspace.filter-packages-from-dir'
import { fixtures } from '@pnpm/test-fixtures'
import type { ProjectManifest } from '@pnpm/types'
@@ -839,6 +839,8 @@ test('deploy with a shared lockfile should correctly handle patchedDependencies'
dir: process.cwd(),
patchedDependencies,
recursive: true,
rootProjectManifest: preparedManifests.root,
rootProjectManifestDir: process.cwd(),
selectedProjectsGraph,
sharedWorkspaceLockfile: true,
lockfileDir: process.cwd(),
@@ -850,13 +852,10 @@ test('deploy with a shared lockfile should correctly handle patchedDependencies'
const lockfile = project.readLockfile()
expect(lockfile.patchedDependencies).toStrictEqual({
'is-positive': {
hash: expect.any(String),
path: '../__patches__/is-positive.patch',
},
} as Record<string, PatchFile>)
'is-positive': expect.any(String),
})
const patchFile = lockfile.patchedDependencies['is-positive']
const patchHash = lockfile.patchedDependencies['is-positive']
const manifest = readPackageJson('deploy') as ProjectManifest
expect(manifest).toStrictEqual({
@@ -878,7 +877,7 @@ test('deploy with a shared lockfile should correctly handle patchedDependencies'
expect(project1Name).toBeDefined()
if (process.platform !== 'win32') {
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project1Name}/node_modules/is-positive`)).toBe(
path.resolve(`deploy/node_modules/.pnpm/is-positive@1.0.0_patch_hash=${patchFile.hash}/node_modules/is-positive`)
path.resolve(`deploy/node_modules/.pnpm/is-positive@1.0.0_patch_hash=${patchHash}/node_modules/is-positive`)
)
}
expect(