Skip to content

Instantly share code, notes, and snippets.

@dvins
Last active November 16, 2024 12:07
Show Gist options
  • Save dvins/33b8fb52480149d37cdeb98890244c5b to your computer and use it in GitHub Desktop.
Save dvins/33b8fb52480149d37cdeb98890244c5b to your computer and use it in GitHub Desktop.
Overriding A Peer Dependency With PNPM

Overriding A Peer Dependency With PNPM

This guide explains how to override peer dependencies in a PNPM monorepo by using a custom hook. It provides a step-by-step solution to ensure consistent versioning across packages that rely on different versions of the same dependency.

Background

When working with a monorepo a challenges arises if you need to use multiple versions of the same package.

Nominally, this can be solved through package aliases and overides. However, a particularly sticky situation is when downstream packages rely peer dependencies of uptstream packages you need two or more versions of.

PNPM overrides support scoping, to explicitly target a package dependecy, e.g., "a@1>b": "1" and this works well for a pure dependency entry. But it does not work for peer dependencies, at all.

Hours of digging will lead you to multiple tickets such as pnpm/pnpm#5391 and pnpm/pnpm#4214, where you'll discover this is presently by design. The recommendation being to use a PNPM Hook, but easier said than done.

Solution

The following approach implements such a PNPM hook for overriding peer dependencies.

It uses a manifest that defines the package and version to look for and when found, the peer dependency to override.

Most importantly, it create a corresponding dependency and deletes the peer dependency. By doing this, we can then use a PNPM override to explitly scope and override the dependency, and in way that ensures for consistent versioning through aliases.

PNPM Peer Dependency Override Hook

Use this hook, or your variation of it, in your .pnpmfile.cjs next to your root-level package.json.

//@ts-check

// https://pnpm.io/pnpmfile
// https://github.com/pnpm/pnpm/issues/4214
// https://github.com/pnpm/pnpm/issues/5391

const rootPkg = require('./package.json');

console.log (`Checking for package peerDependency overrides`);

const remapPeerDependencies = [
  { package: 'react-dom',               packageVersion: '17.', peerDependency: 'react',     newVersion: '17.0' },
  { package: 'react-dom',               packageVersion: '18.', peerDependency: 'react',     newVersion: '18.0' },
  { package: '@tanstack/react-query',   packageVersion: '4.',  peerDependency: 'react',     newVersion: '17.0' },
  { package: '@tanstack/react-query',   packageVersion: '4.',  peerDependency: 'react-dom', newVersion: '17.0' },
  { package: '@tanstack/react-query',   packageVersion: '5.',  peerDependency: 'react',     newVersion: '18.0' },
  { package: '@react-email/components', packageVersion: '0.',  peerDependency: 'react',     newVersion: '18.0' },
];

function overridesPeerDependencies(pkg) {
  if (pkg.peerDependencies) {
    remapPeerDependencies.map(dep => {
      if (pkg.name === dep.package && pkg.version.startsWith(dep.packageVersion)) {
        console.log(`  - Checking ${pkg.name}@${pkg.version}`); // , pkg.peerDependencies);

        if (dep.peerDependency in pkg.peerDependencies) {
          try {
            console.log(`    - Overriding ${pkg.name}@${pkg.version} peerDependency ${dep.peerDependency}@${pkg.peerDependencies[dep.peerDependency]}`);

            // First add a new dependency to the package and then remove the peer dependency.
            // This approach has the added advantage that scoped overrides should now work, too.
            pkg.dependencies[dep.peerDependency] = dep.newVersion;
            delete pkg.peerDependencies[dep.peerDependency];

            console.log(`      - Overrode ${pkg.name}@${pkg.version} peerDependency ${dep.peerDependency}@${pkg.dependencies[dep.peerDependency]}`);
          } catch (err) {
            console.error(err);
          }
        }
      }
    });
  }
}

module.exports = {
  hooks: {
    readPackage(pkg, _context) {
      // skipDeps(pkg);
      overridesPeerDependencies(pkg);
      return pkg;
    },
  },
};

How To Use In Your Root Package

Once you implement the hook with your targeted override definitions you'll still need to bring it all together in your root-level package.json.

In this example, we're using two different versions of @tanstack/react-query. Each of which in turn, has peer dependencies on different versions of React and the React-Dom.

#package.json

{
  "name": "root",
  "description": "How to override peer dependencies from a root-level package.json",
  "author": "[email protected]",
    "repository": {
    "type": "git",
    "url": "https://gist.github.com/dvins/"
  },
  "engines": {
    "node": ">=20",
    "pnpm": ">=8.7"
  },
  "dependencies": {
    "@tanstack/react-query4": "npm:@tanstack/react-query@^4",
    "@tanstack/react-query5": "npm:@tanstack/react-query@^5.51",
    "react-dom17": "npm:react-dom@^17.0.2",
    "react-dom18": "npm:react-dom@^18.3",
    "react17": "npm:react@^17.0.2",
    "react18": "npm:react@^18.3"
  },
  "devDependencies": {
    "@types/react18": "npm:@types/react@^18.3"
  },
  "pnpm": {
    "overrides": {
      "@tanstack/react-query@4>react": "$react17",
      "@tanstack/react-query@4>react-dom": "$react-dom17",
      "@tanstack/react-query@5>react": "$react18",
      "react-dom@17>react": "$react17",
      "react-dom@18>react": "$react18"
    }
  }
}

While we could get away with only using the hook to override the version, the problem is keeping them in sync across lots of package.

That is why we explicitly override through scoping the dependency we created from the peer dependency, and use an alias $alias to the versioned package in our root-level dependencies list.

The other reason for doing this is often times there are other version constraints and rules specified in the "offending" package, so we cannot specify an alias as the override version in the hook and must use an explicity version from the allowed range.

This second ovvrride allows us to be as generic as possible in the hook, and then be consistently explicit across all our uses in the root-level package.

Checking Your Overrides

The best way to tell everything is working (aside from building and running your unit tests) is to check the resolutions after running your pnpm i.

First, you should no longer have any missing peer dependency warnings for your overridden packages.

Subsequently, check how they've been resolved by using the pnpm why {package} command.

For instance, in our example above, we can check how our overridden react peer dependencies were resolved:

> pnpm why react
Checking for package peerDependency overrides
Legend: production dependency, optional only, dev only

dependencies:
@tanstack/react-query 4.36.1
├── react 17.0.2
├─┬ react-dom 17.0.2
│ └── react 17.0.2
└─┬ use-sync-external-store 1.2.2
  └── react 17.0.2 peer
@tanstack/react-query 5.51.18
└── react 18.3.1
react 17.0.2
react 18.3.1
react-dom 17.0.2
└── react 17.0.2
react-dom 18.3.1
└── react 18.3.1

Notice too how the upstream dependencies of other dependencies in the "offending" package have also been appropriately resolved, too.

After we overrode the react peer dependency in tanstack/[email protected] to [email protected], PNPM made our life easier. It automatically resolved the react peer dependency of another dependencies in the package, [email protected], to the version we assigned, [email protected].


Thanks to @tjx666 and pnpm/pnpm#4214 (comment), for getting us started.

@sagish
Copy link

sagish commented Oct 10, 2024

Thanks for this gist!

By removing peerDependencies I was able to solve the issue of conflicting React versions in a workspace mono-repo

const rootPkg = require("./package.json");
const { overrides } = rootPkg.pnpm;

const transformPackages = ["formik"];

module.exports = {
  hooks: {
    readPackage(pkg, _context) {
      if (transformPackages.includes(pkg.name)) {
        delete pkg.peerDependencies;
      }
      return pkg;
    },
  },
};

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