pnpm: pnpm patch does not seem to work with workspaces

I am attempting to patch a transitive dependency which is in the dependency tree of multiple workspaces. In this example, the dependency is extract-files@9.0.0.

From the workspace root, I run pnpm patch extract-files@9.0.0 which gives me the temporary folder link as expected. I open the folder and make the necessary changes. After completing I run pnpm patch-commit <path> which generates the patch file in the root patches folder, adds a pnpm.patchedDependencies property to the root package.json, and adds a patchedDependencies section at the top of the pnpm-lock.yaml file. However, it does not alter any of the references to that dependency elsewhere in the lockfile, and as a result, the versions being used by the workspaces are unchanged.

I revert all changes and then tried making my current working directory one of the workspace folders. I follow the same exact procedure described above and the end result is different. The patches folder is still added to the root, the root package.json still has the pnpm.patchedDependencies property, and the patchedDependencies section is added to the top of the pnpm-lock.yaml file. However, this time the lockfile has also been modified to patch the versions of that dependency relative to that workspace. My initial thought here is that the patch needs to be applied one workspace at a time.

I then change my current working directory to another workspace. I repeat the process. This time the temporary folder already shows the patch (this seems expected). I try committing the patch but there is no change. The patch was not applied to that workspace.

Basically what I’m looking to have happen here is to either apply the patch to all workspaces or at least have a procedure where I can patch each workspace one a time. Ideally something like:

pnpm -r patch dependency@version or pnpm --filter=<workspace> patch dependency@version

It’s definitely possible that I’m doing something wrong here so if there is a procedure that should be taken to make this happen please let me know.

Possibly related issue: #5255

pnpm version:

7.26.3

Code to reproduce the issue:

Expected behavior:

Patches all versions of the dependency

Actual behavior:

Does not patch any versions of the dependency

Additional information:

  • node -v prints: v16.16.0
  • Windows, macOS, or Linux?: Windows

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 1
  • Comments: 23 (7 by maintainers)

Commits related to this issue

Most upvoted comments

@shawnmcknight I think I reproduced the problem and I will try to solve it

@await-ovo I’ve set up a scenario that should be easier to see the problem. Steps:

  • Reclone the repo at https://github.com/shawnmcknight/pnpm-patch-tests. The main branch does not have the patch applied.
  • Run pnpm install to generate the unpatched node_modules
  • git checkout apply-patch. This branch has the patch applied.
  • Run pnpm install. The workspaces’ node_modules will not be updated with the patch.
  • git checkout fixed-lockfile. This branch has had pnpm install --fix-lockfile run on it and the lockfile is in a valid state.
  • Run pnpm install. The workspaces’ node_modules will now be updated with the patch.

The main difference between this flow and what you originally saw is that the node_modules will be populated in an unpatched state. Running pnpm install on the apply-patch branch does not properly apply the patch here because the lockfile is not in a proper state. However, once you run pnpm install on the fixed-lockfile branch everything will work properly.

I debugged through the workflow differences between pnpm patch-commit and pnpm install --fix-lockfile today to try to determine why fixing the lockfile makes the lockfile correct after the patch has been applied but the original patch commit did not.

From what I can tell, the primary difference between the two workflows is that pnpm install will default recursive operations and filter to all available workspaces by default due to this logic in the main cli. This ensures that when fixing the lockfile all packages are included in the evaluation. However, since the patch-commit command is not included in this condition, no packages filters are added, thus only the root package is included in the processing.

I tried changing that line from:

(cmd === 'install' || cmd === 'import' || cmd === "dedupe")

to:

(cmd === 'install' || cmd === 'import' || cmd === "dedupe" || cmd === "patch-commit")

in the hopes that this would allow things to work properly.

That change did make it so all packages were included when the patch-commit workflow called the installer. However, this introduced a different problem because the packages’ manifests were read into variables at the time of filtering. Since patch-commit later modifies the root package’s manifest to augment the pnpm.patchedDependencies property, the in-memory root manifest is now out of sync with the on-disk root manifest. When getOptionsFromRootManifest is called, because the in-memory manifest does not have patchedDependencies applied within it, the installer will end up doing nothing. In the existing workflow, the root manifest is read in later in installDeps because there haven’t been any projects previously selected so the patchedDependencies property is available. So unfortunately my naïve approach to potentially fixing this didn’t pan out.

I don’t know enough about the pnpm codebase to determine how this workflow should be fixed. However, from what I can tell, the patch-commit process needs to be able to process all packages but also be aware of the patch being applied to the root manifest file. My best guess would be to add logic after the root manifest had had its pnpm.patchedDependencies property updated to execute similar filtering logic that is applied when running pnpm install to build the options for allProjects, allProjectsGraph, and selectedProjectsGraph to supply to install.

@zkochan Does this provide any additional insight into the problem? Does my proposed solution to supply project filters to install make sense? I am willing to create a PR to fix this, but I’d like to go down a path that make sense.