refactor: simplify patchedDependencies lockfile format#10911
Conversation
…rs 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>
There was a problem hiding this comment.
Pull request overview
Refactors the lockfile patchedDependencies representation to store only patch hashes (selector → hash), simplifying patch-related types and hashing logic across install/resolve/link flows.
Changes:
- Change lockfile
patchedDependenciesfrom an object ({ path, hash }) to a plain hash string per selector. - Simplify patching types (
PatchInfo) and update patch grouping / lookup utilities and tests to the new format. - Update install/link/build cache key generation and multiple tests to use
patch.hashand the new lockfile shape.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| releasing/plugin-commands-deploy/test/shared-lockfile.test.ts | Updates deploy test expectations for new patchedDependencies format and patch hash usage. |
| pkg-manager/resolve-dependencies/src/resolveDependencies.ts | Uses patch.hash when creating patched package IDs. |
| pkg-manager/headless/src/linkHoistedModules.ts | Uses depNode.patch?.hash in dep state calculation. |
| pkg-manager/headless/src/index.ts | Uses depNode.patch?.hash in dep state calculation. |
| pkg-manager/core/test/install/patch.ts | Updates patch install tests to expect selector → hash in lockfile. |
| pkg-manager/core/src/install/link.ts | Uses depNode.patch?.hash in dep state calculation. |
| pkg-manager/core/src/install/index.ts | Drops path-resolution for patches and groups patched deps from hash-only map. |
| patching/types/src/index.ts | Simplifies PatchInfo to { hash: string }. |
| patching/config/test/index.test.ts | Updates getPatchInfo tests for hash-only patch info. |
| patching/config/test/groupPatchedDependencies.test.ts | Updates grouping tests for selector → hash input and hash-only patch info. |
| patching/config/src/index.ts | Stops exporting removed PatchFile type. |
| patching/config/src/groupPatchedDependencies.ts | Changes grouping input to selector → hash and emits hash-only patch info. |
| lockfile/types/src/index.ts | Updates lockfile type for patchedDependencies to Record<string, string>. |
| lockfile/settings-checker/src/getOutdatedLockfileSetting.ts | Updates outdated-setting checker typing for hash-only patched deps. |
| lockfile/settings-checker/src/calcPatchHashes.ts | Changes calcPatchHashes to return only hashes and removes lockfileDir param. |
| lockfile/fs/test/sortLockfileKeys.test.ts | Updates sort-key test fixtures for hash-only patched deps. |
| exec/build-modules/src/index.ts | Updates dep state calculation to use depNode.patch?.hash. |
Comments suppressed due to low confidence (1)
patching/types/src/index.ts:7
PatchInfono longer contains any patch file path information, but the build/install pipeline applies patches from disk and needs to know where the patch file is located. With this type change, existing code that expectspatch.file.path(e.g. in build-modules) can no longer type-check, and it’s unclear how runtime patch application will locate patch files. Consider introducing a separate runtime patch type that includes bothhashand an absolutepath(derived from user config), while keeping the lockfile representation as selector→hash.
export interface PatchInfo {
hash: string
}
export interface ExtendedPatchInfo extends PatchInfo {
key: string
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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>
…cation 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>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- 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>
Also guard against undefined patchedDependencies entry when ignorePackageManifest is true. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addressing Copilot review commentsAll 7 Copilot review threads have been resolved. Here's what was done: Runtime patch file path (
|
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>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| result.lockfile.patchedDependencies = { ...lockfile.patchedDependencies } | ||
| const manifestPatchedDeps = rootProjectManifest?.pnpm?.patchedDependencies | ||
| if (manifestPatchedDeps) { | ||
| 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 |
There was a problem hiding this comment.
If lockfile.patchedDependencies is set but rootProjectManifest?.pnpm?.patchedDependencies is missing, the deploy output keeps patch hashes in the lockfile but emits no patch file paths in the manifest. Since patch paths are no longer stored in the lockfile, the deployed project may fail to install (especially with --frozen-lockfile). Consider requiring manifestPatchedDeps (and that it covers all patch keys) when copying lockfile.patchedDependencies, and otherwise throw a typed error with a hint or omit patchedDependencies from the deploy output.
| result.lockfile.patchedDependencies = { ...lockfile.patchedDependencies } | |
| const manifestPatchedDeps = rootProjectManifest?.pnpm?.patchedDependencies | |
| if (manifestPatchedDeps) { | |
| 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 | |
| const lockfilePatchedDeps = lockfile.patchedDependencies | |
| const manifestPatchedDeps = rootProjectManifest?.pnpm?.patchedDependencies | |
| // Only propagate patchedDependencies when the manifest defines them | |
| // and covers all patch keys present in the lockfile. | |
| if (manifestPatchedDeps) { | |
| const missingPatchKeys = Object.keys(lockfilePatchedDeps).filter( | |
| (name) => manifestPatchedDeps[name] === undefined | |
| ) | |
| if (missingPatchKeys.length === 0) { | |
| result.lockfile.patchedDependencies = { ...lockfilePatchedDeps } | |
| 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 | |
| } |
| if (!depNode.patch.patchFilePath) { | ||
| throw new Error( | ||
| `Cannot apply patch for ${depPath}: patch file path is missing` | ||
| ) |
There was a problem hiding this comment.
Throwing a plain Error here produces a less actionable failure and bypasses the existing patch-error handling that relies on pnpm-specific error codes. Consider throwing a PnpmError (or another existing typed error) that includes the patch selector / package and a hint to configure patchedDependencies, so users get a consistent error code and message.
- 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>
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>
Summary
patchedDependenciesfield in the lockfile fromRecord<string, { path: string, hash: string }>toRecord<string, string>(selector → hash)PatchFiletype, simplifyPatchInfoto{ hash: string }pathfield — patch file paths come from user config (opts.patchedDependencies), not from the lockfilecalcPatchHashesto return just hashes (no longer needslockfileDirparam)Test plan
groupPatchedDependenciestests verify new formatgetPatchInfotests verify new formatsortLockfileKeystest updated for new format🤖 Generated with Claude Code