Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pnpm.overrides doesn't override peerDependencies #6759

Closed
stefanmaric opened this issue Jul 3, 2023 · 8 comments · Fixed by #6955
Closed

pnpm.overrides doesn't override peerDependencies #6759

stefanmaric opened this issue Jul 3, 2023 · 8 comments · Fixed by #6955

Comments

@stefanmaric
Copy link

pnpm version: v8.6.6

Code to reproduce the issue:

Init a new pnpm package:

pnpm init -y

Add react-dom:

pnpm add react-dom

Check the installed dependencies:

ls -1 ./node_modules/.pnpm/

You should see something like this:

js-tokens@4.0.0/
lock.yaml
loose-envify@1.4.0/
node_modules/
react-dom@18.2.0_react@18.2.0/
react@18.2.0/
scheduler@0.23.0/

react@18.2.0 was installed because it is a peerDependency from react-dom@18.2.0 and auto-install-peers is true by default in pnpm v8.

Now pin the react dependency to an older version using pnpm.overrides inside package.json:

diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -11,5 +11,10 @@
   "license": "ISC",
   "dependencies": {
     "react-dom": "^18.2.0"
+  },
+  "pnpm": {
+    "overrides": {
+      "react": "18.1.0"
+    }
   }
 }

Remove lockfile and node_modules for good measure:

rm -r node_modules pnpm-lock.yaml

Install again:

pnpm install

Expected behavior:

I would expect the installed react version to follow the override and be 18.1.0.

For example:

$ ls -1 ./node_modules/.pnpm/

js-tokens@4.0.0/
lock.yaml
loose-envify@1.4.0/
node_modules/
react-dom@18.2.0_react@18.1.0/
react@18.1.0/
scheduler@0.23.0/

Actual behavior:

Version 18.2.0 of react is still installed:

$ ls -1 ./node_modules/.pnpm/

js-tokens@4.0.0/
lock.yaml
loose-envify@1.4.0/
node_modules/
react-dom@18.2.0_react@18.2.0/
react@18.2.0/
scheduler@0.23.0/

Additional information:

  • node -v prints: v20.3.1
  • Windows, macOS, or Linux?: MacOS and Linux

To workaround this issue, I get the desired behaviour using the following pnpm hook:

// .pnpmfile.cjs

const rootPackage = require('./package.json')

const rootDependencies = {
  ...rootPackage.devDependencies,
  ...rootPackage.peerDependencies,
  ...rootPackage.dependencies,
}

const OVERRIDES = rootPackage.pnpm.overrides

const RESOLVED_VERSIONS = Object.fromEntries(
  Object.entries(OVERRIDES).map(([pkgName, version]) => {
    if (!version.startsWith('$')) return [pkgName, version]

    const pkgRef = version.slice(1)

    if (!(pkgRef in rootDependencies)) {
      throw new Error(
        `The package ${pkgRef} was referenced in OVERRIDES but was not found in package.json`
      )
    }

    return [pkgName, rootDependencies[pkgRef]]
  })
)

const PACKAGE_LOOKUP = Object.keys(RESOLVED_VERSIONS)

function readPackage(pkg, context) {
  for (const target of PACKAGE_LOOKUP) {
    if (pkg.dependencies && target in pkg.dependencies) {
      context.log(
        `Overriding ${target}@${pkg.dependencies[target]} of ${pkg.name} dependencies with ${target}@${RESOLVED_VERSIONS[target]}`
      )
      pkg.dependencies[target] = RESOLVED_VERSIONS[target]
    }
    if (pkg.devDependencies && target in pkg.devDependencies) {
      context.log(
        `Overriding ${target}@${pkg.devDependencies[target]} of ${pkg.name} devDependencies with ${target}@${RESOLVED_VERSIONS[target]}`
      )
      pkg.devDependencies[target] = RESOLVED_VERSIONS[target]
    }
    if (pkg.peerDependencies && target in pkg.peerDependencies) {
      context.log(
        `Overriding ${target}@${pkg.peerDependencies[target]} of ${pkg.name} peerDependencies with ${target}@${RESOLVED_VERSIONS[target]}`
      )
      pkg.peerDependencies[target] = RESOLVED_VERSIONS[target]
    }
  }

  return pkg
}

