mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-18 19:52:31 -04:00
fix(pnpm): resolve relative patchedDependencies against lockfileDir (#12762)
Ensure patchedDependencies paths are resolved relative to the lockfile directory before computing patch hashes. This fixes cases where relative patch paths were incorrectly resolved against the workspace root instead of the lockfile location.
This commit is contained in:
8
.changeset/patched-deps-lockfile-dir.md
Normal file
8
.changeset/patched-deps-lockfile-dir.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@pnpm/lockfile.settings-checker": minor
|
||||
"@pnpm/deps.status": patch
|
||||
"@pnpm/installing.deps-installer": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Relative paths in `patchedDependencies` are now resolved against the lockfile directory when computing patch file hashes, so running `pnpm install` from a subdirectory no longer fails with `ENOENT` looking for the patch file in the wrong location [#12762](https://github.com/pnpm/pnpm/pull/12762).
|
||||
@@ -1665,8 +1665,6 @@ impl Config {
|
||||
/// `patchedDependencies` map the lockfile records: each configured
|
||||
/// key mapped to its patch file's SHA-256 hex digest.
|
||||
///
|
||||
/// The configured patch paths are resolved against the manifest
|
||||
/// dir before hashing.
|
||||
/// Distinct from [`Self::resolved_patched_dependencies`], which
|
||||
/// groups the same entries by package name for the resolver — this
|
||||
/// keeps the user's verbatim keys so the lockfile is byte-faithful
|
||||
|
||||
@@ -46,7 +46,11 @@ impl From<PatchNonSemverRangeError> for ResolvePatchedDependenciesError {
|
||||
/// Each raw path is resolved against `workspace_dir`, hashed into a
|
||||
/// [`PatchInput`] entry, and then grouped. pnpm v11 reads install
|
||||
/// settings (including `patchedDependencies`) from the *workspace*
|
||||
/// manifest, not from `package.json`'s `pnpm` field.
|
||||
/// manifest, not from `package.json`'s `pnpm` field, and anchors
|
||||
/// still-relative patch paths on `lockfileDir` when hashing and
|
||||
/// applying patches. Pacquet's lockfile always lives at the workspace
|
||||
/// root, so `workspace_dir` is that same base; if pacquet ever gains a
|
||||
/// separate `lockfileDir`, this base must follow it to stay in parity.
|
||||
///
|
||||
/// [`BTreeMap`]: std::collections::BTreeMap
|
||||
/// [`PatchGroup::range`]: crate::PatchGroup::range
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
calcPatchHashes,
|
||||
createOverridesMapFromParsed,
|
||||
getOutdatedLockfileSetting,
|
||||
resolvePatchedDependencies,
|
||||
} from '@pnpm/lockfile.settings-checker'
|
||||
import {
|
||||
getWorkspacePackagesByDirectory,
|
||||
@@ -660,11 +661,12 @@ async function assertWantedLockfileUpToDate (
|
||||
wantedLockfileDir,
|
||||
} = opts
|
||||
|
||||
const resolvedPatchedDeps = resolvePatchedDependencies(config.patchedDependencies, wantedLockfileDir)
|
||||
const [
|
||||
patchedDependencies,
|
||||
pnpmfileChecksum,
|
||||
] = await Promise.all([
|
||||
calcPatchHashes(config.patchedDependencies ?? {}),
|
||||
calcPatchHashes(resolvedPatchedDeps ?? {}),
|
||||
config.hooks?.calculatePnpmfileChecksum?.(),
|
||||
])
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
calcPatchHashes,
|
||||
createOverridesMapFromParsed,
|
||||
getOutdatedLockfileSetting,
|
||||
resolvePatchedDependencies,
|
||||
} from '@pnpm/lockfile.settings-checker'
|
||||
import { PACKAGE_MAP_FILENAME, writePackageMap, writePnpFile } from '@pnpm/lockfile.to-pnp'
|
||||
import { allProjectsAreUpToDate, satisfiesPackageManifest } from '@pnpm/lockfile.verification'
|
||||
@@ -638,21 +639,18 @@ export async function mutateModules (
|
||||
}
|
||||
const packageExtensionsChecksum = hashObjectNullableWithPrefix(opts.packageExtensions)
|
||||
const pnpmfileChecksum = await opts.hooks.calculatePnpmfileChecksum?.()
|
||||
const resolvedPatchedDeps = resolvePatchedDependencies(opts.patchedDependencies, opts.lockfileDir)
|
||||
const patchedDependencies = opts.ignorePackageManifest
|
||||
? ctx.wantedLockfile.patchedDependencies
|
||||
: (opts.patchedDependencies ? await calcPatchHashes(opts.patchedDependencies) : {})
|
||||
const patchGroupInput = opts.patchedDependencies
|
||||
: (resolvedPatchedDeps ? await calcPatchHashes(resolvedPatchedDeps) : {})
|
||||
const patchGroupInput = resolvedPatchedDeps
|
||||
? Object.fromEntries(
|
||||
Object.entries(patchedDependencies ?? {}).map(([key, hash]) => {
|
||||
let patchFilePath = opts.patchedDependencies![key]
|
||||
? path.resolve(opts.lockfileDir, opts.patchedDependencies![key])
|
||||
: undefined
|
||||
let patchFilePath: string | undefined = resolvedPatchedDeps[key]
|
||||
if (!patchFilePath) {
|
||||
const lastAt = key.lastIndexOf('@')
|
||||
const pkgName = lastAt > 0 ? key.slice(0, lastAt) : key
|
||||
if (opts.patchedDependencies![pkgName]) {
|
||||
patchFilePath = path.resolve(opts.lockfileDir, opts.patchedDependencies![pkgName])
|
||||
}
|
||||
patchFilePath = resolvedPatchedDeps[pkgName]
|
||||
}
|
||||
return [key, { hash, patchFilePath }]
|
||||
})
|
||||
|
||||
@@ -2,15 +2,17 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { afterAll, expect, jest, test } from '@jest/globals'
|
||||
import { ENGINE_NAME } from '@pnpm/constants'
|
||||
import { ENGINE_NAME, WANTED_LOCKFILE } from '@pnpm/constants'
|
||||
import { createHexHashFromFile } from '@pnpm/crypto.hash'
|
||||
import { install } from '@pnpm/installing.deps-installer'
|
||||
import type { LockfileFile } from '@pnpm/lockfile.types'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import type { PackageFilesIndex } from '@pnpm/store.cafs'
|
||||
import { StoreIndex, storeIndexKey } from '@pnpm/store.index'
|
||||
import { fixtures } from '@pnpm/test-fixtures'
|
||||
import { getIntegrity } from '@pnpm/testing.registry-mock'
|
||||
import { rimrafSync } from '@zkochan/rimraf'
|
||||
import { readYamlFileSync } from 'read-yaml-file'
|
||||
|
||||
import { testDefaults } from '../utils/index.js'
|
||||
|
||||
@@ -570,3 +572,39 @@ test('patch package should fail when the name-only range patch fails to apply',
|
||||
|
||||
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).not.toContain('// patched')
|
||||
})
|
||||
|
||||
test('patch with relative paths resolved against lockfileDir', async () => {
|
||||
prepareEmpty()
|
||||
// The lockfile and the patches dir live in the parent of the project dir
|
||||
// (and of the cwd), so the still-relative patchedDependencies paths can
|
||||
// only be read when resolved against lockfileDir.
|
||||
const lockfileDir = path.resolve('..')
|
||||
const patchesDir = path.join(lockfileDir, 'patches')
|
||||
fs.mkdirSync(patchesDir, { recursive: true })
|
||||
fs.copyFileSync(
|
||||
path.join(f.find('patch-pkg'), 'is-positive@1.0.0.patch'),
|
||||
path.join(patchesDir, 'is-positive@1.0.0.patch')
|
||||
)
|
||||
|
||||
const patchedDependencies = {
|
||||
'is-positive@1.0.0': 'patches/is-positive@1.0.0.patch',
|
||||
}
|
||||
const opts = testDefaults({
|
||||
fastUnpack: false,
|
||||
patchedDependencies,
|
||||
lockfileDir,
|
||||
}, {}, {}, { packageImportMethod: 'hardlink' })
|
||||
await install({
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
}, opts)
|
||||
|
||||
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).toContain('// patched')
|
||||
|
||||
const patchFileHash = await createHexHashFromFile(path.join(patchesDir, 'is-positive@1.0.0.patch'))
|
||||
const lockfile = readYamlFileSync<LockfileFile>(path.join(lockfileDir, WANTED_LOCKFILE))
|
||||
expect(lockfile.patchedDependencies).toStrictEqual({
|
||||
'is-positive@1.0.0': patchFileHash,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { calcPatchHashes } from './calcPatchHashes.js'
|
||||
export { createOverridesMapFromParsed } from './createOverridesMapFromParsed.js'
|
||||
export { type ChangedField, getOutdatedLockfileSetting } from './getOutdatedLockfileSetting.js'
|
||||
export { resolvePatchedDependencies } from './resolvePatchedDependencies.js'
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { map as mapValues } from 'ramda'
|
||||
|
||||
export function resolvePatchedDependencies (
|
||||
patchedDependencies: Record<string, string> | undefined,
|
||||
baseDir: string
|
||||
): Record<string, string> | undefined {
|
||||
if (!patchedDependencies) return undefined
|
||||
return mapValues((patchFile) => path.resolve(baseDir, patchFile), patchedDependencies)
|
||||
}
|
||||
Reference in New Issue
Block a user