module.exports = {
  hooks: {
    readPackage,
  },
}
@stefanmaric
Copy link
Author

Similar in nature to: #4097

@zkochan
Copy link
Member

zkochan commented Jul 3, 2023

Why don't you just install react v18.1.0 as a dev dependency of your project? In that case it will resolve the peer dependency.

@dangreaves
Copy link

This stumped me for a while. I knew that I needed to override the peer dependency, so reached for the overrides feature, but it did not work. Maybe from a usability point of view, the overrides feature should also override peer dependencies, even though it can technically be solved by installing the dependency directly.

@stefanmaric
Copy link
Author

Why don't you just install react v18.1.0 as a dev dependency of your project? In that case it will resolve the peer dependency.

💯, it is indeed easy to fix it that way in this example. But the real-life issue I hit was unrelate to react but some @type/* definitions deep into a monorepo workspace.

So let's say I have my modules ./packages/a and ./packages/b. b depends on a. a depends on npm module external1 and b depends on npm module external2. external1 suggests @types/lib@2.0.0-beta as peer-dependency and external2 suggests @types/lib@1.0.0.

├── a
│   ├── external1
│   └── @types/lib -> 2.0.0-beta
└── b
    ├── a@workspace -> ../../a
    ├── external2
    └── @types/lib -> 1.0.0

Due the way TS compilation works with global namespaces and what not, importing stuff from a within b will pull the types of external1->@types/lib->2.0.0-beta, "breaking" package b even tho a is the one with the incorrect version.

I was able to create a small reproduction here: https://github.com/stefanmaric/pnpm-overrides

This is similarly the issue I'm presenting in the project that spawned this issue:

Screenshot 2023-07-04 at 15 53 50

"Fixes" itself if I don't import from a:

Screenshot 2023-07-04 at 15 54 12

Of course in this case it is easy to simply install all the peer-dependencies across all packages, but at work I have a monorepo with 20+ packages and it was almost impossible for me to find the offending packages; I actually had to spend about 2h today fiddling around and inspecting the node_modules/ tree to try the explicit declaration of peerDependencies workaround., this is on top of the knowledge I had already from working around it with hooks.

Agree this is an weird edge-case due to TS's weirdness, but nonetheless, I would expect pnpm.overrides to work regardless on how the dependency was installed, explicitly or implicitly. 🤔

@zkochan
Copy link
Member

zkochan commented Jul 4, 2023

ok, I have no objections

Of course in this case it is easy to simply install all the peer-dependencies across all packages, but at work I have a monorepo with 20+ packages and it was almost impossible for me to find the offending packages;

You can just install it in the workspace root.

@dangreaves
Copy link

I think it's worth mentioning on the usability side of things, that with implicit peer dependency installations, most users won't actually be able to tell if it's a transitive dependency, or a peer dependency which is causing the problems.

In my case, I had a package which depended on an old version of node-sass which was not compatible with Node 18. The issue presented itself as a postinstall error, where node-sass was falling over trying to compile something. I could clearly see from the error message which version of node-sass was causing the failure, and identified that it was an older version which needed upgrading.

However, it was very difficult to workout which package was actually including node-sass and even more difficult to identify that it was not a dependency, rather a peer dependency.

Ultimately, I knew I needed to override the version, but thought for many hours that it was an ordinary transitive dependency, in which case pnpm.overrides would have worked.

@stefanmaric
Copy link
Author

You can just install it in the workspace root.

They were actually installed in the workspace root to begging with, pnpm.overrides were set with the $ syntax to always use the root version, but the versions deeper in the tree take priority.

@stefanmaric
Copy link
Author

They were actually installed in the workspace root to begging with, pnpm.overrides were set with the $ syntax to always use the root version, but the versions deeper in the tree take priority.

To clarify, this issue popped up while upgrading pnpm from v7 to v8.

The peerDependencies situation in this project has been messed up for a long time but it manifested itself with v8 because of of the automatic installation of peerDependencies, precisely because of nodejs' module resolution:

In v7 the nested module ./packages/gui/component1 didn't have any @types/react installed so TypeScript picked up the one at the workspace root, which was the desired version, with v8, pnpm installed in component1/node_modules/ the version recommended by some @apollo/* packages making JSX types incompatible between workspace modules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

3 participants